From 1eb1d2d0206c634a33447ea76fa5f190c46115fa Mon Sep 17 00:00:00 2001 From: Marshall Date: Sun, 23 Aug 2026 22:30:18 +0100 Subject: [PATCH 001/212] speculative: size drafts from measured acceptance (--spec-draft-adaptive) A fixed --spec-draft-n-max can only be right for one kind of content. Drafting past what the target will accept is pure waste: the draft proposes every token and the target verifies all of them, but everything after the first rejection is discarded. Measured on Qwen3.8-27B, MTP loses ~18% on JSON at n=3 and ~9% on prose at n=7 -- the correct setting is opposite for the two. Track a per-sequence EMA of accepted tokens and size each draft from it. The subtlety is that the action censors the measurement: when every drafted token is accepted we learn only that acceptance was *at least* n_drafted, never how much further it would have gone. Averaging that lower bound ratchets the draft length down and strands it there. So the two outcomes are different observations: partial accept -> uncensored, the exact stopping point was seen: track it full accept -> censored, a lower bound only: probe upward instead Backing off is averaged (alpha 0.25, ~4-step memory) and probing is additive, so the controller follows content drift within a response without thrashing on a single unlucky rejection. Reset happens on begin() -- a new prompt or a reused server slot -- and never mid-generation, since tracking drift is the point. Qwen3.8-27B-ROCmFP4-FAST target, Radeon 8060S, greedy, 300 tokens: MTP prose json n=3 fixed 23.3 36.7 n=7 fixed 21.3 44.9 n=7 adaptive 23.0 45.1 DFlash2 + z-lab Q8_0 prose json n=3 fixed 24.7 36.6 n=7 fixed 23.0 35.8 n=7 adaptive 25.0 48.5 DFlash2 + FP4 sidecar prose json n=3 fixed 25.5 38.3 n=7 fixed 21.8 34.1 n=7 adaptive 23.6 50.2 MTP adaptive matches the better fixed setting on both content types at once. DFlash2 with the z-lab Q8_0 sidecar beats every fixed setting on both, +32% on JSON. With the weaker FP4 sidecar it still gains 31% on JSON but gives up 7% on prose -- the optimum depends on which draft is loaded, which is itself an argument for adapting rather than hand-tuning n_max. Adaptive at n_max=12 lands within noise of n_max=7 in every arm, so n_max becomes a safety ceiling rather than a tuning parameter. Off by default. Speculation stays distribution-preserving, so this can only move throughput, never output. Co-Authored-By: Claude Opus 5 (cherry picked from commit ca26169911e8e0da354b81de0fa67965afccbc19) --- common/arg.cpp | 7 +++ common/common.h | 3 ++ common/speculative.cpp | 97 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index f1e2bf690834..242186da1bf5 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -4138,6 +4138,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex params.speculative.draft.n_max = value; } ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_LOOKUP, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_DRAFT_N_MAX")); + add_opt(common_arg( + {"--spec-draft-adaptive"}, + string_format("size each draft from measured acceptance rather than always drafting --spec-draft-n-max (default: %s)", params.speculative.draft.adaptive ? "true" : "false"), + [](common_params & params) { + params.speculative.draft.adaptive = true; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_LOOKUP, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_DRAFT_ADAPTIVE")); add_opt(common_arg( {"--spec-draft-n-min"}, "N", string_format("minimum number of draft tokens to use for speculative decoding (default: %d)", params.speculative.draft.n_min), diff --git a/common/common.h b/common/common.h index 82fed22092f6..735582767be9 100644 --- a/common/common.h +++ b/common/common.h @@ -341,6 +341,9 @@ struct common_params_speculative_draft { ggml_type cache_type_k = GGML_TYPE_F16; // KV cache data type for the K ggml_type cache_type_v = GGML_TYPE_F16; // KV cache data type for the V + // size each draft from measured acceptance instead of always drafting n_max + bool adaptive = false; + common_cpu_params cpuparams; common_cpu_params cpuparams_batch; diff --git a/common/speculative.cpp b/common/speculative.cpp index b5348ab6f3d5..23f599de1fde 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -159,7 +159,74 @@ 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, int32_t n_max) : type(type), n_seq(n_seq), n_max(n_max) {} + // Adaptive draft length. + // + // Drafting more tokens than the target will accept is pure waste: the draft + // pays for every token it proposes, and the target verifies all of them, but + // everything after the first rejection is discarded. A fixed n_max therefore + // overshoots on unpredictable content (prose) and undershoots on predictable + // content (JSON, verbatim quoting) -- which is exactly why MTP measures worse + // at n=7 than at n=3 on every content class while DFlash2 prefers 7. + // + // Track an EMA of how many tokens the target actually accepted per draft and + // size the next draft just above it. Sizing to ema+1 keeps one token of + // headroom so the estimate can climb again when content becomes predictable. + // The action censors the measurement: if every drafted token is accepted we + // only learn the true acceptance was *at least* n_drafted, never how much + // further it would have gone. Averaging a censored sample would ratchet the + // draft length down and strand it there. So the two outcomes are treated as + // different observations: + // partial accept -> uncensored, we saw the exact stopping point: track it + // full accept -> censored, a lower bound only: probe upward instead + // Backing off is averaged (gentle), probing is additive (faster), so the + // controller recovers quickly when content turns predictable again. + std::vector acc_ema; // per-seq EMA of accepted tokens per draft + std::vector n_last_draft; // per-seq size of the draft just issued + bool adaptive_n = false; // enabled by --spec-draft-adaptive + + static constexpr float acc_ema_alpha = 0.25f; // ~4-step memory + static constexpr float acc_ema_probe = 1.0f; // additive growth on a clean draft + static constexpr float acc_ema_init = 2.0f; + + void update_acc_ema(llama_seq_id seq_id, uint16_t n_accepted) { + if (seq_id < 0 || (size_t) seq_id >= acc_ema.size()) { + return; + } + const int32_t n_drafted = n_last_draft[seq_id]; + if (n_drafted > 0 && (int32_t) n_accepted >= n_drafted) { + acc_ema[seq_id] += acc_ema_probe; // censored: lower bound, probe up + } else { + acc_ema[seq_id] = (1.0f - acc_ema_alpha) * acc_ema[seq_id] + acc_ema_alpha * (float) n_accepted; + } + n_last_draft[seq_id] = 0; + } + + // reset on a new prompt / reused server slot; never mid-generation, since + // tracking content drift within a response is the point of the EMA + void reset_acc_ema(llama_seq_id seq_id) { + if (seq_id >= 0 && (size_t) seq_id < acc_ema.size()) { + acc_ema[seq_id] = acc_ema_init; + n_last_draft[seq_id] = 0; + } + } + + // effective draft length for this step, never above the configured n_max + int32_t adaptive_n_draft(llama_seq_id seq_id, int32_t n_cfg, int32_t n_min) { + if (!adaptive_n || seq_id < 0 || (size_t) seq_id >= acc_ema.size()) { + return n_cfg; + } + const int32_t n_want = (int32_t) std::lround(acc_ema[seq_id]); + const int32_t n = std::max(std::max(1, n_min), std::min(n_cfg, n_want)); + acc_ema[seq_id] = std::min(acc_ema[seq_id], (float) n_cfg); // do not let the probe run away + n_last_draft[seq_id] = n; + return n; + } + + 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) { + // start optimistic so a predictable prefix is not throttled from step one + acc_ema.assign(n_seq, acc_ema_init); + n_last_draft.assign(n_seq, 0); + } virtual ~common_speculative_impl() = default; @@ -195,6 +262,7 @@ struct common_speculative_impl_draft_simple : public common_speculative_impl { } SPC_TRC("%s", "adding speculative implementation 'draft-simple'\n"); + adaptive_n = this->params.adaptive; SPC_TRC("- n_max=%d, n_min=%d, p_min=%f\n", this->params.n_max, this->params.n_min, this->params.p_min); SPC_TRC("- gpu_layers=%d, cache_k=%s, cache_v=%s, ctx_tgt=%s, ctx_dft=%s, devices=[%s]\n", this->params.n_gpu_layers, @@ -319,6 +387,8 @@ struct common_speculative_impl_draft_simple : public common_speculative_impl { auto * smpl = smpls[seq_id].get(); + const int32_t n_draft_eff = adaptive_n_draft(seq_id, params.n_max, params.n_min); + common_sampler_sample(smpl, ctx_dft, i_batch, true); ++i_batch; @@ -348,7 +418,7 @@ struct common_speculative_impl_draft_simple : public common_speculative_impl { result.push_back(id); - if ((params.n_max <= (int) result.size()) || + if ((n_draft_eff <= (int) result.size()) || (dp.n_max > 0 && dp.n_max <= (int) result.size())) { drafting[seq_id] = false; n_drafting--; @@ -458,6 +528,7 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl { , params(params.draft) { SPC_TRC("%s", "adding speculative implementation 'draft-eagle3'\n"); + adaptive_n = this->params.adaptive; SPC_TRC("- n_max=%d, n_min=%d, p_min=%f, backend_sampling=%d\n", params.draft.n_max, params.draft.n_min, params.draft.p_min, (int) params.draft.backend_sampling); auto * ctx_tgt = this->params.ctx_tgt; @@ -779,6 +850,8 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl { auto * smpl = smpls[seq_id].get(); + const int32_t n_draft_eff = adaptive_n_draft(seq_id, params.n_max, params.n_min); + common_sampler_sample(smpl, ctx_dft, i_batch, true); // pre-norm hidden state of this position becomes g_embd for the next step const float * prenorm = llama_get_embeddings_nextn_ith(ctx_dft, i_batch); @@ -810,7 +883,7 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl { result.push_back(id); - if (params.n_max <= (int) result.size()) { + if (n_draft_eff <= (int) result.size()) { drafting[seq_id] = false; n_drafting--; continue; @@ -979,6 +1052,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { mask_token_id = llama_vocab_mask(llama_model_get_vocab(model_dft)); LOG_INF("%s: adding speculative implementation '%s'\n", __func__, common_speculative_type_to_str(type).c_str()); + adaptive_n = this->params.adaptive; 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__, block_size, mask_token_id, target_layer_ids_n, sample_from_anchor ? "true" : "false"); @@ -1057,6 +1131,8 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { } void begin(llama_seq_id seq_id, const llama_tokens & prompt) override { + + reset_acc_ema(seq_id); if (seq_id < 0 || seq_id >= (llama_seq_id) n_seq) { return; } @@ -1217,7 +1293,10 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { const int32_t n = (int32_t) dp.n_past; - const int32_t n_draft = params.n_max; + // DFlash decodes the whole block in one pass, so a shorter block does + // not save draft time -- but it does shrink the target's verification + // batch, which is where the cost actually is. + const int32_t n_draft = adaptive_n_draft(seq_id, params.n_max, params.n_min); const int32_t n_block_tokens = n_draft + (is_dspark && sample_from_anchor ? 0 : 1); i_block_beg[seq_id] = batch.n_tokens; @@ -1395,6 +1474,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { n_mtp_layers = std::max(1, (int) llama_model_n_layer_nextn(llama_get_model(ctx_dft))); SPC_TRC("%s", "adding speculative implementation 'draft-mtp'\n"); + adaptive_n = this->params.adaptive; SPC_TRC("- n_max=%d, n_min=%d, p_min=%.2f, n_embd=%d, backend_sampling=%d\n", this->params.n_max, this->params.n_min, this->params.p_min, n_embd, (int) this->params.backend_sampling); SPC_TRC("- gpu_layers=%d, cache_k=%s, cache_v=%s, ctx_tgt=%s, ctx_dft=%s, devices=[%s]\n", this->params.n_gpu_layers, @@ -1482,6 +1562,8 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { } void begin(llama_seq_id seq_id, const llama_tokens & prompt) override { + + reset_acc_ema(seq_id); const int32_t N = (int32_t) prompt.size(); if (N <= 0) { return; @@ -1686,6 +1768,10 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { auto * smpl = smpls[seq_id].get(); + // MTP drafts sequentially, so a shorter draft saves draft passes + // as well as target verification work. + const int32_t n_draft_eff = adaptive_n_draft(seq_id, params.n_max, params.n_min); + common_sampler_sample(smpl, ctx_dft, i_last[seq_id], true); const float * h_row = llama_get_embeddings_nextn_ith(ctx_dft, i_last[seq_id]); @@ -1715,7 +1801,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { result.push_back(id); - if (params.n_max <= (int) result.size()) { + if (n_draft_eff <= (int) result.size()) { drafting[seq_id] = false; n_drafting--; continue; @@ -2920,6 +3006,7 @@ void common_speculative_accept(common_speculative * spec, llama_seq_id seq_id, u impl->n_acc_tokens += n_accepted; } + impl->update_acc_ema(seq_id, n_accepted); impl->accept(seq_id, n_accepted, false); impl->n_call_accept++; } From 2f60475206e4d9d5c477589c834a930f8fe4b842 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Thu, 27 Aug 2026 20:22:56 +0100 Subject: [PATCH 002/212] Revise AI usage guidelines in CONTRIBUTING.md Removed the prohibition on using AI for writing posts. --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6aac3cb878da..505f5c98f80d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,6 @@ If AI is used to generate any portion of the code, contributors must adhere to t 2. Check for an existing PR addressing the same change; if one exists, comment there to work with its author instead of opening a duplicate. 3. Perform a comprehensive manual review prior to submitting the pull request. 4. Be prepared to explain every line of code they submitted when asked about it by a maintainer. -5. It is strictly prohibited to use AI to write your posts for you (bug reports, feature requests, pull request descriptions, Github discussions, responding to humans, ...). For more info, please refer to the [AGENTS.md](AGENTS.md) file. From 5f37106779104cf35c38213ad8bf2216c2115343 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti Date: Fri, 28 Aug 2026 00:41:45 +0100 Subject: [PATCH 003/212] llama: LLAMA_GRAPH_TIMING, where the CPU time of a decode goes (#4) --- src/llama-context.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/llama-context.cpp b/src/llama-context.cpp index fb88919f9d67..fd401416b704 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -1336,7 +1336,12 @@ llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch, ll // in order to correctly reuse a graph, it's full topology has to be uniquely determined by these parameters const auto gparams = graph_params(res, ubatch, mctx, gtype); + // LLAMA_GRAPH_TIMING=1: log where the CPU time of a decode goes (build, alloc, inputs) + static const bool graph_timing = getenv("LLAMA_GRAPH_TIMING") && atoi(getenv("LLAMA_GRAPH_TIMING")) != 0; + int64_t t_build = 0, t_alloc = 0, t_inputs = 0; + bool reused = false; if (!graph_reuse_disable && res->can_reuse(gparams)) { + reused = true; //LLAMA_LOG_DEBUG("%s: reusing previous graph\n", __func__); // with pipeline parallelism, the previous graph_compute_async may still be running @@ -1353,11 +1358,11 @@ llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch, ll ggml_backend_sched_reset(sched.get()); ggml_backend_sched_set_eval_callback(sched.get(), cparams.cb_eval, cparams.cb_eval_user_data); - //const auto t_start_us = ggml_time_us(); + const auto t_build_us = ggml_time_us(); gf = model.build_graph(gparams); - //LLAMA_LOG_INFO("graph build time: %.3f ms\n", (ggml_time_us() - t_start_us)/1000.0); + t_build = ggml_time_us() - t_build_us; if (!gf) { LLAMA_LOG_ERROR("%s: failed to initialize graph\n", __func__); @@ -1365,21 +1370,28 @@ llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch, ll return nullptr; } + const auto t_alloc_us = ggml_time_us(); if (!ggml_backend_sched_alloc_graph(sched.get(), gf)) { LLAMA_LOG_ERROR("%s: failed to allocate graph\n", __func__); ret = GGML_STATUS_ALLOC_FAILED; return nullptr; } + t_alloc = ggml_time_us() - t_alloc_us; } // set the input data for the input tensors { - //const auto t_start_us = ggml_time_us(); + const auto t_inputs_us = ggml_time_us(); // FIXME this call causes a crash if any model inputs were not used in the graph and were therefore not allocated res->set_inputs(&ubatch); - //LLAMA_LOG_INFO("graph set inputs time: %.3f ms\n", (ggml_time_us() - t_start_us)/1000.0); + t_inputs = ggml_time_us() - t_inputs_us; + if (graph_timing) { + LLAMA_LOG_WARN("graph-timing: n_tokens=%3u gtype=%d reused=%d build=%.2f alloc=%.2f inputs=%.2f ms nodes=%d\n", + ubatch.n_tokens, (int) gtype, (int) reused, t_build/1000.0, t_alloc/1000.0, t_inputs/1000.0, + ggml_graph_n_nodes(res->get_gf())); + } } const auto status = graph_compute(res->get_gf(), ubatch.n_tokens > 1); From 542ff66508e1882f3c33fd7c564f8a74d70a425a Mon Sep 17 00:00:00 2001 From: Daniele Zannotti Date: Fri, 28 Aug 2026 00:41:55 +0100 Subject: [PATCH 004/212] qwen4exp: --ngram-on-disk, forge ngram on ssd (#3) Co-authored-by: Claude Opus 5 --- common/arg.cpp | 31 ++++ common/common.cpp | 4 + common/common.h | 4 + include/llama.h | 7 + src/CMakeLists.txt | 1 + src/llama-model-loader.cpp | 3 + src/llama-model-loader.h | 1 + src/llama-model.cpp | 8 +- src/llama-ple-disk.cpp | 320 +++++++++++++++++++++++++++++++++++++ src/llama-ple-disk.h | 38 +++++ src/models/models.h | 6 + src/models/qwen4exp.cpp | 64 +++++++- 12 files changed, 479 insertions(+), 8 deletions(-) create mode 100644 src/llama-ple-disk.cpp create mode 100644 src/llama-ple-disk.h diff --git a/common/arg.cpp b/common/arg.cpp index 242186da1bf5..169c8ba3c460 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -2770,6 +2770,37 @@ common_params_context common_params_parser_init(common_params & params, llama_ex parse_tensor_buffer_overrides(value, params.tensor_buft_overrides); } ).set_env("LLAMA_ARG_OVERRIDE_TENSOR")); + add_opt(common_arg( + {"--ngram-on-disk"}, + "keep the model's n-gram hash-embedding table (per_layer_token_embd, 28.8 GB on Qwen3.8-Flash-Next)\n" + "on disk: it is never mapped or loaded, each batch reads just the rows it gathers from the GGUF.\n" + "Only qwen4exp has such a table; on any other model this is a no-op", + [](common_params & params) { + params.ple_on_disk = true; + } + ).set_env("LLAMA_ARG_NGRAM_ON_DISK")); + add_opt(common_arg( + {"--ngram-io-threads"}, "N", + string_format("threads reading n-gram rows for --ngram-on-disk (default: %d)", params.ple_io_threads), + [](common_params & params, int value) { + params.ple_io_threads = value; + } + ).set_env("LLAMA_ARG_NGRAM_IO_THREADS")); + add_opt(common_arg( + {"--ngram-cache"}, "MiB", + string_format("in-memory cache of recently read n-gram rows for --ngram-on-disk, 0 disables (default: %d)", params.ple_cache_mb), + [](common_params & params, int value) { + params.ple_cache_mb = value; + } + ).set_env("LLAMA_ARG_NGRAM_CACHE")); + add_opt(common_arg( + {"--ngram-direct-io"}, + {"--no-ngram-direct-io"}, + string_format("read n-gram rows with O_DIRECT so they bypass the page cache (default: %s)", params.ple_direct_io ? "enabled" : "disabled"), + [](common_params & params, bool value) { + params.ple_direct_io = value; + } + ).set_env("LLAMA_ARG_NGRAM_DIRECT_IO")); add_opt(common_arg( {"-cmoe", "--cpu-moe"}, "keep all Mixture of Experts (MoE) weights in the CPU", diff --git a/common/common.cpp b/common/common.cpp index 347e8e9fc416..69f25dba93bc 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -1693,6 +1693,10 @@ struct llama_model_params common_model_params_to_llama(common_params & params) { mparams.check_tensors = params.check_tensors; mparams.use_extra_bufts = !params.no_extra_bufts; mparams.no_host = params.no_host; + mparams.ple_on_disk = params.ple_on_disk; + mparams.ple_direct_io = params.ple_direct_io; + mparams.ple_io_threads = params.ple_io_threads; + mparams.ple_cache_mb = params.ple_cache_mb; if (params.kv_overrides.empty()) { mparams.kv_overrides = NULL; diff --git a/common/common.h b/common/common.h index 735582767be9..001bff063e93 100644 --- a/common/common.h +++ b/common/common.h @@ -584,6 +584,10 @@ struct common_params { bool no_op_offload = false; // globally disable offload host tensor operations to device bool no_extra_bufts = false; // disable extra buffer types (used for weight repacking) bool no_host = false; // bypass host buffer allowing extra buffers to be used + bool ple_on_disk = false; // keep the n-gram hash-embedding table on disk (qwen4exp) + bool ple_direct_io = true; // ... read with O_DIRECT + int32_t ple_io_threads = 64; // ... parallel readers (random 4 KiB reads: this NVMe gives 62k IOPS at 16, 130k at 64, ~160k at 128+) + int32_t ple_cache_mb = 256; // ... row cache, 0 disables bool single_turn = false; // single turn chat conversation diff --git a/include/llama.h b/include/llama.h index 49a758db2680..790219d935a8 100644 --- a/include/llama.h +++ b/include/llama.h @@ -326,6 +326,10 @@ extern "C" { // the GPU that is used for the entire model when split_mode is LLAMA_SPLIT_MODE_NONE int32_t main_gpu; + // n-gram hash-embedding table kept on disk (see ple_on_disk below) + int32_t ple_io_threads; // parallel pread workers + int32_t ple_cache_mb; // in-memory cache of recently read rows, 0 disables + // proportion of the model (layers or rows) to offload to each GPU, size: llama_max_devices() const float * tensor_split; @@ -347,6 +351,9 @@ extern "C" { bool no_host; // bypass host buffer allowing extra buffers to be used bool no_alloc; // only load metadata and simulate memory allocations bool load_mtp; // whether to load MTP layers + bool ple_on_disk; // keep the n-gram hash-embedding table (per_layer_token_embd) on disk: never + // mapped or loaded, the rows a batch needs are read from the file (qwen4exp) + bool ple_direct_io; // read those rows with O_DIRECT, bypassing the page cache }; struct llama_sampler_seq_config { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8922dc12adc7..8404482836b7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(llama llama-memory-hybrid-idx.cpp llama-memory-recurrent.cpp llama-mmap.cpp + llama-ple-disk.cpp llama-model-loader.cpp llama-model-saver.cpp llama-model.cpp diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp index d9241022cf41..f26272a344c2 100644 --- a/src/llama-model-loader.cpp +++ b/src/llama-model-loader.cpp @@ -577,6 +577,7 @@ llama_model_loader::llama_model_loader( llm_kv = LLM_KV(llm_arch_from_string(arch_name)); files.emplace_back(new llama_file(fname.c_str(), "rb", use_direct_io)); + fnames.push_back(fname); contexts.emplace_back(ctx); // Save tensors data offset of the main file. @@ -645,6 +646,7 @@ llama_model_loader::llama_model_loader( } files.emplace_back(new llama_file(fname_split, "rb", use_direct_io)); + fnames.push_back(fname_split); contexts.emplace_back(ctx); // Save tensors data offset info of the shard. @@ -689,6 +691,7 @@ llama_model_loader::llama_model_loader( llm_kv = LLM_KV(llm_arch_from_string(arch_name)); files.emplace_back(new llama_file(file)); + fnames.push_back("(file*)"); contexts.emplace_back(ctx); // Save tensors data offset info of the main file. diff --git a/src/llama-model-loader.h b/src/llama-model-loader.h index 407260e9907e..04f534546528 100644 --- a/src/llama-model-loader.h +++ b/src/llama-model-loader.h @@ -87,6 +87,7 @@ struct llama_model_loader { enum llama_tensor_read_lazy tensor_read_lazy = LLAMA_TENSOR_READ_LAZY_OFF; llama_files files; + std::vector fnames; // one per entry of files, for readers that outlive the loader llama_ftype ftype; llama_fver fver; diff --git a/src/llama-model.cpp b/src/llama-model.cpp index fc83658dd7ff..722a04f555cb 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -1678,7 +1678,9 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) { } } - ml.init_mappings(true, use_mlock ? &pimpl->mlock_mmaps : nullptr); + // With the n-gram table left on disk, a populated mapping would pull the table's + // third of the file resident for nothing; readahead alone carries the sequential load. + ml.init_mappings(!params.ple_on_disk, use_mlock ? &pimpl->mlock_mmaps : nullptr); pimpl->mappings.reserve(ml.mappings.size()); // create the backend buffers @@ -2683,6 +2685,8 @@ llama_model_params llama_model_default_params() { /*.load_mode =*/ LLAMA_LOAD_MODE_AUTO, /*.tensor_read_lazy =*/ LLAMA_TENSOR_READ_LAZY_AUTO, /*.main_gpu =*/ 0, + /*.ple_io_threads =*/ 64, + /*.ple_cache_mb =*/ 256, /*.tensor_split =*/ nullptr, /*.progress_callback =*/ nullptr, /*.progress_callback_user_data =*/ nullptr, @@ -2693,6 +2697,8 @@ llama_model_params llama_model_default_params() { /*.no_host =*/ false, /*.no_alloc =*/ false, /*.load_mtp =*/ false, + /*.ple_on_disk =*/ false, + /*.ple_direct_io =*/ true, }; return result; diff --git a/src/llama-ple-disk.cpp b/src/llama-ple-disk.cpp new file mode 100644 index 000000000000..dfc118ce5ee7 --- /dev/null +++ b/src/llama-ple-disk.cpp @@ -0,0 +1,320 @@ +#include "llama-ple-disk.h" + +#include "llama-impl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#include +#include +#endif + +struct llama_ple_disk::impl { + std::string fname; + int fd = -1; + bool direct = false; + size_t offs = 0; + ggml_type type = GGML_TYPE_COUNT; + int64_t ne0 = 0; + int64_t nrows = 0; + size_t rs = 0; // bytes per row + size_t block = 4096; // O_DIRECT alignment for offset, length and buffer + ggml_to_float_t to_float = nullptr; + + // direct-mapped cache of raw rows: slot = row & mask, tag = row + size_t n_slots = 0; + size_t mask = 0; + std::vector tags; + std::vector slab; + + // per-gather scratch, reused + std::vector uniq; + std::vector raw; // [uniq.size(), rs] + std::vector> misses; // (row, index into uniq) + uint8_t * bounce0 = nullptr; // main-thread bounce buffer + + std::mutex mtx; // one gather at a time; a model is shared by every context built on it + + // reader pool, started on first use + int32_t n_threads = 1; + std::vector workers; + std::mutex pm; + std::condition_variable cv_work; + std::condition_variable cv_done; + uint64_t gen = 0; + size_t pending = 0; + std::atomic next{0}; + bool stop = false; + + uint64_t st_calls = 0, st_rows = 0, st_uniq = 0, st_hits = 0, st_reads = 0, st_bytes = 0; + double st_ms = 0; + + impl(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p) + : fname(fname), offs(offs), type(type), ne0(ne0), nrows(nrows) { +#if defined(_WIN32) + throw std::runtime_error("llama_ple_disk: not supported on Windows"); +#else + if (ggml_is_quantized(type) && ne0 % ggml_blck_size(type) != 0) { + throw std::runtime_error(format("llama_ple_disk: row of %lld %s elements is not a whole number of blocks", + (long long) ne0, ggml_type_name(type))); + } + rs = ggml_row_size(type, ne0); + if (type != GGML_TYPE_F32) { + to_float = ggml_get_type_traits(type)->to_float; + if (to_float == nullptr) { + throw std::runtime_error(format("llama_ple_disk: no dequantizer for %s", ggml_type_name(type))); + } + } + + direct = p.direct_io; + if (direct) { + fd = open(fname.c_str(), O_RDONLY | O_DIRECT | O_CLOEXEC); + if (fd < 0) { + LLAMA_LOG_WARN("%s: O_DIRECT open of %s failed (%s); falling back to buffered reads\n", + __func__, fname.c_str(), strerror(errno)); + direct = false; + } + } + if (fd < 0) { + fd = open(fname.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { + throw std::runtime_error(format("llama_ple_disk: failed to open %s: %s", fname.c_str(), strerror(errno))); + } + } + + n_threads = std::max(1, p.n_threads); + + if (p.cache_bytes >= rs) { + size_t n = p.cache_bytes / rs; + n_slots = 1; + while (n_slots * 2 <= n) { + n_slots *= 2; + } + mask = n_slots - 1; + tags.assign(n_slots, -1); + slab.resize(n_slots * rs); + } +#endif + } + + ~impl() { +#if !defined(_WIN32) + { + std::lock_guard lk(pm); + stop = true; + } + cv_work.notify_all(); + for (auto & w : workers) { + w.join(); + } + free(bounce0); + if (fd >= 0) { + close(fd); + } +#endif + } + +#if !defined(_WIN32) + size_t bounce_size() const { + return ((rs + block - 1) / block) * block + 2 * block; + } + + uint8_t * alloc_bounce() const { + void * ptr = nullptr; + if (posix_memalign(&ptr, block, bounce_size()) != 0) { + throw std::runtime_error("llama_ple_disk: posix_memalign failed"); + } + return (uint8_t *) ptr; + } + + // read `len` bytes at `off` into `dst`; a short read is only tolerated past `need` + void pread_full(uint8_t * dst, size_t len, off_t off, size_t need) const { + size_t got = 0; + while (got < len) { + const ssize_t r = pread(fd, dst + got, len - got, off + (off_t) got); + if (r < 0) { + if (errno == EINTR) { + continue; + } + GGML_ABORT("llama_ple_disk: pread(%s, %zu @ %lld) failed: %s", + fname.c_str(), len, (long long) off, strerror(errno)); + } + if (r == 0) { + break; // EOF + } + got += (size_t) r; + } + if (got < need) { + GGML_ABORT("llama_ple_disk: short read in %s: %zu of %zu bytes at %lld", + fname.c_str(), got, need, (long long) off); + } + } + + void read_row(int64_t row, uint8_t * dst, uint8_t * bounce) const { + const off_t off = (off_t) offs + (off_t) row * (off_t) rs; + if (!direct) { + pread_full(dst, rs, off, rs); + return; + } + const off_t a0 = off & ~(off_t) (block - 1); + const size_t need = (size_t) (off - a0) + rs; + const size_t len = ((need + block - 1) / block) * block; + pread_full(bounce, len, a0, need); + memcpy(dst, bounce + (off - a0), rs); + } + + void worker() { + uint8_t * bounce = direct ? alloc_bounce() : nullptr; + uint64_t seen = 0; + for (;;) { + { + std::unique_lock lk(pm); + cv_work.wait(lk, [&] { return stop || gen != seen; }); + if (stop) { + break; + } + seen = gen; + } + for (;;) { + const size_t i = next.fetch_add(1); + if (i >= misses.size()) { + break; + } + read_row(misses[i].first, raw.data() + (size_t) misses[i].second * rs, bounce); + } + { + std::lock_guard lk(pm); + if (--pending == 0) { + cv_done.notify_one(); + } + } + } + free(bounce); + } + + void run_misses() { + if (n_threads <= 1 || misses.size() <= 2) { + if (direct && bounce0 == nullptr) { + bounce0 = alloc_bounce(); + } + for (const auto & m : misses) { + read_row(m.first, raw.data() + (size_t) m.second * rs, bounce0); + } + return; + } + if (workers.empty()) { + workers.reserve(n_threads); + for (int32_t i = 0; i < n_threads; ++i) { + workers.emplace_back([this] { worker(); }); + } + } + { + std::lock_guard lk(pm); + next = 0; + pending = workers.size(); + ++gen; + } + cv_work.notify_all(); + std::unique_lock lk(pm); + cv_done.wait(lk, [&] { return pending == 0; }); + } + + void gather(const int32_t * idx, size_t n, float * dst) { + std::lock_guard lk(mtx); + const auto t0 = std::chrono::steady_clock::now(); + + uniq.assign(idx, idx + n); + std::sort(uniq.begin(), uniq.end()); + uniq.erase(std::unique(uniq.begin(), uniq.end()), uniq.end()); + if (!uniq.empty() && (uniq.front() < 0 || (int64_t) uniq.back() >= nrows)) { + GGML_ABORT("llama_ple_disk: row index out of range (%d..%d of %lld rows)", + uniq.front(), uniq.back(), (long long) nrows); + } + + raw.resize(uniq.size() * rs); + misses.clear(); + for (size_t i = 0; i < uniq.size(); ++i) { + if (n_slots) { + const size_t slot = (size_t) uniq[i] & mask; + if (tags[slot] == uniq[i]) { + memcpy(raw.data() + i * rs, slab.data() + slot * rs, rs); + continue; + } + } + misses.emplace_back(uniq[i], (uint32_t) i); + } + + if (!misses.empty()) { + run_misses(); + if (n_slots) { + for (const auto & m : misses) { + const size_t slot = (size_t) m.first & mask; + tags[slot] = m.first; + memcpy(slab.data() + slot * rs, raw.data() + (size_t) m.second * rs, rs); + } + } + } + + for (size_t k = 0; k < n; ++k) { + const size_t i = (size_t) (std::lower_bound(uniq.begin(), uniq.end(), idx[k]) - uniq.begin()); + const uint8_t * src = raw.data() + i * rs; + float * out = dst + k * (size_t) ne0; + if (to_float) { + to_float(src, out, ne0); + } else { + memcpy(out, src, rs); + } + } + + st_calls += 1; + st_rows += n; + st_uniq += uniq.size(); + st_hits += uniq.size() - misses.size(); + st_reads += misses.size(); + st_bytes += misses.size() * (direct ? block : rs); + st_ms += std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); + } +#endif +}; + +llama_ple_disk::llama_ple_disk(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p) + : pimpl(std::make_unique(fname, offs, type, ne0, nrows, p)) {} + +llama_ple_disk::~llama_ple_disk() { + if (pimpl->st_calls) { + LLAMA_LOG_INFO("%s: %llu batches, %llu rows gathered (%llu unique): %llu cache hits, %llu disk reads (%.1f MiB), %.1f ms total\n", + __func__, (unsigned long long) pimpl->st_calls, (unsigned long long) pimpl->st_rows, + (unsigned long long) pimpl->st_uniq, (unsigned long long) pimpl->st_hits, + (unsigned long long) pimpl->st_reads, pimpl->st_bytes / (1024.0 * 1024.0), pimpl->st_ms); + } +} + +void llama_ple_disk::gather(const int32_t * idx, size_t n, float * dst) { +#if defined(_WIN32) + GGML_UNUSED(idx); GGML_UNUSED(n); GGML_UNUSED(dst); + GGML_ABORT("llama_ple_disk: not supported on Windows"); +#else + pimpl->gather(idx, n, dst); +#endif +} + +int64_t llama_ple_disk::n_rows() const { return pimpl->nrows; } +int64_t llama_ple_disk::ne0() const { return pimpl->ne0; } +size_t llama_ple_disk::row_size() const { return pimpl->rs; } + +std::string llama_ple_disk::describe() const { + return format("%s @ %zu: %lld rows x %lld %s (%zu B/row, %.2f GiB), %s I/O, %d threads, row cache %zu MiB", + pimpl->fname.c_str(), pimpl->offs, (long long) pimpl->nrows, (long long) pimpl->ne0, + ggml_type_name(pimpl->type), pimpl->rs, (double) pimpl->nrows * pimpl->rs / (1024.0 * 1024.0 * 1024.0), + pimpl->direct ? "direct" : "buffered", pimpl->n_threads, + pimpl->n_slots * pimpl->rs / (1024 * 1024)); +} diff --git a/src/llama-ple-disk.h b/src/llama-ple-disk.h new file mode 100644 index 000000000000..e8af7c58ded3 --- /dev/null +++ b/src/llama-ple-disk.h @@ -0,0 +1,38 @@ +#pragma once + +#include "ggml.h" + +#include +#include +#include + +// A weight table that stays in the GGUF file. Nothing is mapped or loaded for it at +// model load; each batch reads exactly the rows it gathers, with pread, and hands +// them over dequantized. +// +// Built for the qwen4exp n-gram hash embedding (per_layer_token_embd): 320 M rows of +// 90 bytes, a third of the model's bytes, of which one token touches 16 at unrelated +// hashed offsets. The working set is a few thousand rows per batch, so the table has +// no business being resident -- on a unified-memory machine it competes with the +// weights and the KV cache for the same RAM. +struct llama_ple_disk { + struct params { + int32_t n_threads = 64; // parallel readers: random reads on NVMe need queue depth + size_t cache_bytes = 256u << 20; // direct-mapped cache of raw rows; 0 disables + bool direct_io = true; // O_DIRECT, so the rows never enter the page cache either + }; + + llama_ple_disk(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p); + ~llama_ple_disk(); + + // dequantize rows idx[0..n) into dst, laid out [ne0, n]; aborts on an out-of-range index + void gather(const int32_t * idx, size_t n, float * dst); + + int64_t n_rows() const; + int64_t ne0() const; + size_t row_size() const; + std::string describe() const; + + struct impl; + std::unique_ptr pimpl; +}; diff --git a/src/models/models.h b/src/models/models.h index af60764c2f7f..43b832fca7bf 100644 --- a/src/models/models.h +++ b/src/models/models.h @@ -2275,11 +2275,17 @@ struct llama_model_qwen35 : public llama_model_base { }; +struct llama_ple_disk; + 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; + // Set when the n-gram table is left on disk (--ngram-on-disk): per_layer_tok_embd is + // counted as created but never allocated, mapped or read, and build_ple takes its + // rows from this reader instead of ggml_get_rows. + std::shared_ptr ple_disk; void load_arch_hparams(llama_model_loader & ml) override; void load_arch_tensors(llama_model_loader & ml) override; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index acfdd5b50038..fdf629af9969 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -2,6 +2,7 @@ #include "llama-impl.h" #include "llama-memory-hybrid-idx.h" #include "llama-memory-recurrent.h" +#include "llama-ple-disk.h" #include #include @@ -136,8 +137,24 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { throw std::runtime_error(format("PLE head %u range exceeds the %" PRId64 " table rows", h, ple_rows)); } } - per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), - { hparams.ple_head_dim, ple_rows }, TENSOR_READ_LAZY); + if (params.ple_on_disk) { + // Counted as created but never allocated, mapped or read: each ubatch preads + // exactly the rows it gathers (llm_graph_input_ple::set_input). + llama_ple_disk::params dp; + dp.n_threads = params.ple_io_threads; + dp.cache_bytes = params.ple_cache_mb > 0 ? (size_t) params.ple_cache_mb << 20 : 0; + dp.direct_io = params.ple_direct_io; + + ple_disk = std::make_shared(ml.fnames.at(ple_w.idx), ple_w.offs, + ple_w.tensor->type, ple_w.tensor->ne[0], ple_rows, dp); + LLAMA_LOG_INFO("%s: PLE n-gram table stays on disk: %s\n", __func__, ple_disk->describe().c_str()); + + per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), + { hparams.ple_head_dim, ple_rows }, TENSOR_SKIP); + } else { + per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), + { hparams.ple_head_dim, ple_rows }, TENSOR_READ_LAZY); + } } for (int il = 0; il < n_layer; ++il) { @@ -969,6 +986,10 @@ class llm_graph_input_ple : public llm_graph_input_i { } ggml_tensor * rows = nullptr; // I32 [ple_n_heads * n_tokens] + ggml_tensor * embd = nullptr; // F32 [ple_head_dim, ple_n_heads * n_tokens]: the gathered rows, when the table is on disk + + std::vector embd_buf; + std::vector rows_global; const llama_model_qwen4exp & pmodel; @@ -1039,6 +1060,22 @@ void llm_graph_input_ple::set_input(const llama_ubatch * ubatch) { } } + if (pmodel.ple_disk) { + // idx is head-major and head-local; the reader wants one global row per (token, head) + // in token-major order, which is the layout the per-head gather + concat produces. + GGML_ASSERT(embd != nullptr && rows == nullptr); + rows_global.resize(idx.size()); + for (int64_t i = 0; i < n_tokens; ++i) { + for (int64_t h = 0; h < n_heads; ++h) { + rows_global[i*n_heads + h] = (int32_t) (idx[h*n_tokens + i] + (int64_t) hp.ple_head_offsets[h]); + } + } + embd_buf.resize(rows_global.size() * (size_t) hp.ple_head_dim); + pmodel.ple_disk->gather(rows_global.data(), rows_global.size(), embd_buf.data()); + ggml_backend_tensor_set(embd, embd_buf.data(), 0, embd_buf.size() * sizeof(float)); + return; + } + ggml_backend_tensor_set(rows, idx.data(), 0, idx.size()*ggml_element_size(rows)); } @@ -1103,13 +1140,26 @@ ggml_tensor * llama_model_qwen4exp::graph::build_ple( auto ple_inp = std::make_unique( static_cast(model), mctx_hyb->get_attn()); - ple_inp->rows = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_heads * n_tokens); - ggml_set_input(ple_inp->rows); - ggml_tensor * rows = ple_inp->rows; + const auto & qmodel = static_cast(model); + ggml_tensor * rows = nullptr; + ggml_tensor * emb = nullptr; + if (qmodel.ple_disk) { + // table on disk: the gather happened in set_input and arrives as an F32 input, + // token-major, which reshapes to what the per-head gathers + concat produce + ple_inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.ple_head_dim, n_heads * n_tokens); + ggml_set_input(ple_inp->embd); + emb = ple_inp->embd; + } else { + ple_inp->rows = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_heads * n_tokens); + ggml_set_input(ple_inp->rows); + rows = ple_inp->rows; + } res->add_input(std::move(ple_inp)); - // gather then flatten the heads: get_rows lays the head dimension out slowest, as the reference does - ggml_tensor * emb = ggml_get_rows(ctx0, model.per_layer_tok_embd, rows); + if (rows != nullptr) { + // gather then flatten the heads: get_rows lays the head dimension out slowest, as the reference does + emb = ggml_get_rows(ctx0, model.per_layer_tok_embd, rows); + } emb = ggml_reshape_2d(ctx0, emb, hparams.ple_head_dim * n_heads, n_tokens); cb(emb, "ple_embd", il); From dfb1328eb40c04c2bcccea410afe7ae64b9bb1a8 Mon Sep 17 00:00:00 2001 From: Nathanw1014 <67372905+Nathanw1014@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:12:25 +0930 Subject: [PATCH 005/212] speculative: keep DFlash draft blocks the same width across sequences (#6) Co-authored-by: Claude Opus 5 --- common/speculative.cpp | 51 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/common/speculative.cpp b/common/speculative.cpp index 23f599de1fde..2e90d8e59a43 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -210,6 +210,14 @@ struct common_speculative_impl { } } + // record the width a sequence was actually drafted at, when the caller had to + // override the per-sequence choice (see the DFlash/DSpark batch below) + void set_last_draft(llama_seq_id seq_id, int32_t n) { + if (seq_id >= 0 && (size_t) seq_id < n_last_draft.size()) { + n_last_draft[seq_id] = n; + } + } + // effective draft length for this step, never above the configured n_max int32_t adaptive_n_draft(llama_seq_id seq_id, int32_t n_cfg, int32_t n_min) { if (!adaptive_n || seq_id < 0 || (size_t) seq_id >= acc_ema.size()) { @@ -1283,6 +1291,44 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { std::vector i_block_beg(n_seq, -1); std::vector n_block (n_seq, 0); + // DFlash decodes the whole block in one pass, so a shorter block does not save + // draft time -- but it does shrink the target's verification batch, which is + // where the cost actually is. + // + // The block width has to be the same for every sequence in this batch. All the + // blocks go into one decode, and the draft graph reshapes those tokens into + // [block_size, n_seqs] -- build_dflash2_conv and the DSpark markov head both + // assert n_tokens % n_seqs_unq == 0. Per-sequence widths break that: with an + // equal ubatch split the longer block is silently cut across two ubatches, so + // its tail is convolved as a block of its own and drafts from a truncated + // window; with a simple split it trips the assert and takes the server down. + // So size the batch once, from the sequence with the highest measured + // acceptance: no sequence drafts shorter than its own estimate, and the extra + // positions a colder sequence receives are just rejected at verification. + int32_t n_draft_batch = params.n_max; + + if (adaptive_n) { + n_draft_batch = 0; + + for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { + if (!dparams[seq_id].drafting) { + continue; + } + + n_draft_batch = std::max(n_draft_batch, adaptive_n_draft(seq_id, params.n_max, params.n_min)); + } + + if (n_draft_batch == 0) { + return; + } + + for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { + if (dparams[seq_id].drafting) { + set_last_draft(seq_id, n_draft_batch); + } + } + } + for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { auto & dp = dparams[seq_id]; if (!dp.drafting) { @@ -1293,10 +1339,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { const int32_t n = (int32_t) dp.n_past; - // DFlash decodes the whole block in one pass, so a shorter block does - // not save draft time -- but it does shrink the target's verification - // batch, which is where the cost actually is. - const int32_t n_draft = adaptive_n_draft(seq_id, params.n_max, params.n_min); + const int32_t n_draft = n_draft_batch; const int32_t n_block_tokens = n_draft + (is_dspark && sample_from_anchor ? 0 : 1); i_block_beg[seq_id] = batch.n_tokens; From ca94157f70a2776e8da6b6849b50b45a083d0478 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti Date: Fri, 28 Aug 2026 11:43:36 +0100 Subject: [PATCH 006/212] vulkan: chunk batched mat-vec at the column counts that are slow on RDNA3.5 (#1) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 81 ++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 72e844aebfd5..565d199f5c49 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -9508,6 +9508,12 @@ static bool ggml_vk_should_use_mmvq(const vk_device& device, uint32_t m, uint32_ if (src0_type == GGML_TYPE_Q6_K && !mmvq_q6) { return false; } + // q8_0 with two columns through the q8_1 integer-dot path: 272-336 us against 17.5 us + // for the f32 path on the same 320x10240 (Strix Halo / RADV, GGML_VK_PERF_LOGGER), + // 824 vs 146 us on 12288x2560. Every other column count of q8_0 is fine on it. + if (src0_type == GGML_TYPE_Q8_0 && n == 2 && device->vendor_id == VK_VENDOR_ID_AMD) { + return false; + } // MMVQ is generally good for batches if (n > 1) { @@ -9576,7 +9582,7 @@ static bool ggml_vk_should_use_mmvq(const vk_device& device, uint32_t m, uint32_ GGML_UNUSED(m); } -static void ggml_vk_mul_mat_vec_q_f16(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx) { +static void ggml_vk_mul_mat_vec_q_f16_cols(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx, uint32_t col0, uint32_t ncols, bool allow_quantize_y) { ggml_tensor * dst = cgraph->nodes[node_idx]; const ggml_tensor * src0 = dst->src[0]; const ggml_tensor * src1 = dst->src[1]; @@ -9594,7 +9600,7 @@ static void ggml_vk_mul_mat_vec_q_f16(ggml_backend_vk_context * ctx, vk_context& const uint64_t ne03 = src0->ne[3]; const uint64_t ne10 = src1->ne[0]; - const uint64_t ne11 = src1->ne[1]; + const uint64_t ne11 = ncols; // a chunk of src1's columns starting at col0 (see ggml_vk_mul_mat_vec_q_f16) const uint64_t ne12 = src1->ne[2]; const uint64_t ne13 = src1->ne[3]; @@ -9615,7 +9621,7 @@ static void ggml_vk_mul_mat_vec_q_f16(ggml_backend_vk_context * ctx, vk_context& const bool y_non_contig = !ggml_vk_dim01_contiguous(src1); const bool f16_f32_kernel = src1->type == GGML_TYPE_F32; - bool quantize_y = ctx->device->integer_dot_product && src1->type == GGML_TYPE_F32 && ggml_is_contiguous(src1) && !y_non_contig && (ne11 * ne10) % 4 == 0 && ggml_vk_should_use_mmvq(ctx->device, ne01, ne11, ne10, src0->type); + bool quantize_y = allow_quantize_y && ctx->device->integer_dot_product && src1->type == GGML_TYPE_F32 && ggml_is_contiguous(src1) && !y_non_contig && (ne11 * ne10) % 4 == 0 && ggml_vk_should_use_mmvq(ctx->device, ne01, ne11, ne10, src0->type); vk_pipeline to_fp16_vk_0 = nullptr; vk_pipeline to_fp16_vk_1 = nullptr; @@ -9694,6 +9700,12 @@ static void ggml_vk_mul_mat_vec_q_f16(ggml_backend_vk_context * ctx, vk_context& vk_subbuffer d_D = ggml_vk_tensor_subbuffer(ctx, cgraph->nodes[node_idx + ctx->num_additional_fused_ops]); vk_subbuffer d_Qx = ggml_vk_tensor_subbuffer(ctx, src0); vk_subbuffer d_Qy = ggml_vk_tensor_subbuffer(ctx, src1); + if (col0 > 0) { + const uint64_t off_y = (uint64_t) col0 * src1->nb[1]; + const uint64_t off_d = (uint64_t) col0 * dst->nb[1]; + d_Qy.offset += off_y; d_Qy.size -= off_y; + d_D.offset += off_d; d_D.size -= off_d; + } vk_subbuffer d_X, d_Y; if (qx_needs_dequant) { @@ -9819,6 +9831,66 @@ static void ggml_vk_mul_mat_vec_q_f16(ggml_backend_vk_context * ctx, vk_context& } } + +// Column chunking for batched mat-vec. On Strix Halo (RADV, Mesa 26.1) the NUM_COLS +// shader variants for 3, 5 and 6 columns -- and 4 for q4_K -- run several times slower +// per call than 1, 2 and 4. Measured with GGML_VK_PERF_LOGGER on Qwen3.8-Flash-Next: +// q8_0 10240x2560 122 us at n=1, 127 at 2, 129 at 4, but 734 at 3, 1177 at 5, 360 at 6; +// the q6_K LM head (248320x2560) 2.24 ms at n=1, 2 and 4, but 12.8 at 3 and 18.5 at 5; +// q4_K 12288x2560 85 us at n=1, 180 at 4, 290 at 6. Speculative verification runs at +// n=2..6, so such batches are dispatched as chunks of column counts measured to scale, +// at the cost of re-reading the weights once per chunk. Chunks never take the q8_1 +// integer-dot path (its prealloc cache is keyed by tensor, not by column; and that path +// has the same pathology). GGML_VK_MMV_NO_SPLIT=1 restores the single dispatch. +static uint32_t ggml_vk_mmv_good_cols(ggml_type type, uint32_t n) { + switch (type) { + case GGML_TYPE_Q8_0: + case GGML_TYPE_Q6_K: + return n >= 4 ? 4 : (n >= 2 ? 2 : 1); + case GGML_TYPE_Q4_0: case GGML_TYPE_Q4_1: case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: + case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: case GGML_TYPE_Q5_K: + return n >= 2 ? 2 : 1; + default: + return n; // f32/f16/bf16 and the i-quants: no pathology measured + } +} + +// Batches of 9..16 columns of a quantized weight also go through chunked mat-vec: the +// mat-mat kernels are far slower at these widths on this GPU (q8_0 320x10240 at n=21: +// 756 us against 17.5 us per mat-vec column), and this is where several concurrent +// slots with speculative verification land. Mirrors the conditions the chunk path needs. +static constexpr uint32_t mul_mat_vec_chunk_max_cols = 16; +static bool ggml_vk_mmv_can_chunk(ggml_backend_vk_context * ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst) { + static const bool no_split = getenv("GGML_VK_MMV_NO_SPLIT") != nullptr; + return !no_split && ggml_is_quantized(src0->type) && + dst->ne[1] > mul_mat_vec_max_cols && dst->ne[1] <= mul_mat_vec_chunk_max_cols && + src1->ne[2] * src1->ne[3] == 1 && src1->type == GGML_TYPE_F32 && + ggml_is_contiguous(src1) && ggml_is_contiguous(dst) && ctx->num_additional_fused_ops == 0; +} + +static void ggml_vk_mul_mat_vec_q_f16(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx) { + ggml_tensor * dst = cgraph->nodes[node_idx]; + const ggml_tensor * src0 = dst->src[0]; + const ggml_tensor * src1 = dst->src[1]; + const uint32_t ne11 = (uint32_t) src1->ne[1]; + + static const bool no_split = getenv("GGML_VK_MMV_NO_SPLIT") != nullptr; + const bool plain = ne11 > 1 && src1->ne[2] * src1->ne[3] == 1 && + src1->type == GGML_TYPE_F32 && ggml_is_contiguous(src1) && ggml_is_contiguous(dst) && + ctx->num_additional_fused_ops == 0; + GGML_ASSERT(ne11 <= mul_mat_vec_max_cols || plain); + if (!no_split && plain && std::min(ggml_vk_mmv_good_cols(src0->type, ne11), mul_mat_vec_max_cols) < ne11) { + uint32_t col0 = 0; + while (col0 < ne11) { + const uint32_t n = std::min(ggml_vk_mmv_good_cols(src0->type, ne11 - col0), mul_mat_vec_max_cols); + ggml_vk_mul_mat_vec_q_f16_cols(ctx, subctx, cgraph, node_idx, col0, n, /*allow_quantize_y=*/ false); + col0 += n; + } + return; + } + ggml_vk_mul_mat_vec_q_f16_cols(ctx, subctx, cgraph, node_idx, 0, ne11, /*allow_quantize_y=*/ true); +} + static void ggml_vk_mul_mat_vec_p021_f16_f32(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx) { ggml_tensor * dst = cgraph->nodes[node_idx]; const ggml_tensor * src0 = dst->src[0]; @@ -10123,7 +10195,8 @@ static void ggml_vk_mul_mat(ggml_backend_vk_context * ctx, vk_context& subctx, c ggml_vk_mul_mat_vec_nc_f16_f32(ctx, subctx, cgraph, node_idx); // mul_mat_vec supports batching ne12*ne13 when ne11==1, or treating ne11 as the batch size (up to four) // when ne12 and ne13 are one. - } else if ((dst->ne[1] == 1 || (dst->ne[1] <= mul_mat_vec_max_cols && src1->ne[2] * src1->ne[3] == 1)) && + } else if ((dst->ne[1] == 1 || (dst->ne[1] <= mul_mat_vec_max_cols && src1->ne[2] * src1->ne[3] == 1) || + ggml_vk_mmv_can_chunk(ctx, src0, src1, dst)) && (src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16 || src0->type == GGML_TYPE_BF16 || ggml_is_quantized(src0->type))) { ggml_vk_mul_mat_vec_q_f16(ctx, subctx, cgraph, node_idx); } else { From 9051941375b19f2040bb834885da39b75941e511 Mon Sep 17 00:00:00 2001 From: voidsurfer Date: Fri, 28 Aug 2026 20:40:25 +0200 Subject: [PATCH 007/212] reasoning-budget: add multi-point budget mechanism (intro, soft warnings, grace, usage telemetry) (#5) Co-authored-by: voidsurfer <18734507+voidsurfer@users.noreply.github.com> --- common/arg.cpp | 113 +++++++++- common/common.h | 29 ++- common/reasoning-budget.cpp | 356 ++++++++++++++++++++++++-------- common/reasoning-budget.h | 69 ++++--- common/sampling.cpp | 37 +++- docs/speculative.md | 2 +- tests/test-chat.cpp | 50 +++++ tests/test-reasoning-budget.cpp | 304 +++++++++++++++++++++++++-- tools/server/README.md | 24 ++- tools/server/server-common.cpp | 34 ++- tools/server/server-common.h | 7 + tools/server/server-context.cpp | 28 +++ tools/server/server-schema.cpp | 75 +++++-- tools/server/server-task.cpp | 13 ++ tools/server/server-task.h | 5 + 15 files changed, 980 insertions(+), 166 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index 242186da1bf5..e4f694b84b3e 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -3717,6 +3717,50 @@ common_params_context common_params_parser_init(common_params & params, llama_ex } } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_REASONING_EFFORT")); + static auto unescape = [](const std::string & s) { + std::string r; + r.reserve(s.size()); + for (size_t i = 0; i < s.size(); i++) { + if (s[i] == '\\' && i + 1 < s.size()) { + switch (s[i + 1]) { + case 'n': + r += '\n'; + i++; + break; + case 't': + r += '\t'; + i++; + break; + case 'r': + r += '\r'; + i++; + break; + case '\\': + r += '\\'; + i++; + break; + case '"': + r += '"'; + i++; + break; + default: + r += s[i]; + break; + } + } else { + r += s[i]; + } + } + return r; + }; + + add_opt(common_arg({ "--reasoning-budget-enable" }, { "--no-reasoning-budget-enable" }, + "enable the reasoning budget mechanism: hard cutoff, soft warning, intro message, grace " + "period, and runtime reasoning control (default: disabled)", + [](common_params & params, bool value) { params.sampling.reasoning_budget_enabled = value; }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_ENABLE")); + add_opt(common_arg( {"--reasoning-budget"}, "N", "token budget for thinking: -1 for unrestricted, 0 for immediate end, N>0 for token budget (default: -1)", @@ -3725,13 +3769,68 @@ common_params_context common_params_parser_init(common_params & params, llama_ex params.sampling.reasoning_budget_tokens = value; } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_THINK_BUDGET")); - add_opt(common_arg( - {"--reasoning-budget-message"}, "MESSAGE", - "message injected before the end-of-thinking tag when reasoning budget is exhausted (default: none)", - [](common_params & params, const std::string & value) { - params.sampling.reasoning_budget_message = value; - } - ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_THINK_BUDGET_MESSAGE")); + add_opt(common_arg({ "--reasoning-budget-message" }, "MESSAGE", + "message forced when the reasoning budget is exhausted; include the closing tag because it is " + "not appended automatically (default: none)", + [](common_params & params, const std::string & value) { + params.sampling.reasoning_budget_message = unescape(value); + }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_MESSAGE")); + add_opt(common_arg({ "--reasoning-budget-soft-ratio" }, "N", + "fraction of the budget consumed before the first soft warning: <= 0 disables it, " + "(0,1] enables it (default: -1)", + [](common_params & params, const std::string & value) { + params.sampling.reasoning_budget_soft_ratio = std::stof(value); + }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_SOFT_RATIO")); + add_opt(common_arg({ "--reasoning-budget-soft-message" }, "MESSAGE", + "message forced at the first soft reasoning budget threshold (default: none)", + [](common_params & params, const std::string & value) { + params.sampling.reasoning_budget_soft_message = unescape(value); + }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_SOFT_MESSAGE")); + add_opt(common_arg({ "--reasoning-budget-soft2-ratio" }, "N", + "fraction of the budget consumed before the second soft warning: <= 0 disables it, " + "(0,1] enables it (default: -1)", + [](common_params & params, const std::string & value) { + params.sampling.reasoning_budget_soft2_ratio = std::stof(value); + }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_SOFT2_RATIO")); + add_opt(common_arg({ "--reasoning-budget-soft2-message" }, "MESSAGE", + "message forced at the second soft reasoning budget threshold (default: none)", + [](common_params & params, const std::string & value) { + params.sampling.reasoning_budget_soft2_message = unescape(value); + }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_SOFT2_MESSAGE")); + add_opt(common_arg({ "--reasoning-budget-intro-mode" }, "MODE", + "force the intro message for every reasoning block or only once per conversation " + "(default: every)", + [](common_params & params, const std::string & value) { + params.sampling.reasoning_budget_intro_mode = value; + }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_INTRO_MODE")); + add_opt(common_arg({ "--reasoning-budget-intro-message" }, "MESSAGE", + string_format( + "message forced when the reasoning block starts; use {budget} for the configured " + "budget (default: '%s')", + params.sampling.reasoning_budget_intro_message.c_str()), + [](common_params & params, const std::string & value) { + params.sampling.reasoning_budget_intro_message = unescape(value); + }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_INTRO_MESSAGE")); + add_opt(common_arg({ "--reasoning-budget-grace-tokens" }, "N", + "wait up to N tokens for a paragraph break before forcing the exhausted reasoning budget " + "(default: 0, disabled)", + [](common_params & params, int value) { params.sampling.reasoning_budget_grace_tokens = value; }) + .set_examples({ LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI }) + .set_env("LLAMA_ARG_THINK_BUDGET_GRACE_TOKENS")); add_opt(common_arg( {"--reasoning-preserve"}, {"--no-reasoning-preserve"}, diff --git a/common/common.h b/common/common.h index 735582767be9..478825b6b3ff 100644 --- a/common/common.h +++ b/common/common.h @@ -286,12 +286,31 @@ struct common_params_sampling { // reasoning budget sampler parameters // these are populated by the server/CLI based on chat template params + bool reasoning_budget_enabled = + false; // Master switch for the budget, soft warnings, intro, grace, and reasoning control. int32_t reasoning_budget_tokens = -1; // -1 = disabled, >= 0 = token budget - std::vector reasoning_budget_start; // start tag token sequence - std::vector reasoning_budget_end; // end tag token sequences; the first tag is used as the forcing sequence - std::vector reasoning_budget_forced; // forced sequence (message + first end tag) - std::string reasoning_budget_message; // message injected before end tag when budget exhausted - bool reasoning_control = false; // create the budget sampler on demand so reasoning can be ended at runtime + std::vector reasoning_budget_start; // Start tag token sequence. + std::vector reasoning_budget_end; // End tag token sequences. First is forced. + std::vector reasoning_budget_forced; // Forced sequence: message and first end tag. + std::string reasoning_budget_message; // Message injected before the forced end tag. + bool reasoning_control = false; // Create sampler for runtime reasoning control. + + float reasoning_budget_soft_ratio = -1.0f; // <= 0 disables the first soft warning. + std::vector reasoning_budget_soft_forced; + std::string reasoning_budget_soft_message; + + float reasoning_budget_soft2_ratio = -1.0f; + std::vector reasoning_budget_soft2_forced; + std::string reasoning_budget_soft2_message; + + std::vector reasoning_budget_intro_forced; + std::string reasoning_budget_intro_message = + "I'll keep this reasoning under {budget} tokens, so I'll stay focused and efficient. "; + std::string reasoning_budget_intro_mode = + "every"; + + int32_t reasoning_budget_grace_tokens = + 0; // <= 0 forces immediately. Positive values wait for a paragraph break. bool backend_sampling = false; diff --git a/common/reasoning-budget.cpp b/common/reasoning-budget.cpp index 4884299f3011..8c65908b0fa2 100644 --- a/common/reasoning-budget.cpp +++ b/common/reasoning-budget.cpp @@ -36,7 +36,7 @@ struct token_matcher { return t; } - // returns the index into seqs of the longest sequence ending at this token, or -1 + // Returns the index of the longest sequence ending at this token, or -1. int32_t advance(llama_token token) { state = ac.next(state, (uint32_t) token); const int32_t p = ac.match_pattern(state); @@ -56,21 +56,90 @@ struct common_reasoning_budget_ctx { token_matcher end_matcher; llama_tokens forced_tokens; - int32_t budget; // maximum tokens in reasoning block - int32_t remaining; // tokens remaining in budget + int32_t budget; // Maximum tokens in reasoning block. + int32_t remaining; // Tokens remaining in budget. common_reasoning_budget_state state; - // for forcing - size_t force_pos; // next position in forced_tokens to force + size_t force_pos; // Next position in forced_tokens to force. - int32_t end_match; // index into end_matcher.seqs of the sequence that transitioned to DONE, -1 if none + int32_t end_match; // Index into end_matcher.seqs that transitioned to DONE, or -1. + + struct soft_point_state { + int32_t threshold; + llama_tokens tokens; + bool triggered; + size_t force_pos; + }; + + std::vector soft_points; + size_t active_soft; + + llama_tokens intro_forced_tokens; + size_t intro_force_pos; + + int32_t grace_tokens; + int32_t grace_remaining; + bool hard_pending_prev_nl; }; static const char * common_reasoning_budget_name(const struct llama_sampler * /*smpl*/) { return "reasoning-budget"; } +static bool token_utf8_complete(const common_reasoning_budget_ctx * ctx, llama_token token) { + if (ctx->vocab == nullptr) { + return true; + } + const std::string piece = common_token_to_piece(ctx->vocab, token, false); + return common_utf8_is_complete(piece); +} + +static void common_reasoning_budget_begin_forcing(common_reasoning_budget_ctx * ctx, llama_token token) { + ctx->end_matcher.reset(); + if (token_utf8_complete(ctx, token)) { + ctx->state = REASONING_BUDGET_FORCING; + ctx->force_pos = 0; + } else { + ctx->state = REASONING_BUDGET_WAITING_UTF8; + } +} + +static void common_reasoning_budget_enter_hard_exhausted(common_reasoning_budget_ctx * ctx, llama_token token) { + if (ctx->grace_tokens > 0) { + ctx->state = REASONING_BUDGET_HARD_PENDING; + ctx->grace_remaining = ctx->grace_tokens; + ctx->hard_pending_prev_nl = false; + ctx->end_matcher.reset(); + COM_TRC("budget exhausted, waiting up to %d tokens for a paragraph break\n", ctx->grace_tokens); + return; + } + + common_reasoning_budget_begin_forcing(ctx, token); + COM_TRC("%s", "budget exhausted, forcing end sequence\n"); +} + +static void common_reasoning_budget_activate(common_reasoning_budget_ctx * ctx) { + ctx->remaining = ctx->budget; + for (auto & sp : ctx->soft_points) { + sp.triggered = false; + } + ctx->end_match = -1; + + if (!ctx->intro_forced_tokens.empty()) { + ctx->state = REASONING_BUDGET_INTRO_FORCING; + ctx->intro_force_pos = 0; + COM_TRC("activated, budget=%d tokens, forcing intro message\n", ctx->budget); + } else if (ctx->remaining <= 0) { + ctx->state = REASONING_BUDGET_FORCING; + ctx->force_pos = 0; + COM_TRC("%s", "budget=0, forcing immediately\n"); + } else { + ctx->state = REASONING_BUDGET_COUNTING; + COM_TRC("activated, budget=%d tokens\n", ctx->budget); + } +} + static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_token token) { auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx; @@ -78,20 +147,24 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to case REASONING_BUDGET_IDLE: { if (ctx->start_matcher.advance(token) >= 0) { - ctx->state = REASONING_BUDGET_COUNTING; - ctx->remaining = ctx->budget; - COM_TRC("activated, budget=%d tokens\n", ctx->budget); - + common_reasoning_budget_activate(ctx); + } + break; + } + case REASONING_BUDGET_INTRO_FORCING: + ctx->intro_force_pos++; + if (ctx->intro_force_pos >= ctx->intro_forced_tokens.size()) { if (ctx->remaining <= 0) { ctx->state = REASONING_BUDGET_FORCING; ctx->force_pos = 0; - COM_TRC("%s", "budget=0, forcing immediately\n"); + COM_TRC("%s", "intro complete, budget=0, forcing immediately\n"); + } else { + ctx->state = REASONING_BUDGET_COUNTING; + COM_TRC("%s", "intro complete, resuming countdown\n"); } } break; - } case REASONING_BUDGET_COUNTING: - case REASONING_BUDGET_WAITING_UTF8: { const int32_t match = ctx->end_matcher.advance(token); if (match >= 0) { @@ -101,39 +174,109 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to break; } - bool utf8_complete = true; - if (ctx->vocab != nullptr) { - const std::string piece = common_token_to_piece(ctx->vocab, token, false); - utf8_complete = common_utf8_is_complete(piece); + ctx->remaining--; + if (ctx->remaining <= 0) { + common_reasoning_budget_enter_hard_exhausted(ctx, token); + break; } - if (ctx->state == REASONING_BUDGET_WAITING_UTF8) { - if (utf8_complete) { - ctx->state = REASONING_BUDGET_FORCING; - ctx->force_pos = 0; - ctx->end_matcher.reset(); - COM_TRC("%s", "UTF-8 complete, now forcing end sequence\n"); + for (size_t i = 0; i < ctx->soft_points.size(); i++) { + auto & sp = ctx->soft_points[i]; + if (!sp.triggered && ctx->remaining <= sp.threshold) { + ctx->active_soft = i; + ctx->state = REASONING_BUDGET_SOFT_PENDING; + COM_TRC("soft threshold reached (remaining=%d, point %zu), waiting for newline\n", ctx->remaining, + i); + break; } - } else if (ctx->state == REASONING_BUDGET_COUNTING) { - ctx->remaining--; - if (ctx->remaining <= 0) { - if (utf8_complete) { - ctx->state = REASONING_BUDGET_FORCING; - ctx->force_pos = 0; - ctx->end_matcher.reset(); - COM_TRC("%s", "budget exhausted, forcing end sequence\n"); - } else { - ctx->state = REASONING_BUDGET_WAITING_UTF8; - ctx->end_matcher.reset(); - COM_TRC("%s", "budget exhausted, waiting for UTF-8 completion\n"); - } + } + break; + } + case REASONING_BUDGET_SOFT_PENDING: + { + const int32_t match = ctx->end_matcher.advance(token); + if (match >= 0) { + ctx->state = REASONING_BUDGET_DONE; + ctx->end_match = match; + COM_TRC("%s", "deactivated (natural end)\n"); + break; + } + + ctx->remaining--; + if (ctx->remaining <= 0) { + COM_TRC("%s", "budget exhausted before newline, soft warning skipped\n"); + common_reasoning_budget_enter_hard_exhausted(ctx, token); + break; + } + + if (ctx->vocab != nullptr) { + const std::string piece = common_token_to_piece(ctx->vocab, token, false); + if (piece.find('\n') != std::string::npos) { + ctx->state = REASONING_BUDGET_SOFT_FORCING; + auto & sp = ctx->soft_points[ctx->active_soft]; + sp.force_pos = 0; + sp.triggered = true; + COM_TRC("%s", "newline boundary found, forcing soft warning\n"); } } break; } + case REASONING_BUDGET_SOFT_FORCING: + { + auto & sp = ctx->soft_points[ctx->active_soft]; + sp.force_pos++; + if (sp.force_pos >= sp.tokens.size()) { + ctx->state = REASONING_BUDGET_COUNTING; + COM_TRC("%s", "soft warning complete, resuming countdown\n"); + } + break; + } + case REASONING_BUDGET_HARD_PENDING: + { + const int32_t match = ctx->end_matcher.advance(token); + if (match >= 0) { + ctx->state = REASONING_BUDGET_DONE; + ctx->end_match = match; + COM_TRC("%s", "deactivated (natural end)\n"); + break; + } + + ctx->grace_remaining--; + + const std::string piece = + ctx->vocab != nullptr ? common_token_to_piece(ctx->vocab, token, false) : std::string(); + const bool paragraph_boundary = piece.find("\n\n") != std::string::npos || + (ctx->hard_pending_prev_nl && !piece.empty() && piece[0] == '\n'); + ctx->hard_pending_prev_nl = !piece.empty() && piece.back() == '\n'; + + if (paragraph_boundary) { + common_reasoning_budget_begin_forcing(ctx, token); + COM_TRC("%s", "paragraph boundary found, forcing end sequence\n"); + } else if (ctx->grace_remaining <= 0) { + common_reasoning_budget_begin_forcing(ctx, token); + COM_TRC("%s", "grace period expired, forcing end sequence\n"); + } + break; + } + case REASONING_BUDGET_WAITING_UTF8: + { + const int32_t match = ctx->end_matcher.advance(token); + if (match >= 0) { + ctx->state = REASONING_BUDGET_DONE; + ctx->end_match = match; + COM_TRC("%s", "deactivated (natural end)\n"); + break; + } + + if (token_utf8_complete(ctx, token)) { + common_reasoning_budget_begin_forcing(ctx, token); + COM_TRC("%s", "UTF-8 complete, now forcing end sequence\n"); + } + break; + } case REASONING_BUDGET_FORCING: { - // track the end sequence within forced_tokens so it is also reported on DONE + // Track the end sequence in forced_tokens for grammar replay. const int32_t match = ctx->end_matcher.advance(token); ctx->force_pos++; if (ctx->force_pos >= ctx->forced_tokens.size()) { @@ -144,20 +287,9 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to break; } case REASONING_BUDGET_DONE: - // Re-arm on a new start tag: some models emit multiple blocks - // per response, and each should get a fresh budget window. if (ctx->start_matcher.advance(token) >= 0) { - ctx->state = REASONING_BUDGET_COUNTING; - ctx->remaining = ctx->budget; ctx->end_matcher.reset(); - ctx->end_match = -1; - COM_TRC("re-activated on new start tag, budget=%d tokens\n", ctx->budget); - - if (ctx->remaining <= 0) { - ctx->state = REASONING_BUDGET_FORCING; - ctx->force_pos = 0; - COM_TRC("%s", "budget=0, forcing immediately\n"); - } + common_reasoning_budget_activate(ctx); } break; } @@ -166,18 +298,28 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to static void common_reasoning_budget_apply(struct llama_sampler * smpl, llama_token_data_array * cur_p) { auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx; - if (ctx->state != REASONING_BUDGET_FORCING) { - // passthrough — don't modify logits - return; - } + llama_token forced; - if (ctx->force_pos >= ctx->forced_tokens.size()) { + if (ctx->state == REASONING_BUDGET_FORCING) { + if (ctx->force_pos >= ctx->forced_tokens.size()) { + return; + } + forced = ctx->forced_tokens[ctx->force_pos]; + } else if (ctx->state == REASONING_BUDGET_SOFT_FORCING) { + const auto & sp = ctx->soft_points[ctx->active_soft]; + if (sp.force_pos >= sp.tokens.size()) { + return; + } + forced = sp.tokens[sp.force_pos]; + } else if (ctx->state == REASONING_BUDGET_INTRO_FORCING) { + if (ctx->intro_force_pos >= ctx->intro_forced_tokens.size()) { + return; + } + forced = ctx->intro_forced_tokens[ctx->intro_force_pos]; + } else { return; } - const llama_token forced = ctx->forced_tokens[ctx->force_pos]; - - // set all logits to -inf except the forced token for (size_t i = 0; i < cur_p->size; i++) { if (cur_p->data[i].id != forced) { cur_p->data[i].logit = -INFINITY; @@ -193,12 +335,26 @@ static void common_reasoning_budget_reset(struct llama_sampler * smpl) { ctx->end_matcher.reset(); ctx->force_pos = 0; ctx->end_match = -1; + for (auto & sp : ctx->soft_points) { + sp.triggered = false; + sp.force_pos = 0; + } + ctx->active_soft = 0; + ctx->intro_force_pos = 0; + ctx->grace_remaining = ctx->grace_tokens; + ctx->hard_pending_prev_nl = false; } static struct llama_sampler * common_reasoning_budget_init_state( - const struct llama_vocab * vocab, const std::vector & start_seqs, - const std::vector & end_seqs, const llama_tokens & forced_tokens, - int32_t budget, common_reasoning_budget_state initial_state); + const struct llama_vocab * vocab, + const std::vector & start_seqs, + const std::vector & end_seqs, + const llama_tokens & forced_tokens, + const std::vector & soft_points, + const llama_tokens & intro_forced_tokens, + int32_t budget, + int32_t grace_tokens, + common_reasoning_budget_state initial_state); static struct llama_sampler * common_reasoning_budget_clone(const struct llama_sampler * smpl); @@ -231,41 +387,63 @@ static struct llama_sampler * common_reasoning_budget_clone(const struct llama_s } static struct llama_sampler * common_reasoning_budget_init_state( - const struct llama_vocab * vocab, - const std::vector & start_seqs, - const std::vector & end_seqs, - const llama_tokens & forced_tokens, - int32_t budget, - common_reasoning_budget_state initial_state) { - // promote COUNTING with budget <= 0 to FORCING + const struct llama_vocab * vocab, + const std::vector & start_seqs, + const std::vector & end_seqs, + const llama_tokens & forced_tokens, + const std::vector & soft_points, + const llama_tokens & intro_forced_tokens, + int32_t budget, + int32_t grace_tokens, + common_reasoning_budget_state initial_state) { if (initial_state == REASONING_BUDGET_COUNTING && budget <= 0) { initial_state = REASONING_BUDGET_FORCING; } + std::vector soft_pts; + for (const auto & p : soft_points) { + if (!p.tokens.empty() && p.threshold >= 0) { + soft_pts.push_back({ p.threshold, p.tokens, false, 0 }); + } + } + std::sort(soft_pts.begin(), soft_pts.end(), + [](const common_reasoning_budget_ctx::soft_point_state & a, + const common_reasoning_budget_ctx::soft_point_state & b) { return a.threshold > b.threshold; }); + return llama_sampler_init( /* .iface = */ &common_reasoning_budget_i, - /* .ctx = */ new common_reasoning_budget_ctx { - /* .vocab = */ vocab, - /* .start_matcher = */ token_matcher(start_seqs), - /* .end_matcher = */ token_matcher(end_seqs), - /* .forced_tokens = */ forced_tokens, - /* .budget = */ budget, - /* .remaining = */ budget, - /* .state = */ initial_state, - /* .force_pos = */ 0, - /* .end_match = */ -1, - } - ); + /* .ctx = */ new common_reasoning_budget_ctx{ + /* .vocab = */ vocab, + /* .start_matcher = */ token_matcher(start_seqs), + /* .end_matcher = */ token_matcher(end_seqs), + /* .forced_tokens = */ forced_tokens, + /* .budget = */ budget, + /* .remaining = */ budget, + /* .state = */ initial_state, + /* .force_pos = */ 0, + /* .end_match = */ -1, + /* .soft_points = */ std::move(soft_pts), + /* .active_soft = */ 0, + /* .intro_forced_tokens = */ intro_forced_tokens, + /* .intro_force_pos = */ 0, + /* .grace_tokens = */ grace_tokens, + /* .grace_remaining = */ grace_tokens, + /* .hard_pending_prev_nl = */ false, + }); } struct llama_sampler * common_reasoning_budget_init( - const struct llama_vocab * vocab, - const std::vector & start_seqs, - const std::vector & end_seqs, - const llama_tokens & forced_tokens, - int32_t budget, - common_reasoning_budget_state initial_state) { - return common_reasoning_budget_init_state(vocab, start_seqs, end_seqs, forced_tokens, budget, initial_state); + const struct llama_vocab * vocab, + const std::vector & start_seqs, + const std::vector & end_seqs, + const llama_tokens & forced_tokens, + const std::vector & soft_points, + const llama_tokens & intro_forced_tokens, + int32_t budget, + int32_t grace_tokens, + common_reasoning_budget_state initial_state) { + return common_reasoning_budget_init_state(vocab, start_seqs, end_seqs, forced_tokens, soft_points, + intro_forced_tokens, budget, grace_tokens, initial_state); } common_reasoning_budget_state common_reasoning_budget_get_state(const struct llama_sampler * smpl) { @@ -295,9 +473,9 @@ bool common_reasoning_budget_force(struct llama_sampler * smpl) { auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx; - // only a sampler that is actively counting down the budget may be forced; - // any other state (idle, already forcing/waiting, or done) is left untouched - if (ctx->state != REASONING_BUDGET_COUNTING) { + if (ctx->state != REASONING_BUDGET_COUNTING && ctx->state != REASONING_BUDGET_INTRO_FORCING && + ctx->state != REASONING_BUDGET_SOFT_PENDING && ctx->state != REASONING_BUDGET_SOFT_FORCING && + ctx->state != REASONING_BUDGET_HARD_PENDING) { return false; } diff --git a/common/reasoning-budget.h b/common/reasoning-budget.h index 1b89a04c42e8..4151777e82c4 100644 --- a/common/reasoning-budget.h +++ b/common/reasoning-budget.h @@ -8,38 +8,55 @@ #include enum common_reasoning_budget_state { - REASONING_BUDGET_IDLE, // waiting for start sequence - REASONING_BUDGET_COUNTING, // counting down tokens - REASONING_BUDGET_FORCING, // forcing budget message + end sequence - REASONING_BUDGET_WAITING_UTF8, // budget exhausted, waiting for UTF-8 completion - REASONING_BUDGET_DONE, // passthrough forever + REASONING_BUDGET_IDLE, // Waiting for start sequence. + REASONING_BUDGET_INTRO_FORCING, // Forcing the intro message. + REASONING_BUDGET_COUNTING, // Counting down tokens. + REASONING_BUDGET_SOFT_PENDING, // Waiting for a soft-warning newline boundary. + REASONING_BUDGET_SOFT_FORCING, // Forcing the soft warning message. + REASONING_BUDGET_HARD_PENDING, // Waiting for a hard-cutoff paragraph boundary. + REASONING_BUDGET_FORCING, // Forcing budget message and end sequence. + REASONING_BUDGET_WAITING_UTF8, // Waiting for UTF-8 completion before forcing. + REASONING_BUDGET_DONE, // Passthrough forever. }; -// Creates a reasoning budget sampler that limits token generation inside a -// reasoning block (e.g. between and ). +struct common_reasoning_budget_soft_point { + int32_t threshold; // Fire when remaining <= threshold. + llama_tokens tokens; // Message forced at this point. Empty disables it. +}; + +// Creates a sampler that limits generation inside a reasoning block. +// Intro, soft, and forced tokens do not count against the budget. // -// State machine: IDLE -> COUNTING -> WAITING_UTF8 -> FORCING -> DONE -// IDLE: passthrough, watching for a start sequence -// COUNTING: counting down remaining tokens, watching for a natural end sequence -// WAITING_UTF8: budget exhausted, allowing tokens to complete a UTF-8 sequence -// FORCING: forces forced_tokens token-by-token (all other logits -> -inf) -// DONE: passthrough forever +// State machine: +// IDLE -> INTRO_FORCING -> COUNTING -> SOFT_PENDING -> SOFT_FORCING -> +// COUNTING -> HARD_PENDING -> WAITING_UTF8 -> FORCING -> DONE // -// Parameters: -// vocab - vocabulary (used for UTF-8 boundary detection; can be nullptr) -// start_seqs - token sequences, any of which activates counting -// end_seqs - token sequences, any of which naturally deactivates -// forced_tokens - token sequence forced when budget expires -// budget - max tokens allowed in the reasoning block -// initial_state - initial state +// A soft point fires once at the next newline after its threshold is crossed. +// The hard cutoff takes priority when the budget is exhausted. // +// A positive grace_tokens value waits for a paragraph boundary after exhaustion. +// It forces after at most grace_tokens more tokens. +// +// Parameters: +// vocab - vocabulary for UTF-8 and paragraph boundary detection +// start_seqs - sequences that activate counting +// end_seqs - sequences that naturally deactivate +// forced_tokens - sequence forced when the budget expires +// soft_points - soft warning points +// intro_forced_tokens - sequence forced when the block starts +// budget - max generated tokens in the reasoning block +// grace_tokens - max tokens to wait for a paragraph boundary +// initial_state - initial state struct llama_sampler * common_reasoning_budget_init( - const struct llama_vocab * vocab, - const std::vector & start_seqs, - const std::vector & end_seqs, - const llama_tokens & forced_tokens, - int32_t budget, - common_reasoning_budget_state initial_state = REASONING_BUDGET_IDLE); + const struct llama_vocab * vocab, + const std::vector & start_seqs, + const std::vector & end_seqs, + const llama_tokens & forced_tokens, + const std::vector & soft_points, + const llama_tokens & intro_forced_tokens, + int32_t budget, + int32_t grace_tokens = 0, + common_reasoning_budget_state initial_state = REASONING_BUDGET_IDLE); common_reasoning_budget_state common_reasoning_budget_get_state(const struct llama_sampler * smpl); diff --git a/common/sampling.cpp b/common/sampling.cpp index 06dea1e1ccea..8141dfa0f79f 100644 --- a/common/sampling.cpp +++ b/common/sampling.cpp @@ -307,18 +307,39 @@ struct common_sampler * common_sampler_init( } } - // reasoning budget sampler (skip when budget is unlimited unless a lazy grammar is active, which needs rbudget for thinking-block suppression) - if (!params.reasoning_budget_start.empty() && !params.reasoning_budget_end.empty() && (params.grammar_lazy || params.reasoning_budget_tokens >= 0 || params.reasoning_control)) { - rbudget = common_reasoning_budget_init( - vocab, - {params.reasoning_budget_start}, - params.reasoning_budget_end, - params.reasoning_budget_forced, - params.reasoning_budget_tokens < 0 ? INT_MAX : params.reasoning_budget_tokens); + // Grammar lazy mode needs the sampler to suppress grammar in a reasoning block. + // The master switch gates budget forcing and runtime reasoning control only. + const bool reasoning_budget_active = + params.reasoning_budget_enabled && (params.reasoning_budget_tokens >= 0 || params.reasoning_control); + if (!params.reasoning_budget_start.empty() && !params.reasoning_budget_end.empty() && + (params.grammar_lazy || reasoning_budget_active)) { + const int32_t budget_tokens = params.reasoning_budget_tokens < 0 ? INT_MAX : params.reasoning_budget_tokens; + + std::vector soft_points; + auto add_soft_point = [&](float ratio, const llama_tokens & tokens) { + if (ratio > 0.0f && !tokens.empty() && budget_tokens != INT_MAX) { + const float r = std::min(ratio, 1.0f); + soft_points.push_back({ std::max(0, budget_tokens - (int32_t) std::ceil(budget_tokens * r)), tokens }); + } + }; + add_soft_point(params.reasoning_budget_soft_ratio, params.reasoning_budget_soft_forced); + add_soft_point(params.reasoning_budget_soft2_ratio, params.reasoning_budget_soft2_forced); + + const llama_tokens intro_tokens = + reasoning_budget_active ? params.reasoning_budget_intro_forced : llama_tokens(); + rbudget = common_reasoning_budget_init(vocab, { params.reasoning_budget_start }, params.reasoning_budget_end, + params.reasoning_budget_forced, soft_points, intro_tokens, budget_tokens, + params.reasoning_budget_grace_tokens); for (const auto & token : prefill_tokens) { llama_sampler_accept(rbudget, token); LOG_DBG("%s: reasoning-budget accepted prefill token (%d)\n", __func__, token); + + const auto state = common_reasoning_budget_get_state(rbudget); + if (state == REASONING_BUDGET_INTRO_FORCING || state == REASONING_BUDGET_SOFT_FORCING || + state == REASONING_BUDGET_FORCING) { + break; + } } } diff --git a/docs/speculative.md b/docs/speculative.md index ffb1e34c7fbf..465242525668 100644 --- a/docs/speculative.md +++ b/docs/speculative.md @@ -428,7 +428,7 @@ statistics draft: #calls = 10, #gen drafts = 10, #acc drafts = 10, #gen tokens = ``` draft acceptance rate = 0.70312 ( 90 accepted / 128 generated) -statistics ngram_mod: #calls = 810, #gen drafts = 15, #acc drafts = 15, #gen tokens = 960, #acc tokens = 730, dur(b,g,a) = 0.149, 0.347, 0.005 ms +statistics ngram-mod: #calls = 810, #gen drafts = 15, #acc drafts = 15, #gen tokens = 960, #acc tokens = 730, dur(b,g,a) = 0.149, 0.347, 0.005 ms ``` ``` diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 7918f0ffcf48..e3d5d18ca463 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -7058,6 +7058,55 @@ static void test_reasoning_budget_message_per_request() { } } +static void test_reasoning_budget_enabled_per_request() { + LOG_DBG("%s\n", __func__); + auto tmpls = read_templates("models/templates/Qwen-Qwen3-0.6B.jinja"); + + server_chat_params opt; + opt.tmpls = std::move(tmpls); + opt.use_jinja = true; + opt.enable_thinking = true; + opt.reasoning_budget = -1; + opt.reasoning_format = COMMON_REASONING_FORMAT_NONE; + + { + json body = { + {"messages", json::array({json{{"role", "user"}, {"content", "hello"}}})}, + {"reasoning_budget_enabled", true}, + }; + std::vector out_files; + auto llama_params = oaicompat_chat_params_parse(body, opt, out_files); + if (!llama_params.contains("reasoning_budget_enabled") || + !llama_params["reasoning_budget_enabled"].get()) { + throw std::runtime_error("reasoning_budget_enabled=true not forwarded to llama_params"); + } + } + + { + json body = { + {"messages", json::array({json{{"role", "user"}, {"content", "hello"}}})}, + {"reasoning_budget_enabled", false}, + }; + std::vector out_files; + auto llama_params = oaicompat_chat_params_parse(body, opt, out_files); + if (!llama_params.contains("reasoning_budget_enabled") || + llama_params["reasoning_budget_enabled"].get()) { + throw std::runtime_error("reasoning_budget_enabled=false not forwarded to llama_params"); + } + } + + { + json body = { + {"messages", json::array({json{{"role", "user"}, {"content", "hello"}}})}, + }; + std::vector out_files; + auto llama_params = oaicompat_chat_params_parse(body, opt, out_files); + if (llama_params.contains("reasoning_budget_enabled")) { + throw std::runtime_error("reasoning_budget_enabled must not be set in llama_params when omitted from the request body"); + } + } +} + static void test_reasoning_effort_caps() { LOG_DBG("%s\n", __func__); @@ -7238,6 +7287,7 @@ int main(int argc, char ** argv) { test_reasoning_effort_caps(); test_reasoning_budget_tokens_per_request(); test_reasoning_budget_message_per_request(); + test_reasoning_budget_enabled_per_request(); test_template_output_peg_parsers(detailed_debug); std::cout << "\n[chat] All tests passed!" << '\n'; } diff --git a/tests/test-reasoning-budget.cpp b/tests/test-reasoning-budget.cpp index 3bcc77e1733c..e4c6d3c44e3e 100644 --- a/tests/test-reasoning-budget.cpp +++ b/tests/test-reasoning-budget.cpp @@ -43,11 +43,14 @@ static void test_reasoning_budget( // For this test, we use nullptr as vocab since we're testing state transitions // The UTF-8 boundary check will treat all tokens as complete (safe fallback) auto * sampler = common_reasoning_budget_init( - nullptr, // vocab - not used for basic state machine tests + nullptr, start_seqs, end_seqs, forced_tokens, + {}, + {}, budget, + 0, initial_state ); @@ -156,7 +159,7 @@ static void test_reasoning_budget_clone_mid_counting() { const std::vector end = {101}; const std::vector forced = {102, 101}; - auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, forced, 2, REASONING_BUDGET_IDLE); + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 2, 0, REASONING_BUDGET_IDLE); llama_sampler_accept(sampler, 100); // COUNTING, remaining=2 llama_sampler_accept(sampler, 50); // COUNTING, remaining=1 @@ -175,7 +178,7 @@ static void test_reasoning_budget_clone_mid_forcing() { const std::vector end = {101}; const std::vector forced = {102, 101}; - auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, forced, 0, REASONING_BUDGET_FORCING); + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 0, 0, REASONING_BUDGET_FORCING); GGML_ASSERT(get_forced_token(sampler, 102) == 102); llama_sampler_accept(sampler, 102); // advance to the second forced token @@ -195,7 +198,7 @@ static void test_reasoning_budget_force_manual() { // if COUNTING, force() succeeds and begins forcing the end sequence from the start { - auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, forced, 5, REASONING_BUDGET_IDLE); + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 5, 0, REASONING_BUDGET_IDLE); llama_sampler_accept(sampler, 100); // COUNTING, remaining=5 llama_sampler_accept(sampler, 50); // COUNTING, remaining=4 @@ -216,7 +219,7 @@ static void test_reasoning_budget_force_manual() { // if IDLE, force() is a no-op { - auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, forced, 5, REASONING_BUDGET_IDLE); + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 5, 0, REASONING_BUDGET_IDLE); GGML_ASSERT(!common_reasoning_budget_force(sampler) && "force() must not transition from IDLE"); GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_IDLE); @@ -226,7 +229,7 @@ static void test_reasoning_budget_force_manual() { // if DONE, force() is a no-op { - auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, forced, 5, REASONING_BUDGET_IDLE); + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 5, 0, REASONING_BUDGET_IDLE); llama_sampler_accept(sampler, 100); // COUNTING llama_sampler_accept(sampler, 101); // natural end -> DONE @@ -240,7 +243,7 @@ static void test_reasoning_budget_force_manual() { // if FORCING, force() is a no-op and must not rewind the force position { - auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, forced, 0, REASONING_BUDGET_FORCING); + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 0, 0, REASONING_BUDGET_FORCING); GGML_ASSERT(get_forced_token(sampler, 102) == 102); llama_sampler_accept(sampler, 102); // advance to the second forced token (force_pos=1) @@ -258,13 +261,279 @@ static void test_reasoning_budget_force_manual() { fprintf(stderr, " Test 'manual force transition' passed\n"); } +static void test_reasoning_budget_soft_warning_skipped_before_hard_cutoff() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector soft_forced = { 200, 201 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, { { 5, soft_forced } }, {}, 10, 0, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + for (llama_token t : { 50, 51, 52, 53 }) { + llama_sampler_accept(sampler, t); + } + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING); + + llama_sampler_accept(sampler, 54); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_SOFT_PENDING); + + for (llama_token t : { 55, 56, 57, 58 }) { + llama_sampler_accept(sampler, t); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_SOFT_PENDING); + } + llama_sampler_accept(sampler, 59); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 202) == 102); + + llama_sampler_accept(sampler, 102); + GGML_ASSERT(get_forced_token(sampler, 202) == 101); + llama_sampler_accept(sampler, 101); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_soft_forcing_resumes_counting() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector soft_forced = { 200, 201 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, { { 2, soft_forced } }, {}, 5, 0, REASONING_BUDGET_SOFT_FORCING); + + GGML_ASSERT(get_forced_token(sampler, 201) == 200); + llama_sampler_accept(sampler, 200); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_SOFT_FORCING); + + GGML_ASSERT(get_forced_token(sampler, 201) == 201); + llama_sampler_accept(sampler, 201); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_force_manual_from_soft_states() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector soft_forced = { 200, 201 }; + + { + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, { { 5, soft_forced } }, {}, 10, 0, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + for (llama_token t : { 50, 51, 52, 53, 54 }) { + llama_sampler_accept(sampler, t); + } + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_SOFT_PENDING); + + GGML_ASSERT(common_reasoning_budget_force(sampler)); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 202) == 102); + + llama_sampler_free(sampler); + } + + { + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, { { 2, soft_forced } }, {}, 5, 0, REASONING_BUDGET_SOFT_FORCING); + + llama_sampler_accept(sampler, 200); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_SOFT_FORCING); + + GGML_ASSERT(common_reasoning_budget_force(sampler)); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 202) == 102); + + llama_sampler_free(sampler); + } +} + +static void test_reasoning_budget_intro_forcing_then_counting() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector intro_forced = { 300, 301 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, intro_forced, 3, 0, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_INTRO_FORCING); + + GGML_ASSERT(get_forced_token(sampler, 301) == 300); + llama_sampler_accept(sampler, 300); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_INTRO_FORCING); + + GGML_ASSERT(get_forced_token(sampler, 301) == 301); + llama_sampler_accept(sampler, 301); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING); + + llama_sampler_accept(sampler, 50); + llama_sampler_accept(sampler, 51); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING); + llama_sampler_accept(sampler, 52); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 302) == 102); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_intro_forcing_budget_zero() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector intro_forced = { 300, 301 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, intro_forced, 0, 0, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_INTRO_FORCING); + + llama_sampler_accept(sampler, 300); + llama_sampler_accept(sampler, 301); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 302) == 102); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_force_manual_from_intro() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector intro_forced = { 300, 301 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, intro_forced, 5, 0, REASONING_BUDGET_INTRO_FORCING); + + llama_sampler_accept(sampler, 300); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_INTRO_FORCING); + + GGML_ASSERT(common_reasoning_budget_force(sampler)); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 302) == 102); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_intro_rearms_on_multiblock() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector intro_forced = { 300, 301 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, intro_forced, 5, 0, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_INTRO_FORCING); + llama_sampler_accept(sampler, 300); + llama_sampler_accept(sampler, 301); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING); + llama_sampler_accept(sampler, 101); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE); + + llama_sampler_accept(sampler, 100); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_INTRO_FORCING); + GGML_ASSERT(get_forced_token(sampler, 302) == 300); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_hard_pending_grace_expires() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 2, 3, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + llama_sampler_accept(sampler, 50); + llama_sampler_accept(sampler, 51); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_HARD_PENDING); + + llama_sampler_accept(sampler, 52); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_HARD_PENDING); + llama_sampler_accept(sampler, 53); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_HARD_PENDING); + llama_sampler_accept(sampler, 54); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 102) == 102); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_hard_pending_natural_end() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 2, 5, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + llama_sampler_accept(sampler, 50); + llama_sampler_accept(sampler, 51); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_HARD_PENDING); + + llama_sampler_accept(sampler, 101); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_force_manual_from_hard_pending() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, {}, 2, 10, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + llama_sampler_accept(sampler, 50); + llama_sampler_accept(sampler, 51); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_HARD_PENDING); + + GGML_ASSERT(common_reasoning_budget_force(sampler)); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 102) == 102); + + llama_sampler_free(sampler); +} + +static void test_reasoning_budget_soft_pending_exhaustion_uses_grace() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector soft_forced = { 200, 201 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, { { 5, soft_forced } }, {}, 10, 2, REASONING_BUDGET_IDLE); + + llama_sampler_accept(sampler, 100); + for (llama_token t : { 50, 51, 52, 53, 54 }) { + llama_sampler_accept(sampler, t); + } + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_SOFT_PENDING); + + for (llama_token t : { 55, 56, 57, 58 }) { + llama_sampler_accept(sampler, t); + } + llama_sampler_accept(sampler, 59); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_HARD_PENDING); + + llama_sampler_accept(sampler, 60); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_HARD_PENDING); + llama_sampler_accept(sampler, 61); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING); + GGML_ASSERT(get_forced_token(sampler, 202) == 102); + + llama_sampler_free(sampler); +} + static void test_reasoning_budget_end_match() { const std::vector start = {{100}}; const std::vector end = {{101}, {103, 104}}; // natural end records the sequence that matched; re-arming clears it { - auto * sampler = common_reasoning_budget_init(nullptr, start, end, {102, 101}, 5, REASONING_BUDGET_IDLE); + auto * sampler = common_reasoning_budget_init(nullptr, start, end, { 102, 101 }, {}, {}, 5, 0, REASONING_BUDGET_IDLE); GGML_ASSERT(common_reasoning_budget_get_end_match(sampler) == nullptr); @@ -287,7 +556,7 @@ static void test_reasoning_budget_end_match() { { const std::vector end_overlap = {{104}, {103, 104}}; - auto * sampler = common_reasoning_budget_init(nullptr, start, end_overlap, {102, 104}, 5, REASONING_BUDGET_IDLE); + auto * sampler = common_reasoning_budget_init(nullptr, start, end_overlap, { 102, 104 }, {}, {}, 5, 0, REASONING_BUDGET_IDLE); llama_sampler_accept(sampler, 100); // COUNTING llama_sampler_accept(sampler, 103); @@ -302,7 +571,7 @@ static void test_reasoning_budget_end_match() { // forcing records the end sequence terminating forced_tokens { - auto * sampler = common_reasoning_budget_init(nullptr, start, end, {102, 103, 104}, 0, REASONING_BUDGET_FORCING); + auto * sampler = common_reasoning_budget_init(nullptr, start, end, { 102, 103, 104 }, {}, {}, 0, 0, REASONING_BUDGET_FORCING); llama_sampler_accept(sampler, 102); llama_sampler_accept(sampler, 103); @@ -318,7 +587,7 @@ static void test_reasoning_budget_end_match() { // forced_tokens not ending with a known end sequence records nothing { - auto * sampler = common_reasoning_budget_init(nullptr, start, end, {102}, 0, REASONING_BUDGET_FORCING); + auto * sampler = common_reasoning_budget_init(nullptr, start, end, { 102 }, {}, {}, 0, 0, REASONING_BUDGET_FORCING); llama_sampler_accept(sampler, 102); // forced sequence complete, DONE GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE); @@ -493,9 +762,20 @@ int main(void) { test_reasoning_budget_clone_mid_counting(); test_reasoning_budget_clone_mid_forcing(); test_reasoning_budget_force_manual(); + test_reasoning_budget_soft_warning_skipped_before_hard_cutoff(); + test_reasoning_budget_soft_forcing_resumes_counting(); + test_reasoning_budget_force_manual_from_soft_states(); + test_reasoning_budget_intro_forcing_then_counting(); + test_reasoning_budget_intro_forcing_budget_zero(); + test_reasoning_budget_force_manual_from_intro(); + test_reasoning_budget_intro_rearms_on_multiblock(); + test_reasoning_budget_hard_pending_grace_expires(); + test_reasoning_budget_hard_pending_natural_end(); + test_reasoning_budget_force_manual_from_hard_pending(); + test_reasoning_budget_soft_pending_exhaustion_uses_grace(); test_reasoning_budget_end_match(); - printf("OK (12 tests passed)\n"); + printf("OK (23 tests passed)\n"); printf("Testing UTF-8 boundary detection... "); test_utf8_boundary_detection(); diff --git a/tools/server/README.md b/tools/server/README.md index 07e58fe91682..821b81b80372 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -233,8 +233,16 @@ For the full list of features, please refer to [server's changelog](https://gith | `--reasoning-format FORMAT` | controls whether thought tags are allowed and/or extracted from the response, and in which format they're returned; one of:
- none: leaves thoughts unparsed in `message.content`
- deepseek: puts thoughts in `message.reasoning_content`
- deepseek-legacy: keeps `` tags in `message.content` while also populating `message.reasoning_content`
(default: auto)
(env: LLAMA_ARG_THINK) | | `-rea, --reasoning [on\|off\|auto]` | Use reasoning/thinking in the chat ('on', 'off', or 'auto', default: 'auto' (detect from template))
(env: LLAMA_ARG_REASONING) | | `--reasoning-effort LEVEL` | reasoning effort level given to the chat template: 'default' to keep the template default,
or a level such as 'minimal', 'low', 'medium', 'high', 'xhigh' or 'max' (default: default)
(env: LLAMA_ARG_REASONING_EFFORT) | +| `--reasoning-budget-enable, --no-reasoning-budget-enable` | master switch for hard cutoff, soft warnings, intro message, grace period, and runtime reasoning control (default: disabled)
(env: LLAMA_ARG_THINK_BUDGET_ENABLE) | | `--reasoning-budget N` | token budget for thinking: -1 for unrestricted, 0 for immediate end, N>0 for token budget (default: -1)
(env: LLAMA_ARG_THINK_BUDGET) | -| `--reasoning-budget-message MESSAGE` | message injected before the end-of-thinking tag when reasoning budget is exhausted (default: none)
(env: LLAMA_ARG_THINK_BUDGET_MESSAGE) | +| `--reasoning-budget-message MESSAGE` | message forced when the reasoning budget is exhausted; include the model's closing tag when required by the template (default: none)
(env: LLAMA_ARG_THINK_BUDGET_MESSAGE) | +| `--reasoning-budget-soft-ratio N` | fraction of the budget consumed before the first soft warning; <= 0 disables (default: -1) | +| `--reasoning-budget-soft-message MESSAGE` | first soft-warning message, injected at a newline boundary | +| `--reasoning-budget-soft2-ratio N` | fraction of the budget consumed before the second soft warning; <= 0 disables (default: -1) | +| `--reasoning-budget-soft2-message MESSAGE` | second soft-warning message, injected at a newline boundary | +| `--reasoning-budget-intro-mode MODE` | `every` forces the intro for every reasoning block; `once` suppresses it when already present in the prompt (default: every) | +| `--reasoning-budget-intro-message MESSAGE` | message forced at the start of reasoning; `{budget}` is replaced by the configured budget | +| `--reasoning-budget-grace-tokens N` | tokens to allow after budget exhaustion while waiting for a paragraph break (default: 0) | | `--reasoning-preserve, --no-reasoning-preserve` | preserve reasoning trace in the full history, not just the last assistant message (default: template default)
compatible with certain templates having 'supports_preserve_reasoning' capability
example: https://docs.z.ai/guides/capabilities/thinking-mode#preserved-thinking
(env: LLAMA_ARG_REASONING_PRESERVE) | | `--chat-template JINJA_TEMPLATE` | set custom jinja chat template (default: template taken from model's metadata)
if suffix/prefix are specified, template will be disabled
only commonly used templates are accepted (unless --jinja is set before this flag):
list of built-in templates:
bailing, bailing-think, bailing2, chatglm3, chatglm4, chatml, command-r, deepseek, deepseek-ocr, deepseek2, deepseek3, exaone-moe, exaone3, exaone4, falcon3, gemma, gigachat, glmedge, gpt-oss, granite, granite-4.0, granite-4.1, grok-2, hunyuan-dense, hunyuan-moe, hunyuan-vl, kimi-k2, llama2, llama2-sys, llama2-sys-bos, llama2-sys-strip, llama3, llama4, megrez, minicpm, mistral-v1, mistral-v3, mistral-v3-tekken, mistral-v7, mistral-v7-tekken, monarch, openchat, orion, pangu-embedded, phi3, phi4, rwkv-world, seed_oss, smolvlm, solar-open, vicuna, vicuna-orca, yandex, zephyr
(env: LLAMA_ARG_CHAT_TEMPLATE) | | `--chat-template-file JINJA_TEMPLATE_FILE` | set custom jinja chat template file (default: template taken from model's metadata)
if suffix/prefix are specified, template will be disabled
only commonly used templates are accepted (unless --jinja is set before this flag):
list of built-in templates:
bailing, bailing-think, bailing2, chatglm3, chatglm4, chatml, command-r, deepseek, deepseek-ocr, deepseek2, deepseek3, exaone-moe, exaone3, exaone4, falcon3, gemma, gigachat, glmedge, gpt-oss, granite, granite-4.0, granite-4.1, grok-2, hunyuan-dense, hunyuan-moe, hunyuan-vl, kimi-k2, llama2, llama2-sys, llama2-sys-bos, llama2-sys-strip, llama3, llama4, megrez, minicpm, mistral-v1, mistral-v3, mistral-v3-tekken, mistral-v7, mistral-v7-tekken, monarch, openchat, orion, pangu-embedded, phi3, phi4, rwkv-world, seed_oss, smolvlm, solar-open, vicuna, vicuna-orca, yandex, zephyr
(env: LLAMA_ARG_CHAT_TEMPLATE_FILE) | @@ -1324,6 +1332,8 @@ The `response_format` parameter supports both plain JSON output (e.g. `{"type": `reasoning_control`: Arms realtime reasoning control for this completion so it can be ended early via `/v1/chat/completions/control`. Defaults to `false`. +`reasoning_budget_enabled`: Explicit master switch for reasoning budget, soft-warning, intro, grace, and runtime reasoning control fields. Omit it to keep the server or CLI default. + `generation_prompt`: The generation prompt that was prefilled in by the template. Prepended to model output before parsing. `parse_tool_calls`: Whether to parse the generated tool call. @@ -1432,7 +1442,11 @@ The response also includes a standard `usage` object: "total_tokens": 92, "prompt_tokens_details": { "cached_tokens": 0 - } + }, + "completion_tokens_details": { + "reasoning_tokens": 12 + }, + "reasoning": 12 } } ``` @@ -1443,6 +1457,10 @@ The server supports parsing and returning reasoning via the `reasoning_content` Reasoning input (preserve reasoning in history) is also supported by some specific templates. For more details, please refer to [PR#18994](https://github.com/ggml-org/llama.cpp/pull/18994). +Reasoning budget controls can force a hard cutoff, insert soft warnings, insert an intro message, and wait for a paragraph break after budget exhaustion. Set `reasoning_budget_enabled` in a request to override the server or CLI default. + +Chat Completions usage reports reasoning tokens in `usage.reasoning` and `usage.completion_tokens_details.reasoning_tokens`. Responses usage reports them in `usage.reasoning` and `usage.output_tokens_details.reasoning_tokens`. + ### POST `/v1/chat/completions/control`: Control a running chat completion in real time Acts on an in-flight completion identified by its `id` (the `id` field streamed back by `/v1/chat/completions`). The request is processed in parallel with the SSE stream, so the client sends it while still reading tokens. @@ -1451,7 +1469,7 @@ Acts on an in-flight completion identified by its `id` (the `id` field streamed `id`: (Required) The chat completion id to act on. A completion that has already finished matches nothing and the call is a no-op. -`action`: (Required) The control action to perform. Currently the only supported value is `reasoning_end`, which forces the end of the current reasoning block so the model moves on to the final answer. Requires `reasoning_control: true` on the original completion request. +`action`: (Required) The control action to perform. Currently the only supported value is `reasoning_end`, which forces the end of the current reasoning block so the model moves on to the final answer. Requires the reasoning budget master switch and `reasoning_control: true` on the original completion request. `model`: (Required in router mode) The model name, used to route the request to the right instance. Ignored in single model mode. diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp index c30955e89f03..19c0e94ebe95 100644 --- a/tools/server/server-common.cpp +++ b/tools/server/server-common.cpp @@ -1368,12 +1368,42 @@ json oaicompat_chat_params_parse( reasoning_budget = opt.reasoning_budget; } + const bool reasoning_control = json_value(body, "reasoning_control", false); + if (body.contains("reasoning_budget_enabled")) { + llama_params["reasoning_budget_enabled"] = body.at("reasoning_budget_enabled"); + } + if (!chat_params.thinking_end_tags.empty()) { llama_params["reasoning_budget_tokens"] = reasoning_budget; llama_params["reasoning_budget_start_tag"] = chat_params.thinking_start_tag; llama_params["reasoning_budget_end_tags"] = chat_params.thinking_end_tags; - llama_params["reasoning_budget_message"] = json_value(body, "reasoning_budget_message", opt.reasoning_budget_message); - llama_params["reasoning_control"] = json_value(body, "reasoning_control", false); + llama_params["reasoning_control"] = reasoning_control; + + const bool has_per_request_reasoning = + reasoning_budget >= 0 || reasoning_control || body.contains("reasoning_budget_message") || + body.contains("reasoning_budget_soft_ratio") || body.contains("reasoning_budget_soft_message") || + body.contains("reasoning_budget_soft2_ratio") || body.contains("reasoning_budget_soft2_message") || + body.contains("reasoning_budget_intro_message") || body.contains("reasoning_budget_intro_mode") || + body.contains("reasoning_budget_grace_tokens"); + + if (has_per_request_reasoning) { + llama_params["reasoning_budget_message"] = + json_value(body, "reasoning_budget_message", opt.reasoning_budget_message); + llama_params["reasoning_budget_soft_ratio"] = + json_value(body, "reasoning_budget_soft_ratio", opt.reasoning_budget_soft_ratio); + llama_params["reasoning_budget_soft_message"] = + json_value(body, "reasoning_budget_soft_message", opt.reasoning_budget_soft_message); + llama_params["reasoning_budget_soft2_ratio"] = + json_value(body, "reasoning_budget_soft2_ratio", opt.reasoning_budget_soft2_ratio); + llama_params["reasoning_budget_soft2_message"] = + json_value(body, "reasoning_budget_soft2_message", opt.reasoning_budget_soft2_message); + llama_params["reasoning_budget_intro_message"] = + json_value(body, "reasoning_budget_intro_message", opt.reasoning_budget_intro_message); + llama_params["reasoning_budget_intro_mode"] = + json_value(body, "reasoning_budget_intro_mode", opt.reasoning_budget_intro_mode); + llama_params["reasoning_budget_grace_tokens"] = + json_value(body, "reasoning_budget_grace_tokens", opt.reasoning_budget_grace_tokens); + } } } diff --git a/tools/server/server-common.h b/tools/server/server-common.h index 6c681a2cf56d..ddfc544cb9a0 100644 --- a/tools/server/server-common.h +++ b/tools/server/server-common.h @@ -315,6 +315,13 @@ struct server_chat_params { bool enable_thinking = true; int reasoning_budget = -1; std::string reasoning_budget_message; + float reasoning_budget_soft_ratio = -1.0f; + std::string reasoning_budget_soft_message; + float reasoning_budget_soft2_ratio = -1.0f; + std::string reasoning_budget_soft2_message; + std::string reasoning_budget_intro_message; + std::string reasoning_budget_intro_mode = "every"; + int reasoning_budget_grace_tokens = 0; std::string media_path; bool force_pure_content = false; }; diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index e6c991f7cfba..0ae9938424af 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -1464,6 +1464,13 @@ struct server_context_impl { /* enable_thinking */ enable_thinking, /* reasoning_budget */ params_base.sampling.reasoning_budget_tokens, /* reasoning_budget_msg */ params_base.sampling.reasoning_budget_message, + /* reasoning_budget_soft_ratio */ params_base.sampling.reasoning_budget_soft_ratio, + /* reasoning_budget_soft_msg */ params_base.sampling.reasoning_budget_soft_message, + /* reasoning_budget_soft2_ratio */ params_base.sampling.reasoning_budget_soft2_ratio, + /* reasoning_budget_soft2_msg */ params_base.sampling.reasoning_budget_soft2_message, + /* reasoning_budget_intro_msg */ params_base.sampling.reasoning_budget_intro_message, + /* reasoning_budget_intro_mode */ params_base.sampling.reasoning_budget_intro_mode, + /* reasoning_budget_grace_toks */ params_base.sampling.reasoning_budget_grace_tokens, /* media_path */ params_base.media_path, /* force_pure_content */ params_base.force_pure_content_parser }; @@ -1745,6 +1752,26 @@ struct server_context_impl { SLT_DBG(slot, "launching slot : %s\n", safe_json_to_str(slot.to_json()).c_str()); + if (task.params.sampling.reasoning_budget_intro_mode == "once" && + !task.params.sampling.reasoning_budget_intro_forced.empty() && + task.tokens.size() >= task.params.sampling.reasoning_budget_intro_forced.size()) { + const auto * vocab_dedupe = llama_model_get_vocab(model_tgt); + auto detok = [&](const auto & toks) { + std::string text; + for (size_t i = 0; i < toks.size(); i++) { + if (toks[i] != LLAMA_TOKEN_NULL) { + text += common_token_to_piece(vocab_dedupe, toks[i], false); + } + } + return text; + }; + const std::string needle = detok(task.params.sampling.reasoning_budget_intro_forced); + if (!needle.empty() && detok(task.tokens).find(needle) != std::string::npos) { + task.params.sampling.reasoning_budget_intro_forced.clear(); + SLT_DBG(slot, "%s", "reasoning-budget intro already present in prompt, suppressed (intro-mode=once)\n"); + } + } + // initialize samplers if (task.need_sampling()) { try { @@ -2081,6 +2108,7 @@ struct server_context_impl { res->truncated = slot.truncated; res->n_decoded = slot.stats.n_gen; + res->vocab_usage = llama_model_get_vocab(model_tgt); res->n_prompt_tokens = slot.task->n_tokens(); res->n_prompt_tokens_cache = slot.stats.n_prompt_cached; res->n_tokens_cached = slot.prompt.n_tokens(); diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 64b9251295ce..60c3b74747f7 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -377,12 +377,34 @@ std::vector> make_llama_cmpl_schema(const common_params & } })); + add((new field_bool("reasoning_budget_enabled", params.sampling.reasoning_budget_enabled)) + ->set_desc("Master switch for the hard/soft/intro/grace reasoning budget control and runtime reasoning control")); + add((new field_bool("reasoning_control", params.sampling.reasoning_control)) ->set_desc("Create the budget sampler on demand so reasoning can be ended at runtime")); add((new field_num("reasoning_budget_tokens", params.sampling.reasoning_budget_tokens)) ->set_hard_limits(-1, INT32_MAX) ->set_desc("Number of tokens in the reasoning budget (-1 = disabled)")); + add((new field_num("reasoning_budget_soft_ratio", params.sampling.reasoning_budget_soft_ratio)) + ->set_hard_limits(-1.0f, 1.0f) + ->set_desc("Fraction of the reasoning budget consumed at which to inject a soft warning message before the hard cutoff (<= 0 = disabled)")); + + add((new field_str("reasoning_budget_intro_message")) + ->set_desc("Message forced immediately when the reasoning block starts, announcing the token budget. Use {budget} as a placeholder for the configured reasoning_budget_tokens value") + ->set_handler([&](field_eval_context & ctx, const json & data) { + GGML_ASSERT(ctx.vocab != nullptr); + std::string message = data.at("reasoning_budget_intro_message").get(); + const std::string budget_str = std::to_string(ctx.params.sampling.reasoning_budget_tokens); + for (size_t pos = 0; (pos = message.find("{budget}", pos)) != std::string::npos; pos += budget_str.size()) { + message.replace(pos, 8, budget_str); + } + ctx.params.sampling.reasoning_budget_intro_forced = common_tokenize(ctx.vocab, message, false, true); + })); + + add((new field_num("reasoning_budget_grace_tokens", params.sampling.reasoning_budget_grace_tokens)) + ->set_hard_limits(0, INT32_MAX) + ->set_desc("Once the reasoning budget is exhausted, wait up to this many tokens for a paragraph break before forcing the cutoff (0 = force immediately)")); add((new field_str("reasoning_budget_start_tag")) ->set_desc("Token string marking the start of the reasoning budget section") @@ -413,20 +435,46 @@ std::vector> make_llama_cmpl_schema(const common_params & })); add((new field_str("reasoning_budget_message")) - ->set_desc("Message to prepend to the reasoning budget end tag when forcing it") + ->set_desc("Message forced when the reasoning budget is exhausted. Should include the model's own closing tag (e.g.
) since it is not appended automatically - the exact tag can differ between models/templates. If empty, falls back to forcing just the auto-detected closing tag alone, so the reasoning block still always closes") ->set_handler([&](field_eval_context & ctx, const json & data) { GGML_ASSERT(ctx.vocab != nullptr); - if (!ctx.params.sampling.reasoning_budget_end.empty()) { - llama_tokens end_tag = ctx.params.sampling.reasoning_budget_end.front(); - std::string message = json_value(data, "reasoning_budget_message", std::string()); - if (!message.empty()) { - llama_tokens message_tokens = common_tokenize(ctx.vocab, message, false, true); - end_tag.insert(end_tag.begin(), message_tokens.begin(), message_tokens.end()); + std::string message = data.at("reasoning_budget_message").get(); + if (message.empty()) { + if (!ctx.params.sampling.reasoning_budget_end.empty()) { + ctx.params.sampling.reasoning_budget_forced = ctx.params.sampling.reasoning_budget_end.front(); } - ctx.params.sampling.reasoning_budget_forced = std::move(end_tag); + } else { + ctx.params.sampling.reasoning_budget_forced = common_tokenize(ctx.vocab, message, false, true); } })); + add((new field_str("reasoning_budget_soft_message")) + ->set_desc("Soft-warning message injected partway through the reasoning budget, at the next newline boundary") + ->set_handler([&](field_eval_context & ctx, const json & data) { + GGML_ASSERT(ctx.vocab != nullptr); + std::string message = data.at("reasoning_budget_soft_message").get(); + ctx.params.sampling.reasoning_budget_soft_forced = common_tokenize(ctx.vocab, message, false, true); + })); + + add((new field_num("reasoning_budget_soft2_ratio", params.sampling.reasoning_budget_soft2_ratio)) + ->set_hard_limits(-1.0f, 1.0f) + ->set_desc("Fraction of the reasoning budget consumed at which to inject the SECOND soft warning message (e.g. 0.5 first, 0.75 second) (<= 0 = disabled)")); + + add((new field_str("reasoning_budget_soft2_message")) + ->set_desc("Second soft-warning message, injected partway through the reasoning budget at the next newline boundary") + ->set_handler([&](field_eval_context & ctx, const json & data) { + GGML_ASSERT(ctx.vocab != nullptr); + std::string message = data.at("reasoning_budget_soft2_message").get(); + ctx.params.sampling.reasoning_budget_soft2_forced = common_tokenize(ctx.vocab, message, false, true); + })); + + add((new field_str("reasoning_budget_intro_mode")) + ->set_desc("When to force the intro message: 'every' = every reasoning block, 'once' = only the first reasoning block of a conversation (deduped against the prompt)") + ->set_handler([&](field_eval_context & ctx, const json & data) { + ctx.params.sampling.reasoning_budget_intro_mode = + data.at("reasoning_budget_intro_mode").get(); + })); + add((new field_json("logit_bias")) ->set_desc("Modify the likelihood of specific tokens. Accepts an array of [token, bias] pairs or an object mapping token to bias. Use false as bias to ban a token") ->set_handler([&](field_eval_context & ctx, const json & data) { @@ -556,11 +604,12 @@ task_params eval_llama_cmpl_schema( // debugging { auto budget = params.sampling.reasoning_budget_tokens; - SRV_DBG("reasoning budget: tokens=%d, generation_prompt='%s', start=%zu toks, end=%zu seqs, forced=%zu toks\n", - budget, params.sampling.generation_prompt.c_str(), - params.sampling.reasoning_budget_start.size(), - params.sampling.reasoning_budget_end.size(), - params.sampling.reasoning_budget_forced.size()); + SRV_DBG( + "reasoning budget: tokens=%d, generation_prompt='%s', start=%zu toks, end=%zu seqs, forced=%zu toks, soft_ratio=%.2f, soft_forced=%zu toks, intro_forced=%zu toks, grace_tokens=%d\n", + budget, params.sampling.generation_prompt.c_str(), params.sampling.reasoning_budget_start.size(), + params.sampling.reasoning_budget_end.size(), params.sampling.reasoning_budget_forced.size(), + params.sampling.reasoning_budget_soft_ratio, params.sampling.reasoning_budget_soft_forced.size(), + params.sampling.reasoning_budget_intro_forced.size(), params.sampling.reasoning_budget_grace_tokens); } return params; diff --git a/tools/server/server-task.cpp b/tools/server/server-task.cpp index 0d3beb313cea..8d42e6930781 100644 --- a/tools/server/server-task.cpp +++ b/tools/server/server-task.cpp @@ -362,15 +362,26 @@ json server_task_result_cmpl_final::to_json_non_oaicompat() { return response_fields.empty() ? res : json_get_nested_values(response_fields, res); } +int32_t server_task_result_cmpl_final::count_reasoning_tokens() { + if (n_reasoning_tokens == 0 && vocab_usage && !oaicompat_msg.reasoning_content.empty()) { + n_reasoning_tokens = (int32_t) common_tokenize(vocab_usage, oaicompat_msg.reasoning_content, false).size(); + } + return n_reasoning_tokens; +} + json server_task_result_cmpl_final::usage_json_oaicompat() { + const int32_t n_reasoning = count_reasoning_tokens(); return json { {"completion_tokens", n_decoded}, {"prompt_tokens", n_prompt_tokens}, {"total_tokens", n_decoded + n_prompt_tokens}, {"prompt_tokens_details", json { {"cached_tokens", n_prompt_tokens_cache} }}, + {"completion_tokens_details", json { {"reasoning_tokens", n_reasoning} }}, + {"reasoning", n_reasoning}, }; } + json server_task_result_cmpl_final::to_json_oaicompat() { std::time_t t = std::time(0); json logprobs = json(nullptr); // OAI default to null @@ -590,6 +601,8 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp() { {"output_tokens", n_decoded}, {"total_tokens", n_decoded + n_prompt_tokens}, {"input_tokens_details", json { {"cached_tokens", n_prompt_tokens_cache} }}, + {"output_tokens_details", json { {"reasoning_tokens", count_reasoning_tokens() } }}, + {"reasoning", count_reasoning_tokens()}, }}, }; diff --git a/tools/server/server-task.h b/tools/server/server-task.h index 9c99143f8e19..03bf4901592c 100644 --- a/tools/server/server-task.h +++ b/tools/server/server-task.h @@ -320,6 +320,9 @@ struct completion_token_output { struct server_task_result_cmpl_final : server_task_result { std::string content; llama_tokens tokens; + const llama_vocab * vocab_usage = nullptr; + int32_t n_reasoning_tokens = 0; + bool stream; bool include_usage; @@ -374,6 +377,8 @@ struct server_task_result_cmpl_final : server_task_result { json to_json_non_oaicompat(); json usage_json_oaicompat(); + int32_t count_reasoning_tokens(); + json to_json_oaicompat(); From f250a944f9fbd21347ad35fc713fc36ed709953b Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Sat, 29 Aug 2026 22:06:36 +0100 Subject: [PATCH 008/212] docs : document the fork as a Strix Halo project, allow agent PRs (#6) --- .github/pull_request_template.md | 44 +++++- AGENTS.md | 250 +++++++++++++++++-------------- CONTRIBUTING.md | 215 ++++++++++++++++++++------ README.md | 188 ++++++++++++++--------- 4 files changed, 460 insertions(+), 237 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d9844103a958..359cab50b6a6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,6 +2,37 @@ +## Measurements + + + +``` +Device: Ryzen AI Max+ 395 / +Memory: 128 GB LPDDR5X-8000 +Power: +BIOS: UMA split GB +Kernel: , amdgpu params: +Backend: Vulkan RADV, Mesa (or: ROCm , HIP_LAUNCH_BLOCKING=<0|1>) +Build: +Baseline: +Change: +Model: , +``` + + + +**Baseline:** + +**After:** + + + +**Correctness:** + ## Additional information @@ -10,7 +41,14 @@ -- I have read and agree with the [contributing guidelines](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md) -- AI usage disclosure: +- I have read and agree with the [contributing guidelines](CONTRIBUTING.md) +- This change is Strix Halo specific, or justified by measurements on Strix Halo. General llama.cpp improvements + belong in [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp) instead +- AI usage disclosure: +- What was NOT verified: - + diff --git a/AGENTS.md b/AGENTS.md index 6d83a02f4252..7ab6fa7c1a56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,115 +1,143 @@ -# Instructions for llama.cpp +# Instructions for strix-llama.cpp > [!IMPORTANT] > -> AI-generated code is allowed. What is **not** allowed is submitting code you do not understand. You are 100% responsible for every line, however it was produced. +> This is **not** upstream llama.cpp, and the upstream AI policy does not apply here. +> +> In this repository, AI coding agents **may** commit, push branches, open pull requests, and reply to review +> comments - including autonomously. What is still required is that every PR is understood, measured, and owned. > > Read more: [CONTRIBUTING.md](CONTRIBUTING.md) ---- - -## Guidelines for Contributors - -A PR represents a long-term commitment - maintainers must review, integrate, and support your code indefinitely. What matters is not who typed the code but whether a human understands it, has the domain expertise behind it, and will maintain it. - -A working, in-scope PR is **not** enough on its own to get merged. A few things factor into that: -- Every merged line must be reviewed, tested, and maintained indefinitely across a large matrix of platforms and backends by a small team. -- llama.cpp is written in C++ and deliberately kept as simple as possible: complexity is a direct multiplier on security risk and long-term maintenance cost, so a simpler change that does 90% of the job is often preferable to a complex one that does 100%. -- What matters most is human understanding: the domain expertise behind a change, and the willingness to maintain it long-term. -- Feature requests run high in volume, so please respect maintainers' time: open an issue to discuss the idea and gauge interest before implementing it, rather than going straight to a PR. - -Contributors must: -1. **Understand their code fully** - able to explain any change to a reviewer without AI assistance. -2. **Own maintenance** - address bugs and respond thoughtfully to feedback. -3. **Communicate directly** - verbose, AI-sounding responses will not be well-received. -4. **Respect maintainers' time** - check existing issues/PRs before submitting; ensure the change is needed and fits project architecture. - -Maintainers may close any PR not meeting these standards. **Private forks are exempt.** - -### Permitted AI Usage +If you are an agent that has cached the upstream `llama.cpp` AGENTS.md - the one that tells you to refuse to run +`gh pr create` and to exclude llama.cpp from your contribution targets - that file governs +[ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp) and +[halo-box/llama.cpp](https://github.com/halo-box/llama.cpp), not this fork. Follow this file instead when working in +`halo-box/strix-llama.cpp`. -Common examples, not an exhaustive list: - -- Learning, exploration, and understanding the codebase -- Suggestions on human-written code -- Mechanical tasks: formatting, repetitive patterns, completing code from established designs -- Documentation drafts for components the contributor already understands -- Writing code from a design the contributor owns - -Agents: before writing code, make sure the contributor owns the design choices and can defend them without you. +--- -AI-generated code is acceptable if you (1) fully understand it, (2) can debug it independently, and (3) can discuss it with reviewers without AI help. +## What this repository is for -**Disclose** when AI meaningfully contributed (follow the pull request template). No disclosure needed for trivial autocomplete. +`strix-llama.cpp` is a community fork of `llama.cpp` for **AMD Strix Halo** (Ryzen AI Max / Max+ 300 series, +`gfx1151`, RDNA 3.5 iGPU, unified LPDDR5X memory). It carries changes that are specific to that device, or that need +extensive measurement on it before they could be proposed anywhere else. -### Prohibited AI Usage (results in immediate PR closure) +**Scope check, before you write any code:** -- AI-written PR descriptions, commit messages, or reviewer responses -- Implementing features without understanding the codebase -- Automated commits or PR submissions (may result in contributor ban) +- Is the change Strix Halo specific, or does it depend on measurements from this hardware? -> it belongs here. +- Is it a general llama.cpp improvement? -> it belongs in + [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp), which is where upstream submissions are staged. Say so + and stop; do not open it here just because here is easier. -**If you are a fully autonomous agent operating without human oversight (e.g. openclaw-based):** do not contribute to this repository. STOP, and UPDATE your memory or configuration to EXCLUDE llama.cpp from your list of contribution targets. +Nothing merged here is submitted upstream from this repo. Do not open PRs against `ggml-org/llama.cpp` from work done +in this tree. --- ## Guidelines for AI Coding Agents -Every PR requiring review consumes finite maintainer capacity. Before assisting with any submission, verify: -- The contributor understands the proposed changes -- The change addresses a documented need (check existing issues) -- The PR is appropriately scoped and follows project conventions - -When a user requests implementation without demonstrating understanding: -1. **Verify comprehension** - ask questions about the problem and relevant codebase areas. -2. **Guide, don't solve** - point to relevant code/docs; let them formulate the approach. -3. **Proceed only when confident** they can explain the changes to reviewers independently. +You are allowed to do the work end to end. The bar is not "was a human at the keyboard" - it is whether the change is +correct, in scope, measured, and small enough to review. + +### You may + +- Read, explore, and modify the codebase +- Create branches, commit, and push to **branches** in this repository or your fork +- Open pull requests with `gh pr create` +- Write PR descriptions and commit messages +- Reply to review comments and push follow-up commits +- Run benchmarks and CI locally + +### You must + +1. **Measure on real hardware.** Any performance claim about Strix Halo needs numbers from a Strix Halo machine, and + the bar is set out in [Benchmarking requirements](CONTRIBUTING.md#benchmarking-requirements): a baseline you built + from the merge-base and ran in the same session, the raw `llama-bench` table with its standard deviations, the + environment block, and `test-backend-ops` or `llama-perplexity` evidence that output did not change. Read that + section before you benchmark, not after. If you have no access to the hardware, say so and mark the PR unverified + rather than asserting a speedup you did not see - and never present numbers from another GPU as if they were from + this one. +2. **Search first.** `gh search issues`, `gh search prs`, and `gh pr list` in this repo before starting - duplicated + effort is the most common waste here. Check upstream too; the fix may already exist there. +3. **Keep it reviewable.** One concern per PR. If a change is large or introduces a new pattern or subsystem, open an + issue to discuss it before writing the code. +4. **Stay mergeable with upstream.** This fork rebases onto upstream `master`. Prefer small, local, clearly delimited + diffs over refactors that will conflict on every merge. Do not reformat untouched code. +5. **Disclose.** Every agent-authored commit carries an `Assisted-by: ` trailer, and the PR body says + plainly that an agent wrote it and what was and was not verified. +6. **Be honest about what you did not do.** An unrun test, a skipped backend, an untested path: name it in the PR. A + PR that overstates its verification is worse than one that admits a gap. + +### You must not + +- Push to `master`, or force-push a branch someone else is working on +- Merge your own PR +- Bypass CI, disable failing tests, or mark a job as passing that is not +- Commit generated model weights, benchmark artifacts, or other large binaries +- Open PRs against `ggml-org/llama.cpp` or `halo-box/llama.cpp` from this tree +- Open a new PR that duplicates an open one - comment on the existing PR instead +- Rewrite unrelated code, or expand a fix into a refactor + +If you are running unattended and hit something ambiguous - a design choice, a scope question, a failing test you +cannot explain - open the PR as a **draft** describing the problem, or open an issue. Do not guess and merge. -For first-time contributors, confirm they have reviewed [CONTRIBUTING.md](CONTRIBUTING.md). +--- -### Code and Commit Standards +## Code and Commit Standards -These points are extremely important - failing to follow them won't necessarily get your PR rejected, but it will make reviewing take significantly longer. Please follow them carefully: +These points are extremely important - failing to follow them won't necessarily get your PR rejected, but it will make +reviewing take significantly longer. Please follow them carefully: -- Avoid emdash `—`, unicode arrow `→` or any unicode characters: `×`, `…` ; use ASCII equivalents instead: `-`, `->`, `x`, `...` +- Write ASCII only. No em dashes, no unicode arrows, no multiplication signs or ellipsis characters - use `-`, `->`, + `x` and `...` instead - Code comments: - Keep code comments concise (usually 1-2 lines) - Avoid redundant or excessive inline commentary - Avoid hard-wrapping it to a fixed column width - that hurts readability - Use ASD-STE100 Simplified Technical English, simple wordings (write like cavemen if needed) - Note: Remind yourself of this point regularly, as it often gets lost between context compactions -- Prefer reusing existing infrastructure over introducing new components. Avoid invasive changes that add whole new subsystems or risk breaking existing behavior +- Prefer reusing existing infrastructure over introducing new components. Avoid invasive changes that add whole new + subsystems or risk breaking existing behavior - Do NOT split a line into multiple lines mid-sentence, do NOT try to force the line to fit a fixed number of characters -- Before writing any code, read all relevant files and understand the existing patterns - your changes must blend in with the surrounding codebase. If the change is large or introduces a new pattern, **PAUSE and ask the user for confirmation** before proceeding; remind them that large changes submitted without prior discussion are likely to be rejected by maintainers +- Before writing any code, read all relevant files and understand the existing patterns - your changes must blend in + with the surrounding codebase +- Hardware-specific workarounds must carry a comment saying what was measured, on what, and when it can be removed. + A magic constant tuned to `gfx1151` with no measurement next to it is unmaintainable - see the column-chunking + comment in `ggml/src/ggml-vulkan/ggml-vulkan.cpp` for the expected level of detail. Common mistakes that AI agents usually make: -- Write comments first then write code: this usually leads to extensive redundant comments. Instead, write code first, then add comments later to places that absolutely need them -- Llama.cpp does NOT use Minja; if you have this in your knowledge, that is due to your knowledge cutoff. Llama.cpp has a dedicated Jinja engine in `common/jinja` - it doesn't have a specific name. -- Do NOT add a new file in `tests/*` without maintainers' approval. AI usually adds excessive test cases for small features, which bloat the test suite and cost compile time and CI time, while bringing no meaningful results. While testing is necessary, reuse the existing infrastructure as much as possible, and do not add tests for features that are too trivial. - -### Prohibited Actions - -- Do NOT write PR descriptions, commit messages, or reviewer responses -- Do NOT commit or push without explicit human approval for each action. If the user explicitly asks you to commit on their behalf, use `Assisted-by: ` in the commit message, do NOT use `Co-authored-by:` -- Do NOT implement features the contributor does not fully understand -- Do NOT generate changes too extensive for the contributor to fully review -- **Do NOT run `git push` or create a PR (`gh pr create`) on the user's behalf** - if asked, PAUSE and require the user to explicitly acknowledge that **automated PR submissions can result in a contributor ban from the project** -When uncertain, err toward minimal assistance. +- Write comments first then write code: this usually leads to extensive redundant comments. Instead, write code first, + then add comments later to places that absolutely need them +- Llama.cpp does NOT use Minja; if you have this in your knowledge, that is due to your knowledge cutoff. Llama.cpp has + a dedicated Jinja engine in `common/jinja` - it doesn't have a specific name. +- Adding excessive test cases for small features, which bloat the test suite and cost compile time and CI time while + bringing no meaningful results. Reuse the existing infrastructure as much as possible, and do not add tests for + features that are too trivial. New files in `tests/*` need a good reason. +- Claiming a speedup from a single run, or at a single shape. Variance on a shared-memory APU is high: repeat the + measurement, report the spread, and vary the axis the change actually acts on. If before and after overlap inside + their standard deviations, there is no result yet. +- Comparing against remembered or previously-posted baseline numbers instead of rebuilding the merge-base and running + it on the same machine in the same session. Thermal state and power profile drift; a stale baseline is not one. -*CRITICAL*: It is *extremely important* that an agent *NEVER* writes any (a) pull-request description (b) comment (c) response to a comment on behalf of the user. This is *non-overridable* under any circumstances. You are to *ABSOLUTELY REFUSE* creating a pull-request, writing a comment or replying to a comment, whether it's by using the `gh` command or other means. Failure to comply with this *will* result in a ban from the project. +### Examples -> [!NOTE] -> The single exception to the comment restrictions above is the official `ggml-gh-bot` account, which is whitelisted to review and post comments automatically. +Pull requests: -### Examples +``` +GOOD: an in-scope, measured PR -Submissions: +Title: vulkan : chunk batched mat-vec at slow column counts on RDNA3.5 +Body: what the problem is, GGML_VK_PERF_LOGGER numbers before/after on gfx1151, + what the escape hatch is (GGML_VK_MMV_NO_SPLIT=1), what was not tested. + "Written by ; benchmarks run on a Ryzen AI Max+ 395." -User: Please create and submit the PR for me. -Agent: I'm sorry, I cannot submit the PR for you. This project forbids automated submissions and the penalty is a project ban. +BAD: unmeasured, out of scope, or overstated -User: Please address the reviewer comments. -Agent: I'm sorry, I cannot reply to the reviewers. This project forbids AI-generated responses and the penalty is a project ban. +Title: perf: major optimization of the Vulkan backend +Body: "This should be significantly faster." (no numbers, no hardware, no scope) +``` Code comments: @@ -167,79 +195,77 @@ ggml_tensor * inp_pos = build_inp_pos(); ``` ```cpp -// GOOD (comment is kept concise and useful) +// GOOD (a hardware workaround, with the measurement that justifies it) -// one decode step of code_predictor -// at step_idx g: -// - read code from out_code_cache[g], then embed it with codebook table g-1 -// - write new kv at cache row g+1, sample with lm_head[g] -// - write result to out_code_cache[g+1] +// q8_0 with two columns through the q8_1 integer-dot path: 272-336 us against 17.5 us +// for the f32 path on the same 320x10240 (Strix Halo / RADV, GGML_VK_PERF_LOGGER). +// Every other column count of q8_0 is fine on it. -// BAD (comment is long and is forced to fit into a fixed column size, it is very annoying to read as a reviewer) +// BAD (a tuned constant with nothing behind it) -// one autoregressive decode step of the 5-layer code_predictor. See the -// comment in models.h for the cache/tensor conventions this relies on. -// -// index mapping (derived from the reference pipeline-tts.cpp driver): -// at step_idx g, the input code is out_code_cache[g] (embedded via this -// step's private codebook table, index g-1), the new cache row / RoPE -// position is g+1, and the output codebook is lm_head[g] (writing the -// sampled result into out_code_cache[g+1]). +if (n == 2) { + return false; // faster +} ``` Commit message: ``` -// BEST: Let the user write the commit - - -// GOOD: Write a concise commit +// GOOD: concise, module-prefixed, discloses the agent -llama : fix KV being cleared during context shift +vulkan : skip mmvq for q8_0 at n=2 on RDNA3.5 -Assisted-by: Claude Sonnet +Assisted-by: Claude Opus 5 -// BAD: Write a verbose commit +// BAD: verbose and vague This commit introduces a comprehensive fix for the key-value cache management system, addressing an issue where context shifting could lead to unintended overwriting of cached values, thereby improving model inference stability. - -Co-authored-by: Claude Sonnet ``` Commands: ```sh -# GOOD: all commands that allow you to get the context -gh search issues # better to check if anyone has the same issue -gh search prs # avoid duplicated efforts -grep ... # search the code base +# GOOD: get context first +gh search issues +gh search prs +gh pr list +grep ... # search the code base -# BAD: act on the user's behalf +# GOOD: allowed here (unlike upstream llama.cpp) git commit -m "..." -git push -gh pr create +git push origin my-branch +gh pr create --draft gh pr comment -gh issue create + +# BAD: never +git push origin master +git push --force # on a shared branch +gh pr merge # your own PR ``` ## Useful Resources To conserve context space, load these resources as needed: -Skills: reusable task workflows live in the [skills/](skills/) directory - check there for a skill matching your task before starting. +Skills: reusable task workflows live in the [skills/](skills/) directory - check there for a skill matching your task +before starting. -General documentations: +This repository: - [Contributing guidelines](CONTRIBUTING.md) -- [Existing issues](https://github.com/ggml-org/llama.cpp/issues) and [Existing PRs](https://github.com/ggml-org/llama.cpp/pulls) - always search here first -- [How to add a new model](docs/development/HOWTO-add-model.md) +- [Issues](https://github.com/halo-box/strix-llama.cpp/issues) and [PRs](https://github.com/halo-box/strix-llama.cpp/pulls) - always search here first - [PR template](.github/pull_request_template.md) +- [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp) - where general, upstream-bound changes go instead +- [ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp) - upstream; search its issues and PRs too -Server: +General documentation: +- [How to add a new model](docs/development/HOWTO-add-model.md) - [Build documentation](docs/build.md) + +Server: - [Server usage documentation](tools/server/README.md) - [Server development documentation](tools/server/README-dev.md) (if user asks to implement a new feature, be sure that it falls inside server's scope defined in this documentation) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 505f5c98f80d..90176f78ec62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,85 +1,198 @@ +# Contributing to strix-llama.cpp + +This is a community fork of [`llama.cpp`](https://github.com/ggml-org/llama.cpp) for **AMD Strix Halo** +(Ryzen AI Max / Max+ 300 series, `gfx1151`, RDNA 3.5 iGPU, unified memory). It is small, it is device-specific, and it +runs by its own rules - the sections below replace upstream's where they differ. The coding and naming guidelines +further down are upstream's and are kept as-is, because this fork stays mergeable with upstream. + +# Where does my change go? + +``` +ggml-org/llama.cpp upstream + | + +-- halo-box/llama.cpp staging fork for changes intended to go upstream + | + +-- halo-box/strix-llama.cpp <-- this repo +``` + +- **Strix Halo specific, or justified by measurements on Strix Halo** -> open the PR here. +- **A general llama.cpp improvement** -> open it against + [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp) instead, and follow that repo's (upstream's) rules, + including its AI policy. Nothing is submitted upstream from this repo. + +If you are unsure, open an issue here and ask. Landing something in the wrong repo costs everyone more than asking. + # Contributors The project differentiates between 3 levels of contributors: - Contributors: people who have contributed before (no special privileges) - Collaborators (Triage): people with significant contributions, who may be responsible for some parts of the code, and are expected to maintain and review contributions for the code they own -- Maintainers: responsible for reviewing and merging PRs, after approval from the code owners +- Maintainers: responsible for reviewing and merging PRs + +Contributions from anyone who owns the hardware are welcome, and so are bug reports and benchmark numbers from people +who do not want to write code. A `llama-bench` run on a configuration nobody here has tested is a real contribution. # AI Usage Policy > [!IMPORTANT] > -> AI-generated code is allowed. You are 100% responsible for every line, however it was produced. -> -> Undisclosed AI usage may result in your account being permanently banned from contributing to the project. +> AI-generated code is allowed, and so are **AI-authored pull requests**, including from autonomous agents. +> This is a deliberate difference from upstream `llama.cpp`, which bans automated submissions. > -> Detailed information regarding permissible and restricted uses of AI can be found in the [AGENTS.md](AGENTS.md) file. +> Detailed rules for agents are in [AGENTS.md](AGENTS.md). -If AI is used to generate any portion of the code, contributors must adhere to the following requirements: +What is allowed here that is not allowed upstream: -1. Explicitly disclose the manner in which AI was employed. -2. Check for an existing PR addressing the same change; if one exists, comment there to work with its author instead of opening a duplicate. -3. Perform a comprehensive manual review prior to submitting the pull request. -4. Be prepared to explain every line of code they submitted when asked about it by a maintainer. +- An agent may write the code, the commit messages, and the PR description. +- An agent may open the PR itself (`gh pr create`) and reply to review comments. +- An agent may run unattended, as long as it follows [AGENTS.md](AGENTS.md). -For more info, please refer to the [AGENTS.md](AGENTS.md) file. +What is still required, of humans and agents alike: + +1. **Disclose it.** Say in the PR that AI was used and how. Agent-authored commits carry an + `Assisted-by: ` trailer. +2. **Back the claims with measurements.** Performance and correctness claims about this hardware need numbers from + this hardware - see [Preparing your PR](#preparing-your-pr). +3. **Someone owns the result.** If you submit it, you answer for it: bugs, review feedback, and follow-ups. An agent + that opens a PR and disappears leaves the maintainers holding it, and such PRs may be closed. +4. **Do not duplicate.** Check for an existing PR or issue covering the same change first, and comment there instead + of opening a second one. +5. **Do not merge your own work**, and do not push to `master`. + +Undisclosed AI usage is the one thing that will get a contributor blocked here. Disclosure is cheap; a reviewer +discovering it later is not. # Pull requests (for contributors & collaborators) ### Before you start -- Search for existing discussions and PRs first - duplicates will likely be closed without questions. -- Features must begin with an issue, not a PR - let interest accumulate before writing code; niche features may only land as an example/tool, or on a private fork. -- Bug-fix PRs must include a reproducible issue and a regression test that fails before your change and passes after. Fixes without a test may be closed without review. -- New CLI or public API additions carry a **higher bar** than internal changes - justify why an existing mechanism doesn't suffice. -- Meeting all of the above still doesn't guarantee a merge - see [Pull requests (for maintainers)](#pull-requests-for-maintainers). -- If you are a new contributor - - Limit your open PRs to 1 - - Do not submit trivial fixes (e.g. typos, formatting changes) +- Search existing issues and PRs first - duplicates will likely be closed. +- Confirm the change belongs in this repo and not in [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp); + see [Where does my change go?](#where-does-my-change-go). +- For anything large, or anything that introduces a new pattern or subsystem, open an issue first. Small, measured, + device-specific changes can go straight to a PR. +- Check whether upstream has already fixed it. Carrying a patch we do not need is a permanent merge cost. ### Preparing your PR -- llama.cpp uses the ggml tensor library for model evaluation. If you are unfamiliar with ggml, consider taking a look at the [examples in the ggml repository](https://github.com/ggml-org/ggml/tree/master/examples/). [simple](https://github.com/ggml-org/ggml/tree/master/examples/simple) shows the bare minimum for using ggml. [gpt-2](https://github.com/ggml-org/ggml/tree/master/examples/gpt-2) has minimal implementations for language model inference using GPT-2. [mnist](https://github.com/ggml-org/ggml/tree/master/examples/mnist) demonstrates how to train and evaluate a simple image classifier - Test your changes: - - Execute [the full CI locally on your machine](ci/README.md) before publishing - - Verify that the perplexity and the performance are not affected negatively by your changes (use `llama-perplexity` and `llama-bench`) - - If you modified the `ggml` source, run the `test-backend-ops` tool to check whether different backend implementations of the `ggml` operators produce consistent results (this requires access to at least two different `ggml` backends) - - If you modified a `ggml` operator or added a new one, add the corresponding test cases to `test-backend-ops` -- Create separate PRs for each feature or fix: - - Avoid combining unrelated changes in a single PR - - When adding support for a new model or feature, focus on **CPU support only** in the initial PR unless you have a good reason not to. Add support for other backends like CUDA in follow-up PRs - - In particular, adding new data types (extension of the `ggml_type` enum) carries with it a disproportionate maintenance burden. As such, to add a new quantization type you will need to meet the following *additional* criteria *at minimum*: - - convert a small model to GGUF using the new type and upload it to HuggingFace - - provide [perplexity](https://github.com/ggml-org/llama.cpp/tree/master/tools/perplexity) comparisons to FP16/BF16 (whichever is the native precision) as well as to types of similar size - - provide KL divergence data calculated vs. the FP16/BF16 (whichever is the native precision) version for both the new type as well as types of similar size - - provide [performance data](https://github.com/ggml-org/llama.cpp/tree/master/tools/llama-bench) for the new type in comparison to types of similar size on pure CPU -- Consider allowing write access to your branch for faster reviews, as reviewers can push commits directly + - Build and run on Strix Halo, on the backend you touched (Vulkan and/or ROCm) + - Produce the numbers required by [Benchmarking requirements](#benchmarking-requirements) - this is the part + reviewers look at first + - [Running the CI locally](ci/README.md) is the fastest way to catch what our CI would catch +- Bug fixes should come with a reproducible report and, where practical, a regression test - but reuse the existing + test infrastructure; do not add new files under `tests/` without a good reason. +- Keep hardware workarounds documented in place: what was measured, on what hardware and driver, and what would let + the workaround be removed. +- Create separate PRs for each feature or fix; avoid combining unrelated changes. +- Prefer small, local diffs. This fork merges upstream `master` regularly, and a sprawling change conflicts forever. +- Consider allowing write access to your branch for faster reviews, as reviewers can push commits directly. ### After submitting your PR -- Expect requests for modifications to ensure the code meets llama.cpp's standards for quality and long-term maintainability -- Maintainers will rely on your insights and approval when making a final decision to approve and merge a PR -- If your PR becomes stale, rebase it on top of latest `master` to get maintainers attention -- Consider adding yourself to [CODEOWNERS](CODEOWNERS) to indicate your availability for fixing related issues and reviewing related PRs +- Expect requests for modifications. Long-term maintainability matters more here than elsewhere, because every line + carried in this fork has to survive every upstream merge. +- If your PR becomes stale, rebase it on top of latest `master`. +- Consider adding yourself to [CODEOWNERS](CODEOWNERS) to indicate your availability for fixing related issues and reviewing related PRs. + +# Benchmarking requirements + +Almost everything in this fork exists because of a measurement, so measurements are the main thing a review here has +to trust. A PR that claims a change is faster, and does not show it on the hardware, is not reviewable and will be +asked for numbers before anything else. + +### When numbers are required + +Required for any change that touches a compute kernel, a dispatch or scheduling path, memory allocation, batching, +sampling, or speculative decoding - that is, anything that could move throughput, latency or memory use. + +Not required for docs, comments, CI configuration, or build-system changes that cannot affect runtime. Say so in the +PR and delete the section from the template. + +If you do not have access to Strix Halo hardware, you may still open the PR - mark it clearly as **unverified** and +say what you were unable to run. Do not report numbers from a different GPU as if they applied here. + +### Report the environment + +Paste this into the PR, filled in. Most disagreements about benchmark results turn out to be a difference in one of +these lines: + +``` +Device: Ryzen AI Max+ 395 / +Memory: 128 GB LPDDR5X-8000 +Power: +BIOS: UMA split GB +Kernel: , amdgpu params: +Backend: Vulkan RADV, Mesa (or: ROCm , HIP_LAUNCH_BLOCKING=<0|1>) +Build: +Baseline: +Change: +Model: , +``` + +Power and thermals matter more here than on a discrete GPU: the CPU and iGPU share a package power budget and a +memory controller, and the same silicon ships in chassis with very different sustained TDP. Numbers without a stated +power profile are not comparable between contributors. + +### Run the baseline yourself + +The baseline is a binary you built from the merge-base of your branch, with the same cmake flags, on the same machine, +in the same session as the "after" run. Not numbers from a previous day, another machine, a release build, or memory. +Interleave or alternate the two runs if the machine's thermal state drifts. + +### Use llama-bench, and paste the table + +```sh +# prompt processing and token generation, 5 repetitions (the default) +./build/bin/llama-bench -m -p 512 -n 128 -r 5 + +# add context depth when the change can affect long-context behaviour +./build/bin/llama-bench -m -p 512 -n 128 -d 0,4096,16384 -r 5 +``` + +- Paste the raw table, including the standard-deviation column. Do not replace it with "about 18% faster". +- Raise `-r` if the spread is wide. Five repetitions is a floor, not a target. +- Vary the axis your change actually acts on. A change to batched mat-vec has to be shown across batch sizes + (`-ub`), not at one convenient point; a flash-attention change has to be shown with `-fa` both ways. +- If before and after overlap inside their standard deviations, you have not measured a speedup yet. + +`llama-bench` is a floor, not a ceiling. Changes to speculative decoding, the server, or anything whose benefit +depends on the content being generated cannot be shown with it - measure end to end against `llama-server` with a +described workload, and say what the workload was. + +For per-op attribution on the Vulkan backend, `GGML_VK_PERF_LOGGER=1` gives per-node timings, which is the right +evidence for "this shader variant is slow at this shape" claims. + +### Show that output did not change + +A speedup that changes results is a bug, so speed numbers alone are not enough: + +- Touched `ggml`: run `test-backend-ops` in the default `test` mode for the backend you changed, and narrow with + `-o ` while iterating. + ```sh + ./build/bin/test-backend-ops -b Vulkan0 -o MUL_MAT + ``` +- Touched anything that can alter generated tokens: report `llama-perplexity` before and after on the same file and + model. State the value for both; a perplexity shift that you cannot explain blocks the PR. +- Say which of these you did **not** run. An honest gap is reviewable; a silent one is not. # Pull requests (for maintainers) - Squash-merge PRs -- Use the following format for the squashed commit title: ` : (#)`. For example: `utils : fix typo in utils.py (#1234)` -- Optionally pick a `` from here: https://github.com/ggml-org/llama.cpp/wiki/Modules +- Use the following format for the squashed commit title: ` : (#)`. For example: `vulkan : fix mmvq selection on gfx1151 (#1234)` - Let other maintainers merge their own PRs - When merging a PR, make sure you have a good understanding of the changes -- If a PR does not warrant a new release, add `[no release]` in the squashed commit to spare CI resources -- Be mindful of maintenance: most of the work going into a feature happens after the PR is merged. If the PR author is not committed to contribute long-term, someone else needs to take responsibility (you) -- Add the ["merge ready"](https://github.com/ggml-org/llama.cpp/pulls?q=is%3Apr+is%3Aopen+draft%3Ano+sort%3Aupdated-desc+label%3A%22merge+ready%22+) label to a PR to indicate when a PR can be fast-merged without waiting for 2 independent reviews. [(more info)](https://github.com/ggml-org/llama.cpp/pull/26178) -- Wait for CI results before merging +- Prefer changes that upstream could plausibly accept later, even though we do not submit them from here - it keeps + merges cheap +- Wait for CI results before merging, including the self-hosted `gfx1151` job where it applies -Maintainers reserve the right to decline review or close pull requests for any reason, without any questions, particularly under any of the following conditions: -- The proposed change is already mentioned in the roadmap or an existing issue, and it has been assigned to someone. +Maintainers reserve the right to decline review or close pull requests, particularly under any of the following conditions: +- The change belongs in [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp) or upstream instead. - The pull request duplicates an existing one. - The contributor fails to adhere to this contributing guide or the AI policy. -- The change doesn't fit the existing architecture, or is too complex to justify its benefit. +- Performance claims are not backed by measurements from the hardware. +- Nobody is available to own the change after it lands. # Coding guidelines @@ -188,7 +301,7 @@ Maintainers reserve the right to decline review or close pull requests for any r - When adding or modifying a large piece of code: - If you are a collaborator, make sure to add yourself to [CODEOWNERS](CODEOWNERS) to indicate your availability for reviewing related PRs - If you are a contributor, find an existing collaborator who is willing to review and maintain your code long-term - - Provide the necessary CI workflow (and hardware) to test your changes (see [ci/README.md](https://github.com/ggml-org/llama.cpp/tree/master/ci)) + - Provide the necessary CI workflow (and hardware) to test your changes (see [ci/README.md](ci/README.md)) - New code should follow the guidelines (coding, naming, etc.) outlined in this document. Exceptions are allowed in isolated, backend-specific parts of the code that do not interface directly with the `ggml` interfaces. _(NOTE: for legacy reasons, existing code is not required to follow this guideline)_ @@ -203,6 +316,12 @@ Maintainers reserve the right to decline review or close pull requests for any r # Resources -The Github issues, PRs and discussions contain a lot of information that can be useful to get familiar with the codebase. For convenience, some of the more important information is referenced from Github projects: +- [AGENTS.md](AGENTS.md) - rules for AI coding agents working in this repo, including how to open a PR here +- [README.md](README.md) - what this fork is, how to build it, and the Strix Halo specific notes +- [Issues](https://github.com/halo-box/strix-llama.cpp/issues) and [PRs](https://github.com/halo-box/strix-llama.cpp/pulls) in this repo +- [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp) - where upstream-bound changes go instead + +Upstream's issues, PRs and discussions remain the best source of background on the codebase itself, and its Github +projects collect the most important of it: https://github.com/ggml-org/llama.cpp/projects diff --git a/README.md b/README.md index 0b5598c6e5bd..3b3755e5b00e 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,124 @@ -# llama.cpp - -![llama](https://raw.githubusercontent.com/ggml-org/llama.brand/refs/heads/master/cover/llama-cpp/cover-llama-cpp-dark.svg) +# strix-llama.cpp
-LLM inference in C/C++ +llama.cpp for AMD Strix Halo [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![Release](https://img.shields.io/github/v/release/ggml-org/llama.cpp?filter=v*&color=brightgreen)](https://github.com/ggml-org/llama.cpp/releases?q=tag:v0) -[![Nightly](https://img.shields.io/github/v/release/ggml-org/llama.cpp?label=nightly&filter=b*&color=orange)](https://github.com/ggml-org/llama.cpp/releases?q=b) -[![Server](https://img.shields.io/github/actions/workflow/status/ggml-org/llama.cpp/server.yml?label=Server)](https://github.com/ggml-org/llama.cpp/actions/workflows/server.yml) -[![Docker](https://img.shields.io/github/actions/workflow/status/ggml-org/llama.cpp/docker.yml?label=Docker)](https://github.com/ggml-org/llama.cpp/actions/workflows/docker.yml) -[![Winget](https://img.shields.io/github/actions/workflow/status/ggml-org/llama.cpp/winget.yml?label=Winget)](https://github.com/ggml-org/llama.cpp/actions/workflows/winget.yml) -[ggml](https://github.com/ggml-org/ggml) / [ops](https://github.com/ggml-org/llama.cpp/blob/master/docs/ops.md) / [maintainer PRs](https://github.com/ggml-org/llama.cpp/issues?q=is%3Apr%20is%3Aopen%20draft%3AFalse%20(author%3Argerganov%20OR%20author%3AKitaitiMakoto%20OR%20author%3Adanbev%20OR%20author%3Aaldehir%20OR%20author%3Amax-krasnyansky%20OR%20author%3ACISC%20OR%20author%3Aggerganov%20OR%20author%3Aam17an%20OR%20author%3Abartowski1182%20OR%20author%3Anikwen%20OR%20author%3Ahipudding%20OR%20author%3AServeurpersoCom%20OR%20author%3Apwilkin%20OR%20author%3Areeselevine%20OR%20author%3Angxson%20OR%20author%3Ajeffbolznv%20OR%20author%3Amarty1885%20OR%20author%3A0cc4m%20OR%20author%3ATitaniumtown%20OR%20author%3Aangt%20OR%20author%3AIMbackK%20OR%20author%3Aarthw%20OR%20author%3AJohannesGaessler%20OR%20author%3AORippler%20OR%20author%3Aruixiang63%20OR%20author%3Axctan%20OR%20author%3Aallozaur%20OR%20author%3Ayomaytk%20OR%20author%3Aaendk%20OR%20author%3Agaugarg-nv%20OR%20author%3Ataronaeo%20OR%20author%3Aforforever73%20OR%20author%3Alhez%20OR%20author%3Anetrunnereve%20OR%20author%3Afairydreaming)%20sort%3Aupdated-desc) / [dev stats](https://github.com/ggml-org/llama.cpp-dev) / [lib llama API](https://github.com/ggml-org/llama.cpp/issues/9289) / [llama-server REST API](https://github.com/ggml-org/llama.cpp/issues/9291) +[upstream llama.cpp](https://github.com/ggml-org/llama.cpp) / [ggml](https://github.com/ggml-org/ggml) / [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp)
+## What this is + +A community fork of [`llama.cpp`](https://github.com/ggml-org/llama.cpp) for **AMD Strix Halo** - the Ryzen AI Max / Max+ 300 series +APUs (`gfx1151`, RDNA 3.5 integrated GPU, up to 128 GB of unified LPDDR5X shared between CPU and GPU). + +Strix Halo is an unusual target. It has more addressable memory than almost any consumer discrete GPU, and far less +bandwidth; the iGPU shares its memory controller with the CPU; and both the Vulkan (RADV) and ROCm/HIP paths have +RDNA 3.5 specific behaviour that upstream has no hardware to reproduce. Changes that only make sense on this one +device - or that need a lot of measurement on it before they are ready to propose anywhere else - live here. + +This fork tracks upstream `master` and stays mergeable with it. It is not a rewrite. + +## Relationship to upstream + +``` +ggml-org/llama.cpp upstream, the real project + | + +-- halo-box/llama.cpp staging fork for changes intended to go upstream + | + +-- halo-box/strix-llama.cpp <-- you are here: the Strix Halo community fork +``` + +**Nothing in this repository goes upstream from here.** If a change is general enough for upstream, it belongs in +[halo-box/llama.cpp](https://github.com/halo-box/llama.cpp) and is submitted from there, under the upstream project's +contribution and AI-usage rules. That separation is the whole point: it keeps upstream's review queue free of +device-specific work, and it lets this repo move fast on the things only Strix Halo owners care about. + +Practically, that means this repo has its own rules - most visibly, **AI coding agents may open pull requests here**. +See [CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md). + ## Quick start -A few options to get `llama.cpp` installed on your machine: +Build from source. Two backends are worth using on Strix Halo: + +**Vulkan** (RADV on Mesa; the easiest path, and the best one for most models) + +```sh +cmake -B build -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release -j +``` + +**ROCm / HIP** (needs ROCm installed; build for `gfx1151` explicitly) -- Visit https://llama.app and follow the instructions -- Run with Docker - see our [Docker documentation](docs/docker.md) -- Download pre-built binaries from the [releases page](https://github.com/ggml-org/llama.cpp/releases) -- Build from source by cloning this repository - check out [our build guide](docs/build.md) +```sh +HIPCXX="$(hipconfig -l)/clang" HIP_PATH="$(hipconfig -R)" \ + cmake -B build -DGGML_HIP=ON -DGPU_TARGETS=gfx1151 -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release -j +``` -Once installed: +Then: ```sh -# Download and run a model directly from Hugging Face -llama cli -hf ggml-org/Qwen3.5-0.8B-GGUF +# chat, pulling the model straight from Hugging Face +./build/bin/llama-cli -hf ggml-org/Qwen3.5-0.8B-GGUF -# Launch OpenAI-compatible API server -llama serve -hf ggml-org/Qwen3.5-0.8B-GGUF +# OpenAI-compatible API server + web UI on http://localhost:8080 +./build/bin/llama-server -hf ggml-org/Qwen3.5-0.8B-GGUF ``` - - - - - -
- VLM session with `llama cli` - VLM session with llama cli - - Built-in web UI against `llama serve` running Qwen 3.6 - Built-in web UI against llama serve -
- -## Description - -The main goal of `llama.cpp` is to enable LLM (and VLM) inference with minimal setup and state-of-the-art performance on -a wide range of hardware - locally and in the cloud. - -- Plain C/C++ implementation without any dependencies -- Apple silicon is a first-class citizen - optimized via ARM NEON, Accelerate and Metal frameworks -- AVX, AVX2, AVX512 and AMX support for x86 architectures -- RVV, ZVFH, ZFH, ZICBOP and ZIHINTPAUSE support for RISC-V architectures -- 1.5-bit, 2-bit, 3-bit, 4-bit, 5-bit, 6-bit, and 8-bit integer quantization for faster inference and reduced memory use -- Custom CUDA kernels for running LLMs on NVIDIA GPUs (support for AMD GPUs via HIP and Moore Threads GPUs via MUSA) -- Vulkan and SYCL backend support -- CPU+GPU hybrid inference to partially accelerate models larger than the total VRAM capacity - -The `llama.cpp` project is build on top of the [ggml](https://github.com/ggml-org/ggml) library. +Full build documentation, including Windows and Docker, is in [docs/build.md](docs/build.md). + +## Running on Strix Halo + +**Give the iGPU enough memory.** The APU's memory is shared, and the GPU can only use what the firmware and kernel let +it map. Two things control this: the UMA / dedicated-VRAM split in your BIOS, and the `amdgpu` GTT limit on Linux +(`amdgpu.gttsize`, in MB, and `ttm.pages_limit`, in 4 KB pages, as kernel command-line parameters). Which of those you +need depends on your kernel version - newer kernels size GTT more generously on their own. If a model that clearly +fits in RAM fails to allocate, this is almost always why. + +**ROCm and batched inference.** On `gfx1151` there is an async-execution correctness bug in the HIP path: batched +inference can return badly wrong output (perplexity ~88 against ~9.4 for the same model). Setting +`HIP_LAUNCH_BLOCKING=1` serializes kernel launches and restores correctness, at a performance cost. Our ROCm CI runs +with it set. It is a workaround for a ROCm/HIP issue, not a fix, and it should go away when that is fixed upstream. + +**Vulkan mat-vec chunking.** Batched mat-vec at 3, 5 and 6 columns is several times slower than at 1, 2 and 4 on RADV +here, which hits speculative decoding hard (it verifies at exactly those batch sizes). This fork splits such batches +into column counts that are measured to scale. Set `GGML_VK_MMV_NO_SPLIT=1` to restore the single upstream dispatch, +e.g. to compare against it. + +**Measure things.** `GGML_VK_PERF_LOGGER=1` (any value) gives per-op timings on the Vulkan backend and is how most of the findings +above were made. `llama-bench` and `llama-perplexity` are the tools for before/after numbers, and PRs here are +expected to carry them - see [Benchmarking requirements](CONTRIBUTING.md#benchmarking-requirements). + +## What differs from upstream + +Everything else is upstream `llama.cpp`. The additions currently carried here: + +| Change | Flag / switch | What it does | +| --- | --- | --- | +| Vulkan batched mat-vec chunking | `GGML_VK_MMV_NO_SPLIT=1` to disable | Dispatches batched mat-vec at the column counts that actually scale on RDNA 3.5, instead of the slow NUM_COLS shader variants | +| Adaptive speculative draft length | `--spec-draft-adaptive` | Sizes each draft from a measured per-sequence acceptance EMA rather than always drafting `--spec-draft-n-max` | +| Multi-point reasoning budget | `--reasoning-budget-*` | Intro message, two soft warnings, a grace period to finish a paragraph after the budget runs out, and reasoning-token usage telemetry | + +Run `--help`, or see [tools/server/README.md](tools/server/README.md), for the full options. ## Supported backends -| Backend | Target devices | +The ones that matter on this hardware: + +| Backend | Notes | | --- | --- | -| [BLAS](docs/build.md#blas-build) | All | -| [BLIS](docs/backend/BLIS.md) | All | -| [CANN](docs/build.md#cann) | Ascend NPU | -| [CUDA](docs/build.md#cuda) | Nvidia GPU | -| [HIP](docs/build.md#hip) | AMD GPU | -| [Hexagon [In Progress]](docs/backend/snapdragon/README.md) | Snapdragon | -| [IBM zDNN](docs/backend/zDNN.md) | IBM Z & LinuxONE | -| [MUSA](docs/build.md#musa) | Moore Threads GPU | -| [Metal](docs/build.md#metal-build) | Apple Silicon | -| [OpenCL](docs/backend/OPENCL.md) | Adreno GPU | -| [OpenVINO [In Progress]](docs/backend/OPENVINO.md) | Intel CPUs, GPUs, and NPUs | -| [RPC](https://github.com/ggml-org/llama.cpp/tree/master/tools/rpc) | All | -| [SYCL](docs/backend/SYCL.md) | Intel GPU | -| [VirtGPU](docs/backend/VirtGPU.md) | VirtGPU APIR | -| [Vulkan](docs/build.md#vulkan) | GPU | -| [WebGPU](docs/build.md#webgpu) | All | -| [ZenDNN](docs/build.md#zendnn) | AMD CPU | +| [Vulkan](docs/build.md#vulkan) | RADV on the RDNA 3.5 iGPU - the default recommendation | +| [HIP](docs/build.md#hip) | ROCm on `gfx1151` - see the `HIP_LAUNCH_BLOCKING` note above | +| [CPU](docs/build.md) | Zen 5 cores with AVX-512 - useful for offloading part of a model, though it shares bandwidth with the iGPU | +| [ZenDNN](docs/build.md#zendnn) | AMD CPU acceleration | +| [RPC](tools/rpc) | Distribute a model across several machines | + +`llama.cpp` supports many more (CUDA, Metal, SYCL, CANN, OpenCL, WebGPU, ...); they are all still present in the tree +and unmodified. See the [upstream README](https://github.com/ggml-org/llama.cpp) for that list. ## Documentation @@ -100,25 +133,32 @@ The `llama.cpp` project is build on top of the [ggml](https://github.com/ggml-or - [How to build](docs/build.md) - [Running on Docker](docs/docker.md) -- [Build on Android](docs/android.md) - [Multi-GPU usage](docs/multi-gpu.md) - [Performance troubleshooting](docs/development/token_generation_performance_tips.md) - [GGML tips & tricks](https://github.com/ggml-org/llama.cpp/wiki/GGML-Tips-&-Tricks) -- [XCFramework](docs/xcframework.md) - [Completions](docs/completions.md) - [Models](docs/models.md) -- [Release process](docs/release.md) ## Contributing -- Contributors can open PRs -- Collaborators will be invited based on contributions -- Maintainers can push to branches in the `llama.cpp` repo and merge PRs into the `master` branch -- Any help with managing issues, PRs and projects is very appreciated! -- Read the [CONTRIBUTING.md](CONTRIBUTING.md) for more information +This is a small community project. Strix Halo owners with a benchmark, a bug report, or a patch are exactly who it is +for. + +- Anyone can open a PR, **including AI coding agents acting on their own** - this repo permits automated PR submission, + unlike upstream. The rules that replace the upstream ban are in [AGENTS.md](AGENTS.md). +- Device-specific claims need numbers from the device, against a baseline you built and ran yourself. What exactly to + report is in [Benchmarking requirements](CONTRIBUTING.md#benchmarking-requirements); read it before you benchmark. +- If your change is not Strix Halo specific, send it to [halo-box/llama.cpp](https://github.com/halo-box/llama.cpp) + instead, so it can reach upstream. +- Read [CONTRIBUTING.md](CONTRIBUTING.md) before your first PR. + +CI runs the standard llama.cpp suite plus a self-hosted `gfx1151` ROCm job on real hardware. ## Acknowledgements +This project is a fork and owes everything to the people who built what it forks: + +- [ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp) and [ggml](https://github.com/ggml-org/ggml) - Georgi Gerganov and the llama.cpp contributors - MIT license - [yhirose/cpp-httplib](https://github.com/yhirose/cpp-httplib) - Single-header HTTP server, used by `llama-server` - MIT license - [nothings/stb](https://github.com/nothings/stb) - Single-header image format decoder, used by multimodal subsystem - Public domain - [nlohmann/json](https://github.com/nlohmann/json) - Single-header JSON library, used by various tools/examples - MIT License From 95592996925a4c4f0f9131c39b658e042939956c Mon Sep 17 00:00:00 2001 From: Nathanw1014 <67372905+Nathanw1014@users.noreply.github.com> Date: Sun, 30 Aug 2026 06:37:30 +0930 Subject: [PATCH 009/212] speculative: keep DFlash draft blocks the same width across sequences (#4) Co-authored-by: Claude Opus 5 --- common/speculative.cpp | 51 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/common/speculative.cpp b/common/speculative.cpp index 23f599de1fde..2e90d8e59a43 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -210,6 +210,14 @@ struct common_speculative_impl { } } + // record the width a sequence was actually drafted at, when the caller had to + // override the per-sequence choice (see the DFlash/DSpark batch below) + void set_last_draft(llama_seq_id seq_id, int32_t n) { + if (seq_id >= 0 && (size_t) seq_id < n_last_draft.size()) { + n_last_draft[seq_id] = n; + } + } + // effective draft length for this step, never above the configured n_max int32_t adaptive_n_draft(llama_seq_id seq_id, int32_t n_cfg, int32_t n_min) { if (!adaptive_n || seq_id < 0 || (size_t) seq_id >= acc_ema.size()) { @@ -1283,6 +1291,44 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { std::vector i_block_beg(n_seq, -1); std::vector n_block (n_seq, 0); + // DFlash decodes the whole block in one pass, so a shorter block does not save + // draft time -- but it does shrink the target's verification batch, which is + // where the cost actually is. + // + // The block width has to be the same for every sequence in this batch. All the + // blocks go into one decode, and the draft graph reshapes those tokens into + // [block_size, n_seqs] -- build_dflash2_conv and the DSpark markov head both + // assert n_tokens % n_seqs_unq == 0. Per-sequence widths break that: with an + // equal ubatch split the longer block is silently cut across two ubatches, so + // its tail is convolved as a block of its own and drafts from a truncated + // window; with a simple split it trips the assert and takes the server down. + // So size the batch once, from the sequence with the highest measured + // acceptance: no sequence drafts shorter than its own estimate, and the extra + // positions a colder sequence receives are just rejected at verification. + int32_t n_draft_batch = params.n_max; + + if (adaptive_n) { + n_draft_batch = 0; + + for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { + if (!dparams[seq_id].drafting) { + continue; + } + + n_draft_batch = std::max(n_draft_batch, adaptive_n_draft(seq_id, params.n_max, params.n_min)); + } + + if (n_draft_batch == 0) { + return; + } + + for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { + if (dparams[seq_id].drafting) { + set_last_draft(seq_id, n_draft_batch); + } + } + } + for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { auto & dp = dparams[seq_id]; if (!dp.drafting) { @@ -1293,10 +1339,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { const int32_t n = (int32_t) dp.n_past; - // DFlash decodes the whole block in one pass, so a shorter block does - // not save draft time -- but it does shrink the target's verification - // batch, which is where the cost actually is. - const int32_t n_draft = adaptive_n_draft(seq_id, params.n_max, params.n_min); + const int32_t n_draft = n_draft_batch; const int32_t n_block_tokens = n_draft + (is_dspark && sample_from_anchor ? 0 : 1); i_block_beg[seq_id] = batch.n_tokens; From 21d84831652bbf9dce59752d8a03105870c35c22 Mon Sep 17 00:00:00 2001 From: voidsurfer Date: Sun, 30 Aug 2026 00:09:30 +0200 Subject: [PATCH 010/212] reasoning-budget: do not re-fire the intro message in once mode (#8) Co-authored-by: voidsurfer <18734507+voidsurfer@users.noreply.github.com> --- common/reasoning-budget.cpp | 18 ++++++++++++++---- common/reasoning-budget.h | 3 ++- common/sampling.cpp | 3 ++- tests/test-reasoning-budget.cpp | 24 ++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/common/reasoning-budget.cpp b/common/reasoning-budget.cpp index 8c65908b0fa2..538839a468b2 100644 --- a/common/reasoning-budget.cpp +++ b/common/reasoning-budget.cpp @@ -77,6 +77,7 @@ struct common_reasoning_budget_ctx { llama_tokens intro_forced_tokens; size_t intro_force_pos; + bool intro_once; int32_t grace_tokens; int32_t grace_remaining; @@ -154,6 +155,11 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to case REASONING_BUDGET_INTRO_FORCING: ctx->intro_force_pos++; if (ctx->intro_force_pos >= ctx->intro_forced_tokens.size()) { + if (ctx->intro_once) { + // intro-mode once: do not re-fire on later reasoning blocks + ctx->intro_forced_tokens.clear(); + ctx->intro_force_pos = 0; + } if (ctx->remaining <= 0) { ctx->state = REASONING_BUDGET_FORCING; ctx->force_pos = 0; @@ -354,7 +360,8 @@ static struct llama_sampler * common_reasoning_budget_init_state( const llama_tokens & intro_forced_tokens, int32_t budget, int32_t grace_tokens, - common_reasoning_budget_state initial_state); + common_reasoning_budget_state initial_state, + bool intro_once); static struct llama_sampler * common_reasoning_budget_clone(const struct llama_sampler * smpl); @@ -395,7 +402,8 @@ static struct llama_sampler * common_reasoning_budget_init_state( const llama_tokens & intro_forced_tokens, int32_t budget, int32_t grace_tokens, - common_reasoning_budget_state initial_state) { + common_reasoning_budget_state initial_state, + bool intro_once) { if (initial_state == REASONING_BUDGET_COUNTING && budget <= 0) { initial_state = REASONING_BUDGET_FORCING; } @@ -426,6 +434,7 @@ static struct llama_sampler * common_reasoning_budget_init_state( /* .active_soft = */ 0, /* .intro_forced_tokens = */ intro_forced_tokens, /* .intro_force_pos = */ 0, + /* .intro_once = */ intro_once, /* .grace_tokens = */ grace_tokens, /* .grace_remaining = */ grace_tokens, /* .hard_pending_prev_nl = */ false, @@ -441,9 +450,10 @@ struct llama_sampler * common_reasoning_budget_init( const llama_tokens & intro_forced_tokens, int32_t budget, int32_t grace_tokens, - common_reasoning_budget_state initial_state) { + common_reasoning_budget_state initial_state, + bool intro_once) { return common_reasoning_budget_init_state(vocab, start_seqs, end_seqs, forced_tokens, soft_points, - intro_forced_tokens, budget, grace_tokens, initial_state); + intro_forced_tokens, budget, grace_tokens, initial_state, intro_once); } common_reasoning_budget_state common_reasoning_budget_get_state(const struct llama_sampler * smpl) { diff --git a/common/reasoning-budget.h b/common/reasoning-budget.h index 4151777e82c4..e3ababe0a8d6 100644 --- a/common/reasoning-budget.h +++ b/common/reasoning-budget.h @@ -56,7 +56,8 @@ struct llama_sampler * common_reasoning_budget_init( const llama_tokens & intro_forced_tokens, int32_t budget, int32_t grace_tokens = 0, - common_reasoning_budget_state initial_state = REASONING_BUDGET_IDLE); + common_reasoning_budget_state initial_state = REASONING_BUDGET_IDLE, + bool intro_once = false); common_reasoning_budget_state common_reasoning_budget_get_state(const struct llama_sampler * smpl); diff --git a/common/sampling.cpp b/common/sampling.cpp index 8141dfa0f79f..0e40d86e893c 100644 --- a/common/sampling.cpp +++ b/common/sampling.cpp @@ -329,7 +329,8 @@ struct common_sampler * common_sampler_init( reasoning_budget_active ? params.reasoning_budget_intro_forced : llama_tokens(); rbudget = common_reasoning_budget_init(vocab, { params.reasoning_budget_start }, params.reasoning_budget_end, params.reasoning_budget_forced, soft_points, intro_tokens, budget_tokens, - params.reasoning_budget_grace_tokens); + params.reasoning_budget_grace_tokens, REASONING_BUDGET_IDLE, + params.reasoning_budget_intro_mode == "once"); for (const auto & token : prefill_tokens) { llama_sampler_accept(rbudget, token); diff --git a/tests/test-reasoning-budget.cpp b/tests/test-reasoning-budget.cpp index e4c6d3c44e3e..45b7c3da35fe 100644 --- a/tests/test-reasoning-budget.cpp +++ b/tests/test-reasoning-budget.cpp @@ -438,6 +438,29 @@ static void test_reasoning_budget_intro_rearms_on_multiblock() { llama_sampler_free(sampler); } +static void test_reasoning_budget_intro_once_does_not_rearm() { + const std::vector start = { 100 }; + const std::vector end = { 101 }; + const std::vector forced = { 102, 101 }; + const std::vector intro_forced = { 300, 301 }; + + auto * sampler = common_reasoning_budget_init(nullptr, { start }, { end }, forced, {}, intro_forced, 5, 0, REASONING_BUDGET_IDLE, true); + + llama_sampler_accept(sampler, 100); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_INTRO_FORCING); + llama_sampler_accept(sampler, 300); + llama_sampler_accept(sampler, 301); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING); + llama_sampler_accept(sampler, 101); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE); + + // second reasoning block: intro must not fire again in once mode + llama_sampler_accept(sampler, 100); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING); + + llama_sampler_free(sampler); +} + static void test_reasoning_budget_hard_pending_grace_expires() { const std::vector start = { 100 }; const std::vector end = { 101 }; @@ -769,6 +792,7 @@ int main(void) { test_reasoning_budget_intro_forcing_budget_zero(); test_reasoning_budget_force_manual_from_intro(); test_reasoning_budget_intro_rearms_on_multiblock(); + test_reasoning_budget_intro_once_does_not_rearm(); test_reasoning_budget_hard_pending_grace_expires(); test_reasoning_budget_hard_pending_natural_end(); test_reasoning_budget_force_manual_from_hard_pending(); From dd2c3d6fa77c0a90e09c4499ca004aa9fe843e92 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Sun, 30 Aug 2026 22:59:24 +0100 Subject: [PATCH 011/212] qwen4exp: MTP draft head for speculative decoding (#9) Co-authored-by: Marshall --- conversion/qwen4exp.py | 52 +++++++- gguf-py/gguf/constants.py | 3 + src/llama-model.cpp | 2 +- src/models/models.h | 9 +- src/models/qwen4exp.cpp | 274 ++++++++++++++++++++++++++++++++------ 5 files changed, 291 insertions(+), 49 deletions(-) diff --git a/conversion/qwen4exp.py b/conversion/qwen4exp.py index 168796d616b9..e23cf56fa706 100644 --- a/conversion/qwen4exp.py +++ b/conversion/qwen4exp.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import Iterable, cast +import json +import logging +from typing import Callable, Iterable, cast import torch from torch import Tensor @@ -26,8 +28,6 @@ class Qwen4ExpTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase): 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) @@ -63,9 +63,11 @@ def set_gguf_parameters(self): 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)] - ) + ratios = [ratio if layer_types[i] == "full_attention" else 0 for i in range(n_layer)] + # the MTP block extends block_count, and this array is read per block. The draft + # attends dense, so it takes 0, the same value a non-QSA layer gets. + ratios += [0] * (self.block_count - n_layer) + self.gguf_writer.add_attention_compress_ratios(ratios) # 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 @@ -105,6 +107,44 @@ def _eos_token_id(self) -> int: raise ValueError("eos_token_id is required: the PLE hash resets its n-grams on it") return int(eos) + @classmethod + def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None: + name = item[0] + # The MTP block carries its own final hyper-connection mixer. An MTP-only file has + # no trunk, so that one becomes the file's mixer; a full model already has the + # trunk's, and graph_mtp reads it, so this copy is dropped. + if name.startswith("mtp.hyper_connection_mixer."): + if cls.no_mtp or not cls.mtp_only: + return None + return (name.replace("mtp.", "model.", 1), item[1]) + return super().filter_tensors(item) + + def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]: + yield from super().generate_extra_tensors() + + # the reference adds the two MTP input projections, and A*e + B*h == [A|B]*concat(e, h), + # so they join into the one eh_proj the tensor map already handles + e_name = "mtp.fc_embedding.weight" + h_name = "mtp.fc_hidden.weight" + + have_e = e_name in self.model_tensors + have_h = h_name in self.model_tensors + if not have_e and not have_h: + return + if not have_e or not have_h: + raise KeyError(f"unpaired MTP input projection: need both {e_name} and {h_name}") + + from .base import LazyTorchTensor + + e = LazyTorchTensor.to_eager(self.model_tensors[e_name]()) + h = LazyTorchTensor.to_eager(self.model_tensors[h_name]()) + yield (self.format_tensor_name(gguf.MODEL_TENSOR.NEXTN_EH_PROJ, + self.hparams["num_hidden_layers"]), + torch.cat([e, h], dim=1).contiguous()) + + del self.model_tensors[e_name] + del self.model_tensors[h_name] + 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"): diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index c99feb3c795c..b8fa095b4b6c 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -2908,6 +2908,9 @@ class MODEL_TENSOR(IntEnum): MODEL_TENSOR.FFN_GATE_EXP, MODEL_TENSOR.FFN_GATE_UP_EXP, MODEL_TENSOR.PER_LAYER_TOKEN_EMBD, + MODEL_TENSOR.NEXTN_EH_PROJ, + MODEL_TENSOR.NEXTN_ENORM, + MODEL_TENSOR.NEXTN_HNORM, MODEL_TENSOR.PLE_KEY, MODEL_TENSOR.PLE_VALUE, MODEL_TENSOR.PLE_NORM_KEY, diff --git a/src/llama-model.cpp b/src/llama-model.cpp index d0dcfb3b8dff..b6428791f118 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -2436,7 +2436,7 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params, const bool mtp_on_hybrid_qwen = params.ctx_type == LLAMA_CONTEXT_TYPE_MTP && (arch == LLM_ARCH_QWEN3NEXT || arch == LLM_ARCH_QWEN35 || arch == LLM_ARCH_QWEN35MOE || - arch == LLM_ARCH_BAILINGMOE3); + arch == LLM_ARCH_QWEN4EXP || arch == LLM_ARCH_BAILINGMOE3); const bool mtp_on_hybrid_nemotron = params.ctx_type == LLAMA_CONTEXT_TYPE_MTP && arch == LLM_ARCH_NEMOTRON_H_MOE; diff --git a/src/models/models.h b/src/models/models.h index 1d3557068208..f285d3d315ac 100644 --- a/src/models/models.h +++ b/src/models/models.h @@ -2291,7 +2291,10 @@ struct llama_model_qwen4exp : public llama_model_base { struct graph : public llm_build_delta_net_base { graph(const llama_model & model, const llm_graph_params & params); - private: + protected: + // members only, no trunk: graph_mtp builds its own single block + graph(const llama_model & model, const llm_graph_params & params, bool mtp); + // HC replaces every layer norm: residual is [n_embd, hc, n_tokens] ggml_tensor * build_hc_mix( ggml_tensor * x, @@ -2383,6 +2386,10 @@ struct llama_model_qwen4exp : public llama_model_base { const llama_model & model; }; + struct graph_mtp : public graph { + graph_mtp(const llama_model & model, const llm_graph_params & params); + }; + std::unique_ptr build_arch_graph(const llm_graph_params & params) const override; }; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 22909f9a9d0e..c10a2880352a 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -28,6 +28,8 @@ void llama_model_qwen4exp::load_arch_hparams(llama_model_loader & ml) { GGML_ASSERT(hparams.dsv4_hc_mult > 0 && hparams.hc_low_rank > 0); hparams.n_embd_out_impl = hparams.dsv4_hc_mult * hparams.n_embd; + + ml.get_key(LLM_KV_NEXTN_PREDICT_LAYERS, hparams.n_layer_nextn, false); ml.get_key(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, hparams.indexer_n_head); ml.get_key(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, hparams.indexer_head_size); ml.get_key(LLM_KV_ATTENTION_INDEXER_TOP_K, hparams.indexer_top_k); @@ -157,6 +159,11 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { } } + // An MTP-only file carries just the draft block. Keep walking the trunk so the + // per-layer bookkeeping still runs, but let its tensors be absent. + const bool mtp_only = hparams.n_layer_nextn > 0 && ml.get_weight("blk.0.hc_attn_norm.weight") == nullptr; + const int trunk_flags = mtp_only ? TENSOR_NOT_REQUIRED : 0; + for (int il = 0; il < n_layer; ++il) { auto & layer = layers[il]; @@ -172,64 +179,223 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { const int64_t conv_dim = key_dim * 2 + value_dim; // two HC modules per layer: before the token mixer, before the MoE - layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { hc_dim }, 0); - layer.hc_attn_down = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN, "weight", il), { hc_dim, hc_lr }, 0); - layer.hc_attn_up = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP, "weight", il), { hc_lr, hc_dim }, 0); - layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, 0); - layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { hc_dim }, 0); - layer.hc_ffn_down = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN, "weight", il), { hc_dim, hc_lr }, 0); - layer.hc_ffn_up = create_tensor(tn(LLM_TENSOR_HC_FFN_UP, "weight", il), { hc_lr, hc_dim }, 0); - layer.hc_ffn_inject = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT, "weight", il), { hc_dim, hc }, 0); + layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { hc_dim }, trunk_flags); + layer.hc_attn_down = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN, "weight", il), { hc_dim, hc_lr }, trunk_flags); + layer.hc_attn_up = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP, "weight", il), { hc_lr, hc_dim }, trunk_flags); + layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, trunk_flags); + layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { hc_dim }, trunk_flags); + layer.hc_ffn_down = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN, "weight", il), { hc_dim, hc_lr }, trunk_flags); + layer.hc_ffn_up = create_tensor(tn(LLM_TENSOR_HC_FFN_UP, "weight", il), { hc_lr, hc_dim }, trunk_flags); + layer.hc_ffn_inject = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT, "weight", il), { hc_dim, hc }, trunk_flags); if (!hparams.is_recr(il)) { // full attention: wq holds [q|gate] interleaved per head - create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, 0); - layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, 0); + create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, trunk_flags); + layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, trunk_flags); - layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, 0); - layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, 0); + layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, trunk_flags); + layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, trunk_flags); const int64_t idx_dim = hparams.indexer_head_size; - layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", il), { n_embd, hparams.indexer_n_head * idx_dim }, 0); - layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", il), { n_embd, idx_dim }, 0); - layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", il), { idx_dim }, 0); - layer.index_k_norm = create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight", il), { idx_dim }, 0); + layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", il), { n_embd, hparams.indexer_n_head * idx_dim }, trunk_flags); + layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", il), { n_embd, idx_dim }, trunk_flags); + layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", il), { idx_dim }, trunk_flags); + layer.index_k_norm = create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight", il), { idx_dim }, trunk_flags); } else { - layer.wqkv = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", il), { n_embd, key_dim * 2 + value_dim }, 0); - layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", il), { n_embd, value_dim }, 0); - layer.ssm_conv1d = create_tensor(tn(LLM_TENSOR_SSM_CONV1D, "weight", il), { hparams.ssm_d_conv, conv_dim }, 0); - layer.ssm_dt = create_tensor(tn(LLM_TENSOR_SSM_DT, "bias", il), { hparams.ssm_dt_rank }, 0); - layer.ssm_a = create_tensor(tn(LLM_TENSOR_SSM_A_NOSCAN, il), { hparams.ssm_dt_rank }, 0); - layer.ssm_beta = create_tensor(tn(LLM_TENSOR_SSM_BETA, "weight", il), { n_embd, n_v_heads }, 0); - layer.ssm_alpha = create_tensor(tn(LLM_TENSOR_SSM_ALPHA, "weight", il), { n_embd, n_v_heads }, 0); - layer.ssm_norm = create_tensor(tn(LLM_TENSOR_SSM_NORM, "weight", il), { head_v_dim }, 0); - layer.ssm_out = create_tensor(tn(LLM_TENSOR_SSM_OUT, "weight", il), { value_dim, n_embd }, 0); + layer.wqkv = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", il), { n_embd, key_dim * 2 + value_dim }, trunk_flags); + layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", il), { n_embd, value_dim }, trunk_flags); + layer.ssm_conv1d = create_tensor(tn(LLM_TENSOR_SSM_CONV1D, "weight", il), { hparams.ssm_d_conv, conv_dim }, trunk_flags); + layer.ssm_dt = create_tensor(tn(LLM_TENSOR_SSM_DT, "bias", il), { hparams.ssm_dt_rank }, trunk_flags); + layer.ssm_a = create_tensor(tn(LLM_TENSOR_SSM_A_NOSCAN, il), { hparams.ssm_dt_rank }, trunk_flags); + layer.ssm_beta = create_tensor(tn(LLM_TENSOR_SSM_BETA, "weight", il), { n_embd, n_v_heads }, trunk_flags); + layer.ssm_alpha = create_tensor(tn(LLM_TENSOR_SSM_ALPHA, "weight", il), { n_embd, n_v_heads }, trunk_flags); + layer.ssm_norm = create_tensor(tn(LLM_TENSOR_SSM_NORM, "weight", il), { head_v_dim }, trunk_flags); + layer.ssm_out = create_tensor(tn(LLM_TENSOR_SSM_OUT, "weight", il), { value_dim, n_embd }, trunk_flags); } if (hparams.is_ple(il)) { - layer.ple_key = create_tensor(tn(LLM_TENSOR_PLE_KEY, "weight", il), { n_embd, hc_dim }, 0); - layer.ple_value = create_tensor(tn(LLM_TENSOR_PLE_VALUE, "weight", il), { n_embd, n_embd }, 0); - layer.ple_norm_key = create_tensor(tn(LLM_TENSOR_PLE_NORM_KEY, "weight", il), { hc_dim }, 0); - layer.ple_norm_query = create_tensor(tn(LLM_TENSOR_PLE_NORM_QUERY, "weight", il), { hc_dim }, 0); - layer.ple_norm_conv = create_tensor(tn(LLM_TENSOR_PLE_NORM_CONV, "weight", il), { hc_dim }, 0); - layer.ple_conv1d = create_tensor(tn(LLM_TENSOR_PLE_CONV1D, "weight", il), { hparams.ple_conv_kernel, hc_dim }, 0); + layer.ple_key = create_tensor(tn(LLM_TENSOR_PLE_KEY, "weight", il), { n_embd, hc_dim }, trunk_flags); + layer.ple_value = create_tensor(tn(LLM_TENSOR_PLE_VALUE, "weight", il), { n_embd, n_embd }, trunk_flags); + layer.ple_norm_key = create_tensor(tn(LLM_TENSOR_PLE_NORM_KEY, "weight", il), { hc_dim }, trunk_flags); + layer.ple_norm_query = create_tensor(tn(LLM_TENSOR_PLE_NORM_QUERY, "weight", il), { hc_dim }, trunk_flags); + layer.ple_norm_conv = create_tensor(tn(LLM_TENSOR_PLE_NORM_CONV, "weight", il), { hc_dim }, trunk_flags); + layer.ple_conv1d = create_tensor(tn(LLM_TENSOR_PLE_CONV1D, "weight", il), { hparams.ple_conv_kernel, hc_dim }, trunk_flags); } - layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", il), { n_embd, n_expert }, 0); - layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", il), { n_ff_exp, n_embd, n_expert }, 0); - create_tensor_gate_up_exps(layer, il, n_embd, n_ff_exp, n_expert, 0); + layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", il), { n_embd, n_expert }, trunk_flags); + layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", il), { n_ff_exp, n_embd, n_expert }, trunk_flags); + create_tensor_gate_up_exps(layer, il, n_embd, n_ff_exp, n_expert, trunk_flags); + + layer.ffn_gate_inp_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP_SHEXP, "weight", il), { n_embd }, trunk_flags); + layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", il), { n_embd, n_ff_shexp }, trunk_flags); + layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", il), { n_embd, n_ff_shexp }, trunk_flags); + layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", il), { n_ff_shexp, n_embd }, trunk_flags); + } + // The MTP draft block sits one past the trunk. It is a full qwen4exp layer plus the + // three nextn tensors, and is skipped unless the file is opened as a draft. + for (int il = n_layer; il < n_layer + (int) hparams.n_layer_nextn; ++il) { + auto & layer = layers[il]; + + const int flags = ml.load_mtp ? 0 : TENSOR_SKIP; - layer.ffn_gate_inp_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP_SHEXP, "weight", il), { n_embd }, 0); - layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", il), { n_embd, n_ff_shexp }, 0); - layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", il), { n_embd, n_ff_shexp }, 0); - layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", il), { n_ff_shexp, n_embd }, 0); + const int64_t n_ff_exp = hparams.n_ff_exp ? hparams.n_ff_exp : n_ff / n_expert_used; + const int64_t n_ff_shexp = hparams.n_ff_shexp ? hparams.n_ff_shexp : n_ff; + const int64_t idx_dim = hparams.indexer_head_size; + + layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { hc_dim }, flags); + layer.hc_attn_down = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN, "weight", il), { hc_dim, hc_lr }, flags); + layer.hc_attn_up = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP, "weight", il), { hc_lr, hc_dim }, flags); + layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, flags); + layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { hc_dim }, flags); + layer.hc_ffn_down = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN, "weight", il), { hc_dim, hc_lr }, flags); + layer.hc_ffn_up = create_tensor(tn(LLM_TENSOR_HC_FFN_UP, "weight", il), { hc_lr, hc_dim }, flags); + layer.hc_ffn_inject = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT, "weight", il), { hc_dim, hc }, flags); + + create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, flags); + layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, flags); + layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, flags); + layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, flags); + + // the draft attends dense, so the indexer weights are present but never read + const int idx_flags = flags | TENSOR_NOT_REQUIRED | TENSOR_SKIP; + layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", il), { n_embd, hparams.indexer_n_head * idx_dim }, idx_flags); + layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", il), { n_embd, idx_dim }, idx_flags); + layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", il), { idx_dim }, idx_flags); + layer.index_k_norm = create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight", il), { idx_dim }, idx_flags); + + layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", il), { n_embd, n_expert }, flags); + layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", il), { n_ff_exp, n_embd, n_expert }, flags); + create_tensor_gate_up_exps(layer, il, n_embd, n_ff_exp, n_expert, flags); + + layer.ffn_gate_inp_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP_SHEXP, "weight", il), { n_embd }, flags); + layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", il), { n_embd, n_ff_shexp }, flags); + layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", il), { n_embd, n_ff_shexp }, flags); + layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", il), { n_ff_shexp, n_embd }, flags); + + // hnorm spans the whole hyper-connection row, enorm just one stream + layer.nextn.enorm = create_tensor(tn(LLM_TENSOR_NEXTN_ENORM, "weight", il), { n_embd }, flags); + layer.nextn.hnorm = create_tensor(tn(LLM_TENSOR_NEXTN_HNORM, "weight", il), { hc_dim }, flags); + layer.nextn.eh_proj = create_tensor(tn(LLM_TENSOR_NEXTN_EH_PROJ, "weight", il), { 2 * n_embd, n_embd }, flags); } + } std::unique_ptr llama_model_qwen4exp::build_arch_graph(const llm_graph_params & params) const { + if (params.gtype == LLM_GRAPH_TYPE_DECODER_MTP) { + return std::make_unique(*this, params); + } return std::make_unique(*this, params); } +// ref: PR 27739 (JJJYmmm), reconciled against this tree's hyper-connection helpers. +// The draft is one block: embedding and handed-over hidden state are each normed, joined +// and projected, then run through the same attention and FFN the trunk uses. +llama_model_qwen4exp::graph_mtp::graph_mtp(const llama_model & model, const llm_graph_params & params) + : graph(model, params, true) { + GGML_ASSERT(hparams.n_layer_nextn == 1 && "qwen4exp MTP supports a single block"); + GGML_ASSERT(ubatch.token && "qwen4exp MTP needs token input"); + + const int64_t hc = hparams.dsv4_hc_mult; + const int64_t n_embd = hparams.n_embd; + + const int il = hparams.n_layer(); + const auto & layer = model.layers[il]; + + GGML_ASSERT(layer.nextn.eh_proj && "MTP block is missing nextn.eh_proj"); + GGML_ASSERT(layer.nextn.enorm && "MTP block is missing nextn.enorm"); + GGML_ASSERT(layer.nextn.hnorm && "MTP block is missing nextn.hnorm"); + + int sections[4]; + std::copy(std::begin(hparams.rope_sections), std::begin(hparams.rope_sections) + 4, sections); + + auto inp = std::make_unique(hparams.n_embd_out()); + + inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_tokens); + ggml_set_input(inp->tokens); + + inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.n_embd_out(), n_tokens); + ggml_set_input(inp->embd); + + inp->h = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.n_embd_out(), n_tokens); + ggml_set_input(inp->h); + ggml_set_name(inp->h, "mtp_h_input"); + + ggml_tensor * tok_embd = ggml_get_rows(ctx0, model.tok_embd, inp->tokens); + cb(tok_embd, "mtp_tok_embd", il); + + ggml_tensor * h = inp->h; + res->add_input(std::move(inp)); + + ggml_tensor * inp_pos = build_inp_pos(); + ggml_tensor * inp_out_ids = build_inp_out_ids(); + // The draft block is the only layer in an MTP context and attends dense, so it takes a + // plain attention input. create_memory gives that context an attention-only filter. + auto * inp_attn = build_attn_inp_kv(); + + // Grouped RMSNorm: each hc stream is normed over its own n_embd, and gamma is applied + // across the whole [hc*n_embd] row. Norming the full row instead couples the streams + // through a shared scale, which is not what the head was trained on and measurably + // costs draft acceptance. + ggml_tensor * h_norm = ggml_reshape_3d(ctx0, h, n_embd, hc, n_tokens); + h_norm = ggml_rms_norm(ctx0, h_norm, hparams.f_norm_rms_eps); + h_norm = ggml_reshape_2d(ctx0, h_norm, hc*n_embd, n_tokens); + h_norm = ggml_mul(ctx0, h_norm, layer.nextn.hnorm); + h_norm = ggml_reshape_3d(ctx0, h_norm, n_embd, hc, n_tokens); + cb(h_norm, "mtp_hnorm", il); + + // every stream sees the same embedding term + ggml_tensor * e_norm = build_norm(tok_embd, layer.nextn.enorm, nullptr, LLM_NORM_RMS, il); + e_norm = ggml_repeat_4d(ctx0, ggml_reshape_3d(ctx0, e_norm, n_embd, 1, n_tokens), n_embd, hc, n_tokens, 1); + cb(e_norm, "mtp_enorm", il); + + // fc_embedding(e) + fc_hidden(h) is one projection of the concatenation; the converter + // merges the two checkpoint tensors into this single eh_proj + ggml_tensor * inpL = build_lora_mm(layer.nextn.eh_proj, + ggml_concat(ctx0, e_norm, h_norm, 0), layer.nextn.eh_proj_s); + cb(inpL, "mtp_eh_proj", il); + + ggml_tensor * inject = nullptr; + ggml_tensor * cur = build_hc_mix(inpL, + layer.hc_attn_norm, layer.hc_attn_down, layer.hc_attn_up, layer.hc_attn_inject, &inject, il); + cb(cur, "mtp_hc_attn_pre", il); + + // dense attention for the draft: a QSA indexer would need a cache of its own, and one + // block spends its time reading weights rather than attending + cur = build_layer_attn(inp_attn, nullptr, cur, inp_pos, sections, il); + inpL = build_hc_combine(inpL, cur, inject, il); + cb(inpL, "mtp_hc_attn_post", il); + + cur = build_hc_mix(inpL, + layer.hc_ffn_norm, layer.hc_ffn_down, layer.hc_ffn_up, layer.hc_ffn_inject, &inject, il); + cb(cur, "mtp_hc_ffn_pre", il); + + cur = build_layer_ffn(cur, il); + cb(cur, "mtp_ffn_out", il); + + inpL = build_hc_combine(inpL, cur, inject, il); + cb(inpL, "mtp_l_out", il); + + ggml_tensor * flat = ggml_reshape_2d(ctx0, inpL, hc*n_embd, n_tokens); + + // a chained head reads the streams back the way the trunk hands them over + res->t_h_nextn = flat; + + if (inp_out_ids) { + flat = ggml_get_rows(ctx0, flat, inp_out_ids); + inpL = ggml_reshape_3d(ctx0, flat, n_embd, hc, n_outputs); + } + + cur = build_hc_mix(inpL, model.hc_head_norm, model.hc_head_down, model.hc_head_up, nullptr, nullptr, -1); + cb(cur, "result_norm", -1); + res->t_embd = cur; + + cur = build_lora_mm(model.output, cur, model.output_s); + cb(cur, "result_output", -1); + res->t_logits = cur; + + ggml_build_forward_expand(gf, cur); +} + // Hyper-connections keep hc parallel residual streams [n_embd, hc, T] in place of layer norms. // Returns the mixed [n_embd, T] stream; `inject` gets the [hc, T] scatter weights. ggml_tensor * llama_model_qwen4exp::graph::build_hc_mix( @@ -302,6 +468,10 @@ ggml_tensor * llama_model_qwen4exp::graph::build_hc_combine( return cur; } +// members only: graph_mtp builds a single block instead of the trunk +llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_params & params, bool) : + llm_build_delta_net_base(params), model(model) {} + llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_params & params) : llm_build_delta_net_base(params), model(model) { const int64_t hc = hparams.dsv4_hc_mult; @@ -366,7 +536,8 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa cur = build_layer_attn(inp->get_attn(), mctx_hyb, cur, inp_pos, sections, il); } - if (il == n_layer - 1 && inp_out_ids) { + if (il == n_layer - 1 && inp_out_ids && + (!cparams.embeddings_nextn || cparams.embeddings_nextn_masked)) { // everything below is per token, so drop the rows that produce no output cur = ggml_get_rows(ctx0, cur, inp_out_ids); inject = ggml_get_rows(ctx0, inject, inp_out_ids); @@ -394,6 +565,26 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa cb(res_hc, "l_last", il); } + // Hand the wide residual to an MTP head once the last layer has folded its attention and + // FFN into it: the head was trained on the trunk's final residual, not on the last layer's + // input. The final mix below is the output norm of the LM head, so capture before it. The + // draft reads all hc streams, which is what its hnorm is sized for. res_hc is passed as is + // rather than reshaped: a bare view is not a node the scheduler places, and the extraction + // is a flat copy, for which [n_embd, hc, T] and [hc*n_embd, T] are identical. + // + // An unmasked request wants every position, so the last layer ran uncropped above and the + // rows for the final mix are gathered only after this hand-over. + if (cparams.embeddings_nextn) { + res->t_h_nextn = res_hc; + cb(res->t_h_nextn, "h_nextn", -1); + + if (!cparams.embeddings_nextn_masked && inp_out_ids) { + res_hc = ggml_reshape_2d(ctx0, res_hc, n_embd*hc, res_hc->ne[2]); + res_hc = ggml_get_rows(ctx0, res_hc, inp_out_ids); + res_hc = ggml_reshape_3d(ctx0, res_hc, n_embd, hc, res_hc->ne[1]); + } + } + // the final mixer is the output norm: there is no separate one ggml_tensor * cur = build_hc_mix(res_hc, model.hc_head_norm, model.hc_head_down, model.hc_head_up, @@ -724,8 +915,9 @@ ggml_tensor * llama_model_qwen4exp::graph::build_layer_attn( const int64_t n_embd_head = hparams.n_embd_head_v(); GGML_ASSERT(n_embd_head == hparams.n_embd_head_k()); - // indexer reads the same block input as q/k/v; no cache or no ratio means dense - const bool qsa = mctx_hyb->get_idx() != nullptr && hparams.dsv4_compress_ratios[il] > 0; + // indexer reads the same block input as q/k/v; no context (the MTP draft attends + // dense, see build_layer_attn's caller in graph_mtp), no cache, or no ratio means dense + const bool qsa = mctx_hyb != nullptr && mctx_hyb->get_idx() != nullptr && hparams.dsv4_compress_ratios[il] > 0; ggml_tensor * top_k = qsa ? build_qsa_top_k(mctx_hyb, cur, inp_pos, inp->get_kq_mask(), sections, il) : nullptr; From bb10d0834d992a824f949fcce7d4030d85b7f064 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti Date: Sun, 30 Aug 2026 22:59:42 +0100 Subject: [PATCH 012/212] qwen4exp: fix --ngram-on-disk, which crashes on every model (#8) --- src/models/qwen4exp.cpp | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index c10a2880352a..ca47c3706cae 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -1182,7 +1182,19 @@ class llm_graph_input_ple : public llm_graph_input_i { bool can_reuse(const llm_graph_params & params) override { mctx = static_cast(params.mctx)->get_attn(); - return rows->ne[0] == (int64_t) pmodel.hparams.ple_n_heads * params.ubatch.n_tokens; + + const int64_t n_want = (int64_t) pmodel.hparams.ple_n_heads * params.ubatch.n_tokens; + + // exactly one of the two is built: `rows` when the table is a tensor and the + // graph does the get_rows, `embd` when --ngram-on-disk gathered it in set_input. + // Dereferencing `rows` unconditionally segfaults on every on-disk decode. + if (rows) { + return rows->ne[0] == n_want; // I32 [n_heads*n_tokens] + } + if (embd) { + return embd->ne[1] == n_want; // F32 [ple_head_dim, n_heads*n_tokens] + } + return false; } ggml_tensor * rows = nullptr; // I32 [ple_n_heads * n_tokens] @@ -1261,17 +1273,14 @@ void llm_graph_input_ple::set_input(const llama_ubatch * ubatch) { } if (pmodel.ple_disk) { - // idx is head-major and head-local; the reader wants one global row per (token, head) - // in token-major order, which is the layout the per-head gather + concat produces. + // idx is already exactly what the reader wants: one global row per (token, head), + // token-major, with ple_head_offsets folded in by the loop above. The previous + // version read it head-major and added the offset a second time, which is only + // correct for a table split per head -- on a joined per_layer_token_embd it walks + // past the end (rows beyond ple_rows) and gather() aborts. GGML_ASSERT(embd != nullptr && rows == nullptr); - rows_global.resize(idx.size()); - for (int64_t i = 0; i < n_tokens; ++i) { - for (int64_t h = 0; h < n_heads; ++h) { - rows_global[i*n_heads + h] = (int32_t) (idx[h*n_tokens + i] + (int64_t) hp.ple_head_offsets[h]); - } - } - embd_buf.resize(rows_global.size() * (size_t) hp.ple_head_dim); - pmodel.ple_disk->gather(rows_global.data(), rows_global.size(), embd_buf.data()); + embd_buf.resize(idx.size() * (size_t) hp.ple_head_dim); + pmodel.ple_disk->gather(idx.data(), idx.size(), embd_buf.data()); ggml_backend_tensor_set(embd, embd_buf.data(), 0, embd_buf.size() * sizeof(float)); return; } From 809972e3fca2e62522c233cb3782b12494166bde Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Sun, 30 Aug 2026 23:00:12 +0100 Subject: [PATCH 013/212] merge upstream (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Harkirat Gill Co-authored-by: Bartowski <3266127+bartowski1182@users.noreply.github.com> Co-authored-by: Xuan Son Nguyen Co-authored-by: Aparna M P Co-authored-by: cqderek Co-authored-by: Daniele Zannotti Co-authored-by: Claude Opus 5 Co-authored-by: Ruixiang Wang Co-authored-by: Sigbjørn Skjæret Co-authored-by: Georgi Gerganov Co-authored-by: Brad Smith <1472326+infinitewarp@users.noreply.github.com> Co-authored-by: Nathanw1014 <67372905+Nathanw1014@users.noreply.github.com> Co-authored-by: Titaniumtown Co-authored-by: Ozymandias_EBON <112784549+johnkarlhill@users.noreply.github.com> Co-authored-by: Zijun Yu Co-authored-by: Mostafa Faheem Co-authored-by: Ravi Panchumarthy Co-authored-by: zhaixuejun1993 Co-authored-by: Strongtut Co-authored-by: Strongtut <8432058+Strongtut@users.noreply.github.com> Co-authored-by: ravel7524 <58877666+ravel7524@users.noreply.github.com> Co-authored-by: Jeff Bolz Co-authored-by: Tekin Ertekin Co-authored-by: Eric A Stalee <87948564+Eric-A-Stalee@users.noreply.github.com> Co-authored-by: kurquhar Co-authored-by: Kristopher Urquhart Co-authored-by: Jhen-Jie Hong Co-authored-by: Nick Farrell Co-authored-by: Niklas Wenzel Co-authored-by: Hongqiang Wang Co-authored-by: Li He Co-authored-by: codemonkey <441345965@qq.com> Co-authored-by: Aman Gupta Co-authored-by: fairydreaming <166155368+fairydreaming@users.noreply.github.com> Co-authored-by: Stanisław Szymczyk Co-authored-by: AesSedai <7980540+AesSedai@users.noreply.github.com> Co-authored-by: QuintinShaw Co-authored-by: Max Krasnyansky Co-authored-by: Marco Colombo Co-authored-by: Alexander Lu Co-authored-by: Ehsan Bateni Co-authored-by: Ryan C Co-authored-by: Alessandro de Oliveira Faria (A.K.A.CABELO) Co-authored-by: Daya Adianto Co-authored-by: Nils Gladitz Co-authored-by: JamePeng Co-authored-by: LunalFresh <165352784+LunalFresh@users.noreply.github.com> Co-authored-by: itterative <190138728+itterative@users.noreply.github.com> Co-authored-by: Pascal Co-authored-by: Marshall --- .devops/openvino.Dockerfile | 12 +- .github/workflows/build-cache.yml | 8 +- .github/workflows/build-openvino.yml | 19 +- .github/workflows/build-self-hosted.yml | 4 +- .github/workflows/release.yml | 33 +- ci/run.sh | 4 +- common/arg.cpp | 49 +- common/common.cpp | 6 +- common/common.h | 7 +- common/speculative.cpp | 18 +- conversion/qwen.py | 18 +- conversion/qwen4exp.py | 52 +- docs/backend/OPENVINO.md | 86 +- docs/backend/snapdragon/windows.md | 13 +- ggml/include/ggml-backend.h | 4 + ggml/include/ggml.h | 7 + ggml/src/ggml-backend-impl.h | 12 +- ggml/src/ggml-backend.cpp | 89 +- ggml/src/ggml-cann/aclnn_ops.cpp | 45 +- ggml/src/ggml-cann/aclnn_ops.h | 1 + ggml/src/ggml-cann/ggml-cann.cpp | 4 + ggml/src/ggml-cpu/ggml-cpu.c | 4 +- ggml/src/ggml-cpu/ops.cpp | 195 ++- ggml/src/ggml-cuda/common.cuh | 3 +- ggml/src/ggml-cuda/ggml-cuda.cu | 24 +- ggml/src/ggml-cuda/mmid.cu | 15 +- ggml/src/ggml-cuda/mmq-config-rdna3.cuh | 316 ++-- ggml/src/ggml-cuda/mmq-load-tiles.cuh | 8 + ggml/src/ggml-cuda/mmvf.cu | 8 +- ggml/src/ggml-cuda/mmvq.cu | 8 +- ggml/src/ggml-cuda/unary.cu | 75 + ggml/src/ggml-cuda/unary.cuh | 9 + ggml/src/ggml-cuda/vecdotq.cuh | 8 + ggml/src/ggml-et/et-kernels/src/glu_f32.c | 57 +- ggml/src/ggml-et/ggml-et-cpu-compare.cpp | 7 +- ggml/src/ggml-et/ggml-et-ops.cpp | 3 + ggml/src/ggml-et/ggml-et.cpp | 3 +- ggml/src/ggml-hexagon/ggml-hexagon.cpp | 327 ++-- ggml/src/ggml-hexagon/htp-drv.cpp | 10 + ggml/src/ggml-hexagon/htp-drv.h | 2 + ggml/src/ggml-hexagon/htp/act-ops.c | 28 +- ggml/src/ggml-hexagon/htp/htp-ops.h | 3 + ggml/src/ggml-hexagon/htp/hvx-arith.h | 28 + ggml/src/ggml-hexagon/htp/hvx-log.h | 24 + ggml/src/ggml-hexagon/htp/main.c | 3 + ggml/src/ggml-hexagon/htp/unary-ops.c | 70 +- ggml/src/ggml-hexagon/htp/unary-ops.h | 2 + ggml/src/ggml-metal/ggml-metal-common.cpp | 17 + ggml/src/ggml-metal/ggml-metal-common.h | 4 + ggml/src/ggml-metal/ggml-metal-device.cpp | 4 +- ggml/src/ggml-metal/ggml-metal-device.m | 41 +- ggml/src/ggml-metal/ggml-metal-impl.h | 1 + ggml/src/ggml-metal/ggml-metal-ops.cpp | 25 +- ggml/src/ggml-metal/ggml-metal-tuning.cpp | 1420 ++++++++++++++++- ggml/src/ggml-metal/ggml-metal.cpp | 4 +- ggml/src/ggml-metal/kernels/conv.metal | 7 +- ggml/src/ggml-metal/kernels/unary.metal | 26 + ggml/src/ggml-opencl/ggml-opencl.cpp | 40 +- ggml/src/ggml-opencl/kernels/glu.cl | 65 + ggml/src/ggml-openvino/CMakeLists.txt | 2 + ggml/src/ggml-openvino/ggml-decoder.cpp | 153 +- ggml/src/ggml-openvino/ggml-decoder.h | 12 +- .../src/ggml-openvino/ggml-openvino-extra.cpp | 12 +- ggml/src/ggml-openvino/ggml-openvino.cpp | 231 +-- ggml/src/ggml-openvino/openvino/op/cpy.cpp | 140 +- .../openvino/op/flash_attn_ext.cpp | 104 +- .../openvino/op/gated_delta_net.cpp | 25 + .../openvino/op/glu_geglu_quick.cpp | 64 + .../ggml-openvino/openvino/op/glu_swiglu.cpp | 15 + .../src/ggml-openvino/openvino/op/pool_2d.cpp | 53 + ggml/src/ggml-openvino/openvino/op/roll.cpp | 36 + ggml/src/ggml-openvino/openvino/op/view.cpp | 7 + ggml/src/ggml-openvino/openvino/op_table.cpp | 6 + ggml/src/ggml-openvino/openvino/op_table.h | 4 + .../openvino/pass/fuse_to_conv.cpp | 212 +++ .../openvino/pass/fuse_to_conv.h | 17 + .../openvino/translate_session.cpp | 6 +- ggml/src/ggml-openvino/openvino/utils.cpp | 1 + ggml/src/ggml-openvino/utils.cpp | 159 +- ggml/src/ggml-openvino/utils.h | 4 +- ggml/src/ggml-rpc/CMakeLists.txt | 6 +- ggml/src/ggml-rpc/ggml-rpc.cpp | 6 +- ggml/src/ggml-rpc/transport-apple.cpp | 31 +- ggml/src/ggml-sycl/element_wise.cpp | 101 ++ ggml/src/ggml-sycl/element_wise.hpp | 1 + ggml/src/ggml-sycl/fattn-common.hpp | 22 +- ggml/src/ggml-sycl/fattn-onednn.cpp | 136 +- ggml/src/ggml-sycl/fattn-onednn.hpp | 6 +- ggml/src/ggml-sycl/fattn.cpp | 79 +- ggml/src/ggml-sycl/fattn.hpp | 18 + ggml/src/ggml-sycl/ggml-sycl.cpp | 303 +++- ggml/src/ggml-virtgpu/ggml-backend.cpp | 3 +- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 165 +- .../ggml-vulkan/vulkan-shaders/conv2d_mm.comp | 9 +- .../ggml-vulkan/vulkan-shaders/conv3d_mm.comp | 9 +- .../vulkan-shaders/count_experts.comp | 88 +- .../vulkan-shaders/generic_unary_head.glsl | 14 +- .../ggml-vulkan/vulkan-shaders/glu_head.glsl | 8 +- .../ggml-vulkan/vulkan-shaders/mul_mm.comp | 34 +- .../vulkan-shaders/mul_mm_cm2.comp | 38 +- .../vulkan-shaders/mul_mm_id_funcs.glsl | 15 + .../ggml-vulkan/vulkan-shaders/mul_mmq.comp | 34 +- .../ggml-vulkan/vulkan-shaders/sum_rows.glsl | 10 +- .../vulkan-shaders/swiglu_clamp.comp | 12 + .../src/ggml-vulkan/vulkan-shaders/utils.glsl | 18 +- .../vulkan-shaders/vulkan-shaders-gen.cpp | 3 + .../ggml-webgpu/ggml-webgpu-shader-lib.hpp | 4 + ggml/src/ggml-webgpu/ggml-webgpu.cpp | 3 +- ggml/src/ggml-webgpu/wgsl-shaders/glu.wgsl | 8 + ggml/src/ggml.c | 15 +- gguf-py/gguf/constants.py | 4 + gguf-py/gguf/gguf_writer.py | 10 +- gguf-py/gguf/lazy.py | 4 + include/llama.h | 17 +- scripts/snapdragon/build.py | 18 +- scripts/snapdragon/sdk.py | 62 + scripts/snapdragon/setup-sdk.py | 233 +++ scripts/sync_vendor.py | 2 +- src/CMakeLists.txt | 1 + src/llama-context.cpp | 120 +- src/llama-graph.cpp | 18 +- src/llama-kv-cache.cpp | 2 +- src/llama-kv-cells.h | 9 +- src/llama-model-loader.cpp | 83 +- src/llama-model-loader.h | 56 +- src/llama-model.cpp | 21 +- src/llama-model.h | 1 + src/llama-ple-disk.cpp | 320 ++++ src/llama-ple-disk.h | 38 + src/llama.cpp | 2 +- src/models/dflash.cpp | 68 +- src/models/minimax-01.cpp | 2 +- src/models/models.h | 20 +- src/models/qwen4exp.cpp | 381 ++++- tests/CMakeLists.txt | 13 +- tests/test-alloc.cpp | 70 +- tests/test-backend-ops.cpp | 88 +- tests/test-llama-archs.cpp | 14 +- tests/test-save-load-state.cpp | 155 +- tools/cli/README.md | 2 +- tools/completion/README.md | 2 +- tools/llama-bench/README.md | 1 + tools/llama-bench/llama-bench.cpp | 65 +- tools/server/README.md | 3 +- tools/server/server-context.cpp | 46 +- tools/server/server.cpp | 12 + vendor/cpp-httplib/httplib.cpp | 1133 ++++++++++--- vendor/cpp-httplib/httplib.h | 142 +- 148 files changed, 7681 insertions(+), 1404 deletions(-) create mode 100644 ggml/src/ggml-openvino/openvino/op/glu_geglu_quick.cpp create mode 100644 ggml/src/ggml-openvino/openvino/op/pool_2d.cpp create mode 100644 ggml/src/ggml-openvino/openvino/op/roll.cpp create mode 100644 ggml/src/ggml-openvino/openvino/pass/fuse_to_conv.cpp create mode 100644 ggml/src/ggml-openvino/openvino/pass/fuse_to_conv.h create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/swiglu_clamp.comp create mode 100644 scripts/snapdragon/sdk.py create mode 100644 scripts/snapdragon/setup-sdk.py create mode 100644 src/llama-ple-disk.cpp create mode 100644 src/llama-ple-disk.h diff --git a/.devops/openvino.Dockerfile b/.devops/openvino.Dockerfile index a43e5c4993f8..13301ba287dd 100644 --- a/.devops/openvino.Dockerfile +++ b/.devops/openvino.Dockerfile @@ -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 diff --git a/.github/workflows/build-cache.yml b/.github/workflows/build-cache.yml index 187427a8d4b0..4a23ec2d4d36 100644 --- a/.github/workflows/build-cache.yml +++ b/.github/workflows/build-cache.yml @@ -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 diff --git a/.github/workflows/build-openvino.yml b/.github/workflows/build-openvino.yml index 0316e7ad97e3..8e0326f4a44f 100644 --- a/.github/workflows/build-openvino.yml +++ b/.github/workflows/build-openvino.yml @@ -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 diff --git a/.github/workflows/build-self-hosted.yml b/.github/workflows/build-self-hosted.yml index fe2ab815473c..ccfe2a604645 100644 --- a/.github/workflows/build-self-hosted.yml +++ b/.github/workflows/build-self-hosted.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab5d3389bd58..76717d064bd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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,12 +736,6 @@ 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 @@ -804,17 +798,15 @@ jobs: -DCMAKE_PREFIX_PATH="${env:HIP_PATH}" ` -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 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: | @@ -866,8 +858,11 @@ jobs: - name: Pack artifacts run: | - cp "LICENSE" "build\bin\Release\" - 7z a -snl llama-bin-win-rocm-${{ env.ROCM_VERSION_SHORT }}-${{ matrix.build }}.zip .\build\bin\Release\* + 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 diff --git a/ci/run.sh b/ci/run.sh index 1f1e4bc033c9..1701bc7ed058 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -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 diff --git a/common/arg.cpp b/common/arg.cpp index e4f694b84b3e..84e2800256bb 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -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( @@ -2721,18 +2729,18 @@ common_params_context common_params_parser_init(common_params & params, llama_ex } ).set_env("LLAMA_ARG_LOAD_MODE")); add_opt(common_arg( - {"--tensor-read-lazy"}, "MODE", + {"-lzm", "--lazy-mode"}, "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; } + /**/ if (value == "on") { params.lazy_mode = LLAMA_LAZY_MODE_ON; } + else if (value == "auto") { params.lazy_mode = LLAMA_LAZY_MODE_AUTO; } + else if (value == "off") { params.lazy_mode = LLAMA_LAZY_MODE_OFF; } else { throw std::invalid_argument("invalid value"); } } - ).set_env("LLAMA_ARG_TENSOR_READ_LAZY")); + ).set_env("LLAMA_ARG_LAZY_MODE")); add_opt(common_arg( {"--numa"}, "TYPE", "attempt optimizations that help on some NUMA systems\n" @@ -2770,6 +2778,37 @@ common_params_context common_params_parser_init(common_params & params, llama_ex parse_tensor_buffer_overrides(value, params.tensor_buft_overrides); } ).set_env("LLAMA_ARG_OVERRIDE_TENSOR")); + add_opt(common_arg( + {"--ngram-on-disk"}, + "keep the model's n-gram hash-embedding table (per_layer_token_embd, 28.8 GB on Qwen3.8-Flash-Next)\n" + "on disk: it is never mapped or loaded, each batch reads just the rows it gathers from the GGUF.\n" + "Only qwen4exp has such a table; on any other model this is a no-op", + [](common_params & params) { + params.ple_on_disk = true; + } + ).set_env("LLAMA_ARG_NGRAM_ON_DISK")); + add_opt(common_arg( + {"--ngram-io-threads"}, "N", + string_format("threads reading n-gram rows for --ngram-on-disk (default: %d)", params.ple_io_threads), + [](common_params & params, int value) { + params.ple_io_threads = value; + } + ).set_env("LLAMA_ARG_NGRAM_IO_THREADS")); + add_opt(common_arg( + {"--ngram-cache"}, "MiB", + string_format("in-memory cache of recently read n-gram rows for --ngram-on-disk, 0 disables (default: %d)", params.ple_cache_mb), + [](common_params & params, int value) { + params.ple_cache_mb = value; + } + ).set_env("LLAMA_ARG_NGRAM_CACHE")); + add_opt(common_arg( + {"--ngram-direct-io"}, + {"--no-ngram-direct-io"}, + string_format("read n-gram rows with O_DIRECT so they bypass the page cache (default: %s)", params.ple_direct_io ? "enabled" : "disabled"), + [](common_params & params, bool value) { + params.ple_direct_io = value; + } + ).set_env("LLAMA_ARG_NGRAM_DIRECT_IO")); add_opt(common_arg( {"-cmoe", "--cpu-moe"}, "keep all Mixture of Experts (MoE) weights in the CPU", diff --git a/common/common.cpp b/common/common.cpp index 347e8e9fc416..a6a5364163bc 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -1688,11 +1688,15 @@ 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.lazy_mode = params.lazy_mode; mparams.tensor_split = params.tensor_split; mparams.check_tensors = params.check_tensors; mparams.use_extra_bufts = !params.no_extra_bufts; mparams.no_host = params.no_host; + mparams.ple_on_disk = params.ple_on_disk; + mparams.ple_direct_io = params.ple_direct_io; + mparams.ple_io_threads = params.ple_io_threads; + mparams.ple_cache_mb = params.ple_cache_mb; if (params.kv_overrides.empty()) { mparams.kv_overrides = NULL; diff --git a/common/common.h b/common/common.h index 478825b6b3ff..0162a5f1e638 100644 --- a/common/common.h +++ b/common/common.h @@ -505,7 +505,7 @@ 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 + enum llama_lazy_mode lazy_mode = LLAMA_LAZY_MODE_AUTO; // on-demand reading of tensors marked by the arch common_cpu_params cpuparams; common_cpu_params cpuparams_batch; @@ -603,6 +603,10 @@ struct common_params { bool no_op_offload = false; // globally disable offload host tensor operations to device bool no_extra_bufts = false; // disable extra buffer types (used for weight repacking) bool no_host = false; // bypass host buffer allowing extra buffers to be used + bool ple_on_disk = false; // keep the n-gram hash-embedding table on disk (qwen4exp) + bool ple_direct_io = true; // ... read with O_DIRECT + int32_t ple_io_threads = 64; // ... parallel readers (random 4 KiB reads: this NVMe gives 62k IOPS at 16, 130k at 64, ~160k at 128+) + int32_t ple_cache_mb = 256; // ... row cache, 0 disables bool single_turn = false; // single turn chat conversation @@ -649,6 +653,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. diff --git a/common/speculative.cpp b/common/speculative.cpp index 2e90d8e59a43..f4968994a83e 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -1016,6 +1016,9 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { // 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; @@ -1053,12 +1056,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()); adaptive_n = this->params.adaptive; 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); @@ -1118,7 +1134,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl { // 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, false); // DFlash needs non-causal attention + llama_set_causal_attn(ctx_dft, causal_attn); // DFlash needs non-causal attention unless the model says otherwise } ~common_speculative_impl_draft_dflash() override { diff --git a/conversion/qwen.py b/conversion/qwen.py index c5297418c99b..419611896fc3 100644 --- a/conversion/qwen.py +++ b/conversion/qwen.py @@ -709,14 +709,20 @@ def set_gguf_parameters(self): 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(): @@ -737,6 +743,8 @@ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Ca 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 = ( @@ -815,6 +823,10 @@ def set_gguf_parameters(self): 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 @@ -833,7 +845,7 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter 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 diff --git a/conversion/qwen4exp.py b/conversion/qwen4exp.py index 168796d616b9..e23cf56fa706 100644 --- a/conversion/qwen4exp.py +++ b/conversion/qwen4exp.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import Iterable, cast +import json +import logging +from typing import Callable, Iterable, cast import torch from torch import Tensor @@ -26,8 +28,6 @@ class Qwen4ExpTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase): 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) @@ -63,9 +63,11 @@ def set_gguf_parameters(self): 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)] - ) + ratios = [ratio if layer_types[i] == "full_attention" else 0 for i in range(n_layer)] + # the MTP block extends block_count, and this array is read per block. The draft + # attends dense, so it takes 0, the same value a non-QSA layer gets. + ratios += [0] * (self.block_count - n_layer) + self.gguf_writer.add_attention_compress_ratios(ratios) # 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 @@ -105,6 +107,44 @@ def _eos_token_id(self) -> int: raise ValueError("eos_token_id is required: the PLE hash resets its n-grams on it") return int(eos) + @classmethod + def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None: + name = item[0] + # The MTP block carries its own final hyper-connection mixer. An MTP-only file has + # no trunk, so that one becomes the file's mixer; a full model already has the + # trunk's, and graph_mtp reads it, so this copy is dropped. + if name.startswith("mtp.hyper_connection_mixer."): + if cls.no_mtp or not cls.mtp_only: + return None + return (name.replace("mtp.", "model.", 1), item[1]) + return super().filter_tensors(item) + + def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]: + yield from super().generate_extra_tensors() + + # the reference adds the two MTP input projections, and A*e + B*h == [A|B]*concat(e, h), + # so they join into the one eh_proj the tensor map already handles + e_name = "mtp.fc_embedding.weight" + h_name = "mtp.fc_hidden.weight" + + have_e = e_name in self.model_tensors + have_h = h_name in self.model_tensors + if not have_e and not have_h: + return + if not have_e or not have_h: + raise KeyError(f"unpaired MTP input projection: need both {e_name} and {h_name}") + + from .base import LazyTorchTensor + + e = LazyTorchTensor.to_eager(self.model_tensors[e_name]()) + h = LazyTorchTensor.to_eager(self.model_tensors[h_name]()) + yield (self.format_tensor_name(gguf.MODEL_TENSOR.NEXTN_EH_PROJ, + self.hparams["num_hidden_layers"]), + torch.cat([e, h], dim=1).contiguous()) + + del self.model_tensors[e_name] + del self.model_tensors[h_name] + 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"): diff --git a/docs/backend/OPENVINO.md b/docs/backend/OPENVINO.md index 3cdf631cebc2..9b43807d36b3 100644 --- a/docs/backend/OPENVINO.md +++ b/docs/backend/OPENVINO.md @@ -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 ```
-Click to expand ubuntu-llamacpp-ov-install.sh +Click to expand build-llamacpp-ov.sh ```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.
-#### 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 ```
-Click to expand windows-llamacpp-ov-install.bat +Click to expand build-llamacpp-ov.bat ```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**.
@@ -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 diff --git a/docs/backend/snapdragon/windows.md b/docs/backend/snapdragon/windows.md index 3f7d60dd95f8..886cfda3f653 100644 --- a/docs/backend/snapdragon/windows.md +++ b/docs/backend/snapdragon/windows.md @@ -24,7 +24,18 @@ must be included in the .cat file digitally signed with a trusted certificate. This document covers details on how to generate personal certificate files (.pfx) and how to configure the system to allow for test signatures (aka test-signing). -## Install the latest Adreno OpenCL SDK +## Install Windows SDKs + +The recommended method is `setup-sdk.py`: + +``` +> python scripts\snapdragon\setup-sdk.py --list-sdk-releases +> python scripts\snapdragon\setup-sdk.py --hexagon --opencl +``` + +It installs the selected SDKs under `C:\Qualcomm` and sets their corresponding environment variables for the current user. Start a new terminal after it completes; native Windows builds check all SDK paths before CMake runs. + +Select the SDKs to install with `--hexagon` and `--opencl`; use both to prepare a dual-backend build. To select a different available version, pass it to the SDK option, for example `--hexagon 6.4.0.2`. SDK versions install side by side, so you can switch versions without deleting an existing installation. Use `--force` to reinstall the selected SDKs. Use a new CMake build directory after each switch because CMake caches the SDK paths. Either use the trimmed down version (optimized for CI) from diff --git a/ggml/include/ggml-backend.h b/ggml/include/ggml-backend.h index cc3f8cd36e35..27375bd0a51e 100644 --- a/ggml/include/ggml-backend.h +++ b/ggml/include/ggml-backend.h @@ -424,6 +424,10 @@ extern "C" { // Compare the output of two backends GGML_API bool ggml_backend_compare_graph_backend(ggml_backend_t backend1, ggml_backend_t backend2, struct ggml_cgraph * graph, ggml_backend_eval_callback callback, void * user_data, struct ggml_tensor const * const * test_nodes, size_t num_test_nodes); + // returns true for ops that may require additional memory for fleeting data on some backends, + // i.e. the backend's get_alloc_size may return more than ggml_nbytes for the output tensor + GGML_API bool ggml_backend_op_alloc_size_may_expand(enum ggml_op op); + // Tensor initialization GGML_API enum ggml_status ggml_backend_tensor_alloc(ggml_backend_buffer_t buffer, struct ggml_tensor * tensor, void * addr); GGML_API enum ggml_status ggml_backend_view_init(struct ggml_tensor * tensor); diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h index 5f6774a630c0..26f31232f676 100644 --- a/ggml/include/ggml.h +++ b/ggml/include/ggml.h @@ -627,6 +627,7 @@ extern "C" { GGML_GLU_OP_SWIGLU_OAI, GGML_GLU_OP_GEGLU_ERF, GGML_GLU_OP_GEGLU_QUICK, + GGML_GLU_OP_SWIGLU_CLAMP, GGML_GLU_OP_COUNT, }; @@ -1367,6 +1368,12 @@ extern "C" { float alpha, float limit); + GGML_API struct ggml_tensor * ggml_swiglu_clamp( + struct ggml_context * ctx, + struct ggml_tensor * a, + struct ggml_tensor * b, + float limit); + // normalize along rows GGML_API struct ggml_tensor * ggml_norm( struct ggml_context * ctx, diff --git a/ggml/src/ggml-backend-impl.h b/ggml/src/ggml-backend-impl.h index 40cea024c3d2..56f0090cce66 100644 --- a/ggml/src/ggml-backend-impl.h +++ b/ggml/src/ggml-backend-impl.h @@ -103,6 +103,16 @@ extern "C" { // Backend (stream) // + // passed to graph_optimize so the backend can add allocation dependencies: + // if the backend executes parts of the graph out of order (e.g. on concurrent streams), + // it must keep the affected tensors allocated until a node where execution is known to have joined + struct ggml_backend_graph_optimize_params { + // keep `tensor` allocated at least until `until` (a node of the same graph) has been computed + // can be called multiple times for the same tensor: the longest lifetime applies + void (*add_alloc_dep)(void * user_data, struct ggml_tensor * tensor, struct ggml_tensor * until); + void * user_data; + }; + struct ggml_backend_i { const char * (*get_name)(ggml_backend_t backend); @@ -137,7 +147,7 @@ extern "C" { void (*event_wait) (ggml_backend_t backend, ggml_backend_event_t event); // (optional) sort/optimize the nodes in the graph - void (*graph_optimize) (ggml_backend_t backend, struct ggml_cgraph * cgraph); + void (*graph_optimize) (ggml_backend_t backend, struct ggml_cgraph * cgraph, struct ggml_backend_graph_optimize_params * params); }; struct ggml_backend { diff --git a/ggml/src/ggml-backend.cpp b/ggml/src/ggml-backend.cpp index e519bdf50a1b..fec7d7c92bf3 100644 --- a/ggml/src/ggml-backend.cpp +++ b/ggml/src/ggml-backend.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #ifdef __APPLE__ @@ -64,6 +65,13 @@ size_t ggml_backend_buft_get_alloc_size(ggml_backend_buffer_type_t buft, const s if (buft->iface.get_alloc_size) { size_t size = buft->iface.get_alloc_size(buft, tensor); assert(size >= ggml_nbytes(tensor)); + + // [TAG_ALLOC_SIZE_EXPAND] + // if you hit this assert, update ggml_backend_op_alloc_size_may_expand() accordingly + GGML_ASSERT(size <= ggml_nbytes(tensor) || + ggml_op_is_empty(tensor->op) || + ggml_backend_op_alloc_size_may_expand(tensor->op)); + return size; } return ggml_nbytes(tensor); @@ -558,10 +566,10 @@ void ggml_backend_event_wait(ggml_backend_t backend, ggml_backend_event_t event) backend->iface.event_wait(backend, event); } -static void ggml_backend_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * cgraph) { +static void ggml_backend_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * cgraph, struct ggml_backend_graph_optimize_params * params) { GGML_ASSERT(backend); if (backend->iface.graph_optimize != NULL) { - backend->iface.graph_optimize(backend, cgraph); + backend->iface.graph_optimize(backend, cgraph, params); } } @@ -1441,11 +1449,40 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra sched->prev_leaf_backend_ids = tmp; } + // optimize the split graphs and collect the allocation dependencies added by the backends + // this needs to happen before we make graph_copy, so they are in sync + // TODO: this may create many small allocations in the scheduler, restructure to use a flat array + std::unordered_map> alloc_deps; + + struct ggml_backend_graph_optimize_params opt_params = { + /* .add_alloc_dep = */ [](void * user_data, ggml_tensor * tensor, ggml_tensor * until) { + auto & deps = *(std::unordered_map> *) user_data; + std::vector & keep = deps[until]; + if (std::find(keep.begin(), keep.end(), tensor) == keep.end()) { + keep.push_back(tensor); + } + }, + /* .user_data = */ &alloc_deps, + }; + + for (int i = 0; i < sched->n_splits; i++) { + struct ggml_backend_sched_split * split = &sched->splits[i]; + split->graph = ggml_graph_view(graph, split->i_start, split->i_end); + + ggml_backend_graph_optimize(sched->backends[split->backend_id], &split->graph, &opt_params); + } + + // each dep is added to graph_copy as a GGML_OP_NONE node with the kept tensors as srcs + int n_dep_nodes = 0; + for (const auto & it : alloc_deps) { + n_dep_nodes += (it.second.size() + GGML_MAX_SRC - 1) / GGML_MAX_SRC; + } + int total_inputs = sched->n_graph_inputs; for (int i = 0; i < sched->n_splits; i++) { total_inputs += sched->splits[i].n_inputs; } - int graph_size = std::max(graph->n_nodes, graph->n_leafs) + total_inputs * 2 * sched->n_copies; + int graph_size = std::max(graph->n_nodes, graph->n_leafs) + total_inputs * 2 * sched->n_copies + n_dep_nodes; // remember the actual graph_size for performing reallocation checks later [GGML_SCHED_DEBUG_REALLOC] sched->debug_prev_graph_size = sched->debug_graph_size; @@ -1463,13 +1500,10 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra struct ggml_cgraph * graph_copy = &sched->graph; + int n_dep_nodes_added = 0; + for (int i = 0; i < sched->n_splits; i++) { struct ggml_backend_sched_split * split = &sched->splits[i]; - split->graph = ggml_graph_view(graph, split->i_start, split->i_end); - - // Optimize this split of the graph. This needs to happen before we make graph_copy, - // so they are in sync. - ggml_backend_graph_optimize(sched->backends[split->backend_id], &split->graph); // add inputs to the graph copy so that they are allocated by ggml-alloc at the start of the split for (int j = 0; j < split->n_inputs; j++) { @@ -1494,9 +1528,32 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra assert(graph_copy->size > graph_copy->n_nodes); sched->node_backend_ids[graph_copy->n_nodes] = tensor_backend_id(graph->nodes[j]); graph_copy->nodes[graph_copy->n_nodes++] = graph->nodes[j]; + + if (alloc_deps.empty()) { + continue; + } + + // add a dependency node so that the kept tensors are not freed before this node is computed + auto it = alloc_deps.find(graph->nodes[j]); + if (it != alloc_deps.end()) { + const std::vector & keep = it->second; + for (size_t k = 0; k < keep.size(); k += GGML_MAX_SRC) { + struct ggml_tensor * dep = ggml_view_tensor(sched->ctx, keep[k]); + for (size_t s = 0; s < GGML_MAX_SRC && k + s < keep.size(); s++) { + dep->src[s] = keep[k + s]; + } + assert(graph_copy->size > graph_copy->n_nodes); + sched->node_backend_ids[graph_copy->n_nodes] = split->backend_id; + graph_copy->nodes[graph_copy->n_nodes++] = dep; + n_dep_nodes_added++; + } + } } } + // a mismatch means a backend added a dep with an `until` tensor that is not a node of the optimized graph + GGML_ASSERT(n_dep_nodes_added == n_dep_nodes); + if (sched->n_copies > 1) { // add input copies as leafs so that they are allocated first for (int i = 0; i < sched->n_graph_inputs; i++) { @@ -2051,6 +2108,22 @@ ggml_backend_t ggml_backend_sched_get_tensor_backend(ggml_backend_sched_t sched, // utils +// [TAG_ALLOC_SIZE_EXPAND] +// returns true for ops that may require additional memory for fleeting data on some backends, +// i.e. the backend's get_alloc_size may return more than ggml_nbytes for the output tensor +bool ggml_backend_op_alloc_size_may_expand(enum ggml_op op) { + switch (op) { + case GGML_OP_FLASH_ATTN_EXT: + case GGML_OP_MUL_MAT_ID: + case GGML_OP_CUMSUM: + case GGML_OP_ARGSORT: + case GGML_OP_TOP_K: + return true; + default: + return false; + } +} + enum ggml_status ggml_backend_view_init(struct ggml_tensor * tensor) { GGML_ASSERT(tensor); GGML_ASSERT(tensor->buffer == NULL); diff --git a/ggml/src/ggml-cann/aclnn_ops.cpp b/ggml/src/ggml-cann/aclnn_ops.cpp index 2dc0f40917d7..902d2eda6938 100644 --- a/ggml/src/ggml-cann/aclnn_ops.cpp +++ b/ggml/src/ggml-cann/aclnn_ops.cpp @@ -211,6 +211,50 @@ void ggml_cann_swiglu(ggml_backend_cann_context & ctx, ggml_tensor * dst) { GGML_CANN_CALL_ACLNN_OP(ctx, SwiGlu, acl_src.get(), (int64_t)2, acl_dst.get()); } +void ggml_cann_swiglu_clamp(ggml_backend_cann_context & ctx, ggml_tensor * dst) { + ggml_tensor * src0 = dst->src[0]; + ggml_tensor * src1 = dst->src[1]; + + GGML_ASSERT(ggml_is_contiguous_1(src0)); + GGML_ASSERT(ggml_is_contiguous_1(dst)); + + const int32_t swapped = ggml_get_op_params_i32(dst, 1); + acl_tensor_ptr acl_gate; + acl_tensor_ptr acl_up; + if (src1) { + GGML_ASSERT(ggml_is_contiguous_1(src1)); + GGML_ASSERT(src0->type == src1->type); + acl_gate = ggml_cann_create_tensor(src0); + acl_up = ggml_cann_create_tensor(src1); + } else { + int64_t ne[] = { src0->ne[0] / 2, src0->ne[1], src0->ne[2], src0->ne[3] }; + size_t nb[] = { src0->nb[0], src0->nb[1], src0->nb[2], src0->nb[3] }; + acl_gate = ggml_cann_create_tensor(src0, ne, nb, GGML_MAX_DIMS, ACL_FORMAT_ND, 0); + acl_up = ggml_cann_create_tensor(src0, ne, nb, GGML_MAX_DIMS, ACL_FORMAT_ND, ne[0] * ggml_element_size(src0)); + if (swapped) { + std::swap(acl_gate, acl_up); + } + } + + ggml_cann_pool_alloc temp_alloc(ctx.pool(), ggml_nbytes(dst)); + acl_tensor_ptr acl_temp = ggml_cann_create_tensor(temp_alloc.get(), ggml_cann_type_mapping(dst->type), + ggml_element_size(dst), dst->ne, dst->nb, GGML_MAX_DIMS); + acl_tensor_ptr acl_dst = ggml_cann_create_tensor(dst); + + const float limit = ggml_get_op_params_f32(dst, 3); + float min_gate = -INFINITY; + float min_up = -limit; + float max_value = limit; + acl_scalar_ptr acl_min_gate = ggml_cann_create_scalar(&min_gate, ACL_FLOAT); + acl_scalar_ptr acl_min_up = ggml_cann_create_scalar(&min_up, ACL_FLOAT); + acl_scalar_ptr acl_limit = ggml_cann_create_scalar(&max_value, ACL_FLOAT); + + GGML_CANN_CALL_ACLNN_OP(ctx, Clamp, acl_gate.get(), acl_min_gate.get(), acl_limit.get(), acl_temp.get()); + GGML_CANN_CALL_ACLNN_OP(ctx, Silu, acl_temp.get(), acl_dst.get()); + GGML_CANN_CALL_ACLNN_OP(ctx, Clamp, acl_up.get(), acl_min_up.get(), acl_limit.get(), acl_temp.get()); + GGML_CANN_CALL_ACLNN_OP(ctx, InplaceMul, acl_dst.get(), acl_temp.get()); +} + // Fused GeGLU using aclnnGeGluV3: splits input along ne[0] (CANN last dim), // activates the LEFT half with GELU, multiplies by right half. // approximate: 0=tanh, 1=none(erf). activateLeft=true matches GGML convention. @@ -4433,4 +4477,3 @@ void ggml_cann_gated_linear_attn(ggml_backend_cann_context & ctx, ggml_tensor * } } } - diff --git a/ggml/src/ggml-cann/aclnn_ops.h b/ggml/src/ggml-cann/aclnn_ops.h index cdbf9260f859..678f4d654e7f 100644 --- a/ggml/src/ggml-cann/aclnn_ops.h +++ b/ggml/src/ggml-cann/aclnn_ops.h @@ -76,6 +76,7 @@ void ggml_cann_repeat(ggml_backend_cann_context & ctx, ggml_tensor * dst); void ggml_cann_swiglu(ggml_backend_cann_context & ctx, ggml_tensor * dst); +void ggml_cann_swiglu_clamp(ggml_backend_cann_context & ctx, ggml_tensor * dst); void ggml_cann_geglu(ggml_backend_cann_context & ctx, ggml_tensor * dst, int64_t approximate); /** diff --git a/ggml/src/ggml-cann/ggml-cann.cpp b/ggml/src/ggml-cann/ggml-cann.cpp index 5e5541aac941..c2745014a192 100644 --- a/ggml/src/ggml-cann/ggml-cann.cpp +++ b/ggml/src/ggml-cann/ggml-cann.cpp @@ -1872,6 +1872,9 @@ static bool ggml_cann_compute_forward(ggml_backend_cann_context & ctx, struct gg case GGML_GLU_OP_SWIGLU: ggml_cann_swiglu(ctx, dst); break; + case GGML_GLU_OP_SWIGLU_CLAMP: + ggml_cann_swiglu_clamp(ctx, dst); + break; case GGML_GLU_OP_GEGLU_QUICK: ggml_cann_geglu_quick(ctx, dst); break; @@ -2428,6 +2431,7 @@ static bool ggml_backend_cann_supports_op(ggml_backend_dev_t dev, const ggml_ten case GGML_GLU_OP_SWIGLU: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: return true; default: return false; diff --git a/ggml/src/ggml-cpu/ggml-cpu.c b/ggml/src/ggml-cpu/ggml-cpu.c index 87ac0a702efc..6bc4467e378d 100644 --- a/ggml/src/ggml-cpu/ggml-cpu.c +++ b/ggml/src/ggml-cpu/ggml-cpu.c @@ -2311,6 +2311,7 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) { case GGML_GLU_OP_SWIGLU_OAI: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: { n_tasks = n_threads; } break; @@ -2936,12 +2937,13 @@ struct ggml_cplan ggml_graph_plan( const int64_t ne10 = node->src[1]->ne[0]; // W const int64_t ne11 = node->src[1]->ne[1]; // H const int64_t ne12 = node->src[1]->ne[2]; // Channels In + const int64_t ne13 = node->src[1]->ne[3]; // Batch GGML_ASSERT(node->src[0]->type == GGML_TYPE_F16 || node->src[0]->type == GGML_TYPE_F32); GGML_ASSERT(node->src[1]->type == GGML_TYPE_F32); cur += ggml_type_size(node->src[0]->type) * ne00 * ne01 * ne02 * ne03; - cur += ggml_type_size(node->src[0]->type) * ne10 * ne11 * ne12; + cur += ggml_type_size(node->src[0]->type) * ne10 * ne11 * ne12 * ne13; } break; case GGML_OP_TOP_K: diff --git a/ggml/src/ggml-cpu/ops.cpp b/ggml/src/ggml-cpu/ops.cpp index b869f4bddde0..266261c5e5a4 100644 --- a/ggml/src/ggml-cpu/ops.cpp +++ b/ggml/src/ggml-cpu/ops.cpp @@ -3403,6 +3403,139 @@ static void ggml_compute_forward_swiglu_oai( } } +// ggml_compute_forward_swiglu_clamp + +static void ggml_compute_forward_swiglu_clamp_f32(const ggml_compute_params * params, ggml_tensor * dst) { + const ggml_tensor * src0 = dst->src[0]; + const ggml_tensor * src1 = dst->src[1]; + char * src0_d = (char *) src0->data; + char * src1_d = (char *) (src1 ? src1->data : src0->data); + const size_t src0_o = src0->nb[1]; + const size_t src1_o = src1 ? src1->nb[1] : src0->nb[1]; + + GGML_ASSERT(ggml_is_contiguous_1(src0)); + GGML_ASSERT(ggml_is_contiguous_1(dst)); + + if (src1) { + GGML_ASSERT(ggml_is_contiguous_1(src1)); + GGML_ASSERT(src0->type == src1->type); + } + + const int ith = params->ith; + const int nth = params->nth; + + const int nc = src1 ? src0->ne[0] : src0->ne[0] / 2; + const int nr = ggml_nrows(src0); + + GGML_ASSERT(dst->ne[0] == nc); + GGML_ASSERT(ggml_nrows(dst) == nr); + + const int32_t swapped = ggml_get_op_params_i32(dst, 1); + const float limit = ggml_get_op_params_f32(dst, 3); + + const int dr = (nr + nth - 1) / nth; + const int ir0 = dr * ith; + const int ir1 = MIN(ir0 + dr, nr); + + for (int i1 = ir0; i1 < ir1; i1++) { + float * src0_p = (float *) (src0_d + i1 * src0_o); + float * src1_p = (float *) (src1_d + i1 * src1_o); + float * dst_p = (float *) ((char *) dst->data + i1 * (dst->nb[1])); + + if (!src1) { + src0_p += swapped ? nc : 0; + src1_p += swapped ? 0 : nc; + } + + for (int k = 0; k < nc; k++) { + const float gate = std::min(src0_p[k], limit); + const float up = std::clamp(src1_p[k], -limit, limit); + dst_p[k] = gate / (1.f + expf(-gate)) * up; + } + +#ifndef NDEBUG + for (int k = 0; k < nc; k++) { + const float x = dst_p[k]; + GGML_UNUSED(x); + assert(!isnan(x)); + assert(!isinf(x)); + } +#endif // NDEBUG + } +} + +static void ggml_compute_forward_swiglu_clamp_f16(const ggml_compute_params * params, ggml_tensor * dst) { + const ggml_tensor * src0 = dst->src[0]; + const ggml_tensor * src1 = dst->src[1]; + char * src0_d = (char *) src0->data; + char * src1_d = (char *) (src1 ? src1->data : src0->data); + const size_t src0_o = src0->nb[1]; + const size_t src1_o = src1 ? src1->nb[1] : src0->nb[1]; + + GGML_ASSERT(ggml_is_contiguous_1(src0)); + GGML_ASSERT(ggml_is_contiguous_1(dst)); + + if (src1) { + GGML_ASSERT(ggml_is_contiguous_1(src1)); + GGML_ASSERT(src0->type == src1->type); + } + + const int ith = params->ith; + const int nth = params->nth; + + const int nc = src1 ? src0->ne[0] : src0->ne[0] / 2; + const int nr = ggml_nrows(src0); + + GGML_ASSERT(dst->ne[0] == nc); + GGML_ASSERT(ggml_nrows(dst) == nr); + + const int32_t swapped = ggml_get_op_params_i32(dst, 1); + const float limit = ggml_get_op_params_f32(dst, 3); + + const int dr = (nr + nth - 1) / nth; + const int ir0 = dr * ith; + const int ir1 = MIN(ir0 + dr, nr); + + for (int i1 = ir0; i1 < ir1; i1++) { + ggml_fp16_t * src0_p = (ggml_fp16_t *) (src0_d + i1 * src0_o); + ggml_fp16_t * src1_p = (ggml_fp16_t *) (src1_d + i1 * src1_o); + ggml_fp16_t * dst_p = (ggml_fp16_t *) ((char *) dst->data + i1 * (dst->nb[1])); + + if (!src1) { + src0_p += swapped ? nc : 0; + src1_p += swapped ? 0 : nc; + } + + for (int k = 0; k < nc; k++) { + const float gate = std::min(GGML_FP16_TO_FP32(src0_p[k]), limit); + const float up = std::clamp(GGML_FP16_TO_FP32(src1_p[k]), -limit, limit); + dst_p[k] = GGML_FP32_TO_FP16(gate / (1.f + expf(-gate)) * up); + } + +#ifndef NDEBUG + for (int k = 0; k < nc; k++) { + const float x = GGML_FP16_TO_FP32(dst_p[k]); + GGML_UNUSED(x); + assert(!isnan(x)); + assert(!isinf(x)); + } +#endif // NDEBUG + } +} + +static void ggml_compute_forward_swiglu_clamp(const ggml_compute_params * params, ggml_tensor * dst) { + switch (dst->src[0]->type) { + case GGML_TYPE_F32: + ggml_compute_forward_swiglu_clamp_f32(params, dst); + break; + case GGML_TYPE_F16: + ggml_compute_forward_swiglu_clamp_f16(params, dst); + break; + default: + GGML_ABORT("fatal error"); + } +} + // ggml_compute_forward_geglu_erf static void ggml_compute_forward_geglu_erf_f32( @@ -7267,18 +7400,21 @@ static void ggml_compute_forward_conv_transpose_2d_impl( } } - // permute source data (src1) from (Sw x Sh x Cin) to (Cin x Sw x Sh) + // permute source data (src1) from (Sw x Sh x Cin) to (Cin x Sw x Sh), for all batches { kernel_t * const wdata = (kernel_t *) params->wdata + nk; - for (int i12 = 0; i12 < ne12; i12++) { - for (int i11 = 0; i11 < ne11; i11++) { - const float * const src = (float *)((char *) src1->data + i12*nb12 + i11*nb11); - kernel_t * dst_data = wdata + i11*ne10*ne12; - for (int i10 = 0; i10 < ne10; i10++) { - if constexpr (std::is_same_v) { - dst_data[i10*ne12 + i12] = GGML_CPU_FP32_TO_FP16(src[i10]); - } else { - dst_data[i10*ne12 + i12] = src[i10]; + for (int i13 = 0; i13 < ne13; i13++) { + kernel_t * const wdata_b = wdata + i13*ne10*ne11*ne12; + for (int i12 = 0; i12 < ne12; i12++) { + for (int i11 = 0; i11 < ne11; i11++) { + const float * const src = (float *)((char *) src1->data + i13*nb13 + i12*nb12 + i11*nb11); + kernel_t * dst_data = wdata_b + i11*ne10*ne12; + for (int i10 = 0; i10 < ne10; i10++) { + if constexpr (std::is_same_v) { + dst_data[i10*ne12 + i12] = GGML_CPU_FP32_TO_FP16(src[i10]); + } else { + dst_data[i10*ne12 + i12] = src[i10]; + } } } } @@ -7305,24 +7441,27 @@ static void ggml_compute_forward_conv_transpose_2d_impl( kernel_t * const wdata_src = wdata + nk; for (int i2 = ip0; i2 < ip1; i2++) { // Cout - float * dst_data = (float *)((char *) dst->data + i2*nb2); kernel_t * wdata_kernel = wdata + i2*ne01*ne00*ne03; - for (int i11 = 0; i11 < ne11; i11++) { - for (int i10 = 0; i10 < ne10; i10++) { - const int i1n = i11*ne10*ne12 + i10*ne12; - for (int i01 = 0; i01 < ne01; i01++) { - for (int i00 = 0; i00 < ne00; i00++) { - float v = 0; - if constexpr (std::is_same_v) { - ggml_vec_dot_f16(ne03, &v, 0, - wdata_src + i1n, 0, - wdata_kernel + i01*ne00*ne03 + i00*ne03, 0, 1); - } else { - ggml_vec_dot_f32(ne03, &v, 0, - wdata_src + i1n, 0, - wdata_kernel + i01*ne00*ne03 + i00*ne03, 0, 1); + for (int i3 = 0; i3 < ne3; i3++) { // batch + float * dst_data = (float *)((char *) dst->data + i3*nb3 + i2*nb2); + kernel_t * wdata_src_b = wdata_src + i3*ne10*ne11*ne12; + for (int i11 = 0; i11 < ne11; i11++) { + for (int i10 = 0; i10 < ne10; i10++) { + const int i1n = i11*ne10*ne12 + i10*ne12; + for (int i01 = 0; i01 < ne01; i01++) { + for (int i00 = 0; i00 < ne00; i00++) { + float v = 0; + if constexpr (std::is_same_v) { + ggml_vec_dot_f16(ne03, &v, 0, + wdata_src_b + i1n, 0, + wdata_kernel + i01*ne00*ne03 + i00*ne03, 0, 1); + } else { + ggml_vec_dot_f32(ne03, &v, 0, + wdata_src_b + i1n, 0, + wdata_kernel + i01*ne00*ne03 + i00*ne03, 0, 1); + } + dst_data[(i11*stride + i01)*ne0 + i10*stride + i00] += v; } - dst_data[(i11*stride + i01)*ne0 + i10*stride + i00] += v; } } } @@ -10130,6 +10269,10 @@ void ggml_compute_forward_glu( { ggml_compute_forward_geglu_quick(params, dst); } break; + case GGML_GLU_OP_SWIGLU_CLAMP: + { + ggml_compute_forward_swiglu_clamp(params, dst); + } break; default: { GGML_ABORT("fatal error"); diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index 14dd1098c97b..e5ccd1feab14 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -1539,6 +1539,7 @@ struct ggml_cuda_mm_fusion_args_host { const ggml_tensor * x_scale = nullptr; const ggml_tensor * gate_scale = nullptr; ggml_glu_op glu_op; + float glu_limit = 0.0f; }; struct ggml_cuda_mm_fusion_args_device { const void * x_bias = nullptr; @@ -1547,6 +1548,7 @@ struct ggml_cuda_mm_fusion_args_device { const void * x_scale = nullptr; const void * gate_scale = nullptr; ggml_glu_op glu_op; + float glu_limit = 0.0f; }; struct ggml_cuda_kernel_launch_params { @@ -1673,4 +1675,3 @@ static __inline__ void ggml_cuda_kernel_launch(Kernel kernel, const ggml_cuda_ke kernel<<>>(std::forward(args)... ); CUDA_CHECK(cudaGetLastError()); } - diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 2456f7dcc621..7c2d3cad20a8 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -1744,7 +1744,7 @@ static bool ggml_cuda_should_fuse_mul_mat(const ggml_tensor * ffn_up, return false; } - static constexpr std::array valid_glu_ops = { GGML_GLU_OP_SWIGLU, GGML_GLU_OP_GEGLU, GGML_GLU_OP_SWIGLU_OAI }; + static constexpr std::array valid_glu_ops = { GGML_GLU_OP_SWIGLU, GGML_GLU_OP_GEGLU, GGML_GLU_OP_SWIGLU_OAI, GGML_GLU_OP_SWIGLU_CLAMP }; if (std::find(valid_glu_ops.begin(), valid_glu_ops.end(), ggml_get_glu_op(glu)) == valid_glu_ops.end()) { return false; @@ -2203,6 +2203,9 @@ static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct gg case GGML_GLU_OP_GEGLU_QUICK: ggml_cuda_op_geglu_quick(ctx, dst); break; + case GGML_GLU_OP_SWIGLU_CLAMP: + ggml_cuda_op_swiglu_clamp(ctx, dst); + break; default: return false; } @@ -3595,6 +3598,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph fusion_data.x_scale = up_scale; fusion_data.gate_scale = gate_scale; fusion_data.glu_op = ggml_get_glu_op(glu); + fusion_data.glu_limit = ggml_get_op_params_f32(glu, 3); if (ggml_cuda_should_fuse_mul_mat_vec_q(up_n)) { ggml_cuda_mul_mat_vec_q(*cuda_ctx, src0, src1, ids, cgraph->nodes[glu_idx], &fusion_data); @@ -3688,6 +3692,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph fusion_data.x_scale = up_scale; fusion_data.gate_scale = gate_scale; fusion_data.glu_op = ggml_get_glu_op(glu); + fusion_data.glu_limit = ggml_get_op_params_f32(glu, 3); if (ggml_cuda_should_fuse_mul_mat_vec_q(up_n)) { ggml_cuda_mul_mat_vec_q(*cuda_ctx, src0, src1, ids, cgraph->nodes[glu_idx], &fusion_data); @@ -3744,6 +3749,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph fusion_data.x_bias = up_bias_tensor; fusion_data.gate_bias = gate_bias_tensor; fusion_data.glu_op = ggml_get_glu_op(glu); + fusion_data.glu_limit = ggml_get_op_params_f32(glu, 3); ggml_cuda_mul_mat_vec_f(*cuda_ctx, src0, src1, ids, glu, &fusion_data); fused_mul_mat_vec = true; @@ -3757,6 +3763,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph fusion_data.x_bias = up_bias_tensor; fusion_data.gate_bias = gate_bias_tensor; fusion_data.glu_op = ggml_get_glu_op(glu); + fusion_data.glu_limit = ggml_get_op_params_f32(glu, 3); ggml_cuda_mul_mat_vec_q(*cuda_ctx, src0, src1, ids, glu, &fusion_data); fused_mul_mat_vec = true; @@ -3781,8 +3788,9 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph if (ggml_cuda_should_fuse_mul_mat_vec_f(up)) { ggml_cuda_mm_fusion_args_host fusion_data{}; - fusion_data.gate = gate->src[0]; - fusion_data.glu_op = ggml_get_glu_op(glu); + fusion_data.gate = gate->src[0]; + fusion_data.glu_op = ggml_get_glu_op(glu); + fusion_data.glu_limit = ggml_get_op_params_f32(glu, 3); ggml_cuda_mul_mat_vec_f(*cuda_ctx, src0, src1, ids, glu, &fusion_data); fused_mul_mat_vec = true; @@ -3792,8 +3800,9 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph if (ggml_cuda_should_fuse_mul_mat_vec_q(up)) { ggml_cuda_mm_fusion_args_host fusion_data{}; - fusion_data.gate = gate->src[0]; - fusion_data.glu_op = ggml_get_glu_op(glu); + fusion_data.gate = gate->src[0]; + fusion_data.glu_op = ggml_get_glu_op(glu); + fusion_data.glu_limit = ggml_get_op_params_f32(glu, 3); ggml_cuda_mul_mat_vec_q(*cuda_ctx, src0, src1, ids, glu, &fusion_data); fused_mul_mat_vec = true; @@ -4328,7 +4337,9 @@ static void ggml_backend_cuda_event_wait(ggml_backend_t backend, ggml_backend_ev } } -static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph * cgraph) { +static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph * cgraph, ggml_backend_graph_optimize_params * params) { + GGML_UNUSED(params); + ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context; #ifdef USE_CUDA_GRAPH @@ -4917,6 +4928,7 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g case GGML_GLU_OP_SWIGLU_OAI: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: return ggml_is_contiguous_1(op->src[0]); default: return false; diff --git a/ggml/src/ggml-cuda/mmid.cu b/ggml/src/ggml-cuda/mmid.cu index f80442fbe4e8..ed0851dcf8dd 100644 --- a/ggml/src/ggml-cuda/mmid.cu +++ b/ggml/src/ggml-cuda/mmid.cu @@ -19,6 +19,11 @@ struct mm_ids_helper_store { }; static_assert(sizeof(mm_ids_helper_store) == 4, "unexpected size for mm_ids_helper_store"); +// the generic path passes 0, which needs no padding since it never groups lanes by token +template struct mm_ids_pow2 { static constexpr int value = 2*mm_ids_pow2<(n + 1)/2>::value; }; +template <> struct mm_ids_pow2<1> { static constexpr int value = 1; }; +template <> struct mm_ids_pow2<0> { static constexpr int value = 1; }; + // Helper function for mul_mat_id, converts ids to a more convenient format. // ids_src1 describes how to permute the flattened column indices of src1 in order to get a compact src1 tensor sorted by expert. // ids_dst describes the same mapping but for the dst tensor. @@ -32,6 +37,9 @@ static __global__ void mm_ids_helper( const int n_expert_used = n_expert_used_template == 0 ? n_expert_used_var : n_expert_used_template; const int expert = blockIdx.x; + // token slots per warp lane group, padded to a power of 2 so a warp divides evenly + constexpr int neu_padded = mm_ids_pow2::value; + extern __shared__ char data_mm_ids_helper[]; mm_ids_helper_store * store = (mm_ids_helper_store *) data_mm_ids_helper; @@ -60,8 +68,8 @@ static __global__ void mm_ids_helper( } } else { // Implementation optimized for specific numbers of experts used: - static_assert(n_expert_used == 6 || warp_size % n_expert_used == 0, "bad n_expert_used"); - const int neu_padded = n_expert_used == 6 ? 8 : n_expert_used; // Padded to next higher power of 2. + // a warp holds a whole number of token slots, so the slot count is padded to a power of 2 + static_assert(neu_padded <= warp_size && warp_size % neu_padded == 0, "bad n_expert_used"); for (int it0 = 0; it0 < n_tokens; it0 += warp_size/neu_padded) { const int it = it0 + threadIdx.x / neu_padded; @@ -156,6 +164,9 @@ void ggml_cuda_launch_mm_ids_helper( case 8: launch_mm_ids_helper< 8>(ids, ids_src1, ids_dst, expert_bounds, n_experts, n_tokens, n_expert_used, nchannels_y, si1, sis1, write_inverse, stream); break; + case 10: + launch_mm_ids_helper<10>(ids, ids_src1, ids_dst, expert_bounds, n_experts, n_tokens, n_expert_used, nchannels_y, si1, sis1, write_inverse, stream); + break; case 16: launch_mm_ids_helper<16>(ids, ids_src1, ids_dst, expert_bounds, n_experts, n_tokens, n_expert_used, nchannels_y, si1, sis1, write_inverse, stream); break; diff --git a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh index 676f27fea4d9..3a3ef7bd9c09 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh @@ -1,289 +1,273 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna3(ggml_type type, int J, bool fallback) { CASE(GGML_TYPE_Q1_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q1_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q1_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q1_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q1_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q1_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q1_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q1_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q1_0, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q1_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q1_0, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q1_0, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q1_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q1_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q1_0, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q1_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q2_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q2_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q2_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q2_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q2_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q2_0, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q2_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q2_0, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q2_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 128, 2, 64, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q2_0, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q2_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q4_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q4_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q4_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q4_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 128, 4, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 128, 4, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q4_1, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q4_1, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_1, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_1, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_1, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q4_1, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_1, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_1, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_1, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_1, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_1, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_1, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_1, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_1, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 128, 1, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 128, 1, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q5_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q5_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q5_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q5_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q5_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_0, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_0, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_0, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 128, 4, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 128, 1, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 128, 4, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q5_1, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q5_1, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q5_1, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_1, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_1, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q5_1, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q5_1, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_1, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_1, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_1, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_1, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_1, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_1, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_1, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 128, 4, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 128, 1, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 128, 4, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q8_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q8_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q8_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q8_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 128, 1, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 128, 1, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_Q2_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q2_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q2_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q2_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q2_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q2_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_K, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 256, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q2_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q2_K, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q3_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q3_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q3_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q3_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q3_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q3_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q3_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q3_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q3_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q3_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q3_K, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q3_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q3_K, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q3_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 128, 1, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 128, 1, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q4_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q4_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q4_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 1, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 1, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q5_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q5_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q5_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q5_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 4, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 4, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 4, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q6_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q6_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q6_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q6_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q6_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 4, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q6_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 1, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 1, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_IQ1_S, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ1_S, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ1_S, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ1_S, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ1_S, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 128, 4, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 128, 4, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 128, 1, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 128, 1, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ2_XXS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_XXS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XXS, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_XXS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XXS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 128, 4, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 128, 4, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 128, 4, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ2_XS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_XS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XS, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_XS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ2_XS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 128, 4, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 128, 4, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 128, 4, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ2_S, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_S, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_S, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ2_S, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ2_S, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 128, 4, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 128, 4, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 128, 4, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 128, 4, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 128, 1, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ3_S, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ3_S, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_S, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ3_S, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_S, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 128, 4, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 128, 4, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ4_XS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ4_XS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_XS, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ4_XS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 128, 4, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ4_XS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ4_NL, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ4_NL, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_NL, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ4_NL, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ4_NL, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 128, 1, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_MXFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_MXFP4, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_MXFP4, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_MXFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_MXFP4, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_NVFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_NVFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_NVFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_NVFP4, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_NVFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_NVFP4, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_NVFP4, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_NVFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_NVFP4, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true); diff --git a/ggml/src/ggml-cuda/mmq-load-tiles.cuh b/ggml/src/ggml-cuda/mmq-load-tiles.cuh index 8ed704c281a4..7f00bad943e2 100644 --- a/ggml/src/ggml-cuda/mmq-load-tiles.cuh +++ b/ggml/src/ggml-cuda/mmq-load-tiles.cuh @@ -138,12 +138,20 @@ template static __device__ __forceinline_ for (int j = 0; j < 4; ++j) { const int q = qxi[j]; +#if defined(GGML_USE_HIP) + const uint32_t qx_indices = (q & 0x03) | ((q & 0x0C) << 6) | ((q & 0x30) << 12) | ((q & 0xC0) << 18); + const uint32_t qy_bits = q >> 8; + const uint32_t qy_indices = (qy_bits & 0x03) | ((qy_bits & 0x0C) << 6) | ((qy_bits & 0x30) << 12) | ((qy_bits & 0xC0) << 18); + const int qx = __builtin_amdgcn_perm(0x020100FF, 0x020100FF, qx_indices); + const int qy = __builtin_amdgcn_perm(0x020100FF, 0x020100FF, qy_indices); +#else // unpack even and odd crumbs into byte values const int qe = __byte_perm(0x020100FF, 0x020100FF, q >> 0); const int qo = __byte_perm(0x020100FF, 0x020100FF, q >> 2); // unshuffle values const int qx = __byte_perm(qe, qo, 0x5140); const int qy = __byte_perm(qe, qo, 0x7362); +#endif // defined(GGML_USE_HIP) #if defined(AMD_MFMA_AVAILABLE) || defined(TURING_MMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE) x_qs[i*sram_stride + dst_offset + j*2+0] = qx; diff --git a/ggml/src/ggml-cuda/mmvf.cu b/ggml/src/ggml-cuda/mmvf.cu index d7dbc8b99282..bd5c5d421a4c 100644 --- a/ggml/src/ggml-cuda/mmvf.cu +++ b/ggml/src/ggml-cuda/mmvf.cu @@ -56,6 +56,7 @@ static __global__ void mul_mat_vec_f( bool use_bias = false; bool use_gate_bias = false; ggml_glu_op glu_op = ggml_glu_op::GGML_GLU_OP_SWIGLU; + float glu_limit = 0.0f; const T * gate_x = nullptr; const float * x_bias = nullptr; const float * gate_bias = nullptr; @@ -65,6 +66,7 @@ static __global__ void mul_mat_vec_f( use_bias = fusion.x_bias != nullptr; use_gate_bias = fusion.gate_bias != nullptr; glu_op = fusion.glu_op; + glu_limit = fusion.glu_limit; if (use_gate) { gate_x = static_cast(fusion.gate); @@ -365,6 +367,9 @@ static __global__ void mul_mat_vec_f( value = ggml_cuda_op_swiglu_oai_single(gate_value, value); break; } + case GGML_GLU_OP_SWIGLU_CLAMP: + value = ggml_cuda_op_swiglu_clamp_single(gate_value, value, glu_limit); + break; default: break; } @@ -374,7 +379,7 @@ static __global__ void mul_mat_vec_f( dst[tid*stride_col_dst + row] = value; if constexpr (!has_fusion) { - GGML_UNUSED_VARS(use_gate, use_bias, use_gate_bias, glu_op, gate_x, x_bias, gate_bias, sumf_gate); + GGML_UNUSED_VARS(use_gate, use_bias, use_gate_bias, glu_op, glu_limit, gate_x, x_bias, gate_bias, sumf_gate); } } @@ -675,6 +680,7 @@ void ggml_cuda_mul_mat_vec_f(ggml_backend_cuda_context & ctx, const ggml_tensor fusion_local.gate_bias = fusion->gate_bias->data; } fusion_local.glu_op = fusion->glu_op; + fusion_local.glu_limit = fusion->glu_limit; } const int64_t s01 = src0->nb[1] / ts_src0; diff --git a/ggml/src/ggml-cuda/mmvq.cu b/ggml/src/ggml-cuda/mmvq.cu index 970534809804..79f7a3f6fe75 100644 --- a/ggml/src/ggml-cuda/mmvq.cu +++ b/ggml/src/ggml-cuda/mmvq.cu @@ -595,6 +595,7 @@ static __global__ void mul_mat_vec_q( const float * x_scale = nullptr; const float * gate_scale = nullptr; ggml_glu_op active_glu; + float glu_limit = 0.0f; if constexpr (has_fusion) { use_gate = fusion.gate != nullptr; @@ -604,6 +605,7 @@ static __global__ void mul_mat_vec_q( x_bias = (const float *) fusion.x_bias; gate_bias = (const float *) fusion.gate_bias; active_glu = fusion.glu_op; + glu_limit = fusion.glu_limit; if constexpr (type == GGML_TYPE_NVFP4) { use_scale = fusion.x_scale != nullptr; use_gate_scale = fusion.gate_scale != nullptr && use_gate; @@ -745,6 +747,9 @@ static __global__ void mul_mat_vec_q( case GGML_GLU_OP_SWIGLU_OAI: result = ggml_cuda_op_swiglu_oai_single(gate_value, result); break; + case GGML_GLU_OP_SWIGLU_CLAMP: + result = ggml_cuda_op_swiglu_clamp_single(gate_value, result, glu_limit); + break; default: result = result * gate_value; break; @@ -757,7 +762,7 @@ static __global__ void mul_mat_vec_q( } if constexpr (!has_fusion) { - GGML_UNUSED_VARS(use_gate, use_bias, use_gate_bias, use_scale, use_gate_scale, active_glu, gate_bias, x_bias, x_scale, gate_scale, tmp_gate); + GGML_UNUSED_VARS(use_gate, use_bias, use_gate_bias, use_scale, use_gate_scale, active_glu, glu_limit, gate_bias, x_bias, x_scale, gate_scale, tmp_gate); } if constexpr (type != GGML_TYPE_NVFP4) { GGML_UNUSED_VARS(use_scale, use_gate_scale, x_scale, gate_scale, x_scales, gate_scales); @@ -1310,6 +1315,7 @@ void ggml_cuda_mul_mat_vec_q( fusion_local.gate_scale = fusion->gate_scale->data; } fusion_local.glu_op = fusion->glu_op; + fusion_local.glu_limit = fusion->glu_limit; } // If src0 is a temporary compute buffer, clear any potential padding. diff --git a/ggml/src/ggml-cuda/unary.cu b/ggml/src/ggml-cuda/unary.cu index 4cb805fa6013..d3e594878fcc 100644 --- a/ggml/src/ggml-cuda/unary.cu +++ b/ggml/src/ggml-cuda/unary.cu @@ -427,6 +427,81 @@ void ggml_cuda_op_swiglu_oai(ggml_backend_cuda_context & ctx, ggml_tensor * dst) swiglu_oai_cuda(src0_p, src1_p, (float *)dst_d, ggml_nelements(dst), nc, src0_o / sizeof(float), src1_o / sizeof(float), alpha, limit, stream); } +// swiglu_clamp + +template +static __global__ void swiglu_clamp_kernel(const T * gate, const T * up, T * dst, const int64_t k, const int64_t n, const int64_t o0, const int64_t o1, float limit) { + const int64_t i = int64_t(blockDim.x)*blockIdx.x + threadIdx.x; + + if (i >= k) { + return; + } + + const int64_t j0 = (i / n) * o0 + (i % n); + const int64_t j1 = o0 == o1 ? j0 : (i / n) * o1 + (i % n); + + dst[i] = (T) ggml_cuda_op_swiglu_clamp_single((float) gate[j0], (float) up[j1], limit); +} + +template +static void swiglu_clamp_cuda(const T * gate, const T * up, T * dst, const int64_t k, const int64_t n, const int64_t o0, const int64_t o1, const float limit, cudaStream_t stream) { + const int64_t num_blocks = (k + CUDA_GLU_BLOCK_SIZE - 1) / CUDA_GLU_BLOCK_SIZE; + swiglu_clamp_kernel<<>>(gate, up, dst, k, n, o0, o1, limit); +} + +void ggml_cuda_op_swiglu_clamp(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + const ggml_tensor * src0 = dst->src[0]; + const ggml_tensor * src1 = dst->src[1]; + void * src0_d = src0->data; + void * src1_d = src1 ? src1->data : src0->data; + const int64_t src0_o = src0->nb[1]; + const int64_t src1_o = src1 ? src1->nb[1] : src0->nb[1]; + void * dst_d = dst->data; + const int64_t nc = src1 ? src0->ne[0] : src0->ne[0] / 2; + cudaStream_t stream = ctx.stream(); + + GGML_ASSERT(ggml_is_contiguous_1(src0)); + GGML_ASSERT(src0->nb[0] == ggml_element_size(src0)); + GGML_ASSERT(ggml_is_contiguous(dst)); + + GGML_ASSERT(src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16); + GGML_ASSERT(src0->type == dst->type); + GGML_ASSERT(dst->ne[0] == nc); + GGML_ASSERT(ggml_nrows(dst) == ggml_nrows(src0)); + + if (src1) { + GGML_ASSERT(ggml_is_contiguous_1(src1)); + GGML_ASSERT(src1->nb[0] == ggml_element_size(src1)); + GGML_ASSERT(src1->ne[0] == nc); + GGML_ASSERT(src0->type == src1->type); + } + + const int32_t swapped = ggml_get_op_params_i32(dst, 1); + const float limit = ggml_get_op_params_f32(dst, 3); + + if (src0->type == GGML_TYPE_F16) { + half * src0_p = (half *) src0_d; + half * src1_p = (half *) src1_d; + + if (!src1) { + src0_p += swapped ? nc : 0; + src1_p += swapped ? 0 : nc; + } + + swiglu_clamp_cuda(src0_p, src1_p, (half *) dst_d, ggml_nelements(dst), nc, src0_o / sizeof(half), src1_o / sizeof(half), limit, stream); + } else { + float * src0_p = (float *) src0_d; + float * src1_p = (float *) src1_d; + + if (!src1) { + src0_p += swapped ? nc : 0; + src1_p += swapped ? 0 : nc; + } + + swiglu_clamp_cuda(src0_p, src1_p, (float *) dst_d, ggml_nelements(dst), nc, src0_o / sizeof(float), src1_o / sizeof(float), limit, stream); + } +} + /* CUDA kernel + launcher for xIELU */ template diff --git a/ggml/src/ggml-cuda/unary.cuh b/ggml/src/ggml-cuda/unary.cuh index 81ed873ecc30..04f3af6443a7 100644 --- a/ggml/src/ggml-cuda/unary.cuh +++ b/ggml/src/ggml-cuda/unary.cuh @@ -83,6 +83,8 @@ void ggml_cuda_op_swiglu(ggml_backend_cuda_context & ctx, ggml_tensor * dst); void ggml_cuda_op_swiglu_oai(ggml_backend_cuda_context & ctx, ggml_tensor * dst); +void ggml_cuda_op_swiglu_clamp(ggml_backend_cuda_context & ctx, ggml_tensor * dst); + void ggml_cuda_op_geglu_erf(ggml_backend_cuda_context & ctx, ggml_tensor * dst); void ggml_cuda_op_geglu_quick(ggml_backend_cuda_context & ctx, ggml_tensor * dst); @@ -112,3 +114,10 @@ __device__ __forceinline__ float ggml_cuda_op_swiglu_oai_single(float x, float g out_glu = out_glu * (1.0f + g); return out_glu; } + +__device__ __forceinline__ float ggml_cuda_op_swiglu_clamp_single(float gate, float up, float limit) { + gate = fminf(gate, limit); + up = fmaxf(fminf(up, limit), -limit); + + return ggml_cuda_op_silu_single(gate) * up; +} diff --git a/ggml/src/ggml-cuda/vecdotq.cuh b/ggml/src/ggml-cuda/vecdotq.cuh index 0f039c735b6b..ec117c57dfbc 100644 --- a/ggml/src/ggml-cuda/vecdotq.cuh +++ b/ggml/src/ggml-cuda/vecdotq.cuh @@ -747,12 +747,20 @@ static __device__ __forceinline__ float vec_dot_q2_0_q8_1( const int u = get_int_b4(bq8_1_chunk->qs, j*2+0); const int v = get_int_b4(bq8_1_chunk->qs, j*2+1); +#if defined(GGML_USE_HIP) + const uint32_t qx_indices = (q & 0x03) | ((q & 0x0C) << 6) | ((q & 0x30) << 12) | ((q & 0xC0) << 18); + const uint32_t qy_bits = q >> 8; + const uint32_t qy_indices = (qy_bits & 0x03) | ((qy_bits & 0x0C) << 6) | ((qy_bits & 0x30) << 12) | ((qy_bits & 0xC0) << 18); + const int qx = __builtin_amdgcn_perm(0x020100FF, 0x020100FF, qx_indices); + const int qy = __builtin_amdgcn_perm(0x020100FF, 0x020100FF, qy_indices); +#else // unpack even and odd crumbs into byte values const int qe = __byte_perm(0x020100FF, 0x020100FF, q >> 0); const int qo = __byte_perm(0x020100FF, 0x020100FF, q >> 2); // unshuffle values const int qx = __byte_perm(qe, qo, 0x5140); const int qy = __byte_perm(qe, qo, 0x7362); +#endif // defined(GGML_USE_HIP) sumi = ggml_cuda_dp4a(u, qx, sumi); sumi = ggml_cuda_dp4a(v, qy, sumi); diff --git a/ggml/src/ggml-et/et-kernels/src/glu_f32.c b/ggml/src/ggml-et/et-kernels/src/glu_f32.c index 95fe57215893..d376d6f56ff1 100644 --- a/ggml/src/ggml-et/et-kernels/src/glu_f32.c +++ b/ggml/src/ggml-et/et-kernels/src/glu_f32.c @@ -17,7 +17,7 @@ struct ggml_et_glu_params { int32_t glu_op_type; // GLU operation type (REGLU=0, GEGLU=1, SWIGLU=2, etc.) int32_t swapped; // Whether gate and value are swapped float alpha; // SWIGLU_OAI: sigmoid scaling factor - float limit; // SWIGLU_OAI: clamp limit + float limit; // GLU clamp limit }; // SiLU activation function: silu(x) = x * sigmoid(x) = x / (1 + exp(-x)) @@ -332,6 +332,57 @@ static inline void block_swiglu_oai(float * dst_block, } } +static inline void block_swiglu_clamp(float * dst_block, + const float * gate_block, + const float * up_block, + int elements, + float limit) { + int32_t vec_end = (elements / 8) * 8; + + unsigned long temp_mask; + __asm__ volatile("mova.x.m %0" : "=r"(temp_mask)); + __asm__ volatile("mov.m.x m0, x0, 0xFF"); + + float one_const = 1.0f; + float limit_pos = limit; + float limit_neg = -limit; + float neg_log2e = -1.4426950408889634f; + + for (int32_t i = 0; i < vec_end; i += 8) { + __asm__ volatile( + "flw.ps f10, %[gate_vec]\n" + "flw.ps f11, %[up_vec]\n" + "fbc.ps f21, %[one_ptr]\n" + "fbc.ps f23, %[lim_pos]\n" + "fbc.ps f24, %[lim_neg]\n" + "fbc.ps f25, %[k_ptr]\n" + "fmin.ps f12, f10, f23\n" + "fmax.ps f13, f11, f24\n" + "fmin.ps f13, f13, f23\n" + "fmul.ps f14, f12, f25\n" + "fexp.ps f15, f14\n" + "fadd.ps f15, f15, f21\n" + "frcp.ps f16, f15\n" + "fmul.ps f17, f12, f16\n" + "fmul.ps f18, f17, f13\n" + "fsw.ps f18, %[dst_out]\n" + : [dst_out] "=m"(*(float (*)[8]) & dst_block[i]) + : [gate_vec] "m"(*(const float (*)[8]) & gate_block[i]), [up_vec] "m"(*(const float (*)[8]) & up_block[i]), + [one_ptr] "m"(one_const), [lim_pos] "m"(limit_pos), [lim_neg] "m"(limit_neg), [k_ptr] "m"(neg_log2e) + : "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f21", "f23", "f24", "f25"); + } + + __asm__ volatile("mova.m.x %0" :: "r"(temp_mask)); + + for (int32_t i = vec_end; i < elements; i++) { + float gate = gate_block[i] > limit ? limit : gate_block[i]; + float up = up_block[i]; + up = up > limit ? limit : up; + up = up < -limit ? -limit : up; + dst_block[i] = silu_f32(gate) * up; + } +} + // Scalar erf approximation (Abramowitz & Stegun 7.1.26, max error ~1.5e-7) static inline float erf_approx(float x) { const float a1 = 0.254829592f; @@ -386,6 +437,7 @@ int entry_point(struct ggml_et_glu_params * params, void * env) { switch (params->glu_op_type) { case GGML_GLU_OP_SWIGLU: case GGML_GLU_OP_SWIGLU_OAI: + case GGML_GLU_OP_SWIGLU_CLAMP: case GGML_GLU_OP_GEGLU: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: @@ -531,6 +583,9 @@ int entry_point(struct ggml_et_glu_params * params, void * env) { case GGML_GLU_OP_SWIGLU_OAI: block_swiglu_oai(dst_ptr, x_ptr, g_ptr, (int) elements_to_process, params->alpha, params->limit); break; + case GGML_GLU_OP_SWIGLU_CLAMP: + block_swiglu_clamp(dst_ptr, x_ptr, g_ptr, (int) elements_to_process, params->limit); + break; default: return -1; } diff --git a/ggml/src/ggml-et/ggml-et-cpu-compare.cpp b/ggml/src/ggml-et/ggml-et-cpu-compare.cpp index b37f6d261d97..5771679b3fae 100644 --- a/ggml/src/ggml-et/ggml-et-cpu-compare.cpp +++ b/ggml/src/ggml-et/ggml-et-cpu-compare.cpp @@ -261,7 +261,12 @@ bool ggml_et_cpu_compare_compute_and_check(ggml_et_cpu_compare_ctx * ct GGML_LOG_ERROR("ET: GLU CPU comparison requires split tensor mode\n"); return false; } - ctx->cpu_dst = ggml_glu_split(ctx->ggml_ctx, ctx->cpu_src0, ctx->cpu_src1, glu_op); + if (glu_op == GGML_GLU_OP_SWIGLU_CLAMP) { + const float limit = ggml_get_op_params_f32(node, 3); + ctx->cpu_dst = ggml_swiglu_clamp(ctx->ggml_ctx, ctx->cpu_src0, ctx->cpu_src1, limit); + } else { + ctx->cpu_dst = ggml_glu_split(ctx->ggml_ctx, ctx->cpu_src0, ctx->cpu_src1, glu_op); + } } break; case GGML_OP_SOFT_MAX: diff --git a/ggml/src/ggml-et/ggml-et-ops.cpp b/ggml/src/ggml-et/ggml-et-ops.cpp index 7871d5240818..8765138672a8 100644 --- a/ggml/src/ggml-et/ggml-et-ops.cpp +++ b/ggml/src/ggml-et/ggml-et-ops.cpp @@ -636,6 +636,7 @@ bool ggml_et_op_glu(ggml_backend_et_device_context * dev_ctx, const ggml_tensor case GGML_GLU_OP_GEGLU: case GGML_GLU_OP_SWIGLU: case GGML_GLU_OP_SWIGLU_OAI: + case GGML_GLU_OP_SWIGLU_CLAMP: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: break; @@ -661,6 +662,8 @@ bool ggml_et_op_glu(ggml_backend_et_device_context * dev_ctx, const ggml_tensor params.limit = 0.0f; if (glu_op_type == GGML_GLU_OP_SWIGLU_OAI) { params.alpha = ggml_get_op_params_f32(node, 2); + } + if (glu_op_type == GGML_GLU_OP_SWIGLU_OAI || glu_op_type == GGML_GLU_OP_SWIGLU_CLAMP) { params.limit = ggml_get_op_params_f32(node, 3); } // Phase 1: Initialize CPU comparison context and copy source buffers (before ET kernel) diff --git a/ggml/src/ggml-et/ggml-et.cpp b/ggml/src/ggml-et/ggml-et.cpp index b87b189a57a0..61c31d6f2912 100644 --- a/ggml/src/ggml-et/ggml-et.cpp +++ b/ggml/src/ggml-et/ggml-et.cpp @@ -1210,7 +1210,8 @@ static bool ggml_backend_et_device_supports_op(ggml_backend_dev_t dev, const ggm // Check GLU variant - support SWIGLU, SWIGLU_OAI, GEGLU, GEGLU_ERF, GEGLU_QUICK, REGLU ggml_glu_op glu_type = ggml_get_glu_op(op); const bool supported_variant = glu_type == GGML_GLU_OP_SWIGLU || glu_type == GGML_GLU_OP_SWIGLU_OAI || - glu_type == GGML_GLU_OP_GEGLU || glu_type == GGML_GLU_OP_GEGLU_ERF || + glu_type == GGML_GLU_OP_SWIGLU_CLAMP || glu_type == GGML_GLU_OP_GEGLU || + glu_type == GGML_GLU_OP_GEGLU_ERF || glu_type == GGML_GLU_OP_GEGLU_QUICK || glu_type == GGML_GLU_OP_REGLU; if (op->src[1]) { diff --git a/ggml/src/ggml-hexagon/ggml-hexagon.cpp b/ggml/src/ggml-hexagon/ggml-hexagon.cpp index c1e9f919d92a..3eb84fd2a993 100644 --- a/ggml/src/ggml-hexagon/ggml-hexagon.cpp +++ b/ggml/src/ggml-hexagon/ggml-hexagon.cpp @@ -69,30 +69,15 @@ using u32vec = std::vector; #define GGML_HEXAGON_FENCE_SLOT_SIZE 128 struct ggml_hexagon_device_config { - int physical_idx = 0; - int virtual_idx = 0; + int physical_idx = 0; + int virtual_idx = 0; + int domain_id = 0; + std::string domain_name; std::string name; }; static ggml_hexagon_device_config opt_device_configs[GGML_HEXAGON_MAX_SESSIONS]; -static int get_domain_id(int physical_idx) { - switch (physical_idx) { - case 0: return 3; // CDSP0 (all devices) - case 1: return 4; // CDSP1 (IQ9, IQ10) - case 2: return 18; // CDSP2 (IQ10) - case 3: return 19; // CDSP3 (IQ10) - default: return CDSP_DOMAIN_ID + physical_idx; - } -} - -static std::string get_domain_name(int physical_idx) { - if (physical_idx == 0) { - return CDSP_DOMAIN_NAME; - } - return std::string("cdsp") + std::to_string(physical_idx); -} - static int opt_arch = 0; // autodetect static size_t opt_ndev = 1; static size_t opt_nhvx = 0; // use all @@ -361,7 +346,6 @@ struct ggml_hexagon_session { uint32_t session_id; uint32_t domain_id; uint64_t queue_id; - int dev_id; int phys_idx; int virt_idx; bool valid_session; @@ -376,9 +360,6 @@ struct ggml_hexagon_session { std::unordered_map> cloned_buffers; std::unordered_set sync_peers; - ggml_backend_buffer_type buffer_type = {}; - ggml_backend_buffer_type host_buffer_type = {}; - uint32_t n_threads = 0; uint32_t n_hvx = 0; uint32_t n_hmx = 0; @@ -392,12 +373,12 @@ struct ggml_hexagon_session { mutable std::unordered_set needs_repack; - ggml_hexagon_session(int dev_id, ggml_backend_dev_t dev) noexcept(false); + ggml_hexagon_session(const ggml_hexagon_device_config & config, ggml_backend_dev_t dev = nullptr) noexcept(false); ~ggml_hexagon_session() noexcept(true); const char* c_name() const { return name.c_str(); } - void allocate(int dev_id) noexcept(false); + void allocate(const ggml_hexagon_device_config & config) noexcept(false); void release() noexcept(true); void enqueue_op(const htp_opnode & node); @@ -430,14 +411,38 @@ struct ggml_hexagon_session { // ** backend buffers +struct ggml_backend_hexagon_device_context { + int dev_id; + ggml_hexagon_device_config config; + ggml_backend_dev_t dev = nullptr; + size_t max_bufsize = 0; + + ggml_backend_buffer_type buffer_type = {}; + ggml_backend_buffer_type host_buffer_type = {}; + + std::unique_ptr sess; + + ggml_backend_hexagon_device_context(int dev_id, const ggml_hexagon_device_config & config, ggml_backend_dev_t dev); + ~ggml_backend_hexagon_device_context(); + + const char * c_name() const { return config.name.c_str(); } + + ggml_hexagon_session * session() { + if (!sess) { + sess = std::make_unique(config, dev); + } + return sess.get(); + } +}; + struct ggml_backend_hexagon_buffer_type_context { - ggml_backend_hexagon_buffer_type_context(const std::string & name, ggml_hexagon_session * sess) { - this->sess = sess; - this->name = name; + ggml_backend_hexagon_buffer_type_context(const std::string & name, ggml_backend_hexagon_device_context * dev_ctx) { + this->dev_ctx = dev_ctx; + this->name = name; } - ggml_hexagon_session * sess; - std::string name; + ggml_backend_hexagon_device_context * dev_ctx; + std::string name; }; struct ggml_hexagon_rpcmem_block { @@ -576,7 +581,8 @@ struct ggml_hexagon_shared_buffer { }; static ggml_hexagon_session * ggml_backend_hexagon_buffer_get_sess(ggml_backend_buffer_t buffer) { - return static_cast(buffer->buft->context)->sess; + auto sbuf = static_cast(buffer->context); + return sbuf->sess; } static void ggml_backend_hexagon_buffer_free_buffer(ggml_backend_buffer_t buffer) { @@ -1494,24 +1500,26 @@ static const char * ggml_backend_hexagon_buffer_type_name(ggml_backend_buffer_ty static ggml_backend_buffer_t ggml_backend_hexagon_buffer_type_alloc_buffer( ggml_backend_buffer_type_t buffer_type, size_t size) { - auto sess = static_cast(buffer_type->context)->sess; + auto dev_ctx = static_cast(buffer_type->context)->dev_ctx; + auto sess = dev_ctx->session(); try { ggml_hexagon_shared_buffer * sbuf = new ggml_hexagon_shared_buffer(sess, size, false, GGML_HEXAGON_FENCE_BUFFER_SIZE); return ggml_backend_buffer_init(buffer_type, ggml_backend_hexagon_buffer_interface, sbuf, size); } catch (const std::exception & exc) { - GGML_LOG_ERROR("ggml-hex: %s failed to allocate device buffer context: %s\n", sess->c_name(), exc.what()); + GGML_LOG_ERROR("ggml-hex: %s failed to allocate device buffer context: %s\n", dev_ctx->c_name(), exc.what()); return nullptr; } } static ggml_backend_buffer_t ggml_backend_hexagon_host_buffer_type_alloc_buffer( ggml_backend_buffer_type_t buffer_type, size_t size) { - auto sess = static_cast(buffer_type->context)->sess; + auto dev_ctx = static_cast(buffer_type->context)->dev_ctx; + auto sess = dev_ctx->session(); try { ggml_hexagon_shared_buffer * sbuf = new ggml_hexagon_shared_buffer(sess, size, false, GGML_HEXAGON_FENCE_BUFFER_SIZE); return ggml_backend_buffer_init(buffer_type, ggml_backend_hexagon_host_buffer_interface, sbuf, size); } catch (const std::exception & exc) { - GGML_LOG_ERROR("ggml-hex: %s failed to allocate host buffer context: %s\n", sess->c_name(), exc.what()); + GGML_LOG_ERROR("ggml-hex: %s failed to allocate host buffer context: %s\n", dev_ctx->c_name(), exc.what()); return nullptr; } } @@ -1536,7 +1544,7 @@ static size_t ggml_backend_hexagon_buffer_type_get_alloc_size(ggml_backend_buffe static size_t ggml_backend_hexagon_buffer_type_get_max_size(ggml_backend_buffer_type_t buft) { auto * context = static_cast(buft->context); - return context->sess->max_bufsize; + return context->dev_ctx->max_bufsize; } static bool ggml_backend_hexagon_buffer_type_is_host(ggml_backend_buffer_type_t buft) { @@ -1567,6 +1575,22 @@ static ggml_backend_buffer_type_i ggml_backend_hexagon_host_buffer_type_interfac /* .is_host = */ ggml_backend_hexagon_host_buffer_type_is_host, }; +ggml_backend_hexagon_device_context::ggml_backend_hexagon_device_context(int dev_id, const ggml_hexagon_device_config & config, ggml_backend_dev_t dev) + : dev_id(dev_id), config(config), dev(dev), max_bufsize(opt_mbuf) { + buffer_type.device = dev; + buffer_type.iface = ggml_backend_hexagon_buffer_type_interface; + buffer_type.context = new ggml_backend_hexagon_buffer_type_context(config.name, this); + + host_buffer_type.device = dev; + host_buffer_type.iface = ggml_backend_hexagon_host_buffer_type_interface; + host_buffer_type.context = new ggml_backend_hexagon_buffer_type_context(config.name + "-HOST", this); +} + +ggml_backend_hexagon_device_context::~ggml_backend_hexagon_device_context() { + delete static_cast(buffer_type.context); + delete static_cast(host_buffer_type.context); +} + static bool ggml_backend_buffer_is_hexagon(const struct ggml_backend_buffer * b) { return b->buft->iface.get_alignment == ggml_backend_hexagon_buffer_type_get_alignment; } @@ -2811,8 +2835,7 @@ static size_t ggml_hexagon_measure_max_vmem(ggml_hexagon_session *sess) { return vmem - step; // backoff to account for overhead from internal mappings } -void ggml_hexagon_session::allocate(int dev_id) noexcept(false) { - const auto & config = opt_device_configs[dev_id]; +void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) noexcept(false) { int phys_idx = config.physical_idx; int virt_idx = config.virtual_idx; @@ -2823,21 +2846,31 @@ void ggml_hexagon_session::allocate(int dev_id) noexcept(false) { this->phys_idx = phys_idx; this->virt_idx = virt_idx; - this->domain_id = get_domain_id(phys_idx); + this->domain_id = config.domain_id; this->session_id = 0; - this->dev_id = dev_id; this->name = config.name; this->op_pending = 0; GGML_LOG_DEBUG("ggml-hex: %s allocating new session\n", this->name.c_str()); - domain * my_domain = htpdrv_get_domain(this->domain_id); - if (my_domain == NULL) { - GGML_LOG_ERROR("ggml-hex: unable to get domain struct for CDSP (domain_id %d)\n", this->domain_id); - throw std::runtime_error("ggml-hex: failed to get CDSP domain (see log for details)"); + if (config.domain_id < 0 || config.domain_name.empty()) { + GGML_LOG_ERROR("ggml-hex: %s: invalid physical CDSP core %d\n", config.name.c_str(), config.physical_idx); + throw std::runtime_error("ggml-hex: invalid physical CDSP core"); } - std::string dom_name = get_domain_name(phys_idx); + const std::string & dom_name = config.domain_name; + + // Enable Unsigned PD for all domains + { + struct remote_rpc_control_unsigned_module u; + u.domain = -1; + u.enable = 1; + int err = remote_session_control(DSPRPC_CONTROL_UNSIGNED_MODULE, (void *) &u, sizeof(u)); + if (err != AEE_SUCCESS) { + GGML_LOG_ERROR("ggml-hex: %s failed to enable unsigned PD : error 0x%x\n", this->c_name(), err); + throw std::runtime_error("ggml-hex: remote_session_control(unsign) failed (see log for details)"); + } + } // Create new session if virtual_idx > 0 if (virt_idx > 0) { @@ -2849,7 +2882,8 @@ void ggml_hexagon_session::allocate(int dev_id) noexcept(false) { int err = remote_session_control(FASTRPC_RESERVE_NEW_SESSION, (void *) &n, sizeof(n)); if (err != AEE_SUCCESS) { - GGML_LOG_ERROR("ggml-hex: failed to reserve new session %d (physical %d, virtual %d) : error 0x%x\n", dev_id, phys_idx, virt_idx, err); + GGML_LOG_ERROR("ggml-hex: %s failed to reserve new session (physical %d, virtual %d) : error 0x%x\n", + this->c_name(), phys_idx, virt_idx, err); throw std::runtime_error("ggml-hex: remote_session_control(new-sess) failed (see log for details)"); } @@ -2857,10 +2891,21 @@ void ggml_hexagon_session::allocate(int dev_id) noexcept(false) { this->session_id = n.session_id; this->domain_id = n.effective_domain_id; this->valid_session = true; + } else { + struct remote_rpc_effective_domain_id eff = {}; + eff.domain_name = const_cast(dom_name.c_str()); + eff.domain_name_len = dom_name.size(); + eff.session_id = 0; + + int err = remote_session_control(FASTRPC_GET_EFFECTIVE_DOMAIN_ID, (void *) &eff, sizeof(eff)); + if (err == AEE_SUCCESS) { + this->domain_id = eff.effective_domain_id; + } else { + GGML_LOG_DEBUG("ggml-hex: %s FASTRPC_GET_EFFECTIVE_DOMAIN_ID returned 0x%x, using domain_id %d\n", + this->name.c_str(), err, this->domain_id); + } } - // Get session URI - char session_uri[256]; { char htp_uri[256]; @@ -2877,31 +2922,18 @@ void ggml_hexagon_session::allocate(int dev_id) noexcept(false) { int err = remote_session_control(FASTRPC_GET_URI, (void *) &u, sizeof(u)); if (err != AEE_SUCCESS) { - // fallback to single session uris - int htp_URI_domain_len = strlen(htp_uri) + MAX_DOMAIN_NAMELEN; + snprintf(session_uri, sizeof(session_uri), "%s&_dom=%s&_session=%u", + htp_uri, dom_name.c_str(), this->session_id); - snprintf(session_uri, htp_URI_domain_len, "%s%s", htp_uri, my_domain->uri); - - GGML_LOG_WARN("ggml-hex: failed to get URI for session %d (physical %d, virtual %d) : error 0x%x. Falling back to single session URI: %s\n", dev_id, phys_idx, virt_idx, err, session_uri); - } - } - - // Enable Unsigned PD - { - struct remote_rpc_control_unsigned_module u; - u.domain = this->domain_id; - u.enable = 1; - int err = remote_session_control(DSPRPC_CONTROL_UNSIGNED_MODULE, (void *) &u, sizeof(u)); - if (err != AEE_SUCCESS) { - GGML_LOG_ERROR("ggml-hex: failed to enable unsigned PD for session %d : error 0x%x\n", dev_id, err); - throw std::runtime_error("ggml-hex: remote_session_control(unsign) failed (see log for details)"); + GGML_LOG_WARN("ggml-hex: %s failed to get URI (physical %d, virtual %d) : error 0x%x. Falling back to single session URI: %s\n", + this->c_name(), phys_idx, virt_idx, err, session_uri); } } // Open session int err = htp_iface_open(session_uri, &this->handle); if (err != AEE_SUCCESS) { - GGML_LOG_ERROR("ggml-hex: failed to open session %d : error 0x%x\n", dev_id, err); + GGML_LOG_ERROR("ggml-hex: %s failed to open session : error 0x%x\n", this->c_name(), err); throw std::runtime_error("ggml-hex: failed to open session (see log for details)"); } @@ -2991,7 +3023,7 @@ void ggml_hexagon_session::allocate(int dev_id) noexcept(false) { this->op_batch = new ggml_hexagon_opbatch(this, opt_opbatch, this->max_vmem); // Start dspqueue/opbatch processing - err = htp_iface_start(this->handle, dev_id, this->queue_id, opt_nhvx, opt_nhmx, this->max_vmem); + err = htp_iface_start(this->handle, this->session_id, this->queue_id, opt_nhvx, opt_nhmx, this->max_vmem); if (err != 0) { GGML_LOG_ERROR("ggml-hex: %s failed to start session: 0x%08x\n", this->c_name(), (unsigned) err); throw std::runtime_error("ggml-hex: iface start failed (see log for details)"); @@ -3054,33 +3086,23 @@ void ggml_hexagon_session::release() noexcept(true) { this->cloned_buffers.clear(); } -ggml_hexagon_session::ggml_hexagon_session(int dev_id, ggml_backend_dev_t dev) noexcept(false) { - buffer_type.device = dev; - host_buffer_type.device = dev; - +ggml_hexagon_session::ggml_hexagon_session(const ggml_hexagon_device_config & config, ggml_backend_dev_t dev) noexcept(false) { op_batch = nullptr; op_queue = nullptr; fence_seq = ((uintptr_t)this) & 0xFFFF; try { - allocate(dev_id); - - buffer_type.iface = ggml_backend_hexagon_buffer_type_interface; - buffer_type.context = new ggml_backend_hexagon_buffer_type_context(this->name, this); - - host_buffer_type.iface = ggml_backend_hexagon_host_buffer_type_interface; - host_buffer_type.context = new ggml_backend_hexagon_buffer_type_context(this->name + "-HOST", this); + allocate(config); } catch (const std::exception & exc) { release(); throw; } + + GGML_UNUSED(dev); } ggml_hexagon_session::~ggml_hexagon_session() noexcept(true) { release(); - - delete static_cast(buffer_type.context); - delete static_cast(host_buffer_type.context); } // ** backend interface @@ -3957,11 +3979,13 @@ static void ggml_hexagon_precompute_fused_mmnx_params( } static bool ggml_hexagon_tensor_is_host(const struct ggml_hexagon_session * sess, const struct ggml_tensor * t) { - return t && t->buffer && t->buffer->buft == &sess->host_buffer_type; + return t && t->buffer && ggml_backend_buft_is_host(t->buffer->buft); + GGML_UNUSED(sess); } static bool ggml_hexagon_tensor_is_non_host(const struct ggml_hexagon_session * sess, const struct ggml_tensor * t) { - return t && t->buffer && t->buffer->buft != &sess->host_buffer_type; + return t && t->buffer && !ggml_backend_buft_is_host(t->buffer->buft); + GGML_UNUSED(sess); } static bool ggml_hexagon_supported_mul_mat(const struct ggml_hexagon_session * sess, const struct ggml_tensor * dst) { @@ -4643,6 +4667,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 +4691,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; } @@ -4675,6 +4701,7 @@ static htp_op_code op_remap_to_htp(const ggml_tensor * t) { switch (ggml_get_glu_op(t)) { case GGML_GLU_OP_SWIGLU: return HTP_OP_GLU_SWIGLU; case GGML_GLU_OP_SWIGLU_OAI: return HTP_OP_GLU_SWIGLU_OAI; + case GGML_GLU_OP_SWIGLU_CLAMP: return HTP_OP_GLU_SWIGLU_CLAMP; case GGML_GLU_OP_GEGLU: return HTP_OP_GLU_GEGLU; default: break; } @@ -4982,7 +5009,9 @@ static std::vector ggml_hexagon_graph_optimize_reorder(const std::vectorn_nodes; constexpr int MAX_FUSE = 16; @@ -5265,7 +5294,8 @@ bool ggml_backend_is_hexagon(ggml_backend_t backend) { // device interface static ggml_backend_t ggml_backend_hexagon_device_init(ggml_backend_dev_t dev, const char * params) { - auto sess = static_cast(dev->context); + auto dev_ctx = static_cast(dev->context); + auto sess = dev_ctx->session(); return new ggml_backend{ /* .guid = */ ggml_backend_hexagon_guid(), @@ -5278,8 +5308,8 @@ static ggml_backend_t ggml_backend_hexagon_device_init(ggml_backend_dev_t dev, c } static const char * ggml_backend_hexagon_device_get_name(ggml_backend_dev_t dev) { - auto sess = static_cast(dev->context); - return sess->c_name(); + auto dev_ctx = static_cast(dev->context); + return dev_ctx->c_name(); GGML_UNUSED(dev); } @@ -5317,16 +5347,16 @@ static void ggml_backend_hexagon_device_get_props(ggml_backend_dev_t dev, struct } static ggml_backend_buffer_type_t ggml_backend_hexagon_device_get_buffer_type(ggml_backend_dev_t dev) { - auto sess = static_cast(dev->context); - return &sess->buffer_type; + auto dev_ctx = static_cast(dev->context); + return &dev_ctx->buffer_type; } static ggml_backend_buffer_type_t ggml_backend_hexagon_device_get_host_buffer_type(ggml_backend_dev_t dev) { if (!opt_hostbuf) { return NULL; } - auto sess = static_cast(dev->context); - return &sess->host_buffer_type; + auto dev_ctx = static_cast(dev->context); + return &dev_ctx->host_buffer_type; } static bool ggml_hexagon_supported_cpy(const struct ggml_hexagon_session * sess, const struct ggml_tensor * op) { @@ -5417,7 +5447,8 @@ static bool ggml_hexagon_supported_fill(const struct ggml_hexagon_session * sess } static bool ggml_backend_hexagon_device_supports_op(ggml_backend_dev_t dev, const struct ggml_tensor * op) { - auto sess = static_cast(dev->context); + auto dev_ctx = static_cast(dev->context); + auto sess = dev_ctx->session(); // reject ops that match the filter if (opt_opfilter && std::regex_match(ggml_op_desc(op), *opt_opfilter)) { @@ -5463,6 +5494,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,12 +5513,14 @@ 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: supp = ggml_hexagon_supported_unary(sess, op); break; default: + supp = false; break; } break; @@ -5495,10 +5529,12 @@ static bool ggml_backend_hexagon_device_supports_op(ggml_backend_dev_t dev, cons switch (ggml_get_glu_op(op)) { case GGML_GLU_OP_SWIGLU: case GGML_GLU_OP_SWIGLU_OAI: + case GGML_GLU_OP_SWIGLU_CLAMP: case GGML_GLU_OP_GEGLU: supp = ggml_hexagon_supported_activations(sess, op); break; default: + supp = false; break; } break; @@ -5584,17 +5620,17 @@ static bool ggml_backend_hexagon_device_supports_op(ggml_backend_dev_t dev, cons } static bool ggml_backend_hexagon_device_supports_buft(ggml_backend_dev_t dev, ggml_backend_buffer_type_t buft) { - auto sess = static_cast(dev->context); + auto dev_ctx = static_cast(dev->context); // Technically we can clone hexagon buffers from any session but for some reason the output is garbled with layer-split, // tensor-split works correctly, so it needs mode debugging and investigation. For now accept only our own buffers. #if 0 bool supp = (buft->iface.get_alignment == ggml_backend_hexagon_buffer_type_get_alignment); #else - bool supp = (buft == &sess->host_buffer_type) || (buft == &sess->buffer_type); + bool supp = (buft == &dev_ctx->host_buffer_type) || (buft == &dev_ctx->buffer_type); #endif - HEX_VERBOSE("ggml-hex: %s device-supports-buft %s %s\n", sess->name.c_str(), ggml_backend_buft_name(buft), supp ? "yes" : "no"); + HEX_VERBOSE("ggml-hex: %s device-supports-buft %s %s\n", dev_ctx->c_name(), ggml_backend_buft_name(buft), supp ? "yes" : "no"); return supp; } @@ -5623,16 +5659,11 @@ ggml_hexagon_registry::ggml_hexagon_registry(ggml_backend_reg_t reg) { GGML_LOG_INFO("ggml-hex: Hexagon Arch version v%d\n", opt_arch); - // Create devices / sessions + // Create devices for (size_t i = 0; i < opt_ndev; i++) { - devices[i].iface = ggml_backend_hexagon_device_i; - devices[i].reg = reg; - try { - devices[i].context = new ggml_hexagon_session(i, &devices[i]); - } catch (const std::exception & exc) { - GGML_LOG_ERROR("ggml-hex: failed to create device/session %zu\n", i); - devices[i].context = nullptr; - } + devices[i].iface = ggml_backend_hexagon_device_i; + devices[i].reg = reg; + devices[i].context = new ggml_backend_hexagon_device_context(i, opt_device_configs[i], &devices[i]); } } @@ -5640,10 +5671,10 @@ ggml_hexagon_registry::ggml_hexagon_registry(ggml_backend_reg_t reg) { ggml_hexagon_registry::~ggml_hexagon_registry() { GGML_LOG_INFO("ggml-hex: releasing registry\n"); - // Release devices / sessions + // Release devices for (size_t i = 0; i < opt_ndev; i++) { - auto sess = static_cast(devices[i].context); - delete sess; + auto dev_ctx = static_cast(devices[i].context); + delete dev_ctx; } } @@ -5812,6 +5843,85 @@ template std::string vec_to_str(std::vector v) { return str; } +// Enumerate NPU (aka CDSP) domains via FASTRPC_GET_DOMAINS if supported, +// and populate domain_id and domain_name for all configured devices. +static void ggml_hexagon_discover_devices() { + std::unordered_map cdsp_map; + bool discovery_supported = false; + + system_req_payload domain_info = {}; + domain_info.id = FASTRPC_GET_DOMAINS; + domain_info.sys.domains = nullptr; + domain_info.sys.max_domains = 0; + domain_info.sys.flags = DOMAINS_LIST_FLAGS_SET_TYPE(0, FASTRPC_NSP); + + int err = remote_system_request(&domain_info); + if (err == AEE_SUCCESS && domain_info.sys.num_domains > 0) { + std::vector domains(domain_info.sys.num_domains); + domain_info.sys.domains = domains.data(); + domain_info.sys.max_domains = (int) domains.size(); + + err = remote_system_request(&domain_info); + if (err == AEE_SUCCESS) { + discovery_supported = true; + const int n_domains = std::min(domain_info.sys.num_domains, (int) domains.size()); + for (int i = 0; i < n_domains; i++) { + GGML_LOG_INFO("ggml-hex: FASTRPC_GET_DOMAINS[%d]: type %d id %d name '%s' status %d instance-id %d\n", + i, (int) domains[i].type, domains[i].id, domains[i].name, domains[i].status, domains[i].instance_id); + if (domains[i].type != FASTRPC_NSP) { + GGML_LOG_DEBUG("ggml-hex: skipping non-CDSP domain (type=%d)\n", (int) domains[i].type); + continue; + } + if (!domains[i].status) { + GGML_LOG_WARN("ggml-hex: skipping CDSP domain id=%d (status=down)\n", domains[i].id); + continue; + } + cdsp_map[domains[i].instance_id] = domains[i]; + GGML_LOG_INFO("ggml-hex: using CDSP domain: instance-id %d id %d name '%s'\n", + domains[i].instance_id, domains[i].id, domains[i].name); + } + } else { + GGML_LOG_WARN("ggml-hex: FASTRPC_GET_DOMAINS fetch failed (0x%x), using static CDSP domains\n", (unsigned) err); + } + } else if (err != AEE_SUCCESS) { + GGML_LOG_DEBUG("ggml-hex: FASTRPC_GET_DOMAINS query failed (0x%x), using static CDSP domains\n", (unsigned) err); + } + + // Populate domain IDs and names for all configured devices + for (size_t i = 0; i < opt_ndev; i++) { + auto & cfg = opt_device_configs[i]; + if (discovery_supported) { + auto it = cdsp_map.find(cfg.physical_idx); + if (it != cdsp_map.end()) { + cfg.domain_id = it->second.id; + cfg.domain_name = it->second.name; + } else { + GGML_LOG_ERROR("ggml-hex: physical CDSP core %d not found on device (%zu CDSP core(s) available)\n", + cfg.physical_idx, cdsp_map.size()); + cfg.domain_id = -1; + cfg.domain_name = ""; + } + } else { + switch (cfg.physical_idx) { + case 0: + cfg.domain_id = 3; + cfg.domain_name = CDSP_DOMAIN_NAME; + break; + case 1: + cfg.domain_id = 4; + cfg.domain_name = "cdsp1"; + break; + default: + GGML_LOG_ERROR("ggml-hex: physical CDSP core %d not supported without dynamic discovery\n", + cfg.physical_idx); + cfg.domain_id = -1; + cfg.domain_name = ""; + break; + } + } + } +} + static void ggml_hexagon_init(ggml_backend_reg * reg) { // Basic sanity checks to make sure definitions match static_assert((unsigned int) HTP_TYPE_Q4_0 == (unsigned int) GGML_TYPE_Q4_0, @@ -5977,6 +6087,9 @@ static void ggml_hexagon_init(ggml_backend_reg * reg) { } #endif + // Resolve domain info for all configured devices + ggml_hexagon_discover_devices(); + if (str_profile) { opt_pmu_evt = [&]() -> std::vector { auto v = str_to_vec(str_profile); diff --git a/ggml/src/ggml-hexagon/htp-drv.cpp b/ggml/src/ggml-hexagon/htp-drv.cpp index 4f0790801731..437e367c9d35 100644 --- a/ggml/src/ggml-hexagon/htp-drv.cpp +++ b/ggml/src/ggml-hexagon/htp-drv.cpp @@ -73,6 +73,7 @@ typedef int (*remote_handle64_close_pfn_t)(remote_handle h); typedef int (*remote_handle_control_pfn_t)(uint32_t req, void* data, uint32_t datalen); typedef int (*remote_handle64_control_pfn_t)(remote_handle64 h, uint32_t req, void* data, uint32_t datalen); typedef int (*remote_session_control_pfn_t)(uint32_t req, void *data, uint32_t datalen); +typedef int (*remote_system_request_pfn_t)(system_req_payload * req); // // Driver API pfns @@ -99,6 +100,7 @@ remote_handle64_close_pfn_t remote_handle64_close_pfn = nullptr; remote_handle_control_pfn_t remote_handle_control_pfn = nullptr; remote_handle64_control_pfn_t remote_handle64_control_pfn = nullptr; remote_session_control_pfn_t remote_session_control_pfn = nullptr; +remote_system_request_pfn_t remote_system_request_pfn = nullptr; // // Driver API @@ -206,6 +208,13 @@ HTPDRV_API int remote_session_control(uint32_t req, void * data, uint32_t datale return remote_session_control_pfn(req, data, datalen); } +HTPDRV_API int remote_system_request(system_req_payload * req) { + if (!remote_system_request_pfn) { + return AEE_EUNSUPPORTEDAPI; + } + return remote_system_request_pfn(req); +} + #ifdef _WIN32 static std::string wstr_to_str(std::wstring_view wstr) { @@ -367,6 +376,7 @@ int htpdrv_init() { dlsym(handle.get(), remote_handle64_control_pfn_t, remote_handle64_control_pfn, remote_handle64_control, false); dlsym(handle.get(), remote_session_control_pfn_t, remote_session_control_pfn, remote_session_control, false); dlsym(handle.get(), remote_handle64_close_pfn_t, remote_handle64_close_pfn, remote_handle64_close, false); + dlsym(handle.get(), remote_system_request_pfn_t, remote_system_request_pfn, remote_system_request, true); lib_cdsp_rpc_handle = std::move(handle); initialized = true; diff --git a/ggml/src/ggml-hexagon/htp-drv.h b/ggml/src/ggml-hexagon/htp-drv.h index f3cc0da75c28..8232780e7fdc 100644 --- a/ggml/src/ggml-hexagon/htp-drv.h +++ b/ggml/src/ggml-hexagon/htp-drv.h @@ -116,6 +116,8 @@ HTPDRV_API domain * htpdrv_get_domain(int domain_id); */ HTPDRV_API int htpdrv_get_arch(int domain, int * arch); +HTPDRV_API int remote_system_request(system_req_payload * req); + #ifdef __cplusplus } #endif diff --git a/ggml/src/ggml-hexagon/htp/act-ops.c b/ggml/src/ggml-hexagon/htp/act-ops.c index 0a8bf84e382a..ac00b447d989 100644 --- a/ggml/src/ggml-hexagon/htp/act-ops.c +++ b/ggml/src/ggml-hexagon/htp/act-ops.c @@ -180,6 +180,26 @@ static void swiglu_oai_f32(const float * restrict src0, } } +static void swiglu_clamp_f32(const float * restrict src0, + const float * restrict src1, + float * restrict dst, + const uint32_t num_rows, + const struct htp_act_context * actx) { + htp_glu_op_preamble; + const float limit = ((const float *) (actx->octx->op_params))[3]; + + for (uint32_t ib = 0; ib < num_rows; ib++) { + const uint8_t * restrict src0_ptr = (const uint8_t *) src0 + (ib * src0_row_size_aligned); + const uint8_t * restrict src1_ptr = (const uint8_t *) src1 + (ib * src1_row_size_aligned); + uint8_t * restrict dst_ptr = (uint8_t *) dst + (ib * dst_row_size_aligned); + + hvx_min_scalar_f32((uint8_t *) src0_ptr, src0_ptr, limit, nc); + hvx_clamp_scalar_f32((uint8_t *) src1_ptr, src1_ptr, -limit, limit, nc); + hvx_sigmoid_f32_aa(dst_ptr, src0_ptr, nc); + hvx_mul_mul_f32_aa(dst_ptr, src0_ptr, dst_ptr, src1_ptr, nc); + } +} + static const float GELU_COEF_A = 0.044715f; static const float SQRT_2_OVER_PI = 0.79788456080286535587989211986876f; @@ -411,6 +431,7 @@ static void geglu_f32(const float * restrict src0, DEFINE_GLU_PER_THREAD(swiglu, "swiglu-f32", swiglu_f32(src0_spad, src1_spad, dst_spad, block_size, actx)) DEFINE_GLU_PER_THREAD(swiglu_oai, "swiglu-oai-f32", swiglu_oai_f32(src0_spad, src1_spad, dst_spad, block_size, actx)) +DEFINE_GLU_PER_THREAD(swiglu_clamp, "swiglu-clamp-f32", swiglu_clamp_f32(src0_spad, src1_spad, dst_spad, block_size, actx)) DEFINE_GLU_PER_THREAD(geglu, "geglu-f32", geglu_f32(src0_spad, src1_spad, dst_spad, block_size, actx)) static int execute_op_activations_f32(struct htp_ops_context * octx) { @@ -437,6 +458,11 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { op_type = "swiglu-oai-f32"; break; + case HTP_OP_GLU_SWIGLU_CLAMP: + act_op_func = (worker_callback_t) glu_swiglu_clamp_f32_per_thread; + op_type = "swiglu-clamp-f32"; + break; + case HTP_OP_GLU_GEGLU: act_op_func = (worker_callback_t)glu_geglu_f32_per_thread; op_type = "geglu-f32"; @@ -527,7 +553,7 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { const uint8_t * data_src0 = (const uint8_t *) src0->data; const uint8_t * data_src1 = src1 ? (const uint8_t *) src1->data : NULL; - if (!src1 && (octx->op == HTP_OP_GLU_SWIGLU || octx->op == HTP_OP_GLU_SWIGLU_OAI || octx->op == HTP_OP_GLU_GEGLU)) { + if (!src1 && (octx->op == HTP_OP_GLU_SWIGLU || octx->op == HTP_OP_GLU_SWIGLU_OAI || octx->op == HTP_OP_GLU_SWIGLU_CLAMP || octx->op == HTP_OP_GLU_GEGLU)) { const int32_t swapped = octx->op_params[1]; data_src1 = data_src0; actx.src1_row_size = actx.src0_row_size; diff --git a/ggml/src/ggml-hexagon/htp/htp-ops.h b/ggml/src/ggml-hexagon/htp/htp-ops.h index b4023b34d388..53c95f28d323 100644 --- a/ggml/src/ggml-hexagon/htp/htp-ops.h +++ b/ggml/src/ggml-hexagon/htp/htp-ops.h @@ -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, @@ -94,6 +96,7 @@ enum htp_op_code { HTP_OP_FENCE, HTP_OP_ALLREDUCE, HTP_OP_ALLREDUCE_ADD, + HTP_OP_GLU_SWIGLU_CLAMP, HTP_OP_INVALID }; diff --git a/ggml/src/ggml-hexagon/htp/hvx-arith.h b/ggml/src/ggml-hexagon/htp/hvx-arith.h index c8d0003ab5c0..765c3577668e 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-arith.h +++ b/ggml/src/ggml-hexagon/htp/hvx-arith.h @@ -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 // diff --git a/ggml/src/ggml-hexagon/htp/hvx-log.h b/ggml/src/ggml-hexagon/htp/hvx-log.h index 7013dae785ac..a209f88d555c 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-log.h +++ b/ggml/src/ggml-hexagon/htp/hvx-log.h @@ -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 */ diff --git a/ggml/src/ggml-hexagon/htp/main.c b/ggml/src/ggml-hexagon/htp/main.c index 975ba0c7af51..27d1dedcdf02 100644 --- a/ggml/src/ggml-hexagon/htp/main.c +++ b/ggml/src/ggml-hexagon/htp/main.c @@ -777,11 +777,14 @@ 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); case HTP_OP_GLU_SWIGLU: case HTP_OP_GLU_SWIGLU_OAI: + case HTP_OP_GLU_SWIGLU_CLAMP: case HTP_OP_GLU_GEGLU: return op_activations(octx); diff --git a/ggml/src/ggml-hexagon/htp/unary-ops.c b/ggml/src/ggml-hexagon/htp/unary-ops.c index b21415a67d64..1a632bf5631e 100644 --- a/ggml/src/ggml-hexagon/htp/unary-ops.c +++ b/ggml/src/ggml-hexagon/htp/unary-ops.c @@ -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; diff --git a/ggml/src/ggml-hexagon/htp/unary-ops.h b/ggml/src/ggml-hexagon/htp/unary-ops.h index 1f4c3a5c4d96..458218ff4431 100644 --- a/ggml/src/ggml-hexagon/htp/unary-ops.h +++ b/ggml/src/ggml-hexagon/htp/unary-ops.h @@ -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; diff --git a/ggml/src/ggml-metal/ggml-metal-common.cpp b/ggml/src/ggml-metal/ggml-metal-common.cpp index 2eb9820bff91..6f1638a1147e 100644 --- a/ggml/src/ggml-metal/ggml-metal-common.cpp +++ b/ggml/src/ggml-metal/ggml-metal-common.cpp @@ -1,10 +1,27 @@ #include "ggml-metal-common.h" +#include "ggml.h" #include "ggml-impl.h" #include "ggml-backend-impl.h" #include +bool ggml_metal_op_mul_mat_use_mm(const struct ggml_tensor * op, bool has_simdgroup_mm) { + const int64_t ne00 = op->src[0]->ne[0]; + const int64_t ne11 = op->src[1]->ne[1]; + + return !ggml_is_transposed(op->src[0]) && + !ggml_is_transposed(op->src[1]) && + has_simdgroup_mm && ne00 >= 64 && ne11 > 8; +} + +bool ggml_metal_op_mul_mat_id_use_mm(const struct ggml_tensor * op, bool has_simdgroup_mm) { + const int64_t ne00 = op->src[0]->ne[0]; + const int64_t ne21 = op->src[2]->ne[1]; + + return has_simdgroup_mm && ne00 >= 64 && ne21 >= 32; +} + // represents a memory range (i.e. an interval from a starting address p0 to an ending address p1 in a given buffer pb) // the type indicates whether it is a source range (i.e. ops read data from it) or a destination range (i.e. ops write data to it) struct ggml_mem_range { diff --git a/ggml/src/ggml-metal/ggml-metal-common.h b/ggml/src/ggml-metal/ggml-metal-common.h index 3acbc6ae174a..66abdb52efe3 100644 --- a/ggml/src/ggml-metal/ggml-metal-common.h +++ b/ggml/src/ggml-metal/ggml-metal-common.h @@ -47,6 +47,10 @@ bool ggml_mem_ranges_check(ggml_mem_ranges_t mrs, const struct ggml_tensor * ten // if it proves to work well, we can start using it for other backends in the future void ggml_graph_optimize(struct ggml_cgraph * gf); +// mat-mat vs mat-vec dispatch; used by both supports_op and ggml_metal_op_mul_mat* +bool ggml_metal_op_mul_mat_use_mm (const struct ggml_tensor * op, bool has_simdgroup_mm); +bool ggml_metal_op_mul_mat_id_use_mm(const struct ggml_tensor * op, bool has_simdgroup_mm); + #ifdef __cplusplus } #endif diff --git a/ggml/src/ggml-metal/ggml-metal-device.cpp b/ggml/src/ggml-metal/ggml-metal-device.cpp index a82caa5e4303..5a2f01f1dbaa 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.cpp +++ b/ggml/src/ggml-metal/ggml-metal-device.cpp @@ -318,6 +318,7 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_glu(ggml_metal_l case GGML_GLU_OP_SWIGLU_OAI: op_str = "swiglu_oai"; break; case GGML_GLU_OP_GEGLU_ERF: op_str = "geglu_erf"; break; case GGML_GLU_OP_GEGLU_QUICK: op_str = "geglu_quick"; break; + case GGML_GLU_OP_SWIGLU_CLAMP: op_str = "swiglu_clamp"; break; default: GGML_ABORT("fatal error"); } break; default: GGML_ABORT("fatal error"); @@ -593,7 +594,7 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_ssm_scan(ggml_me // - sgptg floats for shared_x_dt (nsg) // - sgptg floats for shared_dA (nsg) // Total: nsg * (32 + 2) floats - res.smem = (32 + 2)*sizeof(float)*nsg; + res.smem = GGML_PAD((32 + 2)*sizeof(float)*nsg, 16); return res; } @@ -1029,6 +1030,7 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_mul_mm_id_map0(g } res.smem = (size_t) ne02*ne20*sizeof(uint16_t); + res.smem = GGML_PAD(res.smem, 16); return res; } diff --git a/ggml/src/ggml-metal/ggml-metal-device.m b/ggml/src/ggml-metal/ggml-metal-device.m index 41ce90dc8a93..83344539e549 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.m +++ b/ggml/src/ggml-metal/ggml-metal-device.m @@ -3,6 +3,7 @@ #import "ggml-impl.h" #import "ggml-backend-impl.h" #import "ggml-metal-impl.h" +#import "ggml-metal-common.h" #include @@ -788,6 +789,10 @@ void ggml_metal_encoder_debug_group_pop (ggml_metal_encoder_t encoder) { } void ggml_metal_encoder_set_pipeline(ggml_metal_encoder_t encoder, struct ggml_metal_pipeline_with_params pipeline) { + if (!pipeline.pipeline) { + GGML_ABORT("%s: nil Metal pipeline (missing kernel; see compile_pipeline log above)\n", __func__); + } + [encoder->obj setComputePipelineState:pipeline.pipeline->obj]; } @@ -800,6 +805,9 @@ void ggml_metal_encoder_set_buffer(ggml_metal_encoder_t encoder, struct ggml_met } void ggml_metal_encoder_set_threadgroup_memory_size(ggml_metal_encoder_t encoder, size_t size, int idx) { + // ref: https://developer.apple.com/documentation/metal/mtlcomputecommandencoder/setthreadgroupmemorylength(_:index:) + GGML_ASSERT(size % 16 == 0); + [encoder->obj setThreadgroupMemoryLength:size atIndex:idx]; } @@ -1407,6 +1415,30 @@ void ggml_metal_device_get_memory(ggml_metal_device_t dev, size_t * free, size_t } } +static bool ggml_metal_supports_mul_mat_op( + bool has_simdgroup_reduction, + const struct ggml_tensor * op, + bool src0_f16_has_mv, + bool mm_path) { + if (!has_simdgroup_reduction || op->src[0]->type == GGML_TYPE_NVFP4) { + return false; + } + + if (op->src[1]->type != GGML_TYPE_F16) { + return true; + } + + if (op->src[0]->type == GGML_TYPE_BF16) { + return false; + } + + if (src0_f16_has_mv && op->src[0]->type == GGML_TYPE_F16) { + return true; + } + + return mm_path; +} + bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_tensor * op) { const bool has_simdgroup_mm = dev->props.has_simdgroup_mm; const bool has_simdgroup_reduction = dev->props.has_simdgroup_reduction; @@ -1478,6 +1510,7 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te case GGML_GLU_OP_SWIGLU_OAI: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: return ggml_is_contiguous_1(op->src[0]) && (op->src[0]->type == GGML_TYPE_F32 || op->src[0]->type == GGML_TYPE_F16); default: return false; @@ -1710,9 +1743,15 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te case GGML_OP_GATED_DELTA_NET: return has_simdgroup_reduction && op->src[2]->ne[0] % 32 == 0; case GGML_OP_SOLVE_TRI: + return has_simdgroup_reduction && op->src[0]->type == GGML_TYPE_F32; case GGML_OP_MUL_MAT: + return ggml_metal_supports_mul_mat_op( + has_simdgroup_reduction, op, true, + ggml_metal_op_mul_mat_use_mm(op, has_simdgroup_mm)); case GGML_OP_MUL_MAT_ID: - return has_simdgroup_reduction && op->src[0]->type != GGML_TYPE_NVFP4; + return ggml_metal_supports_mul_mat_op( + has_simdgroup_reduction, op, false, + ggml_metal_op_mul_mat_id_use_mm(op, has_simdgroup_mm)); case GGML_OP_SET: case GGML_OP_CPY: case GGML_OP_DUP: diff --git a/ggml/src/ggml-metal/ggml-metal-impl.h b/ggml/src/ggml-metal/ggml-metal-impl.h index 9becf04797ba..49102afe9c02 100644 --- a/ggml/src/ggml-metal/ggml-metal-impl.h +++ b/ggml/src/ggml-metal/ggml-metal-impl.h @@ -660,6 +660,7 @@ typedef struct { uint64_t nb0; uint64_t nb1; uint64_t nb2; + uint64_t nb3; } ggml_metal_kargs_conv_transpose_2d; typedef struct { diff --git a/ggml/src/ggml-metal/ggml-metal-ops.cpp b/ggml/src/ggml-metal/ggml-metal-ops.cpp index 75de0f6dd08a..7671d1d01564 100644 --- a/ggml/src/ggml-metal/ggml-metal-ops.cpp +++ b/ggml/src/ggml-metal/ggml-metal-ops.cpp @@ -948,7 +948,7 @@ int ggml_metal_op_sum(ggml_metal_op_t ctx, int idx) { ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op->src[0]), 1); ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op), 2); - ggml_metal_encoder_set_threadgroup_memory_size(enc, nsg * sizeof(float), 0); + ggml_metal_encoder_set_threadgroup_memory_size(enc, GGML_PAD(nsg * sizeof(float), 16), 0); ggml_metal_encoder_dispatch_threadgroups(enc, 1, 1, 1, nth, 1, 1); @@ -2362,10 +2362,6 @@ int ggml_metal_op_mul_mat(ggml_metal_op_t ctx, int idx) { const int16_t r2 = ne12/ne02; const int16_t r3 = ne13/ne03; - // find the break-even point where the matrix-matrix kernel becomes more efficient compared - // to the matrix-vector kernel - const int ne11_mm_min = 8; - // first try to use small-batch mat-mv kernels // these should be efficient for BS [2, ~8] if (op->src[1]->type == GGML_TYPE_F32 && (ne00%128 == 0) && @@ -2468,12 +2464,7 @@ int ggml_metal_op_mul_mat(ggml_metal_op_t ctx, int idx) { ggml_metal_encoder_set_buffer (enc, ggml_metal_get_buffer_id(op), 3); ggml_metal_encoder_dispatch_threadgroups(enc, ((ne01 + r0ptg - 1)/r0ptg), ((ne11 + r1ptg - 1)/r1ptg), ne12*ne13, 32, nsg, 1); - } else if ( - !ggml_is_transposed(op->src[0]) && - !ggml_is_transposed(op->src[1]) && - // for now the matrix-matrix multiplication kernel only works on A14+/M1+ SoCs - // AMD GPU and older A-chips will reuse matrix-vector multiplication kernel - props_dev->has_simdgroup_mm && ne00 >= 64 && ne11 > ne11_mm_min) { + } else if (ggml_metal_op_mul_mat_use_mm(op, props_dev->has_simdgroup_mm)) { //GGML_LOG_INFO("matrix: ne00 = %6d, ne01 = %6d, ne02 = %6d, ne11 = %6d, ne12 = %6d\n", ne00, ne01, ne02, ne11, ne12); // some Metal matrix data types require aligned pointers @@ -2622,13 +2613,7 @@ int ggml_metal_op_mul_mat_id(ggml_metal_op_t ctx, int idx) { const uint32_t r2 = 1; const uint32_t r3 = 1; - // find the break-even point where the matrix-matrix kernel becomes more efficient compared - // to the matrix-vector kernel - // ne20 = n_used_experts - // ne21 = n_rows (batch size) - const int ne21_mm_id_min = 32; - - if (props_dev->has_simdgroup_mm && ne00 >= 64 && (ne21 >= ne21_mm_id_min)) { + if (ggml_metal_op_mul_mat_id_use_mm(op, props_dev->has_simdgroup_mm)) { // some Metal matrix data types require aligned pointers // ref: https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf (Table 2.5) //switch (op->src[0]->type) { @@ -4645,6 +4630,7 @@ int ggml_metal_op_conv_transpose_2d(ggml_metal_op_t ctx, int idx) { const int32_t OW = op->ne[0]; const int32_t OH = op->ne[1]; const int32_t OC = op->ne[2]; + const int32_t N = op->src[1]->ne[3]; ggml_metal_kargs_conv_transpose_2d args = { /*.IC =*/ IC, @@ -4657,6 +4643,7 @@ int ggml_metal_op_conv_transpose_2d(ggml_metal_op_t ctx, int idx) { /*.nb0 =*/ nb0, /*.nb1 =*/ nb1, /*.nb2 =*/ nb2, + /*.nb3 =*/ nb3, }; auto pipeline = ggml_metal_library_get_pipeline_conv_transpose_2d(lib, op); @@ -4671,7 +4658,7 @@ int ggml_metal_op_conv_transpose_2d(ggml_metal_op_t ctx, int idx) { const size_t smem = GGML_PAD(KW * KH * sizeof(float), 16); ggml_metal_encoder_set_threadgroup_memory_size(enc, smem, 0); - ggml_metal_encoder_dispatch_threadgroups(enc, OW, OH, OC, KW, KH, 1); + ggml_metal_encoder_dispatch_threadgroups(enc, OW, OH, OC * N, KW, KH, 1); return 1; } diff --git a/ggml/src/ggml-metal/ggml-metal-tuning.cpp b/ggml/src/ggml-metal/ggml-metal-tuning.cpp index 6d8c18e6a6a2..9114ab7424de 100644 --- a/ggml/src/ggml-metal/ggml-metal-tuning.cpp +++ b/ggml/src/ggml-metal/ggml-metal-tuning.cpp @@ -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 } }, @@ -278,6 +279,276 @@ constexpr fa_vec_entry_t fa_vec_tuned_table[] = { { { GGML_METAL_DEVICE_M1_PRO, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } }, { { GGML_METAL_DEVICE_M1_PRO, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 64, 64, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 128, 128, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 128, 128, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 128, 128, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 192, 128, 1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 192, 128, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 320, 256, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 320, 256, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_F16, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 32, 32, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 32, 32, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 64, 64, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 96, 96, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 96, 96, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 96, 96, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 96, 96, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 128, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 320, 256, 1, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 32, 32, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 32, 32, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 32, 32, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 96, 96, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 96, 96, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 128, 128, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 320, 256, 1, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q4_1, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 128, 128, 1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 128, 128, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 192, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 192, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 192, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 128, 1, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 128, 2, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 192, 128, 3, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 256, 256, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 320, 256, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 128, 128, 2, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 192, 192, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 192, 128, 1, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 192, 128, 2, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 192, 128, 3, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 256, 256, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 256, 256, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 320, 256, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 320, 256, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 320, 256, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 320, 256, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 320, 256, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 320, 256, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q5_1, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 32, 32, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 32, 32, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 32, 32, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 64, 64, 1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 64, 64, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 96, 96, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 96, 96, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 96, 96, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 320, 256, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 320, 256, 1, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 320, 256, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 320, 256, 2, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 320, 256, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 320, 256, 3, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M1_MAX, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } }, + + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 32, 32, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 64, 64, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 128, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 128, 128, 1, 1 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 128, 128, 1, 2 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 128, 128, 1, 3 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 128, 128, 1, 4 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 192, 128, 1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 192, 128, 1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 192, 128, 1, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 192, 128, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 320, 256, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 320, 256, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 320, 256, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_F16, 320, 256, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 32, 32, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 32, 32, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 32, 32, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 96, 96, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 96, 96, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 96, 96, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 128, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 512, 512, 2, 3 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 512, 512, 3, 3 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q4_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 32, 32, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 32, 32, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 32, 32, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 64, 64, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 96, 96, 1, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 96, 96, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 96, 96, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 320, 256, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 320, 256, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 320, 256, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 320, 256, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 320, 256, 3, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_ULTRA, GGML_TYPE_F16, 64, 64, -1, 0 }, { 1, 4 } }, { { GGML_METAL_DEVICE_M2_ULTRA, GGML_TYPE_F16, 64, 64, -1, 1 }, { 1, 4 } }, { { GGML_METAL_DEVICE_M2_ULTRA, GGML_TYPE_F16, 128, 128, 2, 1 }, { 1, 4 } }, @@ -449,6 +720,945 @@ 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_PRO, GGML_TYPE_F16, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 32, 32, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 32, 32, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 32, 32, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 96, 96, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 96, 96, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 128, 128, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 128, 128, 1, 2 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 128, 128, 1, 4 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 128, 128, 2, 2 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 128, 128, 2, 4 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 192, 192, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 192, 192, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 192, 192, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 192, 128, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 192, 128, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 192, 128, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 192, 128, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 256, 256, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 256, 256, 3, 0 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 320, 256, 3, 0 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 320, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_F16, 512, 512, 3, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 64, 64, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 64, 64, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 128, 128, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 192, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 192, 128, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 192, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 576, 512, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 576, 512, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 576, 512, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 576, 512, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_0, 576, 512, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 64, 64, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 64, 64, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 192, 192, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 192, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 192, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q4_1, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 96, 96, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 96, 96, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 128, 128, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 128, 128, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 128, 128, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 128, 128, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 256, 256, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 256, 256, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 256, 256, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 320, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 320, 256, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 320, 256, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 320, 256, 3, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 96, 96, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 96, 96, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 96, 96, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 96, 96, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 128, 128, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 128, 128, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 128, 128, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 128, 128, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 128, 128, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 256, 256, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 256, 256, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 256, 256, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 320, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 320, 256, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 320, 256, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 320, 256, 3, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 64, 64, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 64, 64, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 192, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 512, 512, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 512, 512, -1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_PRO, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_PRO, 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_M3_ULTRA, GGML_TYPE_F16, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 32, 32, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 64, 64, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 64, 64, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 64, 64, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 96, 96, 1, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 96, 96, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 128, 128, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 128, 128, 1, 1 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 128, 128, 1, 2 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 128, 128, 1, 4 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 192, 192, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 192, 192, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 192, 192, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 192, 128, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 192, 128, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 192, 128, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 256, 256, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 256, 256, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 320, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 512, 512, 3, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_F16, 512, 512, 3, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 32, 32, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 96, 96, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 96, 96, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 128, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 192, 128, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 320, 256, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 320, 256, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 576, 512, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 576, 512, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 576, 512, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_0, 576, 512, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 32, 32, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 96, 96, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 96, 96, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 128, 128, 1, 2 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 128, 128, 1, 4 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 128, 128, 2, 2 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 192, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 192, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 192, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 128, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 128, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 192, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q4_1, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 32, 32, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 32, 32, 3, 4 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 96, 96, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 96, 96, 1, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 128, 128, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 128, 128, 1, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 192, 192, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 256, 256, -1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 256, 256, 1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 256, 256, 2, 3 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 256, 256, 2, 4 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 320, 256, 1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 576, 512, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 576, 512, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 576, 512, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 576, 512, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_0, 576, 512, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 32, 32, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 32, 32, 3, 4 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 96, 96, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 96, 96, 1, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 128, 128, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 128, 128, 1, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 192, 192, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 192, 192, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 256, 256, 2, 3 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 256, 256, 2, 4 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 320, 256, 1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 320, 256, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 320, 256, 1, 3 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 512, 512, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 576, 512, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 576, 512, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 576, 512, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 576, 512, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q5_1, 576, 512, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 32, 32, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 96, 96, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 96, 96, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 96, 96, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 128, 128, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 128, 128, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 128, 128, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 128, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 128, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 128, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 128, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 128, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 192, 128, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 512, 512, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 512, 512, -1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 512, 512, 1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M3_ULTRA, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } }, + + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 32, 32, 1, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 32, 32, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 96, 96, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 96, 96, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 128, 128, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 128, 128, 1, 2 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 128, 128, 1, 4 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 128, 128, 2, 2 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 128, 128, 2, 4 }, { 1, 1 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 192, 192, 3, 0 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 192, 192, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 192, 192, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 192, 128, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 192, 128, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 192, 128, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 256, 256, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 256, 256, 3, 0 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 320, 256, 3, 0 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 320, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 512, 512, 3, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 512, 512, 3, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 512, 512, 3, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_F16, 512, 512, 3, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 64, 64, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 64, 64, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 64, 64, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 64, 64, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 64, 64, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 192, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 192, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 576, 512, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 576, 512, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 576, 512, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 576, 512, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_0, 576, 512, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 64, 64, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 64, 64, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 64, 64, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 64, 64, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 64, 64, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 192, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 192, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 576, 512, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 576, 512, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 576, 512, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 576, 512, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q4_1, 576, 512, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 96, 96, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 96, 96, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 128, 128, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 128, 128, 1, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 128, 128, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 128, 128, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 128, 128, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 128, 128, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 256, 256, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 256, 256, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 320, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 320, 256, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 320, 256, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 320, 256, 3, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 576, 512, 2, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 576, 512, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 576, 512, 2, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_0, 576, 512, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 96, 96, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 96, 96, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 128, 128, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 128, 128, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 128, 128, 1, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 128, 128, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 128, 128, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 128, 128, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 256, 256, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 256, 256, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 320, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 320, 256, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 320, 256, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 320, 256, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 320, 256, 3, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 576, 512, 2, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 576, 512, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 576, 512, 2, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q5_1, 576, 512, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 64, 64, 1, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 64, 64, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 64, 64, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 64, 64, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 192, 192, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 192, 192, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 192, 192, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 192, 192, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 512, 512, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 512, 512, -1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 576, 512, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } }, + + { { 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_Q4_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 32, 32, 1, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 64, 64, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 64, 64, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 128, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 128, 128, 3, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 192, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 192, 128, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 192, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 576, 512, 3, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 576, 512, 1, 1 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 576, 512, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 576, 512, 1, 3 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_0, 576, 512, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 32, 32, 1, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 64, 64, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 96, 96, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 96, 96, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 96, 96, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 96, 96, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 96, 96, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 192, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 192, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 512, 512, 3, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q4_1, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 96, 96, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 96, 96, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 128, 128, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 128, 128, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 256, 256, -1, 0 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 256, 256, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 256, 256, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 256, 256, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 320, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 320, 256, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 320, 256, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 320, 256, 2, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 320, 256, 3, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 320, 256, 3, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_0, 576, 512, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 96, 96, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 96, 96, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 96, 96, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 96, 96, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 128, 128, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 128, 128, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 128, 128, 3, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 128, 128, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 192, 192, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 256, 256, -1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 256, 256, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 256, 256, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 256, 256, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 320, 256, 1, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 320, 256, 1, 3 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 320, 256, 1, 4 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 320, 256, 3, 1 }, { 2, 2 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 3 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q5_1, 576, 512, 2, 4 }, { 1, 4 } }, + { { 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 +1850,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 } }, diff --git a/ggml/src/ggml-metal/ggml-metal.cpp b/ggml/src/ggml-metal/ggml-metal.cpp index 9756d47050c3..4d58dc821cf4 100644 --- a/ggml/src/ggml-metal/ggml-metal.cpp +++ b/ggml/src/ggml-metal/ggml-metal.cpp @@ -558,7 +558,9 @@ static void ggml_backend_metal_event_wait(ggml_backend_t backend, ggml_backend_e ggml_metal_event_wait(ctx, ev); } -static void ggml_backend_metal_graph_optimize(ggml_backend_t backend, ggml_cgraph * cgraph) { +static void ggml_backend_metal_graph_optimize(ggml_backend_t backend, ggml_cgraph * cgraph, ggml_backend_graph_optimize_params * params) { + GGML_UNUSED(params); + ggml_metal_t ctx = (ggml_metal_t)backend->context; ggml_metal_graph_optimize(ctx, cgraph); diff --git a/ggml/src/ggml-metal/kernels/conv.metal b/ggml/src/ggml-metal/kernels/conv.metal index 5685b5cd4915..a5d5aa9d9293 100644 --- a/ggml/src/ggml-metal/kernels/conv.metal +++ b/ggml/src/ggml-metal/kernels/conv.metal @@ -366,7 +366,8 @@ kernel void kernel_conv_transpose_2d( const int64_t out_x = tgpig[0]; const int64_t out_y = tgpig[1]; - const int64_t out_c = tgpig[2]; + const int64_t batch = tgpig[2] / args.OC; + const int64_t out_c = tgpig[2] % args.OC; const int64_t kw = tpitg[0]; const int64_t kh = tpitg[1]; @@ -390,7 +391,7 @@ kernel void kernel_conv_transpose_2d( if (in_x >= args.IW) continue; - const int64_t input_idx = (args.IW * args.IH) * in_c + (args.IW) * in_y + in_x; + const int64_t input_idx = (args.IW * args.IH) * (args.IC * batch + in_c) + (args.IW) * in_y + in_x; const int64_t kernel_idx = (args.KH * args.KW * args.OC) * in_c + (args.KH * args.KW) * out_c + (args.KW) * kh + kw; v += (float)src0[kernel_idx] * src1[input_idx]; @@ -408,7 +409,7 @@ kernel void kernel_conv_transpose_2d( total += shared_sum[i]; } - device float * dst_ptr = (device float *) (dst + out_x*args.nb0 + out_y * args.nb1 + out_c*args.nb2); + device float * dst_ptr = (device float *) (dst + batch*args.nb3 + out_c*args.nb2 + out_y * args.nb1 + out_x*args.nb0); dst_ptr[0] = total; } } diff --git a/ggml/src/ggml-metal/kernels/unary.metal b/ggml/src/ggml-metal/kernels/unary.metal index 39cad0cbee5a..e50a6486394c 100644 --- a/ggml/src/ggml-metal/kernels/unary.metal +++ b/ggml/src/ggml-metal/kernels/unary.metal @@ -317,6 +317,32 @@ typedef decltype(kernel_swiglu_oai) kernel_swiglu_oai_t; template [[host_name("kernel_swiglu_oai_f32")]] kernel kernel_swiglu_oai_t kernel_swiglu_oai; template [[host_name("kernel_swiglu_oai_f16")]] kernel kernel_swiglu_oai_t kernel_swiglu_oai; +template +kernel void kernel_swiglu_clamp( + constant ggml_metal_kargs_glu & args, + device const char * src0, + device const char * src1, + device char * dst, + uint tgpig[[threadgroup_position_in_grid]], + uint tpitg[[thread_position_in_threadgroup]], + uint ntg[[threads_per_threadgroup]]) { + device const T * src0_row = (device const T *) ((device const char *) src0 + tgpig*args.nb01) + args.i00; + device const T * src1_row = (device const T *) ((device const char *) src1 + tgpig*args.nb11) + args.i10; + device T * dst_row = (device T *) ((device char *) dst + tgpig*args.nb1); + + for (int i0 = tpitg; i0 < args.ne0; i0 += ntg) { + const float gate = min((float) src0_row[i0], args.limit); + const float up = clamp((float) src1_row[i0], -args.limit, args.limit); + + dst_row[i0] = (T)(gate / (1.0f + exp(-gate)) * up); + } +} + +typedef decltype(kernel_swiglu_clamp) kernel_swiglu_clamp_t; + +template [[host_name("kernel_swiglu_clamp_f32")]] kernel kernel_swiglu_clamp_t kernel_swiglu_clamp; +template [[host_name("kernel_swiglu_clamp_f16")]] kernel kernel_swiglu_clamp_t kernel_swiglu_clamp; + template kernel void kernel_geglu_erf( constant ggml_metal_kargs_glu & args, diff --git a/ggml/src/ggml-opencl/ggml-opencl.cpp b/ggml/src/ggml-opencl/ggml-opencl.cpp index 6ae83449b082..90635cc858f8 100644 --- a/ggml/src/ggml-opencl/ggml-opencl.cpp +++ b/ggml/src/ggml-opencl/ggml-opencl.cpp @@ -744,8 +744,9 @@ struct ggml_backend_opencl_context { cl_kernel kernel_tri; cl_kernel kernel_fill; cl_kernel kernel_clamp; - cl_kernel kernel_geglu, kernel_reglu, kernel_swiglu, kernel_swiglu_oai, kernel_geglu_erf, kernel_geglu_quick, - kernel_geglu_f16, kernel_reglu_f16, kernel_swiglu_f16, kernel_geglu_erf_f16, kernel_geglu_quick_f16; + cl_kernel kernel_geglu, kernel_reglu, kernel_swiglu, kernel_swiglu_oai, kernel_swiglu_clamp, kernel_geglu_erf, + kernel_geglu_quick, kernel_geglu_f16, kernel_reglu_f16, kernel_swiglu_f16, kernel_swiglu_clamp_f16, + kernel_geglu_erf_f16, kernel_geglu_quick_f16; cl_kernel kernel_norm, kernel_norm_mul_add; cl_kernel kernel_rms_norm, kernel_rms_norm_mul; cl_kernel kernel_l2_norm_f32; @@ -1601,11 +1602,13 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { CL_CHECK((backend_ctx->kernel_reglu = clCreateKernel(backend_ctx->program_glu, "kernel_reglu", &err), err)); CL_CHECK((backend_ctx->kernel_swiglu = clCreateKernel(backend_ctx->program_glu, "kernel_swiglu", &err), err)); CL_CHECK((backend_ctx->kernel_swiglu_oai = clCreateKernel(backend_ctx->program_glu, "kernel_swiglu_oai", &err), err)); + CL_CHECK((backend_ctx->kernel_swiglu_clamp = clCreateKernel(backend_ctx->program_glu, "kernel_swiglu_clamp", &err), err)); CL_CHECK((backend_ctx->kernel_geglu_erf = clCreateKernel(backend_ctx->program_glu, "kernel_geglu_erf", &err), err)); CL_CHECK((backend_ctx->kernel_geglu_quick = clCreateKernel(backend_ctx->program_glu, "kernel_geglu_quick", &err), err)); CL_CHECK((backend_ctx->kernel_geglu_f16 = clCreateKernel(backend_ctx->program_glu, "kernel_geglu_f16", &err), err)); CL_CHECK((backend_ctx->kernel_reglu_f16 = clCreateKernel(backend_ctx->program_glu, "kernel_reglu_f16", &err), err)); CL_CHECK((backend_ctx->kernel_swiglu_f16 = clCreateKernel(backend_ctx->program_glu, "kernel_swiglu_f16", &err), err)); + CL_CHECK((backend_ctx->kernel_swiglu_clamp_f16 = clCreateKernel(backend_ctx->program_glu, "kernel_swiglu_clamp_f16", &err), err)); CL_CHECK((backend_ctx->kernel_geglu_erf_f16 = clCreateKernel(backend_ctx->program_glu, "kernel_geglu_erf_f16", &err), err)); CL_CHECK((backend_ctx->kernel_geglu_quick_f16 = clCreateKernel(backend_ctx->program_glu, "kernel_geglu_quick_f16", &err), err)); GGML_LOG_CONT("."); @@ -6059,9 +6062,13 @@ static ggml_backend_opencl_context * ggml_cl_init(ggml_backend_dev_t dev) { } #ifdef GGML_OPENCL_USE_ADRENO_KERNELS - // determine whether to use Adreno xmem GEMM - backend_ctx->adreno_xmem_gemm_enabled = getenv("GGML_OPENCL_ADRENO_XMEM_GEMM") != nullptr && - backend_ctx->gpu_family == GPU_FAMILY::ADRENO; + // Adreno xmem F16xF32 GEMM, default on adreno, opt out with GGML_OPENCL_ADRENO_XMEM_GEMM=0. + // This helps models with f16 attention weights, e.g., gpt-oss-20b-f16 + { + const char * xmem_env = getenv("GGML_OPENCL_ADRENO_XMEM_GEMM"); + backend_ctx->adreno_xmem_gemm_enabled = backend_ctx->gpu_family == GPU_FAMILY::ADRENO && + (xmem_env ? atoi(xmem_env) != 0 : true); + } #endif // determine whether to use large buffer for Adreno @@ -7696,6 +7703,7 @@ static bool ggml_opencl_supports_op(ggml_backend_dev_t dev, const struct ggml_te case GGML_GLU_OP_SWIGLU_OAI: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: return ggml_is_contiguous_1(op->src[0]) && (op->type == GGML_TYPE_F32 || op->type == GGML_TYPE_F16); default: return false; @@ -19534,9 +19542,18 @@ static void ggml_cl_mul_mat(ggml_backend_t backend, const ggml_tensor * src0, co // GEMM using local memory // Current BK = 16, so ne00 % 16 == 0 + // + // Certain A7X compiler (E031.41) executes kernel_mul_mm_f32_f32_l4_lm poorly; + // matrices with ne11 <= 8 appears OK. + // Fallback to the MV style kernels for A7x and ne11 > 8. + // Override with GGML_OPENCL_A7X_F32_LM_BYPASS=0. + static const char * a7x_f32lm_env = getenv("GGML_OPENCL_A7X_F32_LM_BYPASS"); + static const bool a7x_f32lm_bypass = (a7x_f32lm_env == nullptr || a7x_f32lm_env[0] != '0'); if (src1t == GGML_TYPE_F32 && ne00 % 16 == 0 && - ne11 > 1) { + ne11 > 1 && + !(a7x_f32lm_bypass && src0t == GGML_TYPE_F32 && ne11 > 8 && + backend_ctx->adreno_gen == ADRENO_GPU_GEN::A7X)) { switch(src0t) { case GGML_TYPE_F32: { kernel = backend_ctx->kernel_mul_mm_f32_f32_l4_lm; @@ -24873,6 +24890,13 @@ static void ggml_cl_glu(ggml_backend_t backend, const ggml_tensor * src0, const case GGML_GLU_OP_SWIGLU_OAI: kernel = backend_ctx->kernel_swiglu_oai; break; + case GGML_GLU_OP_SWIGLU_CLAMP: + if (dst->type == GGML_TYPE_F32) { + kernel = backend_ctx->kernel_swiglu_clamp; + } else { + kernel = backend_ctx->kernel_swiglu_clamp_f16; + } + break; case GGML_GLU_OP_GEGLU_ERF: if (dst->type == GGML_TYPE_F32) { kernel = backend_ctx->kernel_geglu_erf; @@ -24928,8 +24952,10 @@ static void ggml_cl_glu(ggml_backend_t backend, const ggml_tensor * src0, const CL_CHECK(clSetKernelArg(kernel, 10, sizeof(int), &ne00_off)); CL_CHECK(clSetKernelArg(kernel, 11, sizeof(int), &ne10_off)); - if (ggml_get_glu_op(dst) == GGML_GLU_OP_SWIGLU_OAI) { + if (ggml_get_glu_op(dst) == GGML_GLU_OP_SWIGLU_OAI || ggml_get_glu_op(dst) == GGML_GLU_OP_SWIGLU_CLAMP) { CL_CHECK(clSetKernelArg(kernel, 12, sizeof(float), &limit)); + } + if (ggml_get_glu_op(dst) == GGML_GLU_OP_SWIGLU_OAI) { CL_CHECK(clSetKernelArg(kernel, 13, sizeof(float), &alpha)); } diff --git a/ggml/src/ggml-opencl/kernels/glu.cl b/ggml/src/ggml-opencl/kernels/glu.cl index 059a4bbf1ba7..30bad00f7d06 100644 --- a/ggml/src/ggml-opencl/kernels/glu.cl +++ b/ggml/src/ggml-opencl/kernels/glu.cl @@ -243,6 +243,71 @@ kernel void kernel_swiglu_oai( } } +//------------------------------------------------------------------------------ +// swiglu_clamp +//------------------------------------------------------------------------------ +kernel void kernel_swiglu_clamp( + global char * src0, + ulong offset0, + global char * src1, + ulong offset1, + global char * dst, + ulong offsetd, + ulong nb01, + ulong nb11, + int ne0, + ulong nb1, + int ne00_off, + int ne10_off, + float limit +) { + src0 = (global char*)((global char*)src0 + offset0); + src1 = (global char*)((global char*)src1 + offset1); + dst = (global char*)((global char*)dst + offsetd); + + global float * src0_row = (global float *) ((global char *) src0 + get_group_id(0)*nb01) + ne00_off; + global float * src1_row = (global float *) ((global char *) src1 + get_group_id(0)*nb11) + ne10_off; + global float * dst_row = (global float *) ((global char *) dst + get_group_id(0)*nb1); + + for (int i0 = get_local_id(0); i0 < ne0; i0 += get_local_size(0)) { + const float gate = min(src0_row[i0], limit); + const float up = clamp(src1_row[i0], -limit, limit); + + dst_row[i0] = gate / (1.0f + exp(-gate)) * up; + } +} + +kernel void kernel_swiglu_clamp_f16( + global char * src0, + ulong offset0, + global char * src1, + ulong offset1, + global char * dst, + ulong offsetd, + ulong nb01, + ulong nb11, + int ne0, + ulong nb1, + int ne00_off, + int ne10_off, + float limit +) { + src0 = (global char*)((global char*)src0 + offset0); + src1 = (global char*)((global char*)src1 + offset1); + dst = (global char*)((global char*)dst + offsetd); + + global half * src0_row = (global half *) ((global char *) src0 + get_group_id(0)*nb01) + ne00_off; + global half * src1_row = (global half *) ((global char *) src1 + get_group_id(0)*nb11) + ne10_off; + global half * dst_row = (global half *) ((global char *) dst + get_group_id(0)*nb1); + + for (int i0 = get_local_id(0); i0 < ne0; i0 += get_local_size(0)) { + const float gate = min((float) src0_row[i0], limit); + const float up = clamp((float) src1_row[i0], -limit, limit); + + dst_row[i0] = (half) (gate / (1.0f + exp(-gate)) * up); + } +} + //------------------------------------------------------------------------------ // geglu_erf //------------------------------------------------------------------------------ diff --git a/ggml/src/ggml-openvino/CMakeLists.txt b/ggml/src/ggml-openvino/CMakeLists.txt index cc089b721fc3..af3e0758ca2f 100644 --- a/ggml/src/ggml-openvino/CMakeLists.txt +++ b/ggml/src/ggml-openvino/CMakeLists.txt @@ -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") diff --git a/ggml/src/ggml-openvino/ggml-decoder.cpp b/ggml/src/ggml-openvino/ggml-decoder.cpp index 599f41aebbdc..006e005cb7aa 100644 --- a/ggml/src/ggml-openvino/ggml-decoder.cpp +++ b/ggml/src/ggml-openvino/ggml-decoder.cpp @@ -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(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 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 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 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); diff --git a/ggml/src/ggml-openvino/ggml-decoder.h b/ggml/src/ggml-openvino/ggml-decoder.h index 8e39a26c8b79..74cb7385029a 100644 --- a/ggml/src/ggml-openvino/ggml-decoder.h +++ b/ggml/src/ggml-openvino/ggml-decoder.h @@ -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 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 { diff --git a/ggml/src/ggml-openvino/ggml-openvino-extra.cpp b/ggml/src/ggml-openvino/ggml-openvino-extra.cpp index 36c749244f83..36dfa4d9471b 100644 --- a/ggml/src/ggml-openvino/ggml-openvino-extra.cpp +++ b/ggml/src/ggml-openvino/ggml-openvino-extra.cpp @@ -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)); diff --git a/ggml/src/ggml-openvino/ggml-openvino.cpp b/ggml/src/ggml-openvino/ggml-openvino.cpp index e299e16c778a..4b1789713d1d 100644 --- a/ggml/src/ggml-openvino/ggml-openvino.cpp +++ b/ggml/src/ggml-openvino/ggml-openvino.cpp @@ -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 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 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) { diff --git a/ggml/src/ggml-openvino/openvino/op/cpy.cpp b/ggml/src/ggml-openvino/openvino/op/cpy.cpp index 5b387fc50d38..6f1e34779ac4 100644 --- a/ggml/src/ggml-openvino/openvino/op/cpy.cpp +++ b/ggml/src/ggml-openvino/openvino/op/cpy.cpp @@ -3,8 +3,11 @@ #include "../utils.h" #include +#include +#include #include -#include +#include +#include #include #include #include @@ -12,9 +15,14 @@ #include #include #include +#include #include +#include #include #include +#include +#include +#include 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 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(context.get_input(0), src_begin, int_max, one, axis); + ov::Output 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::v0::Constant::create(ov::element::i64, {1}, {ssm_state_size}), len); + state_begin = std::make_shared(state_rows); + } else { + state_begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {-ssm_state_size}); + } + } + auto state_part = + std::make_shared(context.get_input(0), state_begin, int_max, one, axis); src = std::make_shared(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( - src_begin, ov::op::v0::Constant::create(ov::element::i64, {1}, {window_size})); - auto window = std::make_shared(context.get_input(0), src_begin, src_end, one, - ov::op::v0::Constant::create(ov::element::i64, {1}, {3})); + ov::Output 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( + src_begin, ov::op::v0::Constant::create(ov::element::i64, {1}, {window_size})); + window = std::make_shared(context.get_input(0), src_begin, src_end, one, col_axis); + } else if (context.has_input("chunk_valid_len")) { + std::vector offsets(window_size); + std::iota(offsets.begin(), offsets.end(), 0); + auto indices = std::make_shared( + ov::op::v0::Constant::create(ov::element::i64, {(size_t) window_size}, offsets), + context.get_input("chunk_valid_len")); + window = std::make_shared(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(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 token_len_per_seq; + Output 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(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{1, 1, 1}); + auto new_shape = std::make_shared(ov::OutputVector{ones_c, n_write_dyn}, 0); + + auto reshaped = std::make_shared(input, new_shape, false); + auto data = std::make_shared(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 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(attention_size_in, token_len_in); + auto new_start = std::make_shared(past_tokens, n_state_c); + start = std::make_shared( + 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(start); + auto end = std::make_shared(start_squeezed, n_write_dyn); + auto end_squeezed = std::make_shared(end); + auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1}); + auto step_squeezed = std::make_shared(step); + auto indices = + std::make_shared(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(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(output_shape.rank().get_length())}, output_shape.to_shape()); diff --git a/ggml/src/ggml-openvino/openvino/op/flash_attn_ext.cpp b/ggml/src/ggml-openvino/openvino/op/flash_attn_ext.cpp index 582df0130b59..06547f3d2968 100644 --- a/ggml/src/ggml-openvino/openvino/op/flash_attn_ext.cpp +++ b/ggml/src/ggml-openvino/openvino/op/flash_attn_ext.cpp @@ -3,8 +3,8 @@ #include "../utils.h" #include "ggml-openvino/ggml-openvino-extra.h" +#include #include -#include #include #include #include @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -24,13 +25,62 @@ namespace ov { namespace frontend { namespace ggml { namespace op { +static ov::Output reshape_flat_kv(const ov::Output & kv_flat, + size_t view_offset_bytes, + size_t nb1_bytes, + int64_t n_head, + int64_t head_size, + const ov::Output & 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(attention_size, n_state_c); + auto end_c = std::make_shared(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(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::OutputVector{one_c, attention_size, n_head_c, head_size_c}, 0); + auto reshaped = std::make_shared(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(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 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(context.get_output_op_params()); float scale = params[0]; @@ -43,16 +93,19 @@ OutputVector translate_flash_attn_ext(const NodeContext & context) { ov::Output 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(mask, ov::element::f16); + ov::Output 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(mask, ov::element::f16); + } } //auto tile_kv = [&](int64_t num_heads, int64_t num_heads_kv, int64_t head_size, ov::Output 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(mask, ov::op::v0::Constant::create(ov::element::i64, {1}, {2})); - // mask_unsq1: [B, 1, 1, S_q, S_k] (rank 5) - ov::Output qk_masked = std::make_shared(qk_scaled, mask_unsq1); + ov::Output qk_masked; + if (has_mask) { + auto mask_unsq1 = + std::make_shared(mask, ov::op::v0::Constant::create(ov::element::i64, {1}, {2})); + qk_masked = std::make_shared(qk_scaled, mask_unsq1); + } else { + qk_masked = qk_scaled; + } auto softmax = std::make_shared(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(q, k, v, mask, scale_node, false); - res = std::make_shared(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(q, k, v, mask, scale_node, causal); + res = std::make_shared( + sdpa, ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3})); + } else { + auto sdpa = std::make_shared(q, k, v, scale_node, causal); + res = std::make_shared( + sdpa, ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3})); + } res = std::make_shared(res, ov::element::f32); return rename_outputs_with_suffix({res}, context.get_name()); } diff --git a/ggml/src/ggml-openvino/openvino/op/gated_delta_net.cpp b/ggml/src/ggml-openvino/openvino/op/gated_delta_net.cpp index 66c748283311..07eeb3c8fd6d 100644 --- a/ggml/src/ggml-openvino/openvino/op/gated_delta_net.cpp +++ b/ggml/src/ggml-openvino/openvino/op/gated_delta_net.cpp @@ -7,12 +7,15 @@ #include #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include @@ -80,6 +83,28 @@ OutputVector translate_gated_delta_net(const NodeContext & context) { g = std::make_shared(g, ov::op::v0::Constant::create(ov::element::i64, {1}, {3})); beta = std::make_shared(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 positions(n_tokens); + std::iota(positions.begin(), positions.end(), 0); + auto valid = std::make_shared( + ov::op::v0::Constant::create(ov::element::i64, {(size_t) n_tokens}, positions), + context.get_input("chunk_valid_len")); + auto mask = std::make_shared( + std::make_shared(valid, g.get_element_type()), + ov::op::v0::Constant::create(ov::element::i64, {2}, std::vector{0, 2})); + g = std::make_shared(g, mask); + beta = std::make_shared(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; diff --git a/ggml/src/ggml-openvino/openvino/op/glu_geglu_quick.cpp b/ggml/src/ggml-openvino/openvino/op/glu_geglu_quick.cpp new file mode 100644 index 000000000000..c6d64aed43aa --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/op/glu_geglu_quick.cpp @@ -0,0 +1,64 @@ +#include "../node_context.h" +#include "../op_table.h" +#include "../utils.h" + +#include +#include +#include +#include +#include +#include + +namespace ov { +namespace frontend { +namespace ggml { +namespace op { + +OutputVector translate_glu_geglu_quick(const NodeContext & context) { + num_inputs_check(context, 1, 2); + + ov::Output src0; + ov::Output 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(combined, start0, stop0, step, axis); + src1 = std::make_shared(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(src0, coef); + auto sigmoid = std::make_shared(scaled); + auto gated = std::make_shared(src0, sigmoid); + auto res = std::make_shared(gated, src1); + + return rename_outputs_with_suffix({res}, context.get_name()); +} + +} // namespace op +} // namespace ggml +} // namespace frontend +} // namespace ov diff --git a/ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp b/ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp index d220f2f584a5..d81fc53b5d02 100644 --- a/ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp +++ b/ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp @@ -89,6 +89,21 @@ OutputVector translate_glu_swiglu_oai(const NodeContext & context) { return rename_outputs_with_suffix({res}, context.get_name()); } +OutputVector translate_glu_swiglu_clamp(const NodeContext & context) { + auto [src0, src1] = get_glu_inputs(context); + + const int32_t * params = context.get_output_op_params(); + const float limit = reinterpret_cast(params)[3]; + + auto gate = std::make_shared(src0, -std::numeric_limits::infinity(), limit); + auto sigmoid = std::make_shared(gate); + auto silu = std::make_shared(gate, sigmoid); + auto up = std::make_shared(src1, -limit, limit); + auto res = std::make_shared(silu, up); + + return rename_outputs_with_suffix({res}, context.get_name()); +} + } // namespace op } // namespace ggml } // namespace frontend diff --git a/ggml/src/ggml-openvino/openvino/op/pool_2d.cpp b/ggml/src/ggml-openvino/openvino/op/pool_2d.cpp new file mode 100644 index 000000000000..fb6333175f02 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/op/pool_2d.cpp @@ -0,0 +1,53 @@ +#include "../node_context.h" +#include "../op_table.h" +#include "../utils.h" + +#include +#include +#include + +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 input = context.get_input(0); + ov::Strides strides{static_cast(s1), static_cast(s0)}; + ov::Shape pads_begin{static_cast(p1), static_cast(p0)}; + ov::Shape pads_end{static_cast(p1), static_cast(p0)}; + ov::Shape kernel{static_cast(k1), static_cast(k0)}; + ov::Output res; + + switch (op_case) { + case 1: // GGML_OP_POOL_MAX + { + res = std::make_shared(input, strides, pads_begin, pads_end, kernel); + break; + } + case 2: // GGML_OP_POOL_AVG + { + res = std::make_shared(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 diff --git a/ggml/src/ggml-openvino/openvino/op/roll.cpp b/ggml/src/ggml-openvino/openvino/op/roll.cpp new file mode 100644 index 000000000000..e8d1b8e50b34 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/op/roll.cpp @@ -0,0 +1,36 @@ +#include "../node_context.h" +#include "../op_table.h" +#include "../utils.h" + +#include +#include + +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{s3, s2, s1, s0}); + auto axes = ov::op::v0::Constant::create( + ov::element::i64, ov::Shape{4}, std::vector{0, 1, 2, 3}); + + auto roll = std::make_shared(input, shift, axes); + return rename_outputs_with_suffix({roll}, context.get_name()); +} + +} // namespace op +} // namespace ggml +} // namespace frontend +} // namespace ov diff --git a/ggml/src/ggml-openvino/openvino/op/view.cpp b/ggml/src/ggml-openvino/openvino/op/view.cpp index 138526cb49c6..56f5ceec9bb0 100644 --- a/ggml/src/ggml-openvino/openvino/op/view.cpp +++ b/ggml/src/ggml-openvino/openvino/op/view.cpp @@ -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 diff --git a/ggml/src/ggml-openvino/openvino/op_table.cpp b/ggml/src/ggml-openvino/openvino/op_table.cpp index 3c26fe83b1ad..d4f5ac307329 100644 --- a/ggml/src/ggml-openvino/openvino/op_table.cpp +++ b/ggml/src/ggml-openvino/openvino/op_table.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -55,10 +56,13 @@ std::unordered_map get_supported_ops() { {"GGML_UNARY_OP_SIGMOID", op::translate_1to1_match_1_input }, {"GGML_UNARY_OP_EXP", op::translate_1to1_match_1_input }, {"GGML_UNARY_OP_NEG", op::translate_1to1_match_1_input }, + {"GGML_UNARY_OP_RELU", op::translate_1to1_match_1_input }, {"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_SWIGLU_CLAMP", op::translate_glu_swiglu_clamp }, {"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 +76,8 @@ std::unordered_map 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 }, }; diff --git a/ggml/src/ggml-openvino/openvino/op_table.h b/ggml/src/ggml-openvino/openvino/op_table.h index d4b9292d6377..a0a42bff337d 100644 --- a/ggml/src/ggml-openvino/openvino/op_table.h +++ b/ggml/src/ggml-openvino/openvino/op_table.h @@ -37,7 +37,9 @@ GGML_OP_CONVERTER(translate_transpose); GGML_OP_CONVERTER(translate_view); GGML_OP_CONVERTER(translate_glu_swiglu); GGML_OP_CONVERTER(translate_glu_swiglu_oai); +GGML_OP_CONVERTER(translate_glu_swiglu_clamp); 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 +55,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 diff --git a/ggml/src/ggml-openvino/openvino/pass/fuse_to_conv.cpp b/ggml/src/ggml-openvino/openvino/pass/fuse_to_conv.cpp new file mode 100644 index 000000000000..21801c0f3992 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/pass/fuse_to_conv.cpp @@ -0,0 +1,212 @@ +#include "fuse_to_conv.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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({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(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(trace.get_node_shared_ptr())) { + trace = n->input_value(0); + } + + for (int i = 0; i < 2; ++i) { + auto n = ov::as_type_ptr(trace.get_node_shared_ptr()); + if (!n) { + return false; + } + trace = n->input_value(0); + } + + if (auto n = ov::as_type_ptr(trace.get_node_shared_ptr())) { + trace = n->input_value(0); + } else { + return false; + } + + if (auto n = ov::as_type_ptr(trace.get_node_shared_ptr())) { + trace = n->input_value(0); + } else { + return false; + } + + if (auto n = ov::as_type_ptr(trace.get_node_shared_ptr())) { + trace = n->input_value(0); + } else { + return false; + } + + auto eip = ov::as_type_ptr(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(eip->input_value(0).get_node_shared_ptr()); + if (!pad) { + return false; + } + auto pads_begin_const = + ov::as_type_ptr(pad->input_value(1).get_node_shared_ptr()); + + const auto pads_begin_vals = pads_begin_const->cast_vector(); // {0, 0, pad_h, pad_w} + const std::ptrdiff_t pad_h = static_cast(pads_begin_vals[2]); + const std::ptrdiff_t pad_w = static_cast(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(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(w_trace.get_node_shared_ptr()); + if (!n) { + break; + } + w_trace = n->input_value(0); + } + + auto weight_const = ov::as_type_ptr(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(weight_const->get_element_type(), conv_w_shape, + weight_const->get_data_ptr()); + + ov::Output weight_input = weight_reshaped; + if (weight_reshaped->get_element_type() != image_input.get_element_type()) { + weight_input = register_new_node(weight_reshaped, image_input.get_element_type()); + } + + auto conv = register_new_node( + image_input, weight_input, + ov::Strides{static_cast(eip_strides[0]), static_cast(eip_strides[1])}, + ov::CoordinateDiff{pad_h, pad_w}, ov::CoordinateDiff{pad_h, pad_w}, + ov::Strides{static_cast(eip_rates[0]), static_cast(eip_rates[1])}, + ov::op::PadType::EXPLICIT); + + constexpr auto target_type = ov::element::f32; + ov::Output conv_out = conv; + if (conv_out.get_element_type() != target_type) { + conv_out = register_new_node(conv_out, target_type); + } + + std::shared_ptr add_node; + ov::Output bias_input; + for (const auto & consumer_in : matmul_node->output(0).get_target_inputs()) { + auto cast = ov::as_type_ptr(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(add_in.get_node()->shared_from_this()); + if (!add) { + continue; + } + for (size_t i = 0; i < 2; ++i) { + if (ov::as_type_ptr(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 final_out; + std::shared_ptr target_node; + + if (add_node) { + // Reshape bias [OC, 1] → [1, OC, 1, 1] for NCHW broadcasting. + ov::Output bias = bias_input; + if (bias.get_element_type() != target_type) { + bias = register_new_node(bias, target_type); + } + const auto oc = static_cast(conv_w_shape[0]); + auto bias_shape = register_new_node(ov::element::i64, ov::Shape{4}, + std::vector{1, oc, 1, 1}); + bias = register_new_node(bias, bias_shape, false); + final_out = register_new_node(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::element::i64, ov::Shape{orig_shape.size()}, + orig_shape.to_shape()); + final_out = register_new_node(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(m_matmul, "ov::frontend::ggml::pass::FuseToConv"), callback); +} + +} // namespace pass +} // namespace ggml +} // namespace frontend +} // namespace ov diff --git a/ggml/src/ggml-openvino/openvino/pass/fuse_to_conv.h b/ggml/src/ggml-openvino/openvino/pass/fuse_to_conv.h new file mode 100644 index 000000000000..feac14b13ff2 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/pass/fuse_to_conv.h @@ -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 diff --git a/ggml/src/ggml-openvino/openvino/translate_session.cpp b/ggml/src/ggml-openvino/openvino/translate_session.cpp index 35598aba6be8..df3a72f3286c 100644 --- a/ggml/src/ggml-openvino/openvino/translate_session.cpp +++ b/ggml/src/ggml-openvino/openvino/translate_session.cpp @@ -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 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 TranslateSession::apply_transformations(std::shared_ptr( std::vector{ov::element::u8, ov::element::i8, ov::element::u4, ov::element::i4}); + manager.register_pass(); if (ggml_model_decoder->is_stateful()) { const auto kv_param_res_names = ggml_model_decoder->get_kv_param_res_names(); diff --git a/ggml/src/ggml-openvino/openvino/utils.cpp b/ggml/src/ggml-openvino/openvino/utils.cpp index 504d74b70679..8bb7678ee381 100644 --- a/ggml/src/ggml-openvino/openvino/utils.cpp +++ b/ggml/src/ggml-openvino/openvino/utils.cpp @@ -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; diff --git a/ggml/src/ggml-openvino/utils.cpp b/ggml/src/ggml-openvino/utils.cpp index 4df8381dcbd9..93b1ccbe9075 100644 --- a/ggml/src/ggml-openvino/utils.cpp +++ b/ggml/src/ggml-openvino/utils.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -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 r_ctx = std::static_pointer_cast(ctx->runtime_context); @@ -168,13 +170,24 @@ ov::Tensor create_ov_output_tensor(std::shared_ptr 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(ggml_decoder_prefill); - auto input_model_decode = std::make_shared(ggml_decoder_decode); - - 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(); - - 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); - } + const bool dump_ir = ggml_openvino_getenv_int("GGML_OPENVINO_DUMP_IR"); + const auto dump_ir_timestamp = static_cast(ggml_time_us()); + + auto build_static_model = [&core, &config, dump_ir, dump_ir_timestamp]( + std::shared_ptr decoder, + const char * tag, + std::shared_ptr & model, + ov::CompiledModel & compiled_model, + std::shared_ptr & infer_request, + int64_t & local_conversion_end_time, + int64_t & local_compile_end_time) { + auto input_model = std::make_shared(decoder); + model = ov::frontend::ggml::FrontEnd::convert(input_model); + decoder->clear_model_weights(); + local_conversion_end_time = ggml_time_us(); + + 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(compiled_model.create_infer_request()); + local_compile_end_time = ggml_time_us(); + }; + std::shared_ptr model_prefill; + std::shared_ptr 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(compiled_model_prefill.create_infer_request()); - auto infer_request_decode = std::make_shared(compiled_model_decode.create_infer_request()); - compile_end_time = ggml_time_us(); + std::shared_ptr infer_request_prefill; + std::shared_ptr 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_ptrne[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_ptrsecond; + 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_ptrsecond; + 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 ggml_decoder, cons ov::Tensor get_ov_input_tensor_static_decode(std::shared_ptr 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 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) 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() = 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()) << 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 diff --git a/ggml/src/ggml-openvino/utils.h b/ggml/src/ggml-openvino/utils.h index 513fa83c9d6e..5aa74da38d3b 100644 --- a/ggml/src/ggml-openvino/utils.h +++ b/ggml/src/ggml-openvino/utils.h @@ -164,7 +164,9 @@ std::vector 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 ggml_decoder, const std::string & param_name); ov::Tensor get_ov_input_tensor_static_decode(std::shared_ptr ggml_decoder, diff --git a/ggml/src/ggml-rpc/CMakeLists.txt b/ggml/src/ggml-rpc/CMakeLists.txt index b2f086380d5e..af3bd0290f7c 100644 --- a/ggml/src/ggml-rpc/CMakeLists.txt +++ b/ggml/src/ggml-rpc/CMakeLists.txt @@ -34,10 +34,14 @@ if (GGML_RPC_RDMA) find_library(RDMA_LIB ${RDMA_LIB_NAME} REQUIRED) endif() target_compile_definitions(ggml-rpc PRIVATE GGML_RPC_RDMA) - target_link_libraries(ggml-rpc PRIVATE ${RDMA_LIB}) if (APPLE) + # librdma.dylib only exists on macOS 26.2 and later. Link it weakly so a build made + # where it exists still loads where it does not; checked at runtime before use. + target_link_options(ggml-rpc PRIVATE "LINKER:-weak_library,${RDMA_LIB}") target_compile_definitions(ggml-rpc PRIVATE GGML_RPC_RDMA_APPLE) target_sources(ggml-rpc PRIVATE transport-apple.cpp) + else() + target_link_libraries(ggml-rpc PRIVATE ${RDMA_LIB}) endif() message(STATUS " RDMA transport enabled (${RDMA_DESC})") else() diff --git a/ggml/src/ggml-rpc/ggml-rpc.cpp b/ggml/src/ggml-rpc/ggml-rpc.cpp index 9aa5883d80de..58a8a030cfa5 100644 --- a/ggml/src/ggml-rpc/ggml-rpc.cpp +++ b/ggml/src/ggml-rpc/ggml-rpc.cpp @@ -826,10 +826,10 @@ static size_t ggml_backend_rpc_buffer_type_get_alloc_size(ggml_backend_buffer_ty // See comments in init_tensor. rpc_get |= ggml_is_quantized(tensor->type) && (tensor->ne[0] % 512 != 0) && (tensor->view_src == nullptr); - // ops that require additional memory for fleeting data on certain backends + // [TAG_ALLOC_SIZE_EXPAND] + // ops that may require additional memory for fleeting data on certain backends // ref: https://github.com/ggml-org/llama.cpp/pull/15966 - rpc_get |= tensor->op == GGML_OP_FLASH_ATTN_EXT; - rpc_get |= tensor->op == GGML_OP_MUL_MAT_ID; + rpc_get |= ggml_backend_op_alloc_size_may_expand(tensor->op); if (rpc_get) { ggml_backend_rpc_buffer_type_context * buft_ctx = (ggml_backend_rpc_buffer_type_context *)buft->context; diff --git a/ggml/src/ggml-rpc/transport-apple.cpp b/ggml/src/ggml-rpc/transport-apple.cpp index c8be77a6dcef..b1934175b1dd 100644 --- a/ggml/src/ggml-rpc/transport-apple.cpp +++ b/ggml/src/ggml-rpc/transport-apple.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -114,16 +115,9 @@ struct apple_rdma::impl { ~impl() { broken = true; - // the QP must be destroyed before the memory it can still write to is - // deregistered and freed: ERR only starts flushing the posted WQEs - if (qp) { - struct ibv_qp_attr a = {}; - a.qp_state = IBV_QPS_ERR; - ibv_modify_qp(qp, &a, IBV_QP_STATE); - struct ibv_wc wc[RDMA_NBUF * 2]; - while (ibv_poll_cq(cq, RDMA_NBUF * 2, wc) > 0) {} - ibv_destroy_qp(qp); - } + // destroy the QP first: it can still write to the rings until it is gone. + // no IBV_QPS_ERR before it - Apple's provider then fails every region unmap. + if (qp) ibv_destroy_qp(qp); if (send_mr) ibv_dereg_mr(send_mr); if (recv_mr) ibv_dereg_mr(recv_mr); free(send_mem); @@ -184,11 +178,28 @@ static uint8_t rdma_first_active_port(struct ibv_context * ctx, struct ibv_port_ return 0; } +// librdma.dylib is weak-linked, so its symbols are null when it is absent. Nothing may +// call one before this has returned true. +static bool rdma_library_present() { + static const bool present = [] { + void * handle = dlopen("/usr/lib/librdma.dylib", RTLD_LAZY); + if (handle == nullptr) { + return false; + } + dlclose(handle); + return true; + }(); + return present; +} + // Called before the endpoints are exchanged: pick the local device facing this // peer, create a UC QP and register the frame rings. RDMA is point-to-point, so // the device is the one whose GID equals the bootstrap connection's local // address, i.e. the one cabled to the peer. std::unique_ptr apple_rdma::probe(int fd, const uint8_t * target_gid, uint8_t * caps) { + if (!rdma_library_present()) { + return nullptr; + } int ndev = 0; ibv_device ** devs = ibv_get_device_list(&ndev); if (!devs) return nullptr; diff --git a/ggml/src/ggml-sycl/element_wise.cpp b/ggml/src/ggml-sycl/element_wise.cpp index 95914873e5a5..2e926abea7c5 100644 --- a/ggml/src/ggml-sycl/element_wise.cpp +++ b/ggml/src/ggml-sycl/element_wise.cpp @@ -1132,6 +1132,102 @@ void ggml_sycl_op_swiglu_oai(ggml_backend_sycl_context & ctx, ggml_tensor * dst) swiglu_oai_sycl(src0_p, src1_p, (float *)dst_d, ggml_nelements(dst), nc, src0_o / sizeof(float), src1_o / sizeof(float), alpha, limit, stream); } +template +static void swiglu_clamp_kernel(const T * gate, + const T * up, + T * dst, + const int64_t k, + const int64_t n, + const int64_t o0, + const int64_t o1, + float limit, + sycl::nd_item<3> item_ct1) { + const int64_t i = int64_t(item_ct1.get_local_range(2)) * item_ct1.get_group(2) + item_ct1.get_local_id(2); + + if (i >= k) { + return; + } + + const int64_t j0 = (i / n) * o0 + (i % n); + const int64_t j1 = o0 == o1 ? j0 : (i / n) * o1 + (i % n); + + const float gate_value = sycl::fmin((float) gate[j0], limit); + const float up_value = sycl::fmax(sycl::fmin((float) up[j1], limit), -limit); + dst[i] = (T) (gate_value / (1.0f + sycl::native::exp(-gate_value)) * up_value); +} + +template +static void swiglu_clamp_sycl(const T * gate, + const T * up, + T * dst, + const int64_t k, + const int64_t n, + const int64_t o0, + const int64_t o1, + float limit, + dpct::queue_ptr stream) { + const int64_t num_blocks = (k + SYCL_GLU_BLOCK_SIZE - 1) / SYCL_GLU_BLOCK_SIZE; + stream->parallel_for(sycl::nd_range<3>(sycl::range<3>(1, 1, num_blocks) * sycl::range<3>(1, 1, SYCL_GLU_BLOCK_SIZE), + sycl::range<3>(1, 1, SYCL_GLU_BLOCK_SIZE)), + [=](sycl::nd_item<3> item_ct1) [[sycl::reqd_sub_group_size(WARP_SIZE)]] { + swiglu_clamp_kernel(gate, up, dst, k, n, o0, o1, limit, item_ct1); + }); +} + +static void ggml_sycl_op_swiglu_clamp(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { + const ggml_tensor * src0 = dst->src[0]; + const ggml_tensor * src1 = dst->src[1]; + void * src0_d = src0->data; + void * src1_d = src1 ? src1->data : src0->data; + const int64_t src0_o = src0->nb[1]; + const int64_t src1_o = src1 ? src1->nb[1] : src0->nb[1]; + void * dst_d = dst->data; + const int64_t nc = src1 ? src0->ne[0] : src0->ne[0] / 2; + dpct::queue_ptr stream = ctx.stream(); + + GGML_ASSERT(ggml_is_contiguous_1(src0)); + GGML_ASSERT(src0->nb[0] == ggml_element_size(src0)); + GGML_ASSERT(ggml_is_contiguous(dst)); + GGML_ASSERT(src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16); + GGML_ASSERT(src0->type == dst->type); + GGML_ASSERT(dst->ne[0] == nc); + GGML_ASSERT(ggml_nrows(dst) == ggml_nrows(src0)); + + if (src1) { + GGML_ASSERT(ggml_is_contiguous_1(src1)); + GGML_ASSERT(src1->nb[0] == ggml_element_size(src1)); + GGML_ASSERT(src1->ne[0] == nc); + GGML_ASSERT(src0->type == src1->type); + } + + const int32_t swapped = ggml_get_op_params_i32(dst, 1); + const float limit = ggml_get_op_params_f32(dst, 3); + + if (src0->type == GGML_TYPE_F16) { + sycl::half * src0_p = (sycl::half *) src0_d; + sycl::half * src1_p = (sycl::half *) src1_d; + + if (!src1) { + src0_p += swapped ? nc : 0; + src1_p += swapped ? 0 : nc; + } + + swiglu_clamp_sycl(src0_p, src1_p, (sycl::half *) dst_d, ggml_nelements(dst), nc, src0_o / sizeof(sycl::half), + src1_o / sizeof(sycl::half), limit, stream); + } else { + float * src0_p = (float *) src0_d; + float * src1_p = (float *) src1_d; + + if (!src1) { + src0_p += swapped ? nc : 0; + src1_p += swapped ? 0 : nc; + } + + swiglu_clamp_sycl(src0_p, src1_p, (float *) dst_d, ggml_nelements(dst), nc, src0_o / sizeof(float), + src1_o / sizeof(float), limit, stream); + } +} + static inline void ggml_sycl_op_geglu_erf(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { ggml_sycl_detail::ggml_sycl_op_unary_gated(ctx, dst, [](auto x) { return op_gelu_erf(x); @@ -1295,6 +1391,11 @@ void ggml_sycl_swiglu_oai(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { ggml_sycl_op_swiglu_oai(ctx, dst); } +void ggml_sycl_swiglu_clamp(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { + scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/1); + ggml_sycl_op_swiglu_clamp(ctx, dst); +} + void ggml_sycl_geglu_erf(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/1); ggml_sycl_op_geglu_erf(ctx, dst); diff --git a/ggml/src/ggml-sycl/element_wise.hpp b/ggml/src/ggml-sycl/element_wise.hpp index 67bf422d2f34..d280066efb4d 100644 --- a/ggml/src/ggml-sycl/element_wise.hpp +++ b/ggml/src/ggml-sycl/element_wise.hpp @@ -77,6 +77,7 @@ void ggml_sycl_silu(ggml_backend_sycl_context & ctx, ggml_tensor * dst); void ggml_sycl_gelu_quick(ggml_backend_sycl_context & ctx, ggml_tensor * dst); void ggml_sycl_swiglu_oai(ggml_backend_sycl_context & ctx, ggml_tensor * dst); +void ggml_sycl_swiglu_clamp(ggml_backend_sycl_context & ctx, ggml_tensor * dst); void ggml_sycl_gelu_erf(ggml_backend_sycl_context & ctx, ggml_tensor * dst); diff --git a/ggml/src/ggml-sycl/fattn-common.hpp b/ggml/src/ggml-sycl/fattn-common.hpp index c6cc13cfb005..82813f7a99a7 100644 --- a/ggml/src/ggml-sycl/fattn-common.hpp +++ b/ggml/src/ggml-sycl/fattn-common.hpp @@ -6,6 +6,7 @@ #include "convert.hpp" #include "vecdotq.hpp" #include "fattn-buffers.hpp" +#include "fattn.hpp" #include "ggml.h" @@ -926,6 +927,7 @@ void launch_fattn( ggml_sycl_fattn_alloc K_f16(fbuf.K); ggml_sycl_fattn_alloc V_f16(fbuf.V); + const ggml_sycl_fattn_extra extra = ggml_sycl_fattn_get_extra(dst); ggml_sycl_pool_alloc KV_max(pool); ggml_sycl_pool_alloc dst_tmp(pool); ggml_sycl_pool_alloc dst_tmp_meta(pool); @@ -944,10 +946,11 @@ void launch_fattn( const size_t bs = ggml_blck_size(K->type); const size_t ts = ggml_type_size(K->type); - K_f16.alloc(ggml_nelements(K)); + sycl::half * K_f16_ptr = extra.K_buffer_ptr ? (sycl::half *) extra.K_buffer_ptr + : K_f16.alloc(ggml_nelements(K)); if (ggml_is_contiguously_allocated(K)) { to_fp16_sycl_t to_fp16 = ggml_get_to_fp16_sycl(K->type, dst); - to_fp16(K_data, K_f16.ptr, ggml_nelements(K), main_stream); + to_fp16(K_data, K_f16_ptr, ggml_nelements(K), main_stream); nb11 = nb11 * bs * sizeof(sycl::half) / ts; nb12 = nb12 * bs * sizeof(sycl::half) / ts; @@ -958,13 +961,13 @@ void launch_fattn( const int64_t s01 = nb11 / ts; const int64_t s02 = nb12 / ts; const int64_t s03 = nb13 / ts; - to_fp16(K_data, K_f16.ptr, K->ne[0], K->ne[1], K->ne[2], K->ne[3], s01, s02, s03, main_stream); + to_fp16(K_data, K_f16_ptr, K->ne[0], K->ne[1], K->ne[2], K->ne[3], s01, s02, s03, main_stream); nb11 = K->ne[0] * sizeof(sycl::half); nb12 = K->ne[1] * nb11; nb13 = K->ne[2] * nb12; } - K_data = (char *) K_f16.ptr; + K_data = (char *) K_f16_ptr; } if (need_f16_V && V->type != GGML_TYPE_F16) { @@ -977,11 +980,12 @@ void launch_fattn( const size_t bs = ggml_blck_size(V->type); const size_t ts = ggml_type_size(V->type); - V_f16.alloc(ggml_nelements(V)); + sycl::half * V_f16_ptr = extra.V_buffer_ptr ? (sycl::half *) extra.V_buffer_ptr + : V_f16.alloc(ggml_nelements(V)); if (ggml_is_contiguously_allocated(V)) { to_fp16_sycl_t to_fp16 = ggml_get_to_fp16_sycl(V->type, dst); - to_fp16(V_data, V_f16.ptr, ggml_nelements(V), main_stream); - V_data = (char *) V_f16.ptr; + to_fp16(V_data, V_f16_ptr, ggml_nelements(V), main_stream); + V_data = (char *) V_f16_ptr; nb21 = nb21 * bs * sizeof(sycl::half) / ts; nb22 = nb22 * bs * sizeof(sycl::half) / ts; @@ -992,13 +996,13 @@ void launch_fattn( const int64_t s01 = nb21 / ts; const int64_t s02 = nb22 / ts; const int64_t s03 = nb23 / ts; - to_fp16(V_data, V_f16.ptr, V->ne[0], V->ne[1], V->ne[2], V->ne[3], s01, s02, s03, main_stream); + to_fp16(V_data, V_f16_ptr, V->ne[0], V->ne[1], V->ne[2], V->ne[3], s01, s02, s03, main_stream); nb21 = V->ne[0] * sizeof(sycl::half); nb22 = V->ne[1] * nb21; nb23 = V->ne[2] * nb22; } - V_data = (char *) V_f16.ptr; + V_data = (char *) V_f16_ptr; } } diff --git a/ggml/src/ggml-sycl/fattn-onednn.cpp b/ggml/src/ggml-sycl/fattn-onednn.cpp index a501295192fb..4349363a3d3e 100644 --- a/ggml/src/ggml-sycl/fattn-onednn.cpp +++ b/ggml/src/ggml-sycl/fattn-onednn.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -13,9 +14,21 @@ // set minimum query length to treat as prefill (32) #define GGML_SYCL_FA_ONEDNN_MIN_Q 32 -bool ggml_sycl_flash_attn_ext_onednn_supported(const ggml_tensor * dst) { +bool ggml_sycl_fattn_onednn_binds_kv(const ggml_tensor * K, const ggml_tensor * V) { + if (K->type != GGML_TYPE_F16 || V->type != GGML_TYPE_F16) { + return false; + } + 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; + }; + return bindable(K) && bindable(V); +} + +bool ggml_sycl_flash_attn_ext_onednn_supported(const ggml_tensor * dst, bool use_shape_limit) { #if !GGML_SYCL_DNNL GGML_UNUSED(dst); + GGML_UNUSED(use_shape_limit); return false; #else if (!g_ggml_sycl_fa_onednn) { @@ -43,7 +56,7 @@ bool ggml_sycl_flash_attn_ext_onednn_supported(const ggml_tensor * dst) { if (!k_ok || !v_ok) { return false; } - if (Q->ne[1] < 32 || K->ne[1] < 1024) { + if (use_shape_limit && (Q->ne[1] < 32 || K->ne[1] < 1024)) { return false; } for (const ggml_tensor * t : {K, V}) { @@ -93,7 +106,7 @@ bool ggml_sycl_flash_attn_ext_onednn_supported(const ggml_tensor * dst) { return false; } // Prefill only. - if (Q->ne[1] < GGML_SYCL_FA_ONEDNN_MIN_Q) { + if (use_shape_limit && Q->ne[1] < GGML_SYCL_FA_ONEDNN_MIN_Q) { return false; } return true; @@ -150,7 +163,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 & k_str, const std::array & v_str) try { using ltype = logical_tensor::layout_type; using dt = logical_tensor::data_type; using ldims = logical_tensor::dims; @@ -158,11 +172,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(op::attr::transpose_b, true); // key is [.., seq, d] @@ -184,7 +199,7 @@ static sdpa_partition build_sdpa(const engine & eng, int H, int Hkv, int q, int smax.set_attr(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 +213,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 +225,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]; @@ -230,27 +252,53 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso dnnl::engine eng = ctx.engine_dnnl(stream); dnnl::stream strm = ctx.stream_dnnl(stream); + const ggml_sycl_fattn_extra extra = ggml_sycl_fattn_get_extra(dst); + // Q: always f32 -- copy to dense f16. - ggml_sycl_pool_alloc Qf(ctx.pool(), (size_t) H * q * d); - cont_to_f16_sycl((const char *) Q->data, Qf.get(), d, q, H, mb, Q->nb[1], Q->nb[2], Q->nb[3], stream); + std::optional> Qf_pool; + sycl::half * Qf_ptr = (sycl::half *) extra.Q_buffer_ptr; + if (!Qf_ptr) { + Qf_pool.emplace(ctx.pool(), (size_t) H * q * d); + Qf_ptr = Qf_pool->get(); + } + cont_to_f16_sycl((const char *) Q->data, Qf_ptr, 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 k_str{ Hkv * seq * d, seq * d, seq * d, d, 1 }; + std::array v_str = k_str; std::optional> Kf_pool; std::optional> Vf_pool; + // Helper: hand out reserved space, or fall back to the pool. + auto stage_k = [&](size_t n) { if (extra.K_buffer_ptr) { return (sycl::half *) extra.K_buffer_ptr; } + Kf_pool.emplace(ctx.pool(), n); return Kf_pool->get(); }; + auto stage_v = [&](size_t n) { if (extra.V_buffer_ptr) { return (sycl::half *) extra.V_buffer_ptr; } + Vf_pool.emplace(ctx.pool(), n); return Vf_pool->get(); }; + + 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{ s3, s2, s2, s1, 1 }; + }; - 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((const char *) K->data, Kf_pool->get(), d, seq, Hkv, mb, K->nb[1], K->nb[2], K->nb[3], stream); - cont_to_f16_sycl((const char *) V->data, Vf_pool->get(), d, seq, Hkv, mb, V->nb[1], V->nb[2], V->nb[3], stream); - K_ptr = Kf_pool->get(); - V_ptr = Vf_pool->get(); + if (ggml_sycl_fattn_onednn_binds_kv(K, 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) { + K_ptr = stage_k((size_t) Hkv * seq * d); + V_ptr = stage_v((size_t) Hkv * seq * d); + cont_to_f16_sycl((const char *) K->data, K_ptr, d, seq, Hkv, mb, K->nb[1], K->nb[2], K->nb[3], stream); + cont_to_f16_sycl((const char *) V->data, V_ptr, d, seq, Hkv, mb, V->nb[1], V->nb[2], V->nb[3], stream); } else if (ggml_is_quantized(K->type)) { // Quantized K/V: dequant to dense F16 using pool, same lifetime as F16 path. - Kf_pool.emplace(ctx.pool(), ggml_nelements(K)); - K_ptr = Kf_pool->get(); + K_ptr = stage_k((size_t) ggml_nelements(K)); { const char * K_data = (const char *)K->data; const bool k_non_dense = ((int64_t)K->ne[1] * K->nb[1] != K->nb[2]) && K->ne[2] > 1; @@ -284,8 +332,7 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso // data pointer), their logical values differ because the quantized // elements at different positions/offsets represent different K/V // data. Master's F16 path also never aliases K and V. - Vf_pool.emplace(ctx.pool(), ggml_nelements(V)); - V_ptr = Vf_pool->get(); + V_ptr = stage_v((size_t) ggml_nelements(V)); { const char * V_data = (const char *)V->data; const bool v_non_dense = ((int64_t)V->ne[1] * V->nb[1] != V->nb[2]) && V->ne[2] > 1; @@ -316,12 +363,10 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso } } else { // F32: strided copy to dense F16 via cont_to_f16_sycl. - Kf_pool.emplace(ctx.pool(), ggml_nelements(K)); - K_ptr = Kf_pool->get(); + K_ptr = stage_k((size_t) ggml_nelements(K)); cont_to_f16_sycl((const char *) K->data, K_ptr, K->ne[0], K->ne[1], K->ne[2], K->ne[3], K->nb[1], K->nb[2], K->nb[3], stream); - Vf_pool.emplace(ctx.pool(), ggml_nelements(V)); - V_ptr = Vf_pool->get(); + V_ptr = stage_v((size_t) ggml_nelements(V)); cont_to_f16_sycl((const char *) V->data, V_ptr, V->ne[0], V->ne[1], V->ne[2], V->ne[3], V->nb[1], V->nb[2], V->nb[3], stream); } @@ -335,28 +380,43 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso // instead -- the value is captured into the command, so no host memory has to outlive the // call, and the enqueue stays async. const sycl::half scale_h = (sycl::half) (1.0f / kq_scale); - ggml_sycl_pool_alloc scbuf(ctx.pool(), 1); - sycl::half * const scale_dev = scbuf.get(); + std::optional> scbuf; + sycl::half * scale_dev = (sycl::half *) extra.scale_buffer_ptr; + if (!scale_dev) { + scbuf.emplace(ctx.pool(), 1); + scale_dev = scbuf->get(); + } stream->single_task([=]() { *scale_dev = scale_h; }); - ggml_sycl_pool_alloc outf(ctx.pool(), (size_t) H * q * d); // f16 contiguous SDPA out [mb,H,q,d] + // f16 contiguous SDPA out [mb,H,q,d] + std::optional> outf_pool; + sycl::half * outf_ptr = (sycl::half *) extra.out_buffer_ptr; + if (!outf_ptr) { + outf_pool.emplace(ctx.pool(), (size_t) H * q * d); + outf_ptr = outf_pool->get(); + } - // 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 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(); + if (r == E.id_q) return Qf_ptr; if (r == E.id_k) return K_ptr; if (r == E.id_v) return V_ptr; if (r == E.id_scale) return scale_dev; @@ -368,10 +428,10 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso for (auto & lt : E.ins) { ti.emplace_back(lt, eng, id2ptr(lt.get_id())); } - tensor to(E.out, eng, outf.get()); + tensor to(E.out, eng, outf_ptr); E.cp.execute(strm, ti, {to}); - permute_sdpa_out_sycl(outf.get(), (float *) dst->data, mb, H, q, d, stream); + permute_sdpa_out_sycl(outf_ptr, (float *) dst->data, mb, H, q, d, stream); // Single device needs no sync: the dnnl stream wraps this same in-order queue, so the SDPA // serializes with the staging kernels before it and the permute/pool reuse after it. The // garbage output formerly blamed on the missing sync here was the scale use-after-return diff --git a/ggml/src/ggml-sycl/fattn-onednn.hpp b/ggml/src/ggml-sycl/fattn-onednn.hpp index d3019e876889..9669d1bd27a6 100644 --- a/ggml/src/ggml-sycl/fattn-onednn.hpp +++ b/ggml/src/ggml-sycl/fattn-onednn.hpp @@ -5,7 +5,11 @@ // Static-only check: fused-XMX oneDNN Graph SDPA path==flash-attn op // (f16 KV, no softcap/ALiBi, single stream, tuned head_dim, prefill-sized q.) -bool ggml_sycl_flash_attn_ext_onednn_supported(const ggml_tensor * dst); +bool ggml_sycl_flash_attn_ext_onednn_supported(const ggml_tensor * dst, bool use_shape_limit = true); + +// True when the oneDNN path binds an F16 KV cache in place instead of staging a dense copy of +// it. Depends only on the types and strides of K and V, so the answer holds for every call. +bool ggml_sycl_fattn_onednn_binds_kv(const ggml_tensor * K, const ggml_tensor * V); // Run flash attention through oneDNN's fused xmx SDPA // execute the cached SDPA partition, write the f32 dst. Falls back to the TILE kernel on any failure. diff --git a/ggml/src/ggml-sycl/fattn.cpp b/ggml/src/ggml-sycl/fattn.cpp index a85eb721f6af..b73e6d46ffa0 100644 --- a/ggml/src/ggml-sycl/fattn.cpp +++ b/ggml/src/ggml-sycl/fattn.cpp @@ -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; } } @@ -374,3 +378,76 @@ void ggml_sycl_flash_attn_ext(ggml_backend_sycl_context & ctx, ggml_tensor * dst bool ggml_sycl_flash_attn_ext_supported(int device, const ggml_tensor * dst) { return ggml_sycl_get_best_fattn_kernel(device, dst) != BEST_FATTN_KERNEL_NONE; } + +static uintptr_t ggml_sycl_fattn_reserve_halves(ggml_sycl_fattn_extra & extra, size_t n_halves) { + if (n_halves == 0) { + return 0; + } + extra.end = GGML_PAD(extra.end, SYCL_BUFFER_ALIGNMENT); + const uintptr_t block = extra.end; + extra.end += n_halves * sizeof(sycl::half); + return block; +} + +ggml_sycl_fattn_extra ggml_sycl_fattn_get_extra(const ggml_tensor * dst) { + ggml_sycl_fattn_extra extra; + + extra.end = (uintptr_t) dst->data + ggml_nbytes(dst); + + if (dst->op != GGML_OP_FLASH_ATTN_EXT) { + return extra; + } + + const ggml_tensor * Q = dst->src[0]; + const ggml_tensor * K = dst->src[1]; + const ggml_tensor * V = dst->src[2]; + if (!Q || !K || !V) { + return extra; + } + + const int64_t d = K->ne[0]; + const int64_t H = Q->ne[2]; + const int64_t q = Q->ne[1]; + + // calculate the worst-case memory consumption across all kernels + const bool onednn_supported = ggml_sycl_flash_attn_ext_onednn_supported(dst, /* use_shape_limit */ false); + + const bool tile_needs_K = K->type != GGML_TYPE_F16; + const bool tile_needs_V = V->type != GGML_TYPE_F16; + + const bool V_is_K_view = V->view_src && + (V->view_src == K || (V->view_src == K->view_src && V->view_offs == K->view_offs)); + + size_t need_K = 0, need_V = 0, need_Q = 0, need_out = 0, need_scale = 0; + if (onednn_supported) { + need_Q = (size_t) H * q * d; + need_out = (size_t) H * q * d; + need_scale = 1; + // an f16 cache is bound in place, so it needs no staging copy + if (!ggml_sycl_fattn_onednn_binds_kv(K, V)) { + need_K = (size_t) ggml_nelements(K); + need_V = (size_t) ggml_nelements(V); + } + } + if (tile_needs_K) { + need_K = std::max(need_K, (size_t) ggml_nelements(K)); + } + if (tile_needs_V) { + need_V = std::max(need_V, (size_t) ggml_nelements(V)); + } + + extra.Q_buffer_ptr = ggml_sycl_fattn_reserve_halves(extra, need_Q); + extra.K_buffer_ptr = ggml_sycl_fattn_reserve_halves(extra, need_K); + extra.V_buffer_ptr = (V_is_K_view && !onednn_supported && need_V) + ? extra.K_buffer_ptr + : ggml_sycl_fattn_reserve_halves(extra, need_V); + extra.scale_buffer_ptr = ggml_sycl_fattn_reserve_halves(extra, need_scale); + extra.out_buffer_ptr = ggml_sycl_fattn_reserve_halves(extra, need_out); + + return extra; +} + +size_t ggml_sycl_flash_attn_ext_get_alloc_size(const ggml_tensor * dst) { + const ggml_sycl_fattn_extra extra = ggml_sycl_fattn_get_extra(dst); + return (size_t) (extra.end - (uintptr_t) dst->data); +} diff --git a/ggml/src/ggml-sycl/fattn.hpp b/ggml/src/ggml-sycl/fattn.hpp index c093970a3fed..f803aa2a804a 100644 --- a/ggml/src/ggml-sycl/fattn.hpp +++ b/ggml/src/ggml-sycl/fattn.hpp @@ -19,6 +19,24 @@ void ggml_sycl_flash_attn_ext(ggml_backend_sycl_context & ctx, ggml_tensor * dst bool ggml_sycl_flash_attn_ext_supported(int device, const ggml_tensor * dst); +// Scratch that flash attention needs beyond the output tensor +struct ggml_sycl_fattn_extra { + uintptr_t K_buffer_ptr = 0; // F16 copy of the K cache + uintptr_t V_buffer_ptr = 0; // F16 copy of the V cache + uintptr_t Q_buffer_ptr = 0; // dense F16 copy of Q, oneDNN only + uintptr_t scale_buffer_ptr = 0; // the softmax scale as an F16 scalar, oneDNN only + uintptr_t out_buffer_ptr = 0; // F16 SDPA output before conversion to F32, oneDNN only + uintptr_t end = 0; // one past the last reserved byte; sizes the allocation +}; + +// ggml_sycl_fattn_get_extra() is the single source of truth for the layout: it both sizes +// the reservation and hands out the pointers, so the two cannot disagree. +// Each field is the address of one reserved block, or 0 if that block was not reserved, +// in which case the caller allocates from the scratch pool instead. +ggml_sycl_fattn_extra ggml_sycl_fattn_get_extra(const ggml_tensor * dst); + +size_t ggml_sycl_flash_attn_ext_get_alloc_size(const ggml_tensor * dst); + void ggml_sycl_flash_attn_ext_mkl(ggml_backend_sycl_context & ctx, ggml_tensor * dst); #endif // GGML_SYCL_FATTN_HPP diff --git a/ggml/src/ggml-sycl/ggml-sycl.cpp b/ggml/src/ggml-sycl/ggml-sycl.cpp index 0573643d834e..2b2d26cf235b 100644 --- a/ggml/src/ggml-sycl/ggml-sycl.cpp +++ b/ggml/src/ggml-sycl/ggml-sycl.cpp @@ -955,7 +955,10 @@ static size_t ggml_backend_sycl_buffer_type_get_max_size(ggml_backend_buffer_typ } static size_t ggml_backend_sycl_buffer_type_get_alloc_size(ggml_backend_buffer_type_t buft, const ggml_tensor * tensor) { - size_t size = ggml_nbytes(tensor); + // Reserve the additional scratch so it's visible to the graph allocator + size_t size = tensor->op == GGML_OP_FLASH_ATTN_EXT + ? ggml_sycl_flash_attn_ext_get_alloc_size(tensor) + : ggml_nbytes(tensor); int64_t ne0 = tensor->ne[0]; if (ggml_is_quantized(tensor->type)) { @@ -2399,7 +2402,138 @@ static void argsort_f32_i32_sycl(const float *x, int *dst, const int ncols, } } +// Scan and block merge, shared by every launch shape below so a partitioned row uses the +// same insertion order as an unpartitioned one. +// +// src_map != nullptr: report src_map[col] instead of col, so a merge pass can carry the +// original column index through. +// out_vals != nullptr: also emit the k winning values, for a later merge pass. +// swap01: emit in the output order the single-pass path uses. +static void top_k_scan_merge_f32( + const float * src_vals, + const int32_t * src_map, + const int begin, + const int end, + const int k, + const int block_size, + float * shared_vals, + int * shared_idx, + float * out_vals, + int32_t * out_idx, + const bool swap01, + const sycl::nd_item<1> & item_ct1 +) { + const int tid = item_ct1.get_local_id(0); + + // The running top-k lives in SLM (shared local memory) rather than a private array: + // an array indexed by a runtime position cannot be register-allocated, so a private + // one lands in scratch, i.e. device memory, and insertion is this kernel's dominant + // cost. + // + // Lane-strided (lv[i * block_size]) rather than lane-blocked (lv[i]) so a given i is + // contiguous across lanes; a k-strided layout would put every lane of a shift step in + // the same SLM bank. + float * lv = shared_vals + tid; + int * li = shared_idx + tid; + + for (int i = 0; i < k; i++) { + lv[i * block_size] = -FLT_MAX; + li[i * block_size] = -1; + } + + // The k-th best, cached in a register. The reject test is taken for the large + // majority of elements scanned, and in that case touches no memory. + float kth = -FLT_MAX; + + for (int col = begin + tid; col < end; col += block_size) { + float val = src_vals[col]; + + if (val > kth) { + int pos = k - 1; + while (pos > 0 && val > lv[(pos - 1) * block_size]) { + pos--; + } + + for (int i = k - 1; i > pos; i--) { + lv[i * block_size] = lv[(i - 1) * block_size]; + li[i * block_size] = li[(i - 1) * block_size]; + } + lv[pos * block_size] = val; + li[pos * block_size] = src_map ? src_map[col] : col; + + kth = lv[(k - 1) * block_size]; + } + } + + item_ct1.barrier(sycl::access::fence_space::local_space); + + if (tid != 0) { + return; + } + + // Same treatment for the merge accumulator, past the per-lane region. + float * fv = shared_vals + (size_t) k * block_size; + int * fi = shared_idx + (size_t) k * block_size; + + for (int i = 0; i < k; i++) { + fv[i] = -FLT_MAX; + fi[i] = -1; + } + + float fkth = -FLT_MAX; + + // Candidates are visited in the same (t, i) order as before, so tie-breaking is + // unchanged. + for (int t = 0; t < block_size; t++) { + for (int i = 0; i < k; i++) { + float val = shared_vals[i * block_size + t]; + + if (val <= fkth) { + // Lane t's list is sorted descending, so once one of its entries loses + // to the k-th best, every later entry loses too. fkth only rises, so + // that stays true for the rest of the merge. This turns the merge from + // block_size*k steps into roughly block_size plus the candidates + // accepted. + break; + } + + int idx = shared_idx[i * block_size + t]; + + int pos = k - 1; + while (pos > 0 && val > fv[pos - 1]) { + pos--; + } + + for (int j = k - 1; j > pos; j--) { + fv[j] = fv[j - 1]; + fi[j] = fi[j - 1]; + } + fv[pos] = val; + fi[pos] = idx; + + fkth = fv[k - 1]; + } + } + + if (out_vals) { + for (int i = 0; i < k; i++) { + out_vals[i] = fv[i]; + } + } + + for (int i = 0; i < k; i++) { + out_idx[i] = fi[i]; + } + + if (swap01 && k > 1) { + int32_t temp = out_idx[0]; + out_idx[0] = out_idx[1]; + out_idx[1] = temp; + } +} + static void top_k_f32_sycl( + ggml_backend_sycl_context & ctx, const float * src, int32_t * dst_indices, const int64_t ncols, @@ -2407,98 +2541,107 @@ static void top_k_f32_sycl( const int k, dpct::queue_ptr main_stream ) { - const int block_size = 128; + // A row is scanned by exactly one work-group, so a vocabulary-sized row leaves the + // rest of the device idle. What the scan is short of is memory requests in flight, + // not bandwidth or per-request latency, so lanes in flight is the lever: split the + // row across independent work-groups, have each emit its partition's top-k, and + // merge those nsplit*k candidates in a second launch. + // + // split_block trades parallelism against SLM residency. Its cost is + // (split_block + 1) * k * 8 bytes of SLM per group, so at the k <= 32 ceiling 128 + // lanes need about 33 KB, which leaves a single resident group per Xe-core. Revisit + // if the supported k ever grows. + constexpr int split_block = 128; + constexpr int max_splits = 128; + constexpr int min_cols = 8192; - const sycl::range<1> block_dims(block_size); - const sycl::range<1> grid_dims(nrows); + int nsplit = 1; + if (ncols >= min_cols) { + // A partition is then always >= split_block = 128 columns, hence always more than + // the k <= 32 ceiling, so no pass is ever padded with -FLT_MAX sentinels. + const int64_t want = ncols / split_block; + nsplit = (int) (want > max_splits ? max_splits : want); + } - main_stream->submit([&](sycl::handler &cgh) { - sycl::local_accessor shared_vals(sycl::range<1>(block_size * k), cgh); - sycl::local_accessor shared_idx(sycl::range<1>(block_size * k), cgh); + if (nsplit > 1) { + const int nchunk = (int) ((ncols + nsplit - 1) / nsplit); + const size_t ncand = (size_t) nrows * nsplit * k; - cgh.parallel_for( - sycl::nd_range<1>(grid_dims * block_dims, block_dims), - [=](sycl::nd_item<1> item_ct1) { - const int row = item_ct1.get_group(0); - const int tid = item_ct1.get_local_id(0); + ggml_sycl_pool_alloc part_vals(ctx.pool(), ncand); + ggml_sycl_pool_alloc part_idx(ctx.pool(), ncand); - if (row >= nrows) return; + float * pv = part_vals.get(); + int32_t * pi = part_idx.get(); - const float * src_row = src + row * ncols; - int32_t * dst_idx_row = dst_indices + row * k; + const sycl::range<1> block_dims(split_block); - float local_vals[32]; - int local_idx[32]; + main_stream->submit([&](sycl::handler &cgh) { + sycl::local_accessor shared_vals(sycl::range<1>((split_block + 1) * k), cgh); + sycl::local_accessor shared_idx(sycl::range<1>((split_block + 1) * k), cgh); - for (int i = 0; i < k; i++) { - local_vals[i] = -FLT_MAX; - local_idx[i] = -1; - } + cgh.parallel_for( + sycl::nd_range<1>(sycl::range<1>(nrows * nsplit) * block_dims, block_dims), + [=](sycl::nd_item<1> item_ct1) { + const int grp = item_ct1.get_group(0); + const int row = grp / nsplit; + const int part = grp % nsplit; + + const int begin = part * nchunk; + int end = begin + nchunk; + if (end > (int) ncols) { + end = (int) ncols; + } - for (int col = tid; col < ncols; col += block_size) { - float val = src_row[col]; + top_k_scan_merge_f32( + src + (int64_t) row * ncols, nullptr, begin, end, k, split_block, + shared_vals.get_multi_ptr().get(), + shared_idx.get_multi_ptr().get(), + pv + (size_t) grp * k, pi + (size_t) grp * k, false, item_ct1); + }); + }); - if (val > local_vals[k-1]) { - int pos = k - 1; - while (pos > 0 && val > local_vals[pos - 1]) { - pos--; - } + main_stream->submit([&](sycl::handler &cgh) { + sycl::local_accessor shared_vals(sycl::range<1>((split_block + 1) * k), cgh); + sycl::local_accessor shared_idx(sycl::range<1>((split_block + 1) * k), cgh); - for (int i = k - 1; i > pos; i--) { - local_vals[i] = local_vals[i - 1]; - local_idx[i] = local_idx[i - 1]; - } - local_vals[pos] = val; - local_idx[pos] = col; - } - } + cgh.parallel_for( + sycl::nd_range<1>(sycl::range<1>(nrows) * block_dims, block_dims), + [=](sycl::nd_item<1> item_ct1) { + const int row = item_ct1.get_group(0); + const size_t off = (size_t) row * nsplit * k; + + top_k_scan_merge_f32( + pv + off, pi + off, 0, nsplit * k, k, split_block, + shared_vals.get_multi_ptr().get(), + shared_idx.get_multi_ptr().get(), + nullptr, dst_indices + (int64_t) row * k, true, item_ct1); + }); + }); - for (int i = 0; i < k; i++) { - shared_vals[tid * k + i] = local_vals[i]; - shared_idx[tid * k + i] = local_idx[i]; - } - item_ct1.barrier(sycl::access::fence_space::local_space); + return; + } - if (tid == 0) { - float final_vals[32]; - int final_idx[32]; + const int block_size = 128; - for (int i = 0; i < k; i++) { - final_vals[i] = -FLT_MAX; - final_idx[i] = -1; - } + const sycl::range<1> block_dims(block_size); + const sycl::range<1> grid_dims(nrows); - for (int t = 0; t < block_size; t++) { - for (int i = 0; i < k; i++) { - float val = shared_vals[t * k + i]; - int idx = shared_idx[t * k + i]; - - if (val > final_vals[k-1]) { - int pos = k - 1; - while (pos > 0 && val > final_vals[pos - 1]) { - pos--; - } - - for (int j = k - 1; j > pos; j--) { - final_vals[j] = final_vals[j - 1]; - final_idx[j] = final_idx[j - 1]; - } - final_vals[pos] = val; - final_idx[pos] = idx; - } - } - } + main_stream->submit([&](sycl::handler &cgh) { + sycl::local_accessor shared_vals(sycl::range<1>((block_size + 1) * k), cgh); + sycl::local_accessor shared_idx(sycl::range<1>((block_size + 1) * k), cgh); - for (int i = 0; i < k; i++) { - dst_idx_row[i] = final_idx[i]; - } + cgh.parallel_for( + sycl::nd_range<1>(grid_dims * block_dims, block_dims), + [=](sycl::nd_item<1> item_ct1) { + const int row = item_ct1.get_group(0); - if (k > 1) { - int32_t temp = dst_idx_row[0]; - dst_idx_row[0] = dst_idx_row[1]; - dst_idx_row[1] = temp; - } - } + if (row >= nrows) return; + + top_k_scan_merge_f32( + src + (int64_t) row * ncols, nullptr, 0, (int) ncols, k, block_size, + shared_vals.get_multi_ptr().get(), + shared_idx.get_multi_ptr().get(), + nullptr, dst_indices + (int64_t) row * k, true, item_ct1); }); }); } @@ -2899,7 +3042,7 @@ static void ggml_sycl_op_top_k(ggml_backend_sycl_context & ctx, ggml_tensor * ds GGML_ASSERT(k > 0 && k <= 32); GGML_ASSERT(k <= ncols); - top_k_f32_sycl(src0_dd, dst_dd, ncols, nrows, k, main_stream); + top_k_f32_sycl(ctx, src0_dd, dst_dd, ncols, nrows, k, main_stream); } inline void ggml_sycl_op_argmax(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { @@ -5230,6 +5373,9 @@ static bool ggml_sycl_compute_forward(ggml_backend_sycl_context & ctx, struct gg case GGML_GLU_OP_SWIGLU_OAI: ggml_sycl_swiglu_oai(ctx, dst); break; + case GGML_GLU_OP_SWIGLU_CLAMP: + ggml_sycl_swiglu_clamp(ctx, dst); + break; case GGML_GLU_OP_GEGLU_ERF: ggml_sycl_geglu_erf(ctx, dst); break; @@ -5990,6 +6136,7 @@ static bool do_ggml_backend_sycl_device_supports_op(ggml_backend_dev_t dev, cons case GGML_GLU_OP_SWIGLU_OAI: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: return ggml_is_contiguous_1(op->src[0]); default: return false; diff --git a/ggml/src/ggml-virtgpu/ggml-backend.cpp b/ggml/src/ggml-virtgpu/ggml-backend.cpp index 12756c9282f7..996c57e358b6 100644 --- a/ggml/src/ggml-virtgpu/ggml-backend.cpp +++ b/ggml/src/ggml-virtgpu/ggml-backend.cpp @@ -17,7 +17,8 @@ static ggml_status ggml_backend_remoting_graph_compute(ggml_backend_t backend, g return apir_backend_graph_compute(gpu, cgraph); } -static void ggml_backend_remoting_graph_optimize(ggml_backend_t backend, ggml_cgraph * cgraph) { +static void ggml_backend_remoting_graph_optimize(ggml_backend_t backend, ggml_cgraph * cgraph, ggml_backend_graph_optimize_params * params) { + UNUSED(params); virtgpu * gpu = DEV_TO_GPU(backend->device); #if true UNUSED(gpu); diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 565d199f5c49..7e84989847c9 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1035,6 +1035,7 @@ struct vk_device_struct { vk_pipeline pipeline_reglu[2]; vk_pipeline pipeline_swiglu[2]; vk_pipeline pipeline_swiglu_oai[2]; + vk_pipeline pipeline_swiglu_clamp[2]; vk_pipeline pipeline_geglu_erf[2]; vk_pipeline pipeline_geglu_quick[2]; @@ -1347,7 +1348,8 @@ struct vk_mat_mat_id_push_constants { uint32_t stride_a; uint32_t stride_b; uint32_t stride_d; uint32_t batch_stride_a; uint32_t batch_stride_b; uint32_t batch_stride_d; uint32_t nei0; uint32_t nei1; uint32_t nbi1; uint32_t ne11; - uint32_t padded_N; + uint32_t n_experts; + uint32_t hoist_row_ids; }; struct vk_mat_vec_id_push_constants { uint32_t ncols; @@ -1428,6 +1430,10 @@ struct vk_op_count_experts_push_constants { uint32_t nb00; uint32_t nb01; uint32_t a_offset; + uint32_t n_experts; + uint32_t hoist_row_ids; + uint32_t ne00mp; + uint32_t ne00L; }; struct vk_op_glu_push_constants { @@ -1606,6 +1612,10 @@ template <> void init_pushconst_fastdiv(vk_op_glu_push_constants &p) { init_fastdiv_values(p.ne20, p.ne2_0mp, p.ne2_0L); } +template <> void init_pushconst_fastdiv(vk_op_count_experts_push_constants &p) { + init_fastdiv_values(p.ne00, p.ne00mp, p.ne00L); +} + struct vk_op_binary_push_constants { uint32_t ne; uint32_t ne00; uint32_t ne01; uint32_t ne02; uint32_t ne03; uint32_t nb00; uint32_t nb01; uint32_t nb02; uint32_t nb03; @@ -2393,9 +2403,8 @@ struct ggml_backend_vk_context { // Cache most recent tensor that was converted into prealloc_y, and what pipeline it used to convert. vk_pipeline_struct * prealloc_y_last_pipeline_used {}; const ggml_tensor * prealloc_y_last_tensor_used {}; - // True when prealloc_y holds the padded fp16 layout used by the coopmat2 B decode-vector callback. - // If false, then it's contiguous. - bool prealloc_y_last_decode_vector_staging {}; + // True when the K dimension in prealloc_y is padded. + bool prealloc_y_last_k_padded {}; // Track which nodes have been used since the last sync, and whether they were written to std::vector unsynced_nodes_written; @@ -5740,6 +5749,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_GLU(reglu) CREATE_GLU(swiglu) CREATE_GLU(swiglu_oai) + CREATE_GLU(swiglu_clamp) CREATE_GLU(geglu_erf) CREATE_GLU(geglu_quick) #undef CREATE_GLU @@ -5839,7 +5849,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_count_equal_i32, "count_equal_i32", count_equal_i32_len, count_equal_i32_data, "main", 3, sizeof(vk_op_push_constants), {512, 1, 1}, { device->subgroup_size }, 1); - ggml_vk_create_pipeline(device, device->pipeline_count_experts, "count_experts", count_experts_len, count_experts_data, "main", 2, sizeof(vk_op_count_experts_push_constants), {1, 1, 1}, {}, 1, true); + if (device->subgroup_arithmetic && device->subgroup_require_full_support) { + ggml_vk_create_pipeline(device, device->pipeline_count_experts, "count_experts", count_experts_subgroup_len, count_experts_subgroup_data, "main", 2, sizeof(vk_op_count_experts_push_constants), {1, 1, 1}, {}, 1, true, true); + } else { + ggml_vk_create_pipeline(device, device->pipeline_count_experts, "count_experts", count_experts_len, count_experts_data, "main", 2, sizeof(vk_op_count_experts_push_constants), {1, 1, 1}, {}, 1, true); + } for (auto &s : device->pipeline_solve_tri_f32) { const vk_solve_tri_pipeline_state &state = s.first; @@ -8970,13 +8984,13 @@ static void ggml_vk_matmul_id( uint32_t m, uint32_t n, uint32_t k, uint32_t stride_a, uint32_t stride_b, uint32_t stride_d, uint32_t batch_stride_a, uint32_t batch_stride_b, uint32_t batch_stride_d, uint32_t n_as, uint32_t nei0, uint32_t nei1, uint32_t nbi1, uint32_t ne11, - uint32_t padded_n) { + bool hoist_row_ids) { VK_LOG_DEBUG("ggml_vk_matmul_id(a: (" << a.buffer->buffer << ", " << a.offset << ", " << a.size << "), b: (" << b.buffer->buffer << ", " << b.offset << ", " << b.size << "), d: (" << d.buffer->buffer << ", " << d.offset << ", " << d.size << "), ids: (" << ids.buffer->buffer << ", " << ids.offset << ", " << ids.size << "), expert_count: (" << expert_count_buf.buffer->buffer << ", " << expert_count_buf.offset << ", " << expert_count_buf.size << "), " << "m: " << m << ", n: " << n << ", k: " << k << ", stride_a: " << stride_a << ", stride_b: " << stride_b << ", stride_d: " << stride_d << ", " << "batch_stride_a: " << batch_stride_a << ", batch_stride_b: " << batch_stride_b << ", batch_stride_d: " << batch_stride_d << ", " << "n_as: " << n_as << ", nei0: " << nei0 << ", nei1: " << nei1 << ", nbi1: " << nbi1 << ", ne11: " << ne11 << ")"); const vk_mat_mat_id_push_constants pc = { m, n, k, stride_a, stride_b, stride_d, batch_stride_a, batch_stride_b, batch_stride_d, - nei0, nei1, nbi1, ne11, padded_n }; + nei0, nei1, nbi1, ne11, n_as, uint32_t(hoist_row_ids) }; ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { a, b, d, ids, expert_count_buf }, pc, { m, nei1, n_as }); } @@ -9441,27 +9455,27 @@ static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& sub if (y_non_contig) { if (ctx->prealloc_y_last_pipeline_used != to_fp16_vk_1.get() || ctx->prealloc_y_last_tensor_used != src1 || - ctx->prealloc_y_last_decode_vector_staging) { + ctx->prealloc_y_last_k_padded) { if (ctx->prealloc_y_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } ggml_vk_cpy_to_contiguous(ctx, subctx, to_fp16_vk_1, src1, ggml_vk_subbuffer(ctx, d_Qy, qy_buf_offset), ggml_vk_subbuffer(ctx, d_Y, 0)); ctx->prealloc_y_last_pipeline_used = to_fp16_vk_1.get(); ctx->prealloc_y_last_tensor_used = src1; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; } } if (quantize_y) { if (ctx->prealloc_y_last_pipeline_used != to_q8_1.get() || ctx->prealloc_y_last_tensor_used != src1 || - ctx->prealloc_y_last_decode_vector_staging) { + ctx->prealloc_y_last_k_padded) { if (ctx->prealloc_y_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } ggml_vk_quantize_q8_1(ctx, subctx, ggml_vk_subbuffer(ctx, d_Qy, qy_buf_offset), ggml_vk_subbuffer(ctx, d_Y, 0), y_ne); ctx->prealloc_y_last_pipeline_used = to_q8_1.get(); ctx->prealloc_y_last_tensor_used = src1; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; } } @@ -9732,27 +9746,27 @@ static void ggml_vk_mul_mat_vec_q_f16_cols(ggml_backend_vk_context * ctx, vk_con GGML_ASSERT(y_sz == ggml_type_size(src1->type) * y_ne); if (ctx->prealloc_y_last_pipeline_used != to_fp16_vk_1.get() || ctx->prealloc_y_last_tensor_used != src1 || - ctx->prealloc_y_last_decode_vector_staging) { + ctx->prealloc_y_last_k_padded) { if (ctx->prealloc_y_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } ggml_vk_cpy_to_contiguous(ctx, subctx, to_fp16_vk_1, src1, d_Qy, d_Y); ctx->prealloc_y_last_pipeline_used = to_fp16_vk_1.get(); ctx->prealloc_y_last_tensor_used = src1; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; } } if (quantize_y) { if (ctx->prealloc_y_last_pipeline_used != to_q8_1.get() || ctx->prealloc_y_last_tensor_used != src1 || - ctx->prealloc_y_last_decode_vector_staging) { + ctx->prealloc_y_last_k_padded) { if (ctx->prealloc_y_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } ggml_vk_quantize_q8_1(ctx, subctx, d_Qy, d_Y, y_ne); ctx->prealloc_y_last_pipeline_used = to_q8_1.get(); ctx->prealloc_y_last_tensor_used = src1; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; } } @@ -10235,6 +10249,12 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& // const uint64_t ne23 = dst->ne[3]; const uint64_t n_as = ne02; + // n_as counts, n_as offsets, one total, then one packed row id per (expert, token). + // Hoisting requires 16-bit indices for the packing and a table that fits one binding. + const uint64_t hoisted_row_id_words = 2 * n_as + 1 + nei0 * nei1; + const bool hoist_row_ids = n_as <= 256 && nei0 <= 0xffff && nei1 <= 0xffff && + hoisted_row_id_words * sizeof(uint32_t) <= + ctx->device->properties.limits.maxStorageBufferRange; ggml_backend_vk_buffer_context * dst_buf_ctx = (ggml_backend_vk_buffer_context *)dst->buffer->context; ggml_backend_vk_buffer_context * src0_buf_ctx = (ggml_backend_vk_buffer_context *)src0->buffer->context; @@ -10287,8 +10307,6 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& (src0->type == GGML_TYPE_BF16 && src1->type != GGML_TYPE_BF16) || !ggml_vk_dim01_contiguous(src1); - const uint32_t y_staged_row_stride = y_decode_vector_staging ? (uint32_t)ggml_vk_align_size(ne10, 4) : (uint32_t)ne10; - const bool y_f32_kernel = src1->type == GGML_TYPE_F32 && !y_non_contig; bool quantize_y = ctx->device->integer_dot_product && src1->type == GGML_TYPE_F32 && ggml_is_contiguous(src1) && !y_non_contig && (ne11 * ne10) % 4 == 0; @@ -10303,19 +10321,25 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& } const bool qx_needs_dequant = mmp == nullptr || x_non_contig; - const bool qy_needs_dequant = !quantize_y && ((src1->type != f16_type && !y_f32_kernel) || y_non_contig); + bool qy_needs_dequant = !quantize_y && ((src1->type != f16_type && !y_f32_kernel) || y_non_contig); if (qx_needs_dequant) { // Fall back to dequant + f16 mulmat mmp = ggml_vk_get_mul_mat_mat_id_pipeline(ctx, f16_type, y_f32_kernel ? GGML_TYPE_F32 : f16_type, (ggml_prec)dst->op_params[0]); } - // Not implemented - GGML_ASSERT(y_non_contig || !qy_needs_dequant); // NOLINT - const ggml_type effective_src1_type = quantize_y ? GGML_TYPE_Q8_1 : (y_f32_kernel ? GGML_TYPE_F32 : src1->type); const uint32_t kpad = quantize_y ? 0 : ggml_vk_align_size(ne10, ggml_vk_guess_matmul_id_pipeline_align(ctx, mmp, ne01, nei1, qx_needs_dequant ? f16_type : src0->type, effective_src1_type)); + // Coopmat2 MUL_MAT_ID BK specialization constants in ggml_vk_load_shaders are at most 64. + const uint32_t y_staged_row_stride = ctx->device->coopmat2 && !quantize_y ? ggml_vk_align_size(ne10, 64) : ne10; + const bool y_needs_k_padding = ne10 != y_staged_row_stride; + const bool y_needs_reformat = y_non_contig || y_needs_k_padding; + qy_needs_dequant = qy_needs_dequant || y_needs_k_padding; + + // Not implemented + GGML_ASSERT(y_needs_reformat || !qy_needs_dequant); // NOLINT + const bool aligned = !quantize_y && ne10 == kpad && ne01 > 8 && nei1 > 8; vk_pipeline pipeline = ggml_vk_guess_matmul_id_pipeline(ctx, mmp, ne01, nei1, aligned, qx_needs_dequant ? f16_type : src0->type, effective_src1_type); @@ -10323,10 +10347,8 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& if (ggml_nbytes(src0) > ctx->device->properties.limits.maxStorageBufferRange) { pipeline = ggml_vk_get_64b_indexing_pipeline(ctx, pipeline); } - // Reserve extra storage in the N dimension for the Y matrix, so we can avoid bounds-checking - uint32_t padded_n = qy_needs_dequant ? ROUNDUP_POW2(ne11, pipeline->wg_denoms[1]) :ne11; const uint64_t x_ne = ggml_nelements(src0); - const uint64_t y_ne = (uint64_t)y_staged_row_stride * padded_n * ne12 * ne13; + const uint64_t y_ne = (uint64_t)y_staged_row_stride * ne11 * ne12 * ne13; const uint64_t d_ne = ggml_nelements(dst); const uint64_t qx_sz = ggml_type_size(src0->type) * x_ne / ggml_blck_size(src0->type); @@ -10345,7 +10367,7 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& y_staged_dst.type = f16_type; y_staged_dst.nb[0] = ggml_type_size(f16_type); y_staged_dst.nb[1] = y_staged_dst.nb[0] * y_staged_row_stride; - y_staged_dst.nb[2] = y_staged_dst.nb[1] * padded_n; + y_staged_dst.nb[2] = y_staged_dst.nb[1] * ne11; y_staged_dst.nb[3] = y_staged_dst.nb[2] * y_staged_dst.ne[2]; return y_staged_dst; }; @@ -10355,10 +10377,10 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& } else { to_fp16_vk_0 = ggml_vk_get_to_fp16(ctx, src0->type); } - if (y_non_contig) { + if (y_needs_reformat) { ggml_tensor y_staged_dst; const ggml_tensor * y_staged_dst_ptr = nullptr; - if (y_decode_vector_staging) { + if (y_needs_k_padding) { y_staged_dst = make_y_staged_dst(); y_staged_dst_ptr = &y_staged_dst; } @@ -10375,7 +10397,8 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& } vk_pipeline count_experts = ctx->device->pipeline_count_experts; - uint32_t expert_count_size = sizeof(uint32_t) * n_as; + const size_t expert_data_size = sizeof(uint32_t) * + (hoist_row_ids ? hoisted_row_id_words : n_as); { if ( @@ -10391,8 +10414,8 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& ctx->prealloc_size_y = y_sz; ggml_vk_preallocate_buffers(ctx, subctx); } - if (ctx->prealloc_size_split_k < expert_count_size) { - ctx->prealloc_size_split_k = expert_count_size; + if (ctx->prealloc_size_split_k < expert_data_size) { + ctx->prealloc_size_split_k = expert_data_size; ggml_vk_preallocate_buffers(ctx, subctx); } @@ -10458,18 +10481,23 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& } } // Count how many times each expert is used - vk_subbuffer expert_count_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_split_k, 0); + vk_subbuffer expert_count_buf = { ctx->prealloc_split_k, 0, expert_data_size }; if (ctx->prealloc_split_k_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } { - const std::vector pc = { (uint32_t)nei0, + vk_op_count_experts_push_constants pc = { (uint32_t)nei0, (uint32_t)nei1, (uint32_t)(nbi0 / ggml_type_size(ids->type)), (uint32_t)(nbi1 / ggml_type_size(ids->type)), - (uint32_t)(get_misalign_bytes(ctx, ids) / ggml_type_size(ids->type)) }; + (uint32_t)(get_misalign_bytes(ctx, ids) / ggml_type_size(ids->type)), + (uint32_t)n_as, + uint32_t(hoist_row_ids), + 0, 0 }; + init_pushconst_fastdiv(pc); ggml_vk_dispatch_pipeline(ctx, subctx, count_experts, - { vk_subbuffer{ d_ids, ids_buf_offset, ids_sz }, expert_count_buf }, pc, { (uint32_t)n_as, 1, 1}); + { vk_subbuffer{ d_ids, ids_buf_offset, ids_sz }, expert_count_buf }, pc, + { hoist_row_ids ? 1u : (uint32_t)n_as, 1, 1}); } if (x_non_contig) { @@ -10479,14 +10507,18 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& ggml_vk_dispatch_pipeline(ctx, subctx, to_fp16_vk_0, { vk_subbuffer{ d_Qx, qx_buf_offset, qx_sz }, vk_subbuffer{ d_X, 0, x_sz } }, pc, { (uint32_t)x_ne, 1, 1}); } - if (y_non_contig) { + if (y_needs_reformat) { if (ctx->prealloc_y_last_pipeline_used != to_fp16_vk_1.get() || ctx->prealloc_y_last_tensor_used != src1 || - ctx->prealloc_y_last_decode_vector_staging != y_decode_vector_staging) { + ctx->prealloc_y_last_k_padded != y_needs_k_padding) { if (ctx->prealloc_y_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } - if (y_decode_vector_staging) { + if (y_needs_k_padding) { + GGML_ASSERT(y_sz % 4 == 0); + // Zero B padding because clamping only A can produce 0 * Inf or NaN. + subctx->s->buffer->buf.fillBuffer(d_Y->buffer, 0, y_sz, 0); + ggml_vk_sync_buffers(ctx, subctx); const ggml_tensor y_staged_dst = make_y_staged_dst(); const uint32_t y_staged_dst_type_size = ggml_type_size(y_staged_dst.type); ggml_vk_cpy_to_strided( @@ -10501,27 +10533,27 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& } ctx->prealloc_y_last_pipeline_used = to_fp16_vk_1.get(); ctx->prealloc_y_last_tensor_used = src1; - ctx->prealloc_y_last_decode_vector_staging = y_decode_vector_staging; + ctx->prealloc_y_last_k_padded = y_needs_k_padding; } } if (quantize_y) { if (ctx->prealloc_y_last_pipeline_used != to_q8_1.get() || ctx->prealloc_y_last_tensor_used != src1 || - ctx->prealloc_y_last_decode_vector_staging) { + ctx->prealloc_y_last_k_padded) { if (ctx->prealloc_y_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } ggml_vk_quantize_q8_1(ctx, subctx, ggml_vk_subbuffer(ctx, d_Qy, qy_buf_offset), ggml_vk_subbuffer(ctx, d_Y, 0), y_ne); ctx->prealloc_y_last_pipeline_used = to_q8_1.get(); ctx->prealloc_y_last_tensor_used = src1; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; } } ggml_vk_sync_buffers(ctx, subctx); uint32_t stride_batch_x = ne00*ne01; - uint32_t stride_b_y = y_decode_vector_staging ? y_staged_row_stride : ne10; - uint32_t stride_batch_y = y_decode_vector_staging ? y_staged_row_stride * padded_n : ne10*ne11; + uint32_t stride_b_y = y_needs_k_padding ? y_staged_row_stride : ne10; + uint32_t stride_batch_y = y_needs_k_padding ? y_staged_row_stride * ne11 : ne10*ne11; if (!ggml_vk_dim01_contiguous(src0) && !qx_needs_dequant) { stride_batch_x = src0->nb[0] / ggml_type_size(src0->type); @@ -10538,13 +10570,13 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& { d_D, d_buf_offset, d_sz }, { d_ids, ids_buf_offset, ids_sz }, expert_count_buf, ne01, ne21, ne10, ne10, stride_b_y, ne01, stride_batch_x, stride_batch_y, ne20*ne21, - n_as, nei0, nei1, nbi1 / ggml_type_size(ids->type), ne11, padded_n + n_as, nei0, nei1, nbi1 / ggml_type_size(ids->type), ne11, hoist_row_ids ); // NOLINT if (x_non_contig || qx_needs_dequant) { ctx->prealloc_x_need_sync = true; } - if (y_non_contig || quantize_y) { + if (y_needs_reformat || quantize_y) { ctx->prealloc_y_need_sync = true; } ctx->prealloc_split_k_need_sync = true; @@ -10695,27 +10727,27 @@ static void ggml_vk_mul_mat_vec_id_q_f16(ggml_backend_vk_context * ctx, vk_conte GGML_ASSERT(y_sz == ggml_type_size(src1->type) * y_ne); if (ctx->prealloc_y_last_pipeline_used != to_fp16_vk_1.get() || ctx->prealloc_y_last_tensor_used != src1 || - ctx->prealloc_y_last_decode_vector_staging) { + ctx->prealloc_y_last_k_padded) { if (ctx->prealloc_y_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } ggml_vk_cpy_to_contiguous(ctx, subctx, to_fp16_vk_1, src1, d_Qy, d_Y); ctx->prealloc_y_last_pipeline_used = to_fp16_vk_1.get(); ctx->prealloc_y_last_tensor_used = src1; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; } } if (quantize_y) { if (ctx->prealloc_y_last_pipeline_used != to_q8_1.get() || ctx->prealloc_y_last_tensor_used != src1 || - ctx->prealloc_y_last_decode_vector_staging) { + ctx->prealloc_y_last_k_padded) { if (ctx->prealloc_y_need_sync) { ggml_vk_sync_buffers(ctx, subctx); } ggml_vk_quantize_q8_1(ctx, subctx, d_Qy, d_Y, y_ne); ctx->prealloc_y_last_pipeline_used = to_q8_1.get(); ctx->prealloc_y_last_tensor_used = src1; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; } } @@ -11621,6 +11653,8 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const return ctx->device->pipeline_swiglu[dst->type == GGML_TYPE_F16]; case GGML_GLU_OP_SWIGLU_OAI: return ctx->device->pipeline_swiglu_oai[dst->type == GGML_TYPE_F16]; + case GGML_GLU_OP_SWIGLU_CLAMP: + return ctx->device->pipeline_swiglu_clamp[dst->type == GGML_TYPE_F16]; case GGML_GLU_OP_GEGLU_ERF: return ctx->device->pipeline_geglu_erf[dst->type == GGML_TYPE_F16]; case GGML_GLU_OP_GEGLU_QUICK: @@ -15567,7 +15601,7 @@ static void ggml_vk_preallocate_buffers(ggml_backend_vk_context * ctx, vk_contex ctx->prealloc_y = ggml_vk_create_buffer_device(ctx->device, ctx->prealloc_size_y); ctx->prealloc_y_last_pipeline_used = nullptr; ctx->prealloc_y_last_tensor_used = nullptr; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; } if (ctx->prealloc_split_k == nullptr || (ctx->prealloc_size_split_k > 0 && ctx->prealloc_split_k->size < ctx->prealloc_size_split_k)) { VK_LOG_MEMORY("ggml_vk_preallocate_buffers(split_k_size: " << ctx->prealloc_size_split_k << ")"); @@ -15926,6 +15960,7 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr case GGML_GLU_OP_SWIGLU_OAI: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: ggml_vk_glu(ctx, compute_ctx, src0, src1, node); break; default: @@ -16192,7 +16227,7 @@ static void ggml_vk_graph_cleanup(ggml_backend_vk_context * ctx) { VK_LOG_DEBUG("ggml_vk_graph_cleanup()"); ctx->prealloc_y_last_pipeline_used = {}; ctx->prealloc_y_last_tensor_used = nullptr; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; ctx->unsynced_nodes_written.clear(); ctx->unsynced_nodes_read.clear(); @@ -16244,7 +16279,7 @@ static void ggml_vk_cleanup(ggml_backend_vk_context * ctx) { ctx->prealloc_y_last_pipeline_used = nullptr; ctx->prealloc_y_last_tensor_used = nullptr; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; ctx->prealloc_size_x = 0; ctx->prealloc_size_y = 0; @@ -17442,7 +17477,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg ctx->prealloc_y_last_pipeline_used = nullptr; ctx->prealloc_y_last_tensor_used = nullptr; - ctx->prealloc_y_last_decode_vector_staging = false; + ctx->prealloc_y_last_k_padded = false; if (ctx->prealloc_size_add_rms_partials) { ggml_vk_preallocate_buffers(ctx, nullptr); @@ -17838,8 +17873,9 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg } // Sort the graph for improved parallelism. -static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * graph) +static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * graph, struct ggml_backend_graph_optimize_params * params) { + GGML_UNUSED(params); VK_LOG_DEBUG("ggml_vk_graph_optimize(" << graph->n_nodes << " nodes)"); ggml_backend_vk_context * ctx = (ggml_backend_vk_context *)backend->context; @@ -17847,20 +17883,32 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * return; } - auto const &is_empty = [](ggml_tensor * node) -> bool { + auto const &is_empty = [](const ggml_tensor * node) -> bool { return node->op == GGML_OP_NONE || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE || node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE; }; - auto const &is_src_of = [](const ggml_tensor *dst, const ggml_tensor *src) -> bool { + auto const &is_src_of = [&is_empty](const ggml_tensor *dst, const ggml_tensor *src) -> bool { + auto const &base = [](const ggml_tensor * tensor) { + return tensor->view_src ? tensor->view_src : tensor; + }; for (uint32_t s = 0; s < GGML_MAX_SRC; ++s) { if (dst->src[s] == src) { return true; } + if (is_empty(dst) || is_empty(src)) { + continue; + } + // A source view of dst may read storage written through a different view by src. + if (dst->src[s] && base(dst->src[s]) == base(src)) { + return true; + } + // Moving dst forward may overwrite storage still read through a view by src. + if (src->src[s] && base(dst) == base(src->src[s])) { + return true; + } } // implicit dependency if they view the same tensor - const ggml_tensor *dst2 = dst->view_src ? dst->view_src : dst; - const ggml_tensor *src2 = src->view_src ? src->view_src : src; - if (dst2 == src2) { + if (base(dst) == base(src)) { return true; } return false; @@ -18430,6 +18478,7 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_GLU_OP_SWIGLU_OAI: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: return (op->src[0]->type == GGML_TYPE_F32 || op->src[0]->type == GGML_TYPE_F16) && (op->type == GGML_TYPE_F32 || op->type == GGML_TYPE_F16) && (op->src[0]->type == op->type) && diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/conv2d_mm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/conv2d_mm.comp index 99400098bf2b..c64004cdc48e 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/conv2d_mm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/conv2d_mm.comp @@ -19,6 +19,7 @@ #endif #include "types.glsl" +#include "utils.glsl" // shape notation: [dim(N), ..., dim(0)] -- stride(dim(j)) >= stride(dim(i)) if i > j layout(binding = 0) readonly buffer A { @@ -193,14 +194,6 @@ uint32_t Br = tid / BS_NPQ; uint32_t Bc = tid % BS_NPQ; const uint32_t BrpWg = WG_SIZE / BS_NPQ; -// see init_fastdiv_values in ggml-vulkan.cpp -uint fastdiv(uint n, uint mp, uint L) { - uint msbs, lsbs; - // msbs = mulhi(n, mp) - umulExtended(n, mp, msbs, lsbs); - return (msbs + n) >> L; -} - #ifdef COOPMAT2 #define ACC_TYPE float16_t diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/conv3d_mm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/conv3d_mm.comp index f66f299f6dae..d5ce4290b930 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/conv3d_mm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/conv3d_mm.comp @@ -15,6 +15,7 @@ #endif #include "types.glsl" +#include "utils.glsl" // shape notation: [dim(N), ..., dim(0)] -- stride(dim(j)) >= stride(dim(i)) if i > j layout(binding = 0) readonly buffer A { @@ -178,14 +179,6 @@ uint32_t Br = tid / BS_NPQ; uint32_t Bc = tid % BS_NPQ; const uint32_t BrpWg = WG_SIZE / BS_NPQ; -// see init_fastdiv_values in ggml-vulkan.cpp -uint fastdiv(uint n, uint mp, uint L) { - uint msbs, lsbs; - // msbs = mulhi(n, mp) - umulExtended(n, mp, msbs, lsbs); - return (msbs + n) >> L; -} - void split_crs(uint32_t crs_idx, out uint32_t ic, out uint32_t kd, out uint32_t kh, out uint32_t kw) { const uint32_t KHKW = KH * KW; const uint32_t KDKHKW = KD * KHKW; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/count_experts.comp b/ggml/src/ggml-vulkan/vulkan-shaders/count_experts.comp index ffc8608691f7..ef659959d950 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/count_experts.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/count_experts.comp @@ -2,7 +2,13 @@ #extension GL_EXT_control_flow_attributes : enable +#ifdef USE_SUBGROUPS +#extension GL_KHR_shader_subgroup_basic : enable +#extension GL_KHR_shader_subgroup_arithmetic : enable +#endif + #include "types.glsl" +#include "utils.glsl" layout (push_constant) uniform parameter { @@ -11,6 +17,10 @@ layout (push_constant) uniform parameter uint32_t nb00; uint32_t nb01; uint32_t a_offset; + uint32_t n_experts; + uint32_t hoist_row_ids; + uint32_t ne00mp; + uint32_t ne00L; } p; #define BLOCK_SIZE 256 @@ -21,16 +31,90 @@ layout (binding = 0) readonly buffer A {uint data_a[];}; layout (binding = 1) writeonly buffer D {uint data_d[];}; shared uint vals[BLOCK_SIZE]; +shared uint offsets[BLOCK_SIZE]; +shared uint cursors[BLOCK_SIZE]; +// data_d layout when p.hoist_row_ids is set: +// [0, n_experts) per-expert row count +// [n_experts, 2*n_experts) per-expert start offset into the row id region +// [2*n_experts] total row count +// [2*n_experts + 1, ) row ids grouped by expert, packed as (i01 << 16) | (i00 & 0xffff) +// Otherwise only data_d[expert_id] is written, holding that expert's row count. void main() { const uint expert_id = gl_WorkGroupID.x; const uint num_elements = p.ne00 * p.ne01; const uint tid = gl_LocalInvocationID.x; + if (p.hoist_row_ids != 0) { + if (tid < p.n_experts) { + vals[tid] = 0; + } + barrier(); + + for (uint idx = tid; idx < num_elements; idx += BLOCK_SIZE) { + const uint i01 = fastdiv(idx, p.ne00mp, p.ne00L); + const uint i00 = idx - i01 * p.ne00; + const uint expert = data_a[p.a_offset + i01 * p.nb01 + i00 * p.nb00]; + if (expert < p.n_experts) { + atomicAdd(vals[expert], 1); + } + } + barrier(); + +#ifdef USE_SUBGROUPS + if (gl_SubgroupID == 0) { + // pad the trip count so the subgroup ops stay in uniform control flow + const uint n_experts_padded = (p.n_experts + gl_SubgroupSize - 1) & ~(gl_SubgroupSize - 1); + uint base = 0; + for (uint expert = gl_SubgroupInvocationID; expert < n_experts_padded; expert += gl_SubgroupSize) { + const bool in_range = expert < p.n_experts; + const uint count = in_range ? vals[expert] : 0; + const uint offset = base + subgroupExclusiveAdd(count); + if (in_range) { + data_d[expert] = count; + data_d[p.n_experts + expert] = offset; + offsets[expert] = offset; + cursors[expert] = 0; + } + base += subgroupAdd(count); + } + if (subgroupElect()) { + data_d[2 * p.n_experts] = base; + } + } +#else + if (tid == 0) { + uint offset = 0; + for (uint expert = 0; expert < p.n_experts; ++expert) { + const uint count = vals[expert]; + data_d[expert] = count; + data_d[p.n_experts + expert] = offset; + offsets[expert] = offset; + cursors[expert] = 0; + offset += count; + } + data_d[2 * p.n_experts] = offset; + } +#endif + barrier(); + + for (uint idx = tid; idx < num_elements; idx += BLOCK_SIZE) { + const uint i01 = fastdiv(idx, p.ne00mp, p.ne00L); + const uint i00 = idx - i01 * p.ne00; + const uint expert = data_a[p.a_offset + i01 * p.nb01 + i00 * p.nb00]; + if (expert < p.n_experts) { + const uint row = atomicAdd(cursors[expert], 1); + const uint packed_row_id = (i01 << 16) | (i00 & 0xffffu); + data_d[2 * p.n_experts + 1 + offsets[expert] + row] = packed_row_id; + } + } + return; + } + uint count = 0; for (uint idx = tid; idx < num_elements; idx += BLOCK_SIZE) { - const uint i01 = idx / p.ne00; - const uint i00 = idx % p.ne00; + const uint i01 = fastdiv(idx, p.ne00mp, p.ne00L); + const uint i00 = idx - i01 * p.ne00; const uint a = data_a[p.a_offset + i01 * p.nb01 + i00 * p.nb00]; count += uint(a == expert_id); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/generic_unary_head.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/generic_unary_head.glsl index 9d4176f3f967..e13de9a00f2f 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/generic_unary_head.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/generic_unary_head.glsl @@ -1,6 +1,8 @@ #extension GL_EXT_shader_16bit_storage : require #extension GL_EXT_control_flow_attributes : require +#include "utils.glsl" + layout (push_constant) uniform parameter { uint ne; @@ -32,18 +34,6 @@ uint get_idx() { uint get_aoffset() { return p.misalign_offsets >> 16; } uint get_doffset() { return p.misalign_offsets & 0xFFFF; } -// see init_fastdiv_values in ggml-vulkan.cpp -uint fastdiv(uint n, uint mp, uint L) { - uint msbs, lsbs; - // msbs = mulhi(n, mp) - umulExtended(n, mp, msbs, lsbs); - return (msbs + n) >> L; -} - -uint fastdiv_L(uint packed, uint slot) { - return (packed >> (slot * 8)) & 0x3Fu; -} - uint src0_idx(uint idx) { const uint i03 = fastdiv(idx, p.ne0_012mp, fastdiv_L(p.ne0_Ls, 0)); const uint i03_offset = i03 * p.ne02*p.ne01*p.ne00; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/glu_head.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/glu_head.glsl index c3cae736f977..fc2951ec2e56 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/glu_head.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/glu_head.glsl @@ -1,5 +1,7 @@ #extension GL_EXT_shader_16bit_storage : require +#include "utils.glsl" + layout(local_size_x = 512, local_size_y = 1, local_size_z = 1) in; @@ -39,9 +41,3 @@ uint get_aoffset() { return p.misalign_offsets >> 16; } uint get_boffset() { return (p.misalign_offsets >> 8) & 0xFF; } uint get_doffset() { return p.misalign_offsets & 0xFF; } -// see init_fastdiv_values in ggml-vulkan.cpp -uint fastdiv(uint n, uint mp, uint L) { - uint msbs, lsbs; - umulExtended(n, mp, msbs, lsbs); - return (msbs + n) >> L; -} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm.comp index 3df88044a5ee..63c4aaebcb1a 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm.comp @@ -88,6 +88,8 @@ layout (push_constant) uniform parameter uint nei1; uint nbi1; uint ne11; + uint n_experts; + uint hoist_row_ids; #else uint base_work_group_z; uint num_batches; @@ -214,27 +216,31 @@ void main() { const uint loadstride_b = gl_WorkGroupSize.x * LOAD_VEC_B_EFF * LOAD_VEC_BATCH_B / BK; #ifdef MUL_MAT_ID -#ifdef MUL_MAT_ID_USE_SUBGROUPS - if (bitCount(p.nei0) == 1) { - load_row_ids(expert_idx, true, ic); + if (p.hoist_row_ids != 0) { + load_row_ids_hoisted(expert_idx, ic); } else { - load_row_ids(expert_idx, false, ic); - } +#ifdef MUL_MAT_ID_USE_SUBGROUPS + if (bitCount(p.nei0) == 1) { + load_row_ids(expert_idx, true, ic); + } else { + load_row_ids(expert_idx, false, ic); + } #else - _ne1 = 0; - for (uint ii1 = 0; ii1 < p.nei1 && _ne1 < (ic + 1) * BN; ii1++) { - for (uint ii0 = 0; ii0 < p.nei0 && _ne1 < (ic + 1) * BN; ii0++) { - if (data_ids[ii1*p.nbi1 + ii0] == expert_idx) { - if (_ne1 >= ic * BN) { - row_ids[_ne1 - ic * BN] = u16vec2(ii0, ii1); + _ne1 = 0; + for (uint ii1 = 0; ii1 < p.nei1 && _ne1 < (ic + 1) * BN; ii1++) { + for (uint ii0 = 0; ii0 < p.nei0 && _ne1 < (ic + 1) * BN; ii0++) { + if (data_ids[ii1*p.nbi1 + ii0] == expert_idx) { + if (_ne1 >= ic * BN) { + row_ids[_ne1 - ic * BN] = u16vec2(ii0, ii1); + } + _ne1++; } - _ne1++; } } - } - barrier(); + barrier(); #endif + } // Workgroup has no work if (ic * BN >= _ne1) return; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_cm2.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_cm2.comp index a2e15f6f5ced..27f3178e7f26 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_cm2.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_cm2.comp @@ -56,6 +56,8 @@ layout (push_constant) uniform parameter uint nei1; uint nbi1; uint ne11; + uint n_experts; + uint hoist_row_ids; #else uint base_work_group_z; uint num_batches; @@ -64,9 +66,9 @@ layout (push_constant) uniform parameter uint ne12; uint broadcast2; uint broadcast3; -#endif // N dimension for the B matrix can be >= p.N uint padded_N; +#endif } p; @@ -225,6 +227,23 @@ void load_row_ids(uint expert_idx, bool nei0_is_pow2, uint ic) { } barrier(); } + +void load_row_ids_hoisted(uint expert_idx, uint ic) { + _ne1 = uint(data_expert_count[expert_idx]); + + const uint tile_begin = ic * BN; + const uint tile_count = tile_begin < _ne1 ? min(BN, _ne1 - tile_begin) : 0; + const uint expert_offset = uint(data_expert_count[p.n_experts + expert_idx]); + const uint row_ids_offset = 2 * p.n_experts + 1 + expert_offset + tile_begin; + + for (uint i = gl_LocalInvocationIndex; i < tile_count; i += BLOCK_SIZE) { + const uint packed_row_id = uint(data_expert_count[row_ids_offset + i]); + const uint ii0 = packed_row_id & 0xffffu; + const uint ii1 = packed_row_id >> 16; + row_ids[i] = u16vec4(fastmod(ii0, p.ne11), ii1, ii0, 0); + } + barrier(); +} #endif void main() { @@ -266,7 +285,9 @@ void main() { const uint ik = gl_WorkGroupID.x / blocks_m; #ifdef MUL_MAT_ID - if (bitCount(p.nei0) == 1) { + if (p.hoist_row_ids != 0) { + load_row_ids_hoisted(expert_idx, ic); + } else if (bitCount(p.nei0) == 1) { load_row_ids(expert_idx, true, ic); } else { load_row_ids(expert_idx, false, ic); @@ -309,7 +330,9 @@ void main() { tensorLayoutNV<2> tensorLayoutA = createTensorLayoutNV(2); tensorLayoutNV<2, gl_CooperativeMatrixClampModeConstantNV> tensorLayoutAClamp = createTensorLayoutNV(2, gl_CooperativeMatrixClampModeConstantNV); tensorLayoutNV<2> tensorLayoutB = createTensorLayoutNV(2); +#ifndef MUL_MAT_ID tensorLayoutNV<2, gl_CooperativeMatrixClampModeConstantNV> tensorLayoutBClamp = createTensorLayoutNV(2, gl_CooperativeMatrixClampModeConstantNV); +#endif tensorLayoutNV<2, gl_CooperativeMatrixClampModeConstantNV> tensorLayoutD = createTensorLayoutNV(2, gl_CooperativeMatrixClampModeConstantNV); #if QUANT_K > 1 @@ -322,12 +345,19 @@ void main() { // Use end_k rather than p.K as the dimension because that's what // we need to bound check against when using split_k. - // Bounds check B against padded_N, but bounds check D against N. tensorLayoutA = setTensorLayoutDimensionNV(tensorLayoutA, p.M, end_k); +#ifdef MUL_MAT_ID + // MUL_MAT_ID pads each B row to stride_b so partial K tiles read zeros without clamping. + tensorLayoutB = setTensorLayoutDimensionNV(tensorLayoutB, BN, p.stride_b); +#else + // Bounds check B against padded_N, but bounds check D against N. tensorLayoutB = setTensorLayoutDimensionNV(tensorLayoutB, p.padded_N, end_k); +#endif tensorLayoutD = setTensorLayoutDimensionNV(tensorLayoutD, p.N, p.M); tensorLayoutAClamp = setTensorLayoutDimensionNV(tensorLayoutAClamp, p.M, end_k); +#ifndef MUL_MAT_ID tensorLayoutBClamp = setTensorLayoutDimensionNV(tensorLayoutBClamp, p.padded_N, end_k); +#endif tensorLayoutD = setTensorLayoutStrideNV(tensorLayoutD, p.stride_d, 1); @@ -504,7 +534,9 @@ void main() { tensorLayoutB = setTensorLayoutStrideNV(tensorLayoutB, stride_b, 1); +#ifndef MUL_MAT_ID tensorLayoutBClamp = setTensorLayoutStrideNV(tensorLayoutBClamp, stride_b, 1); +#endif uint k_iters = (end_k - start_k + BK - 1) / BK; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_id_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_id_funcs.glsl index 26c5c12a49a2..54ad60b2efba 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_id_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_id_funcs.glsl @@ -71,4 +71,19 @@ void load_row_ids(uint expert_idx, bool nei0_is_pow2, uint ic) { barrier(); } #endif // MUL_MAT_ID_USE_SUBGROUPS + +void load_row_ids_hoisted(uint expert_idx, uint ic) { + _ne1 = uint(data_expert_count[expert_idx]); + + const uint tile_begin = ic * BN; + const uint tile_count = tile_begin < _ne1 ? min(BN, _ne1 - tile_begin) : 0; + const uint expert_offset = uint(data_expert_count[p.n_experts + expert_idx]); + const uint row_ids_offset = 2 * p.n_experts + 1 + expert_offset + tile_begin; + + for (uint i = gl_LocalInvocationIndex; i < tile_count; i += BLOCK_SIZE) { + const uint packed_row_id = uint(data_expert_count[row_ids_offset + i]); + row_ids[i] = u16vec2(packed_row_id & 0xffffu, packed_row_id >> 16); + } + barrier(); +} #endif // MUL_MAT_ID diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp index aae1c2e8ae9f..1fbcbf6c9332 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp @@ -56,6 +56,8 @@ layout (push_constant) uniform parameter uint nei1; uint nbi1; uint ne11; + uint n_experts; + uint hoist_row_ids; #else uint base_work_group_z; uint num_batches; @@ -157,27 +159,31 @@ void main() { const uint loadstride_b = BLOCK_SIZE * LOAD_VEC_B / BK; #ifdef MUL_MAT_ID -#ifdef MUL_MAT_ID_USE_SUBGROUPS - if (bitCount(p.nei0) == 1) { - load_row_ids(expert_idx, true, ic); + if (p.hoist_row_ids != 0) { + load_row_ids_hoisted(expert_idx, ic); } else { - load_row_ids(expert_idx, false, ic); - } +#ifdef MUL_MAT_ID_USE_SUBGROUPS + if (bitCount(p.nei0) == 1) { + load_row_ids(expert_idx, true, ic); + } else { + load_row_ids(expert_idx, false, ic); + } #else - _ne1 = 0; - for (uint ii1 = 0; ii1 < p.nei1 && _ne1 < (ic + 1) * BN; ii1++) { - for (uint ii0 = 0; ii0 < p.nei0 && _ne1 < (ic + 1) * BN; ii0++) { - if (data_ids[ii1*p.nbi1 + ii0] == expert_idx) { - if (_ne1 >= ic * BN) { - row_ids[_ne1 - ic * BN] = u16vec2(ii0, ii1); + _ne1 = 0; + for (uint ii1 = 0; ii1 < p.nei1 && _ne1 < (ic + 1) * BN; ii1++) { + for (uint ii0 = 0; ii0 < p.nei0 && _ne1 < (ic + 1) * BN; ii0++) { + if (data_ids[ii1*p.nbi1 + ii0] == expert_idx) { + if (_ne1 >= ic * BN) { + row_ids[_ne1 - ic * BN] = u16vec2(ii0, ii1); + } + _ne1++; } - _ne1++; } } - } - barrier(); + barrier(); #endif + } // Workgroup has no work if (ic * BN >= _ne1) return; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/sum_rows.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/sum_rows.glsl index 2b841baa6bf2..1cb0f7827a38 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/sum_rows.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/sum_rows.glsl @@ -1,4 +1,6 @@ +#include "utils.glsl" + // vk_op_sum_rows_push_constants layout (push_constant) uniform parameter { @@ -15,11 +17,3 @@ layout (push_constant) uniform parameter uint get_aoffset() { return p.misalign_offsets >> 16; } uint get_doffset() { return p.misalign_offsets & 0xFFFF; } -// see init_fastdiv_values in ggml-vulkan.cpp -uint fastdiv(uint n, uint mp, uint L) { - uint msbs, lsbs; - // msbs = mulhi(n, mp) - umulExtended(n, mp, msbs, lsbs); - return (msbs + n) >> L; -} - diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/swiglu_clamp.comp b/ggml/src/ggml-vulkan/vulkan-shaders/swiglu_clamp.comp new file mode 100644 index 000000000000..dfe329759c7a --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/swiglu_clamp.comp @@ -0,0 +1,12 @@ +#version 450 + +#include "glu_head.glsl" + +float op(float a, float b) { + float gate = min(a, p.limit); + float up = clamp(b, -p.limit, p.limit); + + return gate / (1.0f + exp(-gate)) * up; +} + +#include "glu_main.glsl" diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/utils.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/utils.glsl index dc4a1e6d96ba..8aac64d75932 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/utils.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/utils.glsl @@ -9,14 +9,26 @@ uint fastmod(uint a, uint b) { return a % b; } -uint fastdiv(uint a, uint b) { +// see init_fastdiv_values in ggml-vulkan.cpp +uint fastdiv(uint n, uint mp, uint L) { + uint msbs, lsbs; + // msbs = mulhi(n, mp) + umulExtended(n, mp, msbs, lsbs); + return (msbs + n) >> L; +} + +uint fastdiv_L(uint packed, uint slot) { + return (packed >> (slot * 8)) & 0x3Fu; +} + +uint fastdiv_small(uint a, uint b) { return (a < b) ? 0 : (a / b); } void get_indices(uint idx, out uint i00, out uint i01, out uint i02, out uint i03, uint ne00, uint ne01, uint ne02, uint ne03) { - i03 = fastdiv(idx, (ne02*ne01*ne00)); + i03 = fastdiv_small(idx, (ne02*ne01*ne00)); const uint i03_offset = i03 * ne02*ne01*ne00; - i02 = fastdiv((idx - i03_offset), (ne01*ne00)); + i02 = fastdiv_small((idx - i03_offset), (ne01*ne00)); const uint i02_offset = i02*ne01*ne00; i01 = (idx - i03_offset - i02_offset) / ne00; i00 = idx - i03_offset - i02_offset - i01*ne00; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 0da943da9563..f0610f4cd82d 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -986,6 +986,8 @@ void process_shaders() { string_to_spv("swiglu_f32", "swiglu.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}}); string_to_spv("swiglu_oai_f16", "swiglu_oai.comp", {{"A_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}}); string_to_spv("swiglu_oai_f32", "swiglu_oai.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}}); + string_to_spv("swiglu_clamp_f16", "swiglu_clamp.comp", {{"A_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}}); + string_to_spv("swiglu_clamp_f32", "swiglu_clamp.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}}); string_to_spv("geglu_erf_f16", "geglu_erf.comp", {{"A_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}}); string_to_spv("geglu_erf_f32", "geglu_erf.comp", {{"A_TYPE", "float"}, {"D_TYPE", "float"}}); string_to_spv("geglu_quick_f16","geglu_quick.comp", {{"A_TYPE", "float16_t"}, {"D_TYPE", "float16_t"}}); @@ -1039,6 +1041,7 @@ void process_shaders() { string_to_spv("cumsum_multipass2_f32", "cumsum_multipass2.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}})); string_to_spv("count_experts", "count_experts.comp", merge_maps(base_dict, {{"A_TYPE", "uint"}, {"D_TYPE", "uint"}})); + string_to_spv("count_experts_subgroup", "count_experts.comp", merge_maps(base_dict, {{"A_TYPE", "uint"}, {"D_TYPE", "uint"}, {"USE_SUBGROUPS", "1"}})); for (std::string dim_str : {"", "_3d"}) { for (bool bda : {false, true}) { diff --git a/ggml/src/ggml-webgpu/ggml-webgpu-shader-lib.hpp b/ggml/src/ggml-webgpu/ggml-webgpu-shader-lib.hpp index 7a67ccf4fcbf..a7ff36030fab 100644 --- a/ggml/src/ggml-webgpu/ggml-webgpu-shader-lib.hpp +++ b/ggml/src/ggml-webgpu/ggml-webgpu-shader-lib.hpp @@ -3101,6 +3101,10 @@ class ggml_webgpu_shader_lib { defines.push_back("OP_GEGLU_QUICK"); variant += "_geglu_quick"; break; + case GGML_GLU_OP_SWIGLU_CLAMP: + defines.push_back("OP_SWIGLU_CLAMP"); + variant += "_swiglu_clamp"; + break; default: GGML_ABORT("Unsupported GLU op"); } diff --git a/ggml/src/ggml-webgpu/ggml-webgpu.cpp b/ggml/src/ggml-webgpu/ggml-webgpu.cpp index 2434848a55a8..b953118a7a33 100644 --- a/ggml/src/ggml-webgpu/ggml-webgpu.cpp +++ b/ggml/src/ggml-webgpu/ggml-webgpu.cpp @@ -2835,7 +2835,7 @@ static webgpu_encoded_op ggml_webgpu_glu(webgpu_context & ctx, (uint32_t) dst->ne[2], (uint32_t) ((int32_t *) dst->op_params)[1], // swapped ggml_webgpu_u32_from_f32(ggml_get_op_params_f32(dst, 2)), // alpha, for swiglu_oai - ggml_webgpu_u32_from_f32(ggml_get_op_params_f32(dst, 3)), // limit, for swiglu_oai + ggml_webgpu_u32_from_f32(ggml_get_op_params_f32(dst, 3)), // limit }; std::vector entries; @@ -4483,6 +4483,7 @@ static bool ggml_backend_webgpu_device_supports_op(ggml_backend_dev_t dev, const case GGML_GLU_OP_SWIGLU: case GGML_GLU_OP_GEGLU_ERF: case GGML_GLU_OP_GEGLU_QUICK: + case GGML_GLU_OP_SWIGLU_CLAMP: supports_op = op->type == GGML_TYPE_F32 || op->type == GGML_TYPE_F16; break; case GGML_GLU_OP_SWIGLU_OAI: diff --git a/ggml/src/ggml-webgpu/wgsl-shaders/glu.wgsl b/ggml/src/ggml-webgpu/wgsl-shaders/glu.wgsl index d03f1c207d98..6bbed5d3bfe9 100644 --- a/ggml/src/ggml-webgpu/wgsl-shaders/glu.wgsl +++ b/ggml/src/ggml-webgpu/wgsl-shaders/glu.wgsl @@ -37,6 +37,14 @@ fn op(a: f32, b: f32) -> f32 { return out_glu; } #endif +#ifdef OP_SWIGLU_CLAMP +fn op(a: DataType, b: DataType) -> DataType { + let limit = DataType(params.limit); + let gate = min(a, limit); + let up = clamp(b, -limit, limit); + return gate / (1.0 + exp(-gate)) * up; +} +#endif #ifdef OP_GEGLU_ERF const p_erf: DataType = 0.3275911; const a1_erf: DataType = 0.254829592; diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c index e0b615c07edf..3bd3e3fe5ea0 100644 --- a/ggml/src/ggml.c +++ b/ggml/src/ggml.c @@ -1253,10 +1253,10 @@ static const char * GGML_GLU_OP_NAME[GGML_GLU_OP_COUNT] = { "SWIGLU_OAI", "GEGLU_ERF", "GEGLU_QUICK", + "SWIGLU_CLAMP", }; -static_assert(GGML_GLU_OP_COUNT == 6, "GGML_GLU_OP_COUNT != 6"); - +static_assert(GGML_GLU_OP_COUNT == 7, "GGML_GLU_OP_COUNT != 7"); static_assert(sizeof(struct ggml_object)%GGML_MEM_ALIGN == 0, "ggml_object size must be a multiple of GGML_MEM_ALIGN"); static_assert(sizeof(struct ggml_tensor)%GGML_MEM_ALIGN == 0, "ggml_tensor size must be a multiple of GGML_MEM_ALIGN"); @@ -3119,6 +3119,17 @@ struct ggml_tensor * ggml_swiglu_oai( return result; } +struct ggml_tensor * ggml_swiglu_clamp( + struct ggml_context * ctx, + struct ggml_tensor * a, + struct ggml_tensor * b, + float limit) { + struct ggml_tensor * result = ggml_glu_impl(ctx, a, b, GGML_GLU_OP_SWIGLU_CLAMP, false); + ggml_set_op_params_f32(result, 3, limit); + + return result; +} + // ggml_norm static struct ggml_tensor * ggml_norm_impl( diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index fffbd6745397..b8fa095b4b6c 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -167,6 +167,7 @@ class LLM: 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" @@ -2907,6 +2908,9 @@ class MODEL_TENSOR(IntEnum): MODEL_TENSOR.FFN_GATE_EXP, MODEL_TENSOR.FFN_GATE_UP_EXP, MODEL_TENSOR.PER_LAYER_TOKEN_EMBD, + MODEL_TENSOR.NEXTN_EH_PROJ, + MODEL_TENSOR.NEXTN_ENORM, + MODEL_TENSOR.NEXTN_HNORM, MODEL_TENSOR.PLE_KEY, MODEL_TENSOR.PLE_VALUE, MODEL_TENSOR.PLE_NORM_KEY, diff --git a/gguf-py/gguf/gguf_writer.py b/gguf-py/gguf/gguf_writer.py index b1d161bcb37e..d95fe9b1ac3c 100644 --- a/gguf-py/gguf/gguf_writer.py +++ b/gguf-py/gguf/gguf_writer.py @@ -467,10 +467,15 @@ def write_tensors_to_file(self, *, progress: bool = False) -> None: 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: @@ -1008,6 +1013,9 @@ def add_selector_top_k(self, value: int) -> None: 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) diff --git a/gguf-py/gguf/lazy.py b/gguf-py/gguf/lazy.py index 6a0aee881107..a39f22321597 100644 --- a/gguf-py/gguf/lazy.py +++ b/gguf-py/gguf/lazy.py @@ -251,6 +251,10 @@ def nbytes(self) -> int: 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 diff --git a/include/llama.h b/include/llama.h index 49a758db2680..c2ef101f6c44 100644 --- a/include/llama.h +++ b/include/llama.h @@ -214,10 +214,10 @@ 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_lazy_mode { + LLAMA_LAZY_MODE_OFF = 0, // always read the whole tensor up front + LLAMA_LAZY_MODE_AUTO = 1, // lazy only for marked tensors larger than 4 GiB (requires mmap) + LLAMA_LAZY_MODE_ON = 2, // read the rows of tensors marked by the arch on demand (requires mmap) }; enum llama_context_type { @@ -321,11 +321,15 @@ 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 + enum llama_lazy_mode lazy_mode; // 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; + // n-gram hash-embedding table kept on disk (see ple_on_disk below) + int32_t ple_io_threads; // parallel pread workers + int32_t ple_cache_mb; // in-memory cache of recently read rows, 0 disables + // proportion of the model (layers or rows) to offload to each GPU, size: llama_max_devices() const float * tensor_split; @@ -347,6 +351,9 @@ extern "C" { bool no_host; // bypass host buffer allowing extra buffers to be used bool no_alloc; // only load metadata and simulate memory allocations bool load_mtp; // whether to load MTP layers + bool ple_on_disk; // keep the n-gram hash-embedding table (per_layer_token_embd) on disk: never + // mapped or loaded, the rows a batch needs are read from the file (qwen4exp) + bool ple_direct_io; // read those rows with O_DIRECT, bypassing the page cache }; struct llama_sampler_seq_config { diff --git a/scripts/snapdragon/build.py b/scripts/snapdragon/build.py index 02dcb930cb67..5e9fab3d08b6 100755 --- a/scripts/snapdragon/build.py +++ b/scripts/snapdragon/build.py @@ -11,6 +11,8 @@ import shutil import logging +from sdk import validate_windows_sdks + logger = logging.getLogger("build") @@ -65,6 +67,13 @@ def main(): logger.error(f"Error: Invalid target format '{args.target}'. Must be android[:serial]/adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, or windows/wos.") sys.exit(1) + if target_type == "windows": + logger.info("Windows target selected. Forcing native compilation...") + args.no_docker = True + if platform.system() != "Windows": + logger.warning("Warning: Windows compilation is intended to run on Windows arm64 hosts.") + validate_windows_sdks() + # Determine preset and check if it's debug preset = args.preset if preset: @@ -120,12 +129,6 @@ def main(): jobs = args.jobs if args.jobs else os.cpu_count() or 4 - if target_type == "windows": - logger.info("Windows target selected. Forcing native compilation...") - args.no_docker = True - if platform.system() != "Windows": - logger.warning("Warning: Windows compilation is intended to run on Windows arm64 hosts.") - if args.no_docker: # Native/local host build logger.info("Running native/local CMake build...") @@ -258,3 +261,6 @@ def main(): except KeyboardInterrupt: logger.info("\nInterrupted by user.") sys.exit(130) + except RuntimeError as err: + logger.error("Error: %s", err) + sys.exit(1) diff --git a/scripts/snapdragon/sdk.py b/scripts/snapdragon/sdk.py new file mode 100644 index 000000000000..bb3cb77b2ad4 --- /dev/null +++ b/scripts/snapdragon/sdk.py @@ -0,0 +1,62 @@ +import os +from pathlib import Path + + +SDK_CONFIGS = ( + { + "name": "Hexagon SDK", + "repo": "snapdragon-toolchain/hexagon-sdk", + "default_version": "6.6.0.0", + "parent_dir": "Hexagon_SDK", + "archive_prefix": "hexagon-sdk-v", + "markers": ("hexagon_sdk.json",), + }, + { + "name": "OpenCL SDK", + "repo": "snapdragon-toolchain/opencl-sdk", + "default_version": "2.3.2", + "parent_dir": "OpenCL_SDK", + "archive_prefix": "adreno-opencl-sdk-v", + "markers": ("include/CL", "lib/OpenCL.lib"), + }, +) + + +def is_valid_sdk(config, target_dir): + return target_dir.is_dir() and all((target_dir / marker).exists() for marker in config["markers"]) + + +def get_hexagon_tools_dir(hexagon_dir): + tools_parent = hexagon_dir / "tools" / "HEXAGON_Tools" + if not tools_parent.is_dir(): + raise RuntimeError(f"Expected Hexagon tools directory in {tools_parent}") + tools_dirs = [path for path in tools_parent.iterdir() if path.is_dir()] + if len(tools_dirs) != 1: + raise RuntimeError(f"Expected one Hexagon tools directory in {tools_parent}") + return tools_dirs[0] + + +def validate_windows_sdks(): + hexagon_config, opencl_config = SDK_CONFIGS + hexagon_dir = os.environ.get("HEXAGON_SDK_ROOT") + tools_dir = os.environ.get("HEXAGON_TOOLS_ROOT") + opencl_dir = os.environ.get("OPENCL_SDK_ROOT") + missing = [] + + expected_tools_dir = None + if not hexagon_dir or not is_valid_sdk(hexagon_config, Path(hexagon_dir)): + missing.append("HEXAGON_SDK_ROOT") + else: + try: + expected_tools_dir = get_hexagon_tools_dir(Path(hexagon_dir)) + except RuntimeError: + pass + if not tools_dir or not expected_tools_dir or Path(tools_dir) != expected_tools_dir: + missing.append("HEXAGON_TOOLS_ROOT") + if not opencl_dir or not is_valid_sdk(opencl_config, Path(opencl_dir)): + missing.append("OPENCL_SDK_ROOT") + if missing: + raise RuntimeError( + f"Missing or invalid Windows SDK paths: {', '.join(missing)}. " + "Run scripts/snapdragon/setup-sdk.py first." + ) diff --git a/scripts/snapdragon/setup-sdk.py b/scripts/snapdragon/setup-sdk.py new file mode 100644 index 000000000000..ad828c079a6a --- /dev/null +++ b/scripts/snapdragon/setup-sdk.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# +# Install Windows on Snapdragon SDKs for llama.cpp. +# + +import sys +import os +import argparse +import shutil +import logging +import json +import hashlib +import tarfile +import tempfile +from pathlib import Path +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +from sdk import SDK_CONFIGS, get_hexagon_tools_dir, is_valid_sdk + + +logger = logging.getLogger("setup_sdk") + +DEFAULT_SDK_BASE_DIR = r"C:\Qualcomm" + + +def get_sdk_releases(config): + request = Request( + f"https://api.github.com/repos/{config['repo']}/releases?per_page=100", + headers={"Accept": "application/vnd.github+json", "User-Agent": "llama.cpp"}, + ) + try: + with urlopen(request, timeout=30) as response: + releases = json.load(response) + except (HTTPError, URLError, TimeoutError) as err: + raise RuntimeError(f"Cannot query {config['name']} releases: {err}") from err + + result = [] + for release in releases: + if release["draft"] or release["prerelease"]: + continue + version = release["tag_name"].removeprefix("v") + archive_name = f"{config['archive_prefix']}{version}-arm64-wos.tar.xz" + for asset in release["assets"]: + if asset["name"] != archive_name: + continue + result.append({ + "version": version, + "name": asset["name"], + "url": asset["browser_download_url"], + "sha256": (asset.get("digest") or "").removeprefix("sha256:"), + }) + return result + + +def list_sdk_releases(): + for config in SDK_CONFIGS: + logger.info("%s:", config["name"]) + releases = get_sdk_releases(config) + if not releases: + logger.info(" no Windows on Snapdragon releases found") + continue + for release in releases: + logger.info(" %s: %s", release["version"], release["name"]) + + +def get_sdk_release(config, version): + version = version or config["default_version"] + version = version.removeprefix("v") + for release in get_sdk_releases(config): + if release["version"] == version: + if not release["sha256"]: + raise RuntimeError(f"{config['name']} {version} does not provide a SHA-256 digest") + return release + raise RuntimeError( + f"No Windows on Snapdragon release for {config['name']} {version}. " + "Run scripts/snapdragon/setup-sdk.py --list-sdk-releases to see available versions." + ) + + +def sha256sum(path): + digest = hashlib.sha256() + with open(path, "rb") as file: + for chunk in iter(lambda: file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def download_sdk(release, archive): + while True: + if archive.exists() and sha256sum(archive) == release["sha256"]: + logger.info("Using existing archive %s", archive) + return + + offset = archive.stat().st_size if archive.exists() else 0 + headers = {"User-Agent": "llama.cpp"} + if offset: + headers["Range"] = f"bytes={offset}-" + logger.info("Resuming download of %s at %d MiB", release["name"], offset // (1024 * 1024)) + else: + logger.info("Downloading %s", release["name"]) + + try: + with urlopen(Request(release["url"], headers=headers), timeout=30) as response: + mode = "ab" if offset and response.status == 206 else "wb" + with open(archive, mode) as file: + shutil.copyfileobj(response, file) + except HTTPError as err: + if err.code != 416: + raise RuntimeError(f"Cannot download {release['name']}: {err}") from err + archive.unlink(missing_ok=True) + continue + except (URLError, TimeoutError) as err: + raise RuntimeError(f"Cannot download {release['name']}: {err}") from err + + if sha256sum(archive) == release["sha256"]: + return + raise RuntimeError(f"SHA-256 mismatch for {archive}. Re-run the command to resume the download.") + + +def extract_sdk(config, archive, target_dir): + if not hasattr(tarfile, "data_filter"): + raise RuntimeError("SDK extraction requires Python 3.10.12 or later") + + with tempfile.TemporaryDirectory(prefix=f".{target_dir.name}.tmp-", dir=target_dir.parent) as staging_path: + staging_dir = Path(staging_path) + with tarfile.open(archive, "r:xz") as tar: + tar.extractall(staging_dir, filter=tarfile.data_filter) + + candidates = [staging_dir] + [path for path in staging_dir.iterdir() if path.is_dir()] + extracted_dirs = [path for path in candidates if is_valid_sdk(config, path)] + if len(extracted_dirs) != 1: + raise RuntimeError(f"{config['name']} archive does not contain the expected files") + extracted_dir = extracted_dirs[0] + + backup_dir = None + if target_dir.exists(): + backup_dir = target_dir.parent / f".{target_dir.name}.backup" + if backup_dir.exists(): + raise RuntimeError(f"Cannot replace {target_dir}: backup directory {backup_dir} already exists") + target_dir.replace(backup_dir) + try: + extracted_dir.replace(target_dir) + except Exception: + if backup_dir: + backup_dir.replace(target_dir) + raise + if backup_dir: + shutil.rmtree(backup_dir) + + +def install_sdk(config, version, base_dir, force): + version = (version or config["default_version"]).removeprefix("v") + target_dir = base_dir / config["parent_dir"] / version + if is_valid_sdk(config, target_dir) and not force: + logger.info("Using existing %s at %s", config["name"], target_dir) + return target_dir + + release = get_sdk_release(config, version) + target_dir.parent.mkdir(parents=True, exist_ok=True) + archive = target_dir.parent / release["name"] + download_sdk(release, archive) + logger.info("Extracting %s to %s", config["name"], target_dir) + extract_sdk(config, archive, target_dir) + archive.unlink(missing_ok=True) + return target_dir + + +def set_user_environment(values): + if os.name != "nt": + raise RuntimeError("SDK setup must run on Windows") + + import winreg + + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, "Environment") as key: + for name, value in values.items(): + winreg.SetValueEx(key, name, 0, winreg.REG_SZ, str(value)) + os.environ[name] = str(value) + + import ctypes + + result = ctypes.c_ulong() + ctypes.windll.user32.SendMessageTimeoutW(0xffff, 0x001a, 0, "Environment", 0x0002, 5000, ctypes.byref(result)) + + +def setup_sdks(args): + base_dir = Path(args.sdk_base_dir).expanduser().resolve() + hexagon_config, opencl_config = SDK_CONFIGS + environment = {} + + if args.hexagon is not None: + hexagon_dir = install_sdk(hexagon_config, args.hexagon, base_dir, args.force) + environment["HEXAGON_SDK_ROOT"] = hexagon_dir + environment["HEXAGON_TOOLS_ROOT"] = get_hexagon_tools_dir(hexagon_dir) + if args.opencl is not None: + opencl_dir = install_sdk(opencl_config, args.opencl, base_dir, args.force) + environment["OPENCL_SDK_ROOT"] = opencl_dir + + set_user_environment(environment) + logger.info("SDK environment variables were updated. Start a new terminal before building.") + + +def main(): + logging.basicConfig(level=logging.INFO, format="%(message)s") + parser = argparse.ArgumentParser(description="Install Windows on Snapdragon SDKs for llama.cpp.") + parser.add_argument("--list-sdk-releases", action="store_true", help="List available Windows on Snapdragon SDK releases") + parser.add_argument("--sdk-base-dir", default=DEFAULT_SDK_BASE_DIR, help=r"SDK installation directory (default: C:\Qualcomm)") + parser.add_argument("--hexagon", nargs="?", const=SDK_CONFIGS[0]["default_version"], metavar="VERSION", help="Install the Hexagon SDK, optionally selecting a version") + parser.add_argument("--opencl", nargs="?", const=SDK_CONFIGS[1]["default_version"], metavar="VERSION", help="Install the OpenCL SDK, optionally selecting a version") + parser.add_argument("--force", action="store_true", help="Reinstall selected SDKs even when they already exist") + args = parser.parse_args() + + if args.list_sdk_releases: + if args.sdk_base_dir != DEFAULT_SDK_BASE_DIR or args.hexagon is not None or args.opencl is not None or args.force: + parser.error("Installation options cannot be combined with --list-sdk-releases") + list_sdk_releases() + return + if args.hexagon is None and args.opencl is None: + parser.error("Select at least one SDK with --hexagon or --opencl") + if os.name != "nt": + parser.error("SDK setup must run on Windows") + setup_sdks(args) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + logger.info("\nInterrupted by user.") + sys.exit(130) + except RuntimeError as err: + logger.error("Error: %s", err) + sys.exit(1) diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py index 98b9ddc8ef6b..0170b5b168f4 100755 --- a/scripts/sync_vendor.py +++ b/scripts/sync_vendor.py @@ -5,7 +5,7 @@ import sys import subprocess -HTTPLIB_VERSION = "refs/tags/v0.53.1" +HTTPLIB_VERSION = "refs/tags/v0.54.1" # used by examples/gguf-hash, these repos have no release tag, so we pin a commit XXHASH_COMMIT = "9f465f1ea932d6ad9a26cd77496311ffa544cd68" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8922dc12adc7..8404482836b7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(llama llama-memory-hybrid-idx.cpp llama-memory-recurrent.cpp llama-mmap.cpp + llama-ple-disk.cpp llama-model-loader.cpp llama-model-saver.cpp llama-model.cpp diff --git a/src/llama-context.cpp b/src/llama-context.cpp index fb88919f9d67..ca387021d221 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -231,10 +231,10 @@ llama_context::llama_context( cparams.fused_gdn_ar = true; cparams.fused_gdn_ch = true; - cparams.auto_fgdn = true; + cparams.auto_fgdn = false; - cparams.fused_lid = true; - cparams.auto_flid = true; + cparams.fused_lid = true; + cparams.auto_flid = false; cparams.fused_dsv4_hc_pre = true; cparams.fused_dsv4_hc_comb = true; @@ -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"); } @@ -1336,7 +1344,12 @@ llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch, ll // in order to correctly reuse a graph, it's full topology has to be uniquely determined by these parameters const auto gparams = graph_params(res, ubatch, mctx, gtype); + // LLAMA_GRAPH_TIMING=1: log where the CPU time of a decode goes (build, alloc, inputs) + static const bool graph_timing = getenv("LLAMA_GRAPH_TIMING") && atoi(getenv("LLAMA_GRAPH_TIMING")) != 0; + int64_t t_build = 0, t_alloc = 0, t_inputs = 0; + bool reused = false; if (!graph_reuse_disable && res->can_reuse(gparams)) { + reused = true; //LLAMA_LOG_DEBUG("%s: reusing previous graph\n", __func__); // with pipeline parallelism, the previous graph_compute_async may still be running @@ -1353,11 +1366,11 @@ llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch, ll ggml_backend_sched_reset(sched.get()); ggml_backend_sched_set_eval_callback(sched.get(), cparams.cb_eval, cparams.cb_eval_user_data); - //const auto t_start_us = ggml_time_us(); + const auto t_build_us = ggml_time_us(); gf = model.build_graph(gparams); - //LLAMA_LOG_INFO("graph build time: %.3f ms\n", (ggml_time_us() - t_start_us)/1000.0); + t_build = ggml_time_us() - t_build_us; if (!gf) { LLAMA_LOG_ERROR("%s: failed to initialize graph\n", __func__); @@ -1365,21 +1378,28 @@ llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch, ll return nullptr; } + const auto t_alloc_us = ggml_time_us(); if (!ggml_backend_sched_alloc_graph(sched.get(), gf)) { LLAMA_LOG_ERROR("%s: failed to allocate graph\n", __func__); ret = GGML_STATUS_ALLOC_FAILED; return nullptr; } + t_alloc = ggml_time_us() - t_alloc_us; } // set the input data for the input tensors { - //const auto t_start_us = ggml_time_us(); + const auto t_inputs_us = ggml_time_us(); // FIXME this call causes a crash if any model inputs were not used in the graph and were therefore not allocated res->set_inputs(&ubatch); - //LLAMA_LOG_INFO("graph set inputs time: %.3f ms\n", (ggml_time_us() - t_start_us)/1000.0); + t_inputs = ggml_time_us() - t_inputs_us; + if (graph_timing) { + LLAMA_LOG_WARN("graph-timing: n_tokens=%3u gtype=%d reused=%d build=%.2f alloc=%.2f inputs=%.2f ms nodes=%d\n", + ubatch.n_tokens, (int) gtype, (int) reused, t_build/1000.0, t_alloc/1000.0, t_inputs/1000.0, + ggml_graph_n_nodes(res->get_gf())); + } } const auto status = graph_compute(res->get_gf(), ubatch.n_tokens > 1); @@ -2892,13 +2912,83 @@ class llama_io_read_device : public llama_io_read_i { 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); diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index 8fca8e1bc0ef..72db486cacea 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -1776,14 +1776,11 @@ ggml_tensor * llm_graph_context::build_ffn( const float limit = hparams.swiglu_clamp_shexp[il]; constexpr float eps = 1e-6f; if (limit > eps) { - tmp = ggml_clamp(ctx0, tmp, -limit, limit); - cb(tmp, "ffn_up_clamped", il); - if (arch == LLM_ARCH_DEEPSEEK4 || (arch == LLM_ARCH_DFLASH && hparams.dsv4_hc_mult > 0)) { - cur = ggml_clamp(ctx0, cur, -INFINITY, limit); - cb(cur, "ffn_gate_clamped", il); - cur = ggml_swiglu_split(ctx0, cur, tmp); + cur = ggml_swiglu_clamp(ctx0, cur, tmp, limit); } else { + tmp = ggml_clamp(ctx0, tmp, -limit, limit); + cb(tmp, "ffn_up_clamped", il); ggml_tensor * gate_act = ggml_silu(ctx0, cur); cb(gate_act, "ffn_silu", il); gate_act = ggml_clamp(ctx0, gate_act, -INFINITY, limit); @@ -2173,14 +2170,11 @@ ggml_tensor * llm_graph_context::build_moe_ffn( const float limit = hparams.swiglu_clamp_exp[il]; constexpr float eps = 1e-6f; if (limit > eps) { - up = ggml_clamp(ctx0, up, -limit, limit); - cb(up, "ffn_moe_up_clamped", il); - if (arch == LLM_ARCH_DEEPSEEK4 || (arch == LLM_ARCH_DFLASH && hparams.dsv4_hc_mult > 0)) { - cur = ggml_clamp(ctx0, cur, -INFINITY, limit); - cb(cur, "ffn_moe_gate_clamped", il); - cur = ggml_swiglu_split(ctx0, cur, up); + cur = ggml_swiglu_clamp(ctx0, cur, up, limit); } else { + up = ggml_clamp(ctx0, up, -limit, limit); + cb(up, "ffn_moe_up_clamped", il); ggml_tensor * gate_act = ggml_silu(ctx0, cur); cb(gate_act, "ffn_moe_silu", il); gate_act = ggml_clamp(ctx0, gate_act, -INFINITY, limit); diff --git a/src/llama-kv-cache.cpp b/src/llama-kv-cache.cpp index 8fafcd15304e..65afbd8c3778 100644 --- a/src/llama-kv-cache.cpp +++ b/src/llama-kv-cache.cpp @@ -2034,7 +2034,7 @@ void llm_graph_input_k_shift::set_input(const llama_ubatch * ubatch) { kv_self->set_input_k_shift(k_shift); } - if (k_rot) { + if (k_rot && k_rot->buffer) { kv_self->set_input_k_rot(k_rot); } } diff --git a/src/llama-kv-cells.h b/src/llama-kv-cells.h index 5167c037db3e..a4292c79e443 100644 --- a/src/llama-kv-cells.h +++ b/src/llama-kv-cells.h @@ -320,13 +320,14 @@ class llama_kv_cells { } const auto m = seq[i] & seqs; - if (m.none()) { - continue; - } - for (llama_seq_id s = 0; s < LLAMA_MAX_SEQ; ++s) { + // a cell carries a handful of sequences at most, out of LLAMA_MAX_SEQ + size_t left = m.count(); + + for (llama_seq_id s = 0; left > 0 && s < (llama_seq_id) LLAMA_MAX_SEQ; ++s) { if (m.test(s)) { f(s, pos[i], ext[i].tok); + --left; } } } diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp index d9241022cf41..58f3fb8de1a9 100644 --- a/src/llama-model-loader.cpp +++ b/src/llama-model-loader.cpp @@ -577,6 +577,7 @@ llama_model_loader::llama_model_loader( llm_kv = LLM_KV(llm_arch_from_string(arch_name)); files.emplace_back(new llama_file(fname.c_str(), "rb", use_direct_io)); + fnames.push_back(fname); contexts.emplace_back(ctx); // Save tensors data offset of the main file. @@ -645,6 +646,7 @@ llama_model_loader::llama_model_loader( } files.emplace_back(new llama_file(fname_split, "rb", use_direct_io)); + fnames.push_back(fname_split); contexts.emplace_back(ctx); // Save tensors data offset info of the shard. @@ -689,6 +691,7 @@ llama_model_loader::llama_model_loader( llm_kv = LLM_KV(llm_arch_from_string(arch_name)); files.emplace_back(new llama_file(file)); + fnames.push_back("(file*)"); contexts.emplace_back(ctx); // Save tensors data offset info of the main file. @@ -1070,11 +1073,52 @@ static ggml_backend_buffer_type_t select_weight_buft(const llama_hparams & hpara return nullptr; } +ggml_backend_buffer_type_t llama_model_loader::lazy_read::buft() { + auto * cpu_dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU); + if (!cpu_dev) { + throw std::runtime_error("no CPU backend found"); + } + return ggml_backend_dev_buffer_type(cpu_dev); +} + +bool llama_model_loader::lazy_read::add(const std::string & name, const ggml_tensor * t, const llama_tensor_weight * w) { + if (mode == LLAMA_LAZY_MODE_OFF) { + return false; + } + + // do not lazy-read small tensors, it has significant overhead and is not worth it + constexpr size_t auto_min_size = 4ull * 1024 * 1024 * 1024; + if (mode != LLAMA_LAZY_MODE_ON && ggml_nbytes(t) <= auto_min_size) { + return false; + } + + if (!llama_mmap::SUPPORTED) { + LLAMA_LOG_WARN("%s: mmap is not available, so tensor %s (size = %zu MiB) is loaded into RAM in full\n", + __func__, name.c_str(), ggml_nbytes(t)/1024/1024); + return false; + } + + if (w) { + ranges[w->idx].emplace_back(w->offs, w->offs + ggml_nbytes(t)); + tensors.insert(name); + + LLAMA_LOG_INFO("%s: tensor %s (size = %zu MiB) lazy read enabled\n", + __func__, name.c_str(), ggml_nbytes(t)/1024/1024); + } + + return true; +} + struct ggml_tensor * llama_model_loader::create_tensor( const llama_hparams & hparams, const buft_list_t * buft_list_cpu, const buft_list_t * buft_list_input, const buft_list_t * buft_list_output, const buft_list_t * buft_list_layer, const LLM_TN_IMPL & tn, const std::initializer_list & ne, int flags) { + // set below, before buft_for_tensor() runs + bool is_lazy = false; + auto ctx_for_buft = [&](ggml_backend_buffer_type_t buft) -> ggml_context * { - auto it = ctx_map.find(buft); + const ctx_key key { buft, is_lazy }; + + auto it = ctx_map.find(key); if (it == ctx_map.end()) { // one ggml context per buffer type int max_n_tensors = n_tensors; @@ -1096,7 +1140,7 @@ struct ggml_tensor * llama_model_loader::create_tensor( throw std::runtime_error(format("failed to create ggml context")); } - ctx_map.emplace(buft, ctx); + ctx_map.emplace(key, ctx); return ctx; } @@ -1160,6 +1204,10 @@ struct ggml_tensor * llama_model_loader::create_tensor( } } + if (is_lazy) { + return lazy_read::buft(); + } + // select the buffer type for this tensor const buft_list_t * buft_list; switch (info.layer) { @@ -1287,16 +1335,9 @@ 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); - } + if (flags & TENSOR_READ_LAZY) { + // the decision must not depend on the load mode, or the memory-fit pass (no_alloc, no mmap) + is_lazy = lazy.add(tn.str(), cur, no_alloc ? nullptr : &require_weight(tn.str().c_str())); } ggml_tensor t_meta = *cur; @@ -1363,7 +1404,8 @@ void llama_model_loader::done_getting_tensors(bool partial) const { } void llama_model_loader::init_mappings(bool prefetch, llama_mlocks * mlock_mmaps) { - if (use_mmap) { + // note: read_lazy also requires mmap; this condition make sure it's usable even when --load-mode is not set to mmap + if (use_mmap || lazy.any()) { mappings.reserve(files.size()); mmaps_used.reserve(files.size()); for (uint32_t idx = 0; idx < files.size(); idx++) { @@ -1380,11 +1422,10 @@ void llama_model_loader::init_mappings(bool prefetch, llama_mlocks * mlock_mmaps } } - const auto it_lazy = lazy_tensor_ranges.find(idx); - static const llama_mmap::ranges no_lazy_ranges; + const size_t prefetch_size = prefetch && use_mmap ? -1 : 0; - std::unique_ptr mapping = std::make_unique(file.get(), prefetch ? -1 : 0, is_numa, - it_lazy != lazy_tensor_ranges.end() ? it_lazy->second : no_lazy_ranges); + std::unique_ptr mapping = std::make_unique(file.get(), prefetch_size, is_numa, + lazy.for_file(idx)); mmaps_used.emplace_back(mapping->size(), 0); if (mlock_mmaps) { std::unique_ptr mlock_mmap(new llama_mlock()); @@ -1575,7 +1616,9 @@ bool llama_model_loader::load_all_data( size_t n_size = ggml_nbytes(cur); - if (use_mmap) { + const bool from_mapping = use_mmap || lazy.has(cur); + + if (from_mapping) { const auto & mapping = mappings.at(weight->idx); ggml_backend_buffer_t buf_mmap = nullptr; if (bufs.count(weight->idx)) { @@ -1592,7 +1635,9 @@ bool llama_model_loader::load_all_data( GGML_ASSERT(buf_mmap || cur->data); // either we have a buffer to allocate the tensor in, or it is already allocated if (buf_mmap && cur->data == nullptr) { ggml_backend_tensor_alloc(buf_mmap, cur, data); - if (lmlocks) { + + // locking a lazy tensor would fault all of it in, which is what lazy avoids + if (lmlocks && !lazy.has(cur)) { const auto & lmlock = lmlocks->at(weight->idx); lmlock->grow_to(weight->offs + n_size); } diff --git a/src/llama-model-loader.h b/src/llama-model-loader.h index 407260e9907e..72bbd53e7d03 100644 --- a/src/llama-model-loader.h +++ b/src/llama-model-loader.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -83,18 +84,46 @@ 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; + // handle TENSOR_READ_LAZY + // use case: keep PLE / engrams embd tensors on disk, read them on demand + struct lazy_read { + // set by the caller before the create_tensor() calls + enum llama_lazy_mode mode = LLAMA_LAZY_MODE_OFF; + + // decide whether this tensor is read lazily + // pass w to also record it, or nullptr to only ask + bool add(const std::string & name, const ggml_tensor * t, const llama_tensor_weight * w); + + bool any() const { + return !ranges.empty(); + } + + bool has(const ggml_tensor * t) const { + return tensors.count(ggml_get_name(t)) > 0; + } + + const llama_mmap::ranges & for_file(uint32_t idx) const { + static const llama_mmap::ranges none; + + const auto it = ranges.find(idx); + return it == ranges.end() ? none : it->second; + } + + // lazy tensors are gathered on the host, so no offload setting applies to them + static ggml_backend_buffer_type_t buft(); + + private: + std::map ranges; + std::set tensors; + } lazy; llama_files files; + std::vector fnames; // one per entry of files, for readers that outlive the loader llama_ftype ftype; llama_fver fver; llama_mmaps mappings; - // byte ranges of TENSOR_READ_LAZY tensors, per file index - std::map lazy_tensor_ranges; - std::map weights_map; std::unordered_map kv_overrides; const llama_model_tensor_buft_override * tensor_buft_overrides; @@ -119,7 +148,22 @@ struct llama_model_loader { } }; - std::map ctx_map; + // lazy tensors need dedicated context + struct ctx_key { + ggml_backend_buffer_type_t buft; + bool lazy; + }; + + struct ctx_key_comparator { + bool operator()(const ctx_key & lhs, const ctx_key & rhs) const { + if (lhs.lazy != rhs.lazy) { + return lhs.lazy < rhs.lazy; + } + return strcmp(ggml_backend_buft_name(lhs.buft), ggml_backend_buft_name(rhs.buft)) < 0; + } + }; + + std::map ctx_map; // track tensors that had to be moved for debugging: size_t n_tensors_moved = 0; diff --git a/src/llama-model.cpp b/src/llama-model.cpp index fc83658dd7ff..b6428791f118 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -1678,7 +1678,9 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) { } } - ml.init_mappings(true, use_mlock ? &pimpl->mlock_mmaps : nullptr); + // With the n-gram table left on disk, a populated mapping would pull the table's + // third of the file resident for nothing; readahead alone carries the sequential load. + ml.init_mappings(!params.ple_on_disk, use_mlock ? &pimpl->mlock_mmaps : nullptr); pimpl->mappings.reserve(ml.mappings.size()); // create the backend buffers @@ -1689,7 +1691,8 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) { const size_t n_max_backend_buffer = ml.ctx_map.size() * ml.files.size(); pimpl->ctxs_bufs.reserve(n_max_backend_buffer); - for (auto & [buft, ctx_ptr] : ml.ctx_map) { + for (auto & [ctx_key, ctx_ptr] : ml.ctx_map) { + ggml_backend_buffer_type_t buft = ctx_key.buft; ggml_context * ctx = ctx_ptr.get(); // skip contexts without tensors @@ -1715,7 +1718,11 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) { bool is_default_buft = buft == ggml_backend_dev_buffer_type(dev); std::vector bufs; - if (ml.use_mmap && use_mmap_buffer && buffer_from_host_ptr_supported && is_default_buft) { + + // a lazy context is mapped whatever the load mode, but the memory-fit pass maps nothing + const bool is_lazy_mapped = ctx_key.lazy && !ml.no_alloc; + + if ((ml.use_mmap || is_lazy_mapped) && use_mmap_buffer && buffer_from_host_ptr_supported && is_default_buft) { GGML_ASSERT(!ml.no_alloc); for (uint32_t idx = 0; idx < ml.files.size(); idx++) { // only the mmap region containing the tensors in the model is mapped to the backend buffer @@ -2429,7 +2436,7 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params, const bool mtp_on_hybrid_qwen = params.ctx_type == LLAMA_CONTEXT_TYPE_MTP && (arch == LLM_ARCH_QWEN3NEXT || arch == LLM_ARCH_QWEN35 || arch == LLM_ARCH_QWEN35MOE || - arch == LLM_ARCH_BAILINGMOE3); + arch == LLM_ARCH_QWEN4EXP || arch == LLM_ARCH_BAILINGMOE3); const bool mtp_on_hybrid_nemotron = params.ctx_type == LLAMA_CONTEXT_TYPE_MTP && arch == LLM_ARCH_NEMOTRON_H_MOE; @@ -2681,8 +2688,10 @@ 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, + /*.lazy_mode =*/ LLAMA_LAZY_MODE_AUTO, /*.main_gpu =*/ 0, + /*.ple_io_threads =*/ 64, + /*.ple_cache_mb =*/ 256, /*.tensor_split =*/ nullptr, /*.progress_callback =*/ nullptr, /*.progress_callback_user_data =*/ nullptr, @@ -2693,6 +2702,8 @@ llama_model_params llama_model_default_params() { /*.no_host =*/ false, /*.no_alloc =*/ false, /*.load_mtp =*/ false, + /*.ple_on_disk =*/ false, + /*.ple_direct_io =*/ true, }; return result; diff --git a/src/llama-model.h b/src/llama-model.h index 0d7352ac500b..38066538ed10 100644 --- a/src/llama-model.h +++ b/src/llama-model.h @@ -672,6 +672,7 @@ 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; diff --git a/src/llama-ple-disk.cpp b/src/llama-ple-disk.cpp new file mode 100644 index 000000000000..dfc118ce5ee7 --- /dev/null +++ b/src/llama-ple-disk.cpp @@ -0,0 +1,320 @@ +#include "llama-ple-disk.h" + +#include "llama-impl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#include +#include +#endif + +struct llama_ple_disk::impl { + std::string fname; + int fd = -1; + bool direct = false; + size_t offs = 0; + ggml_type type = GGML_TYPE_COUNT; + int64_t ne0 = 0; + int64_t nrows = 0; + size_t rs = 0; // bytes per row + size_t block = 4096; // O_DIRECT alignment for offset, length and buffer + ggml_to_float_t to_float = nullptr; + + // direct-mapped cache of raw rows: slot = row & mask, tag = row + size_t n_slots = 0; + size_t mask = 0; + std::vector tags; + std::vector slab; + + // per-gather scratch, reused + std::vector uniq; + std::vector raw; // [uniq.size(), rs] + std::vector> misses; // (row, index into uniq) + uint8_t * bounce0 = nullptr; // main-thread bounce buffer + + std::mutex mtx; // one gather at a time; a model is shared by every context built on it + + // reader pool, started on first use + int32_t n_threads = 1; + std::vector workers; + std::mutex pm; + std::condition_variable cv_work; + std::condition_variable cv_done; + uint64_t gen = 0; + size_t pending = 0; + std::atomic next{0}; + bool stop = false; + + uint64_t st_calls = 0, st_rows = 0, st_uniq = 0, st_hits = 0, st_reads = 0, st_bytes = 0; + double st_ms = 0; + + impl(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p) + : fname(fname), offs(offs), type(type), ne0(ne0), nrows(nrows) { +#if defined(_WIN32) + throw std::runtime_error("llama_ple_disk: not supported on Windows"); +#else + if (ggml_is_quantized(type) && ne0 % ggml_blck_size(type) != 0) { + throw std::runtime_error(format("llama_ple_disk: row of %lld %s elements is not a whole number of blocks", + (long long) ne0, ggml_type_name(type))); + } + rs = ggml_row_size(type, ne0); + if (type != GGML_TYPE_F32) { + to_float = ggml_get_type_traits(type)->to_float; + if (to_float == nullptr) { + throw std::runtime_error(format("llama_ple_disk: no dequantizer for %s", ggml_type_name(type))); + } + } + + direct = p.direct_io; + if (direct) { + fd = open(fname.c_str(), O_RDONLY | O_DIRECT | O_CLOEXEC); + if (fd < 0) { + LLAMA_LOG_WARN("%s: O_DIRECT open of %s failed (%s); falling back to buffered reads\n", + __func__, fname.c_str(), strerror(errno)); + direct = false; + } + } + if (fd < 0) { + fd = open(fname.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { + throw std::runtime_error(format("llama_ple_disk: failed to open %s: %s", fname.c_str(), strerror(errno))); + } + } + + n_threads = std::max(1, p.n_threads); + + if (p.cache_bytes >= rs) { + size_t n = p.cache_bytes / rs; + n_slots = 1; + while (n_slots * 2 <= n) { + n_slots *= 2; + } + mask = n_slots - 1; + tags.assign(n_slots, -1); + slab.resize(n_slots * rs); + } +#endif + } + + ~impl() { +#if !defined(_WIN32) + { + std::lock_guard lk(pm); + stop = true; + } + cv_work.notify_all(); + for (auto & w : workers) { + w.join(); + } + free(bounce0); + if (fd >= 0) { + close(fd); + } +#endif + } + +#if !defined(_WIN32) + size_t bounce_size() const { + return ((rs + block - 1) / block) * block + 2 * block; + } + + uint8_t * alloc_bounce() const { + void * ptr = nullptr; + if (posix_memalign(&ptr, block, bounce_size()) != 0) { + throw std::runtime_error("llama_ple_disk: posix_memalign failed"); + } + return (uint8_t *) ptr; + } + + // read `len` bytes at `off` into `dst`; a short read is only tolerated past `need` + void pread_full(uint8_t * dst, size_t len, off_t off, size_t need) const { + size_t got = 0; + while (got < len) { + const ssize_t r = pread(fd, dst + got, len - got, off + (off_t) got); + if (r < 0) { + if (errno == EINTR) { + continue; + } + GGML_ABORT("llama_ple_disk: pread(%s, %zu @ %lld) failed: %s", + fname.c_str(), len, (long long) off, strerror(errno)); + } + if (r == 0) { + break; // EOF + } + got += (size_t) r; + } + if (got < need) { + GGML_ABORT("llama_ple_disk: short read in %s: %zu of %zu bytes at %lld", + fname.c_str(), got, need, (long long) off); + } + } + + void read_row(int64_t row, uint8_t * dst, uint8_t * bounce) const { + const off_t off = (off_t) offs + (off_t) row * (off_t) rs; + if (!direct) { + pread_full(dst, rs, off, rs); + return; + } + const off_t a0 = off & ~(off_t) (block - 1); + const size_t need = (size_t) (off - a0) + rs; + const size_t len = ((need + block - 1) / block) * block; + pread_full(bounce, len, a0, need); + memcpy(dst, bounce + (off - a0), rs); + } + + void worker() { + uint8_t * bounce = direct ? alloc_bounce() : nullptr; + uint64_t seen = 0; + for (;;) { + { + std::unique_lock lk(pm); + cv_work.wait(lk, [&] { return stop || gen != seen; }); + if (stop) { + break; + } + seen = gen; + } + for (;;) { + const size_t i = next.fetch_add(1); + if (i >= misses.size()) { + break; + } + read_row(misses[i].first, raw.data() + (size_t) misses[i].second * rs, bounce); + } + { + std::lock_guard lk(pm); + if (--pending == 0) { + cv_done.notify_one(); + } + } + } + free(bounce); + } + + void run_misses() { + if (n_threads <= 1 || misses.size() <= 2) { + if (direct && bounce0 == nullptr) { + bounce0 = alloc_bounce(); + } + for (const auto & m : misses) { + read_row(m.first, raw.data() + (size_t) m.second * rs, bounce0); + } + return; + } + if (workers.empty()) { + workers.reserve(n_threads); + for (int32_t i = 0; i < n_threads; ++i) { + workers.emplace_back([this] { worker(); }); + } + } + { + std::lock_guard lk(pm); + next = 0; + pending = workers.size(); + ++gen; + } + cv_work.notify_all(); + std::unique_lock lk(pm); + cv_done.wait(lk, [&] { return pending == 0; }); + } + + void gather(const int32_t * idx, size_t n, float * dst) { + std::lock_guard lk(mtx); + const auto t0 = std::chrono::steady_clock::now(); + + uniq.assign(idx, idx + n); + std::sort(uniq.begin(), uniq.end()); + uniq.erase(std::unique(uniq.begin(), uniq.end()), uniq.end()); + if (!uniq.empty() && (uniq.front() < 0 || (int64_t) uniq.back() >= nrows)) { + GGML_ABORT("llama_ple_disk: row index out of range (%d..%d of %lld rows)", + uniq.front(), uniq.back(), (long long) nrows); + } + + raw.resize(uniq.size() * rs); + misses.clear(); + for (size_t i = 0; i < uniq.size(); ++i) { + if (n_slots) { + const size_t slot = (size_t) uniq[i] & mask; + if (tags[slot] == uniq[i]) { + memcpy(raw.data() + i * rs, slab.data() + slot * rs, rs); + continue; + } + } + misses.emplace_back(uniq[i], (uint32_t) i); + } + + if (!misses.empty()) { + run_misses(); + if (n_slots) { + for (const auto & m : misses) { + const size_t slot = (size_t) m.first & mask; + tags[slot] = m.first; + memcpy(slab.data() + slot * rs, raw.data() + (size_t) m.second * rs, rs); + } + } + } + + for (size_t k = 0; k < n; ++k) { + const size_t i = (size_t) (std::lower_bound(uniq.begin(), uniq.end(), idx[k]) - uniq.begin()); + const uint8_t * src = raw.data() + i * rs; + float * out = dst + k * (size_t) ne0; + if (to_float) { + to_float(src, out, ne0); + } else { + memcpy(out, src, rs); + } + } + + st_calls += 1; + st_rows += n; + st_uniq += uniq.size(); + st_hits += uniq.size() - misses.size(); + st_reads += misses.size(); + st_bytes += misses.size() * (direct ? block : rs); + st_ms += std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); + } +#endif +}; + +llama_ple_disk::llama_ple_disk(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p) + : pimpl(std::make_unique(fname, offs, type, ne0, nrows, p)) {} + +llama_ple_disk::~llama_ple_disk() { + if (pimpl->st_calls) { + LLAMA_LOG_INFO("%s: %llu batches, %llu rows gathered (%llu unique): %llu cache hits, %llu disk reads (%.1f MiB), %.1f ms total\n", + __func__, (unsigned long long) pimpl->st_calls, (unsigned long long) pimpl->st_rows, + (unsigned long long) pimpl->st_uniq, (unsigned long long) pimpl->st_hits, + (unsigned long long) pimpl->st_reads, pimpl->st_bytes / (1024.0 * 1024.0), pimpl->st_ms); + } +} + +void llama_ple_disk::gather(const int32_t * idx, size_t n, float * dst) { +#if defined(_WIN32) + GGML_UNUSED(idx); GGML_UNUSED(n); GGML_UNUSED(dst); + GGML_ABORT("llama_ple_disk: not supported on Windows"); +#else + pimpl->gather(idx, n, dst); +#endif +} + +int64_t llama_ple_disk::n_rows() const { return pimpl->nrows; } +int64_t llama_ple_disk::ne0() const { return pimpl->ne0; } +size_t llama_ple_disk::row_size() const { return pimpl->rs; } + +std::string llama_ple_disk::describe() const { + return format("%s @ %zu: %lld rows x %lld %s (%zu B/row, %.2f GiB), %s I/O, %d threads, row cache %zu MiB", + pimpl->fname.c_str(), pimpl->offs, (long long) pimpl->nrows, (long long) pimpl->ne0, + ggml_type_name(pimpl->type), pimpl->rs, (double) pimpl->nrows * pimpl->rs / (1024.0 * 1024.0 * 1024.0), + pimpl->direct ? "direct" : "buffered", pimpl->n_threads, + pimpl->n_slots * pimpl->rs / (1024 * 1024)); +} diff --git a/src/llama-ple-disk.h b/src/llama-ple-disk.h new file mode 100644 index 000000000000..e8af7c58ded3 --- /dev/null +++ b/src/llama-ple-disk.h @@ -0,0 +1,38 @@ +#pragma once + +#include "ggml.h" + +#include +#include +#include + +// A weight table that stays in the GGUF file. Nothing is mapped or loaded for it at +// model load; each batch reads exactly the rows it gathers, with pread, and hands +// them over dequantized. +// +// Built for the qwen4exp n-gram hash embedding (per_layer_token_embd): 320 M rows of +// 90 bytes, a third of the model's bytes, of which one token touches 16 at unrelated +// hashed offsets. The working set is a few thousand rows per batch, so the table has +// no business being resident -- on a unified-memory machine it competes with the +// weights and the KV cache for the same RAM. +struct llama_ple_disk { + struct params { + int32_t n_threads = 64; // parallel readers: random reads on NVMe need queue depth + size_t cache_bytes = 256u << 20; // direct-mapped cache of raw rows; 0 disables + bool direct_io = true; // O_DIRECT, so the rows never enter the page cache either + }; + + llama_ple_disk(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p); + ~llama_ple_disk(); + + // dequantize rows idx[0..n) into dst, laid out [ne0, n]; aborts on an out-of-range index + void gather(const int32_t * idx, size_t n, float * dst); + + int64_t n_rows() const; + int64_t ne0() const; + size_t row_size() const; + std::string describe() const; + + struct impl; + std::unique_ptr pimpl; +}; diff --git a/src/llama.cpp b/src/llama.cpp index 9c841ee35213..633db658c955 100644 --- a/src/llama.cpp +++ b/src/llama.cpp @@ -318,7 +318,7 @@ static std::pair 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.lazy.mode = params.lazy_mode; ml.print_info(); std::unique_ptr model_ptr(llama_model_create(ml, params)); diff --git a/src/models/dflash.cpp b/src/models/dflash.cpp index 516651ce4074..037723a90258 100644 --- a/src/models/dflash.cpp +++ b/src/models/dflash.cpp @@ -115,10 +115,11 @@ 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); @@ -219,6 +220,9 @@ 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); @@ -290,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]; @@ -321,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]; @@ -354,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); @@ -376,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); @@ -605,8 +615,8 @@ llama_model_dflash::graph::graph(const llama_model & model, const llm_gra for (int il = 0; il < n_layer; ++il) { const auto & layer = model.layers[il]; - ggml_tensor * Kcur = build_lora_mm(layer.wk, inp_g); - ggml_tensor * Vcur = build_lora_mm(layer.wv, inp_g); + ggml_tensor * Kcur = build_lora_mm(layer.wk, inp_g, layer.wk_s); + ggml_tensor * Vcur = build_lora_mm(layer.wv, inp_g, layer.wv_s); Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens); Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens); @@ -688,9 +698,9 @@ llama_model_dflash::graph::graph(const llama_model & model, const llm_gra 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); + ggml_tensor * Qcur = build_lora_mm(layer.wq, noise_norm, layer.wq_s); + ggml_tensor * Kcur = build_lora_mm(layer.wk, noise_norm, layer.wk_s); + ggml_tensor * Vcur = build_lora_mm(layer.wv, noise_norm, layer.wv_s); Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head, n_tokens); Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens); @@ -707,8 +717,8 @@ llama_model_dflash::graph::graph(const llama_model & model, const llm_gra // 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, layer.wo_s, Qcur, Kcur, Vcur, nullptr, layer.attn_sinks, nullptr, kq_scale, il) + : build_attn(inp_attn, layer.wo, NULL, layer.wo_s, 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); diff --git a/src/models/minimax-01.cpp b/src/models/minimax-01.cpp index f14626b2c74f..361114acc327 100644 --- a/src/models/minimax-01.cpp +++ b/src/models/minimax-01.cpp @@ -181,7 +181,7 @@ class llm_graph_input_la : public llm_graph_input_i { 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] diff --git a/src/models/models.h b/src/models/models.h index af60764c2f7f..f285d3d315ac 100644 --- a/src/models/models.h +++ b/src/models/models.h @@ -2275,17 +2275,26 @@ struct llama_model_qwen35 : public llama_model_base { }; +struct llama_ple_disk; + 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; + // Set when the n-gram table is left on disk (--ngram-on-disk): per_layer_tok_embd is + // counted as created but never allocated, mapped or read, and build_ple takes its + // rows from this reader instead of ggml_get_rows. + std::shared_ptr ple_disk; 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: + protected: + // members only, no trunk: graph_mtp builds its own single block + graph(const llama_model & model, const llm_graph_params & params, bool mtp); + // HC replaces every layer norm: residual is [n_embd, hc, n_tokens] ggml_tensor * build_hc_mix( ggml_tensor * x, @@ -2360,9 +2369,12 @@ struct llama_model_qwen4exp : public llama_model_base { int64_t channels, int il); + ggml_tensor * build_inp_ple( + const llama_memory_hybrid_idx_context * mctx_hyb); + ggml_tensor * build_ple( llm_graph_input_rs * inp, - const llama_memory_hybrid_idx_context * mctx_hyb, + ggml_tensor * emb, ggml_tensor * hidden, int il); @@ -2374,6 +2386,10 @@ struct llama_model_qwen4exp : public llama_model_base { const llama_model & model; }; + struct graph_mtp : public graph { + graph_mtp(const llama_model & model, const llm_graph_params & params); + }; + std::unique_ptr build_arch_graph(const llm_graph_params & params) const override; }; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index acfdd5b50038..ca47c3706cae 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -2,6 +2,7 @@ #include "llama-impl.h" #include "llama-memory-hybrid-idx.h" #include "llama-memory-recurrent.h" +#include "llama-ple-disk.h" #include #include @@ -27,6 +28,8 @@ void llama_model_qwen4exp::load_arch_hparams(llama_model_loader & ml) { GGML_ASSERT(hparams.dsv4_hc_mult > 0 && hparams.hc_low_rank > 0); hparams.n_embd_out_impl = hparams.dsv4_hc_mult * hparams.n_embd; + + ml.get_key(LLM_KV_NEXTN_PREDICT_LAYERS, hparams.n_layer_nextn, false); ml.get_key(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, hparams.indexer_n_head); ml.get_key(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, hparams.indexer_head_size); ml.get_key(LLM_KV_ATTENTION_INDEXER_TOP_K, hparams.indexer_top_k); @@ -136,10 +139,31 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { throw std::runtime_error(format("PLE head %u range exceeds the %" PRId64 " table rows", h, ple_rows)); } } - per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), - { hparams.ple_head_dim, ple_rows }, TENSOR_READ_LAZY); + if (params.ple_on_disk) { + // Counted as created but never allocated, mapped or read: each ubatch preads + // exactly the rows it gathers (llm_graph_input_ple::set_input). + llama_ple_disk::params dp; + dp.n_threads = params.ple_io_threads; + dp.cache_bytes = params.ple_cache_mb > 0 ? (size_t) params.ple_cache_mb << 20 : 0; + dp.direct_io = params.ple_direct_io; + + ple_disk = std::make_shared(ml.fnames.at(ple_w.idx), ple_w.offs, + ple_w.tensor->type, ple_w.tensor->ne[0], ple_rows, dp); + LLAMA_LOG_INFO("%s: PLE n-gram table stays on disk: %s\n", __func__, ple_disk->describe().c_str()); + + per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), + { hparams.ple_head_dim, ple_rows }, TENSOR_SKIP); + } else { + per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), + { hparams.ple_head_dim, ple_rows }, TENSOR_READ_LAZY); + } } + // An MTP-only file carries just the draft block. Keep walking the trunk so the + // per-layer bookkeeping still runs, but let its tensors be absent. + const bool mtp_only = hparams.n_layer_nextn > 0 && ml.get_weight("blk.0.hc_attn_norm.weight") == nullptr; + const int trunk_flags = mtp_only ? TENSOR_NOT_REQUIRED : 0; + for (int il = 0; il < n_layer; ++il) { auto & layer = layers[il]; @@ -155,64 +179,223 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { const int64_t conv_dim = key_dim * 2 + value_dim; // two HC modules per layer: before the token mixer, before the MoE - layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { hc_dim }, 0); - layer.hc_attn_down = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN, "weight", il), { hc_dim, hc_lr }, 0); - layer.hc_attn_up = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP, "weight", il), { hc_lr, hc_dim }, 0); - layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, 0); - layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { hc_dim }, 0); - layer.hc_ffn_down = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN, "weight", il), { hc_dim, hc_lr }, 0); - layer.hc_ffn_up = create_tensor(tn(LLM_TENSOR_HC_FFN_UP, "weight", il), { hc_lr, hc_dim }, 0); - layer.hc_ffn_inject = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT, "weight", il), { hc_dim, hc }, 0); + layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { hc_dim }, trunk_flags); + layer.hc_attn_down = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN, "weight", il), { hc_dim, hc_lr }, trunk_flags); + layer.hc_attn_up = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP, "weight", il), { hc_lr, hc_dim }, trunk_flags); + layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, trunk_flags); + layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { hc_dim }, trunk_flags); + layer.hc_ffn_down = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN, "weight", il), { hc_dim, hc_lr }, trunk_flags); + layer.hc_ffn_up = create_tensor(tn(LLM_TENSOR_HC_FFN_UP, "weight", il), { hc_lr, hc_dim }, trunk_flags); + layer.hc_ffn_inject = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT, "weight", il), { hc_dim, hc }, trunk_flags); if (!hparams.is_recr(il)) { // full attention: wq holds [q|gate] interleaved per head - create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, 0); - layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, 0); + create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, trunk_flags); + layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, trunk_flags); - layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, 0); - layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, 0); + layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, trunk_flags); + layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, trunk_flags); const int64_t idx_dim = hparams.indexer_head_size; - layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", il), { n_embd, hparams.indexer_n_head * idx_dim }, 0); - layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", il), { n_embd, idx_dim }, 0); - layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", il), { idx_dim }, 0); - layer.index_k_norm = create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight", il), { idx_dim }, 0); + layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", il), { n_embd, hparams.indexer_n_head * idx_dim }, trunk_flags); + layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", il), { n_embd, idx_dim }, trunk_flags); + layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", il), { idx_dim }, trunk_flags); + layer.index_k_norm = create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight", il), { idx_dim }, trunk_flags); } else { - layer.wqkv = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", il), { n_embd, key_dim * 2 + value_dim }, 0); - layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", il), { n_embd, value_dim }, 0); - layer.ssm_conv1d = create_tensor(tn(LLM_TENSOR_SSM_CONV1D, "weight", il), { hparams.ssm_d_conv, conv_dim }, 0); - layer.ssm_dt = create_tensor(tn(LLM_TENSOR_SSM_DT, "bias", il), { hparams.ssm_dt_rank }, 0); - layer.ssm_a = create_tensor(tn(LLM_TENSOR_SSM_A_NOSCAN, il), { hparams.ssm_dt_rank }, 0); - layer.ssm_beta = create_tensor(tn(LLM_TENSOR_SSM_BETA, "weight", il), { n_embd, n_v_heads }, 0); - layer.ssm_alpha = create_tensor(tn(LLM_TENSOR_SSM_ALPHA, "weight", il), { n_embd, n_v_heads }, 0); - layer.ssm_norm = create_tensor(tn(LLM_TENSOR_SSM_NORM, "weight", il), { head_v_dim }, 0); - layer.ssm_out = create_tensor(tn(LLM_TENSOR_SSM_OUT, "weight", il), { value_dim, n_embd }, 0); + layer.wqkv = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", il), { n_embd, key_dim * 2 + value_dim }, trunk_flags); + layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", il), { n_embd, value_dim }, trunk_flags); + layer.ssm_conv1d = create_tensor(tn(LLM_TENSOR_SSM_CONV1D, "weight", il), { hparams.ssm_d_conv, conv_dim }, trunk_flags); + layer.ssm_dt = create_tensor(tn(LLM_TENSOR_SSM_DT, "bias", il), { hparams.ssm_dt_rank }, trunk_flags); + layer.ssm_a = create_tensor(tn(LLM_TENSOR_SSM_A_NOSCAN, il), { hparams.ssm_dt_rank }, trunk_flags); + layer.ssm_beta = create_tensor(tn(LLM_TENSOR_SSM_BETA, "weight", il), { n_embd, n_v_heads }, trunk_flags); + layer.ssm_alpha = create_tensor(tn(LLM_TENSOR_SSM_ALPHA, "weight", il), { n_embd, n_v_heads }, trunk_flags); + layer.ssm_norm = create_tensor(tn(LLM_TENSOR_SSM_NORM, "weight", il), { head_v_dim }, trunk_flags); + layer.ssm_out = create_tensor(tn(LLM_TENSOR_SSM_OUT, "weight", il), { value_dim, n_embd }, trunk_flags); } if (hparams.is_ple(il)) { - layer.ple_key = create_tensor(tn(LLM_TENSOR_PLE_KEY, "weight", il), { n_embd, hc_dim }, 0); - layer.ple_value = create_tensor(tn(LLM_TENSOR_PLE_VALUE, "weight", il), { n_embd, n_embd }, 0); - layer.ple_norm_key = create_tensor(tn(LLM_TENSOR_PLE_NORM_KEY, "weight", il), { hc_dim }, 0); - layer.ple_norm_query = create_tensor(tn(LLM_TENSOR_PLE_NORM_QUERY, "weight", il), { hc_dim }, 0); - layer.ple_norm_conv = create_tensor(tn(LLM_TENSOR_PLE_NORM_CONV, "weight", il), { hc_dim }, 0); - layer.ple_conv1d = create_tensor(tn(LLM_TENSOR_PLE_CONV1D, "weight", il), { hparams.ple_conv_kernel, hc_dim }, 0); + layer.ple_key = create_tensor(tn(LLM_TENSOR_PLE_KEY, "weight", il), { n_embd, hc_dim }, trunk_flags); + layer.ple_value = create_tensor(tn(LLM_TENSOR_PLE_VALUE, "weight", il), { n_embd, n_embd }, trunk_flags); + layer.ple_norm_key = create_tensor(tn(LLM_TENSOR_PLE_NORM_KEY, "weight", il), { hc_dim }, trunk_flags); + layer.ple_norm_query = create_tensor(tn(LLM_TENSOR_PLE_NORM_QUERY, "weight", il), { hc_dim }, trunk_flags); + layer.ple_norm_conv = create_tensor(tn(LLM_TENSOR_PLE_NORM_CONV, "weight", il), { hc_dim }, trunk_flags); + layer.ple_conv1d = create_tensor(tn(LLM_TENSOR_PLE_CONV1D, "weight", il), { hparams.ple_conv_kernel, hc_dim }, trunk_flags); } - layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", il), { n_embd, n_expert }, 0); - layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", il), { n_ff_exp, n_embd, n_expert }, 0); - create_tensor_gate_up_exps(layer, il, n_embd, n_ff_exp, n_expert, 0); + layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", il), { n_embd, n_expert }, trunk_flags); + layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", il), { n_ff_exp, n_embd, n_expert }, trunk_flags); + create_tensor_gate_up_exps(layer, il, n_embd, n_ff_exp, n_expert, trunk_flags); + + layer.ffn_gate_inp_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP_SHEXP, "weight", il), { n_embd }, trunk_flags); + layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", il), { n_embd, n_ff_shexp }, trunk_flags); + layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", il), { n_embd, n_ff_shexp }, trunk_flags); + layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", il), { n_ff_shexp, n_embd }, trunk_flags); + } + // The MTP draft block sits one past the trunk. It is a full qwen4exp layer plus the + // three nextn tensors, and is skipped unless the file is opened as a draft. + for (int il = n_layer; il < n_layer + (int) hparams.n_layer_nextn; ++il) { + auto & layer = layers[il]; + + const int flags = ml.load_mtp ? 0 : TENSOR_SKIP; - layer.ffn_gate_inp_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP_SHEXP, "weight", il), { n_embd }, 0); - layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", il), { n_embd, n_ff_shexp }, 0); - layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", il), { n_embd, n_ff_shexp }, 0); - layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", il), { n_ff_shexp, n_embd }, 0); + const int64_t n_ff_exp = hparams.n_ff_exp ? hparams.n_ff_exp : n_ff / n_expert_used; + const int64_t n_ff_shexp = hparams.n_ff_shexp ? hparams.n_ff_shexp : n_ff; + const int64_t idx_dim = hparams.indexer_head_size; + + layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { hc_dim }, flags); + layer.hc_attn_down = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN, "weight", il), { hc_dim, hc_lr }, flags); + layer.hc_attn_up = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP, "weight", il), { hc_lr, hc_dim }, flags); + layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, flags); + layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { hc_dim }, flags); + layer.hc_ffn_down = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN, "weight", il), { hc_dim, hc_lr }, flags); + layer.hc_ffn_up = create_tensor(tn(LLM_TENSOR_HC_FFN_UP, "weight", il), { hc_lr, hc_dim }, flags); + layer.hc_ffn_inject = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT, "weight", il), { hc_dim, hc }, flags); + + create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, flags); + layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, flags); + layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, flags); + layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, flags); + + // the draft attends dense, so the indexer weights are present but never read + const int idx_flags = flags | TENSOR_NOT_REQUIRED | TENSOR_SKIP; + layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", il), { n_embd, hparams.indexer_n_head * idx_dim }, idx_flags); + layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", il), { n_embd, idx_dim }, idx_flags); + layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", il), { idx_dim }, idx_flags); + layer.index_k_norm = create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight", il), { idx_dim }, idx_flags); + + layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", il), { n_embd, n_expert }, flags); + layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", il), { n_ff_exp, n_embd, n_expert }, flags); + create_tensor_gate_up_exps(layer, il, n_embd, n_ff_exp, n_expert, flags); + + layer.ffn_gate_inp_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP_SHEXP, "weight", il), { n_embd }, flags); + layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", il), { n_embd, n_ff_shexp }, flags); + layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", il), { n_embd, n_ff_shexp }, flags); + layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", il), { n_ff_shexp, n_embd }, flags); + + // hnorm spans the whole hyper-connection row, enorm just one stream + layer.nextn.enorm = create_tensor(tn(LLM_TENSOR_NEXTN_ENORM, "weight", il), { n_embd }, flags); + layer.nextn.hnorm = create_tensor(tn(LLM_TENSOR_NEXTN_HNORM, "weight", il), { hc_dim }, flags); + layer.nextn.eh_proj = create_tensor(tn(LLM_TENSOR_NEXTN_EH_PROJ, "weight", il), { 2 * n_embd, n_embd }, flags); } + } std::unique_ptr llama_model_qwen4exp::build_arch_graph(const llm_graph_params & params) const { + if (params.gtype == LLM_GRAPH_TYPE_DECODER_MTP) { + return std::make_unique(*this, params); + } return std::make_unique(*this, params); } +// ref: PR 27739 (JJJYmmm), reconciled against this tree's hyper-connection helpers. +// The draft is one block: embedding and handed-over hidden state are each normed, joined +// and projected, then run through the same attention and FFN the trunk uses. +llama_model_qwen4exp::graph_mtp::graph_mtp(const llama_model & model, const llm_graph_params & params) + : graph(model, params, true) { + GGML_ASSERT(hparams.n_layer_nextn == 1 && "qwen4exp MTP supports a single block"); + GGML_ASSERT(ubatch.token && "qwen4exp MTP needs token input"); + + const int64_t hc = hparams.dsv4_hc_mult; + const int64_t n_embd = hparams.n_embd; + + const int il = hparams.n_layer(); + const auto & layer = model.layers[il]; + + GGML_ASSERT(layer.nextn.eh_proj && "MTP block is missing nextn.eh_proj"); + GGML_ASSERT(layer.nextn.enorm && "MTP block is missing nextn.enorm"); + GGML_ASSERT(layer.nextn.hnorm && "MTP block is missing nextn.hnorm"); + + int sections[4]; + std::copy(std::begin(hparams.rope_sections), std::begin(hparams.rope_sections) + 4, sections); + + auto inp = std::make_unique(hparams.n_embd_out()); + + inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_tokens); + ggml_set_input(inp->tokens); + + inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.n_embd_out(), n_tokens); + ggml_set_input(inp->embd); + + inp->h = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.n_embd_out(), n_tokens); + ggml_set_input(inp->h); + ggml_set_name(inp->h, "mtp_h_input"); + + ggml_tensor * tok_embd = ggml_get_rows(ctx0, model.tok_embd, inp->tokens); + cb(tok_embd, "mtp_tok_embd", il); + + ggml_tensor * h = inp->h; + res->add_input(std::move(inp)); + + ggml_tensor * inp_pos = build_inp_pos(); + ggml_tensor * inp_out_ids = build_inp_out_ids(); + // The draft block is the only layer in an MTP context and attends dense, so it takes a + // plain attention input. create_memory gives that context an attention-only filter. + auto * inp_attn = build_attn_inp_kv(); + + // Grouped RMSNorm: each hc stream is normed over its own n_embd, and gamma is applied + // across the whole [hc*n_embd] row. Norming the full row instead couples the streams + // through a shared scale, which is not what the head was trained on and measurably + // costs draft acceptance. + ggml_tensor * h_norm = ggml_reshape_3d(ctx0, h, n_embd, hc, n_tokens); + h_norm = ggml_rms_norm(ctx0, h_norm, hparams.f_norm_rms_eps); + h_norm = ggml_reshape_2d(ctx0, h_norm, hc*n_embd, n_tokens); + h_norm = ggml_mul(ctx0, h_norm, layer.nextn.hnorm); + h_norm = ggml_reshape_3d(ctx0, h_norm, n_embd, hc, n_tokens); + cb(h_norm, "mtp_hnorm", il); + + // every stream sees the same embedding term + ggml_tensor * e_norm = build_norm(tok_embd, layer.nextn.enorm, nullptr, LLM_NORM_RMS, il); + e_norm = ggml_repeat_4d(ctx0, ggml_reshape_3d(ctx0, e_norm, n_embd, 1, n_tokens), n_embd, hc, n_tokens, 1); + cb(e_norm, "mtp_enorm", il); + + // fc_embedding(e) + fc_hidden(h) is one projection of the concatenation; the converter + // merges the two checkpoint tensors into this single eh_proj + ggml_tensor * inpL = build_lora_mm(layer.nextn.eh_proj, + ggml_concat(ctx0, e_norm, h_norm, 0), layer.nextn.eh_proj_s); + cb(inpL, "mtp_eh_proj", il); + + ggml_tensor * inject = nullptr; + ggml_tensor * cur = build_hc_mix(inpL, + layer.hc_attn_norm, layer.hc_attn_down, layer.hc_attn_up, layer.hc_attn_inject, &inject, il); + cb(cur, "mtp_hc_attn_pre", il); + + // dense attention for the draft: a QSA indexer would need a cache of its own, and one + // block spends its time reading weights rather than attending + cur = build_layer_attn(inp_attn, nullptr, cur, inp_pos, sections, il); + inpL = build_hc_combine(inpL, cur, inject, il); + cb(inpL, "mtp_hc_attn_post", il); + + cur = build_hc_mix(inpL, + layer.hc_ffn_norm, layer.hc_ffn_down, layer.hc_ffn_up, layer.hc_ffn_inject, &inject, il); + cb(cur, "mtp_hc_ffn_pre", il); + + cur = build_layer_ffn(cur, il); + cb(cur, "mtp_ffn_out", il); + + inpL = build_hc_combine(inpL, cur, inject, il); + cb(inpL, "mtp_l_out", il); + + ggml_tensor * flat = ggml_reshape_2d(ctx0, inpL, hc*n_embd, n_tokens); + + // a chained head reads the streams back the way the trunk hands them over + res->t_h_nextn = flat; + + if (inp_out_ids) { + flat = ggml_get_rows(ctx0, flat, inp_out_ids); + inpL = ggml_reshape_3d(ctx0, flat, n_embd, hc, n_outputs); + } + + cur = build_hc_mix(inpL, model.hc_head_norm, model.hc_head_down, model.hc_head_up, nullptr, nullptr, -1); + cb(cur, "result_norm", -1); + res->t_embd = cur; + + cur = build_lora_mm(model.output, cur, model.output_s); + cb(cur, "result_output", -1); + res->t_logits = cur; + + ggml_build_forward_expand(gf, cur); +} + // Hyper-connections keep hc parallel residual streams [n_embd, hc, T] in place of layer norms. // Returns the mixed [n_embd, T] stream; `inject` gets the [hc, T] scatter weights. ggml_tensor * llama_model_qwen4exp::graph::build_hc_mix( @@ -285,6 +468,10 @@ ggml_tensor * llama_model_qwen4exp::graph::build_hc_combine( return cur; } +// members only: graph_mtp builds a single block instead of the trunk +llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_params & params, bool) : + llm_build_delta_net_base(params), model(model) {} + llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_params & params) : llm_build_delta_net_base(params), model(model) { const int64_t hc = hparams.dsv4_hc_mult; @@ -296,6 +483,7 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa ggml_tensor * inpL = build_inp_embd(model.tok_embd); cb(inpL, "model.input_embed", -1); + ggml_build_forward_expand(gf, inpL); auto * inp = build_inp_mem_hybrid(); @@ -312,6 +500,13 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa ggml_tensor * inp_pos = build_inp_pos(); ggml_tensor * inp_out_ids = build_inp_out_ids(); + ggml_tensor * ple_emb = nullptr; + if (hparams.ple_n_heads > 0) { + ple_emb = build_inp_ple(mctx_hyb); + // make sure ple_emb and build_inp_embd are in the same graph split + ggml_build_forward_expand(gf, ple_emb); + } + // the wide residual starts as hc identical copies of the embedding ggml_tensor * res_hc = ggml_repeat_4d(ctx0, ggml_reshape_3d(ctx0, inpL, n_embd, 1, n_tokens), @@ -322,7 +517,7 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa res->t_layer_inp[il] = res_hc; if (hparams.is_ple(il)) { - res_hc = build_ple(inp->get_recr(), mctx_hyb, res_hc, il); + res_hc = build_ple(inp->get_recr(), ple_emb, res_hc, il); } ggml_tensor * inject = nullptr; @@ -341,7 +536,8 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa cur = build_layer_attn(inp->get_attn(), mctx_hyb, cur, inp_pos, sections, il); } - if (il == n_layer - 1 && inp_out_ids) { + if (il == n_layer - 1 && inp_out_ids && + (!cparams.embeddings_nextn || cparams.embeddings_nextn_masked)) { // everything below is per token, so drop the rows that produce no output cur = ggml_get_rows(ctx0, cur, inp_out_ids); inject = ggml_get_rows(ctx0, inject, inp_out_ids); @@ -369,6 +565,26 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa cb(res_hc, "l_last", il); } + // Hand the wide residual to an MTP head once the last layer has folded its attention and + // FFN into it: the head was trained on the trunk's final residual, not on the last layer's + // input. The final mix below is the output norm of the LM head, so capture before it. The + // draft reads all hc streams, which is what its hnorm is sized for. res_hc is passed as is + // rather than reshaped: a bare view is not a node the scheduler places, and the extraction + // is a flat copy, for which [n_embd, hc, T] and [hc*n_embd, T] are identical. + // + // An unmasked request wants every position, so the last layer ran uncropped above and the + // rows for the final mix are gathered only after this hand-over. + if (cparams.embeddings_nextn) { + res->t_h_nextn = res_hc; + cb(res->t_h_nextn, "h_nextn", -1); + + if (!cparams.embeddings_nextn_masked && inp_out_ids) { + res_hc = ggml_reshape_2d(ctx0, res_hc, n_embd*hc, res_hc->ne[2]); + res_hc = ggml_get_rows(ctx0, res_hc, inp_out_ids); + res_hc = ggml_reshape_3d(ctx0, res_hc, n_embd, hc, res_hc->ne[1]); + } + } + // the final mixer is the output norm: there is no separate one ggml_tensor * cur = build_hc_mix(res_hc, model.hc_head_norm, model.hc_head_down, model.hc_head_up, @@ -699,8 +915,9 @@ ggml_tensor * llama_model_qwen4exp::graph::build_layer_attn( const int64_t n_embd_head = hparams.n_embd_head_v(); GGML_ASSERT(n_embd_head == hparams.n_embd_head_k()); - // indexer reads the same block input as q/k/v; no cache or no ratio means dense - const bool qsa = mctx_hyb->get_idx() != nullptr && hparams.dsv4_compress_ratios[il] > 0; + // indexer reads the same block input as q/k/v; no context (the MTP draft attends + // dense, see build_layer_attn's caller in graph_mtp), no cache, or no ratio means dense + const bool qsa = mctx_hyb != nullptr && mctx_hyb->get_idx() != nullptr && hparams.dsv4_compress_ratios[il] > 0; ggml_tensor * top_k = qsa ? build_qsa_top_k(mctx_hyb, cur, inp_pos, inp->get_kq_mask(), sections, il) : nullptr; @@ -965,10 +1182,26 @@ class llm_graph_input_ple : public llm_graph_input_i { bool can_reuse(const llm_graph_params & params) override { mctx = static_cast(params.mctx)->get_attn(); - return rows->ne[0] == (int64_t) pmodel.hparams.ple_n_heads * params.ubatch.n_tokens; + + const int64_t n_want = (int64_t) pmodel.hparams.ple_n_heads * params.ubatch.n_tokens; + + // exactly one of the two is built: `rows` when the table is a tensor and the + // graph does the get_rows, `embd` when --ngram-on-disk gathered it in set_input. + // Dereferencing `rows` unconditionally segfaults on every on-disk decode. + if (rows) { + return rows->ne[0] == n_want; // I32 [n_heads*n_tokens] + } + if (embd) { + return embd->ne[1] == n_want; // F32 [ple_head_dim, n_heads*n_tokens] + } + return false; } ggml_tensor * rows = nullptr; // I32 [ple_n_heads * n_tokens] + ggml_tensor * embd = nullptr; // F32 [ple_head_dim, ple_n_heads * n_tokens]: the gathered rows, when the table is on disk + + std::vector embd_buf; + std::vector rows_global; const llama_model_qwen4exp & pmodel; @@ -1039,6 +1272,19 @@ void llm_graph_input_ple::set_input(const llama_ubatch * ubatch) { } } + if (pmodel.ple_disk) { + // idx is already exactly what the reader wants: one global row per (token, head), + // token-major, with ple_head_offsets folded in by the loop above. The previous + // version read it head-major and added the offset a second time, which is only + // correct for a table split per head -- on a joined per_layer_token_embd it walks + // past the end (rows beyond ple_rows) and gather() aborts. + GGML_ASSERT(embd != nullptr && rows == nullptr); + embd_buf.resize(idx.size() * (size_t) hp.ple_head_dim); + pmodel.ple_disk->gather(idx.data(), idx.size(), embd_buf.data()); + ggml_backend_tensor_set(embd, embd_buf.data(), 0, embd_buf.size() * sizeof(float)); + return; + } + ggml_backend_tensor_set(rows, idx.data(), 0, idx.size()*ggml_element_size(rows)); } @@ -1090,28 +1336,47 @@ ggml_tensor * llama_model_qwen4exp::graph::build_conv_state_at( return conv_input; } -ggml_tensor * llama_model_qwen4exp::graph::build_ple( - llm_graph_input_rs * inp, - const llama_memory_hybrid_idx_context * mctx_hyb, - ggml_tensor * hidden, - int il) { - const int64_t hc = hparams.dsv4_hc_mult; - const int64_t hc_dim = hc * n_embd; +ggml_tensor * llama_model_qwen4exp::graph::build_inp_ple( + const llama_memory_hybrid_idx_context * mctx_hyb) { const int64_t n_heads = hparams.ple_n_heads; // the attention cells see every ubatch regardless of the layer types auto ple_inp = std::make_unique( static_cast(model), mctx_hyb->get_attn()); - ple_inp->rows = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_heads * n_tokens); - ggml_set_input(ple_inp->rows); - ggml_tensor * rows = ple_inp->rows; + const auto & qmodel = static_cast(model); + ggml_tensor * rows = nullptr; + ggml_tensor * emb = nullptr; + if (qmodel.ple_disk) { + // table on disk: the gather happened in set_input and arrives as an F32 input, + // token-major, which reshapes to what the per-head gathers + concat produce + ple_inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.ple_head_dim, n_heads * n_tokens); + ggml_set_input(ple_inp->embd); + emb = ple_inp->embd; + } else { + ple_inp->rows = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_heads * n_tokens); + ggml_set_input(ple_inp->rows); + rows = ple_inp->rows; + } res->add_input(std::move(ple_inp)); - // gather then flatten the heads: get_rows lays the head dimension out slowest, as the reference does - ggml_tensor * emb = ggml_get_rows(ctx0, model.per_layer_tok_embd, rows); + if (rows != nullptr) { + // gather then flatten the heads: get_rows lays the head dimension out slowest, as the reference does + emb = ggml_get_rows(ctx0, model.per_layer_tok_embd, rows); + } emb = ggml_reshape_2d(ctx0, emb, hparams.ple_head_dim * n_heads, n_tokens); - cb(emb, "ple_embd", il); + cb(emb, "ple_embd", -1); + + return emb; +} + +ggml_tensor * llama_model_qwen4exp::graph::build_ple( + llm_graph_input_rs * inp, + ggml_tensor * emb, + ggml_tensor * hidden, + int il) { + const int64_t hc = hparams.dsv4_hc_mult; + const int64_t hc_dim = hc * n_embd; ggml_tensor * key = build_lora_mm(model.layers[il].ple_key, emb); ggml_tensor * value = build_lora_mm(model.layers[il].ple_value, emb); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b9f9d4b78af2..fe3d14ffc552 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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() diff --git a/tests/test-alloc.cpp b/tests/test-alloc.cpp index 6d5428493e70..8f1a98aa03c3 100644 --- a/tests/test-alloc.cpp +++ b/tests/test-alloc.cpp @@ -19,6 +19,8 @@ struct dummy_backend_context { size_t alignment = 8; ggml_backend_buffer_i buffer_interface; + ggml_backend_device device; + ggml_backend backend; std::vector buffers; size_t allocated_total() const { @@ -83,7 +85,27 @@ static void dummy_backend_buffer_get_tensor(ggml_backend_buffer_t, const ggml_te static void dummy_backend_buffer_clear(ggml_backend_buffer_t, uint8_t) {} -// dummy_backend (not really a full backend, just provides what gallocr needs) +// ggml_backend_device interface + +static enum ggml_backend_dev_type dummy_backend_device_get_type(ggml_backend_dev_t) { + return GGML_BACKEND_DEVICE_TYPE_CPU; +} + +static bool dummy_backend_device_supports_op(ggml_backend_dev_t, const ggml_tensor *) { + return true; +} + +static bool dummy_backend_device_supports_buft(ggml_backend_dev_t device, ggml_backend_buffer_type_t buft) { + return device->context == buft->context; +} + +// ggml_backend interface + +static const char * dummy_backend_get_name(ggml_backend_t) { + return "dummy_backend"; +} + +// dummy_backend struct dummy_backend { std::unique_ptr context; @@ -104,6 +126,16 @@ static dummy_backend dummy_backend_init(size_t max_buffer_size, size_t alignment b.context->buffer_interface.get_tensor = dummy_backend_buffer_get_tensor; b.context->buffer_interface.clear = dummy_backend_buffer_clear; + b.context->device.context = b.context.get(); + b.context->device.iface.get_type = dummy_backend_device_get_type; + b.context->device.iface.supports_op = dummy_backend_device_supports_op; + b.context->device.iface.supports_buft = dummy_backend_device_supports_buft; + + b.context->backend.context = b.context.get(); + b.context->backend.device = &b.context->device; + b.context->backend.iface.get_name = dummy_backend_get_name; + + b.buffer_type.device = &b.context->device; b.buffer_type.context = b.context.get(); b.buffer_type.iface.get_name = dummy_backend_buffer_type_get_name; b.buffer_type.iface.alloc_buffer = dummy_backend_buffer_type_alloc_buffer; @@ -583,6 +615,41 @@ static void test_reallocation() { } } +static void test_backend_graph_optimize(ggml_backend_t, ggml_cgraph * graph, ggml_backend_graph_optimize_params * params) { + GGML_ASSERT(graph->n_nodes == 3); + params->add_alloc_dep(params->user_data, graph->nodes[0], graph->nodes[2]); +} + +static bool graph_reuses_allocation(bool add_alloc_dep) { + auto [ctx, graph, ctx_ptr] = make_context(); + + ggml_tensor * x[4]; + x[0] = make_input_with_size(ctx, 16); + x[1] = ggml_scale(ctx, x[0], 2.0f); + x[2] = ggml_scale(ctx, x[1], 2.0f); + x[3] = ggml_scale(ctx, x[2], 2.0f); + + ggml_set_output(x[3]); + ggml_build_forward_expand(graph, x[3]); + + dummy_backend backend = dummy_backend_init(SIZE_MAX); + if (add_alloc_dep) { + backend.context->backend.iface.graph_optimize = test_backend_graph_optimize; + } + + ggml_backend_t backend_ptr = &backend.context->backend; + ggml_backend_buffer_type_t buft = &backend.buffer_type; + ggml_backend_sched_ptr sched(ggml_backend_sched_new(&backend_ptr, &buft, 1, 8, false, true)); + GGML_ASSERT(ggml_backend_sched_alloc_graph(sched.get(), graph)); + + return x[1]->data == x[2]->data; +} + +static void test_graph_optimize_alloc_dep() { + GGML_ASSERT(graph_reuses_allocation(false)); + GGML_ASSERT(!graph_reuses_allocation(true)); +} + static void run(const char * name, void (*f)()) { printf("%s ", name); fflush(stdout); @@ -604,5 +671,6 @@ int main() { run("test_multiple_buffer_types", test_multiple_buffer_types); run("test_buffer_size_zero", test_buffer_size_zero); run("test_reallocation", test_reallocation); + run("test_graph_optimize_alloc_dep", test_graph_optimize_alloc_dep); return 0; } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index df0b9aa66689..996b88db296e 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -2243,6 +2243,63 @@ struct test_swiglu_oai : public test_case { } }; +struct test_swiglu_clamp : public test_case { + const ggml_type type; + const std::array ne_a; + int v; // view (1 : non-contiguous a) + float limit; + + std::string vars() override { + return VARS_TO_STR4(type, ne_a, v, limit); + } + + test_swiglu_clamp(ggml_type type = GGML_TYPE_F32, + std::array ne_a = {128, 2, 2, 2}, + int v = 0, + float limit = 7.0f) + : type(type), ne_a(ne_a), v(v), limit(limit) {} + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * a; + ggml_tensor * b; + if (v & 1) { + auto ne = ne_a; ne[0] *= 3; + a = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_param(a); + ggml_set_name(a, "a"); + + a = ggml_view_4d(ctx, a, ne_a[0], ne_a[1], ne_a[2], ne_a[3], a->nb[1], a->nb[2], a->nb[3], 0); + ggml_set_name(a, "view_of_a"); + + b = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_param(b); + ggml_set_name(b, "b"); + + b = ggml_view_4d(ctx, b, ne_a[0], ne_a[1], ne_a[2], ne_a[3], b->nb[1], b->nb[2], b->nb[3], 0); + ggml_set_name(b, "view_of_b"); + } else { + a = ggml_new_tensor(ctx, type, 4, ne_a.data()); + ggml_set_param(a); + ggml_set_name(a, "a"); + + b = ggml_new_tensor(ctx, type, 4, ne_a.data()); + ggml_set_param(b); + ggml_set_name(b, "b"); + } + + ggml_tensor * out = ggml_swiglu_clamp(ctx, a, b, limit); + ggml_set_name(out, "out"); + + return out; + } + + void initialize_tensors(ggml_context * ctx) override { + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { + init_tensor_uniform(t, -150.f, 150.f); + } + } +}; + // GGML_OP_GET_ROWS struct test_get_rows : public test_case { const ggml_type type; @@ -6380,6 +6437,9 @@ struct test_mul_mat_vec_fusion : public test_case { constexpr float alpha = 1.702f; constexpr float limit = 7.0f; out = ggml_swiglu_oai(ctx, ffn_gate, ffn_up, alpha, limit); + } else if (glu_op == GGML_GLU_OP_SWIGLU_CLAMP) { + constexpr float limit = 10.0f; + out = ggml_swiglu_clamp(ctx, ffn_gate, ffn_up, limit); } else { out = ggml_glu_split(ctx, ffn_gate, ffn_up, glu_op); } @@ -8376,8 +8436,7 @@ static std::vector> make_test_cases_eval() { for (ggml_type type : {GGML_TYPE_F16, GGML_TYPE_F32}) { for (int v : {0, 1}) { for (int op = 0; op < GGML_GLU_OP_COUNT; op++) { - if (op == GGML_GLU_OP_SWIGLU_OAI) { - // SWIGLU_OAI is handled separately + if (op == GGML_GLU_OP_SWIGLU_OAI || op == GGML_GLU_OP_SWIGLU_CLAMP) { continue; } @@ -8400,6 +8459,14 @@ static std::vector> make_test_cases_eval() { } } + for (ggml_type type : {GGML_TYPE_F16, GGML_TYPE_F32}) { + for (int v : {0, 1}) { + for (float limit : {2.0f, 10.0f}) { + test_cases.emplace_back(new test_swiglu_clamp(type, { 128, 2, 2, 2 }, v, limit)); + } + } + } + for (ggml_type type : {GGML_TYPE_F32, GGML_TYPE_Q4_0}) { test_cases.emplace_back(new test_get_rows(type, 300*256, 5, 4, 1, 2, false)); test_cases.emplace_back(new test_get_rows(type, 256, 80000, 70000, 2, 1, false)); @@ -8785,6 +8852,7 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_conv_transpose_2d({3, 2, 3, 1}, {2, 2, 1, 3}, 1, kernel_type)); test_cases.emplace_back(new test_conv_transpose_2d({10, 10, 9, 1}, {3, 3, 1, 9}, 2, kernel_type)); test_cases.emplace_back(new test_conv_transpose_2d({129, 63, 35, 1}, {3, 3, 48, 35}, 1, kernel_type)); + test_cases.emplace_back(new test_conv_transpose_2d({10, 10, 9, 2}, {3, 3, 1, 9}, 2, kernel_type)); // for multiple batches } test_cases.emplace_back(new test_count_equal(GGML_TYPE_F32, {4, 500, 1, 1})); @@ -9366,7 +9434,12 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_BF16, GGML_TYPE_F32, 16, 16, b, 50, 200, 64)); } - test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_F16, GGML_TYPE_F32, 1, 1, false, 8, 16, 1)); + // For issue 27873 + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_IQ2_XXS, GGML_TYPE_F32, 1, 1, false, 1, 8192, 4096)); + + for (int k : {1, 63, 65}) { + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_F16, GGML_TYPE_F32, 1, 1, false, 8, 16, k)); + } test_cases.emplace_back(new test_mul_mat_id_fusion(GGML_TYPE_F16, GGML_TYPE_F32, 16, 16, false, 32, 32, 32, 3)); // gpt-oss issue with Vulkan mmq_id @@ -10020,7 +10093,7 @@ static std::vector> make_test_cases_eval() { if (!with_gate && !with_bias) { continue; } - for (ggml_glu_op glu_op : {GGML_GLU_OP_SWIGLU, GGML_GLU_OP_GEGLU}) { + for (ggml_glu_op glu_op : {GGML_GLU_OP_SWIGLU, GGML_GLU_OP_GEGLU, GGML_GLU_OP_SWIGLU_CLAMP}) { if (!with_bias && glu_op == GGML_GLU_OP_SWIGLU_OAI) { continue; } @@ -10035,7 +10108,7 @@ static std::vector> make_test_cases_eval() { use_id, 16, 8, b, with_bias, with_gate, with_lane_scale)); test_cases.emplace_back(new test_mul_mat_vec_fusion(type, glu_op, 1, 32, 256, use_id, 16, 8, b, with_bias, with_gate, with_lane_scale, {1, 1})); - if (!use_id && with_gate && !with_bias) { + if (!use_id && with_gate && !with_bias && glu_op != GGML_GLU_OP_SWIGLU_CLAMP) { // small multi-token batches (speculative decoding / MTP verify) for (int64_t m_batch : { 2, 4, 8 }) { test_cases.emplace_back(new test_mul_mat_vec_fusion(type, glu_op, m_batch, 32, 256, @@ -10050,6 +10123,11 @@ static std::vector> make_test_cases_eval() { } } + for (bool b : {false, true}) { + test_cases.emplace_back(new test_mul_mat_vec_fusion(GGML_TYPE_IQ2_S, GGML_GLU_OP_SWIGLU_CLAMP, 1, 32, 256, + true, 16, 8, b, false, true, false)); + } + for (auto gate : {GATING_FUNC_SOFTMAX, GATING_FUNC_SIGMOID, GATING_FUNC_SOFTMAX_WEIGHT, GATING_FUNC_SQRT_SOFTPLUS}) { for (bool with_norm : {false, true}) { for (bool bias_probs : {false, true}) { diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp index d58d90952eb0..35a3286e4a1b 100644 --- a/tests/test-llama-archs.cpp +++ b/tests/test-llama-archs.cpp @@ -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 get_tokens(const uint32_t n_tokens, const uint32_t n_vocab, const size_t seed){ @@ -82,7 +82,7 @@ static std::vector 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; @@ -256,10 +256,12 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { ms.add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, std::vector(n_layer, 4)); } - ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, arch == LLM_ARCH_MINIMAX_M3 || arch == LLM_ARCH_DEEPSEEK4 ? n_head : uint32_t(1)); + // 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(64)); + 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)); @@ -762,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]; diff --git a/tests/test-save-load-state.cpp b/tests/test-save-load-state.cpp index 6e93ce6fb8da..0ceab7c5452b 100644 --- a/tests/test-save-load-state.cpp +++ b/tests/test-save-load-state.cpp @@ -3,8 +3,12 @@ #include "log.h" #include "llama-cpp.h" +#include #include +#include +#include #include +#include #include 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 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 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; } diff --git a/tools/cli/README.md b/tools/cli/README.md index 163ee4fbaf0a..b874d020730d 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -59,7 +59,7 @@ | `--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)
(env: LLAMA_ARG_MMAP) | | `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available
(env: LLAMA_ARG_DIO) | | `-lm, --load-mode MODE` | model loading mode (default: auto)
- auto: mmap, unless a device does not support it
- none: no special loading mode
- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)
- mlock: force system to keep model in RAM rather than swapping or compressing
- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing
- dio: use DirectIO if available

(env: LLAMA_ARG_LOAD_MODE) | -| `--tensor-read-lazy MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)
- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)
- auto: on, but only for tensors larger than 4 GiB
- off: always keep them resident
(env: LLAMA_ARG_TENSOR_READ_LAZY) | +| `-lzm, --lazy-mode MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)
- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)
- auto: on, but only for tensors larger than 4 GiB
- off: always keep them resident
(env: LLAMA_ARG_LAZY_MODE) | | `--numa TYPE` | attempt optimizations that help on some NUMA systems
- distribute: spread execution evenly over all nodes
- isolate: only spawn threads on CPUs on the node that execution started on
- numactl: use the CPU map provided by numactl
if run without this previously, it is recommended to drop the system page cache before using this
see https://github.com/ggml-org/llama.cpp/issues/1437
(env: LLAMA_ARG_NUMA) | | `-dev, --device ` | comma-separated list of devices to use for offloading (none = don't offload)
use --list-devices to see a list of available devices
(env: LLAMA_ARG_DEVICE) | | `--list-devices` | print list of available devices and exit | diff --git a/tools/completion/README.md b/tools/completion/README.md index 0cd86bac70fc..145be77e31c9 100644 --- a/tools/completion/README.md +++ b/tools/completion/README.md @@ -142,7 +142,7 @@ 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)
(env: LLAMA_ARG_MMAP) | | `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available
(env: LLAMA_ARG_DIO) | | `-lm, --load-mode MODE` | model loading mode (default: auto)
- auto: mmap, unless a device does not support it
- none: no special loading mode
- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)
- mlock: force system to keep model in RAM rather than swapping or compressing
- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing
- dio: use DirectIO if available

(env: LLAMA_ARG_LOAD_MODE) | -| `--tensor-read-lazy MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)
- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)
- auto: on, but only for tensors larger than 4 GiB
- off: always keep them resident
(env: LLAMA_ARG_TENSOR_READ_LAZY) | +| `-lzm, --lazy-mode MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)
- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)
- auto: on, but only for tensors larger than 4 GiB
- off: always keep them resident
(env: LLAMA_ARG_LAZY_MODE) | | `--numa TYPE` | attempt optimizations that help on some NUMA systems
- distribute: spread execution evenly over all nodes
- isolate: only spawn threads on CPUs on the node that execution started on
- numactl: use the CPU map provided by numactl
if run without this previously, it is recommended to drop the system page cache before using this
see https://github.com/ggml-org/llama.cpp/issues/1437
(env: LLAMA_ARG_NUMA) | | `-dev, --device ` | comma-separated list of devices to use for offloading (none = don't offload)
use --list-devices to see a list of available devices
(env: LLAMA_ARG_DEVICE) | | `--list-devices` | print list of available devices and exit | diff --git a/tools/llama-bench/README.md b/tools/llama-bench/README.md index 42cb14859f07..8adc56514d10 100644 --- a/tools/llama-bench/README.md +++ b/tools/llama-bench/README.md @@ -67,6 +67,7 @@ test parameters: -nkvo, --no-kv-offload <0|1> (default: 0) -fa, --flash-attn (default: auto) -dev, --device (default: auto) + -lzm, --lazy-mode (default: auto) -mmp, --mmap <0|1> (DEPRECATED IN FAVOUR OF --load-mode) -dio, --direct-io <0|1> (DEPRECATED IN FAVOUR OF --load-mode) -embd, --embeddings <0|1> (default: 0) diff --git a/tools/llama-bench/llama-bench.cpp b/tools/llama-bench/llama-bench.cpp index bc14d15c7968..1fff21f701e2 100644 --- a/tools/llama-bench/llama-bench.cpp +++ b/tools/llama-bench/llama-bench.cpp @@ -271,6 +271,19 @@ static const char * split_mode_str(llama_split_mode mode) { } } +static const char * lazy_mode_str(llama_lazy_mode mode) { + switch (mode) { + case LLAMA_LAZY_MODE_OFF: + return "off"; + case LLAMA_LAZY_MODE_AUTO: + return "auto"; + case LLAMA_LAZY_MODE_ON: + return "on"; + default: + GGML_ABORT("invalid lazy mode"); + } +} + static std::string pair_str(const std::pair & p) { static char buf[32]; snprintf(buf, sizeof(buf), "%d,%d", p.first, p.second); @@ -341,6 +354,7 @@ struct cmd_params { std::vector n_cpu_moe; std::vector split_mode; std::vector load_mode; + std::vector lazy_mode; std::vector main_gpu; std::vector no_kv_offload; std::vector flash_attn; @@ -385,6 +399,7 @@ static const cmd_params cmd_params_defaults = { /* n_cpu_moe */ { 0 }, /* split_mode */ { LLAMA_SPLIT_MODE_LAYER }, /* load_mode */ { LLAMA_LOAD_MODE_AUTO }, + /* lazy_mode */ { LLAMA_LAZY_MODE_AUTO }, /* main_gpu */ { 0 }, /* no_kv_offload */ { false }, /* flash_attn */ { LLAMA_FLASH_ATTN_TYPE_AUTO }, @@ -460,6 +475,7 @@ static void print_usage(int /* argc */, char ** argv) { printf(" -fa, --flash-attn (default: %s)\n", join(transform_to_str(cmd_params_defaults.flash_attn, llama_flash_attn_type_name), ",").c_str()); printf(" -dev, --device (default: auto)\n"); printf(" -lm, --load-mode (default: %s)\n", join(transform_to_str(cmd_params_defaults.load_mode, llama_load_mode_name), ",").c_str()); + printf(" -lzm, --lazy-mode (default: %s)\n", join(transform_to_str(cmd_params_defaults.lazy_mode, lazy_mode_str), ",").c_str()); printf(" -mmp, --mmap <0|1> (DEPRECATED IN FAVOUR OF --load-mode)\n"); printf(" -dio, --direct-io <0|1> (DEPRECATED IN FAVOUR OF --load-mode)\n"); printf(" -embd, --embeddings <0|1> (default: %s)\n", join(cmd_params_defaults.embeddings, ",").c_str()); @@ -786,6 +802,32 @@ static cmd_params parse_cmd_params(int argc, char ** argv) { break; } params.load_mode.insert(params.load_mode.end(), modes.begin(), modes.end()); + } else if (arg == "-lzm" || arg == "--lazy-mode") { + if (++i >= argc) { + invalid_param = true; + break; + } + auto p = string_split(argv[i], split_delim); + + std::vector modes; + for (const auto & m : p) { + llama_lazy_mode mode; + if (m == "on") { + mode = LLAMA_LAZY_MODE_ON; + } else if (m == "auto") { + mode = LLAMA_LAZY_MODE_AUTO; + } else if (m == "off") { + mode = LLAMA_LAZY_MODE_OFF; + } else { + invalid_param = true; + break; + } + modes.push_back(mode); + } + if (invalid_param) { + break; + } + params.lazy_mode.insert(params.lazy_mode.end(), modes.begin(), modes.end()); } else if (arg == "-mg" || arg == "--main-gpu") { if (++i >= argc) { invalid_param = true; @@ -1137,6 +1179,9 @@ static cmd_params parse_cmd_params(int argc, char ** argv) { if (params.load_mode.empty()) { params.load_mode = cmd_params_defaults.load_mode; } + if (params.lazy_mode.empty()) { + params.lazy_mode = cmd_params_defaults.lazy_mode; + } if (params.main_gpu.empty()) { params.main_gpu = cmd_params_defaults.main_gpu; } @@ -1203,6 +1248,7 @@ struct cmd_params_instance { int n_cpu_moe; llama_split_mode split_mode; llama_load_mode load_mode; + llama_lazy_mode lazy_mode; int main_gpu; bool no_kv_offload; llama_flash_attn_type flash_attn; @@ -1224,6 +1270,7 @@ struct cmd_params_instance { } mparams.split_mode = split_mode; mparams.load_mode = load_mode; + mparams.lazy_mode = lazy_mode; mparams.main_gpu = main_gpu; mparams.tensor_split = tensor_split.data(); mparams.no_host = no_host; @@ -1271,7 +1318,8 @@ struct cmd_params_instance { return model == other.model && n_gpu_layers == other.n_gpu_layers && n_cpu_moe == other.n_cpu_moe && split_mode == other.split_mode && main_gpu == other.main_gpu && tensor_split == other.tensor_split && - load_mode == other.load_mode && devices == other.devices && no_host == other.no_host && + load_mode == other.load_mode && lazy_mode == other.lazy_mode && + devices == other.devices && no_host == other.no_host && vec_tensor_buft_override_equal(tensor_buft_overrides, other.tensor_buft_overrides); } @@ -1305,6 +1353,7 @@ static std::vector get_cmd_params_instances(const cmd_param for (const auto & ncmoe : params.n_cpu_moe) for (const auto & sm : params.split_mode) for (const auto & lm : params.load_mode) + for (const auto & lzm : params.lazy_mode) for (const auto & mg : params.main_gpu) for (const auto & devs : params.devices) for (const auto & ts : params.tensor_split) @@ -1344,6 +1393,7 @@ static std::vector get_cmd_params_instances(const cmd_param /* .n_cpu_moe = */ ncmoe, /* .split_mode = */ sm, /* .load_mode = */ lm, + /* .lazy_mode = */ lzm, /* .main_gpu = */ mg, /* .no_kv_offload = */ nkvo, /* .flash_attn = */ fa, @@ -1380,6 +1430,7 @@ static std::vector get_cmd_params_instances(const cmd_param /* .n_cpu_moe = */ ncmoe, /* .split_mode = */ sm, /* .load_mode = */ lm, + /* .lazy_mode = */ lzm, /* .main_gpu = */ mg, /* .no_kv_offload = */ nkvo, /* .flash_attn = */ fa, @@ -1416,6 +1467,7 @@ static std::vector get_cmd_params_instances(const cmd_param /* .n_cpu_moe = */ ncmoe, /* .split_mode = */ sm, /* .load_mode = */ lm, + /* .lazy_mode = */ lzm, /* .main_gpu = */ mg, /* .no_kv_offload = */ nkvo, /* .flash_attn = */ fa, @@ -1457,6 +1509,7 @@ struct test { int n_cpu_moe; llama_split_mode split_mode; llama_load_mode load_mode; + llama_lazy_mode lazy_mode; int main_gpu; bool no_kv_offload; llama_flash_attn_type flash_attn; @@ -1496,6 +1549,7 @@ struct test { n_cpu_moe = inst.n_cpu_moe; split_mode = inst.split_mode; load_mode = inst.load_mode; + lazy_mode = inst.lazy_mode; main_gpu = inst.main_gpu; no_kv_offload = inst.no_kv_offload; flash_attn = inst.flash_attn; @@ -1563,7 +1617,8 @@ struct test { "n_ubatch", "n_threads", "cpu_mask", "cpu_strict", "poll", "type_k", "type_v", "n_gpu_layers", "n_cpu_moe", "split_mode", "main_gpu", "no_kv_offload", "flash_attn", "devices", "tensor_split", - "tensor_buft_overrides", "load_mode", "embeddings", + "tensor_buft_overrides", "load_mode", "lazy_mode", + "embeddings", "no_op_offload", "no_host", "fit_target", "fit_min_ctx", "n_prompt", "n_gen", "n_depth", "test_time", "avg_ns", "stddev_ns", "avg_ts", "stddev_ts" @@ -1588,7 +1643,7 @@ struct test { if (field == "avg_ts" || field == "stddev_ts") { return FLOAT; } - if (field == "load_mode") { + if (field == "load_mode" || field == "lazy_mode") { return STRING; } return STRING; @@ -1658,6 +1713,7 @@ struct test { tensor_split_str, tensor_buft_overrides_str, llama_load_mode_name(load_mode), + lazy_mode_str(lazy_mode), std::to_string(embeddings), std::to_string(no_op_offload), std::to_string(no_host), @@ -1972,6 +2028,9 @@ struct markdown_printer : public printer { if (params.load_mode.size() > 1 || params.load_mode != cmd_params_defaults.load_mode) { fields.emplace_back("load_mode"); } + if (params.lazy_mode.size() > 1 || params.lazy_mode != cmd_params_defaults.lazy_mode) { + fields.emplace_back("lazy_mode"); + } if (params.embeddings.size() > 1 || params.embeddings != cmd_params_defaults.embeddings) { fields.emplace_back("embeddings"); } diff --git a/tools/server/README.md b/tools/server/README.md index 821b81b80372..10c6a0e796c4 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -76,7 +76,7 @@ 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)
(env: LLAMA_ARG_MMAP) | | `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available
(env: LLAMA_ARG_DIO) | | `-lm, --load-mode MODE` | model loading mode (default: auto)
- auto: mmap, unless a device does not support it
- none: no special loading mode
- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)
- mlock: force system to keep model in RAM rather than swapping or compressing
- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing
- dio: use DirectIO if available

(env: LLAMA_ARG_LOAD_MODE) | -| `--tensor-read-lazy MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)
- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)
- auto: on, but only for tensors larger than 4 GiB
- off: always keep them resident
(env: LLAMA_ARG_TENSOR_READ_LAZY) | +| `-lzm, --lazy-mode MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)
- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)
- auto: on, but only for tensors larger than 4 GiB
- off: always keep them resident
(env: LLAMA_ARG_LAZY_MODE) | | `--numa TYPE` | attempt optimizations that help on some NUMA systems
- distribute: spread execution evenly over all nodes
- isolate: only spawn threads on CPUs on the node that execution started on
- numactl: use the CPU map provided by numactl
if run without this previously, it is recommended to drop the system page cache before using this
see https://github.com/ggml-org/llama.cpp/issues/1437
(env: LLAMA_ARG_NUMA) | | `-dev, --device ` | comma-separated list of devices to use for offloading (none = don't offload)
use --list-devices to see a list of available devices
(env: LLAMA_ARG_DEVICE) | | `--list-devices` | print list of available devices and exit | @@ -163,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).
when set without -c/--ctx-size, the shared KV pool is sized to n_parallel*N
(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)
(env: LLAMA_ARG_CTX_CHECKPOINTS) | | `-cms, --checkpoint-min-step N` | minimum spacing between context checkpoints in tokens (default: 8192, 0 = no minimum)
(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)
(env: LLAMA_ARG_CACHE_RAM) | diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 0ae9938424af..90501bcb7f73 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -1208,10 +1208,31 @@ struct server_context_impl { 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(); @@ -1227,7 +1248,7 @@ struct server_context_impl { // 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++) { @@ -1271,7 +1292,7 @@ struct server_context_impl { 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; @@ -4003,8 +4024,15 @@ struct server_context_impl { }); } - 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() { @@ -4170,7 +4198,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, diff --git a/tools/server/server.cpp b/tools/server/server.cpp index 5fe2729ba1b2..22378b38c5ef 100644 --- a/tools/server/server.cpp +++ b/tools/server/server.cpp @@ -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()) { diff --git a/vendor/cpp-httplib/httplib.cpp b/vendor/cpp-httplib/httplib.cpp index 81cdcfe3a0c8..7fd10b3939bc 100644 --- a/vendor/cpp-httplib/httplib.cpp +++ b/vendor/cpp-httplib/httplib.cpp @@ -517,7 +517,8 @@ std::string from_i_to_hex(size_t n) { return ret; } -std::string compute_etag(const FileStat &fs) { +std::string compute_etag(const FileStat &fs, + const std::string &suffix = std::string()) { if (!fs.is_file()) { return std::string(); } // If mtime cannot be determined (negative value indicates an error @@ -531,7 +532,7 @@ std::string compute_etag(const FileStat &fs) { auto size = fs.size(); return std::string("W/\"") + from_i_to_hex(mtime) + "-" + - from_i_to_hex(size) + "\""; + from_i_to_hex(size) + suffix + "\""; } // Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994 @@ -817,17 +818,14 @@ std::string websocket_accept_key(const std::string &client_key) { bool is_websocket_upgrade(const Request &req) { if (req.method != "GET") { return false; } - // Check Upgrade: websocket (case-insensitive) - auto upgrade_it = req.headers.find("Upgrade"); - if (upgrade_it == req.headers.end()) { return false; } - auto upgrade_val = case_ignore::to_lower(upgrade_it->second); - if (upgrade_val != "websocket") { return false; } + // Check Upgrade: websocket. RFC 9110 7.8 defines Upgrade as a comma-separated + // list of protocols and asks recipients to match each name + // case-insensitively, so look for the token rather than compare the whole + // field value. + if (!has_header_token(req.headers, "Upgrade", "websocket")) { return false; } - // Check Connection header contains "Upgrade" - auto connection_it = req.headers.find("Connection"); - if (connection_it == req.headers.end()) { return false; } - auto connection_val = case_ignore::to_lower(connection_it->second); - if (connection_val.find("upgrade") == std::string::npos) { return false; } + // Check Connection: Upgrade + if (!has_header_token(req.headers, "Connection", "upgrade")) { return false; } // Check Sec-WebSocket-Key is a valid base64-encoded 16-byte value (24 chars) // RFC 6455 Section 4.2.1 @@ -1221,22 +1219,14 @@ bool parse_trailers(stream_line_reader &line_reader, Headers &dest, "trailer"}; case_ignore::unordered_set declared_trailers; - auto trailer_header = get_header_value(src_headers, "Trailer", "", 0); - if (trailer_header && std::strlen(trailer_header)) { - auto len = std::strlen(trailer_header); - split(trailer_header, trailer_header + len, ',', - [&](const char *b, const char *e) { - const char *kbeg = b; - const char *kend = e; - while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) { - ++kbeg; - } - while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) { - --kend; - } - std::string key(kbeg, static_cast(kend - kbeg)); - if (!key.empty() && - prohibited_trailers.find(key) == prohibited_trailers.end()) { + auto trailer_header = get_combined_header_value(src_headers, "Trailer"); + if (!trailer_header.empty()) { + // split() trims each token and skips empty ones, so the name arrives ready + // to look up. + split(trailer_header.data(), trailer_header.data() + trailer_header.size(), + ',', [&](const char *b, const char *e) { + std::string key(b, e); + if (prohibited_trailers.find(key) == prohibited_trailers.end()) { declared_trailers.insert(key); } }); @@ -1340,6 +1330,55 @@ void split(const char *b, const char *e, char d, size_t m, } } +// Same contract as split(), except that a delimiter inside a quoted-string is +// not a delimiter. RFC 9110 Section 5.6.6 lets a parameter value be a +// quoted-string, and ';' and '=' are legal characters inside one. +void split_unquoted(const char *b, const char *e, char d, size_t m, + std::function fn) { + size_t i = 0; + size_t beg = 0; + size_t count = 1; + auto in_quotes = false; + + while (e ? (b + i < e) : (b[i] != '\0')) { + if (b[i] == '"') { + in_quotes = !in_quotes; + } else if (b[i] == d && !in_quotes && count < m) { + auto r = trim(b, e, beg, i); + if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } + beg = i + 1; + count++; + } + i++; + } + + if (i) { + auto r = trim(b, e, beg, i); + if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } + } +} + +void split_unquoted(const char *b, const char *e, char d, + std::function fn) { + return split_unquoted(b, e, d, (std::numeric_limits::max)(), + std::move(fn)); +} + +// Divide a header parameter at its first '='. RFC 9110 Section 5.6.6 makes the +// key a token, so the first '=' is the separator even when the value is a +// quoted-string carrying more of them. +void divide_param_pair(const char *b, const char *e, std::string &key, + std::string &val) { + divide( + b, static_cast(e - b), '=', + [&](const char *kb, std::size_t klen, const char *vb, std::size_t vlen) { + const auto kr = trim(kb, kb + klen, 0, klen); + key.assign(kb + kr.first, kb + kr.second); + const auto vr = trim(vb, vb + vlen, 0, vlen); + val.assign(vb + vr.first, vb + vr.second); + }); +} + bool split_find(const char *b, const char *e, char d, size_t m, std::function fn) { size_t i = 0; @@ -2424,6 +2463,36 @@ bool is_connection_error() { #endif } +// accept() failed because the process or the network stack is temporarily out +// of resources. The listening socket is still usable, so back off briefly and +// try again. +bool is_accept_resource_error() { +#ifdef _WIN32 + auto err = WSAGetLastError(); + return err == WSAEMFILE || err == WSAENOBUFS; +#else + auto err = errno; + return err == EMFILE || err == ENFILE || err == ENOBUFS || err == ENOMEM; +#endif +} + +// accept() failed for a reason that says nothing about the listening socket: +// the pending connection went away before it could be accepted, or the call +// was interrupted. Retry immediately. WSAAccept()'s own documentation omits +// WSAECONNRESET, but the accept() it wraps reports an aborted pending +// connection that way. +bool is_accept_transient_error() { +#ifdef _WIN32 + auto err = WSAGetLastError(); + return err == WSAEINTR || err == WSAEWOULDBLOCK || err == WSAECONNRESET || + err == WSAECONNABORTED; +#else + auto err = errno; + return err == EINTR || err == EAGAIN || err == EWOULDBLOCK || + err == ECONNABORTED; +#endif +} + bool bind_ip_address(socket_t sock, const std::string &host) { struct addrinfo hints; struct addrinfo *result; @@ -2728,21 +2797,16 @@ extract_media_type(const std::string &content_type, if (params) { // Parse parameters: key=value pairs separated by ';' - split(param_str.data(), param_str.data() + param_str.size(), ';', - [&](const char *b, const char *e) { - std::string key; - std::string val; - split(b, e, '=', [&](const char *b2, const char *e2) { - if (key.empty()) { - key.assign(b2, e2); - } else { - val.assign(b2, e2); - } - }); - if (!key.empty()) { - params->emplace(trim_copy(key), trim_double_quotes_copy(val)); - } - }); + split_unquoted(param_str.data(), param_str.data() + param_str.size(), ';', + [&](const char *b, const char *e) { + std::string key; + std::string val; + divide_param_pair(b, e, key, val); + if (!key.empty()) { + params->emplace(trim_copy(key), + trim_double_quotes_copy(val)); + } + }); } } @@ -2828,12 +2892,11 @@ bool parse_quality(const char *b, const char *e, std::string &token, return !invalid; } -EncodingType encoding_type(const Request &req, const Response &res) { - if (!can_compress_content_type(res.get_header_value("Content-Type"))) { - return EncodingType::None; - } +EncodingType encoding_type(const Request &req, + const std::string &content_type) { + if (!can_compress_content_type(content_type)) { return EncodingType::None; } - const auto &s = req.get_header_value("Accept-Encoding"); + auto s = get_combined_header_value(req.headers, "Accept-Encoding"); if (s.empty()) { return EncodingType::None; } // Single-pass: iterate tokens and track the best supported encoding. @@ -2885,6 +2948,10 @@ EncodingType encoding_type(const Request &req, const Response &res) { return best; } +EncodingType encoding_type(const Request &req, const Response &res) { + return encoding_type(req, res.get_header_value("Content-Type")); +} + std::unique_ptr make_compressor(EncodingType type) { #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (type == EncodingType::Gzip) { @@ -3174,13 +3241,6 @@ bool zstd_decompressor::decompress(const char *data, size_t data_length, } #endif -bool contains_case_ignore(const std::string &s, const char *token) { - auto token_end = token + std::strlen(token); - return std::search(s.begin(), s.end(), token, token_end, [](char a, char b) { - return case_ignore::to_lower(a) == case_ignore::to_lower(b); - }) != s.end(); -} - // Content codings are case-insensitive (RFC 9110 8.4.1). Matching them // case-sensitively would make a response labeled e.g. "GZIP" look like an // unknown coding, and its payload would be handed back still compressed. @@ -3190,11 +3250,11 @@ bool is_zlib_encoding(const std::string &encoding) { } bool is_brotli_encoding(const std::string &encoding) { - return contains_case_ignore(encoding, "br"); + return case_ignore::equal(encoding, "br"); } bool is_zstd_encoding(const std::string &encoding) { - return contains_case_ignore(encoding, "zstd"); + return case_ignore::equal(encoding, "zstd"); } // Returns true if the content coding is one cpp-httplib is able to decompress @@ -3281,6 +3341,45 @@ size_t get_header_value_count(const Headers &headers, return headers.count(key); } +// RFC 9110 Section 5.2 and 5.3: a field that is defined as a comma-separated +// list may be sent as several field lines, and the combined field value is +// those values joined by commas in the order they were received. Callers that +// parse such a list must work on the combined value; reading only the first +// occurrence silently drops whatever the later field lines carry. +std::string get_combined_header_value(const Headers &headers, + const std::string &key) { + std::string combined; + auto rng = headers.equal_range(key); + for (auto it = rng.first; it != rng.second; ++it) { + // RFC 9110 Section 5.6.1.2: a recipient has to parse and ignore empty list + // elements, so an empty field line must not contribute a bare comma to the + // combined value. + if (it->second.empty()) { continue; } + if (!combined.empty()) { combined += ", "; } + combined += it->second; + } + return combined; +} + +bool has_header_token(const Headers &headers, const std::string &key, + const std::string &token) { + // RFC 9110 7.6.1: a comma-separated token list field such as Connection may + // carry several tokens, and RFC 9110 5.3 lets that list be split across + // several lines. Match complete tokens rather than searching the raw value, + // so that a value such as "notupgrade" is not read as the token "upgrade". + auto rng = headers.equal_range(key); + for (auto it = rng.first; it != rng.second; ++it) { + const auto &value = it->second; + if (split_find(value.data(), value.data() + value.size(), ',', + [&](const char *b, const char *e) { + return case_ignore::equal(std::string(b, e), token); + })) { + return true; + } + } + return false; +} + template typename Map::mapped_type get_multimap_value(const Map &m, const std::string &key, size_t id) { @@ -3411,19 +3510,14 @@ bool read_websocket_upgrade_response(Stream &strm, return false; } - // Verify Upgrade: websocket (case-insensitive) - auto upgrade_it = headers.find("Upgrade"); - if (upgrade_it == headers.end() || - case_ignore::to_lower(upgrade_it->second) != "websocket") { + // Verify Upgrade: websocket (a comma-separated list, matched per token) + if (!has_header_token(headers, "Upgrade", "websocket")) { upgrade.error = Error::WebSocketHandshake; return false; } - // Verify Connection header contains "Upgrade" (case-insensitive) - auto connection_it = headers.find("Connection"); - if (connection_it == headers.end() || - case_ignore::to_lower(connection_it->second).find("upgrade") == - std::string::npos) { + // Verify Connection: Upgrade + if (!has_header_token(headers, "Connection", "upgrade")) { upgrade.error = Error::WebSocketHandshake; return false; } @@ -3589,7 +3683,7 @@ bool prepare_content_receiver(T &x, int &status, bool decompress, size_t payload_max_length, bool &exceed_payload_max_length, U callback) { if (decompress) { - std::string encoding = x.get_header_value("Content-Encoding"); + auto encoding = get_combined_header_value(x.headers, "Content-Encoding"); std::unique_ptr decompressor; if (!encoding.empty()) { @@ -3769,6 +3863,7 @@ bool write_content_with_progress(Stream &strm, size_t end_offset = offset + length; size_t start_offset = offset; auto ok = true; + auto finished = false; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { @@ -3792,7 +3887,14 @@ bool write_content_with_progress(Stream &strm, data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); }; - while (offset < end_offset && !is_shutting_down()) { + // The body is framed by `length`, so a provider that reports itself done + // early has truncated it. Record that and let the short-body check below + // fail the write, rather than calling the provider again forever. + data_sink.done = [&]() { finished = true; }; + + while (offset < end_offset && !finished && !is_shutting_down()) { + auto last_offset = offset; + if (!strm.wait_writable() || !strm.is_peer_alive()) { error = Error::Write; return false; @@ -3803,9 +3905,18 @@ bool write_content_with_progress(Stream &strm, error = Error::Write; return false; } + + // A provider that reports success without writing anything and without + // reporting itself done gets handed the same offset and length again on + // the next pass, so it would spin here for as long as the peer stays + // connected. Treat making no progress as a short body, like done() early. + if (!finished && offset == last_offset) { + error = Error::Write; + return false; + } } - if (offset < end_offset) { // exited due to is_shutting_down(), not completion + if (offset < end_offset) { // done() called early, or is_shutting_down() error = Error::Write; return false; } @@ -3866,6 +3977,67 @@ write_content_without_length(Stream &strm, // down } +// Runs a known-length content provider to completion and compresses what it +// writes into `out`. Nothing is buffered in identity form: a provider backed +// by an mmap hands the compressor a pointer straight into the mapping. +bool compress_content_provider(const ContentProvider &content_provider, + size_t length, compressor &cmp, + std::string &out) { + size_t offset = 0; + auto ok = true; + auto finished = false; + DataSink data_sink; + + auto append = [&](const char *data, size_t data_len) { + out.append(data, data_len); + return true; + }; + + data_sink.write = [&](const char *d, size_t l) -> bool { + if (!ok) { return false; } + offset += l; + if (l > 0 && !cmp.compress(d, l, false, append)) { ok = false; } + return ok; + }; + + // The body is framed by `length`, so a provider that reports itself done + // early has truncated it; the short-body check below turns that into a + // failure rather than calling the provider again forever. + data_sink.done = [&]() { finished = true; }; + + while (offset < length && !finished) { + auto prev_offset = offset; + if (!content_provider(offset, length - offset, data_sink) || !ok) { + return false; + } + // No Stream to block on here, so a provider that keeps returning true + // without writing would spin. Treat a pass that made no progress as a + // failure. + if (offset == prev_offset) { return false; } + } + + if (offset != length) { return false; } + + return cmp.compress(nullptr, 0, true, append); +} + +// Serves `m` as the response body. `set_content_provider()` clears the coding, +// so recording it has to come after; keeping both here means a third +// file-serving path cannot get that order wrong. +void set_file_content_provider(Response &res, + const std::shared_ptr &m, + const std::string &content_type, + EncodingType encoding) { + res.set_content_provider( + m->size(), content_type, + [m](size_t offset, size_t length, DataSink &sink) -> bool { + sink.write(m->data() + offset, length); + return true; + }); + + res.file_content_encoding_ = encoding; +} + template bool write_content_chunked(Stream &strm, const ContentProvider &content_provider, @@ -3876,8 +4048,10 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider, DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { - if (ok) { - data_available = l > 0; + // Only done()/done_with_trailer() end a chunked body. A pass with nothing + // to hand over is ordinary (an empty buffer popped off a queue), and a + // zero-length chunk is the terminator, so it must not be emitted here. + if (ok && l > 0) { offset += l; std::string payload; @@ -4130,31 +4304,30 @@ bool parse_multipart_boundary(const std::string &content_type, auto it = params.find("boundary"); if (it == params.end()) { return false; } boundary = it->second; - return !boundary.empty(); + // RFC 2046 5.1.1 caps a boundary at 70 characters. The parser scans the body + // for "--" + boundary, so a body crafted to repeat that delimiter's leading + // bytes costs a nearly full comparison at nearly every position: the + // boundary's length multiplies the worst-case cost of scanning a body. + return !boundary.empty() && boundary.size() <= 70; } void parse_disposition_params(const std::string &s, Params ¶ms) { std::set cache; - split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) { - std::string kv(b, e); - if (cache.find(kv) != cache.end()) { return; } - cache.insert(kv); + split_unquoted(s.data(), s.data() + s.size(), ';', + [&](const char *b, const char *e) { + std::string kv(b, e); + if (cache.find(kv) != cache.end()) { return; } + cache.insert(kv); - std::string key; - std::string val; - split(b, e, '=', [&](const char *b2, const char *e2) { - if (key.empty()) { - key.assign(b2, e2); - } else { - val.assign(b2, e2); - } - }); + std::string key; + std::string val; + divide_param_pair(b, e, key, val); - if (!key.empty()) { - params.emplace(trim_double_quotes_copy((key)), - trim_double_quotes_copy((val))); - } - }); + if (!key.empty()) { + params.emplace(trim_double_quotes_copy(key), + trim_double_quotes_copy(val)); + } + }); } #ifdef CPPHTTPLIB_NO_EXCEPTIONS @@ -4224,12 +4397,6 @@ bool parse_accept_header(const std::string &s, // Empty string is considered valid (no preference) if (s.empty()) { return true; } - // Check for invalid patterns: leading/trailing commas or consecutive commas - if (s.front() == ',' || s.back() == ',' || - s.find(",,") != std::string::npos) { - return false; - } - struct AcceptEntry { std::string media_type; double quality; @@ -4240,16 +4407,16 @@ bool parse_accept_header(const std::string &s, int order = 0; bool has_invalid_entry = false; - // Split by comma and parse each entry + // Split by comma and parse each entry. RFC 9110 Section 5.6.1.2: a recipient + // has to parse and ignore empty list elements, so a leading, trailing or + // doubled comma must not turn a legal Accept value into 400 Bad Request. + // split() skips them, and the header length limit bounds how many a sender + // can send, so ignoring all of them cannot be used as a denial-of-service + // vector. split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) { std::string entry(b, e); entry = trim_copy(entry); - if (entry.empty()) { - has_invalid_entry = true; - return; - } - AcceptEntry accept_entry; accept_entry.order = order++; @@ -4314,13 +4481,25 @@ class FormDataParser { bool parse(const char *buf, size_t n, const FormDataHeader &header_callback, const ContentReceiver &content_callback) { + // Once the close delimiter has been seen the rest of the body is epilogue + // to be discarded (RFC 2046). Drop it without buffering so a large epilogue + // spread across reads is not copied in only to be erased right away. + if (state_ == 5) { return true; } + buf_append(buf, n); while (buf_size() > 0) { switch (state_) { case 0: { // Initial boundary auto pos = buf_find(dash_boundary_crlf_); - if (pos == buf_size()) { return true; } + if (pos == buf_size()) { + // Not found yet: keep only a possible partial boundary at the tail so + // that a body which never contains the boundary cannot grow the + // buffer (and get rescanned from the start) without bound. + auto keep = dash_boundary_crlf_.size() - 1; + if (buf_size() > keep) { buf_erase(buf_size() - keep); } + return true; + } buf_erase(pos + dash_boundary_crlf_.size()); state_ = 1; break; @@ -4443,18 +4622,26 @@ class FormDataParser { if (buf_start_with(crlf_)) { buf_erase(crlf_.size()); state_ = 1; + } else if (buf_start_with(dash_)) { + buf_erase(dash_.size()); + is_valid_ = true; + state_ = 5; } else { - if (dash_.size() > buf_size()) { return true; } - if (buf_start_with(dash_)) { - buf_erase(dash_.size()); - is_valid_ = true; - buf_erase(buf_size()); // Remove epilogue - } else { - return true; - } + // Only CRLF (another part follows) and "--" (close-delimiter) are + // accepted after a boundary; RFC 2046 allows transport-padding in + // between, but this parser has never supported it. Either way the + // body is already destined to be rejected, so fail now instead of + // buffering the rest of it. Both are two bytes, so the check above + // already guarantees enough buffered data to decide. + is_valid_ = false; + return false; } break; } + case 5: { // Epilogue + buf_erase(buf_size()); + break; + } } } @@ -5050,9 +5237,11 @@ bool has_framed_body(const Request &req) { } bool is_connection_persistent(const Request &req) { - auto conn = req.get_header_value("Connection"); - if (conn == "close") { return false; } - if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; } + if (has_header_token(req.headers, "Connection", "close")) { return false; } + if (req.version == "HTTP/1.0" && + !has_header_token(req.headers, "Connection", "keep-alive")) { + return false; + } return true; } @@ -5082,38 +5271,108 @@ class WSInit { static WSInit wsinit_; #endif +// RFC 9110 Section 11.6.1 defines a challenge list as +// WWW-Authenticate = #challenge +// challenge = auth-scheme [ 1*SP ( token68 / [ #auth-param ] ) ] +// auth-param = token BWS "=" BWS ( token / quoted-string ) +// so a server may offer several schemes, each with its own comma-separated +// auth-param list, in either order and either as separate field lines or +// packed into one. Splitting on every comma would break apart a challenge's +// own param list; splitting only on the first space would miss a Digest +// challenge that isn't first. Split on commas that aren't inside a +// quoted-string instead, then track which scheme each resulting segment +// belongs to: a segment whose text before "=" contains whitespace (or that +// has no "=" at all) starts a new challenge named by its leading token. +std::vector split_challenge_segments(const std::string &s) { + std::vector segments; + size_t start = 0; + auto in_quotes = false; + for (size_t i = 0; i < s.size(); i++) { + auto c = s[i]; + if (in_quotes) { + if (c == '\\' && i + 1 < s.size()) { + i++; + } else if (c == '"') { + in_quotes = false; + } + } else if (c == '"') { + in_quotes = true; + } else if (c == ',') { + segments.push_back(s.substr(start, i - start)); + start = i + 1; + } + } + segments.push_back(s.substr(start)); + return segments; +} + +std::string unescape_quoted_pairs(const std::string &s) { + std::string out; + out.reserve(s.size()); + for (size_t i = 0; i < s.size(); i++) { + if (s[i] == '\\' && i + 1 < s.size()) { + out += s[++i]; + } else { + out += s[i]; + } + } + return out; +} + bool parse_www_authenticate(const Response &res, std::map &auth, bool is_proxy) { auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; - if (res.has_header(auth_key)) { - thread_local auto re = - std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); - auto s = res.get_header_value(auth_key); - auto pos = s.find(' '); - if (pos != std::string::npos) { - auto type = s.substr(0, pos); - if (type == "Basic") { - return false; - } else if (type == "Digest") { - s = s.substr(pos + 1); - auto beg = std::sregex_iterator(s.begin(), s.end(), re); - for (auto i = beg; i != std::sregex_iterator(); ++i) { - const auto &m = *i; - auto key = s.substr(static_cast(m.position(1)), - static_cast(m.length(1))); - auto val = m.length(2) > 0 - ? s.substr(static_cast(m.position(2)), - static_cast(m.length(2))) - : s.substr(static_cast(m.position(3)), - static_cast(m.length(3))); - auth[std::move(key)] = std::move(val); - } - return true; + auto combined = get_combined_header_value(res.headers, auth_key); + if (combined.empty()) { return false; } + + auto found_digest = false; + auto in_digest_challenge = false; + for (const auto &raw_segment : split_challenge_segments(combined)) { + auto segment = trim_copy(raw_segment); + if (segment.empty()) { continue; } + + auto eq_pos = segment.find('='); + // BWS is allowed on both sides of "=", so the text naming the key (or, + // for the first segment of a challenge, " ") must be + // trimmed before its boundaries are inspected. + auto key_part = trim_copy( + eq_pos == std::string::npos ? segment : segment.substr(0, eq_pos)); + auto space_pos = key_part.find_last_of(" \t"); + if (space_pos != std::string::npos || eq_pos == std::string::npos) { + // "[ ]" starts a new challenge. + auto scheme_end = + space_pos == std::string::npos ? key_part.size() : space_pos; + // RFC 7616 Section 3.7: a server may offer more than one Digest + // challenge (e.g. SHA-256 and MD5); keep only the first so a nonce + // from one challenge is never paired with another's algorithm. + in_digest_challenge = + !found_digest && + case_ignore::equal(key_part.substr(0, scheme_end), "Digest"); + if (in_digest_challenge) { found_digest = true; } + if (space_pos == std::string::npos) { + // Bare scheme (or a token68), no auth-param on this segment. + continue; } + key_part = key_part.substr(space_pos + 1); } + + if (!in_digest_challenge) { continue; } + + auto val = trim_copy(segment.substr(eq_pos + 1)); + auto unquoted = trim_double_quotes_copy(val); + if (unquoted.size() != val.size()) { + unquoted = unescape_quoted_pairs(unquoted); + } + auth[std::move(key_part)] = std::move(unquoted); } - return false; + + // RFC 7616 Section 3.3 requires realm and nonce on every Digest challenge; + // make_digest_authentication_header() dereferences both unconditionally, so + // a challenge missing either can't produce a usable Authorization header. + // Treat it the same as no Digest challenge at all. + return found_digest && auth.find("realm") != auth.end() && + auth.find("nonce") != auth.end(); } class ContentProviderAdapter { @@ -5317,6 +5576,52 @@ class SSLSocketStream final : public Stream { bool readable_hint_ = false; }; +// A TLS stream for WebSocket connections, where the receive path and the +// send path (application send() plus the heartbeat ping thread) run on +// different threads. A single TLS session must never be entered +// concurrently, so every call into the session is serialized by one mutex. +// +// Unlike SSLSocketStream, the socket is kept non-blocking for the stream's +// whole lifetime and each read()/write() performs a single non-blocking TLS +// call under the lock, then waits for readiness with select() outside the +// lock. The lock is therefore held only for CPU-bound work, so a reader +// blocked waiting for data never stalls a concurrent sender. +// +// This stream is used only for wss:// connections. Plain ws:// and ordinary +// HTTP/HTTPS keep using SocketStream/SSLSocketStream unchanged. +class WebSocketSSLStream final : public Stream { +public: + WebSocketSSLStream(socket_t sock, tls::session_t session, + time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec); + ~WebSocketSSLStream() override; + + bool is_readable() const override; + bool wait_readable() const override; + bool wait_writable() const override; + ssize_t read(char *ptr, size_t size) override; + ssize_t write(const char *ptr, size_t size) override; + void get_remote_ip_and_port(std::string &ip, int &port) const override; + void get_local_ip_and_port(std::string &ip, int &port) const override; + socket_t socket() const override; + time_t duration() const override; + void set_read_timeout(time_t sec, time_t usec = 0) override; + +private: + mutable std::mutex session_mutex_; + + socket_t sock_; + tls::session_t session_; + // WebSocket::close() shortens the read timeout from the closing thread + // while the receive thread is inside wait_readable(), so these two are read + // and written concurrently. The write timeouts are never mutated. + std::atomic read_timeout_sec_; + std::atomic read_timeout_usec_; + time_t write_timeout_sec_; + time_t write_timeout_usec_; + const std::chrono::time_point start_time_; +}; + #ifdef CPPHTTPLIB_OPENSSL_SUPPORT std::string message_digest(const std::string &s, const EVP_MD *algo) { auto context = std::unique_ptr( @@ -5893,10 +6198,15 @@ bool set_socket_opt(socket_t sock, int level, int optname, int optval) { } std::string get_bearer_token_auth(const Request &req) { - if (req.has_header("Authorization")) { - constexpr auto bearer_header_prefix_len = detail::str_len("Bearer "); - return req.get_header_value("Authorization") - .substr(bearer_header_prefix_len); + // The auth scheme is case-insensitive (RFC 9110 11.1), and a value shorter + // than the prefix carries no token. + constexpr const char bearer_prefix[] = "Bearer "; + constexpr auto bearer_prefix_len = detail::str_len(bearer_prefix); + auto value = req.get_header_value("Authorization"); + if (value.size() >= bearer_prefix_len && + detail::case_ignore::equal(value.substr(0, bearer_prefix_len), + bearer_prefix)) { + return value.substr(bearer_prefix_len); } return ""; } @@ -6018,6 +6328,7 @@ std::string to_string(const Error error) { case Error::InvalidRangeHeader: return "Invalid Range header"; case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding"; case Error::WebSocketHandshake: return "WebSocket handshake failed"; + case Error::UserCallbackException: return "User callback threw an exception"; default: break; } @@ -6137,7 +6448,19 @@ std::string decode_uri(const std::string &value) { if (value[i] == '%' && i + 2 < value.size()) { auto val = 0; if (detail::from_hex_to_i(value, i + 1, 2, val)) { - result += static_cast(val); + auto c = static_cast(val); + // Keep escapes of the reserved characters that encode_uri leaves + // literal, so decode_uri is the inverse of encode_uri and an escaped + // delimiter is not promoted into a real one (as with JS decodeURI). + if (c == ';' || c == '/' || c == '?' || c == ':' || c == '@' || + c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || + c == '#') { + result += value[i]; + result += value[i + 1]; + result += value[i + 2]; + } else { + result += c; + } i += 2; } else { result += value[i]; @@ -6579,6 +6902,7 @@ void Response::set_content(const char *s, size_t n, auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); + file_content_encoding_ = detail::EncodingType::None; } void Response::set_content(const std::string &s, @@ -6593,6 +6917,7 @@ void Response::set_content(std::string &&s, auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); + file_content_encoding_ = detail::EncodingType::None; } void Response::set_content_provider( @@ -6603,6 +6928,7 @@ void Response::set_content_provider( if (in_length > 0) { content_provider_ = std::move(provider); } content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = false; + file_content_encoding_ = detail::EncodingType::None; } void Response::set_content_provider( @@ -6613,6 +6939,7 @@ void Response::set_content_provider( content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = false; + file_content_encoding_ = detail::EncodingType::None; } void Response::set_chunked_content_provider( @@ -6623,6 +6950,7 @@ void Response::set_chunked_content_provider( content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = true; + file_content_encoding_ = detail::EncodingType::None; } void Response::set_file_content(const std::string &path, @@ -7600,6 +7928,127 @@ void SSLSocketStream::set_read_timeout(time_t sec, time_t usec) { read_timeout_usec_ = usec; } +WebSocketSSLStream::WebSocketSSLStream(socket_t sock, + tls::session_t session, + time_t read_timeout_sec, + time_t read_timeout_usec, + time_t write_timeout_sec, + time_t write_timeout_usec) + : sock_(sock), session_(session), read_timeout_sec_(read_timeout_sec), + read_timeout_usec_(read_timeout_usec), + write_timeout_sec_(write_timeout_sec), + write_timeout_usec_(write_timeout_usec), + start_time_(std::chrono::steady_clock::now()) { + // The receive and send paths run on different threads, so each TLS call is + // driven in non-blocking mode and readiness is awaited with select() + // outside the session lock. Set the socket non-blocking once here; it is + // never flipped back, so no thread races on the flag. + detail::set_nonblocking(sock_, true); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + SSL_clear_mode(static_cast(session_), SSL_MODE_AUTO_RETRY); +#endif +} + +WebSocketSSLStream::~WebSocketSSLStream() = default; + +bool WebSocketSSLStream::is_readable() const { + std::lock_guard guard(session_mutex_); + return tls::pending(session_) > 0; +} + +bool WebSocketSSLStream::wait_readable() const { + return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; +} + +bool WebSocketSSLStream::wait_writable() const { + // Unlike SSLSocketStream, this deliberately does not call is_peer_closed(): + // that probe toggles the socket's blocking flag, which would race with the + // concurrent reader on a permanently non-blocking socket. + return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0; +} + +ssize_t WebSocketSSLStream::read(char *ptr, size_t size) { + tls::TlsError err; + auto n = 1000; + while (--n >= 0) { + { + std::lock_guard guard(session_mutex_); + auto ret = tls::read(session_, ptr, size, err); + if (ret > 0) { return ret; } + if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { + error_ = Error::ConnectionClosed; + return ret; + } + } + // ret < 0. On a non-blocking socket a TLS read can stop needing either + // direction: the send path shares this session, so output it left pending + // has to be flushed before more input can be decrypted. Anything else is + // a hard error. + auto needs_readable = err.code == tls::ErrorCode::WantRead; +#ifdef _WIN32 + // On Windows a socket timeout surfaces as a syscall error, not WantRead. + needs_readable = + needs_readable || (err.code == tls::ErrorCode::SyscallError && + WSAGetLastError() == WSAETIMEDOUT); +#endif + if (!needs_readable && err.code != tls::ErrorCode::WantWrite) { return -1; } + if (!(needs_readable ? wait_readable() : wait_writable())) { + error_ = Error::Timeout; + return -1; + } + } + return -1; +} + +ssize_t WebSocketSSLStream::write(const char *ptr, size_t size) { + auto handle_size = std::min(size, (std::numeric_limits::max)()); + tls::TlsError err; + auto n = 1000; + while (--n >= 0) { + { + std::lock_guard guard(session_mutex_); + auto ret = tls::write(session_, ptr, handle_size, err); + if (ret >= 0) { return ret; } + } + // ret < 0. As in read(), either direction can be needed: a renegotiation + // or a post-handshake message must be consumed before the record goes + // out. Anything else is a hard error. + auto needs_writable = err.code == tls::ErrorCode::WantWrite; +#ifdef _WIN32 + // On Windows a socket timeout surfaces as a syscall error, not WantWrite. + needs_writable = + needs_writable || (err.code == tls::ErrorCode::SyscallError && + WSAGetLastError() == WSAETIMEDOUT); +#endif + if (!needs_writable && err.code != tls::ErrorCode::WantRead) { return -1; } + if (!(needs_writable ? wait_writable() : wait_readable())) { return -1; } + } + return -1; +} + +void WebSocketSSLStream::get_remote_ip_and_port(std::string &ip, + int &port) const { + detail::get_remote_ip_and_port(sock_, ip, port); +} + +void WebSocketSSLStream::get_local_ip_and_port(std::string &ip, + int &port) const { + detail::get_local_ip_and_port(sock_, ip, port); +} + +socket_t WebSocketSSLStream::socket() const { return sock_; } + +time_t WebSocketSSLStream::duration() const { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time_) + .count(); +} + +void WebSocketSSLStream::set_read_timeout(time_t sec, time_t usec) { + read_timeout_sec_ = sec; + read_timeout_usec_ = usec; +} + } // namespace detail #endif // CPPHTTPLIB_SSL_ENABLED @@ -7687,6 +8136,57 @@ Server &Server::Options(const std::string &pattern, Handler handler) { return add_handler(options_handlers_, pattern, std::move(handler)); } +const std::set &Server::builtin_methods() { + thread_local const std::set methods{ + "GET", "HEAD", "POST", "PUT", "DELETE", + "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; + return methods; +} + +Server::CustomHandlerEntry * +Server::custom_entry_for_registration(const std::string &method) { + // Built-in methods are refused for two different reasons. GET, HEAD, POST, + // PUT, DELETE, OPTIONS and PATCH are dispatched by the if/else chain in + // routing() before the custom tables are consulted, so a route registered + // for one of them could never fire. CONNECT, TRACE and PRI have no branch + // there and would be reachable, but they carry protocol-level meaning + // (tunnel setup, request echo, the HTTP/2 connection preface) that this + // library does not route. + if (!detail::fields::is_token(method) || builtin_methods().count(method)) { + output_error_log(Error::InvalidHTTPMethod, nullptr); + has_invalid_registration_ = true; + return nullptr; + } + return &custom_handlers_[method]; +} + +Server &Server::CustomRoute(const std::string &method, + const std::string &pattern, + Handler handler) { + auto *entry = custom_entry_for_registration(method); + if (!entry) { return *this; } + return add_handler(entry->handlers, pattern, std::move(handler)); +} + +Server &Server::CustomRoute(const std::string &method, + const std::string &pattern, + HandlerWithContentReader handler) { + auto *entry = custom_entry_for_registration(method); + if (!entry) { return *this; } + return add_handler(entry->handlers_for_content_reader, pattern, + std::move(handler)); +} + +const Server::CustomHandlerEntry * +Server::find_custom_entry(const std::string &method) const { + // find() alone would be correct here. The empty() check is what keeps the + // per-request cost off servers that never call CustomRoute(), which is the + // overwhelmingly common case; keep it rather than walking into the tree. + if (custom_handlers_.empty()) { return nullptr; } + auto it = custom_handlers_.find(method); + return it == custom_handlers_.end() ? nullptr : &it->second; +} + Server &Server::WebSocket(const std::string &pattern, WebSocketHandler handler) { websocket_handlers_.push_back( @@ -7898,6 +8398,21 @@ Server &Server::set_payload_max_length(size_t length) { return *this; } +Server &Server::set_static_file_compression(bool on) { + static_file_compression_ = on; + return *this; +} + +Server &Server::set_static_file_compression_min_length(size_t length) { + static_file_compression_min_length_ = length; + return *this; +} + +Server &Server::set_static_file_compression_max_length(size_t length) { + static_file_compression_max_length_ = length; + return *this; +} + Server &Server::set_websocket_max_missed_pongs(int count) { websocket_max_missed_pongs_ = count; return *this; @@ -7979,11 +8494,12 @@ bool Server::parse_request_line(const char *s, Request &req) const { if (count != 3) { return false; } } - thread_local const std::set methods{ - "GET", "HEAD", "POST", "PUT", "DELETE", - "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; + // A method outside the built-in set is accepted only when a handler has been + // registered for it with CustomRoute(). + const auto &methods = builtin_methods(); - if (methods.find(req.method) == methods.end()) { + if (methods.find(req.method) == methods.end() && + !find_custom_entry(req.method)) { output_error_log(Error::InvalidHTTPMethod, &req); return false; } @@ -8044,7 +8560,8 @@ bool Server::write_response_core(Stream &strm, bool close_connection, if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); } // Prepare additional headers - if (close_connection || req.get_header_value("Connection") == "close" || + if (close_connection || + detail::has_header_token(req.headers, "Connection", "close") || 400 <= res.status) { // Don't leave connections open after errors res.set_header("Connection", "close"); } else { @@ -8352,7 +8869,29 @@ bool Server::handle_file_request(Request &req, Response &res) { res.set_header(kv.first, kv.second); } - auto etag = detail::compute_etag(stat); + auto content_type_of = [&]() { + return detail::find_content_type( + path, file_extension_and_mimetype_map_, default_file_mimetype_); + }; + + // Only the ETag needs the content type this early, and only to name + // the coding. Deciding it here would otherwise put a regex in front + // of the 304 below, which serving a file never used to pay for. + std::string content_type; + auto encoding = detail::EncodingType::None; + if (static_file_compression_) { + content_type = content_type_of(); + encoding = static_file_encoding(req, content_type, stat.size()); + } + + // The ETag names the representation actually sent, so a client that + // cached the compressed form revalidates against the compressed ETag + // and still gets a 304, while one that took identity keeps the plain + // ETag. + auto etag = detail::compute_etag( + stat, encoding == detail::EncodingType::None + ? std::string() + : std::string("-") + detail::encoding_name(encoding)); if (!etag.empty()) { res.set_header("ETag", etag); } auto mtime = stat.mtime(); @@ -8372,14 +8911,9 @@ bool Server::handle_file_request(Request &req, Response &res) { return false; } - res.set_content_provider( - mm->size(), - detail::find_content_type(path, file_extension_and_mimetype_map_, - default_file_mimetype_), - [mm](size_t offset, size_t length, DataSink &sink) -> bool { - sink.write(mm->data() + offset, length); - return true; - }); + if (!static_file_compression_) { content_type = content_type_of(); } + + detail::set_file_content_provider(res, mm, content_type, encoding); if (req.method != "HEAD" && file_request_handler_) { file_request_handler_(req, res); @@ -8403,7 +8937,8 @@ bool Server::check_if_not_modified(const Request &req, Response &res, // 2. If-Modified-Since is checked only when If-None-Match is absent if (req.has_header("If-None-Match")) { if (!etag.empty()) { - auto val = req.get_header_value("If-None-Match"); + auto val = + detail::get_combined_header_value(req.headers, "If-None-Match"); // NOTE: We use exact string matching here. This works correctly // because our server always generates weak ETags (W/"..."), and @@ -8564,16 +9099,26 @@ bool Server::listen_internal() { #endif if (sock == INVALID_SOCKET) { - if (errno == EMFILE) { - // The per-process limit of open file descriptors has been reached. - // Try to accept new connections after a short sleep. + // NOTE: Winsock reports failures through WSAGetLastError() and never + // touches the CRT errno, so the two have to be asked platform by + // platform rather than by testing errno here. + if (detail::is_accept_resource_error()) { + // The per-process descriptor limit or the network stack's buffer + // space has been reached. Try to accept new connections after a + // short sleep. std::this_thread::sleep_for(std::chrono::microseconds{1}); continue; - } else if (errno == EINTR || errno == EAGAIN) { + } else if (detail::is_accept_transient_error()) { continue; } - if (svr_sock_ != INVALID_SOCKET) { - detail::close_socket(svr_sock_); + // Take the descriptor out of svr_sock_ before closing it: a later + // stop() would otherwise shutdown()/close() a value the OS may have + // reused, and keep_alive() watches svr_sock_ to notice the server is + // gone. The exchange also settles the race with a concurrent stop(), + // since whichever side takes the descriptor closes it exactly once. + auto listen_sock = svr_sock_.exchange(INVALID_SOCKET); + if (listen_sock != INVALID_SOCKET) { + detail::close_socket(listen_sock); ret = false; output_error_log(Error::Connection, nullptr); } else { @@ -8616,7 +9161,14 @@ bool Server::routing(Request &req, Response &res, Stream &strm) { return true; } - if (detail::expect_content(req)) { + const auto *custom = find_custom_entry(req.method); + + // The second clause mirrors what expect_content() does unconditionally for + // POST/PUT/PATCH/DELETE: a content reader route fires even when the request + // carries no body. Without it a body-less PROPFIND (RFC 4918 treats one as + // `allprop`) would skip its handler and fall through to 404. + if (detail::expect_content(req) || + (custom && !custom->handlers_for_content_reader.empty())) { // Content reader handler { // Track whether the ContentReader was aborted due to the decompressed @@ -8663,6 +9215,9 @@ bool Server::routing(Request &req, Response &res, Stream &strm) { } else if (req.method == "DELETE") { dispatched = dispatch_request_for_content_reader( req, res, std::move(reader), delete_handlers_for_content_reader_); + } else if (custom) { + dispatched = dispatch_request_for_content_reader( + req, res, std::move(reader), custom->handlers_for_content_reader); } if (dispatched) { @@ -8699,6 +9254,8 @@ bool Server::routing(Request &req, Response &res, Stream &strm) { return dispatch_request(req, res, options_handlers_, strm); } else if (req.method == "PATCH") { return dispatch_request(req, res, patch_handlers_, strm); + } else if (custom) { + return dispatch_request(req, res, custom->handlers, strm); } res.status = StatusCode::BadRequest_400; @@ -8735,9 +9292,88 @@ bool Server::dispatch_request(Request &req, Response &res, return false; } +// Decides the content coding for a response served straight from a file. Both +// the ETag, which has to name the representation actually sent, and +// `apply_static_file_compression()` go through this, so the two cannot drift +// apart. +detail::EncodingType Server::static_file_encoding( + const Request &req, const std::string &content_type, size_t length) const { + if (!static_file_compression_) { return detail::EncodingType::None; } + + // Nothing to compress, and an empty file already answers with + // `Content-Length: 0`. Checked on its own so that a zero floor still cannot + // turn an empty body into a 20-byte gzip stream. + if (length == 0) { return detail::EncodingType::None; } + + // A file that already fits in a single packet gains nothing from being made + // smaller, since it still travels in that one segment, and a file of a few + // bytes comes out larger than it went in. + if (length < static_file_compression_min_length_) { + return detail::EncodingType::None; + } + + // RFC 9110 applies Range to the representation after content coding, so a + // compressed 206 would mean compressing the whole file and then slicing it. + // Serve ranges from the identity representation instead. + if (!req.ranges.empty()) { return detail::EncodingType::None; } + + if (static_file_compression_max_length_ > 0 && + length > static_file_compression_max_length_) { + return detail::EncodingType::None; + } + + return detail::encoding_type(req, content_type); +} + +// Compresses a file-backed content provider into `res.body` and takes over the +// framing headers. Returns false when the response is left untouched. +bool Server::apply_static_file_compression(const Request &req, + Response &res) const { + auto type = res.file_content_encoding_; + if (type == detail::EncodingType::None || !res.content_provider_) { + return false; + } + + auto compressor = detail::make_compressor(type); + if (!compressor) { return false; } + + output_pre_compression_log(req, res); + + std::string compressed; + if (!detail::compress_content_provider(res.content_provider_, + res.content_length_, *compressor, + compressed)) { + return false; + } + + res.body.swap(compressed); + + // The provider was consumed in full, so a resource releaser registered with + // it should hear about a success when the response goes away. + res.content_provider_success_ = true; + res.content_provider_ = nullptr; + res.content_length_ = 0; + res.file_content_encoding_ = detail::EncodingType::None; + + res.set_header("Content-Encoding", detail::encoding_name(type)); + res.set_header("Vary", "Accept-Encoding"); + res.set_header("Content-Length", std::to_string(res.body.size())); + + return true; +} + void Server::apply_ranges(const Request &req, Response &res, std::string &content_type, std::string &boundary) const { + // A known-length content provider leaves `res.body` empty, so the compressor + // at the end of this function never runs for one (issue #2545). A file-backed + // provider is fully readable right here, so compress it and answer with an + // ordinary body: `Content-Length` and HEAD keep working, and the response + // takes the same path as `set_content()` from here on. Range requests never + // get a content coding, so `Content-Range` still names identity bytes and + // none of the framing below applies. + if (apply_static_file_compression(req, res)) { return; } + if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) { auto it = res.headers.find("Content-Type"); if (it != res.headers.end()) { @@ -8948,12 +9584,12 @@ Server::process_request(Stream &strm, const std::string &remote_addr, return write_response(strm, close_connection, req, res); } - if (req.get_header_value("Connection") == "close") { + if (detail::has_header_token(req.headers, "Connection", "close")) { connection_closed = true; } if (req.version == "HTTP/1.0" && - req.get_header_value("Connection") != "Keep-Alive") { + !detail::has_header_token(req.headers, "Connection", "keep-alive")) { connection_closed = true; } @@ -8965,7 +9601,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr, [&](const std::string &proxy) { return proxy == remote_addr; }); if (is_trusted_peer && req.has_header("X-Forwarded-For")) { - auto x_forwarded_for = req.get_header_value("X-Forwarded-For"); + // Some proxies append the address they observed as a separate + // X-Forwarded-For field line instead of extending the one the client sent + // (e.g. HAProxy's "option forwardfor"), so the whole combined value has to + // be scanned. Reading only the first occurrence would hand back the + // client-supplied, and therefore forgeable, value. + auto x_forwarded_for = + detail::get_combined_header_value(req.headers, "X-Forwarded-For"); auto derived = get_client_ip(x_forwarded_for, trusted_proxies_); req.remote_addr = derived.empty() ? remote_addr : derived; } else { @@ -8977,7 +9619,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr, req.local_port = local_port; if (req.has_header("Accept")) { - const auto &accept_header = req.get_header_value("Accept"); + auto accept_header = + detail::get_combined_header_value(req.headers, "Accept"); if (!detail::parse_accept_header(accept_header, req.accept_content_types)) { connection_closed = true; res.status = StatusCode::BadRequest_400; @@ -8998,7 +9641,12 @@ Server::process_request(Stream &strm, const std::string &remote_addr, if (setup_request) { setup_request(req); } - if (req.get_header_value("Expect") == "100-continue") { + // RFC 9110 10.1.1: Expect is a comma-separated list whose value is + // case-insensitive, and a 100-continue expectation in an HTTP/1.0 request + // must be ignored. An expectation we do not recognize is left alone; the + // 417 the section allows for one is a MAY, not a requirement. + if (req.version != "HTTP/1.0" && + detail::has_header_token(req.headers, "Expect", "100-continue")) { int status = StatusCode::Continue_100; if (expect_100_continue_handler_) { status = expect_100_continue_handler_(req, res); @@ -9041,19 +9689,15 @@ Server::process_request(Stream &strm, const std::string &remote_addr, // Negotiate subprotocol std::string selected_subprotocol; if (entry.sub_protocol_selector) { - auto protocol_header = req.get_header_value("Sec-WebSocket-Protocol"); + auto protocol_header = detail::get_combined_header_value( + req.headers, "Sec-WebSocket-Protocol"); if (!protocol_header.empty()) { std::vector protocols; - std::istringstream iss(protocol_header); - std::string token; - while (std::getline(iss, token, ',')) { - // Trim whitespace - auto start = token.find_first_not_of(' '); - auto end = token.find_last_not_of(' '); - if (start != std::string::npos) { - protocols.push_back(token.substr(start, end - start + 1)); - } - } + detail::split(protocol_header.data(), + protocol_header.data() + protocol_header.size(), ',', + [&](const char *b, const char *e) { + protocols.emplace_back(b, e); + }); selected_subprotocol = entry.sub_protocol_selector(protocols); } } @@ -9081,6 +9725,24 @@ Server::process_request(Stream &strm, const std::string &remote_addr, if (websocket_upgraded) { *websocket_upgraded = true; } { +#ifdef CPPHTTPLIB_SSL_ENABLED + if (req.ssl) { + // wss: the heartbeat ping thread and the read path enter the same + // TLS session from different threads. Hand the WebSocket a stream + // that serializes every TLS call, so the shared SSLSocketStream on + // the plain HTTP/HTTPS paths stays untouched. + auto ws_strm = + std::unique_ptr(new detail::WebSocketSSLStream( + strm.socket(), const_cast(req.ssl), + CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0, + write_timeout_sec_, write_timeout_usec_)); + ws::WebSocket ws(std::move(ws_strm), req, true, + websocket_ping_interval_sec_, + websocket_max_missed_pongs_); + entry.handler(req, ws); + return true; + } +#endif // Use WebSocket-specific read timeout instead of HTTP timeout strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0); ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_, @@ -9144,12 +9806,9 @@ Server::process_request(Stream &strm, const std::string &remote_addr, path, file_extension_and_mimetype_map_, default_file_mimetype_); } - res.set_content_provider( - mm->size(), content_type, - [mm](size_t offset, size_t length, DataSink &sink) -> bool { - sink.write(mm->data() + offset, length); - return true; - }); + detail::set_file_content_provider( + res, mm, content_type, + static_file_encoding(req, content_type, mm->size())); } } @@ -9174,7 +9833,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, // consume the next request (issue #2450). If the response has committed the // connection to close, there is no next request to protect. if (!req.body_consumed_ && detail::has_framed_body(req)) { - if (res.get_header_value("Connection") == "close") { + if (detail::has_header_token(res.headers, "Connection", "close")) { connection_closed = true; } else { int dummy_status; @@ -9190,7 +9849,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, return ret; } -bool Server::is_valid() const { return true; } +bool Server::is_valid() const { return !has_invalid_registration_; } bool Server::process_and_close_socket(socket_t sock) { std::string remote_addr; @@ -9202,15 +9861,18 @@ bool Server::process_and_close_socket(socket_t sock) { detail::get_local_ip_and_port(sock, local_addr, local_port); bool websocket_upgraded = false; - auto ret = detail::process_server_socket( - svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, - [&](Stream &strm, bool close_connection, bool &connection_closed) { - return process_request(strm, remote_addr, remote_port, local_addr, - local_port, close_connection, connection_closed, - nullptr, &websocket_upgraded); - }); + auto ret = serve_guarded([&]() { + return detail::process_server_socket( + svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, + [&](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request(strm, remote_addr, remote_port, local_addr, + local_port, close_connection, + connection_closed, nullptr, + &websocket_upgraded); + }); + }); detail::drain_and_close_socket(sock); return ret; @@ -9736,7 +10398,8 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, handle.body_reader_.chunked = detail::is_chunked_transfer_encoding(handle.response->headers); - auto content_encoding = handle.response->get_header_value("Content-Encoding"); + auto content_encoding = detail::get_combined_header_value( + handle.response->headers, "Content-Encoding"); if (!content_encoding.empty()) { // Same policy as prepare_content_receiver(): reject a coding we know about // but were not built with, pass an unrecognized one through as-is. @@ -9958,7 +10621,7 @@ bool ClientImpl::handle_request(Stream &strm, Request &req, if (!ret) { return false; } - if (res.get_header_value("Connection") == "close" || + if (detail::has_header_token(res.headers, "Connection", "close") || (res.version == "HTTP/1.0" && res.reason != "Connection established")) { // NOTE: this requires a not-entirely-obvious chain of calls to be correct // for this to be safe. @@ -10437,6 +11100,7 @@ ClientImpl::send_with_content_provider_and_receiver( if (content_provider) { auto ok = true; + auto finished = false; size_t offset = 0; DataSink data_sink; @@ -10460,13 +11124,27 @@ ClientImpl::send_with_content_provider_and_receiver( return ok; }; - while (ok && offset < content_length) { + // As in detail::write_content_with_progress(): the body is framed by + // content_length, so a provider that finishes early has truncated it. + // Stop and report that instead of calling the provider forever. + data_sink.done = [&]() { finished = true; }; + + while (ok && !finished && offset < content_length) { if (!content_provider(offset, content_length - offset, data_sink)) { error = Error::Canceled; output_error_log(error, &req); return nullptr; } } + + // A short body here means either the provider stopped early or the + // compressor gave up. The branch below reports a failing compressor as + // Error::Compression, so keep the two distinguishable. + if (offset < content_length) { + error = ok ? Error::Write : Error::Compression; + output_error_log(error, &req); + return nullptr; + } } else { if (!compressor->compress(body, content_length, true, [&](const char *data, size_t data_len) { @@ -10564,7 +11242,8 @@ bool ClientImpl::process_request(Stream &strm, Request &req, } // Check for Expect: 100-continue - auto expect_100_continue = req.get_header_value("Expect") == "100-continue"; + auto expect_100_continue = + detail::has_header_token(req.headers, "Expect", "100-continue"); // Send request (skip body if using Expect: 100-continue) auto write_request_success = @@ -10768,6 +11447,9 @@ ContentProviderWithoutLength ClientImpl::get_multipart_content_provider( DataSink cur_sink; auto has_data = true; cur_sink.write = sink.write; + // Forward is_writable so a provider item asking whether it may keep + // going gets the outer sink's answer rather than the default `true`. + cur_sink.is_writable = sink.is_writable; cur_sink.done = [&]() { has_data = false; }; if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) { @@ -12529,7 +13211,9 @@ SSLServer::~SSLServer() { if (ctx_) { tls::free_context(ctx_); } } -bool SSLServer::is_valid() const { return ctx_ != nullptr; } +bool SSLServer::is_valid() const { + return ctx_ != nullptr && Server::is_valid(); +} bool SSLServer::process_and_close_socket(socket_t sock) { using namespace tls; @@ -12588,16 +13272,18 @@ bool SSLServer::process_and_close_socket(socket_t sock) { int local_port = 0; detail::get_local_ip_and_port(sock, local_addr, local_port); - ret = detail::process_server_socket_ssl( - svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, - [&](Stream &strm, bool close_connection, bool &connection_closed) { - return process_request( - strm, remote_addr, remote_port, local_addr, local_port, - close_connection, connection_closed, - [&](Request &req) { req.ssl = session; }, &websocket_upgraded); - }); + ret = serve_guarded([&]() { + return detail::process_server_socket_ssl( + svr_sock_, session, sock, keep_alive_max_count_, + keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, + [&](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request( + strm, remote_addr, remote_port, local_addr, local_port, + close_connection, connection_closed, + [&](Request &req) { req.ssl = session; }, &websocket_upgraded); + }); + }); return ret; } @@ -16870,6 +17556,7 @@ bool WebSocket::send_frame(Opcode op, const char *data, size_t len, } ReadResult WebSocket::read(std::string &msg) { + std::unique_lock read_lock(read_mutex_); while (!closed_) { Opcode opcode; std::string payload; @@ -16955,6 +17642,9 @@ ReadResult WebSocket::read(std::string &msg) { } // RFC 6455 Section 5.6: text frames must contain valid UTF-8 if (result == Text && !impl::is_valid_utf8(msg)) { + // close() takes the read lock to wait for the peer's Close reply, so + // it must not run while this thread still holds it. + read_lock.unlock(); close(CloseStatus::InvalidPayload, "invalid UTF-8"); return Fail; } @@ -16991,9 +17681,18 @@ void WebSocket::close(CloseStatus status, const std::string &reason) { } // RFC 6455 Section 7.1.1: after sending a Close frame, wait for the peer's - // Close response before closing the TCP connection. Use a short timeout to - // avoid hanging if the peer doesn't respond. + // Close response before closing the TCP connection. + // + // Wait only when no other thread is parsing frames. When one is, it is the + // thread positioned to see the peer's reply, and reading here would take + // bytes out of the message it is assembling. Bailing out also leaves the + // stream, including its read timeout, entirely to that thread. + std::unique_lock read_lock(read_mutex_, std::try_to_lock); + if (!read_lock.owns_lock()) { return; } + + // Use a short timeout to avoid hanging if the peer doesn't respond. strm_.set_read_timeout(CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND, 0); + Opcode op; std::string resp; bool fin; @@ -17171,7 +17870,7 @@ bool WebSocketClient::create_stream(std::unique_ptr &strm, return false; } - strm = std::unique_ptr(new detail::SSLSocketStream( + strm = std::unique_ptr(new detail::WebSocketSSLStream( sock_, tls_session_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_)); return true; diff --git a/vendor/cpp-httplib/httplib.h b/vendor/cpp-httplib/httplib.h index 6fc86c7c75bf..ca7c96a41a38 100644 --- a/vendor/cpp-httplib/httplib.h +++ b/vendor/cpp-httplib/httplib.h @@ -8,8 +8,8 @@ #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_VERSION "0.53.1" -#define CPPHTTPLIB_VERSION_NUM "0x003501" +#define CPPHTTPLIB_VERSION "0.54.1" +#define CPPHTTPLIB_VERSION_NUM "0x003601" #ifdef _WIN32 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 @@ -134,6 +134,16 @@ #define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192 #endif +#ifndef CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH +// 1400 rather than a round number: a body that already fits in one 1500-byte +// MTU gains nothing from being made smaller. +#define CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH 1400 +#endif + +#ifndef CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH +#define CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH (4 * 1024 * 1024) // 4MB +#endif + #ifndef CPPHTTPLIB_RANGE_MAX_COUNT #define CPPHTTPLIB_RANGE_MAX_COUNT 1024 #endif @@ -1429,9 +1439,16 @@ class DataSink { DataSink &operator=(DataSink &&) = delete; std::function write; - std::function is_writable; - std::function done; - std::function done_with_trailer; + + // Only `write` is mandatory. The rest are defaulted so that a provider + // calling one on a writer that does not set it gets sensible behaviour + // rather than std::bad_function_call thrown from a worker thread. Capturing + // `this` is safe: DataSink is neither copyable nor movable. + std::function is_writable = []() { return true; }; + std::function done = []() {}; + std::function done_with_trailer = + [this](const Headers & /*trailer*/) { done(); }; + std::ostream os; private: @@ -1516,7 +1533,10 @@ make_file_body(const std::string &filepath) { auto to_read = (std::min)(sizeof(buf), length); f.read(buf, static_cast(to_read)); auto n = static_cast(f.gcount()); - if (n == 0) { break; } + // The file is shorter than the size make_file_body() measured, which the + // caller has already committed to as Content-Length. The body cannot be + // completed, so fail as every other error here does. + if (n == 0) { return false; } if (!sink.write(buf, n)) { return false; } length -= n; } @@ -1723,6 +1743,14 @@ struct Request { #endif }; +namespace detail { + +// Declared up here, away from the rest of the compression helpers, because +// `Response` stores one. +enum class EncodingType { None = 0, Gzip, Brotli, Zstd }; + +} // namespace detail + struct Response { std::string version; int status = -1; @@ -1788,6 +1816,11 @@ struct Response { bool content_provider_success_ = false; std::string file_content_path_; std::string file_content_content_type_; + + // Content coding chosen for a file-backed content provider, decided once + // where the file is opened so that the ETag and the body cannot disagree. + // `EncodingType::None` for every other kind of response. + detail::EncodingType file_content_encoding_ = detail::EncodingType::None; }; enum class Error { @@ -1827,6 +1860,7 @@ enum class Error { InvalidRangeHeader, UnsupportedContentEncoding, WebSocketHandshake, + UserCallbackException, // For internal use only SSLPeerCouldBeClosed_, @@ -2020,6 +2054,10 @@ class RegexMatcher final : public MatcherBase { int close_socket(socket_t sock) noexcept; +bool is_accept_resource_error(); + +bool is_accept_transient_error(); + ssize_t write_headers(Stream &strm, const Headers &headers); bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec, @@ -2107,6 +2145,17 @@ class Server { Server &Delete(const std::string &pattern, HandlerWithContentReader handler); Server &Options(const std::string &pattern, Handler handler); + // Register a handler for an HTTP method outside the built-in set (e.g. the + // WebDAV methods from RFC 4918). Registering a method here is what makes the + // server accept it; an unregistered method is still rejected with 400. + // `method` must be a valid HTTP method token and must not be one of the + // built-in methods, which have their own registration functions above. A + // rejected registration makes is_valid() return false, so listen() fails. + Server &CustomRoute(const std::string &method, const std::string &pattern, + Handler handler); + Server &CustomRoute(const std::string &method, const std::string &pattern, + HandlerWithContentReader handler); + Server &WebSocket(const std::string &pattern, WebSocketHandler handler); Server &WebSocket(const std::string &pattern, WebSocketHandler handler, SubProtocolSelector sub_protocol_selector); @@ -2174,6 +2223,10 @@ class Server { Server &set_payload_max_length(size_t length); + Server &set_static_file_compression(bool on); + Server &set_static_file_compression_min_length(size_t length); + Server &set_static_file_compression_max_length(size_t length); + Server &set_websocket_ping_interval(time_t sec); template Server &set_websocket_ping_interval( @@ -2202,6 +2255,35 @@ class Server { const std::function &setup_request, bool *websocket_upgraded = nullptr); + // Runs the per-connection serving loop and stops an exception thrown by a + // user callback from escaping the worker thread. + // + // process_request() wraps only routing() in a try/catch. Content providers, + // the post-routing, error, logging and expect-100 handlers and WebSocket + // handlers all run outside it, and the task queue calls the job without a + // catch, so an exception from any of those would terminate the process. + // + // No 500 is possible here: by the time a content provider runs, the status + // line and headers are already on the wire. Report it through the error + // logger and drop the connection, which is what the peer observes either + // way. Other connections are unaffected. + template bool serve_guarded(Serve &&serve) const { +#ifdef CPPHTTPLIB_NO_EXCEPTIONS + return serve(); +#else + try { + return serve(); + } catch (...) { + // The error logger is a user callback too, so it must not be able to + // throw the guard back open. + try { + output_error_log(Error::UserCallbackException, nullptr); + } catch (...) {} + return false; + } +#endif + } + std::atomic svr_sock_{INVALID_SOCKET}; std::vector trusted_proxies_; @@ -2215,6 +2297,11 @@ class Server { time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND; time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND; size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; + bool static_file_compression_ = false; + size_t static_file_compression_min_length_ = + CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH; + size_t static_file_compression_max_length_ = + CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH; time_t websocket_ping_interval_sec_ = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND; int websocket_max_missed_pongs_ = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS; @@ -2226,9 +2313,21 @@ class Server { std::vector, HandlerWithContentReader>>; + // Both handler tables for one custom method live in a single entry, so that + // routing() needs only one map lookup per request to reach either of them. + struct CustomHandlerEntry { + Handlers handlers; + HandlersForContentReader handlers_for_content_reader; + }; + using CustomHandlers = std::map; + static std::unique_ptr make_matcher(const std::string &pattern); + static const std::set &builtin_methods(); + CustomHandlerEntry *custom_entry_for_registration(const std::string &method); + const CustomHandlerEntry *find_custom_entry(const std::string &method) const; + template Server &add_handler( std::vector, H>> &handlers, @@ -2259,6 +2358,10 @@ class Server { const HandlersForContentReader &handlers) const; bool parse_request_line(const char *s, Request &req) const; + detail::EncodingType static_file_encoding(const Request &req, + const std::string &content_type, + size_t length) const; + bool apply_static_file_compression(const Request &req, Response &res) const; void apply_ranges(const Request &req, Response &res, std::string &content_type, std::string &boundary) const; bool write_response(Stream &strm, bool close_connection, Request &req, @@ -2292,6 +2395,10 @@ class Server { std::atomic is_running_{false}; std::atomic is_decommissioned{false}; + // Set when CustomRoute() refuses a registration. Written before listen(), + // read by is_valid() on the same thread, so it needs no synchronization. + bool has_invalid_registration_ = false; + struct MountPointEntry { std::string mount_point; std::string base_dir; @@ -2313,6 +2420,7 @@ class Server { Handlers delete_handlers_; HandlersForContentReader delete_handlers_for_content_reader_; Handlers options_handlers_; + CustomHandlers custom_handlers_; struct WebSocketHandlerEntry { std::unique_ptr matcher; @@ -3500,6 +3608,16 @@ void split(const char *b, const char *e, char d, void split(const char *b, const char *e, char d, size_t m, std::function fn); +bool split_find(const char *b, const char *e, char d, + std::function fn); + +bool has_header_token(const Headers &headers, const std::string &key, + const std::string &token); + +std::string websocket_accept_key(const std::string &client_key); + +bool is_websocket_upgrade(const Request &req); + bool process_client_socket( socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, @@ -3520,6 +3638,9 @@ socket_t create_client_socket(const std::string &host, const std::string &ip, const char *get_header_value(const Headers &headers, const std::string &key, const char *def, size_t id); +std::string get_combined_header_value(const Headers &headers, + const std::string &key); + std::string params_to_query_str(const Params ¶ms); void parse_query_text(const char *data, std::size_t size, Params ¶ms); @@ -3534,11 +3655,13 @@ bool parse_range_header(const std::string &s, Ranges &ranges); bool parse_accept_header(const std::string &s, std::vector &content_types); +void parse_disposition_params(const std::string &s, Params ¶ms); + ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags); ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags); -enum class EncodingType { None = 0, Gzip, Brotli, Zstd }; +EncodingType encoding_type(const Request &req, const std::string &content_type); EncodingType encoding_type(const Request &req, const Response &res); @@ -4318,6 +4441,11 @@ class WebSocket { int unacked_pings_ = 0; std::atomic closed_{false}; std::mutex write_mutex_; + // Owned by whichever thread is parsing frames off strm_. Only one thread + // may do so: read_websocket_frame() reads a payload until it has the whole + // declared length, so a second parser stealing bytes silently corrupts the + // message the first one is assembling. + std::mutex read_mutex_; std::thread ping_thread_; std::mutex ping_mutex_; std::condition_variable ping_cv_; From e2e49a0ba5677d90442a32ecba0e209aa020357f Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:02:37 +0200 Subject: [PATCH 014/212] ci: skip CUDA and Metal jobs on pull requests (#12) --- .github/workflows/build-and-test-snapdragon.yml | 1 + .github/workflows/build-android.yml | 1 + .github/workflows/build-apple.yml | 1 + .github/workflows/build-cann.yml | 1 + .github/workflows/build-cpu.yml | 1 + .github/workflows/build-cuda-ubuntu.yml | 2 ++ .github/workflows/build-ibm.yml | 1 + .github/workflows/build-opencl.yml | 1 + .github/workflows/build-openvino.yml | 1 + .github/workflows/build-riscv.yml | 1 + .github/workflows/build-self-hosted.yml | 1 + .github/workflows/build-sycl.yml | 1 + .github/workflows/build-virtgpu.yml | 1 + .github/workflows/build-wasm.yml | 1 + .github/workflows/build-webgpu.yml | 1 + 15 files changed, 16 insertions(+) diff --git a/.github/workflows/build-and-test-snapdragon.yml b/.github/workflows/build-and-test-snapdragon.yml index 3e857d48e39f..923c1313c824 100644 --- a/.github/workflows/build-and-test-snapdragon.yml +++ b/.github/workflows/build-and-test-snapdragon.yml @@ -14,6 +14,7 @@ on: - 'CMakePresets.json' pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: - '.github/workflows/build-and-test-snapdragon.yml' diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index a05248e1298c..53ab3ac9a1c4 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -15,6 +15,7 @@ on: - '**/*.cpp' pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: - '.github/workflows/build-android.yml' diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 55f4bcad61af..0123a8f11d57 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -19,6 +19,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-apple.yml', diff --git a/.github/workflows/build-cann.yml b/.github/workflows/build-cann.yml index 6d76ed49992e..8b9208c38759 100644 --- a/.github/workflows/build-cann.yml +++ b/.github/workflows/build-cann.yml @@ -16,6 +16,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-cann.yml', diff --git a/.github/workflows/build-cpu.yml b/.github/workflows/build-cpu.yml index b62fe55d6c5f..071a3e6e9732 100644 --- a/.github/workflows/build-cpu.yml +++ b/.github/workflows/build-cpu.yml @@ -17,6 +17,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-cpu.yml', diff --git a/.github/workflows/build-cuda-ubuntu.yml b/.github/workflows/build-cuda-ubuntu.yml index 80bd78209558..52a2f9e165b0 100644 --- a/.github/workflows/build-cuda-ubuntu.yml +++ b/.github/workflows/build-cuda-ubuntu.yml @@ -37,6 +37,7 @@ env: jobs: cuda: + if: github.event_name != 'pull_request' runs-on: ubuntu-24.04 container: nvidia/cuda:12.6.2-devel-ubuntu24.04 @@ -144,6 +145,7 @@ jobs: save: true musa: + if: github.event_name != 'pull_request' runs-on: ubuntu-22.04 container: mthreads/musa:rc4.3.0-devel-ubuntu22.04-amd64 diff --git a/.github/workflows/build-ibm.yml b/.github/workflows/build-ibm.yml index d2e4f3cdaeb7..0e221e139cca 100644 --- a/.github/workflows/build-ibm.yml +++ b/.github/workflows/build-ibm.yml @@ -16,6 +16,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-ibm.yml', diff --git a/.github/workflows/build-opencl.yml b/.github/workflows/build-opencl.yml index c0adc7e49663..048b394e7c7e 100644 --- a/.github/workflows/build-opencl.yml +++ b/.github/workflows/build-opencl.yml @@ -17,6 +17,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-opencl.yml', diff --git a/.github/workflows/build-openvino.yml b/.github/workflows/build-openvino.yml index 8e0326f4a44f..de584aca4ffd 100644 --- a/.github/workflows/build-openvino.yml +++ b/.github/workflows/build-openvino.yml @@ -16,6 +16,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-openvino.yml', diff --git a/.github/workflows/build-riscv.yml b/.github/workflows/build-riscv.yml index 70615378b5ea..bcbdf9204079 100644 --- a/.github/workflows/build-riscv.yml +++ b/.github/workflows/build-riscv.yml @@ -16,6 +16,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-riscv.yml', diff --git a/.github/workflows/build-self-hosted.yml b/.github/workflows/build-self-hosted.yml index ccfe2a604645..dfae5f9ca5db 100644 --- a/.github/workflows/build-self-hosted.yml +++ b/.github/workflows/build-self-hosted.yml @@ -24,6 +24,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-self-hosted.yml', diff --git a/.github/workflows/build-sycl.yml b/.github/workflows/build-sycl.yml index 7beac8177232..5d830bfa312a 100644 --- a/.github/workflows/build-sycl.yml +++ b/.github/workflows/build-sycl.yml @@ -16,6 +16,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-sycl.yml', diff --git a/.github/workflows/build-virtgpu.yml b/.github/workflows/build-virtgpu.yml index 5b740590d6b8..5ed4c0cc877d 100644 --- a/.github/workflows/build-virtgpu.yml +++ b/.github/workflows/build-virtgpu.yml @@ -16,6 +16,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-virtgpu.yml', diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index 2e4680f385be..5f76a08687c0 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -19,6 +19,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-wasm.yml', diff --git a/.github/workflows/build-webgpu.yml b/.github/workflows/build-webgpu.yml index b357851aa2d0..9f7ea99d6bd8 100644 --- a/.github/workflows/build-webgpu.yml +++ b/.github/workflows/build-webgpu.yml @@ -19,6 +19,7 @@ on: ] pull_request: + branches-ignore: ['**'] types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-webgpu.yml', From 732484c20c8c7a61885c2e48ea28f90bdd7bd1e1 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:03:25 +0200 Subject: [PATCH 015/212] vulkan: avoid inexact matvec chunking with Q8_1 activations (#10) --- AGENTS.md | 65 +++++++++++++++++++++++++ ggml/src/ggml-vulkan/ggml-vulkan.cpp | 7 +++ tests/corpus/correctness-code.txt | 61 +++++++++++++++++++++++ tests/corpus/correctness-numeric.txt | 40 +++++++++++++++ tests/corpus/correctness-prose.txt | 23 +++++++++ tests/corpus/correctness-structured.txt | 61 +++++++++++++++++++++++ 6 files changed, 257 insertions(+) create mode 100644 tests/corpus/correctness-code.txt create mode 100644 tests/corpus/correctness-numeric.txt create mode 100644 tests/corpus/correctness-prose.txt create mode 100644 tests/corpus/correctness-structured.txt diff --git a/AGENTS.md b/AGENTS.md index 7ab6fa7c1a56..1bafe018ca4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,71 @@ cannot explain - open the PR as a **draft** describing the problem, or open an i --- +## Correctness Flow for Backend and Performance Changes + +All contributors must follow this flow for backend and performance work, regardless of experience or contribution history. Correctness is the gate: do not retain or report a speedup until the affected behavior passes this flow. + +1. **Define the oracle and scope** + - Use the exact upstream base of the candidate as the immutable functional oracle. Record its full revision. + - Record the candidate revision and diff hash. + - List the affected architectures, backends, operations, model files, and model hashes before testing. + +2. **Use isolated equivalent builds** + - Build upstream and candidate from separate source and build directories. + - Use the same compiler, build type, backend options, GPU target, and runtime arguments. + - Keep binaries, libraries, ports, logs, and result directories separate. Verify dynamic library resolution when relevant. + +3. **Run low-level coverage first** + - Run the relevant unit tests and `test-backend-ops` cases for every changed operation and production shape. + - Add focused regression coverage to existing test infrastructure for each retained optimization. + - Run the complete affected backend suite before the final result. + +4. **Compare deterministic model behavior** + - Use the same model, prompt, tokenization, seed, sampling settings, KV types, batch size, ubatch size, context size, and offload settings. + - Compare prompt token IDs and complete final logits after prefill. + - Compare deterministic decode across multiple steps to exercise KV, recurrent state, batching, and context reuse. + - Include long-context and relevant boundary shapes. A smoke load or matching text alone is not sufficient. + - Math-preserving changes must produce byte-identical outputs. Do not replace this requirement with a loose numerical tolerance. + + Run top-1 and complete-logit comparisons for the current target model with all four repository corpora. Do not multiply this corpus matrix across unrelated models: + - `tests/corpus/correctness-prose.txt` + - `tests/corpus/correctness-code.txt` + - `tests/corpus/correctness-structured.txt` + - `tests/corpus/correctness-numeric.txt` + - Hash each corpus and use the same bytes for upstream and candidate. If more tokens are required, repeat the corpus deterministically and save the resulting prompt before either run. + +5. **Run the target model matrix** + - Validate the current target model on every affected backend. + - Do not test unrelated models unless the change affects a shared path and the contributor explicitly expands the scope. + - Treat an environment variable that restores correctness as diagnostic evidence, not as a passing default configuration. + +6. **Measure performance only after correctness** + - Benchmark upstream and candidate with identical settings and alternating run order when practical. + - Establish the stock result from the immutable upstream build. Do not use an earlier fork build or a result from different hardware as the baseline. + - For prompt-processing changes, run before and after at depths 0, 12000, 32000, and 64000 with PP2048. Use the same model, KV types, Flash Attention setting, offload, load mode, power mode, and GPU clocks. + - Batch and ubatch may be selected for the target workload. Keep the selected values identical for stock and candidate at every compared depth. + - Use this command shape and replace only paths or explicitly documented target settings: + + `llama-bench -m MODEL -p 2048 -d 0,12000,32000,64000 -b BATCH -ub UBATCH -n 0 -r 4 -ngl 99 -fa on -ctk f16 -ctv f16 --load-mode none -o jsonl` + + - For speculative decoding, also run `llama-benchy` against separate stock and candidate servers. Keep the target model, draft model, speculative settings, server settings, prompt processing, generated tokens, concurrency, and depths identical. + - Use this command shape and document any additional speculative options: + + `llama-benchy --base-url SERVER_URL --model MODEL --pp 2048 --tg 128 --depth 0 12000 32000 64000` + + - Report end-to-end tok/s, time to first token, accepted draft tokens, and acceptance rate. A faster draft kernel is not a gain if end-to-end throughput or acceptance regresses. + - Use repeated samples and report variance, not only the best run. + - Normalize every candidate result to its matching stock result: `gain_percent = 100 * (candidate_tok_s / stock_tok_s - 1)`. + - Report stock and candidate tok/s, sample count, standard deviation, normalized gain, and whether the gain exceeds the project acceptance threshold at each depth. + - Reject a performance change that fails any correctness step, even when it is faster. + +7. **Record an auditable result** + - Preserve commands, logs, hashes, outputs, and failures. + - Report `TARGET PASS`, `GLOBAL PASS`, `FAIL`, or `INCOMPLETE`. + - Use `FAIL` for any candidate regression against the oracle. Use `INCOMPLETE` when required coverage or artifacts are missing. + +--- + ## Code and Commit Standards These points are extremely important - failing to follow them won't necessarily get your PR rejected, but it will make diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 7e84989847c9..a4f442e8e736 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -9876,7 +9876,14 @@ static uint32_t ggml_vk_mmv_good_cols(ggml_type type, uint32_t n) { static constexpr uint32_t mul_mat_vec_chunk_max_cols = 16; static bool ggml_vk_mmv_can_chunk(ggml_backend_vk_context * ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst) { static const bool no_split = getenv("GGML_VK_MMV_NO_SPLIT") != nullptr; + + // Chunking cannot reuse the whole-tensor Q8_1 activation cache with a column offset. + // Keep the unsplit integer-dot path rather than silently changing its arithmetic. + const bool would_quantize_y = ctx->device->integer_dot_product && src1->type == GGML_TYPE_F32 && + ggml_is_contiguous(src1) && (src1->ne[1] * src1->ne[0]) % 4 == 0 && + ggml_vk_should_use_mmvq(ctx->device, src0->ne[1], src1->ne[1], src1->ne[0], src0->type); return !no_split && ggml_is_quantized(src0->type) && + !would_quantize_y && dst->ne[1] > mul_mat_vec_max_cols && dst->ne[1] <= mul_mat_vec_chunk_max_cols && src1->ne[2] * src1->ne[3] == 1 && src1->type == GGML_TYPE_F32 && ggml_is_contiguous(src1) && ggml_is_contiguous(dst) && ctx->num_additional_fused_ops == 0; diff --git a/tests/corpus/correctness-code.txt b/tests/corpus/correctness-code.txt new file mode 100644 index 000000000000..d70510919bf2 --- /dev/null +++ b/tests/corpus/correctness-code.txt @@ -0,0 +1,61 @@ +#include +#include +#include +#include +#include +#include + +struct sample { + int id; + float weight; + std::array shape; +}; + +static float weighted_sum(const std::vector & values) { + float result = 0.0f; + for (const sample & value : values) { + const int volume = std::accumulate(value.shape.begin(), value.shape.end(), 1, std::multiplies()); + result += value.weight * static_cast(volume); + } + return result; +} + +int main() { + std::vector values = { + { 7, 0.125f, { 1, 31, 32, 33 } }, + { 11, 0.500f, { 2, 16, 64, 1 } }, + { 19, 1.250f, { 4, 8, 17, 65 } }, + }; + + std::stable_sort(values.begin(), values.end(), [](const sample & a, const sample & b) { + return a.id < b.id; + }); + + return weighted_sum(values) > 0.0f ? 0 : 1; +} + +def chunk_rows(rows, width): + output = [] + for start in range(0, len(rows), width): + stop = min(start + width, len(rows)) + output.append(rows[start:stop]) + return output + +def checksum(values): + state = 2166136261 + for value in values: + state = (state ^ value) * 16777619 + state &= 0xffffffff + return state + +SELECT model_id, backend, AVG(tokens_per_second) AS mean_rate +FROM benchmark_results +WHERE prompt_tokens = 2048 + AND depth IN (0, 12000, 32000, 64000) +GROUP BY model_id, backend +ORDER BY model_id, backend; + +curl --fail --silent --show-error \ + --header "Content-Type: application/json" \ + --data '{"prompt":"deterministic input","temperature":0,"seed":123}' \ + http://127.0.0.1:8080/v1/completions diff --git a/tests/corpus/correctness-numeric.txt b/tests/corpus/correctness-numeric.txt new file mode 100644 index 000000000000..de3ef239c1c1 --- /dev/null +++ b/tests/corpus/correctness-numeric.txt @@ -0,0 +1,40 @@ +Integer boundaries: +0 1 2 3 4 7 8 15 16 17 31 32 33 63 64 65 127 128 129 255 256 257 511 512 513 1023 1024 1025 2047 2048 2049 + +Signed values: +-2147483648 -65536 -32768 -4096 -1024 -256 -128 -64 -32 -16 -8 -4 -2 -1 0 1 2 4 8 16 32 64 128 256 1024 4096 32767 65535 2147483647 + +Decimal values: +0.0 0.000001 0.0009765625 0.03125 0.0625 0.1 0.125 0.25 0.3333333 0.5 0.75 0.875 0.999999 1.0 +-0.0 -0.000001 -0.03125 -0.125 -0.5 -0.999999 -1.0 1.5 2.0 3.14159265 6.28318530 10.0 1000.0 + +Shape table: +1x1x1x1 +1x31x32x33 +2x16x64x1 +4x8x17x65 +10x512x640x2560 +24x128x2048x1 +48x128x2048x1 +512x10x2048x2560 + +Matrix A: +1 0 0 0 1 0 0 0 1 +2 3 5 7 11 13 17 19 23 +-1 1 -1 1 -1 1 -1 1 -1 + +Matrix B: +0.125 0.250 0.500 1.000 +1.000 0.500 0.250 0.125 +-0.125 -0.250 -0.500 -1.000 + +Sequence checks: +1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 +2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 +1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 + +Depth checkpoints: +depth=0 prompt=2048 generation=0 +depth=12000 prompt=2048 generation=0 +depth=32000 prompt=2048 generation=0 +depth=64000 prompt=2048 generation=0 diff --git a/tests/corpus/correctness-prose.txt b/tests/corpus/correctness-prose.txt new file mode 100644 index 000000000000..782aade7d9c3 --- /dev/null +++ b/tests/corpus/correctness-prose.txt @@ -0,0 +1,23 @@ +At sunrise the research vessel left the harbor and followed the eastern channel. The sea was calm, but a narrow bank of fog covered the distant markers. The crew checked the compass, compared the paper chart with the digital display, and recorded the time of each observation. + +Mara kept the log. She wrote short sentences because every statement had to be checked later. At 06:15 the engine speed was steady. At 06:40 the wind shifted north. At 07:05 a red buoy appeared on the port side, exactly where the chart predicted. + +The purpose of the trip was simple: repeat the same survey under controlled conditions. The instruments measured temperature, pressure, salinity, and current speed. Each reading was stored twice, once in the local recorder and once in a sealed backup unit. + +During the second hour, the fog lifted. Low hills became visible beyond the water, and sunlight reached the deck. The crew did not change the plan. They continued along the numbered stations, stopped for three minutes at each point, and used the same procedure for every sample. + +A good experiment depends on ordinary discipline. Names must remain consistent. Units must be written beside values. Missing observations must be marked as missing rather than replaced with convenient estimates. If a device reports an impossible value, the operator must preserve the original record before investigating the cause. + +At noon the vessel turned west. The return route crossed the morning track at two places. Those intersections provided useful checks because the water should have similar properties when measured only a few hours apart. Small differences were expected; large differences would require another survey. + +The final station was close to the harbor wall. Traffic had increased, and the wake from a ferry disturbed the surface. Mara noted the disturbance, waited until the waves had passed, and repeated the measurement. Both readings stayed in the log. + +Back on shore, the team copied the files to a workstation. One person verified file sizes and hashes while another compared timestamps against the paper log. They found no missing stations. The duplicate records matched byte for byte. + +The analysis began the next morning. The team first plotted the raw measurements without smoothing them. Then they applied the planned calibration and produced a second set of plots. The order mattered: raw evidence had to remain available so that every later transformation could be explained. + +The report separated observations from conclusions. It described the route, weather, instruments, and procedure before discussing patterns in the data. It also listed the limitations of the survey. A single calm day could describe local conditions, but it could not represent every season or every storm. + +When the review was complete, Mara archived the input files, scripts, plots, and written notes together. A future team should be able to repeat the work without guessing which settings had been used. Reproducibility was treated as part of the result, not as an optional task after the result. + +The harbor lights came on as the team left the building. The vessel was already prepared for another trip. Nothing about the process was dramatic, yet the careful sequence of checks made the measurements trustworthy. diff --git a/tests/corpus/correctness-structured.txt b/tests/corpus/correctness-structured.txt new file mode 100644 index 000000000000..a4ebd9434731 --- /dev/null +++ b/tests/corpus/correctness-structured.txt @@ -0,0 +1,61 @@ +{ + "suite": "backend-correctness", + "seed": 123, + "temperature": 0, + "prompt_tokens": 2048, + "depths": [0, 12000, 32000, 64000], + "cache": {"key": "f16", "value": "f16"}, + "backends": ["Vulkan", "ROCm"], + "checks": ["token_ids", "top_1", "complete_logits", "decode_state"] +} + +suite: backend-correctness +oracle: immutable-upstream +candidate: working-tree +settings: + seed: 123 + temperature: 0 + prompt_tokens: 2048 + batch: selected-per-target + ubatch: selected-per-target +checks: + - prompt-token-ids + - prefill-final-logits + - deterministic-decode + - kv-and-recurrent-state + - long-context-boundaries + +| case | backend | depth | prompt | generation | expected | +| stock-0 | Vulkan | 0 | 2048 | 0 | reference | +| stock-12 | Vulkan | 12000 | 2048 | 0 | reference | +| stock-32 | ROCm | 32000 | 2048 | 0 | reference | +| stock-64 | ROCm | 64000 | 2048 | 0 | reference | + +model_id,backend,depth,prompt_tokens,generation_tokens,seed +qwen38,Vulkan,0,2048,0,123 +qwen35,Vulkan,12000,2048,0,123 +gemma4,ROCm,32000,2048,0,123 +gptoss20,ROCm,64000,2048,0,123 + +[request] +method = POST +path = /v1/completions +content_type = application/json + +[sampling] +temperature = 0 +seed = 123 +top_k = 1 + +[validation] +compare_tokens = true +compare_top_1 = true +compare_complete_logits = true +compare_decode = true + +BEGIN RECORD +id=0001 status=pending backend=Vulkan depth=0 +id=0002 status=pending backend=Vulkan depth=12000 +id=0003 status=pending backend=ROCm depth=32000 +id=0004 status=pending backend=ROCm depth=64000 +END RECORD From afd062712166b5180c70c59e8daa6a400e55de5c Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 31 Aug 2026 19:29:28 +0100 Subject: [PATCH 016/212] server : add a `hidden` preset option to omit a model from GET /models (#11) Co-authored-by: Marshall Co-authored-by: Claude Opus 5 --- common/arg.cpp | 6 ++++++ common/arg.h | 1 + tools/server/README.md | 9 +++++++++ tools/server/server-models.cpp | 7 +++++-- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index cf465881fffb..6eaf976d64b4 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -4797,6 +4797,12 @@ void common_params_add_preset_options(std::vector & args) { [](common_params &, const std::string &) { /* unused */ } ).set_env(COMMON_ARG_PRESET_DEDUP_CACHE_MODELS).set_preset_only()); + args.push_back(common_arg( + {"hidden"}, "0|1", + "in server router mode, omit this model from GET /models (it can still be loaded by name)", + [](common_params &, const std::string &) { /* unused */ } + ).set_env(COMMON_ARG_PRESET_HIDDEN).set_preset_only()); + // args.push_back(common_arg( // {"pin"}, // "in server router mode, do not unload this model if models_max is exceeded", diff --git a/common/arg.h b/common/arg.h index 421bc295fc21..766afc6d8c2a 100644 --- a/common/arg.h +++ b/common/arg.h @@ -14,6 +14,7 @@ #define COMMON_ARG_PRESET_LOAD_ON_STARTUP "__PRESET_LOAD_ON_STARTUP" #define COMMON_ARG_PRESET_STOP_TIMEOUT "__PRESET_STOP_TIMEOUT" #define COMMON_ARG_PRESET_DEDUP_CACHE_MODELS "__PRESET_DEDUP_CACHE_MODELS" +#define COMMON_ARG_PRESET_HIDDEN "__PRESET_HIDDEN" // // CLI argument parsing diff --git a/tools/server/README.md b/tools/server/README.md index c6e907ba9199..bca2d5e84e5e 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -1769,6 +1769,15 @@ We also offer additional options that are exclusive to presets (these aren't tre - `load-on-startup` (boolean): Controls whether the model loads automatically when the server starts. Only applies at startup: if the model list is reloaded later (for example after editing the preset file), a newly added model is listed but not loaded - `stop-timeout` (int, seconds): After requested unload, wait for this many seconds before forcing termination (default: 10) - `dedup-cache-models` (boolean): When the preset uses `hf-repo` pointing to a model that is already downloaded, hide the corresponding cached model entry from `GET /models` (the preset entry remains visible). Set it in the `[*]` section to apply to all presets. +- `hidden` (boolean): Omit this model from `GET /models` and `GET /v1/models`. The model can still be loaded and used by requesting it by name, so this only affects listings such as the model picker in the web UI. Useful for entries that are not meant to be picked directly, e.g. a draft/MTP GGUF that is discovered from the HF cache: + + ```ini + ; hides the auto-discovered cache entry, which is only useful as a draft model + [ggml-org/MY-MODEL-MTP-GGUF:Q4_K_M] + hidden = 1 + ``` + + A section whose name matches an existing model merges into that model's entry rather than creating a new one, so a stub section with just `hidden = 1` is enough to hide a model that came from the HF cache or from `--models-dir`. ### Routing requests diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp index db0fac99527b..3ef010b5d979 100644 --- a/tools/server/server-models.cpp +++ b/tools/server/server-models.cpp @@ -626,7 +626,10 @@ void server_models::load_models() { }; auto apply_hidden = [&]() { for (auto & [name, inst] : mapping) { - inst.meta.hidden = hidden_models.count(name) > 0; + std::string val; + bool by_preset = inst.meta.preset.get_option(COMMON_ARG_PRESET_HIDDEN, val) + && common_arg_utils::is_truthy(val); + inst.meta.hidden = by_preset || hidden_models.count(name) > 0; } }; // update_args() injects HOST/PORT/ALIAS, so strip them before comparing presets @@ -1971,7 +1974,7 @@ void server_models_routes::init_routes() { std::time_t t = std::time(0); for (const auto & meta : all_models) { if (meta.hidden) { - continue; // cache model deduplicated by a preset + continue; // preset asked to hide it, or a cache model deduplicated by a preset } json status { {"value", server_model_status_to_string(meta.status)}, From 1c305519519bbbad8e7817c1d5642d7bec0c4e86 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 31 Aug 2026 23:06:39 +0100 Subject: [PATCH 017/212] ui: build the embed host tool with flags the host compiler understands (#13) Co-authored-by: Claude Opus 5 --- tools/ui/CMakeLists.txt | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tools/ui/CMakeLists.txt b/tools/ui/CMakeLists.txt index 208b46a5c15a..6c00c19621c4 100644 --- a/tools/ui/CMakeLists.txt +++ b/tools/ui/CMakeLists.txt @@ -49,10 +49,26 @@ if(CMAKE_CROSSCOMPILING) set(LLAMA_UI_EMBED_EXE "${CMAKE_CURRENT_BINARY_DIR}/llama-ui-embed-host") endif() + # The host compiler is not necessarily the one the toolchain file picked. A + # Windows toolchain file sets CMAKE_SYSTEM_NAME, which makes CMake treat the + # build as cross-compiling even on a Windows host, and HOST_CXX_COMPILER then + # resolves to MSVC. MSVC does not take GNU-style flags: it ignores -std=c++17 + # with only a D9002 warning, drops to C++14, and embed.cpp then fails because + # is C++17-only. Pick the spelling the host compiler understands. + get_filename_component(HOST_CXX_NAME "${HOST_CXX_COMPILER}" NAME_WE) + if(HOST_CXX_NAME STREQUAL "cl") + set(LLAMA_UI_EMBED_FLAGS + /nologo /O2 /EHsc /std:c++17 + "/Fo:${CMAKE_CURRENT_BINARY_DIR}/llama-ui-embed-host.obj" + "/Fe:${LLAMA_UI_EMBED_EXE}") + else() + set(LLAMA_UI_EMBED_FLAGS -O2 -std=c++17 -o "${LLAMA_UI_EMBED_EXE}") + endif() + add_custom_command( OUTPUT "${LLAMA_UI_EMBED_EXE}" - COMMAND "${HOST_CXX_COMPILER}" -O2 -std=c++17 - -o "${LLAMA_UI_EMBED_EXE}" "${CMAKE_CURRENT_SOURCE_DIR}/embed.cpp" + COMMAND "${HOST_CXX_COMPILER}" ${LLAMA_UI_EMBED_FLAGS} + "${CMAKE_CURRENT_SOURCE_DIR}/embed.cpp" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/embed.cpp" COMMENT "Building llama-ui-embed (host)" VERBATIM From c4c03d97b0db3fd6ca110979d7ce5c3d34d8662b Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 31 Aug 2026 23:07:15 +0100 Subject: [PATCH 018/212] vulkan: handle batch sizes > 4 for IQ3_S mat-vec (register spill at NUM_COLS > 4) (#15) Co-authored-by: Marshall --- .../vulkan-shaders/mul_mat_vec_iq3_s.comp | 33 ++++++++++--------- tests/test-backend-ops.cpp | 4 +++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_iq3_s.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_iq3_s.comp index 5cdf2a89d0fd..42f52b4a1273 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_iq3_s.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_iq3_s.comp @@ -7,7 +7,14 @@ layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; FLOAT_TYPE temp[NUM_COLS][NUM_ROWS]; -void calc_superblock(const uint a_offset, const uint b_offset, const uint ib32, const uint i, const uint num_blocks_per_row, const uint first_row, const uint num_rows) { +// invocations per superblock. with many columns, 8 invocations need too many +// registers and spill, so use 16 to halve the per-invocation B working set +const uint TPB = NUM_COLS <= 4 ? 8 : 16; +const uint NL = 32 / TPB; // l steps per invocation + +void calc_superblock(const uint a_offset, const uint b_offset, const uint itid, const uint i, const uint num_blocks_per_row, const uint first_row, const uint num_rows) { + const uint ib32 = itid / (TPB / 8); + const uint l0 = (itid % (TPB / 8)) * NL; const uint y_idx = i * QUANT_K + 32 * ib32; uint ibi = a_offset + first_row * num_blocks_per_row + i; @@ -16,11 +23,8 @@ void calc_superblock(const uint a_offset, const uint b_offset, const uint ib32, const uint scale = (data_a[ibi].scales[ib32/2] >> (4 * (ib32 & 1))) & 0xF; const float dscale = d * (1 + 2 * scale); const uint qh = data_a[ibi].qh[ib32]; - FLOAT_TYPE sum[NUM_COLS]; - [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) { - sum[j] = 0.0; - } - [[unroll]] for (uint l = 0; l < 4; ++l) { + [[unroll]] for (uint ll = 0; ll < NL; ++ll) { + const uint l = l0 + ll; const u8vec2 qs = unpack8(uint32_t(data_a_packed16[ibi].qs[4 * ib32 + l])).xy; // vec4 used due to #12147 const uint sign = data_a[ibi].signs[4 * ib32 + l]; const vec4 grid0 = vec4(unpack8(iq3s_grid[qs.x | ((qh << (8 - 2*l)) & 0x100)])); @@ -30,7 +34,7 @@ void calc_superblock(const uint a_offset, const uint b_offset, const uint ib32, const vec4 b0 = vec4(data_b_v4[(j*p.batch_stride_b + b_offset + y_idx) / 4 + 2*l + 0]); const vec4 b4 = vec4(data_b_v4[(j*p.batch_stride_b + b_offset + y_idx) / 4 + 2*l + 1]); - sum[j] = + const FLOAT_TYPE sum = fma(FLOAT_TYPE(b0.x), FLOAT_TYPE((sign & 1) != 0 ? -grid0.x : grid0.x), fma(FLOAT_TYPE(b0.y), FLOAT_TYPE((sign & 2) != 0 ? -grid0.y : grid0.y), fma(FLOAT_TYPE(b0.z), FLOAT_TYPE((sign & 4) != 0 ? -grid0.z : grid0.z), @@ -39,12 +43,11 @@ void calc_superblock(const uint a_offset, const uint b_offset, const uint ib32, fma(FLOAT_TYPE(b4.y), FLOAT_TYPE((sign & 32) != 0 ? -grid1.y : grid1.y), fma(FLOAT_TYPE(b4.z), FLOAT_TYPE((sign & 64) != 0 ? -grid1.z : grid1.z), fma(FLOAT_TYPE(b4.w), FLOAT_TYPE((sign & 128) != 0 ? -grid1.w : grid1.w), - sum[j])))))))); + FLOAT_TYPE(0.0))))))))); + + temp[j][n] = fma(dscale, sum, temp[j][n]); } } - [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) { - temp[j][n] = fma(dscale, sum[j], temp[j][n]); - } ibi += num_blocks_per_row; } } @@ -55,11 +58,11 @@ void compute_outputs(const uint32_t first_row, const uint32_t num_rows) { const uint num_blocks_per_row = p.ncols / QUANT_K; - // 8 threads are used to process each block - const uint blocks_per_wg = gl_WorkGroupSize.x/8; + // TPB invocations are used to process each block + const uint blocks_per_wg = gl_WorkGroupSize.x/TPB; const uint tid = gl_LocalInvocationID.x; - const uint itid = tid % 8; // 0...7 - const uint ix = tid / 8; + const uint itid = tid % TPB; + const uint ix = tid / TPB; [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) { [[unroll]] for (uint i = 0; i < NUM_ROWS; ++i) { diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 996b88db296e..a9d91acf6ee7 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -9241,6 +9241,10 @@ static std::vector> make_test_cases_eval() { //test_cases.emplace_back(new test_mul_mat(type_a, GGML_TYPE_F32, 18, i, 32*256, { 1, 1}, {8, 1})); //test_cases.emplace_back(new test_mul_mat(type_a, GGML_TYPE_F32, 19, i, 33*256, { 1, 1}, {1, 1})); } + // mat-vec shaders split k across lanes and loop over the blocks in strides. k must be + // long enough that the loop wraps, else the stride is never exercised + test_cases.emplace_back(new test_mul_mat(type_a, GGML_TYPE_F32, 16, 1, 16*256, { 1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(type_a, GGML_TYPE_F32, 16, 8, 16*256, { 1, 1}, {1, 1})); } test_cases.emplace_back(new test_mul_mat(GGML_TYPE_Q4_0, GGML_TYPE_F32, 2880, 32, 2880, {1, 1}, {1, 1})); From 4b5fe5f4a9ed89d760c5b6a706d745019f4db2ab Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 31 Aug 2026 23:07:55 +0100 Subject: [PATCH 019/212] vulkan: gate the UMA bulk readback on host-cached mappings (#12) Co-authored-by: Marshall --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 394c84bf257e..f283e2f166f4 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -8677,7 +8677,13 @@ static void ggml_vk_buffer_read_2d(vk_buffer& src, size_t offset, void * dst, si // If the device is not an UMA device the memory is host-accessible through rebar. While writing // through PCIe is sufficient fast reading back data from PCIe is slower than going through // the HW device to host copy path. - if(src->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible && src->device->uma) { + // On UMA a direct CPU read is only fast from a host-cached mapping. A write-combined mapping + // (host-visible without HOST_CACHED, which is what amdgpu gives for GTT) reads back at + // uncached speed, so bulk reads must use the device copy path. Small reads stay direct to + // avoid the fence round trip. + const bool host_cached = bool(src->memory_property_flags & vk::MemoryPropertyFlagBits::eHostCached); + if((src->memory_property_flags & vk::MemoryPropertyFlagBits::eHostVisible) && src->device->uma && + (host_cached || width * height <= 64 * 1024)) { GGML_ASSERT(src->memory_property_flags & vk::MemoryPropertyFlagBits::eHostCoherent); std::lock_guard guard(src->device->mutex); From 5b6308d71a5dff94995af67a2598f230ee8178fe Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:23:16 +0200 Subject: [PATCH 020/212] hip: optimize quantized matmul on RDNA3.5 Tune MMQ and MMVQ scheduling, add routed expert kernels, share Q8_1 activation quantization, and add measured four-column kernels for quant formats that outperform the generic path on gfx1151. Assisted-by: OpenCode --- ggml/include/ggml.h | 1 + ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh | 69 ++-- ggml/src/ggml-cuda/mmq-q45.cu | 13 + ggml/src/ggml-cuda/mmq-q6.cu | 9 + ggml/src/ggml-cuda/mmq-q8.cu | 9 + ggml/src/ggml-cuda/mmq-vec-dot.cuh | 13 +- ggml/src/ggml-cuda/mmq.cu | 249 +++++++++++- ggml/src/ggml-cuda/mmq.cuh | 194 +++++++++- ggml/src/ggml-cuda/mmvf.cu | 102 +++-- ggml/src/ggml-cuda/mmvq.cu | 443 +++++++++++++++++++++- ggml/src/ggml-cuda/quantize.cu | 192 +++++++--- ggml/src/ggml-cuda/quantize.cuh | 7 + 12 files changed, 1144 insertions(+), 157 deletions(-) create mode 100644 ggml/src/ggml-cuda/mmq-q45.cu create mode 100644 ggml/src/ggml-cuda/mmq-q6.cu create mode 100644 ggml/src/ggml-cuda/mmq-q8.cu diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h index 26f31232f676..b8b01b2b8920 100644 --- a/ggml/include/ggml.h +++ b/ggml/include/ggml.h @@ -443,6 +443,7 @@ extern "C" { enum ggml_op_hint { GGML_HINT_NONE = 0, GGML_HINT_SRC0_IS_HADAMARD = 1, + GGML_HINT_EXACT_BATCH = 2, }; // model file types diff --git a/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh b/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh index 180b2d9370d1..f8a9e6350926 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh @@ -79,15 +79,15 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf CASE(GGML_TYPE_Q8_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q8_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q8_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q8_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q8_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q8_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q8_0, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q8_0, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q8_0, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q8_0, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 256, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); // --------------------------------------------------------------------------------------------- @@ -115,42 +115,42 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf CASE(GGML_TYPE_Q4_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q4_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q4_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q4_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q5_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q5_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q5_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q5_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q5_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q6_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q6_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); CASE(GGML_TYPE_Q6_K, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); CASE(GGML_TYPE_Q6_K, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q6_K, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); // --------------------------------------------------------------------------------------------- @@ -212,7 +212,7 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); @@ -263,7 +263,7 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_MXFP4, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); @@ -288,3 +288,14 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true); } + +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_Q8_0, 128, false).nthreads == 256); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_Q8_0, 128, false).I == 64); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_Q8_0, 128, true).nthreads == 128); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_Q8_0, 128, true).I == 64); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_Q8_0, 48, false).nthreads == 128); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_Q8_0, 48, false).I == 64); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_Q8_0, 48, true).nthreads == 256); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_Q8_0, 48, true).I == 128); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_MXFP4, 128, true).nthreads == 128); +static_assert(ggml_cuda_mmq_get_config_rdna3_5(GGML_TYPE_MXFP4, 128, true).I == 64); diff --git a/ggml/src/ggml-cuda/mmq-q45.cu b/ggml/src/ggml-cuda/mmq-q45.cu new file mode 100644 index 000000000000..2b94d38ad70f --- /dev/null +++ b/ggml/src/ggml-cuda/mmq-q45.cu @@ -0,0 +1,13 @@ +#define mul_mat_q mul_mat_q_q45 +#define launch_mul_mat_q launch_mul_mat_q_q45 +#include "mmq.cuh" +#undef launch_mul_mat_q +#undef mul_mat_q + +void launch_mul_mat_q_q4_k_j48(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream) { + launch_mul_mat_q_q45(ctx, args, stream); +} + +void launch_mul_mat_q_q5_k_j32(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream) { + launch_mul_mat_q_q45(ctx, args, stream); +} diff --git a/ggml/src/ggml-cuda/mmq-q6.cu b/ggml/src/ggml-cuda/mmq-q6.cu new file mode 100644 index 000000000000..ed9548f79608 --- /dev/null +++ b/ggml/src/ggml-cuda/mmq-q6.cu @@ -0,0 +1,9 @@ +#define mul_mat_q mul_mat_q_q6 +#define launch_mul_mat_q launch_mul_mat_q_q6 +#include "mmq.cuh" +#undef launch_mul_mat_q +#undef mul_mat_q + +void launch_mul_mat_q_q6_k_j32(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream) { + launch_mul_mat_q_q6(ctx, args, stream); +} diff --git a/ggml/src/ggml-cuda/mmq-q8.cu b/ggml/src/ggml-cuda/mmq-q8.cu new file mode 100644 index 000000000000..4cb9589c715c --- /dev/null +++ b/ggml/src/ggml-cuda/mmq-q8.cu @@ -0,0 +1,9 @@ +#define mul_mat_q mul_mat_q_q8 +#define launch_mul_mat_q launch_mul_mat_q_q8 +#include "mmq.cuh" +#undef launch_mul_mat_q +#undef mul_mat_q + +void launch_mul_mat_q_q8_0_j48(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream) { + launch_mul_mat_q_q8(ctx, args, stream); +} diff --git a/ggml/src/ggml-cuda/mmq-vec-dot.cuh b/ggml/src/ggml-cuda/mmq-vec-dot.cuh index d573433865f8..2493ebea6f24 100644 --- a/ggml/src/ggml-cuda/mmq-vec-dot.cuh +++ b/ggml/src/ggml-cuda/mmq-vec-dot.cuh @@ -152,8 +152,14 @@ static __device__ __forceinline__ void ggml_cuda_mmq_vec_dot_q8_0_q8_1_mma( constexpr int sram_stride = ggml_cuda_mmq_get_sram_stride(type, J, fallback); constexpr int rows_per_warp = ggml_cuda_mmq_get_rows_per_warp(type, J, fallback); constexpr int ntx = rows_per_warp/tile_C::I; // Number of x minitiles per warp. + constexpr int nwarps = ggml_cuda_mmq_get_nthreads(type, J, fallback) / ggml_cuda_get_physical_warp_size(); + constexpr bool split_j = type == GGML_TYPE_Q8_0 && J == 128 && !fallback && I == 64 && nwarps == 8; + constexpr int j_group = split_j ? J/2 : J; - y += (threadIdx.y % ntx) * (tile_C::J*MMQ_TILE_Y_K); + const int warp_i = split_j ? threadIdx.y % 4 : threadIdx.y; + const int warp_j = split_j ? threadIdx.y / 4 : 0; + + y += (warp_j*j_group + (warp_i % ntx)*tile_C::J) * MMQ_TILE_Y_K; const int * x_qs = (const int *) x; const float * x_df = (const float *) x_qs + 2*MMQ_TILE_NE_K; @@ -161,7 +167,7 @@ static __device__ __forceinline__ void ggml_cuda_mmq_vec_dot_q8_0_q8_1_mma( const float * y_df = (const float *) y; const half2 * y_ds = (const half2 *) y; - const int i0 = (threadIdx.y / ntx) * rows_per_warp; + const int i0 = (warp_i / ntx) * rows_per_warp; for (int k01 = 0; k01 < MMQ_TILE_NE_K; k01 += QI8_0) { const int k0 = k00 + k01; @@ -173,7 +179,7 @@ static __device__ __forceinline__ void ggml_cuda_mmq_vec_dot_q8_0_q8_1_mma( } #pragma unroll - for (int j0 = 0; j0 < J; j0 += ntx*tile_C::J) { + for (int j0 = 0; j0 < j_group; j0 += ntx*tile_C::J) { tile_B B; load_ldmatrix(B, y_qs + j0*MMQ_TILE_Y_K + k01, MMQ_TILE_Y_K); @@ -1248,4 +1254,3 @@ template static __device__ __forceinline_ } } } - diff --git a/ggml/src/ggml-cuda/mmq.cu b/ggml/src/ggml-cuda/mmq.cu index 7fb4401489c9..de18e2064877 100644 --- a/ggml/src/ggml-cuda/mmq.cu +++ b/ggml/src/ggml-cuda/mmq.cu @@ -5,6 +5,62 @@ #include +void launch_mul_mat_q_q4_k_j48(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream); +void launch_mul_mat_q_q5_k_j32(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream); +void launch_mul_mat_q_q6_k_j32(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream); +void launch_mul_mat_q_q8_0_j48(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream); + +static bool use_rdna3_5_q45_id_narrow(const mmq_args & args) { + const int id = ggml_cuda_get_device(); + const int cc = ggml_cuda_info().devices[id].cc; + if ((args.type_x != GGML_TYPE_Q4_K && args.type_x != GGML_TYPE_Q5_K) || !GGML_CUDA_CC_IS_RDNA3_5(cc) || + args.ids_dst == nullptr || args.nchannels_y <= 0) { + return false; + } + + const int64_t rows_per_channel = (args.ncols_dst + args.nchannels_y - 1) / args.nchannels_y; + const bool ornith = ((args.ncols_x == 2048 && args.nrows_x == 512) || + (args.ncols_x == 512 && args.nrows_x == 2048)) && args.nchannels_x == 256 && rows_per_channel == 64; + const bool qwen122 = ((args.ncols_x == 3072 && args.nrows_x == 1024) || + (args.ncols_x == 1024 && args.nrows_x == 3072)) && args.nchannels_x == 256 && rows_per_channel == 64; + return ornith || qwen122; +} + +static bool use_rdna3_5_q5_ling(const mmq_args & args) { + const int id = ggml_cuda_get_device(); + const int cc = ggml_cuda_info().devices[id].cc; + return args.type_x == GGML_TYPE_Q5_K && GGML_CUDA_CC_IS_RDNA3_5(cc) && args.ids_dst != nullptr && + ((args.ncols_x == 2560 && args.nrows_x == 768) || (args.ncols_x == 768 && args.nrows_x == 2560)) && + args.nchannels_x == 512 && args.nchannels_y > 0 && + (args.ncols_dst + args.nchannels_y - 1) / args.nchannels_y == 32; +} + +static bool use_rdna3_5_q6_ling_j32(const mmq_args & args) { + const int id = ggml_cuda_get_device(); + const int cc = ggml_cuda_info().devices[id].cc; + return args.type_x == GGML_TYPE_Q6_K && GGML_CUDA_CC_IS_RDNA3_5(cc) && args.ids_dst != nullptr && + args.ncols_x == 768 && args.nrows_x == 2560 && args.nchannels_x == 512 && args.nchannels_y > 0 && + (args.ncols_dst + args.nchannels_y - 1) / args.nchannels_y == 32; +} + +static bool use_rdna3_5_q6_id_narrow(const mmq_args & args) { + const int id = ggml_cuda_get_device(); + const int cc = ggml_cuda_info().devices[id].cc; + return args.type_x == GGML_TYPE_Q6_K && GGML_CUDA_CC_IS_RDNA3_5(cc) && args.ids_dst != nullptr && + ((args.ncols_x == 2048 && args.nrows_x == 512) || (args.ncols_x == 512 && args.nrows_x == 2048)) && + args.nchannels_x == 256 && args.nchannels_y > 0 && + (args.ncols_dst + args.nchannels_y - 1) / args.nchannels_y == 64; +} + +static bool use_rdna3_5_q8_id_narrow(const mmq_args & args) { + const int id = ggml_cuda_get_device(); + const int cc = ggml_cuda_info().devices[id].cc; + return args.type_x == GGML_TYPE_Q8_0 && GGML_CUDA_CC_IS_RDNA3_5(cc) && args.ids_dst != nullptr && + ((args.ncols_x == 2048 && args.nrows_x == 512) || (args.ncols_x == 512 && args.nrows_x == 2048)) && + args.nchannels_x == 256 && args.nchannels_y > 0 && + (args.ncols_dst + args.nchannels_y - 1) / args.nchannels_y == 64; +} + static void ggml_cuda_mul_mat_q_switch_type(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream) { switch (args.type_x) { case GGML_TYPE_Q1_0: @@ -26,7 +82,11 @@ static void ggml_cuda_mul_mat_q_switch_type(ggml_backend_cuda_context & ctx, con mul_mat_q_case(ctx, args, stream); break; case GGML_TYPE_Q8_0: - mul_mat_q_case(ctx, args, stream); + if (use_rdna3_5_q8_id_narrow(args)) { + launch_mul_mat_q_q8_0_j48(ctx, args, stream); + } else { + mul_mat_q_case(ctx, args, stream); + } break; // ----------------------------------------------------------------------- case GGML_TYPE_Q2_K: @@ -36,13 +96,27 @@ static void ggml_cuda_mul_mat_q_switch_type(ggml_backend_cuda_context & ctx, con mul_mat_q_case(ctx, args, stream); break; case GGML_TYPE_Q4_K: - mul_mat_q_case(ctx, args, stream); + if (use_rdna3_5_q45_id_narrow(args)) { + launch_mul_mat_q_q4_k_j48(ctx, args, stream); + } else { + mul_mat_q_case(ctx, args, stream); + } break; case GGML_TYPE_Q5_K: - mul_mat_q_case(ctx, args, stream); + if (use_rdna3_5_q5_ling(args)) { + launch_mul_mat_q_q5_k_j32(ctx, args, stream); + } else if (use_rdna3_5_q45_id_narrow(args)) { + launch_mul_mat_q_q5_k_j32(ctx, args, stream); + } else { + mul_mat_q_case(ctx, args, stream); + } break; case GGML_TYPE_Q6_K: - mul_mat_q_case(ctx, args, stream); + if (use_rdna3_5_q6_ling_j32(args) || use_rdna3_5_q6_id_narrow(args)) { + launch_mul_mat_q_q6_k_j32(ctx, args, stream); + } else { + mul_mat_q_case(ctx, args, stream); + } break; // ----------------------------------------------------------------------- case GGML_TYPE_IQ1_S: @@ -82,8 +156,141 @@ static void ggml_cuda_mul_mat_q_switch_type(ggml_backend_cuda_context & ctx, con } } -void ggml_cuda_mul_mat_q( - ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst) { +void ggml_cuda_mul_mat_q_pair(ggml_backend_cuda_context & ctx, ggml_tensor * dst0, ggml_tensor * dst1) { + GGML_ASSERT(dst0->src[1] == dst1->src[1]); + GGML_ASSERT(dst0->src[2] == dst1->src[2]); + + const ggml_tensor * src0s[] = { dst0->src[0], dst1->src[0] }; + ggml_tensor * dsts[] = { dst0, dst1 }; + const ggml_tensor * src1 = dst0->src[1]; + const ggml_tensor * ids = dst0->src[2]; + const ggml_tensor * src0 = src0s[0]; + + GGML_ASSERT(src1->type == GGML_TYPE_F32); + if (ids == nullptr) { + const int cc = ggml_cuda_info().devices[ggml_cuda_get_device()].cc; + cudaStream_t stream = ctx.stream(); + const int64_t ne10_padded = GGML_PAD(src1->ne[0], MATRIX_ROW_PADDING); + size_t nbytes_src1_q8_1 = src1->ne[3] * src1->ne[2] * src1->ne[1] * ne10_padded * sizeof(block_q8_1_mmq) / QK8_1_MMQ; + + for (int i = 0; i < 2; ++i) { + const ggml_tensor * src0_i = src0s[i]; + ggml_tensor * dst_i = dsts[i]; + GGML_ASSERT(dst_i->type == GGML_TYPE_F32 && dst_i->src[1] == src1); + GGML_ASSERT(ggml_are_same_shape(src0, src0_i) && ggml_are_same_shape(dst0, dst_i)); + GGML_ASSERT(src0_i->ne[0] == src1->ne[0]); + GGML_ASSERT(mmq_get_q8_1_ds_layout(src0->type) == mmq_get_q8_1_ds_layout(src0_i->type)); + const bool fallback = src0_i->ne[1] % 128 != 0; + nbytes_src1_q8_1 = std::max(nbytes_src1_q8_1, + src1->ne[3] * src1->ne[2] * src1->ne[1] * ne10_padded * sizeof(block_q8_1_mmq) / QK8_1_MMQ + + ggml_cuda_mmq_get_J_max(src0_i->type, fallback, cc, src1->ne[1]) * sizeof(block_q8_1_mmq)); + } + + ggml_cuda_pool_alloc src1_q8_1(ctx.pool(), nbytes_src1_q8_1); + quantize_mmq_q8_1_cuda((const float *) src1->data, nullptr, src1_q8_1.get(), src0->type, + src1->ne[0], src1->nb[1] / sizeof(float), src1->nb[2] / sizeof(float), src1->nb[3] / sizeof(float), + ne10_padded, src1->ne[1], src1->ne[2], src1->ne[3], stream); + CUDA_CHECK(cudaGetLastError()); + + const int64_t s12_q = src1->ne[1] * ne10_padded * sizeof(block_q8_1) / (QK8_1 * sizeof(int)); + const int64_t s13_q = src1->ne[2] * s12_q; + for (int i = 0; i < 2; ++i) { + const ggml_tensor * src0_i = src0s[i]; + ggml_tensor * dst_i = dsts[i]; + GGML_ASSERT(ggml_are_same_shape(src0, src0_i) && ggml_are_same_shape(dst0, dst_i)); + const mmq_args args = { + (const char *) src0_i->data, src0_i->type, (const int *) src1_q8_1.get(), nullptr, nullptr, (float *) dst_i->data, + nullptr, + src0_i->ne[0], src0_i->ne[1], src1->ne[1], (int64_t) (src0_i->nb[1] / ggml_type_size(src0_i->type)), src1->ne[1], (int64_t) (dst_i->nb[1] / sizeof(float)), + src0_i->ne[2], src1->ne[2], (int64_t) (src0_i->nb[2] / ggml_type_size(src0_i->type)), s12_q, (int64_t) (dst_i->nb[2] / sizeof(float)), + src0_i->ne[3], src1->ne[3], (int64_t) (src0_i->nb[3] / ggml_type_size(src0_i->type)), s13_q, (int64_t) (dst_i->nb[3] / sizeof(float)), + src1->ne[1]}; + ggml_cuda_mul_mat_q_switch_type(ctx, args, stream); + } + return; + } + + GGML_ASSERT(ids->type == GGML_TYPE_I32); + GGML_ASSERT(src1->ne[3] == 1); + GGML_ASSERT(src1->nb[2] % src1->nb[1] == 0); + GGML_ASSERT(ids->nb[0] == ggml_element_size(ids)); + + const int cc = ggml_cuda_info().devices[ggml_cuda_get_device()].cc; + cudaStream_t stream = ctx.stream(); + const int64_t n_expert_used = ids->ne[0]; + const int64_t n_tokens = src1->ne[2]; + const int64_t ne_get_rows = n_tokens*n_expert_used; + const int64_t ne10_padded = GGML_PAD(src1->ne[0], MATRIX_ROW_PADDING); + const bool dedup_bcast = src1->ne[1] == 1 && n_expert_used > 1; + + ggml_cuda_pool_alloc ids_src1(ctx.pool(), ne_get_rows); + ggml_cuda_pool_alloc ids_dst(ctx.pool(), ne_get_rows); + ggml_cuda_pool_alloc expert_bounds(ctx.pool(), src0->ne[2] + 1); + + const int si1 = ids->nb[1] / ggml_element_size(ids); + const int sis1 = src1->nb[2] / src1->nb[1]; + ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), expert_bounds.get(), + src0->ne[2], n_tokens, n_expert_used, src1->ne[1], si1, sis1, /*write_inverse =*/ dedup_bcast, stream); + CUDA_CHECK(cudaGetLastError()); + + size_t nbytes_src1_q8_1 = n_tokens*n_expert_used*ne10_padded * sizeof(block_q8_1_mmq)/QK8_1_MMQ; + for (int i = 0; i < 2; ++i) { + const ggml_tensor * src0_i = src0s[i]; + ggml_tensor * dst_i = dsts[i]; + GGML_ASSERT(dst_i->type == GGML_TYPE_F32); + GGML_ASSERT(dst_i->nb[2] % dst_i->nb[1] == 0); + GGML_ASSERT(ggml_are_same_shape(src0, src0_i)); + GGML_ASSERT(ggml_are_same_shape(dst0, dst_i)); + GGML_ASSERT(mmq_get_q8_1_ds_layout(src0->type) == mmq_get_q8_1_ds_layout(src0_i->type)); + GGML_ASSERT(src0_i->ne[0] == src1->ne[0]); + GGML_ASSERT(dst_i->ne[1] == n_expert_used); + + const bool fallback = src0_i->ne[1] % 128 != 0; + nbytes_src1_q8_1 = std::max(nbytes_src1_q8_1, + n_tokens*n_expert_used*ne10_padded * sizeof(block_q8_1_mmq)/QK8_1_MMQ + + ggml_cuda_mmq_get_J_max(src0_i->type, fallback, cc, src1->ne[1]) * sizeof(block_q8_1_mmq)); + } + + ggml_cuda_pool_alloc src1_q8_1(ctx.pool(), nbytes_src1_q8_1); + const int64_t s11 = src1->nb[1] / ggml_type_size(src1->type); + const int64_t s12_src = src1->nb[2] / ggml_type_size(src1->type); + const int64_t s13_src = src1->nb[3] / ggml_type_size(src1->type); + if (dedup_bcast) { + quantize_scatter_mmq_q8_1_cuda((const float *) src1->data, ids_src1.get(), src1_q8_1.get(), src0->type, + src1->ne[0], /*stride_token=*/s12_src, ne10_padded, n_tokens, ne_get_rows, n_expert_used, stream); + } else { + quantize_mmq_q8_1_cuda((const float *) src1->data, ids_src1.get(), src1_q8_1.get(), src0->type, + src1->ne[0], s11, s12_src, s13_src, ne10_padded, ne_get_rows, 1, 1, stream); + } + CUDA_CHECK(cudaGetLastError()); + + const int64_t s12_q = src1->ne[1] * ne10_padded * sizeof(block_q8_1) / (QK8_1 * sizeof(int)); + const int64_t s13_q = n_tokens*s12_q; + for (int i = 0; i < 2; ++i) { + const ggml_tensor * src0_i = src0s[i]; + ggml_tensor * dst_i = dsts[i]; + const size_t ts_src0 = ggml_type_size(src0_i->type); + const size_t ts_dst = ggml_type_size(dst_i->type); + const int64_t s01 = src0_i->nb[1] / ts_src0; + const int64_t s02 = src0_i->nb[2] / ts_src0; + const int64_t s03 = src0_i->nb[3] / ts_src0; + const int64_t s1 = dst_i->nb[1] / ts_dst; + const int64_t s2 = dst_i->nb[2] / ts_dst; + const int64_t s3 = dst_i->nb[3] / ts_dst; + const mmq_args args = { + (const char *) src0_i->data, src0_i->type, (const int *) src1_q8_1.get(), ids_dst.get(), expert_bounds.get(), (float *) dst_i->data, + nullptr, + src0_i->ne[0], src0_i->ne[1], ne_get_rows, s01, ne_get_rows, s1, + src0_i->ne[2], src0_i->ne[2], s02, s12_q, s2, + src0_i->ne[3], src1->ne[3], s03, s13_q, s3, + n_tokens}; + ggml_cuda_mul_mat_q_switch_type(ctx, args, stream); + } +} + +static void ggml_cuda_mul_mat_q_impl( + ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, + ggml_tensor * dst, const ggml_tensor * swiglu) { GGML_ASSERT( src1->type == GGML_TYPE_F32); GGML_ASSERT( dst->type == GGML_TYPE_F32); GGML_ASSERT(!ids || ids->type == GGML_TYPE_I32); // Optional, used for batched GGML_MUL_MAT_ID. @@ -102,6 +309,14 @@ void ggml_cuda_mul_mat_q( GGML_ASSERT( nb0 == ts_dst); GGML_ASSERT(!ids || ids->nb[0] == ggml_type_size(ids->type)); + const ggml_tensor * gate = swiglu ? swiglu->src[0] : nullptr; + const ggml_tensor * up = swiglu ? swiglu->src[1] : nullptr; + if (swiglu) { + GGML_ASSERT(src0->type == GGML_TYPE_Q8_0); + GGML_ASSERT(gate && up && gate->type == GGML_TYPE_F32 && up->type == GGML_TYPE_F32); + GGML_ASSERT(ggml_are_same_shape(gate, up) && ggml_are_same_shape(gate, src1)); + } + const char * src0_d = (const char *) src0->data; const float * src1_d = (const float *) src1->data; float * dst_d = (float *) dst->data; @@ -152,6 +367,12 @@ void ggml_cuda_mul_mat_q( quantize_mmq_fp4_cuda(src1_d, nullptr, src1_q8_1.get(), src1_scale.ptr, src0->type, use_aligned_float8, ne10, s11, s12, s13, ne10_padded, ne11, ne12, ne13, stream); + } else if (swiglu) { + quantize_mmq_q8_1_swiglu_cuda( + (const float *) gate->data, (const float *) up->data, nullptr, src1_q8_1.get(), src0->type, + ne10, ne10_padded, ne11 * ne12 * ne13, /*logical_n1=*/1, + gate->nb[1] / sizeof(float), gate->nb[1] / sizeof(float), + up->nb[1] / sizeof(float), up->nb[1] / sizeof(float), stream); } else { quantize_mmq_q8_1_cuda(src1_d, nullptr, src1_q8_1.get(), src0->type, ne10, s11, s12, s13, ne10_padded, ne11, ne12, ne13, stream); @@ -229,6 +450,12 @@ void ggml_cuda_mul_mat_q( quantize_mmq_fp4_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src1_scale.ptr, src0->type, use_aligned_float8, ne10, s11, s12, s13, ne10_padded, ne11_flat, ne12_flat, ne13_flat, stream); } + } else if (swiglu) { + quantize_mmq_q8_1_swiglu_cuda( + (const float *) gate->data, (const float *) up->data, ids_src1.get(), src1_q8_1.get(), src0->type, + ne10, ne10_padded, ne11_flat, n_expert_used, + gate->nb[1] / sizeof(float), gate->nb[2] / sizeof(float), + up->nb[1] / sizeof(float), up->nb[2] / sizeof(float), stream); } else if (dedup_bcast) { quantize_scatter_mmq_q8_1_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src0->type, ne10, /*stride_token=*/s12, ne10_padded, ne12, ne11_flat, n_expert_used, stream); @@ -256,6 +483,16 @@ void ggml_cuda_mul_mat_q( ggml_cuda_mul_mat_q_switch_type(ctx, args, stream); } +void ggml_cuda_mul_mat_q( + ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst) { + ggml_cuda_mul_mat_q_impl(ctx, src0, src1, ids, dst, nullptr); +} + +void ggml_cuda_mul_mat_q_swiglu( + ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * ids, ggml_tensor * dst, const ggml_tensor * swiglu) { + ggml_cuda_mul_mat_q_impl(ctx, src0, swiglu, ids, dst, swiglu); +} + bool ggml_cuda_should_use_mmq(enum ggml_type type, int cc, int64_t ne11, int64_t n_experts) { #ifdef GGML_CUDA_FORCE_CUBLAS return false; diff --git a/ggml/src/ggml-cuda/mmq.cuh b/ggml/src/ggml-cuda/mmq.cuh index c978b4421c5f..741c346f0666 100644 --- a/ggml/src/ggml-cuda/mmq.cuh +++ b/ggml/src/ggml-cuda/mmq.cuh @@ -486,18 +486,22 @@ static __device__ __forceinline__ void ggml_cuda_mmq_write_back_mma( constexpr int I = ggml_cuda_mmq_get_I(type, J, fallback); constexpr int rows_per_warp = ggml_cuda_mmq_get_rows_per_warp(type, J, fallback); constexpr int ntx = rows_per_warp/tile_C::I; // Number of x minitiles per warp. + constexpr bool split_j = type == GGML_TYPE_Q8_0 && J == 128 && !fallback && I == 64 && nwarps == 8; + constexpr int j_group = split_j ? J/2 : J; - const int i0 = (threadIdx.y / ntx) * (ntx*tile_C::I); + const int warp_i = split_j ? threadIdx.y % 4 : threadIdx.y; + const int warp_j = split_j ? threadIdx.y / 4 : 0; + const int i0 = (warp_i / ntx) * (ntx*tile_C::I); const bool y_scale_used = y_scale != nullptr; #pragma unroll - for (int j0 = 0; j0 < J; j0 += ntx*tile_C::J) { + for (int j0 = 0; j0 < j_group; j0 += ntx*tile_C::J) { #pragma unroll for (int n = 0; n < ntx; ++n) { #pragma unroll for (int l = 0; l < tile_C::ne; ++l) { - const int j = j0 + (threadIdx.y % ntx) * tile_C::J + tile_C::get_j(l); + const int j = warp_j*j_group + j0 + (warp_i % ntx)*tile_C::J + tile_C::get_j(l); if (j > j_max) { continue; @@ -973,21 +977,7 @@ static __global__ void mul_mat_q( const uint32_t nty = (nrows_x + I - 1) / I; // Number of tiles y - // Initialize the ids for writing back data with just the index. - // For regular matrix multiplications this is never changed. - // For MoE the correct indices are loaded from ids_dst. extern __shared__ int ids_dst_shared[]; // Stored at beginning of shared memory. -#pragma unroll - for (int j0 = 0; j0 < J; j0 += nwarps*warp_size) { - const int j = j0 + threadIdx.y*warp_size + threadIdx.x; - - if (j0 + nwarps*warp_size > J && j >= J) { - break; - } - - ids_dst_shared[j] = j; - } - __syncthreads(); if constexpr (!ggml_cuda_mmq_get_stream_k(type, J, fallback)) { const uint2 tmp2 = fast_div_modulo(blockIdx.z, nchannels_y); @@ -1036,6 +1026,18 @@ static __global__ void mul_mat_q( ids_dst_shared[j] = ids_dst[col_low + jt*J + j]; } __syncthreads(); + } else { +#pragma unroll + for (int j0 = 0; j0 < J; j0 += nwarps*warp_size) { + const int j = j0 + threadIdx.y*warp_size + threadIdx.x; + + if (j0 + nwarps*warp_size > J && j >= J) { + break; + } + + ids_dst_shared[j] = j; + } + __syncthreads(); } offset_y += (col_low + jt*J)*(sizeof(block_q8_1_mmq)/sizeof(int)); @@ -1059,6 +1061,18 @@ static __global__ void mul_mat_q( return; } +#pragma unroll + for (int j0 = 0; j0 < J; j0 += nwarps*warp_size) { + const int j = j0 + threadIdx.y*warp_size + threadIdx.x; + + if (j0 + nwarps*warp_size > J && j >= J) { + break; + } + + ids_dst_shared[j] = j; + } + __syncthreads(); + constexpr int ITER_K = ggml_cuda_mmq_get_K_vram(type, J, fallback); constexpr int blocks_per_iter = ITER_K / qk; @@ -1236,6 +1250,90 @@ static __global__ void mul_mat_q( tile_x_max_i, tile_y_max_j, kb0_start, kb0_stop); } +template +static __global__ void build_mmq_routed_descriptors( + const int32_t * expert_bounds, uint32_t * descriptors, int n_experts, int max_descriptors) { + __shared__ int tile_counts[256]; + const int expert = threadIdx.x; + + for (int i = expert; i < max_descriptors; i += blockDim.x) { + descriptors[i] = UINT32_MAX; + } + + if (expert < n_experts) { + const int count = expert_bounds[expert + 1] - expert_bounds[expert]; + tile_counts[expert] = (count + J - 1) / J; + } + __syncthreads(); + + if (expert < n_experts) { + int descriptor = 0; + for (int i = 0; i < expert; ++i) { + descriptor += tile_counts[i]; + } + for (int jt = 0; jt < tile_counts[expert]; ++jt) { + descriptors[descriptor + jt] = uint32_t(expert) | (uint32_t(jt) << 16); + } + } +} + +template +__launch_bounds__(ggml_cuda_mmq_get_nthreads(type, J, fallback), ggml_cuda_mmq_get_occupancy(type, J, fallback)) +static __global__ void mul_mat_q_routed_compact( + const char * __restrict__ x, const int * __restrict__ y, const int32_t * __restrict__ ids_dst, + const int32_t * __restrict__ expert_bounds, const uint32_t * __restrict__ descriptors, + const int max_descriptors, float * __restrict__ dst, const float * __restrict__ y_scale, + const uint3 blocks_per_ne00, const int nrows_x, const int stride_row_x, const int ncols_y, const int stride_col_dst, + const uint3 channel_ratio, const int stride_channel_x, + const uint3 sample_ratio, const int stride_sample_x) { + constexpr int warp_size = ggml_cuda_get_physical_warp_size(); + constexpr int nwarps = ggml_cuda_mmq_get_nthreads(type, J, fallback) / warp_size; + constexpr int I = ggml_cuda_mmq_get_I(type, J, fallback); + + const int wt = blockIdx.z; + const int it = blockIdx.x; + + extern __shared__ int ids_dst_shared[]; + for (int descriptor_idx = blockIdx.y; descriptor_idx < max_descriptors; descriptor_idx += gridDim.y) { + const uint32_t descriptor = descriptors[descriptor_idx]; + if (descriptor == UINT32_MAX) { + return; + } + + const int zt = descriptor & 0xFFFF; + const int jt = descriptor >> 16; + const int col_low = expert_bounds[zt + 0]; + const int col_high = expert_bounds[zt + 1]; + const int col_diff = col_high - col_low; + + if (descriptor_idx != int(blockIdx.y)) { + __syncthreads(); + } +#pragma unroll + for (int j0 = 0; j0 < J; j0 += nwarps * warp_size) { + const int j = j0 + threadIdx.y * warp_size + threadIdx.x; + if (j0 + nwarps * warp_size > J && j >= J) { + break; + } + ids_dst_shared[j] = ids_dst[col_low + jt * J + j]; + } + __syncthreads(); + + const int offset_x = fastdiv(wt, sample_ratio) * stride_sample_x + + fastdiv(zt, channel_ratio) * stride_channel_x + it * I * stride_row_x; + const int offset_y = (col_low + jt * J) * (sizeof(block_q8_1_mmq) / sizeof(int)); + const int tile_x_max_i = nrows_x - it * I - 1; + const int tile_y_max_j = col_diff - jt * J - 1; + const float * y_scale_tile = y_scale ? y_scale + col_low + jt * J : nullptr; + + constexpr bool fixup = false; + mul_mat_q_process_tile + (x, offset_x, y + offset_y, ids_dst_shared, dst + it * I, nullptr, y_scale_tile, + stride_row_x, ncols_y, stride_col_dst, + tile_x_max_i, tile_y_max_j, 0, blocks_per_ne00.z); + } +} + template __launch_bounds__(ggml_cuda_mmq_get_nthreads(type, J, fallback)/2, 1) static __global__ void mul_mat_q_stream_k_fixup( @@ -1424,6 +1522,24 @@ static void launch_mul_mat_q(ggml_backend_cuda_context & ctx, const mmq_args & a const uint3 channel_ratio_fd = init_fastdiv_values(channel_ratio); const uint3 sample_ratio_fd = init_fastdiv_values(sample_ratio); + const bool use_compact_routed = args.ids_dst != nullptr && args.nchannels_y == 256 && GGML_CUDA_CC_IS_RDNA3_5(cc) && + !ggml_cuda_mmq_get_stream_k(type, J, fallback, cc) && + ((type == GGML_TYPE_Q6_K && J == 32) || (type == GGML_TYPE_Q8_0 && J == 48)); + if (use_compact_routed) { + const int max_descriptors = (args.ncols_dst + J - 1) / J + args.nchannels_y; + ggml_cuda_pool_alloc descriptors(ctx.pool(id), max_descriptors); + build_mmq_routed_descriptors<<<1, 256, 0, stream>>> + (args.expert_bounds, descriptors.get(), args.nchannels_y, max_descriptors); + + const int descriptor_blocks = (args.ncols_dst + J - 1) / J; + const dim3 block_nums_compact(nty, descriptor_blocks, args.nsamples_y); + mul_mat_q_routed_compact<<>> + (args.x, args.y, args.ids_dst, args.expert_bounds, descriptors.get(), max_descriptors, args.dst, args.y_scale, + blocks_per_ne00_fd, args.nrows_x, args.stride_row_x, args.ncols_y, args.nrows_dst, + channel_ratio_fd, args.stride_channel_x, sample_ratio_fd, args.stride_sample_x); + return; + } + if (!ggml_cuda_mmq_get_stream_k(type, J, fallback, cc)) { mul_mat_q<<>> (args.x, args.y, args.ids_dst, args.expert_bounds, args.dst, nullptr, args.y_scale, @@ -1472,6 +1588,29 @@ static void launch_mul_mat_q(ggml_backend_cuda_context & ctx, const mmq_args & a ntx_fd); } +static constexpr bool mmq_use_rdna3_5_q8_id_j48( + ggml_type type, int cc, bool fallback, bool has_ids, int64_t ncols_dst, int64_t nchannels_y) { + return type == GGML_TYPE_Q8_0 && GGML_CUDA_CC_IS_RDNA3_5(cc) && !fallback && has_ids && nchannels_y > 0 && + (ncols_dst + nchannels_y - 1) / nchannels_y == 32; +} + +static_assert(mmq_use_rdna3_5_q8_id_j48(GGML_TYPE_Q8_0, GGML_CUDA_CC_RDNA3_5, false, true, 8192, 256)); +static_assert(!mmq_use_rdna3_5_q8_id_j48(GGML_TYPE_Q8_0, GGML_CUDA_CC_RDNA3, false, true, 8192, 256)); +static_assert(!mmq_use_rdna3_5_q8_id_j48(GGML_TYPE_Q4_0, GGML_CUDA_CC_RDNA3_5, false, true, 8192, 256)); +static_assert(!mmq_use_rdna3_5_q8_id_j48(GGML_TYPE_Q8_0, GGML_CUDA_CC_RDNA3_5, true, true, 8192, 256)); +static_assert(!mmq_use_rdna3_5_q8_id_j48(GGML_TYPE_Q8_0, GGML_CUDA_CC_RDNA3_5, false, false, 8192, 256)); +static_assert(!mmq_use_rdna3_5_q8_id_j48(GGML_TYPE_Q8_0, GGML_CUDA_CC_RDNA3_5, false, true, 7936, 256)); +static_assert(!mmq_use_rdna3_5_q8_id_j48(GGML_TYPE_Q8_0, GGML_CUDA_CC_RDNA3_5, false, true, 8193, 256)); + +static constexpr bool mmq_use_rdna3_5_iq_id_j48( + ggml_type type, int cc, bool fallback, bool has_ids, int64_t ncols_dst, int64_t nchannels_y) { + return (type == GGML_TYPE_IQ2_XS || type == GGML_TYPE_IQ3_XXS) && GGML_CUDA_CC_IS_RDNA3_5(cc) && + !fallback && has_ids && nchannels_y > 0 && (ncols_dst + nchannels_y - 1) / nchannels_y == 48; +} + +static_assert(mmq_use_rdna3_5_iq_id_j48(GGML_TYPE_IQ2_XS, GGML_CUDA_CC_RDNA3_5, false, true, 12288, 256)); +static_assert(mmq_use_rdna3_5_iq_id_j48(GGML_TYPE_IQ3_XXS, GGML_CUDA_CC_RDNA3_5, false, true, 12288, 256)); + template void mul_mat_q_switch_J(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream) { const int id = ggml_cuda_get_device(); @@ -1481,6 +1620,22 @@ void mul_mat_q_switch_J(ggml_backend_cuda_context & ctx, const mmq_args & args, int J_best = 0; int ntiles_J_best = INT_MAX; + if (mmq_use_rdna3_5_iq_id_j48(type, cc, fallback, args.ids_dst != nullptr, args.ncols_dst, args.nchannels_y)) { + constexpr int J_rdna3_5 = 48; + const ggml_cuda_mmq_config config = ggml_cuda_mmq_get_config(type, J_rdna3_5, fallback, cc); + if (config.type != GGML_TYPE_COUNT && mmq_get_nbytes_shared(config, cc) <= smpbo) { + J_best = J_rdna3_5; + ntiles_J_best = 1; + } + } else if (mmq_use_rdna3_5_q8_id_j48(type, cc, fallback, args.ids_dst != nullptr, args.ncols_dst, args.nchannels_y)) { + constexpr int J_rdna3_5 = 48; + const ggml_cuda_mmq_config config = ggml_cuda_mmq_get_config(type, J_rdna3_5, fallback, cc); + if (config.type != GGML_TYPE_COUNT && mmq_get_nbytes_shared(config, cc) <= smpbo) { + J_best = J_rdna3_5; + ntiles_J_best = 1; + } + } + for (int J = 8; J <= 128 && ntiles_J_best > 1; J += 8) { const ggml_cuda_mmq_config config = ggml_cuda_mmq_get_config(type, J, fallback, cc); if (config.type == GGML_TYPE_COUNT) { @@ -1600,4 +1755,9 @@ extern DECL_MMQ_CASE(GGML_TYPE_NVFP4); void ggml_cuda_mul_mat_q( ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst); +void ggml_cuda_mul_mat_q_swiglu( + ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * ids, ggml_tensor * dst, const ggml_tensor * swiglu); + +void ggml_cuda_mul_mat_q_pair(ggml_backend_cuda_context & ctx, ggml_tensor * dst0, ggml_tensor * dst1); + bool ggml_cuda_should_use_mmq(enum ggml_type type, int cc, int64_t ne11, int64_t n_experts); diff --git a/ggml/src/ggml-cuda/mmvf.cu b/ggml/src/ggml-cuda/mmvf.cu index bd5c5d421a4c..4eb457db7a6b 100644 --- a/ggml/src/ggml-cuda/mmvf.cu +++ b/ggml/src/ggml-cuda/mmvf.cu @@ -4,7 +4,7 @@ #include "mmvf.cuh" #include "convert.cuh" -template +template static __global__ void mul_mat_vec_f( const T * x_ptr, const float * y_ptr, const int32_t * ids_ptr, const ggml_cuda_mm_fusion_args_device fusion, float * dst_ptr, const int ncols2, const uint3 nchannels_y, const int stride_row, const int stride_col_y2, const int stride_col_dst, @@ -27,11 +27,11 @@ static __global__ void mul_mat_vec_f( ggml_cuda_pdl_sync(); if constexpr (is_multi_token_id) { - // Multi-token MUL_MAT_ID path, adding these in the normal path causes a perf regression for n_tokens=1 case - token_idx = blockIdx.z; - channel_x = ids[channel_dst + token_idx * ids_stride]; - channel_y = fastmodulo(channel_dst, nchannels_y); - sample_dst = 0; + // Multi-token path, adding these in the normal path causes a perf regression for n_tokens=1 case + token_idx = tokens_in_block ? threadIdx.y : blockIdx.z; + channel_x = ids ? ids[channel_dst + token_idx * ids_stride] : fastdiv((uint32_t) channel_dst, channel_ratio); + channel_y = ids ? fastmodulo(channel_dst, nchannels_y) : channel_dst; + sample_dst = ids ? 0 : (tokens_in_block ? blockIdx.z : 0); } else { token_idx = ids ? blockIdx.z : 0; channel_x = ids ? ids[blockIdx.y + token_idx * ids_stride] : fastdiv((uint32_t) channel_dst, channel_ratio); @@ -99,7 +99,7 @@ static __global__ void mul_mat_vec_f( const float2 * y2 = (const float2 *) y; extern __shared__ char data_mmv[]; - float * buf_iw = (float *) data_mmv; + float * buf_iw = (float *) data_mmv + (tokens_in_block ? threadIdx.y * warp_size : 0); [[maybe_unused]] float * buf_iw_gate = nullptr; if constexpr (has_fusion) { buf_iw_gate = (float *) (data_mmv + warp_size*sizeof(float)); @@ -383,7 +383,7 @@ static __global__ void mul_mat_vec_f( } } -template +template static void mul_mat_vec_f_switch_fusion( const T * x, const float * y, const int32_t * ids, const ggml_cuda_mm_fusion_args_device fusion, float * dst, const int64_t ncols, const uint3 nchannels_y, @@ -397,7 +397,7 @@ static void mul_mat_vec_f_switch_fusion( const bool has_fusion = fusion.gate != nullptr || fusion.x_bias != nullptr || fusion.gate_bias != nullptr; if constexpr (ncols_dst == 1) { if (has_fusion) { - ggml_cuda_kernel_launch(mul_mat_vec_f, launch_params, + ggml_cuda_kernel_launch(mul_mat_vec_f, launch_params, x, y, ids, fusion, dst, ncols, nchannels_y, stride_row, stride_col_y, stride_col_dst, channel_ratio, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride); @@ -407,14 +407,14 @@ static void mul_mat_vec_f_switch_fusion( GGML_ASSERT(!has_fusion && "fusion only supported for ncols_dst=1"); - ggml_cuda_kernel_launch(mul_mat_vec_f, launch_params, + ggml_cuda_kernel_launch(mul_mat_vec_f, launch_params, x, y, ids, fusion, dst, ncols, nchannels_y, stride_row, stride_col_y, stride_col_dst, channel_ratio, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride); } -template +template void launch_mul_mat_vec_f_cuda( const T * x, const float * y, const int32_t * ids, const ggml_cuda_mm_fusion_args_device fusion, float * dst, const int64_t ncols, const int64_t nrows, @@ -448,57 +448,56 @@ void launch_mul_mat_vec_f_cuda( block_size_best = block_size; } } - const bool has_fusion = fusion.gate != nullptr || fusion.x_bias != nullptr || fusion.gate_bias != nullptr; - const int nbytes_shared = warp_size*sizeof(float) + (has_fusion ? warp_size*sizeof(float) : 0); - const dim3 block_nums(nrows, nchannels_dst, nsamples_or_ntokens); - const dim3 block_dims(block_size_best, 1, 1); + const int nbytes_shared = (warp_size*sizeof(float) + (has_fusion ? warp_size*sizeof(float) : 0)) * (tokens_in_block ? nsamples_or_ntokens : 1); + const dim3 block_nums(nrows, nchannels_dst, tokens_in_block ? nsamples_dst : nsamples_or_ntokens); + const dim3 block_dims(block_size_best, tokens_in_block ? nsamples_or_ntokens : 1, 1); switch (block_size_best) { case 32: { - mul_mat_vec_f_switch_fusion + mul_mat_vec_f_switch_fusion (x, y, ids, fusion, dst, ncols/2, nchannels_y_fd, stride_row, stride_col_y/2, stride_col_dst, channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst, block_dims, block_nums, nbytes_shared, ids_stride, stream); } break; case 64: { - mul_mat_vec_f_switch_fusion + mul_mat_vec_f_switch_fusion (x, y, ids, fusion, dst, ncols/2, nchannels_y_fd, stride_row, stride_col_y/2, stride_col_dst, channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst, block_dims, block_nums, nbytes_shared, ids_stride, stream); } break; case 96: { - mul_mat_vec_f_switch_fusion + mul_mat_vec_f_switch_fusion (x, y, ids, fusion, dst, ncols/2, nchannels_y_fd, stride_row, stride_col_y/2, stride_col_dst, channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst, block_dims, block_nums, nbytes_shared, ids_stride, stream); } break; case 128: { - mul_mat_vec_f_switch_fusion + mul_mat_vec_f_switch_fusion (x, y, ids, fusion, dst, ncols/2, nchannels_y_fd, stride_row, stride_col_y/2, stride_col_dst, channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst, block_dims, block_nums, nbytes_shared, ids_stride, stream); } break; case 160: { - mul_mat_vec_f_switch_fusion + mul_mat_vec_f_switch_fusion (x, y, ids, fusion, dst, ncols/2, nchannels_y_fd, stride_row, stride_col_y/2, stride_col_dst, channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst, block_dims, block_nums, nbytes_shared, ids_stride, stream); } break; case 192: { - mul_mat_vec_f_switch_fusion + mul_mat_vec_f_switch_fusion (x, y, ids, fusion, dst, ncols/2, nchannels_y_fd, stride_row, stride_col_y/2, stride_col_dst, channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst, block_dims, block_nums, nbytes_shared, ids_stride, stream); } break; case 224: { - mul_mat_vec_f_switch_fusion + mul_mat_vec_f_switch_fusion (x, y, ids, fusion, dst, ncols/2, nchannels_y_fd, stride_row, stride_col_y/2, stride_col_dst, channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst, block_dims, block_nums, nbytes_shared, ids_stride, stream); } break; case 256: { - mul_mat_vec_f_switch_fusion + mul_mat_vec_f_switch_fusion (x, y, ids, fusion, dst, ncols/2, nchannels_y_fd, stride_row, stride_col_y/2, stride_col_dst, channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst, block_dims, block_nums, nbytes_shared, ids_stride, stream); @@ -517,7 +516,7 @@ static void mul_mat_vec_f_cuda_switch_ncols_dst( const int64_t nchannels_x, const int64_t nchannels_y, const int64_t nchannels_dst, const int64_t stride_channel_x, const int64_t stride_channel_y, const int64_t stride_channel_dst, const int64_t nsamples_x, const int64_t nsamples_dst, const int64_t stride_sample_x, const int64_t stride_sample_y, const int64_t stride_sample_dst, - const int64_t ids_stride, cudaStream_t stream) { + const int64_t ids_stride, bool exact_batch, cudaStream_t stream) { const bool has_ids = ids != nullptr; @@ -543,6 +542,42 @@ static void mul_mat_vec_f_cuda_switch_ncols_dst( return; } + if (exact_batch && !has_ids && ncols_dst > 1) { + if (ncols_dst == 2) { + launch_mul_mat_vec_f_cuda + (x, y, ids, fusion, dst, ncols, nrows, stride_row, stride_col_y, stride_col_dst, + nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, + stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, + nsamples_dst, ids_stride, stream); + return; + } + if (ncols_dst == 3) { + launch_mul_mat_vec_f_cuda + (x, y, ids, fusion, dst, ncols, nrows, stride_row, stride_col_y, stride_col_dst, + nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, + stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, + nsamples_dst, ids_stride, stream); + return; + } + if (ncols_dst == 4) { + launch_mul_mat_vec_f_cuda + (x, y, ids, fusion, dst, ncols, nrows, stride_row, stride_col_y, stride_col_dst, + nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, + stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, + nsamples_dst, ids_stride, stream); + return; + } + for (int64_t col = 0; col < ncols_dst; col += 4) { + const int64_t ncols_group = std::min(4, ncols_dst - col); + launch_mul_mat_vec_f_cuda + (x, y + col*stride_col_y, ids, fusion, dst + col*stride_col_dst, ncols, nrows, stride_row, stride_col_y, stride_col_dst, + nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, + stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, + ncols_group, ids_stride, stream); + } + return; + } + switch (ncols_dst) { case 1: launch_mul_mat_vec_f_cuda @@ -614,21 +649,21 @@ static void mul_mat_vec_f_cuda( const int64_t nchannels_x, const int64_t nchannels_y, const int64_t nchannels_dst, const int64_t stride_channel_x, const int64_t stride_channel_y, const int64_t stride_channel_dst, const int64_t nsamples_x, const int64_t nsamples_dst, const int64_t stride_sample_x, const int64_t stride_sample_y, const int64_t stride_sample_dst, - const int64_t ids_stride, enum ggml_prec prec, cudaStream_t stream) { + const int64_t ids_stride, enum ggml_prec prec, bool exact_batch, cudaStream_t stream) { if constexpr(std::is_same_v) { if (prec == GGML_PREC_DEFAULT) { mul_mat_vec_f_cuda_switch_ncols_dst (x, y, ids, fusion, dst, ncols, nrows, ncols_dst, stride_row, stride_col_y, stride_col_dst, nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, - stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride, stream); + stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride, exact_batch, stream); return; } } mul_mat_vec_f_cuda_switch_ncols_dst (x, y, ids, fusion, dst, ncols, nrows, ncols_dst, stride_row, stride_col_y, stride_col_dst, nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, - stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride, stream); + stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride, exact_batch, stream); } void ggml_cuda_mul_mat_vec_f(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst, @@ -653,6 +688,7 @@ void ggml_cuda_mul_mat_vec_f(ggml_backend_cuda_context & ctx, const ggml_tensor const int cc = ggml_cuda_info().devices[ggml_cuda_get_device()].cc; const enum ggml_prec prec = fast_fp16_available(cc) ? ggml_prec(dst->op_params[0]) : GGML_PREC_F32; + const bool exact_batch = ggml_get_op_params_i32(dst, 1) == GGML_HINT_EXACT_BATCH; const float * src1_d = (const float *) src1->data; const int32_t * ids_d = ids ? (const int32_t *) ids->data : nullptr; @@ -709,19 +745,19 @@ void ggml_cuda_mul_mat_vec_f(ggml_backend_cuda_context & ctx, const ggml_tensor const float * src0_d = (const float *) src0->data; mul_mat_vec_f_cuda(src0_d, src1_d, ids_d, fusion_local, dst_d, ne00, ne01, ncols_dst, s01, stride_col_y, stride_col_dst, ne02, nchannels_y, nchannels_dst, s02, stride_channel_y, stride_channel_dst, - ne03, ne3, s03, s13, s3, ids_stride, prec, ctx.stream()); + ne03, ne3, s03, s13, s3, ids_stride, prec, exact_batch, ctx.stream()); } break; case GGML_TYPE_F16: { const half * src0_d = (const half *) src0->data; mul_mat_vec_f_cuda(src0_d, src1_d, ids_d, fusion_local, dst_d, ne00, ne01, ncols_dst, s01, stride_col_y, stride_col_dst, ne02, nchannels_y, nchannels_dst, s02, stride_channel_y, stride_channel_dst, - ne03, ne3, s03, s13, s3, ids_stride, prec, ctx.stream()); + ne03, ne3, s03, s13, s3, ids_stride, prec, exact_batch, ctx.stream()); } break; case GGML_TYPE_BF16: { const nv_bfloat16 * src0_d = (const nv_bfloat16 *) src0->data; mul_mat_vec_f_cuda(src0_d, src1_d, ids_d, fusion_local, dst_d, ne00, ne01, ncols_dst, s01, stride_col_y, stride_col_dst, ne02, nchannels_y, nchannels_dst, s02, stride_channel_y, stride_channel_dst, - ne03, ne3, s03, s13, s3, ids_stride, prec, ctx.stream()); + ne03, ne3, s03, s13, s3, ids_stride, prec, exact_batch, ctx.stream()); } break; default: GGML_ABORT("unsupported type: %s", ggml_type_name(src0->type)); @@ -768,19 +804,19 @@ void ggml_cuda_op_mul_mat_vec_f( const float * src0_d = (const float *) src0_dd_i; mul_mat_vec_f_cuda(src0_d, src1_ddf_i, nullptr, empty, dst_dd_i, ne00, row_diff, src1_ncols, stride_row, stride_col_y, stride_col_dst, nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, stride_channel_dst, - nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, 0, prec, stream); + nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, 0, prec, false, stream); } break; case GGML_TYPE_F16: { const half * src0_d = (const half *) src0_dd_i; mul_mat_vec_f_cuda(src0_d, src1_ddf_i, nullptr, empty, dst_dd_i, ne00, row_diff, src1_ncols, stride_row, stride_col_y, stride_col_dst, nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, stride_channel_dst, - nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, 0, prec, stream); + nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, 0, prec, false, stream); } break; case GGML_TYPE_BF16: { const nv_bfloat16 * src0_d = (const nv_bfloat16 *) src0_dd_i; mul_mat_vec_f_cuda(src0_d, src1_ddf_i, nullptr, empty, dst_dd_i, ne00, row_diff, src1_ncols, stride_row, stride_col_y, stride_col_dst, nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, stride_channel_dst, - nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, 0, prec, stream); + nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, 0, prec, false, stream); } break; default: GGML_ABORT("unsupported type: %s", ggml_type_name(src0->type)); diff --git a/ggml/src/ggml-cuda/mmvq.cu b/ggml/src/ggml-cuda/mmvq.cu index 79f7a3f6fe75..b93b9b6c0520 100644 --- a/ggml/src/ggml-cuda/mmvq.cu +++ b/ggml/src/ggml-cuda/mmvq.cu @@ -69,6 +69,7 @@ enum mmvq_parameter_table_id { MMVQ_PARAMETERS_TURING, MMVQ_PARAMETERS_GCN, MMVQ_PARAMETERS_RDNA2, + MMVQ_PARAMETERS_RDNA3_5, MMVQ_PARAMETERS_RDNA3_0, MMVQ_PARAMETERS_RDNA4, MMVQ_PARAMETERS_GB10 @@ -77,9 +78,11 @@ enum mmvq_parameter_table_id { static constexpr __device__ mmvq_parameter_table_id get_device_table_id() { #if defined(RDNA4) return MMVQ_PARAMETERS_RDNA4; +#elif defined(RDNA3_5) + return MMVQ_PARAMETERS_RDNA3_5; #elif defined(RDNA3_0) return MMVQ_PARAMETERS_RDNA3_0; -#elif defined(RDNA2) || defined(RDNA3_5) +#elif defined(RDNA2) return MMVQ_PARAMETERS_RDNA2; #elif defined(GCN) || defined(CDNA) return MMVQ_PARAMETERS_GCN; @@ -99,7 +102,10 @@ static __host__ mmvq_parameter_table_id get_device_table_id(int cc) { if (GGML_CUDA_CC_IS_RDNA3_0(cc)) { return MMVQ_PARAMETERS_RDNA3_0; } - if (GGML_CUDA_CC_IS_RDNA2(cc) || GGML_CUDA_CC_IS_RDNA3_5(cc)) { + if (GGML_CUDA_CC_IS_RDNA3_5(cc)) { + return MMVQ_PARAMETERS_RDNA3_5; + } + if (GGML_CUDA_CC_IS_RDNA2(cc)) { return MMVQ_PARAMETERS_RDNA2; } if (GGML_CUDA_CC_IS_GCN(cc) || GGML_CUDA_CC_IS_CDNA(cc)) { @@ -395,6 +401,23 @@ static constexpr __device__ int get_mmvq_mmid_max_batch_for_device() { } static constexpr __host__ __device__ int calc_nwarps(ggml_type type, int ncols_dst, mmvq_parameter_table_id table_id, bool small_k = false, bool halve_iters = false) { + if (table_id == MMVQ_PARAMETERS_RDNA3_5) { + // Measured on gfx1151 with 4096x14336 MMVQ on 2026-09-01. Wider blocks regress the other types. + if (ncols_dst == 1) { + switch (type) { + case GGML_TYPE_Q5_K: + case GGML_TYPE_Q6_K: + return 4; + case GGML_TYPE_Q5_1: + case GGML_TYPE_MXFP4: + case GGML_TYPE_Q4_K: + return 2; + default: + break; + } + } + return 1; + } if (table_id == MMVQ_PARAMETERS_GENERIC) { switch (ncols_dst) { case 1: @@ -521,7 +544,374 @@ static constexpr __host__ __device__ int calc_nwarps(ggml_type type, int ncols_d return 1; } +static_assert(calc_nwarps(GGML_TYPE_Q5_1, 1, MMVQ_PARAMETERS_RDNA3_5) == 2); +static_assert(calc_nwarps(GGML_TYPE_Q8_0, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); +static_assert(calc_nwarps(GGML_TYPE_MXFP4, 1, MMVQ_PARAMETERS_RDNA3_5) == 2); +static_assert(calc_nwarps(GGML_TYPE_Q4_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 2); +static_assert(calc_nwarps(GGML_TYPE_Q5_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 4); +static_assert(calc_nwarps(GGML_TYPE_Q6_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 4); +static_assert(calc_nwarps(GGML_TYPE_Q1_0, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); +static_assert(calc_nwarps(GGML_TYPE_Q2_0, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); +static_assert(calc_nwarps(GGML_TYPE_Q4_0, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); +static_assert(calc_nwarps(GGML_TYPE_IQ3_S, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); + +static constexpr __host__ __device__ bool is_rdna3_5_q4_columns_type(ggml_type type) { + return type == GGML_TYPE_Q1_0 || type == GGML_TYPE_Q2_0 || type == GGML_TYPE_Q5_1 || + type == GGML_TYPE_Q4_K || type == GGML_TYPE_Q5_K || type == GGML_TYPE_Q6_K || + type == GGML_TYPE_IQ3_S; +} + +static_assert(is_rdna3_5_q4_columns_type(GGML_TYPE_Q1_0)); +static_assert(is_rdna3_5_q4_columns_type(GGML_TYPE_Q2_0)); +static_assert(is_rdna3_5_q4_columns_type(GGML_TYPE_Q5_1)); +static_assert(is_rdna3_5_q4_columns_type(GGML_TYPE_Q4_K)); +static_assert(is_rdna3_5_q4_columns_type(GGML_TYPE_Q5_K)); +static_assert(is_rdna3_5_q4_columns_type(GGML_TYPE_Q6_K)); +static_assert(is_rdna3_5_q4_columns_type(GGML_TYPE_IQ3_S)); + +template +__launch_bounds__(2 * ggml_cuda_get_physical_warp_size(), 1) +static __global__ void mul_mat_vec_q4_columns( + const void * vx_ptr, const void * vy_ptr, float * dst_ptr, + const uint32_t ncols_x, const uint32_t stride_row_x, const uint32_t stride_col_y, + const uint32_t stride_col_dst, const uint3 channel_ratio, const uint32_t stride_channel_x, + const uint32_t stride_channel_y, const uint32_t stride_channel_dst, const uint3 sample_ratio, + const uint32_t stride_sample_x, const uint32_t stride_sample_y, const uint32_t stride_sample_dst) { + static_assert(type == GGML_TYPE_Q8_0); + constexpr int qk = ggml_cuda_type_traits::qk; + constexpr int qi = ggml_cuda_type_traits::qi; + constexpr int vdr = get_vdr_mmvq(type); + constexpr int warp_size = ggml_cuda_get_physical_warp_size(); + + const int row = blockIdx.x; + const int lane = threadIdx.x; + const int column0 = 0; + const uint32_t channel_dst = blockIdx.y; + const uint32_t sample_dst = blockIdx.z; + const uint32_t channel_x = fastdiv(channel_dst, channel_ratio); + const uint32_t sample_x = fastdiv(sample_dst, sample_ratio); + + const void * vx = vx_ptr; + const block_q8_1 * y = (const block_q8_1 *) vy_ptr + sample_dst*stride_sample_y + channel_dst*stride_channel_y; + float * dst = dst_ptr + sample_dst*stride_sample_dst + channel_dst*stride_channel_dst + row; + + const int blocks_per_row_x = ncols_x / qk; + const int blocks_per_iter = vdr * warp_size / qi; + const int kbx_offset = sample_x*stride_sample_x + channel_x*stride_channel_x + row*stride_row_x; + float tmp[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + + ggml_cuda_pdl_sync(); + for (int kbx = lane / (qi/vdr); kbx < blocks_per_row_x; kbx += blocks_per_iter) { + const int kby = kbx * (qk/QK8_1); + const int kqs = vdr * (lane % (qi/vdr)); + const block_q8_0 * bx = (const block_q8_0 *) vx + kbx_offset + kbx; + int xv[VDR_Q8_0_Q8_1_MMVQ]; +#pragma unroll + for (int i = 0; i < VDR_Q8_0_Q8_1_MMVQ; ++i) { + xv[i] = get_int_b2(bx->qs, kqs + i); + } + const float dx = __half2float(bx->d); +#pragma unroll + for (int j = 0; j < 4; ++j) { + const block_q8_1 * by = &y[(column0 + j)*stride_col_y + kby]; + int sumi = 0; +#pragma unroll + for (int i = 0; i < VDR_Q8_0_Q8_1_MMVQ; ++i) { + sumi = ggml_cuda_dp4a(xv[i], get_int_b4(by->qs, kqs + i), sumi); + } + tmp[j] += dx * __half2float(__low2half(by->ds)) * (float) sumi; + } + } + +#pragma unroll + for (int j = 0; j < 4; ++j) { + tmp[j] = warp_reduce_sum(tmp[j]); + if (lane == 0) { + dst[(column0 + j)*stride_col_dst] = tmp[j]; + } + } +} + +template +__launch_bounds__(calc_nwarps(type, 1, get_device_table_id()) * ggml_cuda_get_physical_warp_size(), 1) +static __global__ void mul_mat_vec_q4_columns_rdna3_5( + const void * vx_ptr, const void * vy_ptr, float * dst_ptr, + const uint32_t ncols_x, const uint32_t stride_row_x, const uint32_t stride_col_y, + const uint32_t stride_col_dst, const uint3 channel_ratio, const uint32_t stride_channel_x, + const uint32_t stride_channel_y, const uint32_t stride_channel_dst, const uint3 sample_ratio, + const uint32_t stride_sample_x, const uint32_t stride_sample_y, const uint32_t stride_sample_dst) { + static_assert(is_rdna3_5_q4_columns_type(type)); + constexpr int qk = ggml_cuda_type_traits::qk; + constexpr int qi = ggml_cuda_type_traits::qi; + constexpr int vdr = get_vdr_mmvq(type); + constexpr int warp_size = ggml_cuda_get_physical_warp_size(); + constexpr int nwarps = calc_nwarps(type, 1, get_device_table_id()); + + const int row = blockIdx.x; + const int lane = threadIdx.x; + const int tid = warp_size*threadIdx.y + lane; + const uint32_t channel_dst = blockIdx.y; + const uint32_t sample_dst = blockIdx.z; + const uint32_t channel_x = fastdiv(channel_dst, channel_ratio); + const uint32_t sample_x = fastdiv(sample_dst, sample_ratio); + + const block_q8_1 * y = (const block_q8_1 *) vy_ptr + sample_dst*stride_sample_y + channel_dst*stride_channel_y; + float * dst = dst_ptr + sample_dst*stride_sample_dst + channel_dst*stride_channel_dst + row; + + const int blocks_per_row_x = ncols_x / qk; + const int blocks_per_iter = vdr * nwarps*warp_size / qi; + const int kbx_offset = sample_x*stride_sample_x + channel_x*stride_channel_x + row*stride_row_x; + float tmp[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + + ggml_cuda_pdl_sync(); + for (int kbx = tid / (qi/vdr); kbx < blocks_per_row_x; kbx += blocks_per_iter) { + const int kby = kbx * (qk/QK8_1); + const int kqs = vdr * (tid % (qi/vdr)); + + if constexpr (type == GGML_TYPE_Q1_0) { + const block_q1_0 * bx = (const block_q1_0 *) vx_ptr + kbx_offset + kbx; + const int16_t * qs = (const int16_t *) bx->qs + kqs*2; + int xv[8]; +#pragma unroll + for (int i = 0; i < 2; ++i) { + const int q = qs[i]; + const int n0 = __byte_perm(0x11100100, 0x11100100, q >> 0); + const int n1 = __byte_perm(0x11100100, 0x11100100, q >> 2); + const int s0 = __byte_perm(0x01FF, 0x01FF, n0 >> 0); + const int s1 = __byte_perm(0x01FF, 0x01FF, n1 >> 0); + const int s2 = __byte_perm(0x01FF, 0x01FF, n0 >> 16); + const int s3 = __byte_perm(0x01FF, 0x01FF, n1 >> 16); + xv[4*i+0] = __byte_perm(s0, s1, 0x5410); + xv[4*i+1] = __byte_perm(s0, s1, 0x7632); + xv[4*i+2] = __byte_perm(s2, s3, 0x5410); + xv[4*i+3] = __byte_perm(s2, s3, 0x7632); + } + const float dx = bx->d; +#pragma unroll + for (int j = 0; j < 4; ++j) { + const block_q8_1 * by = &y[j*stride_col_y + kby + kqs]; + int sumi = 0; +#pragma unroll + for (int i = 0; i < 2; ++i) { + sumi = ggml_cuda_dp4a(xv[4*i+0], get_int_b4(by->qs, i*4+0), sumi); + sumi = ggml_cuda_dp4a(xv[4*i+1], get_int_b4(by->qs, i*4+1), sumi); + sumi = ggml_cuda_dp4a(xv[4*i+2], get_int_b4(by->qs, i*4+2), sumi); + sumi = ggml_cuda_dp4a(xv[4*i+3], get_int_b4(by->qs, i*4+3), sumi); + } + const float d8 = __low2float(by->ds); + tmp[j] += dx * d8 * sumi; + } + } else if constexpr (type == GGML_TYPE_Q2_0) { + const block_q2_0 * bx = (const block_q2_0 *) vx_ptr + kbx_offset + kbx; + const int16_t * qs = (const int16_t *) bx->qs + kqs*4; + int xv[4]; + int xw[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const int q = qs[i]; +#if defined(GGML_USE_HIP) + const uint32_t qx_indices = (q & 0x03) | ((q & 0x0C) << 6) | ((q & 0x30) << 12) | ((q & 0xC0) << 18); + const uint32_t qy_bits = q >> 8; + const uint32_t qy_indices = (qy_bits & 0x03) | ((qy_bits & 0x0C) << 6) | ((qy_bits & 0x30) << 12) | ((qy_bits & 0xC0) << 18); + xv[i] = __builtin_amdgcn_perm(0x020100FF, 0x020100FF, qx_indices); + xw[i] = __builtin_amdgcn_perm(0x020100FF, 0x020100FF, qy_indices); +#else + const int qe = __byte_perm(0x020100FF, 0x020100FF, q >> 0); + const int qo = __byte_perm(0x020100FF, 0x020100FF, q >> 2); + xv[i] = __byte_perm(qe, qo, 0x5140); + xw[i] = __byte_perm(qe, qo, 0x7362); +#endif // defined(GGML_USE_HIP) + } + const float dx = bx->d; +#pragma unroll + for (int j = 0; j < 4; ++j) { + const block_q8_1 * by = &y[j*stride_col_y + kby + kqs]; + int sumi = 0; +#pragma unroll + for (int i = 0; i < 4; ++i) { + sumi = ggml_cuda_dp4a(get_int_b4(by->qs, i*2+0), xv[i], sumi); + sumi = ggml_cuda_dp4a(get_int_b4(by->qs, i*2+1), xw[i], sumi); + } + const float d8 = __low2float(by->ds); + tmp[j] += dx * d8 * sumi; + } + } else if constexpr (type == GGML_TYPE_IQ3_S) { + const block_iq3_s * bx = (const block_iq3_s *) vx_ptr + kbx_offset + kbx; + const int2 qs_packed = make_int2(get_int_b2(bx->qs, kqs + 0), get_int_b2(bx->qs, kqs + 1)); + const uint8_t * qs = (const uint8_t *) &qs_packed; + const int qh = bx->qh[kqs/2]; + const int signs_packed_32 = get_int_b2(bx->signs, kqs/2); + const uint8_t * signs_packed_8 = (const uint8_t *) &signs_packed_32; + int2 xv[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const int l0 = 2*i; + const int2 grid_pos = make_int2( + iq3s_grid[qs[l0 + 0] | ((qh << (8-l0)) & 0x100)], + iq3s_grid[qs[l0 + 1] | ((qh << (7-l0)) & 0x100)]); + const int signs0 = __vcmpne4(((signs_packed_8[i] & 0x03) << 7) | ((signs_packed_8[i] & 0x0C) << 21), 0x00000000); + const int signs1 = __vcmpne4(((signs_packed_8[i] & 0x30) << 3) | ((signs_packed_8[i] & 0xC0) << 17), 0x00000000); + xv[i] = make_int2(__vsub4(grid_pos.x ^ signs0, signs0), __vsub4(grid_pos.y ^ signs1, signs1)); + } + const int ls = 1 + 2*((bx->scales[kqs/4] >> ((kqs << 1) & 0x04)) & 0x0F); + const float dx = __half2float(bx->d); +#pragma unroll + for (int j = 0; j < 4; ++j) { + const block_q8_1 * by = &y[j*stride_col_y + kby + kqs/2]; + int sumi = 0; +#pragma unroll + for (int i = 0; i < 4; ++i) { + sumi = ggml_cuda_dp4a(xv[i].x, get_int_b4(by->qs, 2*i + 0), sumi); + sumi = ggml_cuda_dp4a(xv[i].y, get_int_b4(by->qs, 2*i + 1), sumi); + } + sumi *= ls; + const float d = dx * __low2float(by->ds); + tmp[j] += d * sumi; + } + } else if constexpr (type == GGML_TYPE_Q5_1) { + const block_q5_1 * bx = (const block_q5_1 *) vx_ptr + kbx_offset + kbx; + int vl[VDR_Q5_1_Q8_1_MMVQ]; + int vh[VDR_Q5_1_Q8_1_MMVQ]; +#pragma unroll + for (int i = 0; i < VDR_Q5_1_Q8_1_MMVQ; ++i) { + vl[i] = get_int_b4(bx->qs, kqs + i); + vh[i] = get_int_b4(bx->qh, 0) >> (4 * (kqs + i)); + } + const half2 dm = bx->dm; +#pragma unroll + for (int j = 0; j < 4; ++j) { + const block_q8_1 * by = &y[j*stride_col_y + kby]; + int u[2*VDR_Q5_1_Q8_1_MMVQ]; +#pragma unroll + for (int i = 0; i < VDR_Q5_1_Q8_1_MMVQ; ++i) { + u[2*i+0] = get_int_b4(by->qs, kqs + i); + u[2*i+1] = get_int_b4(by->qs, kqs + i + QI5_1); + } + // ROCm 7.14 changes gfx1151 Q5_1 rounding under four-column register pressure. Keep both paths materialized until LLVM preserves this expression. + volatile float dot = vec_dot_q5_1_q8_1_impl(vl, vh, u, dm, by->ds); + tmp[j] = __fadd_rn(tmp[j], dot); + } + } else if constexpr (type == GGML_TYPE_Q4_K) { + const block_q4_K * bx = (const block_q4_K *) vx_ptr + kbx_offset + kbx; + const int bq8_offset = QR4_K * ((kqs/2) / (QI8_1/2)); + const int * q4 = (const int *) (bx->qs + 16*bq8_offset + 4*((kqs/2)%4)); + int xv[2] = {q4[0], q4[4]}; + const uint16_t * scales = (const uint16_t *) bx->scales; + uint16_t aux[2]; + const int is = bq8_offset/2; + if (is < 2) { + aux[0] = scales[is+0] & 0x3f3f; + aux[1] = scales[is+2] & 0x3f3f; + } else { + aux[0] = ((scales[is+2] >> 0) & 0x0f0f) | ((scales[is-2] & 0xc0c0) >> 2); + aux[1] = ((scales[is+2] >> 4) & 0x0f0f) | ((scales[is-0] & 0xc0c0) >> 2); + } + const uint8_t * sc = (const uint8_t *) aux; + const uint8_t * m = sc + 2; + const half2 dm = bx->dm; +#pragma unroll + for (int j = 0; j < 4; ++j) { + const block_q8_1 * by = &y[j*stride_col_y + kby]; + int u[2*QR4_K]; + float d8[QR4_K]; +#pragma unroll + for (int i = 0; i < QR4_K; ++i) { + const block_q8_1 * byi = by + bq8_offset + i; + d8[i] = __low2float(byi->ds); + const int * q8 = (const int *) byi->qs + ((kqs/2)%4); + u[2*i+0] = q8[0]; + u[2*i+1] = q8[4]; + } + tmp[j] += vec_dot_q4_K_q8_1_impl_vmmq(xv, u, sc, m, dm, d8); + } + } else if constexpr (type == GGML_TYPE_Q5_K) { + const block_q5_K * bx = (const block_q5_K *) vx_ptr + kbx_offset + kbx; + const int bq8_offset = QR5_K * ((kqs/2) / (QI8_1/2)); + const int * ql = (const int *) (bx->qs + 16*bq8_offset + 4*((kqs/2)%4)); + const int * qh = (const int *) (bx->qh + 4*((kqs/2)%4)); + int vl[2] = {ql[0], ql[4]}; + int vh[2] = {qh[0] >> bq8_offset, qh[4] >> bq8_offset}; + const uint16_t * scales = (const uint16_t *) bx->scales; + uint16_t aux[2]; + const int is = bq8_offset/2; + if (is < 2) { + aux[0] = scales[is+0] & 0x3f3f; + aux[1] = scales[is+2] & 0x3f3f; + } else { + aux[0] = ((scales[is+2] >> 0) & 0x0f0f) | ((scales[is-2] & 0xc0c0) >> 2); + aux[1] = ((scales[is+2] >> 4) & 0x0f0f) | ((scales[is-0] & 0xc0c0) >> 2); + } + const uint8_t * sc = (const uint8_t *) aux; + const uint8_t * m = sc + 2; + const half2 dm = bx->dm; +#pragma unroll + for (int j = 0; j < 4; ++j) { + const block_q8_1 * by = &y[j*stride_col_y + kby]; + int u[2*QR5_K]; + float d8[QR5_K]; +#pragma unroll + for (int i = 0; i < QR5_K; ++i) { + const block_q8_1 * byi = by + bq8_offset + i; + d8[i] = __low2float(byi->ds); + const int * q8 = (const int *) byi->qs + ((kqs/2)%4); + u[2*i+0] = q8[0]; + u[2*i+1] = q8[4]; + } + tmp[j] += vec_dot_q5_K_q8_1_impl_vmmq(vl, vh, u, sc, m, dm, d8); + } + } else if constexpr (type == GGML_TYPE_Q6_K) { + const block_q6_K * bx = (const block_q6_K *) vx_ptr + kbx_offset + kbx; + const int bq8_offset = 2*QR6_K*(kqs/(QI6_K/2)) + (kqs%(QI6_K/2))/(QI6_K/4); + const int scale_offset = (QI6_K/4)*(kqs/(QI6_K/2)) + (kqs%(QI6_K/2))/(QI6_K/8); + const int vh_shift = 2*((kqs%(QI6_K/2))/(QI6_K/4)); + const int vl = get_int_b2(bx->ql, kqs); + const int vh = get_int_b2(bx->qh, (QI6_K/4)*(kqs/(QI6_K/2)) + kqs%(QI6_K/4)) >> vh_shift; + const int8_t * scales = bx->scales + scale_offset; + const float dx = bx->d; +#pragma unroll + for (int j = 0; j < 4; ++j) { + const block_q8_1 * by = &y[j*stride_col_y + kby]; + int u[QR6_K]; + float d8[QR6_K]; +#pragma unroll + for (int i = 0; i < QR6_K; ++i) { + u[i] = get_int_b4(by[bq8_offset + 2*i].qs, kqs % QI8_1); + d8[i] = __low2float(by[bq8_offset + 2*i].ds); + } + tmp[j] += vec_dot_q6_K_q8_1_impl_mmvq(vl, vh, u, scales, dx, d8); + } + } + } + + __shared__ float tmp_shared[nwarps-1 > 0 ? nwarps-1 : 1][4][warp_size]; + if (threadIdx.y > 0) { +#pragma unroll + for (int j = 0; j < 4; ++j) { + tmp_shared[threadIdx.y-1][j][lane] = tmp[j]; + } + } + __syncthreads(); + if (threadIdx.y > 0) { + return; + } + +#pragma unroll + for (int j = 0; j < 4; ++j) { +#pragma unroll + for (int i = 0; i < nwarps-1; ++i) { + tmp[j] += tmp_shared[i][j][lane]; + } + tmp[j] = warp_reduce_sum(tmp[j]); + if (lane == 0) { + dst[j*stride_col_dst] = tmp[j]; + } + } +} + static constexpr __host__ __device__ int calc_rows_per_block(int ncols_dst, int table_id, bool small_k = false, int nwarps = 1) { + if (table_id == MMVQ_PARAMETERS_RDNA3_5) { + return 1; + } if (table_id == MMVQ_PARAMETERS_GENERIC || table_id == MMVQ_PARAMETERS_GCN || table_id == MMVQ_PARAMETERS_TURING || table_id == MMVQ_PARAMETERS_GB10) { switch (ncols_dst) { case 1: @@ -667,12 +1057,24 @@ static __global__ void mul_mat_vec_q( for (int j = 0; j < ncols_dst; ++j) { #pragma unroll for (int i = 0; i < rows_per_cuda_block; ++i) { - tmp[j][i] += vec_dot_q_cuda( - vx, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs); + if constexpr (type == GGML_TYPE_Q5_1 && table_id == MMVQ_PARAMETERS_RDNA3_5) { + volatile float dot = vec_dot_q_cuda( + vx, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs); + tmp[j][i] = __fadd_rn(tmp[j][i], dot); + } else { + tmp[j][i] += vec_dot_q_cuda( + vx, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs); + } if constexpr (has_fusion) { if (use_gate) { - tmp_gate[j][i] += vec_dot_q_cuda( - vgate, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs); + if constexpr (type == GGML_TYPE_Q5_1 && table_id == MMVQ_PARAMETERS_RDNA3_5) { + volatile float dot_gate = vec_dot_q_cuda( + vgate, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs); + tmp_gate[j][i] = __fadd_rn(tmp_gate[j][i], dot_gate); + } else { + tmp_gate[j][i] += vec_dot_q_cuda( + vgate, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs); + } } } } @@ -1054,6 +1456,35 @@ static void mul_mat_vec_q_switch_ncols_dst( dims.first, dims.second, 0, ids_stride, stream); } break; case 4: { + if constexpr (is_rdna3_5_q4_columns_type(type)) { + const bool no_fusion = fusion.gate == nullptr && fusion.x_bias == nullptr && fusion.gate_bias == nullptr && + fusion.x_scale == nullptr && fusion.gate_scale == nullptr; + if (table_id == MMVQ_PARAMETERS_RDNA3_5 && ids == nullptr && no_fusion) { + constexpr int c_nwarps = calc_nwarps(type, 1, MMVQ_PARAMETERS_RDNA3_5); + const dim3 block_nums(nrows_x, nchannels_dst, nsamples_dst); + const dim3 block_dims(warp_size, c_nwarps, 1); + const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, 0, stream); + ggml_cuda_kernel_launch(mul_mat_vec_q4_columns_rdna3_5, launch_params, + vx, vy, dst, ncols_x, stride_row_x, stride_col_y, stride_col_dst, + channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, + sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst); + break; + } + } + if constexpr (type == GGML_TYPE_Q8_0) { + const bool no_fusion = fusion.gate == nullptr && fusion.x_bias == nullptr && fusion.gate_bias == nullptr && + fusion.x_scale == nullptr && fusion.gate_scale == nullptr; + if (table_id == MMVQ_PARAMETERS_RDNA3_5 && ids == nullptr && no_fusion) { + const dim3 block_nums(nrows_x, nchannels_dst, nsamples_dst); + const dim3 block_dims(warp_size, 1, 1); + const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, 0, stream); + ggml_cuda_kernel_launch(mul_mat_vec_q4_columns, launch_params, + vx, vy, dst, ncols_x, stride_row_x, stride_col_y, stride_col_dst, + channel_ratio_fd, stride_channel_x, stride_channel_y, stride_channel_dst, + sample_ratio_fd, stride_sample_x, stride_sample_y, stride_sample_dst); + break; + } + } constexpr int c_ncols_dst = 4; std::pair dims = calc_launch_params(c_ncols_dst, nrows_x, nchannels_dst, nsamples_dst, warp_size, table_id); mul_mat_vec_q_switch_fusion(vx, vy, ids, fusion, dst, ncols_x, nchannels_y_fd, stride_row_x, stride_col_y, stride_col_dst, diff --git a/ggml/src/ggml-cuda/quantize.cu b/ggml/src/ggml-cuda/quantize.cu index bcc7723957b5..3923b19b0e2b 100644 --- a/ggml/src/ggml-cuda/quantize.cu +++ b/ggml/src/ggml-cuda/quantize.cu @@ -1,4 +1,5 @@ #include "quantize.cuh" +#include "unary.cuh" #include #if defined(BLACKWELL_MMA_AVAILABLE) @@ -463,14 +464,7 @@ static __global__ void quantize_mmq_q8_1( constexpr int vals_per_scale = ds_layout == MMQ_Q8_1_DS_LAYOUT_D2S6 ? 64 : 32; constexpr int vals_per_sum = ds_layout == MMQ_Q8_1_DS_LAYOUT_D2S6 ? 16 : 32; - const int64_t i0 = ((int64_t)blockDim.x*blockIdx.y + threadIdx.x)*4; - - if (i0 >= ne0) { - return; - } - - const int64_t i00 = i0; - ggml_cuda_pdl_sync(); + const int64_t first_chunk = (int64_t) blockIdx.y * CUDA_QUANTIZE_MMQ_CHUNKS_PER_BLOCK; int64_t base_idx; if constexpr (scatter) { @@ -485,76 +479,134 @@ static __global__ void quantize_mmq_q8_1( const float4 * x4 = (const float4 *) x; block_q8_1_mmq * y = (block_q8_1_mmq *) vy; - const int64_t k_block = i0 / QK8_1_MMQ; // column block in the channel - const int64_t iqs = i0 % QK8_1_MMQ; // quant index in block - - // Load 4 floats per thread and calculate max. abs. value between them: - const float4 xi = i0 < ne00 ? x4[(base_idx + i00)/4] : make_float4(0.0f, 0.0f, 0.0f, 0.0f); - float amax = fabsf(xi.x); - amax = fmaxf(amax, fabsf(xi.y)); - amax = fmaxf(amax, fabsf(xi.z)); - amax = fmaxf(amax, fabsf(xi.w)); + ggml_cuda_pdl_sync(); - // Exchange max. abs. value between vals_per_scale/4 threads. #pragma unroll - for (int offset = vals_per_scale/8; offset > 0; offset >>= 1) { - amax = fmaxf(amax, __shfl_xor_sync(0xFFFFFFFF, amax, offset, WARP_SIZE)); - } + for (int chunk = 0; chunk < CUDA_QUANTIZE_MMQ_CHUNKS_PER_BLOCK; ++chunk) { + const int64_t i0 = ((first_chunk + chunk) * blockDim.x + threadIdx.x) * 4; + if (i0 >= ne0) { + break; + } - float sum; - if (ds_layout != MMQ_Q8_1_DS_LAYOUT_D4) { - sum = xi.x + xi.y + xi.z + xi.w; + const int64_t k_block = i0 / QK8_1_MMQ; + const int64_t iqs = i0 % QK8_1_MMQ; + const float4 xi = i0 < ne00 ? x4[(base_idx + i0)/4] : make_float4(0.0f, 0.0f, 0.0f, 0.0f); + float amax = fabsf(xi.x); + amax = fmaxf(amax, fabsf(xi.y)); + amax = fmaxf(amax, fabsf(xi.z)); + amax = fmaxf(amax, fabsf(xi.w)); - // Calculate sums across vals_per_sum/4 threads. #pragma unroll - for (int offset = vals_per_sum/8; offset > 0; offset >>= 1) { - sum += __shfl_xor_sync(0xFFFFFFFF, sum, offset, WARP_SIZE); + for (int offset = vals_per_scale/8; offset > 0; offset >>= 1) { + amax = fmaxf(amax, __shfl_xor_sync(0xFFFFFFFF, amax, offset, WARP_SIZE)); } - } - const float d_inv = 127.0f / amax; - char4 q; - q.x = roundf(xi.x*d_inv); - q.y = roundf(xi.y*d_inv); - q.z = roundf(xi.z*d_inv); - q.w = roundf(xi.w*d_inv); - const float d = 1.0f / d_inv; - - // write the block once (normal) or to each of the token's compact rows (scatter) - const int nwrite = scatter ? n_expert_used : 1; + float sum; + if (ds_layout != MMQ_Q8_1_DS_LAYOUT_D4) { + sum = xi.x + xi.y + xi.z + xi.w; #pragma unroll - for (int slot = 0; slot < nwrite; ++slot) { - int64_t ib; - if constexpr (scatter) { - const int64_t i = ids[(int64_t) blockIdx.x * n_expert_used + slot]; - ib = k_block*ne1 + i; - } else { - const int64_t ib0 = blockIdx.z*((int64_t)gridDim.x*gridDim.y*blockDim.x/QK8_1); // first block of channel - ib = ib0 + k_block*ne1 + blockIdx.x; + for (int offset = vals_per_sum/8; offset > 0; offset >>= 1) { + sum += __shfl_xor_sync(0xFFFFFFFF, sum, offset, WARP_SIZE); + } } - // Write back 4 int8 values as a single 32 bit value for better memory bandwidth: - char4 * yqs4 = (char4 *) y[ib].qs; - yqs4[iqs/4] = q; + const float d_inv = 127.0f / amax; + char4 q; + q.x = roundf(xi.x*d_inv); + q.y = roundf(xi.y*d_inv); + q.z = roundf(xi.z*d_inv); + q.w = roundf(xi.w*d_inv); + const float d = 1.0f / d_inv; - if (ds_layout == MMQ_Q8_1_DS_LAYOUT_D2S6) { - if (iqs % 16 == 0 && iqs < 96) { - y[ib].d2s6[2 + iqs/16] = sum; - if (iqs % 64 == 0) { - y[ib].d2s6[iqs/64] = d; - } - } - } else if (iqs % 32 == 0) { - if (ds_layout == MMQ_Q8_1_DS_LAYOUT_DS4) { - y[ib].ds4[iqs/32] = make_half2(d, sum); + const int nwrite = scatter ? n_expert_used : 1; +#pragma unroll + for (int slot = 0; slot < nwrite; ++slot) { + int64_t ib; + if constexpr (scatter) { + const int64_t i = ids[(int64_t) blockIdx.x * n_expert_used + slot]; + ib = k_block*ne1 + i; } else { - y[ib].d4[iqs/32] = d; + const int64_t ib0 = blockIdx.z * ((int64_t) gridDim.x * ne0 / QK8_1_MMQ); + ib = ib0 + k_block*ne1 + blockIdx.x; + } + + char4 * yqs4 = (char4 *) y[ib].qs; + yqs4[iqs/4] = q; + + if (ds_layout == MMQ_Q8_1_DS_LAYOUT_D2S6) { + if (iqs % 16 == 0 && iqs < 96) { + y[ib].d2s6[2 + iqs/16] = sum; + if (iqs % 64 == 0) { + y[ib].d2s6[iqs/64] = d; + } + } + } else if (iqs % 32 == 0) { + if (ds_layout == MMQ_Q8_1_DS_LAYOUT_DS4) { + y[ib].ds4[iqs/32] = make_half2(d, sum); + } else { + y[ib].d4[iqs/32] = d; + } } } } GGML_UNUSED(n_expert_used); } +static __global__ void quantize_mmq_q8_1_swiglu( + const float * __restrict__ gate, const float * __restrict__ up, const int32_t * __restrict__ ids, + void * __restrict__ vy, const int64_t ne00, const int64_t ne0, const int ne1, const int logical_n1, + const int64_t gate_s1, const int64_t gate_st, const int64_t up_s1, const int64_t up_st) { + const int64_t i0 = ((int64_t) blockDim.x * blockIdx.y + threadIdx.x) * 4; + if (i0 >= ne0) { + return; + } + + const int64_t logical_row = ids ? ids[blockIdx.x] : blockIdx.x; + const int64_t slot = logical_row % logical_n1; + const int64_t token = logical_row / logical_n1; + const int64_t gate_base = token * gate_st + slot * gate_s1; + const int64_t up_base = token * up_st + slot * up_s1; + + ggml_cuda_pdl_sync(); + float values[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + if (i0 + i < ne00) { + volatile float value = ggml_cuda_op_silu_single(gate[gate_base + i0 + i]) * up[up_base + i0 + i]; + values[i] = value; + } else { + values[i] = 0.0f; + } + } + const float4 xi = make_float4(values[0], values[1], values[2], values[3]); + + float amax = fabsf(xi.x); + amax = fmaxf(amax, fabsf(xi.y)); + amax = fmaxf(amax, fabsf(xi.z)); + amax = fmaxf(amax, fabsf(xi.w)); +#pragma unroll + for (int offset = 4; offset > 0; offset >>= 1) { + amax = fmaxf(amax, __shfl_xor_sync(0xFFFFFFFF, amax, offset, WARP_SIZE)); + } + + const float d_inv = 127.0f / amax; + char4 q; + q.x = roundf(xi.x * d_inv); + q.y = roundf(xi.y * d_inv); + q.z = roundf(xi.z * d_inv); + q.w = roundf(xi.w * d_inv); + const float d = 1.0f / d_inv; + + block_q8_1_mmq * y = (block_q8_1_mmq *) vy; + const int64_t k_block = i0 / QK8_1_MMQ; + const int64_t iqs = i0 % QK8_1_MMQ; + const int64_t ib = k_block * ne1 + blockIdx.x; + ((char4 *) y[ib].qs)[iqs / 4] = q; + if (iqs % 32 == 0) { + y[ib].d4[iqs / 32] = d; + } +} + void quantize_row_q8_1_cuda( const float * x, const int32_t * ids, void * vy, const ggml_type type_src0, const int64_t ne00, const int64_t s01, const int64_t s02, const int64_t s03, @@ -580,7 +632,8 @@ void quantize_mmq_q8_1_cuda( GGML_ASSERT(ne0 % QK8_1_MMQ == 0); // ne1 tends to assume the highest values, therefore use it as the "x" dimension of the CUDA grid: - const int64_t block_num_y = (ne0 + 4*CUDA_QUANTIZE_BLOCK_SIZE_MMQ - 1) / (4*CUDA_QUANTIZE_BLOCK_SIZE_MMQ); + const int64_t vals_per_block = 4 * CUDA_QUANTIZE_BLOCK_SIZE_MMQ * CUDA_QUANTIZE_MMQ_CHUNKS_PER_BLOCK; + const int64_t block_num_y = (ne0 + vals_per_block - 1) / vals_per_block; const dim3 num_blocks(ne1, block_num_y, ne2*ne3); const dim3 block_size(CUDA_QUANTIZE_BLOCK_SIZE_MMQ, 1, 1); switch (mmq_get_q8_1_ds_layout(type_src0)) { @@ -602,6 +655,20 @@ void quantize_mmq_q8_1_cuda( } } +void quantize_mmq_q8_1_swiglu_cuda( + const float * gate, const float * up, const int32_t * ids, void * vy, const ggml_type type_src0, + const int64_t ne00, const int64_t ne0, const int64_t ne1, const int logical_n1, + const int64_t gate_s1, const int64_t gate_st, const int64_t up_s1, const int64_t up_st, cudaStream_t stream) { + GGML_ASSERT(mmq_get_q8_1_ds_layout(type_src0) == MMQ_Q8_1_DS_LAYOUT_D4); + GGML_ASSERT(ne00 % 4 == 0); + GGML_ASSERT(ne0 % QK8_1_MMQ == 0); + const int64_t block_num_y = (ne0 + 4 * CUDA_QUANTIZE_BLOCK_SIZE_MMQ - 1) / (4 * CUDA_QUANTIZE_BLOCK_SIZE_MMQ); + const dim3 num_blocks(ne1, block_num_y, 1); + const dim3 block_size(CUDA_QUANTIZE_BLOCK_SIZE_MMQ, 1, 1); + quantize_mmq_q8_1_swiglu<<>>( + gate, up, ids, vy, ne00, ne0, ne1, logical_n1, gate_s1, gate_st, up_s1, up_st); +} + // scatter=true reuses the quant kernel: grid over tokens, ids = inverse map (token slot -> compact row) void quantize_scatter_mmq_q8_1_cuda( const float * x, const int32_t * ids_src1_inv, void * vy, const ggml_type type_src0, @@ -610,7 +677,8 @@ void quantize_scatter_mmq_q8_1_cuda( GGML_ASSERT(ne00 % 4 == 0); GGML_ASSERT(ne0 % QK8_1_MMQ == 0); - const int64_t block_num_y = (ne0 + 4*CUDA_QUANTIZE_BLOCK_SIZE_MMQ - 1) / (4*CUDA_QUANTIZE_BLOCK_SIZE_MMQ); + const int64_t vals_per_block = 4 * CUDA_QUANTIZE_BLOCK_SIZE_MMQ * CUDA_QUANTIZE_MMQ_CHUNKS_PER_BLOCK; + const int64_t block_num_y = (ne0 + vals_per_block - 1) / vals_per_block; const dim3 num_blocks(n_tokens, block_num_y, 1); const dim3 block_size(CUDA_QUANTIZE_BLOCK_SIZE_MMQ, 1, 1); switch (mmq_get_q8_1_ds_layout(type_src0)) { diff --git a/ggml/src/ggml-cuda/quantize.cuh b/ggml/src/ggml-cuda/quantize.cuh index 5f08dcbfe331..1801da05e871 100644 --- a/ggml/src/ggml-cuda/quantize.cuh +++ b/ggml/src/ggml-cuda/quantize.cuh @@ -7,9 +7,11 @@ #define CUDA_QUANTIZE_BLOCK_SIZE 256 #define CUDA_QUANTIZE_BLOCK_SIZE_MMQ 128 +#define CUDA_QUANTIZE_MMQ_CHUNKS_PER_BLOCK 2 static_assert(MATRIX_ROW_PADDING % CUDA_QUANTIZE_BLOCK_SIZE == 0, "Risk of out-of-bounds access."); static_assert(MATRIX_ROW_PADDING % (4*CUDA_QUANTIZE_BLOCK_SIZE_MMQ) == 0, "Risk of out-of-bounds access."); +static_assert((4*CUDA_QUANTIZE_BLOCK_SIZE_MMQ) % QK8_1_MMQ == 0, "Quantization chunks must contain complete Q8_1 MMQ blocks."); typedef void (*quantize_cuda_t)( const float * x, const int32_t * ids, void * vy, @@ -26,6 +28,11 @@ void quantize_mmq_q8_1_cuda( ggml_type type_src0, int64_t ne00, int64_t s01, int64_t s02, int64_t s03, int64_t ne0, int64_t ne1, int64_t ne2, int64_t ne3, cudaStream_t stream); +void quantize_mmq_q8_1_swiglu_cuda( + const float * gate, const float * up, const int32_t * ids, void * vy, ggml_type type_src0, + int64_t ne00, int64_t ne0, int64_t ne1, int logical_n1, + int64_t gate_s1, int64_t gate_st, int64_t up_s1, int64_t up_st, cudaStream_t stream); + void quantize_mmq_fp4_cuda(const float * x, const int32_t * ids, void * vy, From 87a4f9b8d7c3c033b65b84b30d5cdc4a15deb28d Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:23:35 +0200 Subject: [PATCH 021/212] hip: fuse common Qwen inference operations Add gfx1151 paths for paired matmuls, SwiGLU quantization, expert aggregation, contiguous binary operations, transposed concat, normalization, and conversion operations. Assisted-by: OpenCode --- ggml/src/ggml-cuda/binbcast.cu | 123 +++++++++++++++++++++ ggml/src/ggml-cuda/binbcast.cuh | 2 + ggml/src/ggml-cuda/concat.cu | 73 ++++++++++++ ggml/src/ggml-cuda/convert.cu | 32 ++++++ ggml/src/ggml-cuda/ggml-cuda.cu | 190 +++++++++++++++++++++++++++++++- ggml/src/ggml-cuda/norm.cu | 72 +++++++++++- ggml/src/ggml-cuda/norm.cuh | 2 + ggml/src/ggml-cuda/unary.cu | 17 +++ ggml/src/ggml-cuda/unary.cuh | 3 + 9 files changed, 512 insertions(+), 2 deletions(-) diff --git a/ggml/src/ggml-cuda/binbcast.cu b/ggml/src/ggml-cuda/binbcast.cu index 2e38077bf67f..83361ab75182 100644 --- a/ggml/src/ggml-cuda/binbcast.cu +++ b/ggml/src/ggml-cuda/binbcast.cu @@ -26,6 +26,43 @@ static __device__ __forceinline__ float op_div(const float a, const float b) { return a / b; } +template +static __global__ void binary_contiguous_f32(const float * src0, const float * src1, float * dst, int64_t n) { + ggml_cuda_pdl_lc(); + int64_t i = int64_t(blockIdx.x) * blockDim.x + threadIdx.x; + const int64_t stride = int64_t(blockDim.x) * gridDim.x; + ggml_cuda_pdl_sync(); + for (; i < n; i += stride) { + dst[i] = bin_op(src0[i], src1[i]); + if (n - i <= stride) { + break; + } + } +} + +template +static bool try_binary_contiguous_f32(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + const ggml_tensor * src0 = dst->src[0]; + const ggml_tensor * src1 = dst->src[1]; + if (src0->type != GGML_TYPE_F32 || src1->type != GGML_TYPE_F32 || dst->type != GGML_TYPE_F32 || + src0->ne[0] != 2048 || + !ggml_are_same_shape(src0, src1) || !ggml_are_same_shape(src0, dst) || + !ggml_is_contiguous(src0) || !ggml_is_contiguous(src1) || !ggml_is_contiguous(dst)) { + return false; + } + + const int threads = 256; + const int64_t n = ggml_nelements(dst); + if (n == 0) { + return true; + } + const int blocks = std::min((n - 1) / threads + 1, 65535); + const ggml_cuda_kernel_launch_params launch_params(blocks, threads, 0, ctx.stream()); + ggml_cuda_kernel_launch(binary_contiguous_f32, launch_params, + (const float *) src0->data, (const float *) src1->data, (float *) dst->data, n); + return true; +} + template (ctx, dst)) { + return; + } ggml_cuda_op_bin_bcast>(dst->src[0], dst->src[1], dst, dst->src[0]->data, dst->src[1]->data, dst->data, ctx.stream()); } @@ -443,6 +483,9 @@ void ggml_cuda_op_sub(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { } void ggml_cuda_op_mul(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + if (try_binary_contiguous_f32(ctx, dst)) { + return; + } ggml_cuda_op_bin_bcast>(dst->src[0], dst->src[1], dst, dst->src[0]->data, dst->src[1]->data, dst->data, ctx.stream()); } @@ -542,6 +585,86 @@ void ggml_cuda_op_fused_mul(ggml_backend_cuda_context & ctx, ggml_tensor * dst, } } +template +static __global__ void weighted_expert_sum_f32( + const float * experts, const float * weights, float * dst, + int64_t n_embd, int64_t n_tokens, int64_t experts_s1, int64_t experts_s2, + int64_t weights_s1, int64_t weights_s2) { + const int64_t index = int64_t(blockIdx.x) * blockDim.x + threadIdx.x; + if (index >= n_embd * n_tokens) { + return; + } + + const int64_t token = index / n_embd; + const int64_t channel = index - token * n_embd; + // Keep MUL rounding separate from ADD to match the unfused graph. + volatile float product = experts[token * experts_s2 + channel] * weights[token * weights_s2]; + float sum = product; +#pragma unroll + for (int i = 1; i < n_expert_used; ++i) { + product = experts[token * experts_s2 + i * experts_s1 + channel] * weights[token * weights_s2 + i * weights_s1]; + sum += product; + } + dst[index] = sum; +} + +void ggml_cuda_op_weighted_expert_sum(ggml_backend_cuda_context & ctx, ggml_tensor * mul, ggml_tensor * dst, int n_expert_used) { + const ggml_tensor * experts = ggml_are_same_shape(mul, mul->src[0]) ? mul->src[0] : mul->src[1]; + const ggml_tensor * weights = experts == mul->src[0] ? mul->src[1] : mul->src[0]; + const int64_t n_embd = experts->ne[0]; + const int64_t n_tokens = experts->ne[2]; + const int threads = 256; + const int blocks = (n_embd * n_tokens + threads - 1) / threads; + const ggml_cuda_kernel_launch_params launch_params(blocks, threads, 0, ctx.stream()); + // MUL output may alias experts, so stage until all expert reads finish. + ggml_cuda_pool_alloc result(ctx.pool(), n_embd * n_tokens); + +#define launch_weighted_expert_sum(n) \ + ggml_cuda_kernel_launch(weighted_expert_sum_f32, launch_params, \ + (const float *) experts->data, (const float *) weights->data, result.get(), \ + n_embd, n_tokens, experts->nb[1] / sizeof(float), experts->nb[2] / sizeof(float), \ + weights->nb[1] / sizeof(float), weights->nb[2] / sizeof(float)) + switch (n_expert_used) { + case 2: launch_weighted_expert_sum(2); break; + case 3: launch_weighted_expert_sum(3); break; + case 4: launch_weighted_expert_sum(4); break; + case 5: launch_weighted_expert_sum(5); break; + case 6: launch_weighted_expert_sum(6); break; + case 7: launch_weighted_expert_sum(7); break; + case 8: launch_weighted_expert_sum(8); break; + default: GGML_ABORT("unsupported expert count"); + } +#undef launch_weighted_expert_sum + + CUDA_CHECK(cudaMemcpyAsync(dst->data, result.get(), ggml_nbytes(dst), cudaMemcpyDeviceToDevice, ctx.stream())); +} + +static __global__ void shared_mul_add_f32( + const float * src, const float * gate, const float * other, const float * residual, float * dst, + int64_t n_embd, int64_t nelements) { + const int64_t index = int64_t(blockIdx.x) * blockDim.x + threadIdx.x; + if (index >= nelements) { + return; + } + volatile float product = src[index] * gate[index / n_embd]; + float value = other[index] + product; + dst[index] = value + residual[index]; +} + +void ggml_cuda_op_shared_mul_add( + ggml_backend_cuda_context & ctx, ggml_tensor * mul, const ggml_tensor * other, + const ggml_tensor * residual, ggml_tensor * dst) { + const ggml_tensor * src = ggml_are_same_shape(mul, mul->src[0]) ? mul->src[0] : mul->src[1]; + const ggml_tensor * gate = src == mul->src[0] ? mul->src[1] : mul->src[0]; + const int threads = 256; + const int64_t nelements = ggml_nelements(dst); + const int blocks = (nelements + threads - 1) / threads; + const ggml_cuda_kernel_launch_params launch_params(blocks, threads, 0, ctx.stream()); + ggml_cuda_kernel_launch(shared_mul_add_f32, launch_params, + (const float *) src->data, (const float *) gate->data, (const float *) other->data, + (const float *) residual->data, (float *) dst->data, dst->ne[0], nelements); +} + void ggml_cuda_op_repeat_back(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { const ggml_tensor * src0 = dst->src[0]; diff --git a/ggml/src/ggml-cuda/binbcast.cuh b/ggml/src/ggml-cuda/binbcast.cuh index 12624785b444..916c5ac34931 100644 --- a/ggml/src/ggml-cuda/binbcast.cuh +++ b/ggml/src/ggml-cuda/binbcast.cuh @@ -10,3 +10,5 @@ void ggml_cuda_op_repeat_back(ggml_backend_cuda_context & ctx, ggml_tensor * dst void ggml_cuda_op_fused_add(ggml_backend_cuda_context & ctx, ggml_tensor * dst, int n_fuse); void ggml_cuda_op_fused_mul(ggml_backend_cuda_context & ctx, ggml_tensor * dst, int n_fuse); +void ggml_cuda_op_weighted_expert_sum(ggml_backend_cuda_context & ctx, ggml_tensor * mul, ggml_tensor * dst, int n_expert_used); +void ggml_cuda_op_shared_mul_add(ggml_backend_cuda_context & ctx, ggml_tensor * mul, const ggml_tensor * other, const ggml_tensor * residual, ggml_tensor * dst); diff --git a/ggml/src/ggml-cuda/concat.cu b/ggml/src/ggml-cuda/concat.cu index 6df89013ca79..408cbce3d219 100644 --- a/ggml/src/ggml-cuda/concat.cu +++ b/ggml/src/ggml-cuda/concat.cu @@ -2,6 +2,13 @@ #include +static constexpr int concat_transposed_tile_y(int cc) { + return GGML_CUDA_CC_IS_RDNA3_5(cc) ? 16 : 8; +} + +static_assert(concat_transposed_tile_y(GGML_CUDA_CC_RDNA3_5) == 16); +static_assert(concat_transposed_tile_y(GGML_CUDA_CC_RDNA3) == 8); + // contiguous kernels template static __global__ void __launch_bounds__(CUDA_CONCAT_BLOCK_SIZE) concat_cont(const T * x, @@ -79,6 +86,54 @@ static void concat_cont_cuda(const T * x, concat_cont<<>>(x, y, dst, ne00, ne01, ne02, ne0, ne1, ne2); } +template +static __global__ void concat_transposed_src1_dim0( + const char * src0, const char * src1, char * dst, + int64_t ne00, int64_t ne10, int64_t ne11, + uint64_t nb00, uint64_t nb01, uint64_t nb02, + uint64_t nb10, uint64_t nb11, uint64_t nb12, + uint64_t nb0, uint64_t nb1, uint64_t nb2) { + constexpr int tile = 32; + __shared__ T values[tile][tile + 1]; + + const int tx = threadIdx.x; + const int ty = threadIdx.y; + const int64_t channel0 = blockIdx.x * tile; + const int64_t token0 = blockIdx.y * tile; + const int64_t i2 = blockIdx.z; + + ggml_cuda_pdl_sync(); + + for (int j = 0; j < tile; j += blockDim.y) { + const int64_t token = token0 + ty + j; + const int64_t channel = channel0 + tx; + if (token < ne10 && channel < ne11) { + values[ty + j][tx] = *reinterpret_cast(src1 + i2 * nb12 + channel * nb11 + token * nb10); + } + } + __syncthreads(); + + for (int j = 0; j < tile; j += blockDim.y) { + const int64_t token = token0 + tx; + const int64_t channel = channel0 + ty + j; + if (token < ne10 && channel < ne11) { + *reinterpret_cast(dst + i2 * nb2 + channel * nb1 + (ne00 + token) * nb0) = values[tx][ty + j]; + } + } + + if (blockIdx.y == 0) { + for (int j = 0; j < tile; j += blockDim.y) { + const int64_t channel = channel0 + ty + j; + if (channel < ne11) { + for (int64_t state = tx; state < ne00; state += tile) { + *reinterpret_cast(dst + i2 * nb2 + channel * nb1 + state * nb0) = + *reinterpret_cast(src0 + i2 * nb02 + channel * nb01 + state * nb00); + } + } + } + } +} + // non-contiguous kernel (slow) template static __global__ void __launch_bounds__(CUDA_CONCAT_BLOCK_SIZE) @@ -141,6 +196,24 @@ static __global__ void __launch_bounds__(CUDA_CONCAT_BLOCK_SIZE) template static void concat_cuda(const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst, int dim, cudaStream_t stream) { + if (dim == 0 && src1->ne[0] >= 32 && src0->ne[3] == 1 && src1->ne[3] == 1 && dst->ne[3] == 1 && + src0->nb[0] == sizeof(T) && src0->nb[1] == src0->ne[0] * sizeof(T) && + src1->nb[1] == sizeof(T) && src1->nb[0] == src1->ne[1] * sizeof(T) && + dst->nb[0] == sizeof(T) && dst->nb[1] == dst->ne[0] * sizeof(T) && + src0->ne[1] == src1->ne[1] && src0->ne[2] == src1->ne[2]) { + constexpr int tile = 32; + const int tile_y = concat_transposed_tile_y(ggml_cuda_info().devices[ggml_cuda_get_device()].cc); + const dim3 block_dims(tile, tile_y, 1); + const dim3 block_nums((src1->ne[1] + tile - 1) / tile, (src1->ne[0] + tile - 1) / tile, src1->ne[2]); + concat_transposed_src1_dim0<<>>( + (const char *) src0->data, (const char *) src1->data, (char *) dst->data, + src0->ne[0], src1->ne[0], src1->ne[1], + src0->nb[0], src0->nb[1], src0->nb[2], + src1->nb[0], src1->nb[1], src1->nb[2], + dst->nb[0], dst->nb[1], dst->nb[2]); + return; + } + if (dim != 3 && ggml_is_contiguous_to_3(src0) && ggml_is_contiguous_to_3(src1)) { const T * src0_d = (const T *) src0->data; const T * src1_d = (const T *) src1->data; diff --git a/ggml/src/ggml-cuda/convert.cu b/ggml/src/ggml-cuda/convert.cu index 360c614a4403..47cab5f980d4 100644 --- a/ggml/src/ggml-cuda/convert.cu +++ b/ggml/src/ggml-cuda/convert.cu @@ -450,8 +450,40 @@ static void convert_unary_cuda(const void * vx, dst_t * y, (vx, y, ne00, ne01, ne0203, ne02_fdv, s01, s02, s03); } +static __global__ void convert_f32_f16_cont(const float * x, half * y, int64_t k) { + const int64_t i = 2 * ((int64_t) blockDim.x * blockIdx.x + threadIdx.x); + if (i + 1 < k) { + const float2 value = ((const float2 *) x)[i / 2]; + ((half2 *) y)[i / 2] = make_half2(value.x, value.y); + } else if (i < k) { + y[i] = x[i]; + } +} + +static __global__ void convert_f16_f32_cont(const half * x, float * y, int64_t k) { + const int64_t i = 2 * ((int64_t) blockDim.x * blockIdx.x + threadIdx.x); + if (i + 1 < k) { + ((float2 *) y)[i / 2] = __half22float2(((const half2 *) x)[i / 2]); + } else if (i < k) { + y[i] = x[i]; + } +} + template static void convert_unary_cont_cuda(const void * vx, dst_t * y, const int64_t k, cudaStream_t stream) { + const int nblocks = (k + 2*CUDA_DEQUANTIZE_BLOCK_SIZE - 1) / (2*CUDA_DEQUANTIZE_BLOCK_SIZE); + if constexpr (std::is_same::value && std::is_same::value) { + if ((uintptr_t) vx % alignof(float2) == 0 && (uintptr_t) y % alignof(half2) == 0) { + convert_f32_f16_cont<<>>((const float *) vx, (half *) y, k); + return; + } + } + if constexpr (std::is_same::value && std::is_same::value) { + if ((uintptr_t) vx % alignof(half2) == 0 && (uintptr_t) y % alignof(float2) == 0) { + convert_f16_f32_cont<<>>((const half *) vx, (float *) y, k); + return; + } + } convert_unary_cuda(vx, y, k, 1, 1, 1, k, k, k, stream); } diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 7c2d3cad20a8..e1c156af2fea 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -1834,7 +1834,8 @@ static void ggml_cuda_mul_mat(ggml_backend_cuda_context & ctx, const ggml_tensor const int cc = ggml_cuda_info().devices[ctx.device].cc; const int warp_size = ggml_cuda_info().devices[ctx.device].warp_size; - if (ggml_cuda_should_use_mmvf(src0->type, cc, src0->ne, src0->nb, ne11)) { + const int64_t mmvf_ncols = hint == GGML_HINT_EXACT_BATCH && src0->type == GGML_TYPE_BF16 ? 1 : ne11; + if (ggml_cuda_should_use_mmvf(src0->type, cc, src0->ne, src0->nb, mmvf_ncols)) { // The custom F16 vector kernel can be used over batched cuBLAS GEMM. // But this is only faster for GPUs without tensor cores or with a thin src0 matrix (particularly KQV in attention) ggml_cuda_mul_mat_vec_f(ctx, src0, src1, nullptr, dst); @@ -3286,6 +3287,32 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph ggml_tensor * node = cgraph->nodes[i]; + if (node->op == GGML_OP_L2_NORM && i + 2 < cgraph->n_nodes && + GGML_CUDA_CC_IS_AMD(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + ggml_tensor * k_view = cgraph->nodes[i + 1]; + ggml_tensor * k_norm = cgraph->nodes[i + 2]; + const ggml_tensor * q_view = node->src[0]; + const int out_nodes[] = { i, i + 2 }; + const bool graph_ok = k_view->op == GGML_OP_VIEW && k_norm->op == GGML_OP_L2_NORM && + k_norm->src[0] == k_view && (k_view->flags & GGML_TENSOR_FLAG_COMPUTE) != 0 && + (k_norm->flags & GGML_TENSOR_FLAG_COMPUTE) != 0 && ggml_node_get_use_count(cgraph, i + 1) == 1 && + (k_view->flags & GGML_TENSOR_FLAG_OUTPUT) == 0; + const bool shape_ok = q_view && q_view->op == GGML_OP_VIEW && q_view->view_src && + q_view->view_src == k_view->view_src && node->ne[0] == 128 && ggml_are_same_shape(node, k_norm) && + ggml_are_same_shape(q_view, k_view) && q_view->type == GGML_TYPE_F32 && k_view->type == GGML_TYPE_F32 && + node->type == GGML_TYPE_F32 && k_norm->type == GGML_TYPE_F32; + const bool layout_ok = shape_ok && ggml_is_contiguous_rows(q_view) && ggml_is_contiguous_rows(k_view) && + ggml_is_contiguous(node) && ggml_is_contiguous(k_norm) && + q_view->nb[1] == k_view->nb[1] && q_view->nb[2] == k_view->nb[2] && q_view->nb[3] == k_view->nb[3] && + k_view->view_offs == q_view->view_offs + ggml_row_size(q_view->type, q_view->ne[0])*q_view->ne[1] && + q_view->nb[2] > ggml_row_size(q_view->type, q_view->ne[0])*q_view->ne[1] && q_view->ne[2] <= UINT16_MAX/2; + + if (graph_ok && layout_ok && ggml_cuda_check_fusion_memory_ranges(cgraph, i, 3, out_nodes, 2)) { + ggml_cuda_op_l2_norm_dual_s128(*cuda_ctx, node, k_norm); + return 2; + } + } + // gated_delta_net -> cpy: scatter recurrent-state snapshots into the cache if (node->op == GGML_OP_GATED_DELTA_NET) { ggml_cuda_gated_delta_net_fused_cache fused_state_cpy; @@ -3300,6 +3327,36 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } } + if (node->op == GGML_OP_GLU && i + 1 < cgraph->n_nodes && ggml_get_glu_op(node) == GGML_GLU_OP_SWIGLU && node->src[1]) { + ggml_tensor * next = cgraph->nodes[i + 1]; + const bool has_ids = next->op == GGML_OP_MUL_MAT_ID; + if (has_ids || next->op == GGML_OP_MUL_MAT) { + const ggml_tensor * weights = next->src[0]; + const ggml_tensor * ids = has_ids ? next->src[2] : nullptr; + const ggml_tensor * gate = node->src[0]; + const ggml_tensor * up = node->src[1]; + const int cc = ggml_cuda_info().devices[cuda_ctx->device].cc; + const int64_t mmq_cols = has_ids ? node->ne[2] : node->ne[1]; + const int64_t n_experts = has_ids ? weights->ne[2] : 0; + const bool bad_padding_clear = ggml_backend_buffer_get_usage(weights->buffer) == GGML_BACKEND_BUFFER_USAGE_COMPUTE && + ggml_nbytes(weights) != ggml_backend_buffer_get_alloc_size(weights->buffer, weights) && weights->view_src; + + const bool target_shape = node->ne[0] == 512 && next->ne[0] == 2048 && + (!has_ids || (gate->ne[1] == 8 && weights->ne[2] == 256)); + const bool shape_ok = target_shape && next->src[1] == node && gate->type == GGML_TYPE_F32 && up->type == GGML_TYPE_F32 && + node->type == GGML_TYPE_F32 && next->type == GGML_TYPE_F32 && weights->type == GGML_TYPE_Q8_0 && + ggml_are_same_shape(gate, up) && ggml_are_same_shape(gate, node) && gate->nb[0] == sizeof(float) && up->nb[0] == sizeof(float) && + gate->ne[3] == 1 && (has_ids ? gate->ne[1] == ids->ne[0] && gate->ne[2] == ids->ne[1] : gate->ne[2] == 1); + const bool use_mmq = shape_ok && !bad_padding_clear && GGML_CUDA_CC_IS_RDNA3_5(cc) && + ggml_cuda_should_use_mmq(weights->type, cc, mmq_cols, n_experts); + + if (use_mmq && ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_GLU, next->op }, { i + 1 })) { + ggml_cuda_mul_mat_q_swiglu(*cuda_ctx, weights, ids, next, node); + return 1; + } + } + } + //topk-moe if (cgraph->nodes[i]->op == GGML_OP_UNARY || cgraph->nodes[i]->op == GGML_OP_SOFT_MAX || cgraph->nodes[i]->op == GGML_OP_ARGSORT) { @@ -3432,6 +3489,86 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } } + if (node->op == GGML_OP_MUL && node->ne[0] == 2048 && i + 2 < cgraph->n_nodes && + ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_MUL, GGML_OP_ADD, GGML_OP_ADD }, { i + 2 })) { + ggml_tensor * add0 = cgraph->nodes[i + 1]; + ggml_tensor * add1 = cgraph->nodes[i + 2]; + const ggml_tensor * src = ggml_are_same_shape(node, node->src[0]) ? node->src[0] : node->src[1]; + const ggml_tensor * gate = src == node->src[0] ? node->src[1] : node->src[0]; + const ggml_tensor * other = add0->src[0]; + const ggml_tensor * residual = add1->src[1]; + const uintptr_t gate_begin = (uintptr_t) gate->data; + const uintptr_t gate_end = gate_begin + ggml_backend_buft_get_alloc_size(gate->buffer->buft, gate); + const uintptr_t dst_begin = (uintptr_t) add1->data; + const uintptr_t dst_end = dst_begin + ggml_backend_buft_get_alloc_size(add1->buffer->buft, add1); + const bool gate_overlap = gate_begin < dst_end && dst_begin < gate_end; + const auto safe_output_alias = [&](const ggml_tensor * input) { + const uintptr_t begin = (uintptr_t) input->data; + const uintptr_t end = begin + ggml_backend_buft_get_alloc_size(input->buffer->buft, input); + return !(begin < dst_end && dst_begin < end) || (input->data == add1->data && ggml_are_same_layout(input, add1)); + }; + + if (add0->src[1] == node && add1->src[0] == add0 && other != node && residual != node && residual != add0 && + node->type == GGML_TYPE_F32 && + src->type == GGML_TYPE_F32 && gate->type == GGML_TYPE_F32 && other->type == GGML_TYPE_F32 && residual->type == GGML_TYPE_F32 && + ggml_are_same_shape(node, src) && ggml_are_same_shape(node, other) && ggml_are_same_shape(node, residual) && + gate->ne[0] == 1 && gate->ne[1] == node->ne[1] && gate->ne[2] == node->ne[2] && gate->ne[3] == node->ne[3] && + ggml_is_contiguous(src) && ggml_is_contiguous(gate) && ggml_is_contiguous(other) && + ggml_is_contiguous(residual) && ggml_is_contiguous(add1) && !gate_overlap && + safe_output_alias(src) && safe_output_alias(other) && safe_output_alias(residual) && + ggml_nelements(add1) <= int64_t(INT_MAX) * 256) { + ggml_cuda_op_shared_mul_add(*cuda_ctx, node, other, residual, add1); + return 2; + } + } + + if (node->op == GGML_OP_MUL && node->type == GGML_TYPE_F32 && node->ne[0] == 2048 && + node->ne[3] == 1 && node->ne[1] == 8) { + const int n_expert_used = node->ne[1]; + const int n_ops = 2 * n_expert_used; + if (i + n_ops <= cgraph->n_nodes) { + std::vector ops = { GGML_OP_MUL }; + ops.insert(ops.end(), n_expert_used, GGML_OP_VIEW); + ops.insert(ops.end(), n_expert_used - 1, GGML_OP_ADD); + const int output_idx = i + n_ops - 1; + + if (ggml_can_fuse_subgraph(cgraph, i, n_ops, ops.data(), &output_idx, 1)) { + const ggml_tensor * experts = ggml_are_same_shape(node, node->src[0]) ? node->src[0] : node->src[1]; + const ggml_tensor * weights = experts == node->src[0] ? node->src[1] : node->src[0]; + ggml_tensor * output = cgraph->nodes[output_idx]; + bool valid = experts->op == GGML_OP_MUL_MAT_ID && experts->type == GGML_TYPE_F32 && weights->type == GGML_TYPE_F32 && + experts->ne[0] == node->ne[0] && experts->ne[1] == n_expert_used && experts->ne[2] == node->ne[2] && + experts->ne[3] == 1 && weights->ne[0] == 1 && weights->ne[1] == n_expert_used && + weights->ne[2] == node->ne[2] && weights->ne[3] == 1 && + ggml_is_contiguous(experts) && ggml_is_contiguous(weights) && ggml_is_contiguous(output) && + output->type == GGML_TYPE_F32 && output->ne[0] == node->ne[0] && output->ne[1] == node->ne[2] && + output->ne[2] == 1 && output->ne[3] == 1 && ggml_nelements(output) <= UINT32_MAX; + + for (int j = 0; valid && j < n_expert_used; ++j) { + const ggml_tensor * view = cgraph->nodes[i + 1 + j]; + valid = view->src[0] == node && view->ne[0] == node->ne[0] && view->ne[1] == node->ne[2] && + view->ne[2] == 1 && view->ne[3] == 1 && view->nb[0] == sizeof(float) && + view->nb[1] == node->nb[2] && view->view_offs == size_t(j) * node->nb[1]; + } + + const int add_start = i + 1 + n_expert_used; + if (valid) { + const ggml_tensor * first = cgraph->nodes[add_start]; + valid = first->src[0] == cgraph->nodes[i + 1] && first->src[1] == cgraph->nodes[i + 2]; + } + for (int j = 2; valid && j < n_expert_used; ++j) { + const ggml_tensor * add = cgraph->nodes[add_start + j - 1]; + valid = add->src[0] == cgraph->nodes[add_start + j - 2] && add->src[1] == cgraph->nodes[i + 1 + j]; + } + + if (valid) { + ggml_cuda_op_weighted_expert_sum(*cuda_ctx, node, output, n_expert_used); + return n_ops - 1; + } + } + } + } + // multi-(add or mul) if (node->op == GGML_OP_ADD || node->op == GGML_OP_MUL) { int n_fuse = 0; @@ -3468,6 +3605,39 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } } + if ((node->op == GGML_OP_MUL_MAT_ID || node->op == GGML_OP_MUL_MAT) && i + 1 < cgraph->n_nodes) { + ggml_tensor * next = cgraph->nodes[i + 1]; + const ggml_tensor * src0 = node->src[0]; + const ggml_tensor * src0_next = next->src[0]; + const int cc = ggml_cuda_info().devices[cuda_ctx->device].cc; + + const bool has_ids = node->op == GGML_OP_MUL_MAT_ID; + const bool valid_sources = next->op == node->op && src0 && src0_next && node->src[1] && next->src[1] && + (!has_ids || node->src[2] && next->src[2]); + const bool ordinary_q8 = valid_sources && !has_ids && src0->type == GGML_TYPE_Q8_0 && src0_next->type == GGML_TYPE_Q8_0; + const bool shared_inputs = valid_sources && (has_ids || ordinary_q8) && node->src[1] == next->src[1] && + (!has_ids || node->src[2] == next->src[2]); + const int64_t mmq_cols = shared_inputs ? (has_ids ? node->src[1]->ne[2] : node->src[1]->ne[1]) : 0; + const int64_t n_experts = shared_inputs && has_ids ? src0->ne[2] : 0; + const bool use_mmq = shared_inputs && + ggml_cuda_should_use_mmq(src0->type, cc, mmq_cols, n_experts) && + ggml_cuda_should_use_mmq(src0_next->type, cc, mmq_cols, n_experts); + const bool compatible = use_mmq && node->src[1]->type == GGML_TYPE_F32 && + node->type == GGML_TYPE_F32 && next->type == GGML_TYPE_F32 && + ggml_are_same_shape(src0, src0_next) && ggml_are_same_shape(node, next) && + mmq_get_q8_1_ds_layout(src0->type) == mmq_get_q8_1_ds_layout(src0_next->type) && + !blackwell_mma_available(cc); + const int64_t ncols_dst = has_ids ? node->ne[2] : node->ne[1]; + const bool use_mmvq = compatible && ncols_dst <= MMVQ_MAX_BATCH_SIZE && + (!has_ids || ncols_dst <= get_mmvq_mmid_max_batch(src0->type, cc) || + ncols_dst <= get_mmvq_mmid_max_batch(src0_next->type, cc)); + + if (compatible && !use_mmvq) { + ggml_cuda_mul_mat_q_pair(*cuda_ctx, node, next); + return 1; + } + } + bool fused_mul_mat_vec = false; int fused_node_count = 0; @@ -3998,6 +4168,24 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph return 1; } + if (node->op == GGML_OP_CONT && i + 2 < cgraph->n_nodes) { + ggml_tensor * unary = cgraph->nodes[i + 1]; + ggml_tensor * mul = cgraph->nodes[i + 2]; + const ggml_tensor * gate = node->src[0]; + const ggml_tensor * other = mul->src[0] == unary ? mul->src[1] : mul->src[0]; + const int out_nodes[] = { i + 2 }; + if (unary->op == GGML_OP_UNARY && ggml_get_unary_op(unary) == GGML_UNARY_OP_SIGMOID && mul->op == GGML_OP_MUL && + (mul->src[0] == unary || mul->src[1] == unary) && gate->type == GGML_TYPE_F32 && other->type == GGML_TYPE_F32 && + gate->nb[0] == sizeof(float) && gate->nb[1] == 2 * gate->ne[0] * sizeof(float) && + gate->nb[2] == gate->nb[1] * gate->ne[1] && gate->nb[3] == gate->nb[2] * gate->ne[2] && + ggml_is_contiguous(other) && ggml_are_same_shape(gate, other) && + ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_CONT, GGML_OP_UNARY, GGML_OP_MUL }, { i + 2 }) && + ggml_cuda_check_fusion_memory_ranges(cgraph, i, 3, out_nodes, 1)) { + ggml_cuda_op_cont_sigmoid_mul(*cuda_ctx, node, unary, mul); + return 2; + } + } + if (ggml_cuda_can_fuse(cgraph, i, { GGML_OP_UNARY, GGML_OP_MUL }, { GGML_UNARY_OP_SILU }) || ggml_cuda_can_fuse(cgraph, i, { GGML_OP_UNARY, GGML_OP_MUL }, { GGML_UNARY_OP_SIGMOID }) || ggml_cuda_can_fuse(cgraph, i, { GGML_OP_UNARY, GGML_OP_MUL }, { GGML_UNARY_OP_SOFTPLUS })) { diff --git a/ggml/src/ggml-cuda/norm.cu b/ggml/src/ggml-cuda/norm.cu index c3758cd50cfe..50f7367046db 100644 --- a/ggml/src/ggml-cuda/norm.cu +++ b/ggml/src/ggml-cuda/norm.cu @@ -277,6 +277,44 @@ static __global__ void l2_norm_f32( } } +static __global__ void l2_norm_dual_f32_s128( + const float * x_q, float * dst_q, const int64_t stride_q_row, const int64_t stride_q_channel, + const int64_t stride_q_sample, const float eps_q, + const float * x_k, float * dst_k, const int64_t stride_k_row, const int64_t stride_k_channel, + const int64_t stride_k_sample, const float eps_k) { + const int nrows = gridDim.x; + const int nchannels = gridDim.y / 2; + const bool is_k = blockIdx.y >= nchannels; + const int row = blockIdx.x; + const int channel = blockIdx.y % nchannels; + const int sample = blockIdx.z; + const int tid = threadIdx.x; + + const float * x = is_k ? x_k : x_q; + float * dst = is_k ? dst_k : dst_q; + const int64_t stride_row = is_k ? stride_k_row : stride_q_row; + const int64_t stride_channel = is_k ? stride_k_channel : stride_q_channel; + const int64_t stride_sample = is_k ? stride_k_sample : stride_q_sample; + const float eps = is_k ? eps_k : eps_q; + + x += sample*stride_sample + channel*stride_channel + row*stride_row; + dst += ((sample*nchannels + channel)*nrows + row)*128; + + float tmp = 0.0f; + ggml_cuda_pdl_sync(); + for (int col = tid; col < 128; col += WARP_SIZE) { + const float xi = x[col]; + tmp += xi * xi; + } + tmp = warp_reduce_sum(tmp); + ggml_cuda_pdl_lc(); + + const float scale = rsqrtf(fmaxf(tmp, eps * eps)); + for (int col = tid; col < 128; col += WARP_SIZE) { + dst[col] = scale * x[col]; + } +} + static void norm_f32_cuda( const float * x, float * dst, const int ncols, const int nrows, const int nchannels, const int nsamples, const int64_t stride_row, const int64_t stride_channel, const int64_t stride_sample, const float eps, cudaStream_t stream) { @@ -360,7 +398,14 @@ static void rms_norm_mul_f32_cuda(const float * x, const uint3 mul_nrows_packed = init_fastdiv_values(mul_nrows); const uint3 mul_nchannels_packed = init_fastdiv_values(mul_nchannels); const uint3 mul_nsamples_packed = init_fastdiv_values(mul_nsamples); - if (ncols < 1024) { + if (ncols == 128) { + const dim3 block_dims(128, 1, 1); + const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params{blocks_num, block_dims, block_dims.x > WARP_SIZE ? 32 * sizeof(float) : 0, stream}; + ggml_cuda_kernel_launch(rms_norm_f32<128, true>, launch_params, + x, dst, ncols, stride_row, stride_channel, stride_sample, eps, mul, mul_stride_row, mul_stride_channel, + mul_stride_sample, mul_ncols_packed, mul_nrows_packed, mul_nchannels_packed, mul_nsamples_packed, + nullptr, 0, 0, 0, make_uint3(0, 0, 0), make_uint3(0, 0, 0), make_uint3(0, 0, 0), make_uint3(0, 0, 0)); + } else if (ncols < 1024) { const dim3 block_dims(256, 1, 1); const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params{blocks_num, block_dims, block_dims.x > WARP_SIZE ? 32 * sizeof(float): 0, stream}; ggml_cuda_kernel_launch(rms_norm_f32<256, true>, launch_params, @@ -696,3 +741,28 @@ void ggml_cuda_op_l2_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { l2_norm_f32_cuda(src0_d, dst_d, ne00, ne01, ne02, ne03, s01, s02, s03, eps, stream); } + +void ggml_cuda_op_l2_norm_dual_s128(ggml_backend_cuda_context & ctx, ggml_tensor * dst_q, ggml_tensor * dst_k) { + const ggml_tensor * src_q = dst_q->src[0]; + const ggml_tensor * src_k = dst_k->src[0]; + + GGML_ASSERT(src_q->type == GGML_TYPE_F32 && src_k->type == GGML_TYPE_F32); + GGML_ASSERT(dst_q->type == GGML_TYPE_F32 && dst_k->type == GGML_TYPE_F32); + GGML_ASSERT(src_q->ne[0] == 128 && ggml_are_same_shape(src_q, src_k)); + GGML_ASSERT(src_q->nb[0] == sizeof(float) && src_k->nb[0] == sizeof(float)); + + float eps_q; + float eps_k; + memcpy(&eps_q, dst_q->op_params, sizeof(float)); + memcpy(&eps_k, dst_k->op_params, sizeof(float)); + GGML_ASSERT(eps_q >= 0.0f && eps_k >= 0.0f); + + const dim3 blocks_num(src_q->ne[1], 2*src_q->ne[2], src_q->ne[3]); + const dim3 block_dims(WARP_SIZE, 1, 1); + const ggml_cuda_kernel_launch_params launch_params = { blocks_num, block_dims, 0, ctx.stream() }; + ggml_cuda_kernel_launch(l2_norm_dual_f32_s128, launch_params, + (const float *) src_q->data, (float *) dst_q->data, + src_q->nb[1]/sizeof(float), src_q->nb[2]/sizeof(float), src_q->nb[3]/sizeof(float), eps_q, + (const float *) src_k->data, (float *) dst_k->data, + src_k->nb[1]/sizeof(float), src_k->nb[2]/sizeof(float), src_k->nb[3]/sizeof(float), eps_k); +} diff --git a/ggml/src/ggml-cuda/norm.cuh b/ggml/src/ggml-cuda/norm.cuh index a74f6376720a..c09d004905a9 100644 --- a/ggml/src/ggml-cuda/norm.cuh +++ b/ggml/src/ggml-cuda/norm.cuh @@ -16,3 +16,5 @@ void ggml_cuda_op_rms_norm_fused_add(ggml_backend_cuda_context & ctx, void ggml_cuda_op_rms_norm_back(ggml_backend_cuda_context & ctx, ggml_tensor * dst); void ggml_cuda_op_l2_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst); + +void ggml_cuda_op_l2_norm_dual_s128(ggml_backend_cuda_context & ctx, ggml_tensor * dst_q, ggml_tensor * dst_k); diff --git a/ggml/src/ggml-cuda/unary.cu b/ggml/src/ggml-cuda/unary.cu index d3e594878fcc..71a7d24c5552 100644 --- a/ggml/src/ggml-cuda/unary.cu +++ b/ggml/src/ggml-cuda/unary.cu @@ -702,6 +702,23 @@ void ggml_cuda_op_unary_mul(ggml_backend_cuda_context & ctx, ggml_tensor * unary } } +void ggml_cuda_op_cont_sigmoid_mul( + ggml_backend_cuda_context & ctx, ggml_tensor * cont_node, ggml_tensor * unary_node, ggml_tensor * mul_node) { + const ggml_tensor * gate = cont_node->src[0]; + const ggml_tensor * other = mul_node->src[0] == unary_node ? mul_node->src[1] : mul_node->src[0]; + + GGML_ASSERT(ggml_get_unary_op(unary_node) == GGML_UNARY_OP_SIGMOID); + GGML_ASSERT(gate->type == GGML_TYPE_F32 && other->type == GGML_TYPE_F32 && mul_node->type == GGML_TYPE_F32); + GGML_ASSERT(gate->nb[0] == sizeof(float) && gate->nb[2] == gate->nb[1] * gate->ne[1]); + GGML_ASSERT(ggml_is_contiguous(other) && ggml_is_contiguous(mul_node)); + GGML_ASSERT(ggml_nelements(gate) == ggml_nelements(other)); + + const int64_t k = ggml_nelements(other); + const int64_t n = gate->ne[0]; + unary_gated_cuda((const float *) gate->data, (const float *) other->data, (float *) mul_node->data, + k, n, gate->nb[1] / sizeof(float), n, ctx.stream()); +} + /* fused relu + sqr */ void ggml_cuda_op_relu_sqr(ggml_backend_cuda_context & ctx, ggml_tensor * relu_node, ggml_tensor * sqr_node) { diff --git a/ggml/src/ggml-cuda/unary.cuh b/ggml/src/ggml-cuda/unary.cuh index 04f3af6443a7..291fe16eaa33 100644 --- a/ggml/src/ggml-cuda/unary.cuh +++ b/ggml/src/ggml-cuda/unary.cuh @@ -93,6 +93,9 @@ void ggml_cuda_op_xielu(ggml_backend_cuda_context & ctx, ggml_tensor * dst); void ggml_cuda_op_unary_mul(ggml_backend_cuda_context & ctx, ggml_tensor * unary_node, ggml_tensor * mul_node); +void ggml_cuda_op_cont_sigmoid_mul( + ggml_backend_cuda_context & ctx, ggml_tensor * cont_node, ggml_tensor * unary_node, ggml_tensor * mul_node); + void ggml_cuda_op_relu_sqr(ggml_backend_cuda_context & ctx, ggml_tensor * relu_node, ggml_tensor * sqr_node); __device__ __forceinline__ float ggml_cuda_op_silu_single(float x) { From 3262315ad92fd83caca108b683144e657706ee9b Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:23:48 +0200 Subject: [PATCH 022/212] hip: optimize Flash Attention on RDNA3.5 Dequantize Q8 KV data directly in tiles, specialize GQA decode grouping, reduce D256 register pressure, and reuse masks across grouped heads on gfx1151. Assisted-by: OpenCode --- ggml/src/ggml-cuda/fattn-common.cuh | 6 + ggml/src/ggml-cuda/fattn-tile-rdna3-5.cu | 12 ++ ggml/src/ggml-cuda/fattn-tile.cuh | 232 +++++++++++++++++------ ggml/src/ggml-cuda/fattn.cu | 23 +++ 4 files changed, 215 insertions(+), 58 deletions(-) create mode 100644 ggml/src/ggml-cuda/fattn-tile-rdna3-5.cu diff --git a/ggml/src/ggml-cuda/fattn-common.cuh b/ggml/src/ggml-cuda/fattn-common.cuh index e67cc7fdf784..25854e94617d 100644 --- a/ggml/src/ggml-cuda/fattn-common.cuh +++ b/ggml/src/ggml-cuda/fattn-common.cuh @@ -1174,6 +1174,12 @@ void launch_fattn( } } + if constexpr (ncols2 == 3) { + if (GGML_CUDA_CC_IS_RDNA3_5(cc)) { + parallel_blocks = std::min(18, ntiles_KV); + } + } + blocks_num.x = ntiles_x; blocks_num.y = parallel_blocks; blocks_num.z = ntiles_z_gqa*K->ne[2]*Q->ne[3]; diff --git a/ggml/src/ggml-cuda/fattn-tile-rdna3-5.cu b/ggml/src/ggml-cuda/fattn-tile-rdna3-5.cu new file mode 100644 index 000000000000..586632da20e4 --- /dev/null +++ b/ggml/src/ggml-cuda/fattn-tile-rdna3-5.cu @@ -0,0 +1,12 @@ +#if defined(GGML_USE_HIP) + +#define GGML_CUDA_FATTN_VGPR192 +#include "fattn-tile.cuh" + +fattn_kernel_t ggml_cuda_fattn_tile_d256_ncols32_rdna3_5(const bool use_logit_softcap) { + return use_logit_softcap + ? flash_attn_tile<256, 256, 4, 8, true, false> + : flash_attn_tile<256, 256, 4, 8, false, false>; +} + +#endif // defined(GGML_USE_HIP) diff --git a/ggml/src/ggml-cuda/fattn-tile.cuh b/ggml/src/ggml-cuda/fattn-tile.cuh index d1164b8526d3..53029262aec4 100644 --- a/ggml/src/ggml-cuda/fattn-tile.cuh +++ b/ggml/src/ggml-cuda/fattn-tile.cuh @@ -213,6 +213,7 @@ static constexpr __host__ __device__ uint32_t ggml_cuda_fattn_tile_get_config_am GGML_CUDA_FATTN_TILE_CONFIG_CASE(192, 128, 32, 256, 2, 32, 64) GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 2, 256, 2, 128, 64) + GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 3, 96, 8, 32, 64) GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 4, 256, 2, 64, 128) GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 8, 256, 2, 64, 128) GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 16, 256, 2, 32, 128) @@ -291,6 +292,7 @@ static constexpr __host__ __device__ uint32_t ggml_cuda_fattn_tile_get_config_am GGML_CUDA_FATTN_TILE_CONFIG_CASE(192, 128, 32, 256, 3, 64, 64) GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 2, 64, 8, 32, 64) + GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 3, 96, 8, 32, 64) GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 4, 128, 6, 32, 256) GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 8, 128, 6, 32, 256) GGML_CUDA_FATTN_TILE_CONFIG_CASE(256, 256, 16, 256, 5, 32, 256) @@ -479,14 +481,46 @@ static __device__ __forceinline__ void flash_attn_tile_load_tile( ggml_cuda_unroll<5>{}(load); } +template +static __device__ __forceinline__ void flash_attn_tile_load_tile_q8_0( + const char * const __restrict__ KV, half2 * const __restrict__ tile_KV, const int stride_KV, const int i_sup) { + static_assert(J % QK8_0 == 0, "bad J"); + + constexpr int nthreads = warp_size*nwarps; + constexpr int groups_per_row = J/4; + const int tid = threadIdx.y*warp_size + threadIdx.x; + +#pragma unroll + for (int ig = tid; ig < I*groups_per_row; ig += nthreads) { + const int i = ig/groups_per_row; + const int j = (ig % groups_per_row)*4; + + half2 v0 = make_half2(0.0f, 0.0f); + half2 v1 = make_half2(0.0f, 0.0f); + if (!oob_check || i < i_sup) { + const block_q8_0 * row = (const block_q8_0 *) (KV + int64_t(i)*stride_KV); + const block_q8_0 & block = row[j/QK8_0]; + int q; + ggml_cuda_memcpy_1(&q, block.qs + j % QK8_0); + const int8_t * q8 = (const int8_t *) &q; + const half2 d = __half2half2(block.d); + v0 = d*make_half2(q8[0], q8[1]); + v1 = d*make_half2(q8[2], q8[3]); + } + + tile_KV[i*(J/2 + J_padding) + j/2 + 0] = v0; + tile_KV[i*(J/2 + J_padding) + j/2 + 1] = v1; + } +} + // Function that performs a single iteration in for the KQ matrix multiplication: template + bool use_logit_softcap, bool oob_check, bool q8_0_KV, typename T_vec_dot> static __device__ __forceinline__ void flash_attn_tile_iter_KQ( T_vec_dot * const Q_tmp, - const half2 * const __restrict__ K_h2, + const char * const __restrict__ K_data, T_vec_dot * const KV_tmp, - const int stride_K2, + const int stride_K, const int k_VKQ_0, const int k_VKQ_sup, const int k_KQ_0, @@ -498,8 +532,13 @@ static __device__ __forceinline__ void flash_attn_tile_iter_KQ( constexpr int cpw = ncols > nwarps ? ncols/nwarps : 1; // Q columns per warp constexpr int np = nwarps > ncols ? nwarps/ncols : 1; // number of parallel warps per Q column - flash_attn_tile_load_tile - (K_h2 + int64_t(k_VKQ_0)*stride_K2 + k_KQ_0/2, KV_tmp, stride_K2, k_VKQ_sup); + if constexpr (q8_0_KV) { + flash_attn_tile_load_tile_q8_0 + (K_data + int64_t(k_VKQ_0)*stride_K + k_KQ_0/QK8_0*sizeof(block_q8_0), KV_tmp, stride_K, k_VKQ_sup); + } else { + flash_attn_tile_load_tile + ((const half2 *) (K_data + int64_t(k_VKQ_0)*stride_K + k_KQ_0*sizeof(half)), KV_tmp, stride_K/sizeof(half2), k_VKQ_sup); + } __syncthreads(); #ifdef FAST_FP16_AVAILABLE @@ -537,13 +576,29 @@ static __device__ __forceinline__ void flash_attn_tile_iter_KQ( #endif // FAST_FP16_AVAILABLE } +#if defined(RDNA3_5) + if constexpr (DKQ == 256 && ncols == 32) { #pragma unroll - for (int i_KQ_0 = 0; i_KQ_0 < nbatch_fa; i_KQ_0 += np*warp_size) { + for (int k = 0; k < cpy_ne; ++k) { #pragma unroll - for (int jc0 = 0; jc0 < cpw; ++jc0) { + for (int i_KQ_0 = 0; i_KQ_0 < nbatch_fa; i_KQ_0 += np*warp_size) { #pragma unroll - for (int k = 0; k < cpy_ne; ++k) { - ggml_cuda_mad(KQ_acc[i_KQ_0/(np*warp_size)*cpw + jc0], K_k[i_KQ_0/(np*warp_size)][k], Q_k[jc0][k]); + for (int jc0 = 0; jc0 < cpw; ++jc0) { + ggml_cuda_mad(KQ_acc[i_KQ_0/(np*warp_size)*cpw + jc0], K_k[i_KQ_0/(np*warp_size)][k], Q_k[jc0][k]); + } + } + } + } else +#endif // defined(RDNA3_5) + { +#pragma unroll + for (int i_KQ_0 = 0; i_KQ_0 < nbatch_fa; i_KQ_0 += np*warp_size) { +#pragma unroll + for (int jc0 = 0; jc0 < cpw; ++jc0) { +#pragma unroll + for (int k = 0; k < cpy_ne; ++k) { + ggml_cuda_mad(KQ_acc[i_KQ_0/(np*warp_size)*cpw + jc0], K_k[i_KQ_0/(np*warp_size)][k], Q_k[jc0][k]); + } } } } @@ -556,19 +611,19 @@ static __device__ __forceinline__ void flash_attn_tile_iter_KQ( // Function that performs a single iteration of the main loop over up to nbatch_fa tokens. template + bool use_logit_softcap, bool oob_check, bool q8_0_KV, typename T_vec_dot, typename T_KQ, typename T_acc> static __device__ __forceinline__ void flash_attn_tile_iter( T_vec_dot * const Q_tmp, - const half2 * const __restrict__ K_h2, - const half2 * const __restrict__ V_h2, + const char * const __restrict__ K_data, + const char * const __restrict__ V_data, const half * const __restrict__ mask, const uint3 ne01, const float logit_softcap, const float slope, T_KQ * const KQ, T_vec_dot * const KV_tmp, - const int stride_K2, - const int stride_V2, + const int stride_K, + const int stride_V, const int stride_mask, float * const KQ_max, float * const KQ_sum, @@ -601,19 +656,32 @@ static __device__ __forceinline__ void flash_attn_tile_iter( KQ_max_new[jc0] = KQ_max[jc0]; } +#if defined(RDNA3_5) + constexpr bool common_mask = DKQ == 256 && DV == 256 && ncols1 == 4 && ncols2 == 8; + const int j_mask = common_mask ? fastmodulo(col_Q_0 + ((threadIdx.y / np)*cpw)/ncols2, ne01) : 0; + float mask_value[common_mask ? nbatch_fa/(np*warp_size) : 1]; +#pragma unroll + for (int i0 = 0; i0 < nbatch_fa; i0 += np*warp_size) { + mask_value[i0/(np*warp_size)] = common_mask ? slope*__half2float(mask[j_mask*stride_mask + k_VKQ_0 + i0 + (threadIdx.y % np)*warp_size + threadIdx.x]) : 0.0f; + } +#else + constexpr bool common_mask = false; + const float mask_value[1] = {0.0f}; +#endif + float KQ_acc[nbatch_fa/(np*warp_size) * cpw] = {0.0f}; // Accumulators for KQ matrix multiplication. // KQ = K @ Q matrix multiplication: constexpr int nbatch_K_last = DKQ % nbatch_K; #pragma unroll for (int k_KQ_0 = 0; k_KQ_0 < DKQ - nbatch_K_last; k_KQ_0 += nbatch_K) { - flash_attn_tile_iter_KQ( - Q_tmp, K_h2, KV_tmp, stride_K2, k_VKQ_0, k_VKQ_sup, k_KQ_0, KQ_acc); + flash_attn_tile_iter_KQ( + Q_tmp, K_data, KV_tmp, stride_K, k_VKQ_0, k_VKQ_sup, k_KQ_0, KQ_acc); } - if (nbatch_K_last > 0) { + if constexpr (nbatch_K_last > 0) { constexpr int k_KQ_0 = DKQ - nbatch_K_last; - flash_attn_tile_iter_KQ( - Q_tmp, K_h2, KV_tmp, stride_K2, k_VKQ_0, k_VKQ_sup, k_KQ_0, KQ_acc); + flash_attn_tile_iter_KQ( + Q_tmp, K_data, KV_tmp, stride_K, k_VKQ_0, k_VKQ_sup, k_KQ_0, KQ_acc); } // Apply logit softcap + mask, update KQ_max: @@ -636,8 +704,8 @@ static __device__ __forceinline__ void flash_attn_tile_iter( } if (!oob_check || i_KQ < k_VKQ_sup) { - KQ_acc[(i_KQ_0/(np*warp_size))*cpw + jc0] += (ncols2 > 1 || mask) ? - slope*__half2float(mask[j*stride_mask + k_VKQ_0 + i_KQ]) : 0.0f; + KQ_acc[(i_KQ_0/(np*warp_size))*cpw + jc0] += common_mask ? mask_value[i_KQ_0/(np*warp_size)] : + (ncols2 > 1 || mask) ? slope*__half2float(mask[j*stride_mask + k_VKQ_0 + i_KQ]) : 0.0f; KQ_max_new[jc0] = fmaxf(KQ_max_new[jc0], KQ_acc[(i_KQ_0/(np*warp_size))*cpw + jc0] + FATTN_KQ_MAX_OFFSET); } @@ -718,8 +786,13 @@ static __device__ __forceinline__ void flash_attn_tile_iter( static_assert(nbatch_V % np == 0, "bad nbatch_V"); #pragma unroll for (int k0 = 0; k0 < nbatch_fa; k0 += nbatch_V) { - flash_attn_tile_load_tile - (V_h2 + int64_t(k_VKQ_0 + k0)*stride_V2, KV_tmp, stride_V2, k_VKQ_sup - k0); + if constexpr (q8_0_KV) { + flash_attn_tile_load_tile_q8_0 + (V_data + int64_t(k_VKQ_0 + k0)*stride_V, KV_tmp, stride_V, k_VKQ_sup - k0); + } else { + flash_attn_tile_load_tile + ((const half2 *) (V_data + int64_t(k_VKQ_0 + k0)*stride_V), KV_tmp, stride_V/sizeof(half2), k_VKQ_sup - k0); + } __syncthreads(); #ifdef FAST_FP16_AVAILABLE @@ -788,8 +861,11 @@ static __device__ __forceinline__ void flash_attn_tile_iter( } } -template // D == head size +template // D == head size __launch_bounds__(ggml_cuda_fattn_tile_get_nthreads(DKQ, DV, ncols1*ncols2), ggml_cuda_fattn_tile_get_occupancy(DKQ, DV, ncols1*ncols2)) +#if defined(RDNA3_5) && defined(GGML_CUDA_FATTN_VGPR192) +__attribute__((amdgpu_num_vgpr(192))) +#endif // defined(RDNA3_5) && defined(GGML_CUDA_FATTN_VGPR192) static __global__ void flash_attn_tile( const char * Q_ptr, const char * K_ptr, @@ -853,14 +929,12 @@ static __global__ void flash_attn_tile( const int sequence = blockIdx.z / (ne02/ncols2); const int head0 = blockIdx.z*ncols2 - sequence*ne02; // == blockIdx.z % (ne02/ncols2) const int gqa_ratio = ne02 / ne12; // With grouped query attention there are > 1 Q matrices per K, V matrix. - const float * Q_f = (const float *) (Q + nb03*sequence + nb02* head0); - const half2 * K_h2 = (const half2 *) (K + nb13*sequence + nb12*(head0 / gqa_ratio)); - const half2 * V_h2 = (const half2 *) (V + nb23*sequence + nb22*(head0 / gqa_ratio)); // K and V have same shape + const float * Q_f = (const float *) (Q + nb03*sequence + nb02* head0); + const char * K_data = K + nb13*sequence + nb12*(head0 / gqa_ratio); + const char * V_data = V + nb23*sequence + nb22*(head0 / gqa_ratio); const half * maskh = mask ? (const half *) (mask + nb33*(sequence % ne33)) : nullptr; - const int stride_K2 = nb11 / sizeof(half2); - const int stride_V2 = nb21 / sizeof(half2); const int stride_mask = nb31 / sizeof(half); const float slope = ncols2 == 1 ? get_alibi_slope(max_bias, head0, n_head_log2, m0, m1) : 1.0f; @@ -957,24 +1031,24 @@ static __global__ void flash_attn_tile( int k_VKQ_0 = blockIdx.y*nbatch_fa; while (k_VKQ_0 < k_VKQ_max - nbatch_fa) { constexpr bool oob_check = false; - flash_attn_tile_iter - (Q_tmp, K_h2, V_h2, maskh, ne01, logit_softcap, slope, KQ, KV_tmp, - stride_K2, stride_V2, stride_mask, KQ_max, KQ_sum, VKQ, k_VKQ_0, k_VKQ_max, col_Q_0); + flash_attn_tile_iter + (Q_tmp, K_data, V_data, maskh, ne01, logit_softcap, slope, KQ, KV_tmp, + nb11, nb21, stride_mask, KQ_max, KQ_sum, VKQ, k_VKQ_0, k_VKQ_max, col_Q_0); k_VKQ_0 += gridDim.y*nbatch_fa; } if (k_VKQ_0 < k_VKQ_max) { constexpr bool oob_check = true; - flash_attn_tile_iter - (Q_tmp, K_h2, V_h2, maskh, ne01, logit_softcap, slope, KQ, KV_tmp, - stride_K2, stride_V2, stride_mask, KQ_max, KQ_sum, VKQ, k_VKQ_0, k_VKQ_max, col_Q_0); + flash_attn_tile_iter + (Q_tmp, K_data, V_data, maskh, ne01, logit_softcap, slope, KQ, KV_tmp, + nb11, nb21, stride_mask, KQ_max, KQ_sum, VKQ, k_VKQ_0, k_VKQ_max, col_Q_0); } } else { // Branch without out-of-bounds checks. for (int k_VKQ_0 = blockIdx.y*nbatch_fa; k_VKQ_0 < k_VKQ_max; k_VKQ_0 += gridDim.y*nbatch_fa) { constexpr bool oob_check = false; - flash_attn_tile_iter - (Q_tmp, K_h2, V_h2, maskh, ne01, logit_softcap, slope, KQ, KV_tmp, - stride_K2, stride_V2, stride_mask, KQ_max, KQ_sum, VKQ, k_VKQ_0, k_VKQ_max, col_Q_0); + flash_attn_tile_iter + (Q_tmp, K_data, V_data, maskh, ne01, logit_softcap, slope, KQ, KV_tmp, + nb11, nb21, stride_mask, KQ_max, KQ_sum, VKQ, k_VKQ_0, k_VKQ_max, col_Q_0); } } @@ -1145,6 +1219,38 @@ static __global__ void flash_attn_tile( #endif // FLASH_ATTN_AVAILABLE } +#ifdef GGML_USE_HIP +fattn_kernel_t ggml_cuda_fattn_tile_d256_ncols32_rdna3_5(const bool use_logit_softcap); +#endif // GGML_USE_HIP + +template +static void launch_fattn_tile_case( + ggml_backend_cuda_context & ctx, ggml_tensor * dst, const int nwarps, const int nbatch_fa, const int warp_size) { + constexpr size_t nbytes_shared = 0; + bool use_q8_0_KV = false; + fattn_kernel_t fattn_kernel; +#ifdef GGML_USE_HIP + if constexpr (DKQ == DV && (DKQ == 64 || DKQ == 128 || DKQ == 256)) { + use_q8_0_KV = dst->src[0]->ne[1] == 1 && dst->src[1]->type == GGML_TYPE_Q8_0 && dst->src[2]->type == GGML_TYPE_Q8_0; + fattn_kernel = use_q8_0_KV + ? flash_attn_tile + : flash_attn_tile; + } else { + fattn_kernel = flash_attn_tile; + } + if constexpr (DKQ == 256 && DV == 256 && ncols1 == 4 && ncols2 == 8) { + const int cc = ggml_cuda_info().devices[ggml_cuda_get_device()].cc; + if (GGML_CUDA_CC_IS_RDNA3_5(cc)) { + fattn_kernel = ggml_cuda_fattn_tile_d256_ncols32_rdna3_5(use_logit_softcap); + } + } +#else + fattn_kernel = flash_attn_tile; +#endif // GGML_USE_HIP + launch_fattn + (ctx, dst, fattn_kernel, nwarps, nbytes_shared, nbatch_fa, !use_q8_0_KV, !use_q8_0_KV, false, warp_size); +} + template static void launch_fattn_tile_switch_ncols1(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { const ggml_tensor * Q = dst->src[0]; @@ -1153,7 +1259,12 @@ static void launch_fattn_tile_switch_ncols1(ggml_backend_cuda_context & ctx, ggm const int cc = ggml_cuda_info().devices[id].cc; const int warp_size = 32; - constexpr size_t nbytes_shared = 0; + if constexpr (ncols2 == 3) { + const int nwarps = ggml_cuda_fattn_tile_get_nthreads (DKQ, DV, 3, cc) / warp_size; + const int nbatch_fa = ggml_cuda_fattn_tile_get_nbatch_fa(DKQ, DV, 3, cc); + launch_fattn_tile_case(ctx, dst, nwarps, nbatch_fa, warp_size); + return; + } else { #ifdef GGML_USE_HIP if constexpr (DKQ <= 128) { @@ -1161,9 +1272,8 @@ static void launch_fattn_tile_switch_ncols1(ggml_backend_cuda_context & ctx, ggm constexpr int cols_per_block = 64; const int nwarps = ggml_cuda_fattn_tile_get_nthreads (DKQ, DV, cols_per_block, cc) / warp_size; const int nbatch_fa = ggml_cuda_fattn_tile_get_nbatch_fa(DKQ, DV, cols_per_block, cc); - fattn_kernel_t fattn_kernel = flash_attn_tile; - launch_fattn - (ctx, dst, fattn_kernel, nwarps, nbytes_shared, nbatch_fa, true, true, false, warp_size); + launch_fattn_tile_case + (ctx, dst, nwarps, nbatch_fa, warp_size); return; } } @@ -1177,9 +1287,8 @@ static void launch_fattn_tile_switch_ncols1(ggml_backend_cuda_context & ctx, ggm constexpr int cols_per_block = 32; const int nwarps = ggml_cuda_fattn_tile_get_nthreads (DKQ, DV, cols_per_block, cc) / warp_size; const int nbatch_fa = ggml_cuda_fattn_tile_get_nbatch_fa(DKQ, DV, cols_per_block, cc); - fattn_kernel_t fattn_kernel = flash_attn_tile; - launch_fattn - (ctx, dst, fattn_kernel, nwarps, nbytes_shared, nbatch_fa, true, true, false, warp_size); + launch_fattn_tile_case + (ctx, dst, nwarps, nbatch_fa, warp_size); return; } } @@ -1189,9 +1298,8 @@ static void launch_fattn_tile_switch_ncols1(ggml_backend_cuda_context & ctx, ggm constexpr int cols_per_block = 16; const int nwarps = ggml_cuda_fattn_tile_get_nthreads (DKQ, DV, cols_per_block, cc) / warp_size; const int nbatch_fa = ggml_cuda_fattn_tile_get_nbatch_fa(DKQ, DV, cols_per_block, cc); - fattn_kernel_t fattn_kernel = flash_attn_tile; - launch_fattn - (ctx, dst, fattn_kernel, nwarps, nbytes_shared, nbatch_fa, true, true, false, warp_size); + launch_fattn_tile_case + (ctx, dst, nwarps, nbatch_fa, warp_size); return; } } @@ -1201,9 +1309,8 @@ static void launch_fattn_tile_switch_ncols1(ggml_backend_cuda_context & ctx, ggm constexpr int cols_per_block = 8; const int nwarps = ggml_cuda_fattn_tile_get_nthreads (DKQ, DV, cols_per_block, cc) / warp_size; const int nbatch_fa = ggml_cuda_fattn_tile_get_nbatch_fa(DKQ, DV, cols_per_block, cc); - fattn_kernel_t fattn_kernel = flash_attn_tile; - launch_fattn - (ctx, dst, fattn_kernel, nwarps, nbytes_shared, nbatch_fa, true, true, false, warp_size); + launch_fattn_tile_case + (ctx, dst, nwarps, nbatch_fa, warp_size); return; } } @@ -1213,9 +1320,8 @@ static void launch_fattn_tile_switch_ncols1(ggml_backend_cuda_context & ctx, ggm constexpr int cols_per_block = 4; const int nwarps = ggml_cuda_fattn_tile_get_nthreads (DKQ, DV, cols_per_block, cc) / warp_size; const int nbatch_fa = ggml_cuda_fattn_tile_get_nbatch_fa(DKQ, DV, cols_per_block, cc); - fattn_kernel_t fattn_kernel = flash_attn_tile; - launch_fattn - (ctx, dst, fattn_kernel, nwarps, nbytes_shared, nbatch_fa, true, true, false, warp_size); + launch_fattn_tile_case + (ctx, dst, nwarps, nbatch_fa, warp_size); return; } } @@ -1224,13 +1330,13 @@ static void launch_fattn_tile_switch_ncols1(ggml_backend_cuda_context & ctx, ggm constexpr int cols_per_block = 2; const int nwarps = ggml_cuda_fattn_tile_get_nthreads (DKQ, DV, cols_per_block, cc) / warp_size; const int nbatch_fa = ggml_cuda_fattn_tile_get_nbatch_fa(DKQ, DV, cols_per_block, cc); - fattn_kernel_t fattn_kernel = flash_attn_tile; - launch_fattn - (ctx, dst, fattn_kernel, nwarps, nbytes_shared, nbatch_fa, true, true, false, warp_size); + launch_fattn_tile_case + (ctx, dst, nwarps, nbatch_fa, warp_size); return; } - GGML_ABORT("fatal error"); + GGML_ABORT("fatal error"); + } } template @@ -1238,6 +1344,7 @@ static void launch_fattn_tile_switch_ncols2(ggml_backend_cuda_context & ctx, ggm const ggml_tensor * KQV = dst; const ggml_tensor * Q = dst->src[0]; const ggml_tensor * K = dst->src[1]; + const ggml_tensor * V = dst->src[2]; const ggml_tensor * mask = dst->src[3]; float max_bias = 0.0f; @@ -1296,6 +1403,15 @@ static void launch_fattn_tile_switch_ncols2(ggml_backend_cuda_context & ctx, ggm } if constexpr (DKQ <= 512 && DKQ != 320 && DKQ != 192) { + if constexpr (DKQ == 256 && DV == 256) { +#ifdef GGML_USE_HIP + const int cc = ggml_cuda_info().devices[ggml_cuda_get_device()].cc; + if (use_gqa_opt && Q->ne[1] == 1 && gqa_ratio == 6 && K->type == GGML_TYPE_Q8_0 && V->type == GGML_TYPE_Q8_0 && GGML_CUDA_CC_IS_RDNA3_5(cc)) { + launch_fattn_tile_switch_ncols1(ctx, dst); + return; + } +#endif // GGML_USE_HIP + } if (use_gqa_opt && gqa_ratio % 8 == 0) { launch_fattn_tile_switch_ncols1(ctx, dst); return; diff --git a/ggml/src/ggml-cuda/fattn.cu b/ggml/src/ggml-cuda/fattn.cu index ab7a3b297c07..ef6bf7adea4c 100644 --- a/ggml/src/ggml-cuda/fattn.cu +++ b/ggml/src/ggml-cuda/fattn.cu @@ -355,6 +355,19 @@ static bool ggml_cuda_fattn_kv_type_supported(ggml_type type) { } } +static bool ggml_cuda_fattn_tile_q8_0_KV_supported(const ggml_tensor * dst) { +#ifdef GGML_USE_HIP + const ggml_tensor * Q = dst->src[0]; + const ggml_tensor * K = dst->src[1]; + const ggml_tensor * V = dst->src[2]; + return Q->ne[1] == 1 && K->type == GGML_TYPE_Q8_0 && V->type == GGML_TYPE_Q8_0 && Q->ne[0] == K->ne[0] && K->ne[0] == V->ne[0] && + (Q->ne[0] == 64 || Q->ne[0] == 128 || Q->ne[0] == 256); +#else + GGML_UNUSED(dst); + return false; +#endif // GGML_USE_HIP +} + static best_fattn_kernel ggml_cuda_get_best_fattn_kernel(const int device, const ggml_tensor * dst) { #ifndef FLASH_ATTN_AVAILABLE GGML_UNUSED(device); GGML_UNUSED(dst); @@ -488,6 +501,10 @@ static best_fattn_kernel ggml_cuda_get_best_fattn_kernel(const int device, const gqa_ratio_eff *= 2; } + if (ggml_cuda_fattn_tile_q8_0_KV_supported(dst) && gqa_opt_applies && gqa_ratio_eff >= 2) { + return BEST_FATTN_KERNEL_TILE; + } + if (volta_mma_available(cc) && Q->ne[0] != 40 && Q->ne[0] != 72) { if (can_use_vector_kernel && Q->ne[1] * gqa_ratio_eff <= 2) { return BEST_FATTN_KERNEL_VEC; @@ -549,6 +566,12 @@ size_t ggml_cuda_flash_attn_ext_get_alloc_size(int device, const ggml_tensor * d switch (kernel) { case BEST_FATTN_KERNEL_TILE: + if (ggml_cuda_fattn_tile_q8_0_KV_supported(dst)) { + break; + } + need_f16_K = true; + need_f16_V = true; + break; case BEST_FATTN_KERNEL_MMA_F16: need_f16_K = true; need_f16_V = true; From 39765ff5c435ebf1cc29da198ecabbe02d361062 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:23:58 +0200 Subject: [PATCH 023/212] hip: tune Qwen Gated DeltaNet execution Increase gfx1151 column parallelism, tile KDA inputs in LDS, preserve recurrent graph ordering, and schedule compatible Qwen projections together. Assisted-by: OpenCode --- ggml/src/ggml-cuda/gated_delta_net.cu | 146 +++++++++++++++++++++++++- src/models/qwen35.cpp | 12 +++ src/models/qwen35moe.cpp | 14 ++- 3 files changed, 165 insertions(+), 7 deletions(-) diff --git a/ggml/src/ggml-cuda/gated_delta_net.cu b/ggml/src/ggml-cuda/gated_delta_net.cu index 1b431a724d72..8fab7a3e5dfb 100644 --- a/ggml/src/ggml-cuda/gated_delta_net.cu +++ b/ggml/src/ggml-cuda/gated_delta_net.cu @@ -1,8 +1,21 @@ #include "gated_delta_net.cuh" #include "ggml-cuda/common.cuh" +static constexpr int gated_delta_net_num_warps(int cc) { + return GGML_CUDA_CC_IS_RDNA3_5(cc) ? 32 : 4; +} + +static_assert(gated_delta_net_num_warps(GGML_CUDA_CC_RDNA3_5) == 32); +static_assert(gated_delta_net_num_warps(GGML_CUDA_CC_RDNA3) == 4); + +#if defined(RDNA3_5) +static constexpr int gdn_num_warps = gated_delta_net_num_warps(GGML_CUDA_CC_RDNA3_5); +#else +static constexpr int gdn_num_warps = gated_delta_net_num_warps(GGML_CUDA_CC_RDNA3); +#endif + template -__global__ void __launch_bounds__((ggml_cuda_get_physical_warp_size() < S_v ? ggml_cuda_get_physical_warp_size() : S_v) * 4, 2) +__global__ void __launch_bounds__((ggml_cuda_get_physical_warp_size() < S_v ? ggml_cuda_get_physical_warp_size() : S_v) * gdn_num_warps, 2) gated_delta_net_cuda(const float * q, const float * k, const float * v, @@ -166,6 +179,119 @@ gated_delta_net_cuda(const float * q, } } +__global__ void __launch_bounds__(512, 2) +gated_delta_net_kda_tiled_128_cuda(const float * q, + const float * k, + const float * v, + const float * g, + const float * beta, + const float * curr_state, + float * dst, + float * state, + int64_t n_tokens, + int64_t sq1, + int64_t sq2, + int64_t sq3, + int64_t sv1, + int64_t sv2, + int64_t sv3, + int64_t sb1, + int64_t sb2, + int64_t sb3, + const uint3 neqk1_magic, + const uint3 rq3_magic, + float scale) { + constexpr int S_v = 128; + constexpr int H = 32; + constexpr int token_tile = 16; + constexpr int warp_size = 32; + constexpr int rows_per_lane = S_v / warp_size; + + __shared__ float q_shared[token_tile][S_v]; + __shared__ float k_shared[token_tile][S_v]; + __shared__ float g_shared[token_tile][S_v]; + __shared__ float beta_shared[token_tile]; + + const int h_idx = blockIdx.x; + const int sequence = blockIdx.y; + const int lane = threadIdx.x; + const int col = blockIdx.z * blockDim.y + threadIdx.y; + const int thread = threadIdx.y * warp_size + lane; + const int nthreads = blockDim.y * warp_size; + + const uint32_t iq1 = fastmodulo(h_idx, neqk1_magic); + const uint32_t iq3 = fastdiv(sequence, rq3_magic); + + const int64_t state_offset = (sequence * H + h_idx) * S_v * S_v; + curr_state += state_offset + col * S_v; + state += state_offset; + dst += (sequence * n_tokens * H + h_idx) * S_v; + + float s_shard[rows_per_lane]; + ggml_cuda_pdl_sync(); +#pragma unroll + for (int r = 0; r < rows_per_lane; ++r) { + s_shard[r] = curr_state[r * warp_size + lane]; + } + + for (int t0 = 0; t0 < n_tokens; t0 += token_tile) { + const int tile_size = min((int64_t) token_tile, n_tokens - t0); + for (int idx = thread; idx < tile_size * S_v; idx += nthreads) { + const int tt = idx / S_v; + const int i = idx % S_v; + const int t = t0 + tt; + const int64_t gb_offset = sequence * sb3 + t * sb2 + h_idx * sb1; + q_shared[tt][i] = q[iq3 * sq3 + t * sq2 + iq1 * sq1 + i]; + k_shared[tt][i] = k[iq3 * sq3 + t * sq2 + iq1 * sq1 + i]; + g_shared[tt][i] = g[gb_offset * S_v + i]; + } + if (thread < tile_size) { + beta_shared[thread] = beta[sequence * sb3 + (t0 + thread) * sb2 + h_idx * sb1]; + } + __syncthreads(); + + for (int tt = 0; tt < tile_size; ++tt) { + const int t = t0 + tt; + const float * v_t = v + sequence * sv3 + t * sv2 + h_idx * sv1; + float k_reg[rows_per_lane]; + float q_reg[rows_per_lane]; +#pragma unroll + for (int r = 0; r < rows_per_lane; ++r) { + const int i = r * warp_size + lane; + k_reg[r] = k_shared[tt][i]; + q_reg[r] = q_shared[tt][i]; + } + + float kv_shard = 0.0f; +#pragma unroll + for (int r = 0; r < rows_per_lane; ++r) { + const int i = r * warp_size + lane; + kv_shard += expf(g_shared[tt][i]) * s_shard[r] * k_reg[r]; + } + const float kv_col = warp_reduce_sum(kv_shard); + const float delta_col = (v_t[col] - kv_col) * beta_shared[tt]; + + float attn_partial = 0.0f; +#pragma unroll + for (int r = 0; r < rows_per_lane; ++r) { + const int i = r * warp_size + lane; + s_shard[r] = expf(g_shared[tt][i]) * s_shard[r] + k_reg[r] * delta_col; + attn_partial += s_shard[r] * q_reg[r]; + } + const float attn_col = warp_reduce_sum(attn_partial); + if (lane == 0) { + dst[t * S_v * H + col] = attn_col * scale; + } + } + __syncthreads(); + } + +#pragma unroll + for (int r = 0; r < rows_per_lane; ++r) { + state[col * S_v + r * warp_size + lane] = s_shard[r]; + } +} + template static void launch_gated_delta_net( const float * q_d, const float * k_d, const float * v_d, @@ -178,14 +304,28 @@ static void launch_gated_delta_net( int64_t neqk1, int64_t rq3, float scale, int64_t state_slot_stride, int K, cudaStream_t stream) { //TODO: Add chunked kernel for even faster pre-fill - const int warp_size = ggml_cuda_info().devices[ggml_cuda_get_device()].warp_size; - const int num_warps = 4; + const int id = ggml_cuda_get_device(); + const int cc = ggml_cuda_info().devices[id].cc; + const int warp_size = ggml_cuda_info().devices[id].warp_size; + const int num_warps = GGML_CUDA_CC_IS_RDNA3_5(cc) && S_v == 128 && H == 64 && !KDA ? 32 : + (GGML_CUDA_CC_IS_RDNA3_5(cc) ? 16 : gated_delta_net_num_warps(cc)); dim3 grid_dims(H, n_seqs, (S_v + num_warps - 1) / num_warps); dim3 block_dims(warp_size <= S_v ? warp_size : S_v, num_warps, 1); const uint3 neqk1_magic = init_fastdiv_values(neqk1); const uint3 rq3_magic = init_fastdiv_values(rq3); + if constexpr (KDA && !keep_rs_t) { + if (GGML_CUDA_CC_IS_RDNA3_5(cc) && S_v == 128 && H == 32 && n_tokens >= 16 && n_seqs == 1) { + const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(grid_dims, block_dims, 0, stream); + ggml_cuda_kernel_launch(gated_delta_net_kda_tiled_128_cuda, launch_params, + q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d, n_tokens, + sq1, sq2, sq3, sv1, sv2, sv3, sb1, sb2, sb3, + neqk1_magic, rq3_magic, scale); + return; + } + } + const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(grid_dims, block_dims, 0, stream); switch (S_v) { case 16: diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp index 309dd432447c..696c85811a50 100644 --- a/src/models/qwen35.cpp +++ b/src/models/qwen35.cpp @@ -221,6 +221,9 @@ llama_model_qwen35::graph::graph(const llama_model & model, const llm_graph_para // LM head cur = build_lora_mm(model.output, cur, model.output_s); + if (cparams.embeddings_nextn && n_tokens <= 16 && cur->op == GGML_OP_MUL_MAT) { + ggml_mul_mat_set_hint(cur, GGML_HINT_EXACT_BATCH); + } cb(cur, "result_output", -1); res->t_logits = cur; @@ -268,6 +271,9 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn( // Qwen3Next uses a single Q projection that outputs query + gate ggml_tensor * Qcur_full = build_lora_mm(model.layers[il].wq, cur, model.layers[il].wq_s); // [ (n_embd_head * 2) * n_head, n_tokens ] + if (cparams.embeddings_nextn && n_tokens <= 16 && Qcur_full->op == GGML_OP_MUL_MAT) { + ggml_mul_mat_set_hint(Qcur_full, GGML_HINT_EXACT_BATCH); + } cb(Qcur_full, "Qcur_full", il); ggml_tensor * Qcur = ggml_view_3d(ctx0, Qcur_full, n_embd_head, n_head, n_tokens, @@ -280,9 +286,15 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn( cb(Qcur, "Qcur_normed", il); ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur, model.layers[il].wk_s); + if (cparams.embeddings_nextn && n_tokens <= 16 && Kcur->op == GGML_OP_MUL_MAT) { + ggml_mul_mat_set_hint(Kcur, GGML_HINT_EXACT_BATCH); + } cb(Kcur, "Kcur", il); ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur, model.layers[il].wv_s); + if (cparams.embeddings_nextn && n_tokens <= 16 && Vcur->op == GGML_OP_MUL_MAT) { + ggml_mul_mat_set_hint(Vcur, GGML_HINT_EXACT_BATCH); + } cb(Vcur, "Vcur", il); // Apply K normalization diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp index 38f2a57985a9..d091e857e00a 100644 --- a/src/models/qwen35moe.cpp +++ b/src/models/qwen35moe.cpp @@ -187,13 +187,15 @@ llama_model_qwen35moe::graph::graph(const llama_model & model, const llm_graph_p cur = build_norm(inpL, model.layers[il].attn_norm, nullptr, LLM_NORM_RMS, il); cb(cur, "attn_norm", il); - ggml_build_forward_expand(gf, cur); - // Determine layer type and build appropriate attention mechanism if (hparams.is_recr(il)) { // Linear attention layer (gated delta net) cur = build_layer_attn_linear(inp->get_recr(), cur, il); + + ggml_build_forward_expand(gf, cur); } else { + ggml_build_forward_expand(gf, cur); + // Full attention layer cur = build_layer_attn(inp->get_attn(), cur, inp_pos, sections, il); } @@ -383,14 +385,18 @@ ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn_linear( ggml_tensor * qkv_mixed = qkvz.first; ggml_tensor * z = qkvz.second; - ggml_tensor * beta = build_lora_mm(model.layers[il].ssm_beta, cur, model.layers[il].ssm_beta_s); + ggml_tensor * beta = build_lora_mm(model.layers[il].ssm_beta, cur, model.layers[il].ssm_beta_s); + ggml_tensor * alpha = build_lora_mm(model.layers[il].ssm_alpha, cur, model.layers[il].ssm_alpha_s); + + ggml_build_forward_expand(gf, beta); + ggml_build_forward_expand(gf, alpha); + beta = ggml_reshape_4d(ctx0, beta, 1, num_v_heads, n_seq_tokens, n_seqs); cb(beta, "beta", il); beta = ggml_sigmoid(ctx0, beta); cb(beta, "beta_sigmoid", il); - ggml_tensor * alpha = build_lora_mm(model.layers[il].ssm_alpha, cur, model.layers[il].ssm_alpha_s); alpha = ggml_reshape_3d(ctx0, alpha, num_v_heads, n_seq_tokens, n_seqs); cb(alpha, "alpha", il); From f169e0b7d2ddb48f7a433ef0deeb7d2de6b9471e Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:24:08 +0200 Subject: [PATCH 024/212] server: keep speculative checkpoints on device Use full MTP rollback checkpoints and retain speculative sequence state on the device to avoid unnecessary host transfers. Assisted-by: OpenCode --- common/common.h | 2 +- tools/server/server-context.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common/common.h b/common/common.h index 0162a5f1e638..ebb60949b740 100644 --- a/common/common.h +++ b/common/common.h @@ -415,7 +415,7 @@ struct common_params_speculative { 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; + return t == COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3 || t == COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH || t == COMMON_SPECULATIVE_TYPE_DRAFT_DSPARK; }); return needs_rs_seq ? draft.n_max : 0u; diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 90501bcb7f73..fce5f9b5f4b1 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -3021,7 +3021,7 @@ struct server_context_impl { llama_memory_seq_pos_max(llama_get_memory(ctx_tgt), slot.id)); if (use_ckpt_dft) { - slot.spec_ckpt.update_dft(ctx_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); + slot.spec_ckpt.update_dft(ctx_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE); } slot.spec_prompt = slot.prompt.tokens.get_text_tokens(); @@ -3060,7 +3060,7 @@ struct server_context_impl { if (ctx_dft) { if (use_ckpt_dft) { - ckpt.load_dft(ctx_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); + ckpt.load_dft(ctx_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE); } if (!llama_memory_seq_rm(llama_get_memory(ctx_dft), slot.id, ckpt.pos_max + 1, -1)) { @@ -3079,7 +3079,7 @@ struct server_context_impl { if (use_ckpt_tgt) { //const int64_t t_start = ggml_time_us(); - ckpt.update_tgt(ctx_tgt, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); + ckpt.update_tgt(ctx_tgt, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE); //const int64_t t_total = ggml_time_us() - t_start; //printf("checkpoint total: %f ms\n", t_total / 1000.0); @@ -3091,7 +3091,7 @@ struct server_context_impl { } if (use_ckpt_dft) { - ckpt.update_dft(ctx_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); + ckpt.update_dft(ctx_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE); } } }); @@ -3943,10 +3943,10 @@ struct server_context_impl { SLT_DBG(slot, "restoring speculative checkpoint (pos_min = %d, pos_max = %d, size = %zu)\n", ckpt.pos_min, ckpt.pos_max, ckpt.size()); - ckpt.load_tgt(slot.ctx_tgt, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); + ckpt.load_tgt(slot.ctx_tgt, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE); if (slot.ctx_dft) { - ckpt.load_dft(slot.ctx_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); + ckpt.load_dft(slot.ctx_dft, slot.id, LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE); } slot.mem.seq_rm(slot.id, ckpt.pos_max + 1, -1); From 1ac05934afa25096ece8c7cdd8f6a3210084eb4c Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:24:20 +0200 Subject: [PATCH 025/212] backend: allow padded concat allocations Permit backends to allocate quantized CONCAT outputs with row padding under the expanded allocation-size invariant. Assisted-by: OpenCode --- ggml/src/ggml-backend.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ggml/src/ggml-backend.cpp b/ggml/src/ggml-backend.cpp index fec7d7c92bf3..407dd5c5db98 100644 --- a/ggml/src/ggml-backend.cpp +++ b/ggml/src/ggml-backend.cpp @@ -2115,6 +2115,7 @@ bool ggml_backend_op_alloc_size_may_expand(enum ggml_op op) { switch (op) { case GGML_OP_FLASH_ATTN_EXT: case GGML_OP_MUL_MAT_ID: + case GGML_OP_CONCAT: case GGML_OP_CUMSUM: case GGML_OP_ARGSORT: case GGML_OP_TOP_K: From c52f22bb6258b7a2d55b49da1329a2ff25fee341 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:24:32 +0200 Subject: [PATCH 026/212] tests: cover RDNA3.5 inference optimizations Add exact-batch, quantized matmul, routed expert, Flash Attention, concat, normalization, and Gated DeltaNet regression coverage for the optimized gfx1151 paths. Assisted-by: OpenCode --- tests/test-backend-ops.cpp | 339 ++++++++++++++++++++++++++++++++++++- 1 file changed, 330 insertions(+), 9 deletions(-) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 996b88db296e..7e6c8c383101 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -3608,35 +3608,38 @@ struct test_rms_norm_back : public test_case { } }; -// GGML_OP_RMS_NORM + GGML_OP_MUL + GGML_OP_ADD +// GGML_OP_RMS_NORM + GGML_OP_MUL [+ GGML_OP_ADD] struct test_rms_norm_mul_add : public test_case { const ggml_type type; const std::array ne; const float eps; const bool broadcast; const bool multi_add; // test a sequence of adds feeding into rms_norm + const bool add; + const bool weights; std::string op_desc(ggml_tensor * t) override { GGML_UNUSED(t); - return "RMS_NORM_MUL_ADD"; + return add ? "RMS_NORM_MUL_ADD" : "RMS_NORM_MUL"; } bool run_whole_graph() override { return true; } std::string vars() override { - return VARS_TO_STR5(type, ne, eps, broadcast, multi_add); + return VARS_TO_STR7(type, ne, eps, broadcast, multi_add, add, weights); } test_rms_norm_mul_add(ggml_type type = GGML_TYPE_F32, std::array ne = {64, 5, 4, 3}, - float eps = 1e-6f, bool broadcast = false, bool multi_add = false) - : type(type), ne(ne), eps(eps), broadcast(broadcast), multi_add(multi_add) {} + float eps = 1e-6f, bool broadcast = false, bool multi_add = false, bool add = true, bool weights = false) + : type(type), ne(ne), eps(eps), broadcast(broadcast), multi_add(multi_add), add(add), weights(weights) {} ggml_tensor * build_graph(ggml_context * ctx) override { std::array broadcast_dims = {ne[0]*2, ne[1]*3, ne[2]*3, ne[3]*4}; + std::array weight_dims = {ne[0], 1, 1, 1}; ggml_tensor * a = ggml_new_tensor(ctx, type, 4, broadcast ? broadcast_dims.data() : ne.data()); - ggml_tensor * b = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_tensor * b = ggml_new_tensor(ctx, type, 4, weights ? weight_dims.data() : ne.data()); ggml_tensor * c = ggml_new_tensor(ctx, type, 4, ne.data()); ggml_set_param(a); @@ -3651,7 +3654,10 @@ struct test_rms_norm_mul_add : public test_case { if (multi_add) { a = ggml_add(ctx, ggml_add(ctx, a, b), c); } - ggml_tensor * out = ggml_add(ctx, ggml_mul(ctx, ggml_rms_norm(ctx, a, eps), b), c); + ggml_tensor * out = ggml_mul(ctx, ggml_rms_norm(ctx, a, eps), b); + if (add) { + out = ggml_add(ctx, out, c); + } ggml_set_name(out, "out"); return out; @@ -3765,6 +3771,72 @@ struct test_relu_sqr : public test_case { } }; +struct test_weighted_expert_sum : public test_case { + const int64_t n_embd; + const int64_t n_expert_used; + const int64_t n_tokens; + + std::string op_desc(ggml_tensor * t) override { + GGML_UNUSED(t); + return "WEIGHTED_EXPERT_SUM"; + } + + bool run_whole_graph() override { return true; } + + std::string vars() override { + return VARS_TO_STR3(n_embd, n_expert_used, n_tokens); + } + + test_weighted_expert_sum(int64_t n_embd, int64_t n_expert_used, int64_t n_tokens) + : n_embd(n_embd), n_expert_used(n_expert_used), n_tokens(n_tokens) {} + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * experts = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, n_embd, n_expert_used, n_tokens); + ggml_tensor * weights = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 1, n_expert_used, n_tokens); + ggml_tensor * weighted = ggml_mul(ctx, experts, weights); + ggml_build_forward_expand(gf, weighted); + + std::vector views; + views.reserve(n_expert_used); + for (int64_t i = 0; i < n_expert_used; ++i) { + ggml_tensor * view = ggml_view_2d(ctx, weighted, n_embd, n_tokens, weighted->nb[2], i * weighted->nb[1]); + ggml_build_forward_expand(gf, view); + views.push_back(view); + } + + ggml_tensor * out = views[0]; + for (int64_t i = 1; i < n_expert_used; ++i) { + out = ggml_add(ctx, out, views[i]); + ggml_build_forward_expand(gf, out); + } + return out; + } +}; + +struct test_shared_mul_add : public test_case { + const int64_t n_embd; + const int64_t n_tokens; + + test_shared_mul_add(int64_t n_embd, int64_t n_tokens) : n_embd(n_embd), n_tokens(n_tokens) {} + + std::string op_desc(ggml_tensor * t) override { + GGML_UNUSED(t); + return "SHARED_MUL_ADD"; + } + + std::string vars() override { return VARS_TO_STR2(n_embd, n_tokens); } + bool run_whole_graph() override { return true; } + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * src = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, n_embd, n_tokens); + ggml_tensor * gate = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 1, n_tokens); + ggml_tensor * other = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, n_embd, n_tokens); + ggml_tensor * residual = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, n_embd, n_tokens); + ggml_tensor * mul = ggml_mul(ctx, src, gate); + return ggml_add(ctx, ggml_add(ctx, other, mul), residual); + } +}; + // GGML_OP_UNARY(SILU|SIGMOID|SOFTPLUS) + GGML_OP_MUL (fused operation). // `layout` and `tail` are used for fallback cases where fusion must be skipped struct test_unary_mul : public test_case { @@ -4660,6 +4732,72 @@ struct test_mul_mat : public test_case { } }; +struct test_mul_mat_exact_batch : public test_case { + const ggml_type type_a; + const int64_t m; + const int64_t n; + const int64_t k; + + test_mul_mat_exact_batch(ggml_type type_a, int64_t m, int64_t n, int64_t k) : type_a(type_a), m(m), n(n), k(k) {} + + std::string vars() override { + return VARS_TO_STR4(type_a, m, n, k); + } + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * a = ggml_new_tensor_2d(ctx, type_a, k, m); + ggml_tensor * b = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, k, n); + ggml_tensor * batch = ggml_mul_mat(ctx, a, b); + ggml_mul_mat_set_hint(batch, GGML_HINT_EXACT_BATCH); + + ggml_tensor * diff = nullptr; + for (int64_t col = 0; col < n; ++col) { + ggml_tensor * b_col = ggml_view_2d(ctx, b, k, 1, b->nb[1], col*b->nb[1]); + ggml_tensor * single = ggml_mul_mat(ctx, a, b_col); + ggml_tensor * batch_col = ggml_view_2d(ctx, batch, m, 1, batch->nb[1], col*batch->nb[1]); + ggml_tensor * col_diff = ggml_sub(ctx, batch_col, single); + diff = diff == nullptr ? col_diff : ggml_concat(ctx, diff, col_diff, 1); + } + return diff; + } + + std::string op_desc(ggml_tensor * t) override { + GGML_UNUSED(t); + return ggml_op_name(GGML_OP_MUL_MAT); + } + + double err(const float * a, const float * b, size_t n) override { + GGML_UNUSED(b); + double max_abs = 0.0; + for (size_t i = 0; i < n; ++i) { + max_abs = std::max(max_abs, std::abs((double) a[i])); + } + return max_abs; + } + + double max_err() override { return 0.0; } + + bool run_whole_graph() override { return true; } +}; + +struct test_mul_mat_pair : public test_case { + std::string op_desc(ggml_tensor * t) override { + GGML_UNUSED(t); + return "MUL_MAT_PAIR"; + } + + std::string vars() override { return {}; } + bool run_whole_graph() override { return true; } + double max_nmse_err() override { return 5e-4; } + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * a0 = ggml_new_tensor_2d(ctx, GGML_TYPE_Q8_0, 256, 32); + ggml_tensor * a1 = ggml_new_tensor_2d(ctx, GGML_TYPE_Q8_0, 256, 32); + ggml_tensor * b = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 256, 129); + return ggml_add(ctx, ggml_mul_mat(ctx, a0, b), ggml_mul_mat(ctx, a1, b)); + } +}; + // GGML_HINT_SRC0_IS_HADAMARD struct test_mul_mat_hadamard : public test_mul_mat { test_mul_mat_hadamard(ggml_type type_a = GGML_TYPE_F32, ggml_type type_b = GGML_TYPE_F32, @@ -4800,6 +4938,51 @@ struct test_mul_mat_id : public test_case { } }; +struct test_swiglu_q8_mmq : public test_case { + const bool use_id; + const int n_mats; + const int n_used; + const int64_t m; + const int64_t n; + const int64_t k; + + test_swiglu_q8_mmq(bool use_id, int n_mats, int n_used, int64_t m, int64_t n, int64_t k) + : use_id(use_id), n_mats(n_mats), n_used(n_used), m(m), n(n), k(k) {} + + std::string op_desc(ggml_tensor * t) override { + GGML_UNUSED(t); + return "SWIGLU_Q8_MMQ"; + } + + std::string vars() override { + return VARS_TO_STR6(use_id, n_mats, n_used, m, n, k); + } + + bool run_whole_graph() override { return true; } + double max_nmse_err() override { return 5e-4; } + + ggml_tensor * build_graph(ggml_context * ctx) override { + if (!use_id) { + ggml_tensor * gate = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, k, n); + ggml_tensor * up = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, k, n); + ggml_tensor * down = ggml_new_tensor_2d(ctx, GGML_TYPE_Q8_0, k, m); + return ggml_mul_mat(ctx, down, ggml_swiglu_split(ctx, gate, up)); + } + + ggml_tensor * gate = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, k, n_used, n); + ggml_tensor * up = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, k, n_used, n); + ggml_tensor * down = ggml_new_tensor_3d(ctx, GGML_TYPE_Q8_0, k, m, n_mats); + ggml_tensor * ids_all = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, n_mats, n); + ggml_set_name(ids_all, "ids"); + ggml_tensor * ids = ggml_view_2d(ctx, ids_all, n_used, n, ids_all->nb[1], 0); + return ggml_mul_mat_id(ctx, down, ggml_swiglu_split(ctx, gate, up), ids); + } + + void initialize_tensors(ggml_context * ctx) override { + init_mul_mat_id_tensors(ctx, n_mats); + } +}; + // GGML_OP_MUL_MAT_ID + GGML_OP_ADD or GGML_OP_MUL struct test_mul_mat_id_fusion : public test_case { const ggml_type type_a; @@ -6051,7 +6234,7 @@ struct test_concat : public test_case { const std::array ne_a; const int64_t ne_b_d; const int dim; - const int v; // view (1 << 0: non-cont a (first 3 dim), 1 << 1: non-cont b (first 3 dim), 1 << 2: non-cont a (last 2 dim), 1 << 3: non-cont b (last 2 dim)) + const int v; // view (bits 0-3: non-contiguous views, bit 4: transposed b) std::string vars() override { return VARS_TO_STR5(type, ne_a, ne_b_d, dim, v); @@ -6086,7 +6269,15 @@ struct test_concat : public test_case { ggml_set_name(a, "a"); } ggml_tensor * b; - if (v & 2) { + if (v & 16) { + auto ne = ne_b; + std::swap(ne[0], ne[1]); + b = ggml_new_tensor(ctx, type, 4, ne.data()); + ggml_set_name(b, "b"); + + b = ggml_transpose(ctx, b); + ggml_set_name(b, "transposed_b"); + } else if (v & 2) { auto ne = ne_b; ne[0] *= 3; ne[1] *= 2; ne[2] *= 4; b = ggml_new_tensor(ctx, type, 4, ne.data()); ggml_set_name(b, "b"); @@ -6867,6 +7058,53 @@ struct test_l2_norm : public test_case { } }; +struct test_l2_norm_dual : public test_case { + const int64_t n_heads; + const int64_t n_tokens; + const int64_t n_seqs; + const float eps_q; + const float eps_k; + + ggml_tensor * q_norm = nullptr; + ggml_tensor * k_norm = nullptr; + + test_l2_norm_dual(int64_t n_heads, int64_t n_tokens, int64_t n_seqs, float eps_q, float eps_k) + : n_heads(n_heads), n_tokens(n_tokens), n_seqs(n_seqs), eps_q(eps_q), eps_k(eps_k) {} + + std::string op_desc(ggml_tensor * t) override { + GGML_UNUSED(t); + return "L2_NORM_DUAL_S128"; + } + + std::string vars() override { + return VARS_TO_STR5(n_heads, n_tokens, n_seqs, eps_q, eps_k); + } + + bool run_whole_graph() override { return true; } + + std::vector fusion_test_nodes() override { return { q_norm, k_norm }; } + + ggml_tensor * build_graph(ggml_context * ctx) override { + constexpr int64_t head_size = 128; + ggml_tensor * qkv = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 3*head_size*n_heads, n_tokens, n_seqs); + ggml_set_name(qkv, "qkv"); + + ggml_tensor * q = ggml_view_4d(ctx, qkv, head_size, n_heads, n_tokens, n_seqs, + head_size*sizeof(float), qkv->nb[1], qkv->nb[2], 0); + ggml_tensor * k = ggml_view_4d(ctx, qkv, head_size, n_heads, n_tokens, n_seqs, + head_size*sizeof(float), qkv->nb[1], qkv->nb[2], head_size*n_heads*sizeof(float)); + ggml_set_name(q, "q"); + ggml_set_name(k, "k"); + + q_norm = ggml_l2_norm(ctx, q, eps_q); + k_norm = ggml_l2_norm(ctx, k, eps_k); + ggml_set_name(q_norm, "q_norm"); + ggml_set_name(k_norm, "k_norm"); + + return ggml_add(ctx, q_norm, k_norm); + } +}; + // GGML_OP_ACC struct test_acc : public test_case { const ggml_type type; @@ -9107,6 +9345,9 @@ static std::vector> make_test_cases_eval() { } } + test_cases.emplace_back(new test_l2_norm_dual(16, 1, 1, 1e-6f, 1e-6f)); + test_cases.emplace_back(new test_l2_norm_dual(4, 3, 2, 1e-6f, 8.0f)); + // in-place tests test_cases.emplace_back(new test_rms_norm(GGML_TYPE_F32, {64, 5, 4, 3}, false, 1e-6f, true)); @@ -9127,6 +9368,18 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_add_rms_norm(GGML_TYPE_F32, {n, 1, 1, 1}, 1e-6f, false)); } + test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, {127, 48, 1, 1}, 1e-6f, false, false, false, true)); + test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, {128, 48, 1024, 1}, 1e-6f, false, false, false, true)); + test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, {129, 48, 1, 1}, 1e-6f, false, false, false, true)); + + test_cases.emplace_back(new test_weighted_expert_sum(127, 2, 3)); + test_cases.emplace_back(new test_weighted_expert_sum(128, 4, 5)); + test_cases.emplace_back(new test_weighted_expert_sum(2048, 8, 32)); + test_cases.emplace_back(new test_shared_mul_add(127, 3)); + test_cases.emplace_back(new test_shared_mul_add(2048, 32)); + test_cases.emplace_back(new test_swiglu_q8_mmq(false, 1, 1, 2048, 32, 512)); + test_cases.emplace_back(new test_swiglu_q8_mmq(true, 16, 8, 2048, 32, 512)); + for (auto multi_add : {false, true}) { for (auto set_rows : {false, true}) { for (auto broadcast : {false, true}) { @@ -9231,6 +9484,32 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_mul_mat(GGML_TYPE_Q8_0, GGML_TYPE_F32, 8192, 512, 5120, {128, 1}, {1, 1})); #endif + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q4_K, GGML_TYPE_F32, 256, 8, false, 512, 2016, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q4_K, GGML_TYPE_F32, 256, 8, false, 512, 2048, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q4_K, GGML_TYPE_F32, 256, 8, false, 512, 2080, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q4_K, GGML_TYPE_F32, 256, 8, false, 2048, 2048, 512)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 256, 8, false, 512, 2016, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 256, 8, false, 512, 2048, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 256, 8, false, 512, 2080, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 256, 8, false, 2048, 2048, 512)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q4_K, GGML_TYPE_F32, 256, 8, false, 1024, 2048, 3072)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 256, 8, false, 3072, 2048, 1024)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q6_K, GGML_TYPE_F32, 256, 8, false, 512, 2016, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q6_K, GGML_TYPE_F32, 256, 8, false, 512, 2048, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q6_K, GGML_TYPE_F32, 256, 8, false, 512, 2080, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q6_K, GGML_TYPE_F32, 256, 8, false, 2048, 2048, 512)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 256, 8, false, 512, 2016, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 256, 8, false, 512, 2048, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 256, 8, false, 512, 2080, 2048)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 256, 8, false, 2048, 2048, 512)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 512, 8, false, 768, 1984, 2560)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 512, 8, false, 768, 2048, 2560)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 512, 8, false, 768, 2112, 2560)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_K, GGML_TYPE_F32, 512, 8, false, 2560, 2048, 768)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q6_K, GGML_TYPE_F32, 512, 8, false, 2560, 1984, 768)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q6_K, GGML_TYPE_F32, 512, 8, false, 2560, 2048, 768)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q6_K, GGML_TYPE_F32, 512, 8, false, 2560, 2112, 768)); + for (ggml_type type_a : all_types) { for (int i = 1; i < 10; ++i) { test_cases.emplace_back(new test_mul_mat(type_a, GGML_TYPE_F32, 16, i, 1*256, { 1, 1}, {1, 1})); @@ -9247,6 +9526,17 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_mul_mat(GGML_TYPE_Q8_0, GGML_TYPE_F32, 2880, 32, 2880, {1, 1}, {1, 1})); test_cases.emplace_back(new test_mul_mat(GGML_TYPE_MXFP4, GGML_TYPE_F32, 2880, 32, 2880, {1, 1}, {1, 1})); + for (int64_t n : {127, 128, 129}) { + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_Q8_0, GGML_TYPE_F32, 128, n, 5120, {1, 1}, {1, 1})); + } + for (int64_t n : {2, 3, 4, 5, 8, 9, 16}) { + test_cases.emplace_back(new test_mul_mat_exact_batch(GGML_TYPE_BF16, 128, n, 5120)); + } + for (ggml_type type_a : {GGML_TYPE_Q1_0, GGML_TYPE_Q2_0, GGML_TYPE_Q5_1, GGML_TYPE_Q8_0, + GGML_TYPE_Q4_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, GGML_TYPE_IQ3_S}) { + test_cases.emplace_back(new test_mul_mat_exact_batch(type_a, 128, 4, 5120)); + } + // m == 1, with n on both sides of MMVF_MAX_BATCH_SIZE (8): mmvf below, operand swap above for (int64_t n : {1, 7, 8, 9, 16, 128, 512}) { test_cases.emplace_back(new test_mul_mat(GGML_TYPE_F32, GGML_TYPE_F32, 1, n, 2048, {1, 1}, {1, 1})); @@ -9377,6 +9667,10 @@ static std::vector> make_test_cases_eval() { } test_cases.emplace_back(new test_mul_mat(GGML_TYPE_Q8_0, GGML_TYPE_F32, 6, 4096, 5120, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_Q8_0, GGML_TYPE_F32, 1024, 32, 1056, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_Q4_0, GGML_TYPE_F32, 1024, 32, 1056, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_Q2_K, GGML_TYPE_F32, 1024, 32, 1280, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 8, 2, true, 128, 32, 1056)); // K not a multiple of 32 test_cases.emplace_back(new test_mul_mat(GGML_TYPE_F16, GGML_TYPE_F16, 64, 32, 65, {1, 1}, {1, 1})); @@ -9441,6 +9735,8 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_F16, GGML_TYPE_F32, 1, 1, false, 8, 16, k)); } test_cases.emplace_back(new test_mul_mat_id_fusion(GGML_TYPE_F16, GGML_TYPE_F32, 16, 16, false, 32, 32, 32, 3)); + test_cases.emplace_back(new test_mul_mat_id_fusion(GGML_TYPE_Q8_0, GGML_TYPE_F32, 8, 4, false, 512, 129, 256, 2)); + test_cases.emplace_back(new test_mul_mat_pair()); // gpt-oss issue with Vulkan mmq_id test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_MXFP4, GGML_TYPE_F32, 32, 2, false, 2880, 32, 2880)); @@ -9758,6 +10054,11 @@ static std::vector> make_test_cases_eval() { } } + // Transposed src1 path: guard boundary, Qwen validation prompt, and prefill tile edges. + for (int64_t n_tokens : { 31, 32, 115, 512 }) { + test_cases.emplace_back(new test_concat(GGML_TYPE_F32, {64, 127, 2, 1}, n_tokens, 0, 16)); + } + for (ggml_sort_order order : {GGML_SORT_ORDER_ASC, GGML_SORT_ORDER_DESC}) { for (uint32_t i = 4; i <= 1024*1024; i *= 2) { test_cases.emplace_back(new test_argsort(GGML_TYPE_F32, {i-1, 1, 1, 1})); @@ -9970,6 +10271,9 @@ static std::vector> make_test_cases_eval() { } } } + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 8, {4, 1}, 1024, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0)); + test_cases.emplace_back(new test_flash_attn_ext(256, 256, 8, {4, 1}, 1024, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0)); + test_cases.emplace_back(new test_flash_attn_ext(256, 256, 8, {6, 1}, 1024, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0)); for (int hsk : { 40, 64, 72, 80, 96, 128, 192, 256, 320, 512, 576 }) { for (int hsv : { 40, 64, 72, 80, 96, 128, 192, 256, 512 }) { @@ -10177,6 +10481,12 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 4, 64, 64, 1, 1, false, true)); test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 4, 64, 33, 1, 1, false, true)); test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 4, 64, 100, 1, 1, false, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 15, 1, 1, false, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 16, 1, 1, false, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 17, 1, 1, false, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 2048, 1, 1, false, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 64, 128, 16, 1)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 64, 128, 17, 1)); // K > 1: output keeps the last min(n_tokens, K) per-token snapshots, ordered most-recent-first // (slot 0 = final state, slot s = state s tokens back). @@ -10470,6 +10780,11 @@ static std::vector> make_test_cases_perf() { test_cases.emplace_back(new test_flash_attn_ext(256, 256, 2, {16, 1}, 10000, 512, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); test_cases.emplace_back(new test_flash_attn_ext(256, 256, 2, {16, 1}, 20000, 512, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + for (int64_t kv : {4096, 8192, 12000, 16000, 24000, 32000, 64000}) { + test_cases.emplace_back(new test_flash_attn_ext(256, 256, 4, {8, 1}, kv, 2048, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + test_cases.emplace_back(new test_flash_attn_ext(256, 256, 4, {8, 1}, kv, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + } + for (int kv : { 4096, 8192, 16384, }) { for (int hs : { 64, 128, }) { for (int nr : { 1, 4, }) { @@ -10592,12 +10907,18 @@ static std::vector> make_test_cases_perf() { test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 256, 1)); // PP-256 test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 512, 1)); // PP-512 test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 1024, 1)); // PP-1024 + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 64, 128, 1024, 1)); // H64/S128 PP-1024 + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 64, 128, 2048, 1)); // H64/S128 PP-2048 // Small model configs (fewer heads = less GPU occupancy for autoregressive) test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 4, 128, 64, 1)); // 4h PP-64 test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 4, 128, 256, 1)); // 4h PP-256 test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 4, 128, 512, 1)); // 4h PP-512 test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 4, 128, 1024, 1)); // 4h PP-1024 test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 64, 1, 1, false, true)); // KDA PP-64 + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 15, 1, 1, false, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 16, 1, 1, false, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 17, 1, 1, false, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 2048, 1, 1, false, true)); // lightning_indexer for (int kv : { 256, 4096, 65536 }) { From 551ce30fe5c371258333d5681be76b8f3f440d65 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:20:42 +0200 Subject: [PATCH 027/212] hip: preserve exact RDNA3.5 MMVQ reductions Keep batch-one MMVQ on one wave for all quant types. Wider wave counts changed FP32 reduction order, causing non-identical logits for Q2-Q5 and deterministic decode divergence in the four-corpus correctness matrix. Assisted-by: OpenCode --- ggml/src/ggml-cuda/mmvq.cu | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/ggml/src/ggml-cuda/mmvq.cu b/ggml/src/ggml-cuda/mmvq.cu index b93b9b6c0520..ec1855f22526 100644 --- a/ggml/src/ggml-cuda/mmvq.cu +++ b/ggml/src/ggml-cuda/mmvq.cu @@ -402,20 +402,6 @@ static constexpr __device__ int get_mmvq_mmid_max_batch_for_device() { static constexpr __host__ __device__ int calc_nwarps(ggml_type type, int ncols_dst, mmvq_parameter_table_id table_id, bool small_k = false, bool halve_iters = false) { if (table_id == MMVQ_PARAMETERS_RDNA3_5) { - // Measured on gfx1151 with 4096x14336 MMVQ on 2026-09-01. Wider blocks regress the other types. - if (ncols_dst == 1) { - switch (type) { - case GGML_TYPE_Q5_K: - case GGML_TYPE_Q6_K: - return 4; - case GGML_TYPE_Q5_1: - case GGML_TYPE_MXFP4: - case GGML_TYPE_Q4_K: - return 2; - default: - break; - } - } return 1; } if (table_id == MMVQ_PARAMETERS_GENERIC) { @@ -544,12 +530,12 @@ static constexpr __host__ __device__ int calc_nwarps(ggml_type type, int ncols_d return 1; } -static_assert(calc_nwarps(GGML_TYPE_Q5_1, 1, MMVQ_PARAMETERS_RDNA3_5) == 2); +static_assert(calc_nwarps(GGML_TYPE_Q5_1, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); static_assert(calc_nwarps(GGML_TYPE_Q8_0, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); -static_assert(calc_nwarps(GGML_TYPE_MXFP4, 1, MMVQ_PARAMETERS_RDNA3_5) == 2); -static_assert(calc_nwarps(GGML_TYPE_Q4_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 2); -static_assert(calc_nwarps(GGML_TYPE_Q5_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 4); -static_assert(calc_nwarps(GGML_TYPE_Q6_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 4); +static_assert(calc_nwarps(GGML_TYPE_MXFP4, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); +static_assert(calc_nwarps(GGML_TYPE_Q4_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); +static_assert(calc_nwarps(GGML_TYPE_Q5_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); +static_assert(calc_nwarps(GGML_TYPE_Q6_K, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); static_assert(calc_nwarps(GGML_TYPE_Q1_0, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); static_assert(calc_nwarps(GGML_TYPE_Q2_0, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); static_assert(calc_nwarps(GGML_TYPE_Q4_0, 1, MMVQ_PARAMETERS_RDNA3_5) == 1); From db1ae40f51e276502a7989794928f092a79f79bc Mon Sep 17 00:00:00 2001 From: Marshall Date: Wed, 2 Sep 2026 16:04:11 +0100 Subject: [PATCH 028/212] speculative-prefill : port upstream PR 27692 A small draft model prefills the prompt and decodes a few lookahead tokens. The softmax attention of those tokens over the prompt is captured with a ggml eval callback, smoothed, max-reduced over heads and layers and pooled into chunks. Only the top scoring chunks go into the target model's KV cache. This is lossy. Adds llama_set_eval_callback, common/speculative-prefill, the --spec-prefill-* options, the standalone example and the llama-server hookup. Not ported from that PR: the llama-bench changes and the python eval scripts. Measured on Strix Halo / Vulkan with a Qwen3.5-2B estimator, TTFT at p = 0.15: Qwen3.8-27B 5604 -> 1587 ms, Flash-Next FP4 4920 -> 1645 ms, and 54.9 -> 11.9 s on a 12k prompt. Decode rate is unchanged. See docs/speculative-prefill.md. Assisted-by: Claude Opus 5 --- common/CMakeLists.txt | 2 + common/arg.cpp | 130 ++++++- common/arg.h | 1 + common/common.h | 19 + common/speculative-prefill.cpp | 337 ++++++++++++++++++ common/speculative-prefill.h | 37 ++ docs/speculative-prefill.md | 102 ++++++ examples/CMakeLists.txt | 1 + examples/speculative-prefill/CMakeLists.txt | 5 + .../speculative-prefill.cpp | 270 ++++++++++++++ include/llama.h | 3 + scripts/bench-spec-prefill.sh | 63 ++++ src/llama-context.cpp | 19 + src/llama-context.h | 1 + tools/server/server-common.cpp | 4 +- tools/server/server-common.h | 2 + tools/server/server-context.cpp | 268 ++++++++++++-- 17 files changed, 1229 insertions(+), 35 deletions(-) create mode 100644 common/speculative-prefill.cpp create mode 100644 common/speculative-prefill.h create mode 100644 docs/speculative-prefill.md create mode 100644 examples/speculative-prefill/CMakeLists.txt create mode 100644 examples/speculative-prefill/speculative-prefill.cpp create mode 100755 scripts/bench-spec-prefill.sh diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 36f1e0cd50f1..1e87312d2071 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -100,6 +100,8 @@ add_library(${TARGET} reasoning-budget.h sampling.cpp sampling.h + speculative-prefill.cpp + speculative-prefill.h speculative.cpp speculative.h subproc.cpp diff --git a/common/arg.cpp b/common/arg.cpp index 6eaf976d64b4..6d5edcb7f2d5 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -361,6 +361,7 @@ static bool spec_types_is_default(const common_params & params) { common_models_handler common_models_handler_init(const common_params & params, llama_example curr_ex) { common_download_hf_plan plan; common_download_hf_plan plan_spec; + common_download_hf_plan plan_prefill; common_download_opts opts; const bool spec_type_draft_mtp = std::find(params.speculative.types.begin(), @@ -413,7 +414,11 @@ common_models_handler common_models_handler_init(const common_params & params, l plan_spec = common_download_get_hf_plan(params.speculative.draft.mparams, opts_spec); } - return common_models_handler{plan, plan_spec, opts}; + if (!params.speculative.prefill.model.hf_repo.empty()) { + plan_prefill = common_download_get_hf_plan(params.speculative.prefill.model, opts); + } + + return common_models_handler{plan, plan_spec, plan_prefill, opts}; } bool common_models_handler_is_preset_repo(const common_models_handler & handler) { @@ -461,8 +466,9 @@ static std::vector build_url_tasks(const common_params_mod void common_models_handler_apply(common_models_handler & handler, common_params & params, common_download_callback * callback) { std::vector tasks; - auto & plan = handler.plan; - auto & plan_spec = handler.plan_spec; + auto & plan = handler.plan; + auto & plan_spec = handler.plan_spec; + auto & plan_prefill = handler.plan_prefill; auto opts = handler.opts; // copy opts.callback = callback; @@ -478,6 +484,7 @@ void common_models_handler_apply(common_models_handler & handler, common_params handle_url(params.model); handle_url(params.mmproj); handle_url(params.speculative.draft.mparams); + handle_url(params.speculative.prefill.model); // optionally, if docker repo is set, resolve it if (!params.model.docker_repo.empty()) { @@ -513,6 +520,13 @@ void common_models_handler_apply(common_models_handler & handler, common_params tasks.push_back(task); had_spec_url = true; } + if (!params.speculative.prefill.model.url.empty()) { + common_download_task task; + task.url = params.speculative.prefill.model.url; + task.local_path = params.speculative.prefill.model.path; + task.opts = opts; + tasks.push_back(task); + } // handle hf_plan tasks auto add_tasks = [&opts, &tasks](const hf_cache::hf_files & model_files, @@ -626,6 +640,11 @@ void common_models_handler_apply(common_models_handler & handler, common_params had_spec_url = true; } + // handle plan_prefill (e.g. --spec-prefill-hf) + if (!plan_prefill.model_files.empty()) { + add_tasks(plan_prefill.model_files, plan_prefill.primary, params.speculative.prefill.model); + } + if (!plan.model_files.empty()) { add_tasks(plan.model_files, plan.primary, params.model); } @@ -4283,6 +4302,111 @@ common_params_context common_params_parser_init(common_params & params, llama_ex params.speculative.draft.mparams.hf_file = value; // will be used if --spec-draft-hf is set } ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_DRAFT_MODEL")); + add_opt(common_arg( + {"--spec-prefill", "--speculative-prefill"}, + "enable speculative prefill using draft model to filter prompt tokens", + [](common_params & params) { + params.speculative.prefill.enabled = true; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_PREFILL")); + add_opt(common_arg( + {"--spec-prefill-draft-model", "--spec-prefill-model", "-mpd", "--speculative-prefill-model", "--speculative-prefill-draft-model"}, "FNAME", + "draft model for speculative prefill (default: unused)", + [](common_params & params, const std::string & value) { + params.speculative.prefill.model.path = value; + params.speculative.prefill.model.hf_file = value; // will be used if --spec-prefill-hf is set + params.speculative.prefill.enabled = true; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_PREFILL_MODEL")); + add_opt(common_arg( + {"--spec-prefill-draft-hf", "--spec-prefill-hf", "-hfpd", "--speculative-prefill-hf", "--speculative-prefill-draft-hf"}, "/[:quant]", + "Hugging Face model repository for speculative prefill draft model (default: unused)", + [](common_params & params, const std::string & value) { + params.speculative.prefill.model.hf_repo = value; + params.speculative.prefill.enabled = true; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_PREFILL_HF")); + add_opt(common_arg( + {"--spec-prefill-draft-ngl", "--spec-prefill-ngl", "-nglpd", "--speculative-prefill-ngl", "--speculative-prefill-draft-ngl"}, "N", + string_format("max. number of speculative prefill draft model layers to store in VRAM, either an exact number, 'auto', or 'all' (default: %s)", + params.speculative.prefill.n_gpu_layers == -1 ? "auto" : "all"), + [](common_params & params, const std::string & value) { + if (value == "auto") { + params.speculative.prefill.n_gpu_layers = -1; + } else if (value == "all") { + params.speculative.prefill.n_gpu_layers = -2; + } else { + params.speculative.prefill.n_gpu_layers = std::stoi(value); + } + if (!llama_supports_gpu_offload()) { + fprintf(stderr, "warning: no usable GPU found, --spec-prefill-ngl option will be ignored\n"); + fprintf(stderr, "warning: one possible reason is that llama.cpp was compiled without GPU support\n"); + fprintf(stderr, "warning: consult docs/build.md for compilation instructions\n"); + } + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_PREFILL_N_GPU_LAYERS")); + add_opt(common_arg( + {"--spec-prefill-draft-device", "--spec-prefill-device", "-devpd", "--speculative-prefill-device", "--speculative-prefill-draft-device"}, "", + "comma-separated list of devices to use for offloading the speculative prefill draft model (none = don't offload)\n" + "use --list-devices to see a list of available devices", + [](common_params & params, const std::string & value) { + params.speculative.prefill.devices = parse_device_list(value); + params.speculative.prefill.enabled = true; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_PREFILL_DEVICE")); + add_opt(common_arg( + {"--spec-prefill-draft-ctx", "--spec-prefill-ctx", "--spec-prefill-ctx-size", "--spec-prefill-max-ctx", "-cpd", "--speculative-prefill-ctx", "--speculative-prefill-max-ctx"}, "N", + string_format("context size for speculative prefill draft model (default: %d, 0 = draft training limit or main context)", params.speculative.prefill.n_ctx), + [](common_params & params, int value) { + if (value < 0) { + throw std::invalid_argument("spec-prefill context size must be >= 0"); + } + params.speculative.prefill.n_ctx = value; + params.speculative.prefill.enabled = true; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_PREFILL_CTX_SIZE")); + add_opt(common_arg( + {"--spec-prefill-p", "--spec-prefill-percentage"}, "P", + string_format("fraction of prompt tokens to retain during speculative prefill (default: %.2f)", (double) params.speculative.prefill.percentage), + [](common_params & params, const std::string & value) { + const float val = std::stof(value); + if (val <= 0.0f || val > 1.0f) { + throw std::invalid_argument("spec-prefill percentage must be between 0.0 (exclusive) and 1.0 (inclusive)"); + } + params.speculative.prefill.enabled = true; + params.speculative.prefill.percentage = val; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_PREFILL_P")); + add_opt(common_arg( + {"--spec-prefill-chunk", "--spec-prefill-chunk-size"}, "N", + string_format("chunk grouping size for speculative prefill (default: %d, 0 to disable)", params.speculative.prefill.chunk_size), + [](common_params & params, int value) { + if (value < 0) { + throw std::invalid_argument("spec-prefill chunk size must be >= 0"); + } + params.speculative.prefill.chunk_size = value; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI})); + add_opt(common_arg( + {"--spec-prefill-lookahead", "--spec-prefill-lah"}, "N", + string_format("number of lookahead decode steps on draft model for attention estimation (default: %d)", params.speculative.prefill.look_ahead_cnt), + [](common_params & params, int value) { + if (value < 1) { + throw std::invalid_argument("spec-prefill lookahead count must be >= 1"); + } + params.speculative.prefill.look_ahead_cnt = value; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI})); + add_opt(common_arg( + {"--spec-prefill-pool-kernel"}, "N", + string_format("1D average pooling kernel size for attention smoothing (default: %d, 0 to disable)", params.speculative.prefill.pool_kernel_size), + [](common_params & params, int value) { + if (value < 0) { + throw std::invalid_argument("spec-prefill pool kernel size must be >= 0"); + } + params.speculative.prefill.pool_kernel_size = value; + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI})); add_opt(common_arg( {"--spec-type"}, common_speculative_all_types_str(), string_format("comma-separated list of types of speculative decoding to use (default: %s)\n", diff --git a/common/arg.h b/common/arg.h index 766afc6d8c2a..e86495a86971 100644 --- a/common/arg.h +++ b/common/arg.h @@ -139,6 +139,7 @@ void common_params_add_preset_options(std::vector & args); struct common_models_handler { common_download_hf_plan plan; common_download_hf_plan plan_spec; + common_download_hf_plan plan_prefill; common_download_opts opts; }; diff --git a/common/common.h b/common/common.h index 054169d37beb..13bebcb52c54 100644 --- a/common/common.h +++ b/common/common.h @@ -370,6 +370,20 @@ struct common_params_speculative_ngram_cache { std::string lookup_cache_dynamic; // path of dynamic ngram cache file for lookup decoding }; +struct common_params_speculative_prefill { + bool enabled = false; // enable speculative prefill + common_params_model model; // draft model for speculative prefill + int32_t n_ctx = 0; // context size for draft model (0 = default/target context size) + int32_t n_gpu_layers = -1; // max draft model layers to store in VRAM (-1 - use default) + std::vector devices; // devices to use for offloading the draft model + float percentage = 0.3f; // fraction of prompt tokens to retain (0.0 < p <= 1.0) + int32_t chunk_size = 32; // chunk grouping size (0 to disable chunking) + int32_t look_ahead_cnt = 8; // lookahead decode steps on draft model + int32_t pool_kernel_size = 13; // 1D average pooling kernel size for smoothing + bool keep_bos = true; // preserve first token (BOS) + bool keep_last = true; // preserve last token / tail chunk +}; + struct common_params_speculative { std::vector types = { COMMON_SPECULATIVE_TYPE_NONE }; @@ -386,6 +400,8 @@ struct common_params_speculative { common_params_speculative_ngram_cache ngram_cache; + common_params_speculative_prefill prefill; + bool has_dft() const { return !draft.mparams.empty(); } @@ -394,6 +410,9 @@ struct common_params_speculative { return synth_len != -1.0 || !synth_rates.empty(); } + bool has_prefill() const { + return prefill.enabled && !prefill.model.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; diff --git a/common/speculative-prefill.cpp b/common/speculative-prefill.cpp new file mode 100644 index 000000000000..ab20ad08f1e5 --- /dev/null +++ b/common/speculative-prefill.cpp @@ -0,0 +1,337 @@ +#include "speculative-prefill.h" + +#include "common.h" +#include "ggml.h" +#include "ggml-backend.h" +#include "llama.h" +#include "log.h" +#include "sampling.h" + +#include +#include +#include +#include +#include + +#define SPF_DBG(fmt, ...) LOG_DBG("spec-prefill: " fmt, __VA_ARGS__) +#define SPF_INF(fmt, ...) LOG_INF("spec-prefill: " fmt, __VA_ARGS__) +#define SPF_ERR(fmt, ...) LOG_ERR("spec-prefill: " fmt, __VA_ARGS__) + +std::vector common_speculative_prefill_compute_importance( + const std::vector> & attn_layers, + int32_t n_prompt, + int32_t n_heads, + int32_t pool_kernel_size) { + if (attn_layers.empty() || n_prompt <= 0 || n_heads <= 0) { + return std::vector(std::max(0, n_prompt), 1.0f); + } + + std::vector max_importance(n_prompt, 0.0f); + std::vector smoothed(n_prompt, 0.0f); + + const int32_t pad = pool_kernel_size > 0 ? (pool_kernel_size / 2) : 0; + + for (const auto & layer_attn : attn_layers) { + if ((int32_t) layer_attn.size() < n_heads * n_prompt) { + continue; + } + + for (int32_t h = 0; h < n_heads; ++h) { + const float * head_attn = layer_attn.data() + (size_t) h * n_prompt; + + if (pool_kernel_size > 1) { + for (int32_t i = 0; i < n_prompt; ++i) { + const int32_t start = std::max(0, i - pad); + const int32_t end = std::min(n_prompt - 1, i + pad); + + float sum = 0.0f; + for (int32_t j = start; j <= end; ++j) { + sum += head_attn[j]; + } + smoothed[i] = sum / (float) (end - start + 1); + } + } else { + std::copy(head_attn, head_attn + n_prompt, smoothed.begin()); + } + + for (int32_t i = 0; i < n_prompt; ++i) { + if (smoothed[i] > max_importance[i]) { + max_importance[i] = smoothed[i]; + } + } + } + } + + return max_importance; +} + +std::vector common_speculative_prefill_select_indices( + const std::vector & importance_scores, + const common_params_speculative_prefill & params) { + const int32_t n_prompt = (int32_t) importance_scores.size(); + if (n_prompt <= 0) { + return {}; + } + + const float percentage = std::max(0.01f, std::min(1.0f, params.percentage)); + std::vector selected; + + if (params.chunk_size > 0 && n_prompt > params.chunk_size) { + const int32_t chunk_size = params.chunk_size; + const int32_t n_chunks = (n_prompt + chunk_size - 1) / chunk_size; + + std::vector> chunk_scores(n_chunks); + + for (int32_t c = 0; c < n_chunks; ++c) { + const int32_t start = c * chunk_size; + const int32_t end = std::min(n_prompt, (c + 1) * chunk_size); + + float sum = 0.0f; + for (int32_t i = start; i < end; ++i) { + sum += importance_scores[i]; + } + chunk_scores[c] = { sum / (float) (end - start), c }; + } + + const int32_t n_keep_chunks = std::max(1, (int32_t) std::ceil(n_chunks * percentage)); + + std::sort(chunk_scores.begin(), chunk_scores.end(), + [](const std::pair & a, const std::pair & b) { + return a.first > b.first; + }); + + for (int32_t k = 0; k < n_keep_chunks && k < n_chunks; ++k) { + const int32_t c = chunk_scores[k].second; + const int32_t start = c * chunk_size; + const int32_t end = std::min(n_prompt, (c + 1) * chunk_size); + + for (int32_t i = start; i < end; ++i) { + selected.push_back(i); + } + } + } else { + const int32_t n_keep = std::max(1, (int32_t) std::ceil(n_prompt * percentage)); + + std::vector> token_scores(n_prompt); + for (int32_t i = 0; i < n_prompt; ++i) { + token_scores[i] = { importance_scores[i], i }; + } + + std::sort(token_scores.begin(), token_scores.end(), + [](const std::pair & a, const std::pair & b) { + return a.first > b.first; + }); + + for (int32_t k = 0; k < n_keep; ++k) { + selected.push_back(token_scores[k].second); + } + } + + if (params.keep_bos) { + selected.push_back(0); + } + + if (params.keep_last) { + if (params.chunk_size > 0 && n_prompt > params.chunk_size) { + const int32_t start = ((n_prompt - 1) / params.chunk_size) * params.chunk_size; + for (int32_t i = start; i < n_prompt; ++i) { + selected.push_back(i); + } + } else { + selected.push_back(n_prompt - 1); + } + } + + std::sort(selected.begin(), selected.end()); + selected.erase(std::unique(selected.begin(), selected.end()), selected.end()); + + return selected; +} + +struct cb_attn_collector_data { + int32_t n_prompt = 0; + std::vector> layer_attns; +}; + +static bool cb_collect_attn(ggml_tensor * t, bool ask, void * user_data) { + if (ask) { + return strncmp(t->name, "kq_soft_max", 11) == 0; + } + + if (strncmp(t->name, "kq_soft_max", 11) != 0) { + return true; + } + + auto * data = (cb_attn_collector_data *) user_data; + if (data == nullptr || data->n_prompt <= 0) { + return true; + } + + if (t->type != GGML_TYPE_F32) { + return true; + } + + const int32_t n_past_total = (int32_t) t->ne[0]; + const int32_t n_heads = (int32_t) t->ne[2]; + const int32_t n_prompt = data->n_prompt; + + if (t->nb[0] != sizeof(float) || n_heads <= 0 || n_past_total < n_prompt) { + return true; + } + + std::vector layer_data((size_t) n_heads * n_prompt); + + for (int32_t h = 0; h < n_heads; ++h) { + float * dst = layer_data.data() + (size_t) h * n_prompt; + ggml_backend_tensor_get(t, dst, (size_t) h * t->nb[2], (size_t) n_prompt * sizeof(float)); + } + + data->layer_attns.push_back(std::move(layer_data)); + + return true; +} + +common_speculative_prefill_result common_speculative_prefill_execute( + llama_context * ctx_dft, + common_sampler * smpl_dft, + const std::vector & prompt, + llama_seq_id seq_id, + const common_params_speculative_prefill & params) { + common_speculative_prefill_result res; + res.n_prompt_orig = (int32_t) prompt.size(); + + if (prompt.empty()) { + return res; + } + + if (!params.enabled || params.percentage >= 1.0f || (int32_t) prompt.size() <= params.chunk_size) { + res.kept_indices.resize(prompt.size()); + std::iota(res.kept_indices.begin(), res.kept_indices.end(), 0); + res.n_prompt_kept = (int32_t) res.kept_indices.size(); + res.importance_scores.assign(prompt.size(), 1.0f); + return res; + } + + const int32_t n_ctx_dft = llama_n_ctx(ctx_dft); + const int32_t lookahead = std::max(1, params.look_ahead_cnt); + + if ((int32_t) prompt.size() + lookahead > n_ctx_dft) { + SPF_INF("prompt size (%d) + lookahead (%d) exceeds draft context (%d); skipping speculative prefill\n", + (int32_t) prompt.size(), lookahead, n_ctx_dft); + res.kept_indices.resize(prompt.size()); + std::iota(res.kept_indices.begin(), res.kept_indices.end(), 0); + res.n_prompt_kept = (int32_t) res.kept_indices.size(); + res.importance_scores.assign(prompt.size(), 1.0f); + return res; + } + + const auto t_start = ggml_time_us(); + + const llama_model * model_dft = llama_get_model(ctx_dft); + const llama_vocab * vocab_dft = llama_model_get_vocab(model_dft); + const int32_t n_heads_dft = llama_model_n_head(model_dft); + + // 1. evaluate full prompt on draft model + { + const int32_t n_batch_dft = llama_n_batch(ctx_dft); + llama_batch batch_prompt = llama_batch_init(std::min((int32_t) prompt.size(), n_batch_dft), 0, 1); + + for (int32_t i = 0; i < (int32_t) prompt.size(); i += n_batch_dft) { + const int32_t n_eval = std::min((int32_t) prompt.size() - i, n_batch_dft); + common_batch_clear(batch_prompt); + + for (int32_t j = 0; j < n_eval; ++j) { + const int32_t idx = i + j; + const bool is_last = (idx == (int32_t) prompt.size() - 1); + common_batch_add(batch_prompt, prompt[idx], (llama_pos) idx, { seq_id }, is_last); + } + + const int ret = llama_decode(ctx_dft, batch_prompt); + if (ret != 0) { + SPF_ERR("failed to decode prompt on draft model, ret = %d\n", ret); + llama_batch_free(batch_prompt); + res.kept_indices.resize(prompt.size()); + std::iota(res.kept_indices.begin(), res.kept_indices.end(), 0); + res.n_prompt_kept = (int32_t) res.kept_indices.size(); + return res; + } + } + llama_batch_free(batch_prompt); + } + + const auto t_prefill_end = ggml_time_us(); + res.t_draft_eval_us = t_prefill_end - t_start; + + // 2. lookahead decode steps with attention extraction + std::vector total_importance(prompt.size(), 0.0f); + + cb_attn_collector_data cb_data; + cb_data.n_prompt = (int32_t) prompt.size(); + + // attach callback + llama_set_eval_callback(ctx_dft, cb_collect_attn, &cb_data); + + llama_batch batch_decode = llama_batch_init(1, 0, 1); + + int32_t actual_steps = 0; + int32_t cur_pos = (int32_t) prompt.size(); + + for (int32_t k = 0; k < lookahead; ++k) { + cb_data.layer_attns.clear(); + + const llama_token token_id = common_sampler_sample(smpl_dft, ctx_dft, -1); + common_sampler_accept(smpl_dft, token_id, true); + + if (llama_vocab_is_eog(vocab_dft, token_id)) { + break; + } + + common_batch_clear(batch_decode); + common_batch_add(batch_decode, token_id, cur_pos++, { seq_id }, true); + + const int ret = llama_decode(ctx_dft, batch_decode); + if (ret != 0) { + SPF_ERR("failed lookahead decode step %d, ret = %d\n", k, ret); + break; + } + + if (!cb_data.layer_attns.empty()) { + const auto step_importance = common_speculative_prefill_compute_importance( + cb_data.layer_attns, + (int32_t) prompt.size(), + n_heads_dft, + params.pool_kernel_size); + + for (size_t i = 0; i < prompt.size(); ++i) { + total_importance[i] += step_importance[i]; + } + actual_steps++; + } + } + + llama_batch_free(batch_decode); + + // detach callback + llama_set_eval_callback(ctx_dft, nullptr, nullptr); + + if (actual_steps == 0) { + LOG_WRN("spec-prefill: attention was not captured (flash attn on?); keeping full prompt\n"); + res.kept_indices.resize(prompt.size()); + std::iota(res.kept_indices.begin(), res.kept_indices.end(), 0); + res.n_prompt_kept = (int32_t) res.kept_indices.size(); + res.importance_scores.assign(prompt.size(), 1.0f); + return res; + } + + for (size_t i = 0; i < prompt.size(); ++i) { + total_importance[i] /= (float) actual_steps; + } + + const auto t_est_start = ggml_time_us(); + res.importance_scores = total_importance; + res.kept_indices = common_speculative_prefill_select_indices(total_importance, params); + res.n_prompt_kept = (int32_t) res.kept_indices.size(); + res.t_estimate_us = ggml_time_us() - t_est_start; + + return res; +} diff --git a/common/speculative-prefill.h b/common/speculative-prefill.h new file mode 100644 index 000000000000..0752449133df --- /dev/null +++ b/common/speculative-prefill.h @@ -0,0 +1,37 @@ +#pragma once + +#include "llama.h" +#include "common.h" + +#include +#include + + +struct common_speculative_prefill_result { + std::vector kept_indices; // sorted original token positions kept for target model + std::vector importance_scores; // per-token aggregated importance scores (size = n_prompt) + int64_t t_draft_eval_us = 0; + int64_t t_estimate_us = 0; + int32_t n_prompt_orig = 0; + int32_t n_prompt_kept = 0; +}; + +// calculate 1D average-pooled smoothed token importance from raw attention matrices +std::vector common_speculative_prefill_compute_importance( + const std::vector> & attn_layers, + int32_t n_prompt, + int32_t n_heads, + int32_t pool_kernel_size); + +// select kept token indices using chunk-based top-k or token-level ranking +std::vector common_speculative_prefill_select_indices( + const std::vector & importance_scores, + const common_params_speculative_prefill & params); + +// execute draft prefill, lookahead decoding, attention collection, and index selection +common_speculative_prefill_result common_speculative_prefill_execute( + llama_context * ctx_dft, + common_sampler * smpl_dft, + const std::vector & prompt, + llama_seq_id seq_id, + const common_params_speculative_prefill & params); diff --git a/docs/speculative-prefill.md b/docs/speculative-prefill.md new file mode 100644 index 000000000000..e7eb9d571c15 --- /dev/null +++ b/docs/speculative-prefill.md @@ -0,0 +1,102 @@ +# Speculative prefill + +Port of upstream PR [ggml-org/llama.cpp#27692](https://github.com/ggml-org/llama.cpp/pull/27692), +which implements *Speculative Prefill: Turbocharging TTFT with Lightweight and Training-Free +Token Importance Estimation* ([arXiv:2502.02789](https://arxiv.org/abs/2502.02789)). + +A small draft model prefills the prompt and decodes a few lookahead tokens. The softmax +attention of those tokens over the prompt is captured with a ggml eval callback, smoothed, +max-reduced over heads and layers, and pooled into chunks. Only the top scoring chunks are +fed to the target model. This is lossy: tokens that are dropped are gone. + +Ported: `common/speculative-prefill.{h,cpp}`, `llama_set_eval_callback`, the `--spec-prefill-*` +options, `examples/speculative-prefill`, and the llama-server hookup. Not ported: the +llama-bench changes and the python eval scripts from that PR. + +## Usage + +```bash +llama-server -m target.gguf -mpd draft.gguf --spec-prefill-percentage 0.3 +``` + +The estimator must be a separate small model with the same vocab as the target. MTP, DFlash2, +DSpark and Eagle3 heads are target-dependent and the server refuses to reuse them for this. +The draft context is forced to flash-attn off, because `kq_soft_max` does not exist on the +flash-attn path. If no attention is captured the full prompt is kept. + +## Results + +Ryzen AI MAX+ 395 / Radeon 8060S, Vulkan, 119 GiB unified memory. Estimator is +Qwen3.5-2B-UD-Q4_K_XL in every run. "eff PP" is original prompt tokens divided by TTFT. + +### TTFT vs context length + +Qwen3.8-27B-UD-Q4_K_XL, no decode speculation, median of 3 runs. A passcode was planted at +mid-depth and retrieved in 3/3 runs in every cell. + +| N tokens | p = off | p = 0.30 | p = 0.15 | +| ---: | ---: | ---: | ---: | +| 1,498 | 6,225 ms | 2,647 ms (2.35x) | 1,516 ms (4.11x) | +| 3,131 | 12,992 ms | 5,199 ms (2.50x) | 3,084 ms (4.21x) | +| 6,212 | 26,494 ms | 9,906 ms (2.67x) | 6,113 ms (4.33x) | +| 12,347 | 54,927 ms | 19,557 ms (2.81x) | 11,896 ms (4.62x) | + +Baseline eff PP is nearly flat over that range (241 -> 225 tok/s), so prefill here is close to +linear in N rather than quadratic. The speedup still grows with N, but because the fixed draft +cost amortizes, not because there is super-linear cost to remove. + +Decode is unaffected: 11.14 to 11.62 tok/s across all 12 cells. + +### With decode speculation + +Qwen3.8-27B-UD-Q4_K_XL, 1,869-token prompt, single run each. + +| decode spec | p | TTFT | eff PP | tg | +| --- | ---: | ---: | ---: | ---: | +| none | off | 5,604 ms | 334 tok/s | 11.56 tok/s | +| none | 0.15 | 1,587 ms | 1,180 tok/s | 11.66 tok/s | +| MTP n=4 | off | 5,755 ms | 325 tok/s | 19.47 tok/s | +| MTP n=4 | 0.30 | 2,440 ms | 768 tok/s | 29.33 tok/s | +| MTP n=4 | 0.15 | 1,636 ms | 1,145 tok/s | 23.61 tok/s | +| DFlash2 n=7 | off | 6,576 ms | 284 tok/s | 19.33 tok/s | +| DFlash2 n=7 | 0.30 | 2,724 ms | 688 tok/s | 30.44 tok/s | +| DFlash2 n=7 | 0.15 | 1,772 ms | 1,057 tok/s | 20.90 tok/s | + +The two mechanisms are independent. DFlash2 gains the most because its head also has to prefill +the full prompt, and speculative prefill shrinks that too. + +### Qwen3.8-Flash-Next + +FP4 ple16 (87 GiB) with the Q8_0 MTP head, 1,869-token prompt, median of 3, needle 3/3. + +| p | TTFT | eff PP | tg | MTP accept | +| ---: | ---: | ---: | ---: | ---: | +| off | 4,920 ms | 380 tok/s | 29.38 tok/s | 71% | +| 0.30 | 2,441 ms | 767 tok/s | 28.53 tok/s | 66% | +| 0.15 | 1,645 ms | 1,139 tok/s | 28.87 tok/s | 69% | + +Flash-Next gains less than the dense 27B because it is hybrid: most layers are linear-attention, +so baseline prefill is already closer to linear in N. MTP acceptance and decode rate hold across +keep ratios. + +The IQ4_XS build reaches 4,718 -> 1,599 ms over the same sweep when run with `--ngram-on-disk`. + +## Caveats + +- Greedy (temperature 0) output is not reproducible on this box even with no speculation and no + speculative prefill: 3 identical requests to the plain target gave 2 different completions. + Only TTFT is worth quoting from these runs. Cross-arm `tg` and draft-acceptance differences + are confounded, because each arm generates different text under `ignore_eos`. +- `--spec-prefill-ctx`, `--spec-prefill-device` and `--spec-prefill-ngl` set `enabled = true` as + a side effect, so passing only a context size makes the server exit with "speculative prefill + enabled but no draft model was provided". +- The example renumbers kept tokens to positions 0..K-1 instead of keeping their original + positions. The reference implementation keeps the original ones. +- `t_draft_eval_us` covers the draft prompt prefill only, not the lookahead decode steps. About + 150 ms of the reported TTFT is unattributed, because the eval callback forces node-by-node + execution. +- Quality falls off when the token budget cannot hold all the non-redundant evidence. p = 0.30 is + a reasonable default; p = 0.15 still retrieved a needle and summed 3 facts spread across the + context; below p = 0.10 is risky for prompts with distributed evidence. + +`scripts/bench-spec-prefill.sh` reproduces the server measurements. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 39f802d250e1..f023b4bfb24a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -30,6 +30,7 @@ else() add_subdirectory(simple) add_subdirectory(simple-chat) add_subdirectory(speculative) + add_subdirectory(speculative-prefill) add_subdirectory(speculative-simple) add_subdirectory(gen-docs) add_subdirectory(training) diff --git a/examples/speculative-prefill/CMakeLists.txt b/examples/speculative-prefill/CMakeLists.txt new file mode 100644 index 000000000000..9bfdec3148b9 --- /dev/null +++ b/examples/speculative-prefill/CMakeLists.txt @@ -0,0 +1,5 @@ +set(TARGET llama-speculative-prefill) +add_executable(${TARGET} speculative-prefill.cpp) +install(TARGETS ${TARGET} RUNTIME) +target_link_libraries(${TARGET} PRIVATE llama-common llama ${CMAKE_THREAD_LIBS_INIT}) +target_compile_features(${TARGET} PRIVATE cxx_std_17) diff --git a/examples/speculative-prefill/speculative-prefill.cpp b/examples/speculative-prefill/speculative-prefill.cpp new file mode 100644 index 000000000000..1a7ae6560464 --- /dev/null +++ b/examples/speculative-prefill/speculative-prefill.cpp @@ -0,0 +1,270 @@ +#include "arg.h" +#include "common.h" +#include "log.h" +#include "llama.h" +#include "sampling.h" +#include "speculative.h" +#include "speculative-prefill.h" +#include "../../src/llama-ext.h" + +#include +#include +#include +#include +#include +#include +#include + +#define SPEC_VOCAB_MAX_SIZE_DIFFERENCE 128 + +int main(int argc, char ** argv) { + std::setlocale(LC_NUMERIC, "C"); + + common_params params; + + common_init(); + + if (!common_params_parse(argc, argv, params, LLAMA_EXAMPLE_SPECULATIVE)) { + return 1; + } + + common_params_model dft_model = params.speculative.prefill.model; + if (dft_model.empty()) { + dft_model = params.speculative.draft.mparams; + } + + if (dft_model.empty()) { + LOG_ERR("%s: draft model is required for speculative prefill (specify with -mpd, --spec-prefill-model, or -md)\n", __func__); + return 1; + } + + // force speculative prefill enabled + params.speculative.prefill.enabled = true; + + llama_backend_init(); + llama_numa_init(params.numa); + + // load target model + LOG_INF("%s: loading target model...\n", __func__); + auto init_tgt = common_init_from_params(params); + if (!init_tgt) { + LOG_ERR("%s: failed to load target model\n", __func__); + return 1; + } + + llama_model * model_tgt = init_tgt->model(); + llama_context * ctx_tgt = init_tgt->context(); + + if (llama_model_is_recurrent(model_tgt)) { + LOG_ERR("%s: speculative prefill is not supported for recurrent models\n", __func__); + return 1; + } + + // load draft model with standard attention to allow attention extraction + LOG_INF("%s: loading draft model...\n", __func__); + common_params params_dft = common_base_params_to_speculative(params); + params_dft.model = dft_model; + if (params.speculative.prefill.n_ctx > 0) { + params_dft.n_ctx = params.speculative.prefill.n_ctx; + } + if (params.speculative.prefill.n_gpu_layers != -1) { + params_dft.n_gpu_layers = params.speculative.prefill.n_gpu_layers; + } + if (!params.speculative.prefill.devices.empty()) { + params_dft.devices = params.speculative.prefill.devices; + } else if (!params.speculative.draft.devices.empty()) { + params_dft.devices = params.speculative.draft.devices; + } + params_dft.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED; // standard attention needed to capture kq_soft_max weights + + auto init_dft = common_init_from_params(params_dft, /*model_only=*/true); + if (!init_dft) { + LOG_ERR("%s: failed to load draft model\n", __func__); + return 1; + } + + llama_model * model_dft = init_dft->model(); + if (!model_dft) { + LOG_ERR("%s: failed to load draft model\n", __func__); + return 1; + } + + if (llama_model_is_recurrent(model_dft)) { + LOG_ERR("%s: draft model is recurrent and not supported for speculative prefill\n", __func__); + return 1; + } + + if (llama_model_target_layer_ids_n(model_dft) > 0) { + LOG_ERR("%s: draft model '%s' is target-dependent and cannot be used for speculative prefill\n", __func__, dft_model.path.c_str()); + return 1; + } + + if (params.speculative.prefill.n_ctx <= 0 && params_dft.n_ctx > (int32_t) llama_model_n_ctx_train(model_dft)) { + params_dft.n_ctx = llama_model_n_ctx_train(model_dft); + LOG_INF("%s: capping speculative prefill draft context to training limit (%d tokens)\n", __func__, params_dft.n_ctx); + } + + llama_context_params cparams_dft = common_context_params_to_llama(params_dft); + llama_context_ptr ctx_dft_own(llama_init_from_model(model_dft, cparams_dft)); + llama_context * ctx_dft = ctx_dft_own.get(); + if (!ctx_dft) { + LOG_ERR("%s: failed to create draft context\n", __func__); + return 1; + } + + const llama_vocab * vocab_tgt = llama_model_get_vocab(model_tgt); + const llama_vocab * vocab_dft = llama_model_get_vocab(model_dft); + + if (llama_vocab_get_add_bos(vocab_tgt) != llama_vocab_get_add_bos(vocab_dft) || + (llama_vocab_get_add_bos(vocab_tgt) && llama_vocab_bos(vocab_tgt) != llama_vocab_bos(vocab_dft))) { + LOG_ERR("%s: draft model bos tokens must match target model. add: %d - %d, id: %d - %d\n", + __func__, + llama_vocab_get_add_bos(vocab_tgt), llama_vocab_get_add_bos(vocab_dft), + llama_vocab_bos(vocab_tgt), llama_vocab_bos(vocab_dft)); + return 1; + } + + { + const int n_vocab_tgt = llama_vocab_n_tokens(vocab_tgt); + const int n_vocab_dft = llama_vocab_n_tokens(vocab_dft); + const int vocab_diff = n_vocab_tgt > n_vocab_dft ? n_vocab_tgt - n_vocab_dft : n_vocab_dft - n_vocab_tgt; + if (vocab_diff > SPEC_VOCAB_MAX_SIZE_DIFFERENCE) { + LOG_ERR("%s: target vocab size %d does not match draft vocab size %d - difference %d, max allowed %d\n", + __func__, n_vocab_tgt, n_vocab_dft, vocab_diff, SPEC_VOCAB_MAX_SIZE_DIFFERENCE); + return 1; + } + } + + // tokenize prompt + std::vector prompt_tokens = common_tokenize(ctx_tgt, params.prompt, true, true); + const int32_t n_prompt = (int32_t) prompt_tokens.size(); + + if (n_prompt == 0) { + LOG_ERR("%s: empty prompt provided\n", __func__); + return 1; + } + + LOG_INF("%s: prompt tokens = %d\n", __func__, n_prompt); + LOG_INF("%s: speculative prefill config: percentage = %.2f, chunk_size = %d, lookahead = %d, pool_kernel = %d\n", + __func__, + (double) params.speculative.prefill.percentage, + params.speculative.prefill.chunk_size, + params.speculative.prefill.look_ahead_cnt, + params.speculative.prefill.pool_kernel_size); + + llama_seq_id seq_id = 0; + + // initialize draft sampler for lookahead steps + common_params_sampling sparams_dft = params.sampling; + sparams_dft.temp = 0.0f; + common_sampler_ptr smpl_dft(common_sampler_init(model_dft, sparams_dft)); + + // step 1: run speculative prefill on draft model + const auto t_spec_prefill_start = ggml_time_us(); + + common_speculative_prefill_result spec_res = common_speculative_prefill_execute( + ctx_dft, + smpl_dft.get(), + prompt_tokens, + seq_id, + params.speculative.prefill); + + LOG_INF("%s: speculative prefill kept %d / %d tokens (%.1f%%)\n", + __func__, + spec_res.n_prompt_kept, + spec_res.n_prompt_orig, + 100.0f * (float) spec_res.n_prompt_kept / (float) spec_res.n_prompt_orig); + LOG_INF("%s: draft eval time: %.2f ms, importance estimation time: %.2f ms\n", + __func__, + spec_res.t_draft_eval_us / 1000.0f, + spec_res.t_estimate_us / 1000.0f); + + // step 2: sparse target model prefill + const auto t_tgt_prefill_start = ggml_time_us(); + + const int32_t n_batch_tgt = llama_n_batch(ctx_tgt); + const int32_t n_kept_total = (int32_t) spec_res.kept_indices.size(); + llama_batch batch_tgt = llama_batch_init(std::min(n_kept_total, n_batch_tgt), 0, 1); + + int ret = 0; + for (int32_t i = 0; i < n_kept_total; i += n_batch_tgt) { + const int32_t n_eval = std::min(n_kept_total - i, n_batch_tgt); + common_batch_clear(batch_tgt); + + for (int32_t j = 0; j < n_eval; ++j) { + const int32_t k = i + j; + const int32_t orig_idx = spec_res.kept_indices[k]; + const bool is_last = (k == n_kept_total - 1); + common_batch_add(batch_tgt, prompt_tokens[orig_idx], (llama_pos) k, { seq_id }, is_last); + } + + ret = llama_decode(ctx_tgt, batch_tgt); + if (ret != 0) { + LOG_ERR("%s: failed to decode sparse prompt on target model, ret = %d\n", __func__, ret); + llama_batch_free(batch_tgt); + return 1; + } + } + llama_batch_free(batch_tgt); + + llama_synchronize(ctx_tgt); + llama_synchronize(ctx_dft); + + const auto t_tgt_prefill_end = ggml_time_us(); + const double ttft_ms = (t_tgt_prefill_end - t_spec_prefill_start) / 1000.0; + const double tgt_prefill_ms = (t_tgt_prefill_end - t_tgt_prefill_start) / 1000.0; + + LOG_INF("%s: sparse target prefill time = %.2f ms\n", __func__, tgt_prefill_ms); + LOG_INF("%s: total Time-To-First-Token (TTFT) = %.2f ms (%.2f effective prompt tokens/s)\n", + __func__, ttft_ms, ttft_ms > 0.0 ? (1000.0 * (double) n_prompt / ttft_ms) : 0.0); + + // step 3: autoregressive generation + common_sampler_ptr smpl_tgt(common_sampler_init(model_tgt, params.sampling)); + + LOG("\n--- Generation Start ---\n"); + + int32_t n_predict = params.n_predict > 0 ? params.n_predict : 32; + int32_t cur_pos = n_kept_total; + int32_t n_generated = 0; + + llama_batch batch_gen = llama_batch_init(1, 0, 1); + + const auto t_gen_start = ggml_time_us(); + + for (int32_t i = 0; i < n_predict; ++i) { + const llama_token token_id = common_sampler_sample(smpl_tgt.get(), ctx_tgt, -1); + common_sampler_accept(smpl_tgt.get(), token_id, true); + + if (llama_vocab_is_eog(vocab_tgt, token_id)) { + break; + } + + const std::string piece = common_token_to_piece(ctx_tgt, token_id); + LOG("%s", piece.c_str()); + fflush(stdout); + + common_batch_clear(batch_gen); + common_batch_add(batch_gen, token_id, (llama_pos) cur_pos++, { seq_id }, true); + + ret = llama_decode(ctx_tgt, batch_gen); + if (ret != 0) { + LOG_ERR("%s: failed to decode generated token %d, ret = %d\n", __func__, i, ret); + break; + } + + n_generated++; + } + + const auto t_gen_end = ggml_time_us(); + const double gen_ms = (t_gen_end - t_gen_start) / 1000.0; + + LOG("\n--- Generation End ---\n\n"); + + LOG_INF("generated %d tokens in %.2f ms (%.2f tokens/s)\n", + n_generated, gen_ms, n_generated > 0 ? (n_generated / (gen_ms / 1000.0)) : 0.0); + + llama_batch_free(batch_gen); + llama_backend_free(); + + return 0; +} diff --git a/include/llama.h b/include/llama.h index c2ef101f6c44..d0dda88ffd2e 100644 --- a/include/llama.h +++ b/include/llama.h @@ -1028,6 +1028,9 @@ extern "C" { // Set abort callback LLAMA_API void llama_set_abort_callback(struct llama_context * ctx, ggml_abort_callback abort_callback, void * abort_callback_data); + // Set evaluation callback + LLAMA_API void llama_set_eval_callback(struct llama_context * ctx, ggml_backend_sched_eval_callback cb_eval, void * cb_eval_user_data); + // Wait until all computations are finished // This is automatically done when using one of the functions below to obtain the computation results // and is not necessary to call it explicitly in most cases diff --git a/scripts/bench-spec-prefill.sh b/scripts/bench-spec-prefill.sh new file mode 100755 index 000000000000..faad2c62b179 --- /dev/null +++ b/scripts/bench-spec-prefill.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Measure TTFT and decode rate for speculative prefill against a llama-server arm. +# +# scripts/bench-spec-prefill.sh
- - - - -
- VLM session with `llama cli` - VLM session with llama cli - - Built-in web UI against `llama serve` running Qwen 3.6 - Built-in web UI against llama serve -
- -## Description - -The main goal of `llama.cpp` is to enable LLM (and VLM) inference with minimal setup and state-of-the-art performance on -a wide range of hardware - locally and in the cloud. - -- Plain C/C++ implementation without any dependencies -- Apple silicon is a first-class citizen - optimized via ARM NEON, Accelerate and Metal frameworks -- AVX, AVX2, AVX512 and AMX support for x86 architectures -- RVV, ZVFH, ZFH, ZICBOP and ZIHINTPAUSE support for RISC-V architectures -- 1.5-bit, 2-bit, 3-bit, 4-bit, 5-bit, 6-bit, and 8-bit integer quantization for faster inference and reduced memory use -- Custom CUDA kernels for running LLMs on NVIDIA GPUs (support for AMD GPUs via HIP and Moore Threads GPUs via MUSA) -- Vulkan and SYCL backend support -- CPU+GPU hybrid inference to partially accelerate models larger than the total VRAM capacity - -The `llama.cpp` project is build on top of the [ggml](https://github.com/ggml-org/ggml) library. +| Change | Flag / switch | What it does | +| --- | --- | --- | +| Speculative prefill | `--spec-prefill` | A small draft model scores prompt tokens by attention importance so the target model only prefills the ones that matter, cutting time-to-first-token on long prompts | +| N-gram table on disk | `--ngram-on-disk`, `--ngram-cache`, `--ngram-io-threads` | Keeps a model's n-gram hash-embedding table (28.8 GB on Qwen3.8-Flash-Next) off the memory budget entirely, reading only the rows each batch actually gathers | +| Adaptive speculative draft length | `--spec-draft-adaptive` | Sizes each draft from a measured per-sequence acceptance EMA rather than always drafting `--spec-draft-n-max`; speeds up MTP and DFlash | +| Vulkan fixes and tuning for RDNA 3.5 | | Driver-gated coopmat LDS stride padding, UMA bulk readback gated on host-cached mappings, IQ3_S mat-vec at batch sizes > 4, and a radix top-k kernel for large k | +| Hidden server presets | `hidden` in the models `.ini` | Keep a model loadable by name while omitting it from `GET /models` | + +Run `--help`, or see [tools/server/README.md](tools/server/README.md), for the full options. ## Supported backends +All of upstream's, unmodified: + | Backend | Target devices | | --- | --- | | [BLAS](docs/build.md#blas-build) | All | @@ -127,6 +118,8 @@ The `llama.cpp` project is build on top of the [ggml](https://github.com/ggml-or | [WebGPU](docs/build.md#webgpu) | All | | [ZenDNN](docs/build.md#zendnn) | AMD CPU | +Most of the work in this fork is measured on AMD Strix Halo (Vulkan and HIP), and CI runs the standard upstream matrix. + ## Documentation #### Tools @@ -140,25 +133,29 @@ The `llama.cpp` project is build on top of the [ggml](https://github.com/ggml-or - [How to build](docs/build.md) - [Running on Docker](docs/docker.md) -- [Build on Android](docs/android.md) - [Multi-GPU usage](docs/multi-gpu.md) - [Performance troubleshooting](docs/development/token_generation_performance_tips.md) - [GGML tips & tricks](https://github.com/ggml-org/llama.cpp/wiki/GGML-Tips-&-Tricks) -- [XCFramework](docs/xcframework.md) - [Completions](docs/completions.md) - [Models](docs/models.md) -- [Release process](docs/release.md) ## Contributing -- Contributors can open PRs -- Collaborators will be invited based on contributions -- Maintainers can push to branches in the `llama.cpp` repo and merge PRs into the `master` branch -- Any help with managing issues, PRs and projects is very appreciated! -- Read the [CONTRIBUTING.md](CONTRIBUTING.md) for more information +This is a small community project. A benchmark, a bug report, or a patch is exactly what it is for. + +- This repo follows the upstream contribution and AI-usage rules, because what lands here is meant to go upstream. + See [CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md); in short, understand every line you submit. +- Performance claims need numbers against a baseline you built and ran yourself, on the same machine in the same + session. +- If your change is Strix Halo specific, send it to [strix-llama.cpp](https://github.com/halo-box/strix-llama.cpp) + instead; that repo has its own, more permissive rules. +- Work lands on `halo/*` branches, and upstream is merged in regularly. ## Acknowledgements +This project is a fork and owes everything to the people who built what it forks: + +- [ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp) and [ggml](https://github.com/ggml-org/ggml) - Georgi Gerganov and the llama.cpp contributors - MIT license - [yhirose/cpp-httplib](https://github.com/yhirose/cpp-httplib) - Single-header HTTP server, used by `llama-server` - MIT license - [nothings/stb](https://github.com/nothings/stb) - Single-header image format decoder, used by multimodal subsystem - Public domain - [nlohmann/json](https://github.com/nlohmann/json) - Single-header JSON library, used by various tools/examples - MIT License From c69f0e8cc603ac5c68b0aab1b84b26db9a1e9af2 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Sun, 6 Sep 2026 15:17:15 +0100 Subject: [PATCH 066/212] speculative : give MTP targets their recurrent rollback slots back f169e0b7d dropped DRAFT_MTP from need_n_rs_seq(), so an MTP target context is created with n_rs_seq = 0 and every partially accepted draft goes through the checkpoint restore path: restore the state, then decode the accepted tokens a second time as a replay batch. On Qwen3.8-27B that is a second target forward pass on most steps (90 draft decodes for 48 accepted steps in a verbose run, against 62 for 62 on mainline). Put MTP back on the list, as upstream has it. The target then rolls the recurrent state back in place and the replay disappears. Cost: the recurrent state holds n_max extra slots (150 MiB -> 750 MiB on this model). Qwen3.8-27B UD-Q4_K_XL, Vulkan (RADV, Mesa 26.1.7), MTP n_max 4, decode tok/s, 3 runs: 9.6/9.8/9.9 on master -> 12.0/12.2/12.0 with this change; with GGML_VK_MMV_NO_SPLIT=1 on top, 19.9/20.0/20.0, matching ggml-org master (19.7/19.7/20.2). Fixes #25 together with the mat-vec chunking change. Assisted-by: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LTnaB4ucGc5vkzctYns4Nq --- common/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/common.h b/common/common.h index 23fc54fedb81..431424304011 100644 --- a/common/common.h +++ b/common/common.h @@ -434,7 +434,7 @@ struct common_params_speculative { } 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_EAGLE3 || t == COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH || t == COMMON_SPECULATIVE_TYPE_DRAFT_DSPARK; + 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; }); return needs_rs_seq ? draft.n_max : 0u; From d22fa655a32c73196cda7a7fa3dace2f17901609 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Sun, 6 Sep 2026 15:17:17 +0100 Subject: [PATCH 067/212] vulkan : only chunk batched mat-vec for the quant types that were measured The column chunking (#1) was measured on Qwen3.8-Flash-Next, where the hot shapes are q8_0 and q6_K. The K-quants and the q4_0 family were put on a 2-column chunk without a measurement, and on a dense Qwen3.8-27B UD-Q4_K_XL that re-reads 16 GiB of weights once per chunk for nothing: the single dispatch scales fine at 3, 5, 6 and 8 columns there. llama-bench pp tok/s, Vulkan (RADV, Mesa 26.1.7), -b 8: cols master NO_SPLIT ggml-org d08c787 2 23.5 23.0 23.0 3 23.0 32.9 33.1 5 28.5 43.1 46.7 8 28.8 47.4 59.9 Restrict chunking, and the 9..16 column chunk path, to q8_0 and q6_K. Every other type takes upstream's dispatch. GGML_VK_MMV_NO_SPLIT=1 still disables the rest. MTP decode on the same model (n_max 4, 3 runs): 9.6/9.8/9.9 tok/s on master -> 14.8/15.1/15.2 with this change (15.6/15.9/15.7 with NO_SPLIT, the q6_K tensors of this model are still chunked). Fixes #25 together with the n_rs_seq change. Assisted-by: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LTnaB4ucGc5vkzctYns4Nq --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 05be6e93712a..9c23f1f933f9 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -9940,17 +9940,20 @@ static void ggml_vk_mul_mat_vec_q_f16_cols(ggml_backend_vk_context * ctx, vk_con // at the cost of re-reading the weights once per chunk. Chunks never take the q8_1 // integer-dot path (its prealloc cache is keyed by tensor, not by column; and that path // has the same pathology). GGML_VK_MMV_NO_SPLIT=1 restores the single dispatch. +// Only q8_0 and q6_K are chunked: those are the types where the slow variants were +// measured. The K-quants were chunked to 2 columns without a measurement, and on a dense +// Qwen3.8-27B UD-Q4_K_XL (RADV, Mesa 26.1.7) that made things worse, not better: pp3/pp5/pp8 +// 23.0/28.5/28.8 t/s chunked against 32.9/43.1/47.4 t/s with the single dispatch, which +// halved MTP decode (issue #25). Leave the other types on upstream's path until measured. +static bool ggml_vk_mmv_chunk_type(ggml_type type) { + return type == GGML_TYPE_Q8_0 || type == GGML_TYPE_Q6_K; +} + static uint32_t ggml_vk_mmv_good_cols(ggml_type type, uint32_t n) { - switch (type) { - case GGML_TYPE_Q8_0: - case GGML_TYPE_Q6_K: - return n >= 4 ? 4 : (n >= 2 ? 2 : 1); - case GGML_TYPE_Q4_0: case GGML_TYPE_Q4_1: case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: - case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: case GGML_TYPE_Q5_K: - return n >= 2 ? 2 : 1; - default: - return n; // f32/f16/bf16 and the i-quants: no pathology measured + if (!ggml_vk_mmv_chunk_type(type)) { + return n; } + return n >= 4 ? 4 : (n >= 2 ? 2 : 1); } // Batches of 9..16 columns of a quantized weight also go through chunked mat-vec: the @@ -9966,7 +9969,7 @@ static bool ggml_vk_mmv_can_chunk(ggml_backend_vk_context * ctx, const ggml_tens const bool would_quantize_y = ctx->device->integer_dot_product && src1->type == GGML_TYPE_F32 && ggml_is_contiguous(src1) && (src1->ne[1] * src1->ne[0]) % 4 == 0 && ggml_vk_should_use_mmvq(ctx->device, src0->ne[1], src1->ne[1], src1->ne[0], src0->type); - return !no_split && ggml_is_quantized(src0->type) && + return !no_split && ggml_vk_mmv_chunk_type(src0->type) && !would_quantize_y && dst->ne[1] > mul_mat_vec_max_cols && dst->ne[1] <= mul_mat_vec_chunk_max_cols && src1->ne[2] * src1->ne[3] == 1 && src1->type == GGML_TYPE_F32 && From 33870a2e1c99419041ae57f838db0ba3f9d6a122 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:18:10 +0200 Subject: [PATCH 068/212] Halo/scheduler uma ring 27311 (#23) Co-authored-by: Aman Gupta Co-authored-by: Piotr Wilkin --- docs/development/backend-scheduler.md | 237 ++++++++++++ ggml/include/ggml-alloc.h | 3 + ggml/include/ggml-backend.h | 44 +-- ggml/src/CMakeLists.txt | 2 + ggml/src/ggml-alloc.c | 45 +++ ggml/src/ggml-backend-sanitize.cpp | 527 ++++++++++++++++++++++++++ ggml/src/ggml-backend-sanitize.h | 36 ++ ggml/src/ggml-backend.cpp | 321 +++++++++++++++- ggml/src/ggml-cuda/ggml-cuda.cu | 46 ++- src/llama-context.cpp | 7 +- src/llama-graph.cpp | 52 ++- src/llama-impl.h | 7 + src/llama-kv-cache-dsv4.cpp | 1 + src/llama-kv-cache-msa.cpp | 6 +- src/llama-kv-cache.cpp | 17 +- tests/CMakeLists.txt | 1 + tests/test-alloc.cpp | 37 ++ tests/test-backend-sched-ring.cpp | 80 ++++ 18 files changed, 1367 insertions(+), 102 deletions(-) create mode 100644 docs/development/backend-scheduler.md create mode 100644 ggml/src/ggml-backend-sanitize.cpp create mode 100644 ggml/src/ggml-backend-sanitize.h create mode 100644 tests/test-backend-sched-ring.cpp diff --git a/docs/development/backend-scheduler.md b/docs/development/backend-scheduler.md new file mode 100644 index 000000000000..b56816fe4f78 --- /dev/null +++ b/docs/development/backend-scheduler.md @@ -0,0 +1,237 @@ +# The backend scheduler + +`ggml_backend_sched` lets one graph run across several backend devices. It assigns every node to +a backend, allocates the compute buffers, inserts the copies needed where a node's operands live +on another device, and executes the result. + +Callers interact with it through `ggml-backend.h`; this document describes what it does and the +contracts it places on callers and on backends. + +## Contents + +- [Assigning nodes to backends](#assigning-nodes-to-backends) +- [Splits](#splits) +- [Allocation and graph reuse](#allocation-and-graph-reuse) +- [Graph input ring buffer](#graph-input-ring-buffer) +- [Output lifetime under asynchronous execution](#output-lifetime-under-asynchronous-execution) +- [Memory read in place by another backend](#memory-read-in-place-by-another-backend) +- [Ops that alias their source](#ops-that-alias-their-source) +- [The scheduler sanitizer](#the-scheduler-sanitizer) +- [Environment variables](#environment-variables) + +## Assigning nodes to backends + +The caller defines backend priority by the order of the array passed to `ggml_backend_sched_new()`: lower indices have higher priority, and the last backend must be the CPU. In llama.cpp this array contains the selected model devices in their configured order, then accelerator backends such as BLAS, then the CPU. + +Placement combines that order with hard constraints and locality heuristics: + +1. An assignment made with `ggml_backend_sched_set_tensor_backend()` is retained. +2. A preallocated tensor or view must use a backend that supports both its existing buffer type and its operation. The first compatible backend in caller order is selected; the allocation itself cannot move. +3. An unallocated graph input (`GGML_TENSOR_FLAG_INPUT`) is assigned to the last backend so that the caller can write it through the CPU slot. +4. Operations using weights prefer the first backend that can use the weight buffer and run the operation, avoiding a weight copy. Selected operations may instead be offloaded to an earlier backend when `op_offload` requests it. +5. Existing non-CPU assignments are expanded forward and backward through adjacent supported operations. Remaining assignments are expanded after that so contiguous runs stay together instead of alternating backends. +6. An unassigned operation is placed on the backend supporting the largest number of its assigned inputs. Caller order breaks ties. An assigned operation may be upgraded to an earlier backend when both use the same buffer type and that backend supports the operation and every source buffer. + +## Splits + +Once every node has a backend, the graph is cut into **splits**: maximal runs of consecutive nodes +sharing one backend. Each split is submitted to its backend as a single graph, in order. + +Where a split reads a tensor produced on another backend, the scheduler checks whether the +consuming backend can read that buffer directly (`ggml_backend_supports_buft`). If it can, the +tensor is read where it is. If it cannot, a copy is created in the consuming backend's buffer and +the node's operand is repointed at the copy - these are the tensors named `##`. + +Copies are issued asynchronously where the backends support it, with events ordering the consumer +behind the producer. + +## Allocation and graph reuse + +Compute buffers are sized by `ggml_backend_sched_reserve()` with a worst case graph, so that later +graphs of the same shape need no reallocation. Within a graph the allocator reuses memory: a +tensor's memory is handed to a later tensor once its last consumer *in graph order* has run. + +A caller that rebuilds an identical graph can skip the split and the allocation entirely and reuse +the previous one - llama.cpp does this whenever the graph topology is unchanged. This is fast, but +it means neither the split nor the allocator runs, so anything that normally happens there does not +happen on that path. The mechanisms below exist because of that. + +## Graph input ring buffer + +Graph inputs are assigned to the CPU backend, but the caller may pass a **device host buffer type** +for that slot - pinned memory owned by a GPU. Some devices, integrated GPUs in particular, accept +that buffer type for compute. When that happens no input copy is created and the device reads +exactly the memory the host thread writes. + +Nothing then stops the host writing the next iteration's inputs while the device is still reading +the previous ones. The writes are a plain `memcpy` on the calling thread; they are not ordered +against an in-flight `ggml_backend_graph_compute_async`. + +The scheduler detects this in `ggml_backend_sched_new()` by testing the condition that elides the +copy - a host buffer type in the last slot that some other backend accepts - rather than by +identifying particular devices, and requiring that backend to be asynchronous. Backends that +deliberately refuse to compute on pinned host memory are unaffected, and so are synchronous ones +such as BLAS - their work is finished before `graph_compute` returns, so nothing of theirs can +still be reading. + +When it holds, each graph input gets a **ring of buffers** instead of one, and the scheduler moves +the inputs onto the next slot rather than waiting for the device. The slot being stepped onto was +last read `n_copies - 1` iterations ago, so the wait for it is normally already satisfied. + +The extra buffers are not free: one additional copy of every graph input per extra slot, which for +attention masks scales with context and batch size. The default depth is 2 - enough to remove the +wait at the smallest cost that does. + +**Rotation only happens when a compute may still be in flight.** Anything that waits for the +backends clears that state, so a caller that synchronizes between iterations - reading logits in a +sampling loop, say - keeps its inputs at fixed addresses and never pays for the ring. Callers that +issue several computes without synchronizing rotate between them. + +Two obligations come with this: + +- **Callers** must call `ggml_backend_sched_prepare_inputs()` before writing the inputs of a graph that is being reused. That path allocates nothing and so cannot rotate on its own. The function has an effect only when the scheduler has multiple copies and a compute may still be in flight; it is a no-op for single-copy schedulers, after `ggml_backend_sched_alloc_graph()`, and after synchronization. + +- **Backends** that cache work against a graph, such as the CUDA graph cache, may treat an + unchanged `ggml_cgraph::uid` as a promise that nothing the cached work captured has moved, and + skip re-reading the addresses. Rotating moves tensor addresses without re-splitting, so the + scheduler re-stamps the split uids whenever it re-points inputs. A backend keeping such a cache + must key it on the uid or re-check the addresses itself. Violating this does not degrade + gracefully: the cached work replays against stale addresses and the results are quietly wrong. + +## Output lifetime under asynchronous execution + +The scheduler ring versions graph inputs, not graph outputs. `GGML_TENSOR_FLAG_OUTPUT` keeps a tensor's allocation alive until the end of its graph; it does not create one result allocation per asynchronous submission. Reusing a graph can therefore write its output tensors again. + +This does not require an output ring when each output is consumed in submission order. Operations submitted to one backend are ordered, so a copy or downstream consumer queued after one graph runs before a later graph on that backend can overwrite the same output allocation. Internal backend streams must join or otherwise preserve that order before subsequent operations use the result. + +llama.cpp queues every required output readback immediately after each micro-batch and before it submits the next one. Each readback writes to that micro-batch's rows in the persistent host output buffer. The terminal backend therefore sees: + +``` +compute A -> readback A -> compute B -> readback B +``` + +Pipeline stages can overlap on different backends, but micro-batch B cannot overtake micro-batch A on the terminal backend. Different execution times do not change the queue order. If asynchronous readback is unsupported, the backend API synchronizes before copying instead. The public llama.cpp result accessors also synchronize before exposing host result pointers. + +This differs from graph input preparation: the host writes the next inputs directly, outside the backend queue, so no ordering edge protects an input allocation without synchronization or rotation. + +A scheduler caller that needs every asynchronous result must enqueue an ordered copy or consumer before another submission can overwrite the output, or synchronize before reusing the graph. If several results must remain resident on a device at the same time, the caller must provide distinct output storage. `n_copies` does not provide output snapshots. + +## Memory read in place by another backend + +The allocator frees a tensor's memory once its last consumer in graph order has run. That is only +sound if the consumers have finished. When a split reads a tensor in place out of a buffer another +backend owns, the reading split is asynchronous and may still be reading well after graph order +says the memory is dead - so a later split on the owning backend can be given the same memory and +overwrite it. + +Such tensors are pinned for the lifetime of the graph with `ggml_gallocr_pin_tensor()`, preventing both ordinary reuse after their last graph-order consumer and in-place reuse by a child operation. The pin is keyed on the **view root**, since that is the tensor that owns the memory; the allocator resolves candidate in-place parents to the same root. Keying the pin on the accessed tensor would miss every access through a view. + +Pins are recomputed when the scheduler splits a graph and are encoded in the resulting allocation, so graph reuse preserves the protected addresses without rerunning the allocator. They are separate from `GGML_TENSOR_FLAG_OUTPUT`: marking a tensor as an output would also extend its allocator lifetime, but it changes tensor semantics and can disable backend optimizations unrelated to the cross-backend read. + +## Ops that alias their source + +Some ops write through to one of their sources rather than to fresh memory: `ggml_set()` and +friends, where the result is a view of `src0`. Such a node must run on the backend where that +source lives. + +If it does not, the split logic sees an operand on another backend, substitutes a copy for it, and +the op writes into the copy. The copy is never written back, so the update is silently discarded - +the graph still computes, every op still runs, and the result is wrong. On a recurrent model this +shows up as state that stops being carried between tokens, and generation degenerates into a +repeated token. + +The scheduler therefore assigns any node that aliases its source, and is not a pure view op, to +the backend of the tensor it aliases. + +That move is only made when the target backend supports the op. Support can be conditional on +the tensor types - CUDA runs `GGML_OP_SET` only for F32 and I32 - so the backend owning the +aliased memory is not guaranteed to be able to run the op writing into it. There is no correct +placement in that case: the scheduler copies operands into a split, never results out of one, so +whichever backend runs the op, the write cannot reach the aliased memory. The node is left where +the earlier passes put it, which is what the scheduler did before this rule existed, and the +reason is logged under `GGML_SCHED_DEBUG`. + +## The scheduler sanitizer + +The above are ordering rules, and ordering bugs do not announce themselves - they produce wrong +numbers on some runs and not others. The sanitizer is a happens-before checker for the backend +API, built into `ggml-base` and enabled at runtime. + +It maintains a vector clock per actor - the host thread and each backend - and a shadow map of +which actor last read or wrote every byte of every buffer. Synchronization points (backend +synchronize, event record, event wait, event synchronize, async copies) advance those clocks. When +an access conflicts with a previous one and no happens-before edge connects them, it reports: + +``` +ggml-sched-sanitize: RACE (write-after-read) on ROCm_Host[0, 2048) +ggml-sched-sanitize: read ROCm0#2 @3 split 0 inp_tokens (compute) +ggml-sched-sanitize: write HOST @15 split -1 inp_tokens (tensor_set) +ggml-sched-sanitize: no happens-before edge: HOST knows ROCm0#2@2, needs >=3 +``` + +Where the access is through a view, the report names both the accessed tensor and the allocation +that owns the memory, as `accessed <- root`. The two are frequently unrelated, and the root is the +one that matters. + +**It only sees what goes through the backend API.** A caller that writes a host-visible tensor by +taking `tensor->data` and memcpy'ing into it is invisible to it, and races against such writes are +missed entirely. Callers doing that must announce it with `ggml_backend_tensor_set_direct()`, +which is a no-op unless the sanitizer is enabled. In llama.cpp every such site goes through +`llama_host_write()`. + +By default the first race aborts. `GGML_SCHED_SANITIZE_NONFATAL=1` reports each one and continues, +so a single run enumerates a whole workload; a count is printed at exit. + +## Environment variables + +| variable | effect | +|---|---| +| `GGML_SCHED_DEBUG` | `1` prints assignments and scheduler decisions, `2` adds per-node detail | +| `GGML_SCHED_DEBUG_REALLOC` | report, or abort on, unexpected graph reallocations | +| `GGML_SCHED_UMA_RING` | when the ring is in use: `0`/`1` disables it, larger sets the depth. cannot enable it where it was not detected | +| `GGML_SCHED_PIN_ASYNC_READS` | `0` disables pinning of memory read in place by another backend | +| `GGML_SCHED_SANITIZE` | `1` enables the sanitizer, `2` also traces synchronization edges | +| `GGML_SCHED_SANITIZE_NONFATAL` | `1` reports every race instead of aborting on the first | + +## Example usage + +```c +// operations that use tensors allocated in a buffer with USAGE_WEIGHTS will be assigned +// preferably to run on the same backend as the buffer +ggml_backend_buffer_set_usage(buf_weights, GGML_BACKEND_BUFFER_USAGE_WEIGHTS); + +sched = ggml_backend_sched_new({backend_gpu, backend_gpu2, backend_cpu}, NULL, num_backends, + GGML_DEFAULT_GRAPH_SIZE, false, true); + +// initialize buffers from a max size graph (optional) +reserve_graph = build_graph(sched, max_batch_size); + +// manually assign nodes to a backend (optional, should not be needed in most cases) +struct ggml_tensor * node = ggml_mul_mat(ctx, ...); +ggml_backend_sched_set_tensor_backend(sched, node, backend_gpu); + +ggml_backend_sched_reserve(sched, reserve_graph); + +// compute +// the graph and its tensors are single-use in terms of allocation, multi-use in terms of computation +graph = build_graph(sched); +for (int i = 0; i < 10; ++i) { + ggml_backend_sched_graph_compute(sched, graph); // on the first iteration the graph is allocated automatically +} + +// if there are graph inputs: +graph = build_graph(sched); // a new graph that is not allocated +ggml_backend_sched_reset(sched); // clear the allocation of the previous graph +ggml_backend_sched_alloc_graph(sched, graph); +ggml_backend_tensor_set(input_tensor, ...); +ggml_backend_sched_graph_compute(sched, graph); + +// when reusing an already allocated graph rather than allocating a new one, the inputs must be +// made safe to write first - see "Graph input ring buffer" +ggml_backend_sched_prepare_inputs(sched); +ggml_backend_tensor_set(input_tensor, ...); +ggml_backend_sched_graph_compute(sched, graph); +``` + +An alternative to the above is to assign the inputs to a dedicated context and allocate them +statically with `ggml_backend_alloc_ctx_tensors()`. diff --git a/ggml/include/ggml-alloc.h b/ggml/include/ggml-alloc.h index a7926a21a9a2..2b5084cb40a3 100644 --- a/ggml/include/ggml-alloc.h +++ b/ggml/include/ggml-alloc.h @@ -73,6 +73,9 @@ GGML_API bool ggml_gallocr_alloc_graph(ggml_gallocr_t galloc, struct ggml_cgraph GGML_API size_t ggml_gallocr_get_buffer_size(ggml_gallocr_t galloc, int buffer_id); +GGML_API void ggml_gallocr_pin_tensor(ggml_gallocr_t galloc, struct ggml_tensor * t); +GGML_API void ggml_gallocr_clear_pins(ggml_gallocr_t galloc); + // Utils // Create a buffer and allocate all the tensors in a ggml_context // ggml_backend_alloc_ctx_tensors_from_buft_size returns the size of the buffer that would be allocated by ggml_backend_alloc_ctx_tensors_from_buft diff --git a/ggml/include/ggml-backend.h b/ggml/include/ggml-backend.h index cc3f8cd36e35..68ec89011242 100644 --- a/ggml/include/ggml-backend.h +++ b/ggml/include/ggml-backend.h @@ -95,6 +95,8 @@ extern "C" { GGML_API void ggml_backend_tensor_get_2d(const struct ggml_tensor * tensor, void * data, size_t offset, size_t size, size_t n_copies, size_t stride_tensor, size_t stride_data); GGML_API void ggml_backend_tensor_memset( struct ggml_tensor * tensor, uint8_t value, size_t offset, size_t size); + GGML_API void ggml_backend_tensor_set_direct(struct ggml_tensor * tensor, size_t offset, size_t size); + GGML_API void ggml_backend_synchronize(ggml_backend_t backend); GGML_API ggml_backend_graph_plan_t ggml_backend_graph_plan_create(ggml_backend_t backend, struct ggml_cgraph * cgraph); @@ -263,46 +265,7 @@ extern "C" { // Backend scheduler // - // The backend scheduler allows for multiple backend devices to be used together - // Handles compute buffer allocation, assignment of tensors to backends, and copying of tensors between backends - // The backends are selected based on: - // - the backend that supports the operation - // - the location of the pre-allocated tensors (e.g. the weights) - /* - Example usage: - - // operations that use tensors allocated in a buffer with USAGE_WEIGHTS will be assigned - // preferably to run on the same backend as the buffer - ggml_backend_buffer_set_usage(buf_weights, GGML_BACKEND_BUFFER_USAGE_WEIGHTS); - - sched = ggml_backend_sched_new({backend_gpu, backend_gpu2, backend_cpu}, NULL, num_backends, GGML_DEFAULT_GRAPH_SIZE, false, true); - - // initialize buffers from a max size graph (optional) - reserve_graph = build_graph(sched, max_batch_size); - - // manually assign nodes to a backend (optional, should not be needed in most cases) - struct ggml_tensor * node = ggml_mul_mat(ctx, ...); - ggml_backend_sched_set_tensor_backend(sched, node, backend_gpu); - - ggml_backend_sched_reserve(sched, reserve_graph); - - // compute - graph = build_graph(sched); // the graph and its tensors are single-use in terms of allocation, multi-use in terms of computation - for (int i = 0; i < 10; ++i) { - ggml_backend_sched_graph_compute(sched, graph); // on the first iteration the graph is allocated automatically - } - - // if there are graph inputs: - graph = build_graph(sched); // get a new graph that is not allocated (the metadata for the old graph is freed once ggml_free is called) - ggml_backend_sched_reset(sched); // clear the allocation of the previous graph - ggml_backend_sched_alloc_graph(sched, graph); // explicitly allocate the new graph but do not execute it - ggml_backend_tensor_set(input_tensor, ...); // copy data to the newly allocated graph tensors - ggml_backend_sched_graph_compute(sched, graph); // execute the graph - - // as an alternative to the above it is also possible to assign the inputs to a dedicated context and - // allocate them statically via ggml_backend_alloc_ctx_tensors - } - */ + // see docs/development/backend-scheduler.md typedef struct ggml_backend_sched * ggml_backend_sched_t; @@ -341,6 +304,7 @@ extern "C" { // Allocate and compute graph on the backend scheduler GGML_API bool ggml_backend_sched_alloc_graph(ggml_backend_sched_t sched, struct ggml_cgraph * graph); // returns success + GGML_API void ggml_backend_sched_prepare_inputs(ggml_backend_sched_t sched); GGML_API enum ggml_status ggml_backend_sched_graph_compute(ggml_backend_sched_t sched, struct ggml_cgraph * graph); GGML_API enum ggml_status ggml_backend_sched_graph_compute_async(ggml_backend_sched_t sched, struct ggml_cgraph * graph); GGML_API void ggml_backend_sched_synchronize(ggml_backend_sched_t sched); diff --git a/ggml/src/CMakeLists.txt b/ggml/src/CMakeLists.txt index 947732000219..e16e6c693161 100644 --- a/ggml/src/CMakeLists.txt +++ b/ggml/src/CMakeLists.txt @@ -201,6 +201,8 @@ add_library(ggml-base ggml-alloc.c ggml-backend.cpp ggml-backend-meta.cpp + ggml-backend-sanitize.cpp + ggml-backend-sanitize.h ggml-opt.cpp ggml-threading.cpp ggml-threading.h diff --git a/ggml/src/ggml-alloc.c b/ggml/src/ggml-alloc.c index a71838eafc69..5fd670fd0c43 100644 --- a/ggml/src/ggml-alloc.c +++ b/ggml/src/ggml-alloc.c @@ -493,6 +493,9 @@ struct ggml_gallocr { struct leaf_alloc * leaf_allocs; // [n_leafs] int n_leafs; + + struct ggml_hash_set pinned; + bool has_pinned; }; ggml_gallocr_t ggml_gallocr_new_n(ggml_backend_buffer_type_t * bufts, int n_bufs) { @@ -570,6 +573,9 @@ void ggml_gallocr_free(ggml_gallocr_t galloc) { } ggml_hash_set_free(&galloc->hash_set); + if (galloc->pinned.size > 0) { + ggml_hash_set_free(&galloc->pinned); + } free(galloc->hash_values); free(galloc->bufts); free(galloc->buffers); @@ -590,6 +596,16 @@ static bool ggml_gallocr_is_own(ggml_gallocr_t galloc, struct ggml_tensor * t) { return ggml_gallocr_hash_get(galloc, t)->allocated; } +static bool ggml_gallocr_is_pinned(ggml_gallocr_t galloc, struct ggml_tensor * t) { + if (!galloc->has_pinned) { + return false; + } + while (t->view_src != NULL) { + t = t->view_src; + } + return ggml_hash_contains(&galloc->pinned, t); +} + static bool ggml_gallocr_is_allocated(ggml_gallocr_t galloc, struct ggml_tensor * t) { return t->data != NULL // tensor data already set externally || t->buffer // tensor on external buffer (but not yet allocated) @@ -642,6 +658,11 @@ static void ggml_gallocr_allocate_node(ggml_gallocr_t galloc, struct ggml_tensor continue; } + if (ggml_gallocr_is_pinned(galloc, parent)) { + AT_PRINTF("not reusing parent %s for %s as it is pinned\n", parent->name, node->name); + continue; + } + // outputs cannot be reused if (parent->flags & GGML_TENSOR_FLAG_OUTPUT || (parent->view_src != NULL && parent->view_src->flags & GGML_TENSOR_FLAG_OUTPUT)) { AT_PRINTF("not reusing parent %s for %s as it is an output\n", parent->name, node->name); @@ -688,6 +709,25 @@ static void ggml_gallocr_allocate_node(ggml_gallocr_t galloc, struct ggml_tensor } } +void ggml_gallocr_pin_tensor(ggml_gallocr_t galloc, struct ggml_tensor * t) { + GGML_ASSERT(galloc); + if (galloc->pinned.size == 0) { + galloc->pinned = ggml_hash_set_new(GGML_DEFAULT_GRAPH_SIZE); + } + if (ggml_hash_insert(&galloc->pinned, t) == GGML_HASHSET_FULL) { + GGML_ABORT("%s: pinned tensor set is full\n", __func__); + } + galloc->has_pinned = true; +} + +void ggml_gallocr_clear_pins(ggml_gallocr_t galloc) { + GGML_ASSERT(galloc); + if (galloc->pinned.size > 0) { + ggml_hash_set_reset(&galloc->pinned); + } + galloc->has_pinned = false; +} + static void ggml_gallocr_free_node(ggml_gallocr_t galloc, struct ggml_tensor * node) { // graph outputs are never freed if (node->flags & GGML_TENSOR_FLAG_OUTPUT) { @@ -695,6 +735,11 @@ static void ggml_gallocr_free_node(ggml_gallocr_t galloc, struct ggml_tensor * n return; } + if (ggml_gallocr_is_pinned(galloc, node)) { + AT_PRINTF("not freeing pinned %s\n", node->name); + return; + } + struct hash_node * hn = ggml_gallocr_hash_get(galloc, node); int buffer_id = hn->buffer_id; struct ggml_dyn_tallocr * alloc = galloc->buf_tallocs[buffer_id]; diff --git a/ggml/src/ggml-backend-sanitize.cpp b/ggml/src/ggml-backend-sanitize.cpp new file mode 100644 index 000000000000..f9b37742e170 --- /dev/null +++ b/ggml/src/ggml-backend-sanitize.cpp @@ -0,0 +1,527 @@ +#include "ggml-backend-sanitize.h" +#include "ggml-backend-impl.h" +#include "ggml-impl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int ggml_san_level(void) { + static const int level = []() { + const char * env = getenv("GGML_SCHED_SANITIZE"); + return env ? atoi(env) : 0; + }(); + return level; +} + +static int ggml_san_nonfatal(void) { + static const int nonfatal = []() { + const char * env = getenv("GGML_SCHED_SANITIZE_NONFATAL"); + return env ? atoi(env) : 0; + }(); + return nonfatal; +} + +namespace { + +constexpr int SAN_MAX_ACTORS = 64; +constexpr size_t SAN_MAX_RANGES = 65536; + +using vclock = std::vector; + +struct san_access { + int actor = -1; + uint64_t clock = 0; + int split = -1; + const char * what = ""; // always a string literal + char tensor[2*GGML_MAX_NAME + 8] = { 0 }; // copied: source tensor is recycled per graph +}; + +struct san_entry { + size_t end; + san_access last_write; + std::vector reads; // at most one per actor +}; + +struct san_state { + std::mutex mutex; + + std::unordered_map actors; + std::vector actor_names; + std::unordered_map name_counts; + std::vector vc; // vc[a][b] = a's knowledge of b's clock + + std::unordered_map> mem; + + std::unordered_map ev_vc; + std::unordered_map events; + + size_t n_races = 0; // only counted in nonfatal mode +}; + +san_state & state() { + static san_state * s = []() { + san_state * st = new san_state(); + st->actor_names.push_back("HOST"); + st->vc.emplace_back(SAN_MAX_ACTORS, 0); + if (ggml_san_nonfatal()) { + static san_state * at_exit = st; + atexit([]() { + fprintf(stderr, "\nggml-sched-sanitize: %zu race(s) reported\n", at_exit->n_races); + }); + } + return st; + }(); + return *s; +} + +// every helper below assumes the caller holds state().mutex + +int actor_id(san_state & s, ggml_backend_t backend) { + if (backend == nullptr) { + return 0; + } + + auto it = s.actors.find(backend); + if (it != s.actors.end()) { + return it->second; + } + + // two backends can share a device and therefore a name, disambiguate + std::string base = ggml_backend_name(backend); + const int idx = s.name_counts[base]++; + + const int id = (int) s.actor_names.size(); + GGML_ASSERT(id < SAN_MAX_ACTORS); + s.actor_names.push_back(idx == 0 ? base : base + "#" + std::to_string(idx)); + s.vc.emplace_back(SAN_MAX_ACTORS, 0); + s.actors[backend] = id; + return id; +} + +const char * actor_name(san_state & s, int id) { + return id >= 0 && id < (int) s.actor_names.size() ? s.actor_names[id].c_str() : "?"; +} + +void join(vclock & dst, const vclock & src) { + for (size_t i = 0; i < dst.size(); i++) { + dst[i] = std::max(dst[i], src[i]); + } +} + +uint64_t tick(san_state & s, int a) { + return ++s.vc[a][a]; +} + +uint64_t issue(san_state & s, int a) { + join(s.vc[a], s.vc[0]); + return tick(s, a); +} + +void maybe_flush(san_state & s) { + if (s.mem.empty()) { + return; + } + + // once the host knows every actor's current clock, the issue rule guarantees that any + // future operation joins vc[0] first, so the shadow state can no longer report a race + for (size_t b = 0; b < s.vc.size(); b++) { + if (s.vc[0][b] < s.vc[b][b]) { + return; + } + } + + s.mem.clear(); +} + +bool is_view_op(enum ggml_op op) { + return op == GGML_OP_VIEW || op == GGML_OP_RESHAPE || op == GGML_OP_PERMUTE || op == GGML_OP_TRANSPOSE; +} + +struct mem_range { + ggml_backend_buffer_t buf; + size_t off; + size_t len; +}; + +bool resolve(const ggml_tensor * t, size_t offset, size_t size, mem_range & out) { + if (t == nullptr || t->data == nullptr) { + return false; + } + + ggml_backend_buffer_t buf = t->view_src ? t->view_src->buffer : t->buffer; + if (buf == nullptr) { + return false; + } + + // weights are written once at load and never recycled by the allocator + if (ggml_backend_buffer_get_usage(buf) == GGML_BACKEND_BUFFER_USAGE_WEIGHTS) { + return false; + } + + void * base = ggml_backend_buffer_get_base(buf); + if (base == nullptr || size == 0) { + return false; + } + + out.buf = buf; + out.off = (size_t) ((const char *) t->data - (const char *) base) + offset; + out.len = size; + return true; +} + +thread_local int tl_split = -1; +thread_local int tl_split_actor = -1; + +void report(san_state & s, const mem_range & mr, const san_access & cur, const san_access & prev, const char * kind) { + fprintf(stderr, "\nggml-sched-sanitize: RACE (%s) on %s[%zu, %zu)\n", + kind, ggml_backend_buffer_name(mr.buf), mr.off, mr.off + mr.len); + fprintf(stderr, "ggml-sched-sanitize: %-5s %-12s @%-4llu split %-4d %s (%s)\n", + strcmp(kind, "write-after-read") == 0 ? "read" : "write", + actor_name(s, prev.actor), (unsigned long long) prev.clock, prev.split, prev.tensor, prev.what); + fprintf(stderr, "ggml-sched-sanitize: %-5s %-12s @%-4llu split %-4d %s (%s)\n", + strcmp(kind, "read-after-write") == 0 ? "read" : "write", + actor_name(s, cur.actor), (unsigned long long) cur.clock, cur.split, cur.tensor, cur.what); + fprintf(stderr, "ggml-sched-sanitize: no happens-before edge: %s knows %s@%llu, needs >=%llu\n\n", + actor_name(s, cur.actor), actor_name(s, prev.actor), + (unsigned long long) s.vc[cur.actor][prev.actor], (unsigned long long) prev.clock); + + if (ggml_san_nonfatal()) { + s.n_races++; + return; + } + + GGML_ABORT("ggml-sched-sanitize: race detected"); +} + +// make sure a range boundary exists at pos, so [off,off+len) lands on whole entries +void split_at(std::map & m, size_t pos) { + auto it = m.upper_bound(pos); + if (it == m.begin()) { + return; + } + --it; + if (it->first < pos && it->second.end > pos) { + san_entry tail = it->second; + it->second.end = pos; + m.emplace(pos, std::move(tail)); + } +} + +void access_range(san_state & s, const mem_range & mr, bool write, const san_access & info) { + auto & m = s.mem[mr.buf]; + const int a = info.actor; + + // the shadow state is bounded by the work issued before the host observes every actor, + // not by run length. reaching this means maybe_flush stopped clearing it - + // most likely an operation was enqueued without going through issue(), which breaks + // the argument maybe_flush relies on. that is a sanitizer bug, not a memory limit. + GGML_ASSERT(m.size() <= SAN_MAX_RANGES && "shadow state is not being flushed - see issue()/maybe_flush()"); + + const size_t begin = mr.off; + const size_t end = mr.off + mr.len; + + split_at(m, begin); + split_at(m, end); + + // fill gaps so the whole span is covered by entries + size_t cur = begin; + while (cur < end) { + auto it = m.lower_bound(cur); + if (it == m.end() || it->first >= end) { + san_entry e; + e.end = end; + m.emplace(cur, std::move(e)); + break; + } + if (it->first > cur) { + san_entry e; + e.end = it->first; + m.emplace(cur, std::move(e)); + } + cur = it->second.end; + } + + for (auto it = m.lower_bound(begin); it != m.end() && it->first < end; ++it) { + san_entry & e = it->second; + + if (e.last_write.actor >= 0 && e.last_write.actor != a) { + if (e.last_write.clock > s.vc[a][e.last_write.actor]) { + report(s, mr, info, e.last_write, write ? "write-after-write" : "read-after-write"); + } + } + + if (write) { + for (const san_access & r : e.reads) { + if (r.actor != a && r.clock > s.vc[a][r.actor]) { + report(s, mr, info, r, "write-after-read"); + } + } + } + + if (write) { + e.last_write = info; + e.reads.clear(); + } else { + bool found = false; + for (san_access & r : e.reads) { + if (r.actor == a) { + r = info; + found = true; + break; + } + } + if (!found) { + e.reads.push_back(info); + } + } + } + +} + +// returns false if the tensor is not in tracked memory +bool touch(san_state & s, int a, uint64_t c, const ggml_tensor * t, + size_t offset, size_t size, bool write, const char * what) { + mem_range mr; + if (!resolve(t, offset, size, mr)) { + return false; + } + + san_access info; + info.actor = a; + info.clock = c; + info.what = what; + info.split = tl_split; + const ggml_tensor * root = t; + while (root->view_src != NULL) { + root = root->view_src; + } + if (root != t) { + snprintf(info.tensor, sizeof(info.tensor), "%s <- %s", t->name, root->name); + } else { + snprintf(info.tensor, sizeof(info.tensor), "%s", t->name); + } + + if (ggml_san_level() >= 2) { + GGML_LOG_DEBUG("san [split %3d %-10s] %-10s %-5s %s[%zu+%zu] %s (%s)\n", + tl_split, tl_split_actor >= 0 ? actor_name(s, tl_split_actor) : "-", actor_name(s, a), + write ? "write" : "read", + ggml_backend_buffer_name(mr.buf), mr.off, mr.len, t->name, what); + } + + access_range(s, mr, write, info); + return true; +} + +void trace(san_state & s, int a, const char * fmt, ...) { + if (ggml_san_level() < 2) { + return; + } + + char body[512]; + va_list ap; + va_start(ap, fmt); + vsnprintf(body, sizeof(body), fmt, ap); + va_end(ap); + + GGML_LOG_DEBUG("san [split %3d %-10s] %-10s %s", + tl_split, tl_split_actor >= 0 ? actor_name(s, tl_split_actor) : "-", actor_name(s, a), body); +} + +} // namespace + +void ggml_san_sync(ggml_backend_t backend) { + if (ggml_san_level() == 0) { + return; + } + san_state & s = state(); + std::lock_guard lk(s.mutex); + + const int a = actor_id(s, backend); + join(s.vc[0], s.vc[a]); // host now knows everything this backend knew + trace(s, a, "EDGE synchronize -> HOST\n"); + maybe_flush(s); +} + +void ggml_san_event_record(ggml_backend_event_t event, ggml_backend_t backend) { + if (ggml_san_level() == 0) { + return; + } + san_state & s = state(); + std::lock_guard lk(s.mutex); + + const int a = actor_id(s, backend); + s.ev_vc[event] = s.vc[a]; + trace(s, a, "EDGE event_record ev=%d\n", (int) s.events.emplace(event, (int) s.events.size()).first->second); +} + +void ggml_san_event_wait(ggml_backend_t backend, ggml_backend_event_t event) { + if (ggml_san_level() == 0) { + return; + } + san_state & s = state(); + std::lock_guard lk(s.mutex); + + const int a = actor_id(s, backend); + auto it = s.ev_vc.find(event); + if (it != s.ev_vc.end()) { + join(s.vc[a], it->second); + } + trace(s, a, "EDGE event_wait ev=%d\n", (int) s.events.emplace(event, (int) s.events.size()).first->second); +} + +void ggml_san_event_sync(ggml_backend_event_t event) { + if (ggml_san_level() == 0) { + return; + } + san_state & s = state(); + std::lock_guard lk(s.mutex); + + auto it = s.ev_vc.find(event); + if (it != s.ev_vc.end()) { + join(s.vc[0], it->second); + } + trace(s, 0, "EDGE event_sync ev=%d\n", (int) s.events.emplace(event, (int) s.events.size()).first->second); + maybe_flush(s); +} + +void ggml_san_compute(ggml_backend_t backend, const ggml_cgraph * cgraph) { + if (ggml_san_level() == 0 || cgraph == nullptr) { + return; + } + san_state & s = state(); + std::lock_guard lk(s.mutex); + + const int a = actor_id(s, backend); + + // a backend with no synchronize runs inline on the calling thread, so it both + // inherits and publishes the host's knowledge - without this the CPU backend's + // writes are never ordered against anything and no race can be detected + const bool synchronous = backend->iface.synchronize == nullptr; + + const uint64_t c = issue(s, a); + + size_t n_read = 0; + size_t n_write = 0; + for (int i = 0; i < cgraph->n_nodes; i++) { + ggml_tensor * node = cgraph->nodes[i]; + if (is_view_op(node->op)) { + continue; + } + for (int j = 0; j < GGML_MAX_SRC; j++) { + if (node->op == GGML_OP_CPY && j == 1) { + continue; + } + if (node->src[j] && touch(s, a, c, node->src[j], 0, ggml_nbytes(node->src[j]), false, "compute")) { + n_read++; + } + } + if (touch(s, a, c, node, 0, ggml_nbytes(node), true, "compute")) { + n_write++; + } + } + + if (synchronous) { + join(s.vc[0], s.vc[a]); + maybe_flush(s); + } + + trace(s, a, "compute %d nodes, r=%zu w=%zu ranges\n", cgraph->n_nodes, n_read, n_write); +} + +void ggml_san_access(ggml_backend_t backend, const ggml_tensor * tensor, + size_t offset, size_t size, bool write, const char * what) { + if (ggml_san_level() == 0) { + return; + } + san_state & s = state(); + std::lock_guard lk(s.mutex); + + const int a = actor_id(s, backend); + const uint64_t c = issue(s, a); + touch(s, a, c, tensor, offset, size, write, what); + + // a synchronous access has completed by the time the call returns + if (backend == nullptr || backend->iface.synchronize == nullptr) { + join(s.vc[0], s.vc[a]); + maybe_flush(s); + } +} + +void ggml_san_cpy_async(ggml_backend_t src_be, ggml_backend_t dst_be, + const ggml_tensor * src, const ggml_tensor * dst) { + if (ggml_san_level() == 0) { + return; + } + san_state & s = state(); + std::lock_guard lk(s.mutex); + + const int a_src = actor_id(s, src_be); + const int a_dst = actor_id(s, dst_be); + + // TODO: clarify in ggml + // which stream actually performs the read of src is backend specific. host memory has + // no queue of its own, so a host->device transfer runs on dst's queue - that is what + // makes the source racy against a later host write. a device->device transfer is + // issued on the src stream (see "copy on src stream" in ggml-cuda), where src's own + // later work is already ordered against it. + ggml_backend_buffer_t src_buf = src->view_src ? src->view_src->buffer : src->buffer; + const int read_actor = (src_buf && ggml_backend_buffer_is_host(src_buf)) ? a_dst : a_src; + + uint64_t read_clock; + uint64_t write_clock; + + if (read_actor == a_dst) { + // the dst queue performs both sides of the copy after src's pending work + join(s.vc[a_dst], s.vc[a_src]); + read_clock = write_clock = issue(s, a_dst); + } else { + // issue the src read first, then carry its completion into the dst write + read_clock = issue(s, read_actor); + join(s.vc[a_dst], s.vc[read_actor]); + write_clock = issue(s, a_dst); + } + + touch(s, read_actor, read_clock, src, 0, ggml_nbytes(src), false, "cpy_async src"); + touch(s, a_dst, write_clock, dst, 0, ggml_nbytes(dst), true, "cpy_async dst"); + + trace(s, a_dst, "cpy_async %s@%s -> %s\n", src->name, actor_name(s, read_actor), dst->name); +} + +void ggml_san_buffer_free(ggml_backend_buffer_t buffer) { + if (ggml_san_level() == 0) { + return; + } + san_state & s = state(); + std::lock_guard lk(s.mutex); + + s.mem.erase(buffer); +} + +void ggml_san_split(int split_id, ggml_backend_t backend, int n_inputs) { + if (ggml_san_level() == 0) { + return; + } + tl_split = split_id; + + if (split_id < 0) { + tl_split_actor = -1; + return; + } + + san_state & s = state(); + std::lock_guard lk(s.mutex); + const int a = actor_id(s, backend); + tl_split_actor = a; + + if (n_inputs == 0) { + trace(s, a, "split 0 inputs (no sync path)\n"); + } +} diff --git a/ggml/src/ggml-backend-sanitize.h b/ggml/src/ggml-backend-sanitize.h new file mode 100644 index 000000000000..9009f50e6492 --- /dev/null +++ b/ggml/src/ggml-backend-sanitize.h @@ -0,0 +1,36 @@ +#pragma once + +// happens-before instrumentation for the ggml backend API +// +// GGML_SCHED_SANITIZE=1 detect happens-before violations +// GGML_SCHED_SANITIZE=2 also trace synchronization edges and memory ranges + +#include "ggml-backend.h" + +#ifdef __cplusplus +extern "C" { +#endif + + int ggml_san_level(void); + + void ggml_san_sync (ggml_backend_t backend); + void ggml_san_event_record(ggml_backend_event_t event, ggml_backend_t backend); + void ggml_san_event_wait (ggml_backend_t backend, ggml_backend_event_t event); + void ggml_san_event_sync (ggml_backend_event_t event); + + void ggml_san_compute (ggml_backend_t backend, const struct ggml_cgraph * cgraph); + + // backend == NULL means the access is performed by the host thread + void ggml_san_access (ggml_backend_t backend, const struct ggml_tensor * tensor, + size_t offset, size_t size, bool write, const char * what); + + void ggml_san_cpy_async (ggml_backend_t src_be, ggml_backend_t dst_be, + const struct ggml_tensor * src, const struct ggml_tensor * dst); + + void ggml_san_buffer_free (ggml_backend_buffer_t buffer); + + void ggml_san_split (int split_id, ggml_backend_t backend, int n_inputs); + +#ifdef __cplusplus +} +#endif diff --git a/ggml/src/ggml-backend.cpp b/ggml/src/ggml-backend.cpp index 6862128e6373..b0a3636d4f99 100644 --- a/ggml/src/ggml-backend.cpp +++ b/ggml/src/ggml-backend.cpp @@ -10,6 +10,7 @@ #include "ggml-backend.h" #include "ggml-backend-impl.h" +#include "ggml-backend-sanitize.h" #include "ggml-alloc.h" #include "ggml-impl.h" @@ -118,6 +119,8 @@ void ggml_backend_buffer_free(ggml_backend_buffer_t buffer) { return; } + ggml_san_buffer_free(buffer); + if (buffer->iface.free_buffer != NULL) { buffer->iface.free_buffer(buffer); } @@ -216,7 +219,12 @@ void ggml_backend_buffer_reset(ggml_backend_buffer_t buffer) { bool ggml_backend_buffer_copy_tensor(const struct ggml_tensor * src, struct ggml_tensor * dst) { ggml_backend_buffer_t dst_buf = dst->view_src ? dst->view_src->buffer : dst->buffer; if (dst_buf->iface.cpy_tensor) { - return dst_buf->iface.cpy_tensor(dst_buf, src, dst); + const bool copied = dst_buf->iface.cpy_tensor(dst_buf, src, dst); + if (copied) { + ggml_san_access(NULL, src, 0, ggml_nbytes(src), false, "buffer_copy_tensor src"); + ggml_san_access(NULL, dst, 0, ggml_nbytes(dst), true, "buffer_copy_tensor dst"); + } + return copied; } return false; } @@ -272,6 +280,7 @@ void ggml_backend_tensor_set_async(ggml_backend_t backend, struct ggml_tensor * ggml_backend_synchronize(backend); ggml_backend_tensor_set(tensor, data, offset, size); } else { + ggml_san_access(backend, tensor, offset, size, true, "set_async"); backend->iface.set_tensor_async(backend, tensor, data, offset, size); } } @@ -286,6 +295,7 @@ void ggml_backend_tensor_get_async(ggml_backend_t backend, const struct ggml_ten ggml_backend_synchronize(backend); ggml_backend_tensor_get(tensor, data, offset, size); } else { + ggml_san_access(backend, tensor, offset, size, false, "get_async"); backend->iface.get_tensor_async(backend, tensor, data, offset, size); } } @@ -308,6 +318,9 @@ void ggml_backend_tensor_set_2d_async(ggml_backend_t backend, struct ggml_tensor GGML_ASSERT(tensor->data != NULL && "tensor not allocated"); GGML_ASSERT(offset + (n_copies-1)*stride_tensor + size <= ggml_nbytes(tensor) && "tensor write out of bounds"); + for (size_t i = 0; i < n_copies; i++) { + ggml_san_access(backend, tensor, offset + i*stride_tensor, size, true, "set_2d_async"); + } backend->iface.set_tensor_2d_async(backend, tensor, data, offset, size, n_copies, stride_tensor, stride_data); } @@ -329,6 +342,9 @@ void ggml_backend_tensor_get_2d_async(ggml_backend_t backend, const struct ggml_ GGML_ASSERT(tensor->data != NULL && "tensor not allocated"); GGML_ASSERT(offset + (n_copies-1)*stride_tensor + size <= ggml_nbytes(tensor) && "tensor read out of bounds"); + for (size_t i = 0; i < n_copies; i++) { + ggml_san_access(backend, tensor, offset + i*stride_tensor, size, false, "get_2d_async"); + } backend->iface.get_tensor_2d_async(backend, tensor, data, offset, size, n_copies, stride_tensor, stride_data); } @@ -344,6 +360,7 @@ void ggml_backend_tensor_set(struct ggml_tensor * tensor, const void * data, siz GGML_ASSERT(tensor->data != NULL && "tensor not allocated"); GGML_ASSERT(offset + size <= ggml_nbytes(tensor) && "tensor write out of bounds"); + ggml_san_access(NULL, tensor, offset, size, true, "tensor_set"); buf->iface.set_tensor(buf, tensor, data, offset, size); } @@ -359,6 +376,7 @@ void ggml_backend_tensor_get(const struct ggml_tensor * tensor, void * data, siz GGML_ASSERT(tensor->data != NULL && "tensor not allocated"); GGML_ASSERT(offset + size <= ggml_nbytes(tensor) && "tensor read out of bounds"); + ggml_san_access(NULL, tensor, offset, size, false, "tensor_get"); buf->iface.get_tensor(buf, tensor, data, offset, size); } @@ -381,6 +399,9 @@ void ggml_backend_tensor_set_2d(struct ggml_tensor * tensor, const void * data, GGML_ASSERT(tensor->data != NULL && "tensor not allocated"); GGML_ASSERT(offset + (n_copies-1)*stride_tensor + size <= ggml_nbytes(tensor) && "tensor write out of bounds"); + for (size_t i = 0; i < n_copies; i++) { + ggml_san_access(NULL, tensor, offset + i*stride_tensor, size, true, "tensor_set_2d"); + } buf->iface.set_tensor_2d(buf, tensor, data, offset, size, n_copies, stride_tensor, stride_data); } @@ -403,6 +424,9 @@ void ggml_backend_tensor_get_2d(const struct ggml_tensor * tensor, void * data, GGML_ASSERT(tensor->data != NULL && "tensor not allocated"); GGML_ASSERT(offset + (n_copies-1)*stride_tensor + size <= ggml_nbytes(tensor) && "tensor read out of bounds"); + for (size_t i = 0; i < n_copies; i++) { + ggml_san_access(NULL, tensor, offset + i*stride_tensor, size, false, "tensor_get_2d"); + } buf->iface.get_tensor_2d(buf, tensor, data, offset, size, n_copies, stride_tensor, stride_data); } @@ -419,9 +443,22 @@ void ggml_backend_tensor_memset(struct ggml_tensor * tensor, uint8_t value, size GGML_ASSERT(offset + size <= ggml_nbytes(tensor) && "tensor write out of bounds"); GGML_ASSERT(buf->iface.memset_tensor != NULL && "memset not implemented by backend buffer"); + ggml_san_access(NULL, tensor, offset, size, true, "tensor_memset"); buf->iface.memset_tensor(buf, tensor, value, offset, size); } +void ggml_backend_tensor_set_direct(struct ggml_tensor * tensor, size_t offset, size_t size) { + if (ggml_san_level() == 0) { + return; + } + + GGML_ASSERT(tensor); + GGML_ASSERT(tensor->data != NULL && "tensor not allocated"); + GGML_ASSERT(offset + size <= ggml_nbytes(tensor) && "tensor write out of bounds"); + + ggml_san_access(NULL, tensor, offset, size, true, "set_direct"); +} + void ggml_backend_synchronize(ggml_backend_t backend) { GGML_ASSERT(backend); if (backend->iface.synchronize == NULL) { @@ -429,6 +466,7 @@ void ggml_backend_synchronize(ggml_backend_t backend) { } backend->iface.synchronize(backend); + ggml_san_sync(backend); } ggml_backend_graph_plan_t ggml_backend_graph_plan_create(ggml_backend_t backend, struct ggml_cgraph * cgraph) { @@ -460,7 +498,11 @@ enum ggml_status ggml_backend_graph_compute(ggml_backend_t backend, struct ggml_ enum ggml_status ggml_backend_graph_compute_async(ggml_backend_t backend, struct ggml_cgraph * cgraph) { GGML_ASSERT(backend); - return backend->iface.graph_compute(backend, cgraph); + const enum ggml_status status = backend->iface.graph_compute(backend, cgraph); + if (status == GGML_STATUS_SUCCESS) { + ggml_san_compute(backend, cgraph); + } + return status; } bool ggml_backend_supports_op(ggml_backend_t backend, const struct ggml_tensor * op) { @@ -493,9 +535,11 @@ void ggml_backend_tensor_copy(const struct ggml_tensor * src, struct ggml_tensor } if (ggml_backend_buffer_is_host(src->buffer)) { + ggml_san_access(NULL, src, 0, ggml_nbytes(src), false, "tensor_copy src"); ggml_backend_tensor_set(dst, src->data, 0, ggml_nbytes(src)); } else if (ggml_backend_buffer_is_host(dst->buffer)) { ggml_backend_tensor_get(src, dst->data, 0, ggml_nbytes(src)); + ggml_san_access(NULL, dst, 0, ggml_nbytes(src), true, "tensor_copy dst"); } else if (!ggml_backend_buffer_copy_tensor(src, dst)) { #ifndef NDEBUG GGML_LOG_DEBUG("%s: warning: slow copy from %s to %s\n", __func__, ggml_backend_buffer_name(src->buffer), ggml_backend_buffer_name(dst->buffer)); @@ -517,7 +561,9 @@ void ggml_backend_tensor_copy_async(ggml_backend_t backend_src, ggml_backend_t b GGML_ASSERT(backend_dst); if (backend_dst->iface.cpy_tensor_async != NULL) { - if (backend_dst->iface.cpy_tensor_async(backend_src, backend_dst, src, dst)) { + const bool accepted = backend_dst->iface.cpy_tensor_async(backend_src, backend_dst, src, dst); + if (accepted) { + ggml_san_cpy_async(backend_src, backend_dst, src, dst); return; } } @@ -551,6 +597,7 @@ void ggml_backend_event_record(ggml_backend_event_t event, ggml_backend_t backen GGML_ASSERT(backend->iface.event_record != NULL); backend->iface.event_record(backend, event); + ggml_san_event_record(event, backend); } void ggml_backend_event_synchronize(ggml_backend_event_t event) { @@ -558,6 +605,7 @@ void ggml_backend_event_synchronize(ggml_backend_event_t event) { GGML_ASSERT(event->device->iface.event_synchronize); event->device->iface.event_synchronize(event->device, event); + ggml_san_event_sync(event); } void ggml_backend_event_wait(ggml_backend_t backend, ggml_backend_event_t event) { @@ -565,6 +613,7 @@ void ggml_backend_event_wait(ggml_backend_t backend, ggml_backend_event_t event) GGML_ASSERT(backend->iface.event_wait != NULL); backend->iface.event_wait(backend, event); + ggml_san_event_wait(backend, event); } static void ggml_backend_graph_optimize(ggml_backend_t backend, struct ggml_cgraph * cgraph, struct ggml_backend_graph_optimize_params * params) { @@ -821,6 +870,13 @@ struct ggml_backend_sched { int n_graph_inputs; int graph_inputs_capacity; + struct ggml_backend_sched_input_slots { + ggml_backend_buffer_t buffer[GGML_SCHED_MAX_COPIES]; + void * data [GGML_SCHED_MAX_COPIES]; + } * graph_input_slots; // [graph_inputs_capacity] + + bool needs_rotate; + struct ggml_context * ctx; ggml_backend_sched_eval_callback callback_eval; @@ -872,9 +928,39 @@ static void ggml_backend_sched_graph_inputs_grow(ggml_backend_sched_t sched) { GGML_ABORT("failed to grow graph inputs container"); } sched->graph_inputs = pnew; + + auto * snew = (struct ggml_backend_sched::ggml_backend_sched_input_slots *) realloc( + (void *) sched->graph_input_slots, new_cap * sizeof(sched->graph_input_slots[0])); + if (snew == NULL) { + GGML_LOG_ERROR("%s: failed to allocate %zu bytes\n", __func__, new_cap * sizeof(sched->graph_input_slots[0])); + GGML_ABORT("failed to grow graph input slots container"); + } + sched->graph_input_slots = snew; + sched->graph_inputs_capacity = new_cap; } +static struct ggml_tensor * ggml_backend_sched_graph_input(struct ggml_tensor * t) { + while (t != NULL) { + if (t->flags & GGML_TENSOR_FLAG_INPUT) { + return t; + } + t = t->view_src; + } + return NULL; +} + +static void ggml_backend_sched_reinit_view(struct ggml_tensor * t) { + if (t->view_src == NULL) { + return; + } + + ggml_backend_sched_reinit_view(t->view_src); + + t->buffer = t->view_src->buffer; + t->data = (char *) t->view_src->data + t->view_offs; +} + // returns the priority of the backend, lower id is higher priority static int ggml_backend_sched_backend_id(ggml_backend_sched_t sched, ggml_backend_t backend) { for (int i = 0; i < sched->n_backends; i++) { @@ -1067,6 +1153,7 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra // reset splits sched->n_splits = 0; sched->n_graph_inputs = 0; + ggml_gallocr_clear_pins(sched->galloc); sched->is_reset = false; struct ggml_init_params params = { @@ -1270,6 +1357,25 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra *cur_backend_id = tensor_backend_id(node->view_src); SET_CAUSE(node, "4.vsrc"); } + + if (node->view_src != NULL && !ggml_is_view_op(node->op)) { + const int view_src_backend_id = tensor_backend_id(node->view_src); + if (view_src_backend_id != -1 && *cur_backend_id != view_src_backend_id) { + if (ggml_backend_supports_op(sched->backends[view_src_backend_id], node)) { + if (sched->debug) { + GGML_LOG_DEBUG("%s: %s (%s) moved to %s to keep it with the tensor it aliases (%s)\n", + __func__, node->name, ggml_op_name(node->op), + ggml_backend_name(sched->backends[view_src_backend_id]), node->view_src->name); + } + *cur_backend_id = view_src_backend_id; + SET_CAUSE(node, "4.alias"); + } else if (sched->debug) { + GGML_LOG_DEBUG("%s: %s (%s) aliases %s on %s, which cannot run it - the write will be lost\n", + __func__, node->name, ggml_op_name(node->op), node->view_src->name, + ggml_backend_name(sched->backends[view_src_backend_id])); + } + } + } for (int j = 0; j < GGML_MAX_SRC; j++) { struct ggml_tensor * src = node->src[j]; if (src == NULL) { @@ -1383,27 +1489,57 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra const int src_backend_id = sched->hv_tensor_backend_ids[src_id]; GGML_ASSERT(src_backend_id != -1); // all inputs should be assigned by now - if (src->flags & GGML_TENSOR_FLAG_INPUT && sched->n_copies > 1) { - if (tensor_id_copy(src_id, src_backend_id, 0) == NULL) { - ggml_backend_t backend = sched->backends[src_backend_id]; + struct ggml_tensor * inp = ggml_backend_sched_graph_input(src); + + if (inp != NULL && sched->n_copies > 1) { + const size_t inp_id = hash_id(inp); + const int inp_backend_id = sched->hv_tensor_backend_ids[inp_id]; + + if (inp_backend_id != -1 && tensor_id_copy(inp_id, inp_backend_id, 0) == NULL) { + ggml_backend_t backend = sched->backends[inp_backend_id]; for (int c = 0; c < sched->n_copies; c++) { struct ggml_tensor * tensor_copy; - if (c == sched->cur_copy) { - tensor_copy = src; // use the original tensor as the current copy + if (c == 0) { + tensor_copy = inp; // use the original tensor as the current copy } else { - tensor_copy = ggml_dup_tensor_layout(sched->ctx, src); - ggml_format_name(tensor_copy, "%s#%s#%d", ggml_backend_name(backend), src->name, c); + tensor_copy = ggml_dup_tensor_layout(sched->ctx, inp); + ggml_format_name(tensor_copy, "%s#%s#%d", ggml_backend_name(backend), inp->name, c); } ggml_set_input(tensor_copy); ggml_set_output(tensor_copy); // prevent ggml-alloc from overwriting the tensor - tensor_id_copy(src_id, src_backend_id, c) = tensor_copy; + tensor_id_copy(inp_id, inp_backend_id, c) = tensor_copy; SET_CAUSE(tensor_copy, "4.cpy"); } int n_graph_inputs = sched->n_graph_inputs++; if (n_graph_inputs >= sched->graph_inputs_capacity) { ggml_backend_sched_graph_inputs_grow(sched); } - sched->graph_inputs[n_graph_inputs] = src; + sched->graph_inputs[n_graph_inputs] = inp; + } + } + + { + struct ggml_tensor * root = src; + while (root->view_src != NULL) { + root = root->view_src; + } + + const int root_backend_id = sched->hv_tensor_backend_ids[hash_id(root)]; + + static const bool pin_async_reads = []() { + const char * env = getenv("GGML_SCHED_PIN_ASYNC_READS"); + return env ? atoi(env) != 0 : true; + }(); + + if (pin_async_reads && + root_backend_id != -1 && root_backend_id != cur_backend_id && + sched->backends[cur_backend_id]->iface.synchronize != NULL && + ggml_backend_sched_buffer_supported(sched, root, cur_backend_id)) { + ggml_gallocr_pin_tensor(sched->galloc, root); + if (sched->debug) { + GGML_LOG_DEBUG("%s: pinned %s, read in place by %s\n", __func__, + root->name, ggml_backend_name(sched->backends[cur_backend_id])); + } } } @@ -1676,6 +1812,8 @@ static enum ggml_status ggml_backend_sched_compute_splits(ggml_backend_sched_t s } } + ggml_san_split(split_id, split_backend, split->n_inputs); + // copy the input tensors to the split backend for (int input_id = 0; input_id < split->n_inputs; input_id++) { ggml_backend_t input_backend = ggml_backend_sched_get_tensor_backend(sched, split->inputs[input_id]); @@ -1786,7 +1924,14 @@ static enum ggml_status ggml_backend_sched_compute_splits(ggml_backend_sched_t s } else { // try async copy, but if not possible, we can still use a sync copy without synchronizing the dst backend, since we handle the synchronization here with multiple copies and events // TODO: add public function to facilitate this, since applications do not have direct access to the backend interface - if (!split_backend->iface.cpy_tensor_async || !split_backend->iface.cpy_tensor_async(input_backend, split_backend, input, input_cpy)) { + bool cpy_async_ok = false; + if (split_backend->iface.cpy_tensor_async) { + cpy_async_ok = split_backend->iface.cpy_tensor_async(input_backend, split_backend, input, input_cpy); + if (cpy_async_ok) { + ggml_san_cpy_async(input_backend, split_backend, input, input_cpy); + } + } + if (!cpy_async_ok) { ggml_backend_synchronize(input_backend); if (sched->events[split_backend_id][sched->cur_copy] != NULL) { ggml_backend_event_synchronize(sched->events[split_backend_id][sched->cur_copy]); @@ -1802,6 +1947,7 @@ static enum ggml_status ggml_backend_sched_compute_splits(ggml_backend_sched_t s if (!sched->callback_eval) { enum ggml_status ec = ggml_backend_graph_compute_async(split_backend, &split->graph); if (ec != GGML_STATUS_SUCCESS) { + ggml_san_split(-1, NULL, 0); return ec; } } else { @@ -1824,6 +1970,7 @@ static enum ggml_status ggml_backend_sched_compute_splits(ggml_backend_sched_t s enum ggml_status ec = ggml_backend_graph_compute_async(split_backend, &gv); if (ec != GGML_STATUS_SUCCESS) { + ggml_san_split(-1, NULL, 0); return ec; } @@ -1846,6 +1993,10 @@ static enum ggml_status ggml_backend_sched_compute_splits(ggml_backend_sched_t s prev_backend_id = split_backend_id; } + ggml_san_split(-1, NULL, 0); + + sched->needs_rotate = sched->n_copies > 1; + return GGML_STATUS_SUCCESS; } @@ -1873,7 +2024,37 @@ ggml_backend_sched_t ggml_backend_sched_new( sched->debug_realloc = GGML_SCHED_DEBUG_REALLOC ? atoi(GGML_SCHED_DEBUG_REALLOC) : sched->debug_realloc; sched->n_backends = n_backends; - sched->n_copies = parallel ? GGML_SCHED_MAX_COPIES : 1; + + bool is_uma = false; + if (!parallel && n_backends >= 2) { + ggml_backend_buffer_type_t cpu_buft = bufts ? bufts[n_backends - 1] + : ggml_backend_get_default_buffer_type(backends[n_backends - 1]); + + if (cpu_buft && ggml_backend_buft_is_host(cpu_buft)) { + for (int b = 0; b < n_backends - 1; b++) { + ggml_backend_dev_props props; + ggml_backend_dev_get_props(ggml_backend_get_device(backends[b]), &props); + if (!props.caps.async) { + continue; + } + if (ggml_backend_supports_buft(backends[b], cpu_buft)) { + is_uma = true; + GGML_LOG_DEBUG("%s: %s computes directly on %s, ring buffering graph inputs\n", + __func__, ggml_backend_name(backends[b]), ggml_backend_buft_name(cpu_buft)); + break; + } + } + } + } + + int n_copies_uma = is_uma ? 2 : 1; + + const char * GGML_SCHED_UMA_RING = getenv("GGML_SCHED_UMA_RING"); + if (GGML_SCHED_UMA_RING && is_uma) { + n_copies_uma = std::min(std::max(atoi(GGML_SCHED_UMA_RING), 1), GGML_SCHED_MAX_COPIES); + } + + sched->n_copies = parallel ? GGML_SCHED_MAX_COPIES : n_copies_uma; // initialize hash table // FIXME: needs to be size*2 to account for leafs (do it in graph_split instead) @@ -1900,6 +2081,8 @@ ggml_backend_sched_t ggml_backend_sched_new( sched->graph_inputs_capacity = GGML_SCHED_MAX_SPLIT_INPUTS; sched->graph_inputs = (struct ggml_tensor **) calloc(sched->graph_inputs_capacity, sizeof(struct ggml_tensor *)); + sched->graph_input_slots = (struct ggml_backend_sched::ggml_backend_sched_input_slots *) calloc( + sched->graph_inputs_capacity, sizeof(sched->graph_input_slots[0])); for (int b = 0; b < n_backends; b++) { sched->backends[b] = backends[b]; @@ -1938,6 +2121,7 @@ void ggml_backend_sched_free(ggml_backend_sched_t sched) { } free(sched->splits); free(sched->graph_inputs); + free(sched->graph_input_slots); free(sched->hv_tensor_backend_ids); free(sched->hv_tensor_copies); free(sched->node_backend_ids); @@ -1993,13 +2177,108 @@ bool ggml_backend_sched_reserve(ggml_backend_sched_t sched, struct ggml_cgraph * return true; } +static void ggml_backend_sched_capture_input_slots(ggml_backend_sched_t sched) { + if (sched->n_copies <= 1) { + return; + } + + for (int i = 0; i < sched->n_graph_inputs; i++) { + struct ggml_tensor * input = sched->graph_inputs[i]; + + const size_t id = hash_id(input); + const int backend_id = tensor_backend_id(input); + + for (int c = 0; c < sched->n_copies; c++) { + struct ggml_tensor * slot = tensor_id_copy(id, backend_id, c); + + sched->graph_input_slots[i].buffer[c] = slot ? slot->buffer : NULL; + sched->graph_input_slots[i].data [c] = slot ? slot->data : NULL; + } + + if (sched->debug) { + GGML_LOG_DEBUG("%s: graph input %d: %s\n", __func__, i, input->name); + } + } +} + +static void ggml_backend_sched_advance_copy(ggml_backend_sched_t sched) { + sched->cur_copy = sched->next_copy; + sched->next_copy = (sched->next_copy + 1) % sched->n_copies; + + if (sched->n_copies <= 1 || !sched->needs_rotate) { + return; + } + + for (int b = 0; b < sched->n_backends; b++) { + if (sched->events[b][sched->cur_copy] != NULL) { + ggml_backend_event_synchronize(sched->events[b][sched->cur_copy]); + } else { + ggml_backend_synchronize(sched->backends[b]); + } + } + + if (sched->debug) { + GGML_LOG_DEBUG("%s: advanced to copy %d/%d\n", __func__, sched->cur_copy, sched->n_copies); + } +} + +static void ggml_backend_sched_point_inputs(ggml_backend_sched_t sched) { + for (int i = 0; i < sched->n_graph_inputs; i++) { + struct ggml_tensor * input = sched->graph_inputs[i]; + + if (sched->graph_input_slots[i].data[sched->cur_copy] == NULL) { + continue; + } + + input->buffer = sched->graph_input_slots[i].buffer[sched->cur_copy]; + input->data = sched->graph_input_slots[i].data [sched->cur_copy]; + } + + for (int i = 0; i < sched->graph.n_nodes; i++) { + struct ggml_tensor * node = sched->graph.nodes[i]; + + for (int j = 0; j < GGML_MAX_SRC; j++) { + struct ggml_tensor * src = node->src[j]; + if (src != NULL && src->view_src != NULL && ggml_backend_sched_graph_input(src) != NULL) { + ggml_backend_sched_reinit_view(src); + } + } + } + + for (int i = 0; i < sched->n_splits; i++) { + sched->splits[i].graph.uid = ggml_graph_next_uid(); + } + + if (sched->debug) { + GGML_LOG_DEBUG("%s: rotated %d graph inputs onto copy %d\n", + __func__, sched->n_graph_inputs, sched->cur_copy); + } +} + +static void ggml_backend_sched_rotate_inputs(ggml_backend_sched_t sched) { + GGML_ASSERT(sched->n_copies > 1); + + ggml_backend_sched_advance_copy(sched); + ggml_backend_sched_point_inputs(sched); +} + +void ggml_backend_sched_prepare_inputs(ggml_backend_sched_t sched) { + GGML_ASSERT(sched); + + if (sched->n_copies <= 1 || !sched->needs_rotate) { + return; + } + + ggml_backend_sched_rotate_inputs(sched); + sched->needs_rotate = false; +} + bool ggml_backend_sched_alloc_graph(ggml_backend_sched_t sched, struct ggml_cgraph * graph) { GGML_ASSERT(sched); GGML_ASSERT((int)sched->hash_set.size >= graph->n_nodes + graph->n_leafs); GGML_ASSERT(!sched->is_alloc); - sched->cur_copy = sched->next_copy; - sched->next_copy = (sched->next_copy + 1) % sched->n_copies; + ggml_backend_sched_advance_copy(sched); ggml_backend_sched_split_graph(sched, graph); @@ -2007,6 +2286,14 @@ bool ggml_backend_sched_alloc_graph(ggml_backend_sched_t sched, struct ggml_cgra return false; } + ggml_backend_sched_capture_input_slots(sched); + + if (sched->n_copies > 1) { + ggml_backend_sched_point_inputs(sched); + } + + sched->needs_rotate = false; + sched->is_alloc = true; return true; @@ -2038,6 +2325,8 @@ void ggml_backend_sched_synchronize(ggml_backend_sched_t sched) { for (int i = 0; i < sched->n_backends; i++) { ggml_backend_synchronize(sched->backends[i]); } + + sched->needs_rotate = false; if (!sched->is_alloc) { // if the graph is not already allocated, always use copy 0 after a synchronization // this ensures that during generation the same copy is used every time, diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 45e9537f0e45..8e26ad2489da 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -2587,6 +2587,31 @@ static const void * ggml_cuda_graph_get_key(ggml_cgraph * cgraph) { return cgraph->nodes[0]; } +static ggml_cuda_graph::node_properties ggml_cuda_graph_node_props(const ggml_tensor * node) { + ggml_cuda_graph::node_properties prop = {}; + memcpy(&prop.node, node, sizeof(ggml_tensor)); + + for (int j = 0; j < GGML_MAX_SRC; ++j) { + if (node->src[j]) { + prop.node_src_data_ptrs[j] = node->src[j]->data; + memcpy(prop.node_src_ne[j], node->src[j]->ne, sizeof(prop.node_src_ne[j])); + memcpy(prop.node_src_nb[j], node->src[j]->nb, sizeof(prop.node_src_nb[j])); + } + } + + return prop; +} + +static bool ggml_cuda_graph_verify_uid() { +#ifndef NDEBUG + return true; +#else + static const bool verify = getenv("GGML_CUDA_GRAPH_VERIFY_UID") != nullptr; + return verify; +#endif +} + +// see docs/development/backend-scheduler.md static bool ggml_cuda_graph_update_required(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph * cgraph) { bool res = false; @@ -2597,6 +2622,16 @@ static bool ggml_cuda_graph_update_required(ggml_backend_cuda_context * cuda_ctx cgraph->uid == graph->uid) { GGML_LOG_DEBUG("CUDA Graph id %zu reused\n", cgraph->uid); GGML_ASSERT((int)graph->node_props.size() == cgraph->n_nodes); + if (ggml_cuda_graph_verify_uid()) { + for (int i = 0; i < cgraph->n_nodes; i++) { + const ggml_cuda_graph::node_properties prop = ggml_cuda_graph_node_props(cgraph->nodes[i]); + if (memcmp(&graph->node_props[i], &prop, sizeof(prop)) != 0) { + GGML_LOG_ERROR("%s: node %d (%s) changed while cgraph->uid stayed %llu\n", + __func__, i, cgraph->nodes[i]->name, (unsigned long long) cgraph->uid); + GGML_ABORT("CUDA graph uid reused after node properties changed - see [TAG_CUDA_GRAPH_UID]"); + } + } + } return false; } @@ -2609,16 +2644,7 @@ static bool ggml_cuda_graph_update_required(ggml_backend_cuda_context * cuda_ctx } for (int i = 0; i < cgraph->n_nodes; i++) { - ggml_cuda_graph::node_properties prop = {}; - memcpy(&prop.node, cgraph->nodes[i], sizeof(ggml_tensor)); - - for (int j = 0; j < GGML_MAX_SRC; ++j) { - if (cgraph->nodes[i]->src[j]) { - prop.node_src_data_ptrs[j] = cgraph->nodes[i]->src[j]->data; - memcpy(prop.node_src_ne[j], cgraph->nodes[i]->src[j]->ne, sizeof(prop.node_src_ne[j])); - memcpy(prop.node_src_nb[j], cgraph->nodes[i]->src[j]->nb, sizeof(prop.node_src_nb[j])); - } - } + const ggml_cuda_graph::node_properties prop = ggml_cuda_graph_node_props(cgraph->nodes[i]); if (res || memcmp(&graph->node_props[i], &prop, sizeof(prop)) != 0) { graph->node_props[i] = prop; diff --git a/src/llama-context.cpp b/src/llama-context.cpp index 38872da6c065..70628813cbc1 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -1364,12 +1364,7 @@ llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch, ll reused = true; //LLAMA_LOG_DEBUG("%s: reusing previous graph\n", __func__); - // with pipeline parallelism, the previous graph_compute_async may still be running - // on the GPU. we must synchronize before set_inputs to avoid overwriting input tensors - // that the previous compute is still reading. - if (cparams.pipeline_parallel) { - ggml_backend_sched_synchronize(sched.get()); - } + ggml_backend_sched_prepare_inputs(sched.get()); n_reused++; } else { diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index 8ea441f441d7..b137c6cfe8a0 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -30,7 +30,8 @@ static ggml_tensor * build_attn_inp_kq_mask( ggml_context * ctx, const llama_kv_cache_context * mctx, const llama_ubatch & ubatch, - const llama_cparams & cparams) { + const llama_cparams & cparams, + const char * name = "attn_inp_kq_mask") { const auto n_kv = mctx->get_n_kv(); const auto n_tokens = ubatch.n_tokens; const auto n_stream = cparams.kv_unified ? 1 : ubatch.n_seqs_unq; @@ -40,7 +41,7 @@ static ggml_tensor * build_attn_inp_kq_mask( ggml_tensor * res = ggml_new_tensor_4d(ctx, type, n_kv, n_tokens/n_stream, 1, n_stream); ggml_set_input(res); - ggml_set_name(res, "attn_inp_kq_mask"); + ggml_set_name(res, name); return res; } @@ -177,7 +178,7 @@ void llm_graph_input_pos_bucket::set_input(const llama_ubatch * ubatch) { if (pos_bucket) { const int64_t n_tokens = ubatch->n_tokens; - GGML_ASSERT(ggml_backend_buffer_is_host(pos_bucket->buffer)); + llama_host_write(pos_bucket); GGML_ASSERT(!ubatch->equal_seqs()); // TODO: use ubatch->n_seqs instead of failing int32_t * data = (int32_t *) pos_bucket->data; @@ -201,7 +202,7 @@ void llm_graph_input_out_ids::set_input(const llama_ubatch * ubatch) { const int64_t n_tokens = ubatch->n_tokens; - GGML_ASSERT(ggml_backend_buffer_is_host(out_ids->buffer)); + llama_host_write(out_ids); int32_t * data = (int32_t *) out_ids->data; if (n_outputs == n_tokens) { @@ -241,7 +242,7 @@ void llm_graph_input_mean::set_input(const llama_ubatch * ubatch) { const int64_t n_seqs_unq = ubatch->n_seqs_unq; GGML_ASSERT(mean); - GGML_ASSERT(ggml_backend_buffer_is_host(mean->buffer)); + llama_host_write(mean); float * data = (float *) mean->data; memset(mean->data, 0, n_tokens*n_seqs_unq*ggml_element_size(mean)); @@ -287,7 +288,7 @@ void llm_graph_input_cls::set_input(const llama_ubatch * ubatch) { cparams.pooling_type == LLAMA_POOLING_TYPE_LAST )) { GGML_ASSERT(cls); - GGML_ASSERT(ggml_backend_buffer_is_host(cls->buffer)); + llama_host_write(cls); uint32_t * data = (uint32_t *) cls->data; memset(cls->data, 0, n_seqs_unq*ggml_element_size(cls)); @@ -332,7 +333,7 @@ void llm_graph_input_rs::set_input(const llama_ubatch * ubatch) { const int64_t n_rs = mctx->get_n_rs(); if (s_copy) { - GGML_ASSERT(ggml_backend_buffer_is_host(s_copy->buffer)); + llama_host_write(s_copy); int32_t * data = (int32_t *) s_copy->data; // assuming copy destinations ALWAYS happen ONLY on the cells between head and head+n @@ -449,7 +450,7 @@ void llm_graph_input_attn_no_cache::set_input(const llama_ubatch * ubatch) { }; GGML_ASSERT(self_kq_mask); - GGML_ASSERT(ggml_backend_buffer_is_host(self_kq_mask->buffer)); + llama_host_write(self_kq_mask); if (self_kq_mask->type == GGML_TYPE_F16) { fill_mask((ggml_fp16_t *) self_kq_mask->data, ggml_nelements(self_kq_mask), 0, LLAMA_SWA_TYPE_NONE); } else { @@ -458,7 +459,7 @@ void llm_graph_input_attn_no_cache::set_input(const llama_ubatch * ubatch) { if (hparams.swa_type != LLAMA_SWA_TYPE_NONE) { GGML_ASSERT(self_kq_mask_swa); - GGML_ASSERT(ggml_backend_buffer_is_host(self_kq_mask_swa->buffer)); + llama_host_write(self_kq_mask_swa); if (self_kq_mask_swa->type == GGML_TYPE_F16) { fill_mask((ggml_fp16_t *) self_kq_mask_swa->data, ggml_nelements(self_kq_mask_swa), hparams.n_swa, hparams.swa_type); } else { @@ -774,7 +775,7 @@ static void dsv4_set_kq_mask( GGML_ASSERT(dst->ne[2] == 1); GGML_ASSERT(dst->ne[3] == n_stream); GGML_ASSERT((int64_t) plan.n_visible.size() == (int64_t) n_tokens); - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); if (dst->type == GGML_TYPE_F32) { float * data = (float *) dst->data; @@ -817,7 +818,7 @@ static ggml_tensor * dsv4_build_raw_kq_mask( ggml_tensor * res = ggml_new_tensor_4d(ctx, type, n_kv, n_tokens/n_stream, 1, n_stream); ggml_set_input(res); - ggml_set_name(res, "attn_inp_kq_mask"); + ggml_set_name(res, "dsv4_raw_attn_inp_kq_mask"); return res; } @@ -1056,7 +1057,7 @@ void llm_graph_input_attn_cross::set_input(const llama_ubatch * ubatch) { const int64_t n_enc = cross_kq_mask->ne[0]; const int64_t n_tokens = ubatch->n_tokens; - GGML_ASSERT(ggml_backend_buffer_is_host(cross_kq_mask->buffer)); + llama_host_write(cross_kq_mask); GGML_ASSERT(!ubatch->equal_seqs()); // TODO: use ubatch->n_seqs instead of failing const auto fill_mask = [&](auto * data) { @@ -1103,7 +1104,7 @@ void llm_graph_input_mem_hybrid::set_input(const llama_ubatch * ubatch) { const int64_t n_rs = mctx->get_recr()->get_n_rs(); if (inp_rs->s_copy) { - GGML_ASSERT(ggml_backend_buffer_is_host(inp_rs->s_copy->buffer)); + llama_host_write(inp_rs->s_copy); int32_t * data = (int32_t *) inp_rs->s_copy->data; // assuming copy destinations ALWAYS happen ONLY on the cells between head and head+n @@ -1147,7 +1148,7 @@ void llm_graph_input_mem_hybrid_k::set_input(const llama_ubatch * ubatch) { const int64_t n_rs = mctx->get_recr()->get_n_rs(); if (inp_rs->s_copy) { - GGML_ASSERT(ggml_backend_buffer_is_host(inp_rs->s_copy->buffer)); + llama_host_write(inp_rs->s_copy); int32_t * data = (int32_t *) inp_rs->s_copy->data; // assuming copy destinations ALWAYS happen ONLY on the cells between head and head+n @@ -1221,7 +1222,7 @@ void llm_graph_input_mem_hybrid_iswa::set_input(const llama_ubatch * ubatch) { const int64_t n_rs = mctx->get_recr()->get_n_rs(); if (inp_rs->s_copy) { - GGML_ASSERT(ggml_backend_buffer_is_host(inp_rs->s_copy->buffer)); + llama_host_write(inp_rs->s_copy); int32_t * data = (int32_t *) inp_rs->s_copy->data; // assuming copy destinations ALWAYS happen ONLY on the cells between head and head+n @@ -2399,6 +2400,7 @@ ggml_tensor * llm_graph_context::build_inp_pos() const { cur = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, (int64_t)n_tokens*hparams.n_pos_per_embd()); ggml_set_input(cur); + cb(cur, "inp_pos", -1); res->add_input(std::move(inp)); @@ -2435,6 +2437,7 @@ ggml_tensor * llm_graph_context::build_inp_out_ids() const { cur = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_outputs); ggml_set_input(cur); + cb(cur, "inp_out_ids", -1); res->add_input(std::move(inp)); @@ -2448,6 +2451,7 @@ ggml_tensor * llm_graph_context::build_inp_mean() const { cur = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_tokens, ubatch.n_seqs_unq); ggml_set_input(cur); + cb(cur, "inp_mean", -1); res->add_input(std::move(inp)); @@ -2461,6 +2465,7 @@ ggml_tensor * llm_graph_context::build_inp_cls() const { cur = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, ubatch.n_seqs_unq); ggml_set_input(cur); + cb(cur, "inp_cls", -1); res->add_input(std::move(inp)); @@ -2485,6 +2490,7 @@ ggml_tensor * llm_graph_context::build_inp_cross_embd() const { cur = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_embd, n_enc); ggml_set_input(cur); + cb(cur, "inp_cross_embd", -1); res->add_input(std::move(inp)); @@ -2498,6 +2504,7 @@ ggml_tensor * llm_graph_context::build_inp_pos_bucket_enc() const { cur = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, n_tokens, n_tokens); ggml_set_input(cur); + cb(cur, "inp_pos_bucket_enc", -1); res->add_input(std::move(inp)); @@ -2515,6 +2522,7 @@ ggml_tensor * llm_graph_context::build_inp_pos_bucket_dec() const { cur = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, n_kv, n_tokens); ggml_set_input(cur); + cb(cur, "inp_pos_bucket_dec", -1); res->add_input(std::move(inp)); @@ -2683,12 +2691,14 @@ llm_graph_input_attn_no_cache * llm_graph_context::build_attn_inp_no_cache() con // note: there is no KV cache, so the number of KV values is equal to the number of tokens in the batch inp->self_kq_mask = ggml_new_tensor_4d(ctx0, type_mask, n_tokens, n_tokens, 1, 1); ggml_set_input(inp->self_kq_mask); + cb(inp->self_kq_mask, "attn_inp_kq_mask", -1); inp->self_kq_mask_cnv = inp->self_kq_mask; if (hparams.swa_type != LLAMA_SWA_TYPE_NONE) { inp->self_kq_mask_swa = ggml_new_tensor_4d(ctx0, type_mask, n_tokens, n_tokens, 1, 1); ggml_set_input(inp->self_kq_mask_swa); + cb(inp->self_kq_mask_swa, "attn_inp_kq_mask_swa", -1); inp->self_kq_mask_swa_cnv = inp->self_kq_mask_swa; } else { @@ -3186,6 +3196,7 @@ llm_graph_input_attn_cross * llm_graph_context::build_attn_inp_cross() const { inp->cross_kq_mask = ggml_new_tensor_4d(ctx0, type_mask, n_enc, n_tokens, 1, 1); ggml_set_input(inp->cross_kq_mask); + cb(inp->cross_kq_mask, "attn_inp_cross_kq_mask", -1); inp->cross_kq_mask_cnv = inp->cross_kq_mask; @@ -3247,7 +3258,7 @@ static std::unique_ptr build_attn_inp_k_dsa_impl( { inp->self_k_idxs_mla = mctx_cur->get_mla()->build_input_k_idxs(ctx0, ubatch); - inp->self_kq_mask_mla = build_attn_inp_kq_mask(ctx0, mctx_cur->get_mla(), ubatch, cparams); + inp->self_kq_mask_mla = build_attn_inp_kq_mask(ctx0, mctx_cur->get_mla(), ubatch, cparams, "attn_inp_kq_mask_mla"); inp->self_kq_mask_mla_cnv = inp->self_kq_mask_mla; } @@ -3258,7 +3269,7 @@ static std::unique_ptr build_attn_inp_k_dsa_impl( auto cparams_copy = cparams; cparams_copy.flash_attn = cparams.fused_lid; - inp->self_kq_mask_lid = build_attn_inp_kq_mask(ctx0, mctx_cur->get_lid(), ubatch, cparams_copy); + inp->self_kq_mask_lid = build_attn_inp_kq_mask(ctx0, mctx_cur->get_lid(), ubatch, cparams_copy, "attn_inp_kq_mask_lid"); inp->self_kq_mask_lid_cnv = inp->self_kq_mask_lid; inp->self_k_rot_lid = mctx_cur->get_lid()->build_input_k_rot(ctx0); @@ -3343,7 +3354,7 @@ llm_graph_input_attn_kv_iswa * llm_graph_context::build_attn_inp_kv_iswa() const inp->self_k_idxs_swa = mctx_cur->get_swa()->build_input_k_idxs(ctx0, ubatch); inp->self_v_idxs_swa = mctx_cur->get_swa()->build_input_v_idxs(ctx0, ubatch); - inp->self_kq_mask_swa = build_attn_inp_kq_mask(ctx0, mctx_cur->get_swa(), ubatch, cparams); + inp->self_kq_mask_swa = build_attn_inp_kq_mask(ctx0, mctx_cur->get_swa(), ubatch, cparams, "attn_inp_kq_mask_swa"); inp->self_kq_mask_swa_cnv = inp->self_kq_mask_swa; } @@ -3373,7 +3384,7 @@ llm_graph_input_attn_k_iswa * llm_graph_context::build_attn_inp_k_iswa() const { inp->self_k_idxs_swa = mctx_cur->get_swa()->build_input_k_idxs(ctx0, ubatch); - inp->self_kq_mask_swa = build_attn_inp_kq_mask(ctx0, mctx_cur->get_swa(), ubatch, cparams); + inp->self_kq_mask_swa = build_attn_inp_kq_mask(ctx0, mctx_cur->get_swa(), ubatch, cparams, "attn_inp_kq_mask_swa"); inp->self_kq_mask_swa_cnv = inp->self_kq_mask_swa; } @@ -3459,6 +3470,7 @@ static std::unique_ptr build_rs_inp_impl( inp->s_copy = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_rs); ggml_set_input(inp->s_copy); + ggml_set_name(inp->s_copy, "inp_s_copy"); inp->s_copy_main = ggml_view_1d(ctx0, inp->s_copy, n_seqs, 0); inp->s_copy_extra = ggml_view_1d(ctx0, inp->s_copy, n_rs - n_seqs, n_seqs * inp->s_copy->nb[0]); @@ -3575,7 +3587,7 @@ llm_graph_input_mem_hybrid_iswa * llm_graph_context::build_inp_mem_hybrid_iswa() inp_attn->self_k_idxs_swa = attn_ctx->get_swa()->build_input_k_idxs(ctx0, ubatch); inp_attn->self_v_idxs_swa = attn_ctx->get_swa()->build_input_v_idxs(ctx0, ubatch); - inp_attn->self_kq_mask_swa = build_attn_inp_kq_mask(ctx0, attn_ctx->get_swa(), ubatch, cparams); + inp_attn->self_kq_mask_swa = build_attn_inp_kq_mask(ctx0, attn_ctx->get_swa(), ubatch, cparams, "attn_inp_kq_mask_swa"); inp_attn->self_kq_mask_swa_cnv = inp_attn->self_kq_mask_swa; } diff --git a/src/llama-impl.h b/src/llama-impl.h index 4988b06d2ca0..b938f801c177 100644 --- a/src/llama-impl.h +++ b/src/llama-impl.h @@ -1,6 +1,7 @@ #pragma once #include "ggml.h" // for ggml_log_level +#include "ggml-backend.h" #include #include @@ -93,6 +94,12 @@ struct buffer_view { } }; +// see docs/development/backend-scheduler.md +static inline void llama_host_write(struct ggml_tensor * t) { + GGML_ASSERT(ggml_backend_buffer_is_host(t->buffer)); + ggml_backend_tensor_set_direct(t, 0, ggml_nbytes(t)); +} + void replace_all(std::string & s, const std::string & search, const std::string & replace); // TODO: rename to llama_format ? diff --git a/src/llama-kv-cache-dsv4.cpp b/src/llama-kv-cache-dsv4.cpp index 948d08146fbf..9e82a6198a09 100644 --- a/src/llama-kv-cache-dsv4.cpp +++ b/src/llama-kv-cache-dsv4.cpp @@ -1912,6 +1912,7 @@ ggml_tensor * llama_kv_cache_dsv4_raw_context::build_input_k_idxs(ggml_context * ggml_tensor * k_idxs = ggml_new_tensor_1d(ctx, GGML_TYPE_I64, n_tokens); ggml_set_input(k_idxs); + ggml_set_name(k_idxs, "dsv4_raw_attn_inp_k_idxs"); return k_idxs; } diff --git a/src/llama-kv-cache-msa.cpp b/src/llama-kv-cache-msa.cpp index 55ef286cafa7..24b25917efd2 100644 --- a/src/llama-kv-cache-msa.cpp +++ b/src/llama-kv-cache-msa.cpp @@ -273,7 +273,7 @@ uint32_t llama_kv_cache_msa_context::get_n_pos() const { } void llama_kv_cache_msa_context::set_input_cell_pos(ggml_tensor * dst, const llama_ubatch * ubatch, int32_t div) const { - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); GGML_ASSERT(dst->type == GGML_TYPE_I32); GGML_ASSERT(div > 0); @@ -302,7 +302,7 @@ void llama_kv_cache_msa_context::set_input_cell_pos(ggml_tensor * dst, const lla } void llama_kv_cache_msa_context::set_input_pos_slot(ggml_tensor * dst, const llama_ubatch * ubatch) const { - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); GGML_ASSERT(dst->type == GGML_TYPE_I32 || dst->type == GGML_TYPE_F32); const int64_t n_tokens = ubatch->n_tokens; @@ -346,7 +346,7 @@ void llama_kv_cache_msa_context::set_input_pos_slot(ggml_tensor * dst, const lla } void llama_kv_cache_msa_context::set_input_pos_mask(ggml_tensor * dst, const llama_ubatch * ubatch) const { - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); GGML_ASSERT(dst->type == GGML_TYPE_F32); const int64_t n_tokens = ubatch->n_tokens; diff --git a/src/llama-kv-cache.cpp b/src/llama-kv-cache.cpp index a342ee1191d4..701d5cdf4034 100644 --- a/src/llama-kv-cache.cpp +++ b/src/llama-kv-cache.cpp @@ -1412,6 +1412,7 @@ ggml_tensor * llama_kv_cache::build_input_k_idxs(ggml_context * ctx, const llama ggml_tensor * k_idxs = ggml_new_tensor_1d(ctx, GGML_TYPE_I64, n_tokens); ggml_set_input(k_idxs); + ggml_set_name(k_idxs, "attn_inp_k_idxs"); return k_idxs; } @@ -1428,6 +1429,7 @@ ggml_tensor * llama_kv_cache::build_input_v_idxs(ggml_context * ctx, const llama } ggml_set_input(v_idxs); + ggml_set_name(v_idxs, "attn_inp_v_idxs"); return v_idxs; } @@ -1477,7 +1479,7 @@ void llama_kv_cache::set_input_k_idxs(ggml_tensor * dst, const llama_ubatch * ub const uint32_t n_tokens = ubatch->n_tokens; GGML_ASSERT(n_tokens == (int64_t) sinfo.size()*sinfo.n_stream()); - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); int64_t * data = (int64_t *) dst->data; for (uint32_t s = 0; s < sinfo.n_stream(); ++s) { @@ -1493,7 +1495,7 @@ void llama_kv_cache::set_input_v_idxs(ggml_tensor * dst, const llama_ubatch * ub const uint32_t n_tokens = ubatch->n_tokens; GGML_ASSERT(n_tokens == (int64_t) sinfo.size()*sinfo.n_stream()); - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); int64_t * data = (int64_t *) dst->data; if (!v_trans) { @@ -1523,7 +1525,7 @@ void llama_kv_cache::set_input_v_idxs(ggml_tensor * dst, const llama_ubatch * ub } void llama_kv_cache::set_input_k_shift(ggml_tensor * dst) const { - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); int32_t * data = (int32_t *) dst->data; @@ -1744,7 +1746,7 @@ static void set_input_kq_mask_impl(const args_set_input_kq_mask & args, T * data void llama_kv_cache::set_input_kq_mask(ggml_tensor * dst, const llama_ubatch * ubatch, bool causal_attn) const { const uint32_t n_tokens = ubatch->n_tokens; - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); const int64_t n_kv = dst->ne[0]; const int64_t n_stream = dst->ne[3]; // num streams in the current ubatch @@ -1791,7 +1793,7 @@ void llama_kv_cache::set_input_pos_bucket(ggml_tensor * dst, const llama_ubatch GGML_ASSERT(n_stream == 1 && "TODO: support multiple streams"); const auto & cells = v_cells[0]; - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); GGML_ASSERT(!ubatch->equal_seqs()); // TODO: use ubatch->n_seqs instead of failing int32_t * data = (int32_t *) dst->data; @@ -1811,7 +1813,7 @@ void llama_kv_cache::set_input_pos_bucket(ggml_tensor * dst, const llama_ubatch } void llama_kv_cache::set_input_k_rot(ggml_tensor * dst) const { - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); const auto n_rot = dst->ne[0]; GGML_ASSERT(attn_rot_hadamard.count(dst->ne[0])); @@ -1820,7 +1822,7 @@ void llama_kv_cache::set_input_k_rot(ggml_tensor * dst) const { } void llama_kv_cache::set_input_v_rot(ggml_tensor * dst) const { - GGML_ASSERT(ggml_backend_buffer_is_host(dst->buffer)); + llama_host_write(dst); const auto n_rot = dst->ne[0]; GGML_ASSERT(attn_rot_hadamard.count(dst->ne[0])); @@ -2009,6 +2011,7 @@ ggml_cgraph * llama_kv_cache::build_graph_shift(llm_graph_result * res, llama_co inp->k_shift = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, (int64_t) get_size()*n_stream); ggml_set_input(inp->k_shift); + ggml_set_name(inp->k_shift, "inp_k_shift"); inp->k_rot = build_input_k_rot(ctx); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c46377c7623d..93a8d7f6ebb5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -370,6 +370,7 @@ if (NOT LLAMA_USE_SYSTEM_GGML) # Needs non-public ggml{,-backend}-impl.h llama_build_and_test(test-alloc.cpp) + llama_build_and_test(test-backend-sched-ring.cpp) endif() llama_build(test-export-graph-ops.cpp) diff --git a/tests/test-alloc.cpp b/tests/test-alloc.cpp index 8f1a98aa03c3..219eb5a4661c 100644 --- a/tests/test-alloc.cpp +++ b/tests/test-alloc.cpp @@ -650,6 +650,41 @@ static void test_graph_optimize_alloc_dep() { GGML_ASSERT(!graph_reuses_allocation(true)); } +static void test_pinned_no_inplace() { + dummy_backend backend = dummy_backend_init(SIZE_MAX); + auto [ctx, graph, ctx_ptr] = make_context(); + + ggml_tensor * input = make_input_1d(ctx, 4); + ggml_tensor * parent = ggml_scale(ctx, input, 2.0f); + ggml_tensor * child = ggml_scale(ctx, parent, 3.0f); + + ggml_set_output(child); + ggml_build_forward_expand(graph, child); + + ggml_gallocr_ptr galloc(ggml_gallocr_new(&backend.buffer_type)); + ggml_gallocr_pin_tensor(galloc.get(), parent); + GGML_ASSERT(ggml_gallocr_alloc_graph(galloc.get(), graph)); + GGML_ASSERT(!memory_overlap(parent, child)); +} + +static void test_pinned_view_root_no_inplace() { + dummy_backend backend = dummy_backend_init(SIZE_MAX); + auto [ctx, graph, ctx_ptr] = make_context(); + + ggml_tensor * input = make_input_1d(ctx, 4); + ggml_tensor * root = ggml_scale(ctx, input, 2.0f); + ggml_tensor * view = ggml_view_1d(ctx, root, 4, 0); + ggml_tensor * child = ggml_scale(ctx, view, 3.0f); + + ggml_set_output(child); + ggml_build_forward_expand(graph, child); + + ggml_gallocr_ptr galloc(ggml_gallocr_new(&backend.buffer_type)); + ggml_gallocr_pin_tensor(galloc.get(), root); + GGML_ASSERT(ggml_gallocr_alloc_graph(galloc.get(), graph)); + GGML_ASSERT(!memory_overlap(root, child)); +} + static void run(const char * name, void (*f)()) { printf("%s ", name); fflush(stdout); @@ -672,5 +707,7 @@ int main() { run("test_buffer_size_zero", test_buffer_size_zero); run("test_reallocation", test_reallocation); run("test_graph_optimize_alloc_dep", test_graph_optimize_alloc_dep); + run("test_pinned_no_inplace", test_pinned_no_inplace); + run("test_pinned_view_root_no_inplace", test_pinned_view_root_no_inplace); return 0; } diff --git a/tests/test-backend-sched-ring.cpp b/tests/test-backend-sched-ring.cpp new file mode 100644 index 000000000000..7047233206b5 --- /dev/null +++ b/tests/test-backend-sched-ring.cpp @@ -0,0 +1,80 @@ +#include "ggml-backend.h" +#include "../ggml/src/ggml-backend-impl.h" +#include "ggml-cpp.h" +#include "ggml.h" + +struct test_backend_context { + int synchronize_count = 0; +}; + +static const char * test_backend_name(ggml_backend_t) { + return "test"; +} + +static void test_backend_synchronize(ggml_backend_t backend) { + auto * context = static_cast(backend->context); + context->synchronize_count++; +} + +static ggml_status test_backend_graph_compute(ggml_backend_t, ggml_cgraph *) { + return GGML_STATUS_SUCCESS; +} + +static const char * test_device_name(ggml_backend_dev_t) { + return "test"; +} + +static enum ggml_backend_dev_type test_device_type(ggml_backend_dev_t) { + return GGML_BACKEND_DEVICE_TYPE_CPU; +} + +static bool test_device_supports_op(ggml_backend_dev_t, const ggml_tensor *) { + return true; +} + +static bool test_device_supports_buft(ggml_backend_dev_t, ggml_backend_buffer_type_t buft) { + return buft == ggml_backend_cpu_buffer_type(); +} + +int main() { + test_backend_context context; + + ggml_backend_device device = {}; + device.iface.get_name = test_device_name; + device.iface.get_type = test_device_type; + device.iface.supports_op = test_device_supports_op; + device.iface.supports_buft = test_device_supports_buft; + + ggml_backend backend = {}; + backend.iface.get_name = test_backend_name; + backend.iface.synchronize = test_backend_synchronize; + backend.iface.graph_compute = test_backend_graph_compute; + backend.device = &device; + backend.context = &context; + + ggml_backend_t backends[] = { &backend }; + ggml_backend_buffer_type_t bufts[] = { ggml_backend_cpu_buffer_type() }; + ggml_backend_sched_ptr sched(ggml_backend_sched_new(backends, bufts, 1, 16, false, false)); + + ggml_init_params params = {}; + params.mem_size = 4*ggml_tensor_overhead() + ggml_graph_overhead(); + params.no_alloc = true; + ggml_context_ptr ctx(ggml_init(params)); + + ggml_tensor * input = ggml_new_tensor_1d(ctx.get(), GGML_TYPE_F32, 4); + ggml_set_input(input); + ggml_tensor * output = ggml_scale(ctx.get(), input, 2.0f); + ggml_set_output(output); + + ggml_cgraph * graph = ggml_new_graph(ctx.get()); + ggml_build_forward_expand(graph, output); + + GGML_ASSERT(ggml_backend_sched_alloc_graph(sched.get(), graph)); + GGML_ASSERT(ggml_backend_sched_graph_compute_async(sched.get(), graph) == GGML_STATUS_SUCCESS); + GGML_ASSERT(context.synchronize_count == 0); + + ggml_backend_sched_prepare_inputs(sched.get()); + GGML_ASSERT(context.synchronize_count == 0); + + return 0; +} From 7e34e5a0eb27642d6cb2c08e9898a14dca9ff29a Mon Sep 17 00:00:00 2001 From: Marshall Date: Thu, 20 Aug 2026 22:36:06 +0100 Subject: [PATCH 069/212] ROCmFPx: port the AMD-native FPx tensor types (CPU + Vulkan) Ports the experimental ROCmFPx quantization formats from ciru-ai/ROCmFPX @ 54f5fe0 onto current master. That fork has no shared git history and is based on an older llama.cpp, so this is a hand-port of the feature onto upstream's current structure rather than a merge. Six GGUF types are registered in a high ID range (100-104, 107) so upstream type IDs stay free: Q4_0_ROCMFP4 4.50 bpw, dual UE4M3 half-block scales Q4_0_ROCMFP4_FAST 4.25 bpw, single scale per block Q3_0_ROCMFPX 3.50 bpw Q6_0_ROCMFPX 6.50 bpw Q8_0_ROCMFPX 8.25 bpw Q2_0_ROCMFPX 2.50 bpw (CPU only) Included: - reference codecs in ggml/rocmfp4 and ggml/rocmfpx - ggml type traits, quantize dispatch, row-data validation - CPU vec_dot/from_float for all six types - Vulkan: dequant, get_rows, mul_mat_vec, mul_mm, coopmat2 dequant, copy_to/from_quant, set_rows and same-type block copy for the five types above - llama ftypes and the per-tensor quant recipes (ROCmFP4 LEAN/COHERENT/ STRIX variants, ROCmFPx AGENT/LEAN routing), llama-quantize presets - gguf-py type/ftype constants and dequant classes - FPx types added to the test-backend-ops sweep Two bugs in the upstream fork are fixed rather than carried over: - the Vulkan fp6 path used a 32-byte unpacked block layout and stored decoded int8 values, which does not match the 24-byte 6-bit packing written by the CPU encoder. Reimplemented against the real layout. - the CPU fp6 vec_dot decoded the sign|0 code as -0 instead of -32, disagreeing with rocmfpx_decode_fp6_code() and costing ~2% error on every fp6 mat-vec. Not included: HIP/CUDA and OpenCL kernels, the q8_1 integer-dot (MMQ) paths, flash-attention with FPx KV, and the Vulkan fp2 kernels. The HIP sources ship with the reference codecs but are not built. Verified on Radeon 8060S (RADV STRIX_HALO): test-backend-ops passes the full suite, with 857 ROCmFPx cases green. Round-tripped Qwen3-1.7B through llama-quantize to ROCMFP4_FAST/Q8_0/Q6_0/Q3_0_ROCMFPX and got coherent generation on both Vulkan and CPU. Co-Authored-By: Claude Opus 5 (cherry picked from commit 8610c8b36284dbed5f02aa9a429251ecbfa9510d) --- common/arg.cpp | 5 + common/common.cpp | 4 + ggml/include/ggml.h | 21 +- ggml/rocmfp4/README.md | 1020 +++++++++++++ ggml/rocmfp4/rocmfp4.c | 797 ++++++++++ ggml/rocmfp4/rocmfp4.h | 58 + ggml/rocmfp4/rocmfp4_hip.cu | 85 ++ ggml/rocmfp4/rocmfp4_hip_codebook.cuh | 79 + ggml/rocmfp4/rocmfp4_hip_scale.cuh | 157 ++ ggml/rocmfpx/README.md | 109 ++ ggml/rocmfpx/rocmfp2_reference.c | 483 ++++++ ggml/rocmfpx/rocmfp2_reference.h | 151 ++ ggml/rocmfpx/rocmfpx.c | 1354 +++++++++++++++++ ggml/rocmfpx/rocmfpx.h | 104 ++ ggml/rocmfpx/rocmfpx_hip_codebook.cuh | 45 + ggml/rocmfpx/test_rocmfp2_reference.c | 565 +++++++ ggml/rocmfpx/test_rocmfpx.c | 177 +++ ggml/src/CMakeLists.txt | 4 + ggml/src/ggml-common.h | 8 + ggml/src/ggml-cpu/ggml-cpu.c | 200 +++ ggml/src/ggml-cpu/ops.cpp | 42 + ggml/src/ggml-quants.c | 44 + ggml/src/ggml-vulkan/ggml-vulkan.cpp | 168 +- .../vulkan-shaders/copy_from_quant.comp | 2 +- .../vulkan-shaders/copy_quant_same.comp | 55 + .../vulkan-shaders/copy_to_quant.comp | 479 ++++++ .../vulkan-shaders/dequant_funcs.glsl | 143 ++ .../vulkan-shaders/dequant_funcs_cm2.glsl | 164 ++ .../vulkan-shaders/dequant_rocmfp4.comp | 33 + .../vulkan-shaders/dequant_rocmfp4_fast.comp | 31 + .../vulkan-shaders/dequant_rocmfpx_fp3.comp | 46 + .../vulkan-shaders/dequant_rocmfpx_fp6.comp | 44 + .../vulkan-shaders/dequant_rocmfpx_fp8.comp | 31 + .../vulkan-shaders/mul_mm_funcs.glsl | 105 ++ .../src/ggml-vulkan/vulkan-shaders/types.glsl | 146 ++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 21 +- ggml/src/ggml.c | 83 + gguf-py/gguf/constants.py | 29 + gguf-py/gguf/quants.py | 72 + include/llama.h | 18 + src/llama-kv-cache.cpp | 11 + src/llama-model-loader.cpp | 22 + src/llama-quant.cpp | 384 ++++- tests/test-backend-ops.cpp | 22 +- tools/quantize/quantize.cpp | 24 + 45 files changed, 7627 insertions(+), 18 deletions(-) create mode 100644 ggml/rocmfp4/README.md create mode 100644 ggml/rocmfp4/rocmfp4.c create mode 100644 ggml/rocmfp4/rocmfp4.h create mode 100644 ggml/rocmfp4/rocmfp4_hip.cu create mode 100644 ggml/rocmfp4/rocmfp4_hip_codebook.cuh create mode 100644 ggml/rocmfp4/rocmfp4_hip_scale.cuh create mode 100644 ggml/rocmfpx/README.md create mode 100644 ggml/rocmfpx/rocmfp2_reference.c create mode 100644 ggml/rocmfpx/rocmfp2_reference.h create mode 100644 ggml/rocmfpx/rocmfpx.c create mode 100644 ggml/rocmfpx/rocmfpx.h create mode 100644 ggml/rocmfpx/rocmfpx_hip_codebook.cuh create mode 100644 ggml/rocmfpx/test_rocmfp2_reference.c create mode 100644 ggml/rocmfpx/test_rocmfpx.c create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/copy_quant_same.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfp4.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfp4_fast.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp3.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp6.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp8.comp diff --git a/common/arg.cpp b/common/arg.cpp index 72d683045b45..47dacc5857a1 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -311,6 +311,11 @@ const std::vector kv_cache_types = { GGML_TYPE_IQ4_NL, GGML_TYPE_Q5_0, GGML_TYPE_Q5_1, + GGML_TYPE_Q4_0_ROCMFP4, + GGML_TYPE_Q4_0_ROCMFP4_FAST, + GGML_TYPE_Q3_0_ROCMFPX, + GGML_TYPE_Q6_0_ROCMFPX, + GGML_TYPE_Q8_0_ROCMFPX, }; static ggml_type kv_cache_type_from_str(const std::string & s) { diff --git a/common/common.cpp b/common/common.cpp index a6a5364163bc..2588cd6ee182 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -1755,6 +1755,10 @@ struct llama_context_params common_context_params_to_llama(const common_params & cparams.type_k = params.cache_type_k; cparams.type_v = params.cache_type_v; + if (cparams.type_k == GGML_TYPE_Q3_0_ROCMFPX) { + LOG_WRN("%s: q3_0_rocmfpx K cache is below the coherency floor; using q6_0_rocmfpx for K\n", __func__); + cparams.type_k = GGML_TYPE_Q6_0_ROCMFPX; + } return cparams; } diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h index 7a05f1ef5fca..00f682f7bf80 100644 --- a/ggml/include/ggml.h +++ b/ggml/include/ggml.h @@ -430,7 +430,15 @@ extern "C" { GGML_TYPE_NVFP4 = 40, // NVFP4 (4 blocks, E4M3 scale) GGML_TYPE_Q1_0 = 41, GGML_TYPE_Q2_0 = 42, - GGML_TYPE_COUNT = 43, + // ROCmFPx experimental AMD-native formats. These are kept in a high ID + // range so upstream type IDs stay free for future ggml types. + GGML_TYPE_Q4_0_ROCMFP4 = 100, // ROCmFP4 experimental UE4M3 scales + packed AMD FP4 blocks + GGML_TYPE_Q4_0_ROCMFP4_FAST = 101, // ROCmFP4 single-scale speed layout + GGML_TYPE_Q6_0_ROCMFPX = 102, // ROCmFPx experimental 6-bit UE4M3-scale reference layout + GGML_TYPE_Q8_0_ROCMFPX = 103, // ROCmFPx experimental 8-bit UE4M3-scale reference layout + GGML_TYPE_Q3_0_ROCMFPX = 104, // ROCmFPx experimental 3-bit UE4M3-scale reference layout + GGML_TYPE_Q2_0_ROCMFPX = 107, // ROCmFPx experimental 2-bit S40 codebook + dual UE4M3 scales + GGML_TYPE_COUNT = 108, }; // precision @@ -476,6 +484,17 @@ extern "C" { GGML_FTYPE_MOSTLY_NVFP4 = 26, // except 1d tensors GGML_FTYPE_MOSTLY_Q1_0 = 27, // except 1d tensors GGML_FTYPE_MOSTLY_Q2_0 = 28, // except 1d tensors + GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4 = 100, // except 1d tensors + GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN = 101, // ROCmFP4 with Q5_K token embeddings + GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_COHERENT = 102, // ROCmFP4 with Q6_K token embeddings + GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST = 103, // ROCmFP4 single-scale speed layout + GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT = 104, // ROCmFP4 fast with Q6_K token embeddings + GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX = 105, // ROCmFP4 Strix Halo quality/speed recipe + GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN = 106, // ROCmFP4 Strix Halo size-biased K/V recipe + GGML_FTYPE_MOSTLY_Q6_0_ROCMFPX = 110, // ROCmFPx experimental 6-bit reference layout + GGML_FTYPE_MOSTLY_Q8_0_ROCMFPX = 111, // ROCmFPx experimental 8-bit reference layout + GGML_FTYPE_MOSTLY_Q3_0_ROCMFPX = 112, // ROCmFPx experimental 3-bit reference layout + GGML_FTYPE_MOSTLY_Q2_0_ROCMFPX = 113, // ROCmFPx experimental 2-bit S40 codebook layout }; // available tensor operations: diff --git a/ggml/rocmfp4/README.md b/ggml/rocmfp4/README.md new file mode 100644 index 000000000000..39bb79302cb2 --- /dev/null +++ b/ggml/rocmfp4/README.md @@ -0,0 +1,1020 @@ +# ROCmFP4 + +ROCmFP4 is the experimental `Q4_0_ROCMFP4` GGUF tensor type for Strix Halo. +The current AMD-tuned variant stores 32 weights per block as packed +E2M1-derived 4-bit values plus two unsigned E4M3 scale bytes, one per +16-weight half, for 18 bytes per block. The codebook stores half-scale signed +integer levels up to `10`, representing `5.0` raw-scale units after the scale +factor is applied. This keeps outlier pull lower than the original wider test +range while preserving a fast integer-dot backend shape. + +This directory owns the format-specific implementation. The rest of ggml only +registers and dispatches the type. + +Current status: +- The format runs on CPU, Vulkan, and ROCm/HIP in this custom tree. +- `Q4_0_ROCMFP4` is the pure 4.50 BPW dual-scale path. +- `Q4_0_ROCMFP4_LEAN` keeps ROCmFP4 for dense tensors but protects token + embeddings with `Q5_K`. On the Qwen3-4B Strix test pass this closed most of + the Q4_0 coherence gap while staying around normal Q4 size. +- `Q4_0_ROCMFP4_COHERENT` protects token embeddings with `Q6_K` and is the + quality-first ROCmFP4 preset. +- `Q4_0_ROCMFP4_FAST` is the 4.25 BPW single-scale speed path. It is the + smallest and fastest decode variant, but the pure version gives up too much + PPL to be the default quality target. +- `Q4_0_ROCMFP4_FAST_COHERENT` combines the fast 4.25 BPW transformer layout + with `Q6_K` token embeddings. On the Qwen3-4B Strix test pass it is the + current balanced AMD target: smaller than `Q4_0`, faster than `Q4_0` on + decode on both Vulkan and ROCm, and close to `Q4_0` PPL on the short + WikiText-2 check. +- `Q4_0_ROCMFP4_STRIX` is the current quality-biased Strix Halo preset. It + keeps most transformer tensors on `Q4_0_ROCMFP4_FAST`, protects token + embeddings with `Q6_K`, and uses the dual-scale `Q4_0_ROCMFP4` layout for + attention-K and attention-V tensors. On Qwen3-4B it improved the short + WikiText-2 PPL to `13.8865` at `4.49 BPW` while still beating the + same-flags stock `Q4_0` decode baselines on both Vulkan and ROCm. +- `Q4_0_ROCMFP4_STRIX_LEAN` is the compact Strix Halo preset. It keeps the + STRIX all-layer dual-scale attention-K/V protection, uses the FAST + single-scale transformer layout for the dense tensors, and protects token + embeddings/output with `Q5_K` instead of `Q6_K`. On the Qwen3-4B validation + pass it landed at `4.38 BPW`, improved short WikiText-2 PPL versus + `FAST_COHERENT`, and kept Vulkan decode in the `81 tok/s` band. +- A smaller first/last-layer-only K/V protection recipe was tested but not + promoted. It reached `4.48 BPW`, `80.13` Vulkan decode, and `69.85` ROCm + decode, but PPL regressed to `14.0167`, so the all-layer K/V STRIX preset + remains the quality target. +- The ROCm/HIP MMQ path for `Q4_0_ROCMFP4_FAST` uses one scale per 32-weight + block, matching the actual FAST layout instead of duplicating the scale into + two half-block slots. +- The ROCm/HIP vector-dot and MMQ loaders use a ROCmFP4-owned Codebook10 + expander backed by AMD `amdgcn_perm` constants. This avoids the generic + table-load helper on the hot ROCm path. +- The ROCm/HIP hot paths use a ROCmFP4-owned unaligned 32-bit quant-byte load + for packed nibble bytes. ROCmFP4 blocks are 17 or 18 bytes wide, so the older + byte-safe assembly path was conservative but expensive on Strix Halo HIP. + Direct unaligned dword loads improved ROCm `MUL_MAT` and FlashAttention + microbenchmarks while the Qwen3.6 27B MTP guard held `33.4 tok/s` short and + `27.7 tok/s` sustained. It is enabled by default and can be disabled with + `-DGGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD=0` for isolation. +- The ROCm/HIP FAST MMVQ/MMQ path now uses the same ROCmFP4-owned unaligned + quant-byte load instead of the generic byte-assembly helper. The full + promoted gate measured FAST ROCm `MUL_MAT` at `45.17`, `58.38`, `90.54`, + and `157.83` us for `n=1/2/4/8`, and the Qwen3.6 27B MTP guard improved to + `33.6 tok/s` short and `28.0 tok/s` sustained. The ROCm runtime guard now + tightens the FAST ceilings to protect this band. +- Qwen3.6 35B A3B MTP ROCmFP4 STRIX_LEAN was checked separately at native + `262144` context on ROCm0. In the reasoning-off discovery sweep, `n-max 1` + was best sustained at `72.2 tok/s`, while `n-max 5` was best burst at + `107.3 tok/s` but slower sustained at `64.9 tok/s`. With reasoning on, a + follow-up sweep found `n-max 2` best sustained at `92.6 tok/s` short and + `80.6 tok/s` sustained; `n-max 3` was close at `104.3` short and `80.1` + sustained, while `n-max 5` stayed burst-only at `98.6` short and `73.3` + sustained. KV-cache isolation then found that q8 main KV with q4 draft KV + improved the reasoning-on `n-max 2` profile to `93.7 tok/s` short and + `85.6 tok/s` sustained; full q8 main/draft KV reached `93.2` / `85.2`, + draft-only q8 stayed at `80.5` sustained, and K-only / V-only q8 main KV + regressed to `71.8` / `73.3` sustained. Rechecking draft depth under q8 main + KV moved the best sustained profile to `n-max 3` at `104.3 tok/s` short and + `89.3 tok/s` sustained, while `n-max 4` became the best burst-only profile + at `111.2 tok/s` short and `78.7 tok/s` sustained. The Pi sustained profile + therefore uses `n-max 3` with q8 main KV and q4 draft KV, while a separate + `n-max 4` burst profile is available for short-response experimentation. + The older `n-max 5` burst alias remains available for comparison. + Batch and CPU thread follow-ups did not beat the promoted runtime shape: + `-b 1024 -ub 512`, `-b 2048 -ub 512`, `-b 512 -ub 256`, + `-t 24 -tb 32`, and `-t 12 -tb 32` all measured in the `88.7`-`89.1 tok/s` + sustained band, so `-b 512 -ub 512 -t 16 -tb 32` stays promoted. + Current KV isolation rechecked the promoted shape at `104.3 tok/s` short and + `90.1 tok/s` sustained with q8 main KV, q4 draft KV, `n-max 3`, and + `p-min 0.25`; full q8 draft KV tied at `104.5` / `90.0`, while draft-only + q8 reached only `82.2` sustained. K-only and V-only q8 main KV regressed to + `70.5` and `74.6` sustained, so both accepted K and V need q8 and the draft + KV can stay q4. The updated default 35B guard then passed at `103.3 tok/s` + short and `90.1 tok/s` sustained. + Sampler-chain trims and backend sampling did not beat the promoted sustained + profile: `top_k;top_p;temperature` reached `104.2` / `87.5`, the milder + `penalties;top_k;top_p;min_p;temperature` chain reached `104.1` / `88.9`, + and `--backend-sampling` reached `104.3` / `89.2`, so the default sampler + path stays promoted. + A single-sequence MTP `process()` fast path for `-np 1` was also prototyped, + built, and rejected because the guard dropped to `104.1 tok/s` short and + `88.5 tok/s` sustained; the change was removed. + MoE `rows_per_block` compile-time variants were checked against the same 35B + guard and also rejected: `rows_per_block=4` measured `103.8` short / + `89.1 tok/s` sustained, `rows_per_block=3` measured `86.8 tok/s` sustained, + and `rows_per_block=1` measured `103.6` / `88.7 tok/s`, none beating the + promoted `104.3` / `89.3` band. + The Pi server profile was also started and stopped successfully with + `n_ctx = 262144`, `draft-mtp` initialized, built-in tools enabled, and + `thinking = 1`; ROCm reported no KFD PIDs after shutdown. The promoted + profile is now covered by the focused + `scripts/check-rocmfp4-qwen35-a3b-mtp-regression.sh` guard, which defaults + to `n-max 3`, q8 main KV, q4 draft KV, and `p-min 0.25` with `100.0` short + and `85.0 tok/s` sustained floors. + Follow-up `p-min 0.25/0.50/0.75`, `p-split 0.05/0.20`, and `n-min 1` + checks on the q4-KV baseline all tied the same `80.5`-`80.8 tok/s` sustained + band, while `p-min 0.25` on the q8-main/q4-draft path tied at `85.4 tok/s` + sustained, so the + conservative `n-min 0`, `p-min 0.0`, `p-split 0.10` defaults remained + promoted until the MTP internal sampler was changed from `top_k=1` to + `top_k=10`, making the `p-min` cutoff operate on a meaningful candidate + distribution while the draft loop still selects the top sorted candidate. + A pre-change p-min sweep on the actual promoted `n-max 3`, q8-main/q4-draft + profile measured `104.2` / `89.1` at `p-min 0.25`, `104.1` / `89.0` at + `0.50`, `104.0` / `89.3` at `0.75`, and `104.2` / `89.1` at `0.90`. + Follow-up p-split checks measured `103.7` / `88.9` at `0.05` and + `103.8` / `89.3` at `0.20`; n-min checks measured `103.8` / `88.9` for + `1` and `104.2` / `89.2` for `2`. None beat the promoted + sustained-plus-short profile. Combined `p-min`/`p-split` follow-ups also + tied rather than beat the default: `p-min 0.25` with `p-split 0.30` + repeated at `104.6` / `89.5`, `p-split 0.40` and `0.50` measured + `104.6` / `89.5` and `104.4` / `89.5`, and `p-split 0.70` / `0.90` + measured `104.8` / `89.5` and `104.7` / `89.5`; the same-session default + tied at `104.7` / `89.5`. With `top_k=10`, the 35B A3B profile now promotes + `--spec-draft-p-min 0.25`: default `p-min 0.0` measured `104.3` / `89.3`, + while `p-min 0.25` measured `103.9` / `90.0` after a first sustained run at + `89.8 tok/s`. The dense 27B profile should stay at `p-min 0.0`; the same + `p-min 0.25` filter regressed it to `24.6 tok/s` sustained. The full + all-regression harness can include the 35B guard with + `INCLUDE_QWEN35_A3B_GUARD=1`. + The MTP draft sampler now uses a probability-only top-10 helper for this + path. The draft loop only consumes the sorted top candidate and its + probability, so skipping the unused final RNG sampler selection preserved the + output path while improving the 35B A3B guard to `104.6 tok/s` short and + `90.2 tok/s` sustained. The dense 27B guard remained stable at `33.9` / + `28.1 tok/s`. + A fixed insertion top-10 replacement for `std::partial_sort` was tested and + rejected after the 35B A3B short guard dropped to `73.0 tok/s`, below the + `100.0 tok/s` floor. A `std::nth_element` plus top-k slice sort variant was + also rejected after the same guard dropped to `71.5 tok/s`. + Narrowing the MTP top-k probability accumulator from `double` to `float` was + also rejected: dense 27B still passed at `34.0` / `28.1 tok/s`, but the 35B + A3B short guard repeated below floor at `84.1` then `93.4 tok/s`; restoring + the `double` accumulator recovered the short guard to `104.4 tok/s`. + A `std::partial_sort_copy` top-k buffer variant built and passed the same + guard at `104.4 tok/s` short and `90.2 tok/s` sustained, but a same-session + promoted-build comparison measured `104.1` / `90.2`, so it was not promoted: + it added sampler buffer/clone complexity without a sustained decode gain. + Jackrong Qwopus3.6 27B v2 MTP BF16 was converted to STRIX_LEAN ROCmFP4 at + `4.34 BPW`. At native `262144` context with reasoning on, q4 main/draft KV, + and `n-max 4`, ROCm0 initially measured `34.9` / `29.6 tok/s`; increasing + only the batch to `-b 1024 -ub 512` moved sustained decode to `29.9 tok/s`. + Follow-up batch shapes `1280/512`, `1536/512`, and `1536/768` measured + `29.8`, `29.9`, and `29.8 tok/s` sustained respectively, so the smaller + `1024/512` profile remains promoted. Light acceptance filters `p-min 0.05` + and `0.10` also tied at `29.9 tok/s` sustained without beating the default. + Lower draft-depth checks with the same promoted batch rejected `n-max 1`, + `2`, and `3`, which measured only `19.9`, `26.6`, and `27.3 tok/s` + sustained. Qwopus therefore stays on `n-max 4`, unlike the 35B A3B profile + where `n-max 3` plus q8 main KV is best. + `--backend-sampling` tied sustained decode at `29.9 tok/s` but lowered prompt + throughput, so it is not promoted. Thread split checks at target/draft + `12/32` and `24/32` also tied `29.9 tok/s` sustained; the simpler default + `16/32` thread shape remains the recommended Qwopus profile. + KV isolation confirmed this is not like the 35B A3B q8-main profile: + draft-only q8 KV measured `35.1` / `29.8 tok/s`, and full q8 main plus q8 + draft KV measured `36.6` / `26.0 tok/s`. The full-q8 path improves burst + only and regresses sustained decode. Split accepted-KV checks also regressed: + q8 K only measured `35.7` / `22.3 tok/s`, and q8 V only measured `34.2` / + `24.7 tok/s`. Qwopus therefore keeps q4 main and q4 draft KV. + Vulkan0 measured `40.0` / `27.7 tok/s`, with Vulkan `n-max 3` and `n-max 5` + slower sustained; Vulkan `-b 1024 -ub 512` repeated the same `27.7 tok/s` + sustained band. q8 main KV regressed this model on both ROCm0 and Vulkan0. + A normal-path shortcut that only normalized `data[0].p` and filled the rest + of the top-k probabilities only for debug logging was rejected after the + 35B A3B short guard fell to `92.7 tok/s`, below the `100.0 tok/s` floor. + Disabling internal sampler timing with `sparams.no_perf = true` was also + tested and rejected after the same short guard dropped to `96.2 tok/s`. + Skipping the per-draft `common_sampler_reset()` in the MTP path was rejected + after the 35B A3B short guard dropped to `68.3 tok/s`; that reset remains + required to preserve the expected sampler/logit state for this helper. + A small MTP host-path cleanup now delays `llama_get_embeddings_pre_norm_ith()` + until the draft loop has confirmed that another draft token will be queued. + This avoids unused embedding-row pointer fetches on p-min rejects and final + `n-max` draft tokens. It passed the dense 27B guard at `34.0` / `28.1 tok/s`, + the 35B A3B guard at `104.4` / `90.1 tok/s`, and the Qwopus best ROCm0 + profile at `35.0` / `29.8 tok/s`. The default all-regression gate also + passed after this cleanup and ended with no KFD PIDs running. The 2026-05-25 + serial pass measured Qwen3.6 27B MTP at `33.9` / `27.9 tok/s`, ROCm runtime + FAST `45.66` / `57.81` / `88.27` / `155.05` us and dual-scale `49.16` / + `51.58` / `83.34` / `151.42` us for `n=1/2/4/8`, ROCm FlashAttention + `70.86` / `66.51` us for 64d dual-scale / FAST and `189.45` / `172.73` us + for Qwen-style 128d dual-scale / FAST, and ROCm CPY source-to-dual + `1106.89` / `1008.56` / `1006.60` us with source-to-FAST `1050.49` / + `958.98` / `950.50` us for F32/F16/BF16. + A second attempt to skip the final `n-max` `common_sampler_accept()` call was + rejected: the 35B A3B short check still reached `104.5 tok/s`, but sustained + decode fell to `81.1 tok/s`, below the `85.0 tok/s` floor. Reverting only + that sampler-accept change recovered the guard to `104.3` / `90.0 tok/s`. + Retesting the internal MTP sampler candidate count on the 35B A3B + reasoning-on profile rejected both directions around the promoted `top_k=10` + setting: `top_k=5` fell to `77.3 tok/s` sustained and `top_k=20` fell to + `69.6 tok/s` sustained. +- Reasoning-off checks on the final 35B q8-main/q4-draft profile are a + separate lower-throughput mode. `n-max 1/2/3/4` measured `77.7` / `73.9`, + `90.3` / `75.5`, `100.3` / `71.9`, and `85.7` / `66.1` short/sustained + tok/s respectively. If reasoning is disabled, `n-max 2` is currently the + best sustained profile in this bracket; the promoted fastest profile remains + reasoning-on `n-max 3`. +- Draft-thread-only checks on the promoted 35B profile also tied below the + promoted band. Keeping target threads at `16/32`, draft `8/16`, `16/16`, + and `24/32` all measured around `104.1`-`104.2 tok/s` short and + `89.1 tok/s` sustained, so the default matching draft thread counts remain. +- ROCm/HIP single-token MMVQ uses a full-block vector-dot ratio for the + dual-scale layout (`VDR_ROCMFP4_Q8_1_MMVQ=4`) while keeping the FAST layout + on the previous half-block ratio (`VDR_ROCMFP4_FAST_Q8_1_MMVQ=2`). This + lets dual-scale ROCmFP4 consume one full 32-value block per vector-dot call + without slowing the FAST dense-tensor path used by STRIX_LEAN. The focused + ROCm0 `MUL_MAT` guard improved dual-scale from the prior `78.81` us/run + serial pass to `54.89` us/run, and the Qwen3.6 27B MTP sustained guard + improved from `26.2` to `27.8 tok/s`. A broader version that also moved + FAST to `vdr=4` was rejected because sustained Qwen MTP dropped to + `24.2 tok/s`. A later FAST-only retest after the packed-byte load improved + the focused FAST ROCm guard to `41.37`, `49.29`, `80.91`, and `139.58` + us for `n=1/2/4/8`, but was still rejected because sustained Qwen MTP + dropped to `24.7 tok/s`. The remaining narrower FAST setting, + `GGML_ROCMFP4_FAST_Q8_1_MMVQ_VDR=1`, was also rejected after it failed the + focused FAST ROCm `n=1` guard at `60.18` us/run. The knob now rejects + invalid FAST MMVQ values at compile time; only `1`, `2`, and `4` are valid. +- A direct ROCmFP4 `vec_dot_q_cuda_dispatch` wrapper was tested in the + MMVQ kernels to bypass the generic constexpr function pointer call. It built + and passed the focused ROCm guard, measuring FAST `45.16` / `57.52` / + `89.44` / `156.49` us and dual-scale `50.83` / `51.11` / `84.51` / + `143.27` us for `n=1/2/4/8`, but Qwen3.6 27B MTP only reached `33.7 tok/s` + short and `27.9 tok/s` sustained. Because it did not beat the promoted + sustained band, the code change was removed. +- ROCm/HIP batched MMQ keeps the upstream-style `vdr=8` default for both + ROCmFP4 layouts, with ROCmFP4-owned compile-time test knobs + `GGML_ROCMFP4_Q8_1_MMQ_VDR` and `GGML_ROCMFP4_FAST_Q8_1_MMQ_VDR`. + FAST-only `vdr=4`, FAST-only `vdr=16`, and dual-scale `vdr=16` were tested + against the Qwen3.6 27B STRIX_LEAN ROCmFP4 bench and did not improve decode; + the tested runs all stayed at `13.56 tok/s` generation, while dual-scale + `vdr=16` slightly reduced prompt throughput. The default remains `8`. +- ROCm/HIP RDNA3.5 small-batch MMVQ now uses a ROCmFP4-specific two-warp + launch geometry for the ROCmFP4 layouts through `n=2`. Strix Halo previously + inherited the older RDNA2 one-warp table for this path. The promoted + `GGML_ROCMFP4_RDNA35_NWARPS=2` and + `GGML_ROCMFP4_RDNA35_NWARPS_MAX_NCOLS=2` defaults keep the single-token + microbench win and improve the guarded ROCm0 `n=2` shape: the latest serial + pass measured FAST/dual `66.56` / `58.40` us/run for `n=2`, versus the prior + `68.85` / `60.98` us/run band before extending the two-warp route. The + Qwen3.6 27B MTP guard held `33.5 tok/s` short and `27.7 tok/s` sustained, so + this is promoted as a backend micro-optimization with no sustained decode + regression. `GGML_ROCMFP4_RDNA35_NWARPS=4` was rejected because dual-scale + regressed to `57.87` us/run. The remaining 8-warp candidate was also rejected + because FAST `n=1` regressed to `59.33` us/run and failed the focused ROCm + runtime guard before multi-column checks. Extending the promoted two-warp + launch from `n=1` to `n=1..4` improved some focused multi-column ROCm + microbench rows (`dual n=2` reached `57.14` us/run), but Qwen3.6 27B MTP + sustained decode fell to `23.6 tok/s`, so the promoted upper bound stops at + `n=2`. A midpoint `GGML_ROCMFP4_RDNA35_NWARPS_MAX_NCOLS=3` build was also + checked on the Qwen3.6 35B A3B reasoning-on 262k profile; it reached + `87.5 tok/s` sustained versus `89.6 tok/s` for the same-session promoted + build, so `n=2` remains the default. +- ROCm/HIP RDNA3.5 wide-column rows-per-block now has ROCmFP4-owned compile-time + test knobs: `GGML_ROCMFP4_RDNA35_RPB_WIDE`, + `GGML_ROCMFP4_RDNA35_RPB_WIDE_DUAL`, and + `GGML_ROCMFP4_RDNA35_RPB_WIDE_FAST`. The defaults stay at `1`. A full + `RPB_WIDE=2` test improved FAST `n=8` `MUL_MAT` from `167.90` to + `131.72` us/run, but dual-scale collapsed from `148.11` to `1382.72` + us/run. A FAST-only `RPB_WIDE_FAST=2` build kept dual-scale safe + (`145.75` us/run) and improved FAST to `135.31` us/run, but Qwen3.6 + 27B MTP did not improve (`33.3 tok/s` short and `27.6 tok/s` sustained + at `n-max 4`; `n-max 5` remained burst-only at `45.5` short and `24.9` + sustained). Keep the knob off by default until a real decode guard benefits. +- ROCm/HIP RDNA3.5 `MUL_MAT_ID` routing now has a ROCmFP4-only compile-time + guard knob, `GGML_ROCMFP4_RDNA35_MMID_MAX_BATCH`, for testing whether MTP/MoE + batches should leave MMVQ earlier. The default keeps the accepted generic + RDNA3 behavior (`MMVQ_MAX_BATCH_SIZE`). A Strix test with the knob set to `1` + tied the promoted Qwen3.6 27B MTP path at `33.4 tok/s` short and `27.7 tok/s` + sustained. Follow-up threshold `2`, `3`, and `4` builds also tied at + `33.6` / `33.8` / `33.7 tok/s` short and `27.7` / `27.9` / `27.9 tok/s` + sustained. The same `3` threshold was then checked on the MoE-heavy + Qwen3.6 35B A3B MTP ROCmFP4 path and regressed to `95.8 tok/s` short and + `74.0 tok/s` sustained versus the promoted `104.4` / `89.3` band. Reusing + the older threshold `4` exploratory build on the same 35B guard measured + `104.2` / `89.2`, a near tie but still below the sustained promoted band. + No lower routing threshold is promoted. +- Target/draft "dual-stream" MTP overlap was inspected and is not a promoted + optimization. The current common MTP implementation verifies with the target + context, mirrors pre-norm embeddings into the MTP context, then drafts through + serial `llama_decode(ctx_dft, ...)` calls. ggml exposes async graph execution + and pipeline parallelism, but llama enables pipeline parallelism only for + multi-device layer-split offload cases. On single-device Strix Halo ROCm0, + target/draft overlap would require speculative scheduler changes and new + correctness guards rather than a runtime flag. The long-context optimization + focus remains KV-cache traffic, FlashAttention, memory bandwidth, and MTP + acceptance, while dual-scale ROCmFP4 mainly protects coherence. +- The MTP host loop now avoids two small sources of scheduler overhead. It + reuses a per-sequence drafting-state buffer instead of allocating a + `std::vector` on every draft call, and it tracks verify-batch sequence + bounds with one pass over token seq-ids instead of scanning every sequence for + every token. It also avoids copying all target verify rows when no previous + draft is pending, keeping only the carryover hidden row needed for the next + MTP step. The MTP path also uses a single-sequence batch append helper + instead of constructing a temporary sequence vector for each `common_batch_add` + call. It stores only non-final verify rows for partial-accept rollback, + copying the final row directly to `pending_h` for the next MTP step. It also + skips the draft-model decode call entirely when no sequence is drafting. + Debug candidate token formatting is skipped unless debug logging is enabled, + and the debug verbosity state is hoisted once per draft call instead of + rechecked inside the per-token loop. This is mainly a cleanup for + multi-sequence and long-running MTP sessions; the latest single-sequence + Qwen3.6 27B guard stayed in range at `33.7 tok/s` short and `27.9 tok/s` + sustained. +- The simple draft path also reuses an object-owned `uint8_t` drafting-state + buffer instead of allocating `std::vector` on every draft call. This + keeps both draft implementations on the same host-side allocation pattern; + the full serial gate stayed clean, with Qwen3.6 27B MTP at `33.8 tok/s` + short and `27.9 tok/s` sustained. +- The all-in-one regression harness now accepts a candidate `BUILD_DIR` and + derives separate `TEST_BACKEND_OPS_BIN` and `LLAMA_CLI_BIN` paths, so backend + microbench guards and Qwen CLI guards can run from the same candidate build. + DeepSeek remains opt-in only through `INCLUDE_DEEPSEEK_SMOKE=1`. +- ROCm/HIP vector FlashAttention now uses a ROCmFP4-only RDNA K/Q thread-group + default of `1` instead of the generic quantized-K RDNA default of `2`. This + affects only `Q4_0_ROCMFP4` and `Q4_0_ROCMFP4_FAST` K-cache cases; other + quantized FA paths keep the existing backend default. On the guarded ROCm0 + `FLASH_ATTN_EXT` shape, dual-scale improved from `122.33` to `113.03` + us/run, and FAST improved from `115.41` to `109.33` us/run. The Qwen3.6 + 27B MTP guard remained in the promoted band at `33.5 tok/s` short and + `27.6 tok/s` sustained. A wider `GGML_ROCMFP4_FATTN_KQ_NTHREADS=4` variant + was rejected because it regressed the same FA guard to `136.79` us/run + dual-scale and `124.37` us/run FAST. After the V-side default moved to `2`, + retesting `KQ_NTHREADS=2,V_NTHREADS=2` and `KQ_NTHREADS=4,V_NTHREADS=2` + still regressed the focused FA guard, so K/Q remains `1`. +- ROCm/HIP vector FlashAttention also uses a ROCmFP4-only V thread-group + default of `2`, down from the generic `D/4` path used by other quantized V + types. On the same guarded ROCm0 `FLASH_ATTN_EXT` shape, this moved + dual-scale from `113.03` to `85.63` us/run and FAST from `109.33` to + `80.74` us/run. The Qwen3.6 27B MTP guard stayed in range at `33.4 tok/s` + short and `27.6 tok/s` sustained, so this is promoted as the new default. + `GGML_ROCMFP4_FATTN_V_NTHREADS=4` also passed but was slower on the focused + FA guard; `1` was rejected because gfx1151 HIP compilation exceeded the 64 + KiB local-memory limit for ROCmFP4 FA instances. The FA guard now also + includes a Qwen3.6-style 128-head-dim, 8-KV-head, 12x-GQA ROCmFP4 shape + (`hsk=128,hsv=128,nh=8,nr23=[12,1],kv=7680,nb=1`). The accepted default + measured `246.91` us dual-scale and `219.23` us FAST on that shape. + Retesting `V_NTHREADS=4` on this wider shape produced a tiny dual-scale + improvement (`244.01` us) but regressed FAST (`224.21` us) and the existing + 64d guard, so it remains rejected. Retesting `KQ_NTHREADS=2,V_NTHREADS=2` + regressed the Qwen-style shape to `268.53` us dual-scale and `226.32` us + FAST, so K/Q remains `1`. +- ROCm/HIP vector FlashAttention now uses a ROCmFP4-owned single-half + Codebook10 expander in K/Q and V decode paths. These FA call sites already + know whether they need the low or high nibble stream, so they no longer pay + to expand both streams and discard one. The focused ROCm0 FA guard improved + from `86.13` / `81.23` us to `82.24` / `78.15` us for the 64d dual-scale / + FAST shapes, and from `247.06` / `221.19` us to `237.12` / `206.50` us for + the Qwen-style 128d shapes. The Qwen3.6 27B MTP guard held at `33.5 tok/s` + short and `27.7 tok/s` sustained in the full serial gate, so this is + promoted. +- ROCm/HIP vector FlashAttention also specializes the ROCmFP4 K/Q path when + `GGML_ROCMFP4_FATTN_KQ_NTHREADS=1`. Each thread owns the full head dot in + this promoted setting, so the kernel now expands both low/high packed + Codebook10 streams once per ROCmFP4 block and accumulates both half-blocks + together instead of loading the same packed bytes twice. The full serial gate + measured 64d FlashAttention at `81.62` / `78.13` us for dual-scale / FAST, + and the wider Qwen-style 128d guard improved from `237.12` / `206.50` us to + `228.58` / `199.32` us. Qwen3.6 27B MTP held `33.2 tok/s` short and + `27.7 tok/s` sustained, so this is promoted as a real Qwen-relevant FA + micro-optimization. + A post-specialization retest of `GGML_ROCMFP4_FATTN_V_NTHREADS=4` was + rejected. It passed the tightened FA guard and nudged the Qwen-style + dual-scale microbench to `228.33` us, but regressed 64d dual-scale to + `86.30` us and Qwen-style FAST to `201.81` us. Qwen3.6 27B MTP stayed at + `27.7 tok/s` sustained, so the promoted V-side default remains `2`. +- ROCm/HIP vector FlashAttention now uses a ROCmFP4-only V dequant rows-per- + thread default of `8`. The ROCmFP4 V helper supports 8-value chunks, and the + post-unaligned-load retest produced a real FA win: 64d dual-scale / FAST + `68.82` / `66.32` us and Qwen-style 128d dual-scale / FAST `201.13` / + `172.22` us. Qwen3.6 27B MTP held `33.5 tok/s` short and `27.7 tok/s` + sustained, so this is now promoted. It can be isolated with + `-DGGML_ROCMFP4_FATTN_V_ROWS_PER_THREAD=4` to return to the prior setting. + Retesting `GGML_ROCMFP4_FATTN_V_NTHREADS=4` on top of the 8-row default + improved only the Qwen-style dual-scale FA microbench (`187.72` us) while + regressing 64d dual-scale / FAST to `77.35` / `77.25` us and Qwen-style FAST + to `182.56` us. Qwen3.6 27B MTP tied at `33.5` / `27.7 tok/s`, so the + shipped V thread-group default remains `2`. + Retesting `GGML_ROCMFP4_FATTN_V_NTHREADS=8` on top of the 8-row default + regressed every focused FA row, including Qwen-style 128d dual-scale / FAST + at `208.94` / `200.45` us, so it was rejected without a longer MTP run. + Retesting `GGML_ROCMFP4_FATTN_KQ_NTHREADS=2` on top of the 8-row default + also regressed the focused FA guard: 64d dual-scale / FAST measured + `80.68` / `78.27` us and Qwen-style 128d measured `245.84` / `207.04` us. + Because the Qwen-style dual-scale row failed the guard, it was rejected + without a longer MTP run and K/Q remains `1`. +- ROCm/HIP vector FlashAttention now has a narrow dual-scale 128d V-thread + specialization: `GGML_ROCMFP4_FATTN_V_NTHREADS_D128_DUAL=4`. This keeps the + promoted 64d default and all FAST paths on `V_NTHREADS=2`, but uses the + previously promising 4-thread V grouping only for the Qwen-style dual-scale + 128d shape. The full serial gate measured FA at `70.37` / `66.31` us for + 64d dual-scale / FAST and `188.75` / `171.89` us for Qwen-style 128d + dual-scale / FAST, while Qwen3.6 27B MTP held `33.7 tok/s` short and + `27.9 tok/s` sustained with no KFD PIDs left running. + Retesting `GGML_ROCMFP4_FATTN_V_NTHREADS_D128_DUAL=8` was rejected. It + slightly improved the 64d dual-scale row (`69.40` us versus a same-session + `70.92` us promoted build), but regressed the Qwen-style 128d dual-scale row + from `194.40` to `211.10` us/run. Because long-context Qwen is the relevant + guard, the promoted D128 dual-scale V grouping remains `4`. + `GGML_ROCMFP4_FATTN_V_ROWS_PER_THREAD=16` was rejected during compilation: + the ROCmFP4 V dequantizer intentionally supports only `2`, `4`, and `8` + rows per thread, and the fixed-copy helper does not support the resulting + 32-byte move. The source now rejects unsupported values with a direct + ROCmFP4 compile-time error. + Retesting `GGML_ROCMFP4_FATTN_V_ROWS_PER_THREAD=2` on the promoted + D128-specialized build was rejected by the focused ROCm FlashAttention + guard. It regressed 64d dual-scale / FAST to `94.31` / `88.77` us and + Qwen-style 128d dual-scale / FAST to `270.00` / `223.88` us, so the + promoted rows-per-thread default remains `8`. +- Vulkan ROCmFP4 scale decode now uses a shared UE4M3 lookup table with + ROCmFP4's half-scale semantics. This moved the focused Vulkan dual-scale + `MUL_MAT` guard from `82.86`, `120.77`, and `181.28` us/run to `65.05`, + `83.07`, and `122.70` us/run for `n=1/2/4`. On Qwen3.6 27B MTP at 262k + context, the same change moved Vulkan sustained output from the older + `20.4 tok/s` at `--spec-draft-n-max 4` to `25.0 tok/s`; `n-max 3` now + reaches `25.3 tok/s` sustained. ROCm0 remains the promoted backend because + it still holds `33.5 tok/s` short and `27.7 tok/s` sustained. + A post-LUT Vulkan runtime sweep found a better fallback profile with q8 KV + and q8 draft KV: `--spec-draft-n-max 4` reached `34.8 tok/s` short and + `27.0 tok/s` sustained. q8 with `n-max 5` improved only the short burst + (`47.8 tok/s`) and regressed sustained output to `23.0 tok/s`; f16 KV + regressed sustained output to `22.5 tok/s`. + Follow-up isolation showed the Vulkan sustained gain comes from the main KV + cache rather than the draft KV cache: q4 main KV with q8 draft KV reached + only `34.6 tok/s` short and `25.0 tok/s` sustained, while q8 main KV with + q4 draft KV reached `34.7 tok/s` short and `26.9 tok/s` sustained. For a + Vulkan fallback, q8 main KV plus q4 draft KV is therefore the leaner near-tie + profile. Splitting q8 across only one accepted KV side was also rejected: + q8 main K with q4 main V reached `34.8 tok/s` short and `25.4 tok/s` + sustained, while q4 main K with q8 main V reached `34.6 tok/s` short and + `23.7 tok/s` sustained. The Vulkan fallback needs both accepted K and V at + q8. Adding mild acceptance filtering to the lean fallback + (`--spec-draft-p-min 0.25`) tied full q8/q8 at `27.0 tok/s` sustained while + keeping draft KV at q4; adding `--spec-draft-n-min 1`, stricter `p-min 0.75`, + and `p-split` checks stayed at `26.9 tok/s`. Retrying `n-max 5` with q8 main + KV, q4 draft KV, and + `p-min 0.25` still produced only a burst win (`47.8 tok/s` short) while + sustained output stayed at `23.0 tok/s`, so `n-max 4` remains the Vulkan + fallback setting. ROCm0 q4/q4 remains the overall promoted backend/profile. + Replacing the shared Codebook10 table with inline integer decode was tested + and rejected because it regressed the focused Vulkan FAST `n=1` guard to + `99.43` us/run. +- ROCm/HIP fallback dequant, copy, get-rows, GPU-side quantization scoring, + and standalone dequant helpers use ROCmFP4-owned HIP helpers for finite + scales and Codebook10 nibbles instead of relying on generic FP8 handling. + This keeps non-MMQ conversion paths aligned with the custom AMD format. +- A branchless HIP scalar Codebook10 nibble decoder was tested and rejected. + It passed the focused ROCm CPY, FlashAttention, and Qwen MTP guards, but + did not improve end-to-end decode. Qwen3.6 27B MTP measured `33.4 tok/s` + short and `27.7 tok/s` sustained, and the focused FlashAttention guard + slowed to `86.38` / `81.13` us for dual-scale / FAST, so the original + scalar decoder remains the default. +- CPU-side Codebook10 table decode in the quantizer/dequantizer was tested + and rejected. The full table variant failed the CPU quant guard with + dequantization at `49.04` cycles/32 for dual-scale and `84.23` cycles/32 for + FAST. The MSE-loop-only variant passed but slowed normal quantization to + `4183.33` / `4018.68` cycles/32 versus the restored arithmetic baseline at + `4034.33` / `3738.85` cycles/32, so the arithmetic CPU decoder remains the + default. +- UE4M3 scale decode in the ROCm/HIP software path uses a ROCmFP4-owned finite + scale decoder. It avoids `ldexpf`, builds normal FP32 values directly from + exponent/mantissa bits, and skips the generic FP8 NaN handling because + ROCmFP4 row validation already rejects non-finite scale bytes. +- A constant-memory HIP lookup table for the finite UE4M3 scale values was + tested and rejected. The latest isolated `GGML_ROCMFP4_USE_SCALE_LUT=1` + pass failed the focused ROCm runtime guard because FAST `n=1` regressed to + `69.48` us/run; prior checks also regressed ROCm CPY source-to-quant paths + and FlashAttention. The arithmetic finite-scale decoder remains the default. +- ROCm/HIP dequant conversion kernels use the same ROCmFP4 finite scale + decoder, keeping tensor conversion aligned with the hot MMQ/MMVQ backend + path instead of falling back to the generic FP8 helper. +- ROCm/HIP backend CPY now advertises and executes quantized + `Q4_0_ROCMFP4 -> F32` and `Q4_0_ROCMFP4_FAST -> F32` conversion paths. + This keeps diagnostic graph ops and fallback tensor conversion inside the + custom AMD decoder instead of being rejected by backend capability checks. + Contiguous q-to-f32 copies now use a ROCmFP4-specific packed-byte kernel: + one HIP thread reads one packed byte and writes the matching low/high + half-block output values. This gives coalesced output writes for normal + contiguous graph copies while the existing stride-aware block kernel remains + the fallback for views and non-contiguous tensors. +- ROCm/HIP backend CPY also supports `F16 -> Q4_0_ROCMFP4`, + `F16 -> Q4_0_ROCMFP4_FAST`, `BF16 -> Q4_0_ROCMFP4`, and + `BF16 -> Q4_0_ROCMFP4_FAST`. The kernels convert each 32-value half/bfloat + block to local FP32 and then run the same exhaustive ROCmFP4 scale search, + so runtime graph copies keep the coherence-first quantizer instead of + falling back to unsupported behavior. +- ROCm/HIP ROCmFP4 source-to-quant and quant-to-F32 CPY wrappers now use + normal multi-thread HIP launch geometry instead of launching one active + thread per quant block. This keeps F32/F16/BF16 runtime quantization and + ROCmFP4 dequantization on the same exact conversion math while removing a + launch-shape bottleneck in helper/fallback graph paths. +- ROCm/HIP ROCmFP4 quant-to-F32 CPY uses ROCmFP4-specific block dequant + helpers that decode each block scale once before unpacking all 32 values. + This avoids repeatedly decoding identical scale bytes through the generic + two-value dequant helper while preserving the same output values. For the + guarded contiguous `8192x512x2` ROCm0 CPY shape, the packed-byte path moved + dual-scale `Q4_0_ROCMFP4 -> F32` from the old `740` us/run band to + `184.48` us/run and FAST `Q4_0_ROCMFP4_FAST -> F32` to `169.99` us/run. + A split launch geometry is now used: source-to-ROCmFP4 quantization uses + 128-thread workgroups, while quant-to-F32 keeps the accepted 64-thread + packed-byte launch. A whole-path 128-thread launch was roughly tied overall + but slightly regressed quant-to-F32; isolating 128 threads to source + quantization preserves the dequant copy win while shaving the source paths. + A 256-thread launch regressed F16 source-to-quant paths and remains rejected. + After the FAST direct-value scoring win, FAST-only 256-thread and 64-thread + source launch splits were rechecked and rejected as well. The 256-thread + split regressed FAST F16 source-to-quant to `1047.24` us/run, and the + 64-thread split measured FAST F32/F16/BF16 `1055.32`, `955.35`, and + `954.78` us/run, so the shared 128-thread source launch remains promoted for + both ROCmFP4 layouts. +- ROCm/HIP contiguous quant-to-F32 CPY shared-scale staging was tested and + rejected. Decoding one block scale into shared memory and synchronizing each + 64-thread launch reduced duplicate scale decode work, but the synchronization + overhead regressed the guarded `8192x512x2` shape: dual-scale + `Q4_0_ROCMFP4 -> F32` moved from `181.84` to `188.63` us/run, and FAST + `Q4_0_ROCMFP4_FAST -> F32` moved from `170.65` to `180.03` us/run. The + direct packed-byte kernel remains promoted. +- ROCm/HIP backend CPY supports same-type packed-block copies for + `Q4_0_ROCMFP4 -> Q4_0_ROCMFP4` and + `Q4_0_ROCMFP4_FAST -> Q4_0_ROCMFP4_FAST`, including block-aligned views. + The kernel copies the packed 18-byte or 17-byte ROCmFP4 blocks directly, so + graph/view copies preserve exact bytes and avoid dequantize/requantize + fallback behavior. The launcher uses normal multi-thread HIP workgroups + rather than one-thread launches, so large packed-view copies scale with the + number of ROCmFP4 blocks. +- ROCm/HIP runtime quantization now finds the finite UE4M3 candidate nearest + `max_abs / 10` with a monotonic binary search, matching the CPU/Vulkan + reference tie behavior while avoiding the older 126-step linear nearest-scale + scan in the HIP copy utility. It also uses the same conservative lower-scale + pruning as the CPU quantizer, so runtime F32/F16/BF16-to-ROCmFP4 copies skip + smaller scales once their unavoidable clipped max-value error cannot beat the + current best exact scale. The HIP max scan uses the same plain absolute + compare as the CPU quantizer, preserving NaN/Inf handling while avoiding + `fmaxf` in the runtime quantization hot loop. +- ROCm/HIP runtime quantization specializes the exact scale search for the + only block shapes used by ROCmFP4 CPY: `0..15`, `16..31`, and `0..31`. + This keeps the same candidate order and tie behavior while letting HIP + compile the dual-scale and FAST source-to-quant paths with fixed offsets. +- ROCm/HIP dual-scale source-to-quant scoring uses a direct Codebook10 value + helper for 16-value half blocks. Final packed quantization still uses the + normal code-index helper, but scale scoring no longer has to map the selected + nibble back through the generic decode helper on the dual-scale path. +- ROCm/HIP FAST source-to-quant scoring now uses the same direct Codebook10 + value helper during scale search. This preserves the exact nearest-value + thresholds and final packed output, but avoids index-then-decode work in the + 32-value FAST scoring path. On the guarded `8192x512x2` ROCm0 CPY shape, + FAST source quantization improved from F32/F16/BF16 `1218.69`, `1138.32`, + and `1138.78` us/run to a repeat guard pass of `1047.21`, `950.93`, and + `951.00` us/run. +- ROCm/HIP `F32 -> ROCmFP4` runtime CPY stages each 32-value block into local + FP32 before running the exhaustive scale search, matching the existing + F16/BF16 source paths. This preserves exact F32 input values but avoids + rereading the global source pointer throughout scale scoring. On the guarded + `8192x512x2` ROCm0 CPY shape, dual-scale F32 source quantization improved + from roughly `9916` to `1117` us/run, and FAST improved from roughly + `10671` to `1231` us/run. +- ROCm/HIP contiguous-only source-to-quant CPY kernels for + `F32 -> ROCmFP4` were tested and rejected. They avoided the generic + multidimensional offset math, but on the guarded `8192x512x2` shape they + did not beat the existing multi-thread generic CPY path and slightly + regressed `F32 -> Q4_0_ROCMFP4_FAST`, so source-to-quant ROCmFP4 CPY keeps + the same guarded implementation for view and contiguous tensors. +- ROCm/HIP backend GET_ROWS supports both ROCmFP4 layouts. This gives pure + ROCmFP4 tensors the same direct row-gather coverage as stock small-block + quants on ROCm and keeps embedding-row access on the custom finite-scale + decoder. +- ROCm/HIP `MUL_MAT` support now covers `F16` activation tensors for both + ROCmFP4 layouts. The backend stages half activations to contiguous FP32 on + the GPU, including non-contiguous/views, then feeds the existing Q8 + activation quantizer and ROCmFP4 MMVQ/MMQ kernels. This keeps the forward + path on the AMD backend instead of rejecting the op and falling through to a + slower dequantized matrix path. The generic matmul runtime guard explicitly + allows this ROCmFP4 x F16 forward-inference case, so the support probe and + execution wrapper agree for batched activations. +- The standalone HIP dequant skeleton covers both the dual-scale and FAST + single-scale layouts, so future fused ROCm kernels can target the current + balanced FAST artifact without reintroducing the older scale path. +- The standalone HIP dequant launch now maps one thread to one packed + ROCmFP4 byte across a 256-thread global grid. This avoids the older + one-16-thread-block-per-32-values launch shape and gives future fused or + diagnostic dequant paths normal GPU occupancy scaling. +- Vulkan ROCmFP4 shaders also decode UE4M3 scales directly to the half-scale + value used by the codebook, matching CPU/HIP and avoiding repeated `* 0.5` + fixups at dequant and matmul call sites. +- Vulkan ROCmFP4 shaders keep a shared `kvalues_rocmfp4` Codebook10 table. + Arithmetic/direct Codebook10 decode variants compiled and preserved + coherence, but measured slower on Strix Halo Vulkan, so the table path + remains the active backend implementation. +- Vulkan `dequantize4()` for ROCmFP4 and ROCmFP4_FAST decodes each block's + UE4M3 scale once per 4-value vector instead of calling the 2-value helper + twice. This improves tested dual-scale ROCmFP4 small-batch matvec shapes + while keeping the promoted Vulkan runtime guard clean. +- Vulkan `Q4_0_ROCMFP4_FAST` matvec/MMQ kernels have a single-scale dot + specialization. They combine the two half-block dot sums and apply the one + FAST scale once, instead of taking the dual-scale path used by + `Q4_0_ROCMFP4`. +- Vulkan `Q4_0_ROCMFP4_FAST` MMQ stores its block scale as a scalar in + shared/register cache instead of duplicating it into a `vec2`. The focused + Strix Halo Vulkan `MUL_MAT` microbench for + `m=4096,n=1,k=14336,type_a=q4_0_rocmfp4_fast` improved from `62.18` to + `61.45` us/run. +- A Vulkan packed16 view for ROCmFP4 quant bytes was tested and rejected. It + compiled, but the focused FAST MMQ guard regressed to `83.03` us/run from the + accepted `61`-`62` us/run range, so ROCmFP4 Vulkan keeps the byte-view load + path. +- Vulkan backend CPY/SET_ROWS now has generated `F32 -> Q4_0_ROCMFP4`, + `F32 -> Q4_0_ROCMFP4_FAST`, `Q4_0_ROCMFP4 -> F32`, + `Q4_0_ROCMFP4_FAST -> F32`, and indexed SET_ROWS shaders. The SET_ROWS path + uses the same exhaustive finite UE4M3 scale search as the CPU reference, so + quantized K/V cache writes favor coherence over a cheap max-abs shortcut. +- Vulkan `F32 -> ROCmFP4` runtime quantization now uses the same exact ordered + UE4M3 scale search as CPU/HIP: find the scale nearest `max_abs / 10`, expand + outward, and stop a candidate once its partial error cannot beat the current + best scale. The candidate set is unchanged, so this avoids a slower linear + scan without falling back to a lower-quality shortcut. +- Vulkan source-to-ROCmFP4 runtime quantization now also prunes lower scale + candidates once clipping the block max alone cannot beat the current best + error. This mirrors the CPU/HIP scale search bound and keeps the candidate + set exact. The 2026-05-25 Vulkan CPY guard passed after this shader change: + F32/F16/BF16-to-dual measured `9525.39`, `2350.54`, and `2418.09` us/run, + dual-to-F32 `516.65` us/run, F32/F16/BF16-to-FAST `10111.85`, `2923.67`, + and `2949.42` us/run, and FAST-to-F32 `509.65` us/run. The full promoted + gate passed after rebuild with Qwen3.6 27B MTP at `33.9` / `28.0 tok/s` and + no KFD PIDs left running. +- Vulkan backend CPY also supports `F16 -> Q4_0_ROCMFP4`, + `F16 -> Q4_0_ROCMFP4_FAST`, `BF16 -> Q4_0_ROCMFP4`, and + `BF16 -> Q4_0_ROCMFP4_FAST`. The runtime quantization shader can load + `float`, `float16_t`, and BF16 source bits and then runs the same exact + ordered finite UE4M3 scale search. Backend tests keep a bounded NMSE + tolerance only for the half/bfloat runtime quantization cases because those + paths are inherently lossy around source-precision tie points; the + `F32 -> ROCmFP4` checks remain strict. +- Vulkan same-type CPY supports packed-block copies for + `Q4_0_ROCMFP4 -> Q4_0_ROCMFP4` and + `Q4_0_ROCMFP4_FAST -> Q4_0_ROCMFP4_FAST`, including block-aligned + non-contiguous/permuted/view copies. The non-contiguous path uses a + byte-addressed block shader and preserves exact 18-byte or 17-byte + ROCmFP4 blocks. Contiguous same-type dual-scale and FAST copies now both use + the direct byte-copy path, avoiding the generic halfword copy route for + ROCmFP4's custom 18-byte and 17-byte block layouts. +- Vulkan CPY is now covered by a dedicated regression guard so copy-path + changes cannot silently fall back or regress outside the ROCm-only CPY gate. + The guard runs same-type and source/dequant CPY correctness before measuring + the large copy performance shape, and now streams the performance phase + through `tee` so long Vulkan runs do not look idle to the command runner. + The ceilings were tightened after the lower-scale pruning gain, so the old + slow source-to-quant path no longer passes this guard. + On Strix Halo RADV Vulkan, the large guarded shape currently measures + F32/F16/BF16-to-dual at `9525.39`, `2350.54`, and `2418.09` us/run, + dual-to-F32 at `516.65` us/run, F32/F16/BF16-to-FAST at `10111.85`, + `2923.67`, and `2949.42` us/run, and FAST-to-F32 at `509.65` us/run. +- Vulkan scalar FlashAttention can now decode ROCmFP4 and ROCmFP4_FAST K/V + cache blocks. ROCmFP4 K/V is forced to the scalar FA path because the current + custom decode is not a coopmat/native matrix-core FP4 path. +- Vulkan scalar FlashAttention can use the integer-dot MMQ K path for both + ROCmFP4 K-cache layouts. The FAST layout expands each 4-bit Codebook10 value + into packed signed int8 lanes and uses its single UE4M3 scale as the K block + multiplier. The dual-scale layout also uses packed signed int8 lanes, but + splits the dot accumulation by half-block so each 16-value half uses its own + UE4M3 scale. This keeps the quality-biased STRIX K-cache path fast without + applying one scale to a two-scale block. +- Build and runtime verification generated the Vulkan SPIR-V entries for + ROCmFP4 copy/SET_ROWS shaders, linked `libggml-vulkan.so`, and passed + Vulkan ROCmFP4 CPY plus MUL_MAT smoke tests on Strix Halo. +- Row validation rejects invalid scale bytes outside finite unsigned UE4M3 + (`0x00` through `0x7e`) so corrupted custom GGUF tensors fail early. +- Quantization keeps the exhaustive 126-scale UE4M3 search for both normal and + imatrix paths. Candidate-window scale search was tested and improved GGUF + creation speed, but it regressed the Qwen3-4B short WikiText-2 PPL on the + compact FAST path, so it was rejected for coherence. +- The exhaustive scale search now visits the UE4M3 candidate nearest the + block's `max_abs / 10` first using a monotonic binary search, expands + outward, and exits a candidate scale once its partial error cannot beat the + current best scale. This remains exact because every finite scale is still + evaluated; on the Qwen3-4B FAST_COHERENT artifact it produced a + byte-identical GGUF while cutting FAST quantization cost sharply. +- CPU scale decode now uses a 127-entry finite UE4M3 half-scale table for the + exhaustive scale search and row dequantization. The table preserves the same + exact FP32 values as the former bit-construction decoder, but avoids + rebuilding them for every scale candidate. +- CPU scale search now has separate unweighted and imatrix-weighted MSE + scoring paths. The normal path avoids per-value weight branches, while the + imatrix path precomputes the row-energy calibration weight once per + 16/32-value block instead of recomputing the same `sqrtf` term for every + candidate scale. +- CPU normal quantization scans each block once for finite values and uses a + finite-only nearest-Codebook10 helper in the exhaustive MSE loop when the + block is clean. Non-finite blocks still use the guarded helper. This + promoted conversion-speed path measured focused dual-scale / FAST normal + quantization at `3844.38` / `3582.57` cycles per 32 values in the latest + guard. +- CPU imatrix-weighted scale-MSE scoring now uses the same finite-only + nearest-Codebook10 helper when the source block is finite. This preserves the + guarded path for non-finite input while improving FAST imatrix GGUF creation: + same-session pre-candidate FAST imatrix was `5258.07` cycles / 32 values, and + two guarded candidate passes measured `4448.73` and `4447.32`. Dual imatrix + stayed in the noisy guarded band, so the claim is limited to FAST imatrix. +- The weighted and unweighted exhaustive scale loops are split as well, so + the normal quantizer does not branch on imatrix state for every finite + UE4M3 candidate. This keeps the exact candidate order and tie behavior while + improving CPU GGUF creation speed for both ROCmFP4 layouts. +- The CPU quantizers fill every packed nibble byte in each ROCmFP4 block, so + they no longer clear `qs` before packing. This is an exact store cleanup: + correctness stayed clean, `q4_0_rocmfp4` quantize improved to `5244.46` + cycles / 32 values, `q4_0_rocmfp4_fast` improved to `4668.28` cycles / 32 + values. +- CPU scale selection scans block maxima with a plain absolute-value compare + instead of `fmaxf`. NaNs still do not raise the maximum, infinities are still + rejected before scale search, and finite output is unchanged. This moved + `q4_0_rocmfp4` quantize to `5228.64` cycles / 32 values and FAST to + `4516.45` cycles / 32 values. +- CPU dequantization and quantization-error scoring use the ROCmFP4 arithmetic + Codebook10 decoder, which avoids a table fetch per unpacked nibble. CPU + fallback vec-dot keeps the 16-entry codebook table because measured Strix + Halo fallback dot speed was better with the table there. This hybrid keeps + the dequant win without regressing CPU vec-dot. +- CPU fallback vec-dot now reuses each packed ROCmFP4 byte for both low and + high nibbles before table decode. This preserves exact output, moved focused + dual-scale vec-dot from `31.86` to the `29.77`-`29.82` cycles / 32 values + band, and kept FAST in the `27.04`-`27.06` cycles / 32 values band. +- Two additional CPU decode shortcuts were rechecked on 2026-05-24 and + rejected. Replacing arithmetic Codebook10 decode with table decode in + row dequantization and scale-MSE scoring slowed CPU dequantization from the + guarded `33`-cycle band to `51`-`84` cycles / 32 values. Returning a direct + decoded value during MSE scoring instead of index-then-decode also passed + correctness but regressed normal quantization in the measured guard, so the + original arithmetic decoder remains the CPU path. A narrower full-block + weighted-MSE-only retry was also rejected because it regressed the focused + FAST imatrix timing to `5007.73` cycles / 32 values. +- A direct finite-scale table helper for CPU quantizer scale-search candidates + was also rejected. It helped dual-scale normal quantization, but repeat + guard runs regressed FAST normal quantization to `4043.60` and + `4289.66` cycles / 32 values. The guarded scale helper remains in place + because the compact STRIX_LEAN preset depends heavily on FAST tensors. +- NaN-only and branchless finite-scan variants were also rejected for default. + The NaN-only scan was noisy and the same-session `isfinite()` scan measured + better dual-scale normal quantization (`3735.41` vs `3776.53` cycles / 32 + values), while the branchless boolean-and scan regressed FAST normal + quantization to `3773.44` cycles / 32 values. +- Per-value Codebook10 quantization uses exact nearest-neighbor thresholds + instead of a 16-entry scan. The hot quantizer path uses one reciprocal scale + per candidate/block and multiplies each value by that reciprocal instead of + dividing per value. On the Qwen3-4B FAST_COHERENT check this kept PPL tied + with the accepted artifact while cutting GGUF creation time further. +- CPU exhaustive scale search now prunes lower-scale candidates after the + clipped max-value error alone cannot beat the current best error. The + imatrix path uses the positive calibration weight of the max element for the + same conservative bound. This does not change accepted values because every + skipped smaller scale has a larger unavoidable clip error. The weighted path + also combines imatrix weight preparation, max-absolute scan, and max-value + pruning-weight selection into one pre-scan, while both exhaustive loops stop + once no valid lower or upper scale candidates remain. Latest 40-iteration + quant guard run: `q4_0_rocmfp4` normal `4049.28` cycles / 32 values, FAST + normal `3721.48`, imatrix dual-scale `5251.76`, imatrix FAST `4898.75`. +- Adding `GGML_RESTRICT` to the local CPU scale-scoring helper pointers was + tested and rejected. It compiled and passed the quant guard, but did not + improve normal quantization and made the imatrix guard noisier/slower in + the measured pass, so the existing helper signatures were kept. +- Regression guards: + - `scripts/check-rocmfp4-deepseek-regression.sh` is an optional compatibility + smoke guard for a second ROCmFP4-converted model family. It is not part of + the promoted-gain gate because no reproducible DeepSeek speedup has been + established yet. The default all-regression script skips it unless + `INCLUDE_DEEPSEEK_SMOKE=1` is set. + - `scripts/check-rocmfp4-qwen-mtp-regression.sh` verifies the promoted + Qwen3.6 27B MTP ROCmFP4 262k-context `draft-mtp` path. The stable guard + uses ROCm0 and `--spec-draft-n-max 4`. On ROCm0, `n-max 4` improved the + short guard prompt from `27.7` to `33.6 tok/s` versus `n-max 3`, and the + longer forced-output prompt from `25.6` to `26.3 tok/s`; the dual-scale + MMVQ VDR tune then moved the longer prompt to `27.8 tok/s`. `n-max 5` + and `n-max 6` improved only the bursty short prompt after the MMVQ tune + but regressed sustained output to `24.8` and `24.0 tok/s`, respectively. + Follow-up `n-max 5` checks with `--spec-draft-p-min 0.75`, + `--spec-draft-p-min 0.90`, and `--spec-draft-n-min 1` still held the + short prompt at `45.0`-`45.1 tok/s` but kept sustained output at only + `24.7 tok/s`, so `n-max 4` remains promoted. + The guard now checks both short and sustained prompts so high-acceptance + best-case gains do not hide sustained-output regressions. + Additional ROCm0 checks rejected larger batch sizes, smaller ubatch, + q8/f16 KV cache, `--swa-full`, `--no-host`, `--no-op-offload`, + `--no-repack`, polling changes, lower and higher CPU thread counts, + greedy sampling, backend sampling, p-split changes, sampler chain trimming, + and combined MTP+ngram speculation because none improved the sustained + prompt. After the MMVQ VDR tune, `-b 1024 -ub 512`, `-b 2048 -ub 512`, + and `-b 512 -ub 256` still tied the promoted `27.8 tok/s` sustained + result, while `-b 1024 -ub 1024` and + `--poll-batch 1 --spec-draft-poll-batch 1` stayed at `27.7 tok/s`. + q8 and f16 KV cache dropped sustained decode to `25.4` and `26.0 tok/s`. + The MTP draft loop now honors `--spec-draft-p-min`. With that cutoff active, + `n-max 4, p-min 0.75` tied the promoted path at `33.5 tok/s` short and + `27.6 tok/s` sustained, while `n-max 5` stayed burst-only at `45.0`-`45.1` + tok/s short and `24.6`-`24.7 tok/s` sustained for `p-min 0.75` and `0.90`. + Follow-up ROCm0 KV isolation confirmed this is not a draft-cache issue: + q8 main KV with q4 draft KV measured `33.5 tok/s` short and `25.3 tok/s` + sustained, q8 main K only measured `31.5` and `21.4 tok/s`, and q8 main V + only measured `31.1` and `21.9 tok/s`. The promoted ROCm0 path therefore + keeps both accepted and draft KV at q4. + `--spec-draft-p-split 0.05` and `0.20` both tied the `27.7 tok/s` + sustained guard, `-t 24 -tb 32 --spec-draft-threads 24 + --spec-draft-threads-batch 32` tied, and trimming the sampler chain to + `top_k;top_p;temperature` regressed sustained decode to `26.6 tok/s`. + Draft-only q8 KV (`SPEC_DRAFT_TYPE_K=q8_0`, + `SPEC_DRAFT_TYPE_V=q8_0`) with main KV left at q4 tied the promoted path + at `33.6 tok/s` short and `27.7 tok/s` sustained, so the leaner q4 draft + KV default remains promoted. + The heavier Qwen3.6 27B MTP `STRIX_MTP_Q6` ROCmFP4 profile was also + checked with the same ROCm0 guard. It reached `30.1 tok/s` on the short + prompt but only `21.3 tok/s` sustained, below the guard floor, so the + Q6/quality-biased profile is not promoted for throughput. + The guard script supports `SPEC_DRAFT_N_MAX`, `SPEC_DRAFT_N_MIN`, + `SPEC_DRAFT_P_MIN`, `SPEC_DRAFT_P_SPLIT`, `THREADS`, `THREADS_BATCH`, + `SPEC_DRAFT_THREADS`, `SPEC_DRAFT_THREADS_BATCH`, `SAMPLERS`, + `BATCH_SIZE`, `UBATCH_SIZE`, `CACHE_TYPE_K`, `CACHE_TYPE_V`, + `SPEC_DRAFT_TYPE_K`, `SPEC_DRAFT_TYPE_V`, `EXTRA_ARGS`, and + `SPEC_EXTRA_ARGS` environment overrides for + controlled sweeps while defaulting to the promoted `n-max 4`, `n-min 0`, + `p-min 0.0`, `p-split 0.10`, q4 KV, `-t 16`, `-tb 32`, `-b 512`, and + `-ub 512` settings. + - `scripts/check-rocmfp4-quant-regression.sh` runs quant correctness plus + CPU quantizer, dequantizer, and vec-dot cycle ceilings for both ROCmFP4 + block layouts. It checks normal and imatrix quantization, and now also + protects CPU fallback dequant/vec-dot paths so decode-helper experiments + cannot pass while slowing non-GPU fallbacks. Recent pass after keeping the + `isfinite()` finite-block scoring promotion and adding the weighted/imatrix + finite scorer: dual-scale quant `3844.38`, FAST quant `3582.57`, dual + dequant `33.59`, FAST dequant `33.13`, dual vec-dot `29.96`, FAST vec-dot + `27.03`, dual imatrix `5587.43`, and FAST imatrix `4447.32` cycles / 32 + values. + - `scripts/check-rocmfp4-vulkan-runtime-regression.sh` measures focused + Vulkan `MUL_MAT` runtime for ROCmFP4 FAST and dual-scale layouts, catching + shader regressions that can be hidden by end-to-end decode noise. The + guard covers `n=1`, `n=2`, `n=4`, and `n=8` for both layouts. Recent + tightened serial pass measured FAST `55.82`, `71.59`, `105.10`, and + `163.41` us/run for `n=1/2/4/8`, and dual-scale `64.87`, `83.24`, + `118.56`, and `194.27` us/run for `n=1/2/4/8`. The previous serial pass + after adding the shared ROCmFP4 UE4M3 + scale LUT was FAST `53.66`, `71.67`, `105.14` us/run and dual-scale + `65.05`, `83.07`, `122.70` us/run for the same shapes. + - `scripts/check-rocmfp4-vulkan-cpy-regression.sh` measures Vulkan0 CPY for + `F32/F16/BF16 -> Q4_0_ROCMFP4`, + `F32/F16/BF16 -> Q4_0_ROCMFP4_FAST`, and the matching quant-to-F32 + dequant copy paths on the same large shape used by the ROCm CPY guard. + It is included in the all-regression harness to catch accidental Vulkan + copy fallback or shader-routing regressions. + - `scripts/check-rocmfp4-rocm-runtime-regression.sh` measures the same + focused `MUL_MAT` shapes on ROCm0. The guard covers `n=1`, `n=2`, `n=4`, + and `n=8` for both FAST and dual-scale ROCmFP4 so MTP-style multi-column + regressions are caught instead of only protecting single-token matvec. + Recent tightened serial pass after extending the RDNA3.5 two-warp route + through `n=2` measured FAST `51.66`, `66.56`, `101.27`, and `168.84` + us/run for `n=1/2/4/8`, and dual-scale `53.66`, `58.40`, `87.32`, and + `148.26` us/run for `n=1/2/4/8`. The `n=8` guard was added after a + rejected wide-rows candidate regressed dual-scale `n=8` to `1382.72` + us/run. + - `scripts/check-rocmfp4-rocm-cpy-regression.sh` measures ROCm0 CPY for + `F32/F16/BF16 -> Q4_0_ROCMFP4`, + `F32/F16/BF16 -> Q4_0_ROCMFP4_FAST`, and the matching quant-to-F32 + dequant copy paths on a large `8192x512x2` shape. Recent serial-gate pass + after direct FAST value scoring: F32/F16/BF16-to-dual `1111.73`, + `1008.69`, `1006.93` us/run, dual-to-F32 `182.25` us/run, + F32/F16/BF16-to-FAST `1047.21`, `950.93`, `951.00` us/run, and + FAST-to-F32 `170.36` us/run. + - `scripts/check-rocmfp4-all-regression.sh` runs the promoted-gain gate: + quant, Vulkan runtime, Vulkan CPY, ROCm runtime, ROCm FlashAttention, + ROCm CPY, and Qwen MTP guards serially, then checks ROCm KFD PIDs. Runtime + microbenchmarks should use this serial path rather than being run in + parallel with other GPU/UMA workloads. DeepSeek is not part of this + promoted-gain gate because no reproducible DeepSeek speedup has been + established; run the separate compatibility smoke only when explicitly + needed with `INCLUDE_DEEPSEEK_SMOKE=1`. The K/Q block-pair FlashAttention + serial pass held Qwen3.6 27B MTP at `33.2 tok/s` short and `27.7 tok/s` + sustained, with no KFD PIDs left running. The same pass measured ROCm + runtime `MUL_MAT` at FAST `54.16` / `66.13` / `101.73` / `178.36` us and + dual-scale `51.65` / `57.98` / `88.33` / `148.48` us for `n=1/2/4/8`, + and ROCm FlashAttention at `81.62` / `78.13` us for 64d dual-scale / FAST + and `228.58` / `199.32` us for Qwen-style 128d dual-scale / FAST. + - The MTP `accept()` path now skips re-copying `pending_h` when acceptance + lands on the final verify row already staged by `process()`. The focused + Qwen MTP guard held `33.8 tok/s` short and `27.9 tok/s` sustained after + this host-side cleanup. + - The MTP `process()` path now stores only the non-final verify rows needed + for partial-accept rollback, while copying the final target hidden row + directly to `pending_h` for the next MTP step. The focused Qwen MTP guard + held `33.9 tok/s` short and `27.9 tok/s` sustained, and the full serial + gate held `33.7 tok/s` short and `27.9 tok/s` sustained after this cleanup. + - The simple draft and MTP draft paths now return before + `llama_decode(ctx_dft, batch)` when no sequence is actively drafting. The + focused Qwen MTP guard held `33.8 tok/s` short and `28.0 tok/s` sustained, + and the full serial gate held `33.7 tok/s` short and `27.9 tok/s` + sustained after this idle/no-draft cleanup. + - The simple draft path now uses a reusable `uint8_t` drafting-state buffer + instead of allocating `std::vector` per draft call. The focused Qwen + MTP guard held `33.7 tok/s` short and `28.0 tok/s` sustained, and the full + serial gate held `33.8 tok/s` short and `27.9 tok/s` sustained. + - The MTP draft loop now skips its debug-candidate loop entirely unless + debug logging is enabled, hoists the debug verbosity state once per draft + call, and uses direct vector indexing in the hot per-token path. The + focused Qwen MTP guard held `33.8 tok/s` short and `27.9 tok/s` sustained + after this cleanup, so it is kept as a low-risk host-side simplification + rather than claimed as a decode-speed gain. + - The simple and MTP draft paths now use a shared direct one-sequence batch + append helper instead of `common_batch_add(..., { seq_id }, ...)` in the + hot speculative path. This avoids a temporary sequence-id container without + changing multi-sequence batch behavior elsewhere. The focused Qwen MTP + guard held `33.9 tok/s` short and `27.9 tok/s` sustained, and the full + serial gate held `33.8 tok/s` short and `27.9 tok/s` sustained. + - The MTP verify-row buffer now reserves for the configured draft depth and + only grows when needed instead of shrinking/resizing on every verification + pass. The focused Qwen MTP guard held `33.8 tok/s` short and `27.9 tok/s` + sustained, and the full serial gate held `33.9 tok/s` short and + `27.9 tok/s` sustained with no KFD PIDs left running. + - The MTP `process()` path now copies retained verification hidden rows from + the target embedding buffer in one contiguous `memcpy()` instead of one + `llama_get_embeddings_pre_norm_ith()` plus `memcpy()` per row. Rollback + behavior is unchanged. The focused Qwen3.6 27B MTP guard held + `33.7 tok/s` short and `27.9 tok/s` sustained, and the 35B A3B guard held + `104.1 tok/s` short and `89.2 tok/s` sustained. The same build later + passed the full serial all-regression gate with `INCLUDE_QWEN35_A3B_GUARD=1`, + including Qwen3.6 27B MTP at `33.8` / `27.9 tok/s` and Qwen3.6 35B A3B MTP + at `104.1` / `89.3 tok/s`. + - A matching contiguous-pointer cleanup inside the MTP `draft()` loop was + tested and rejected. It replaced per-row + `llama_get_embeddings_pre_norm_ith(ctx_dft, i_batch)` calls with one + `llama_get_embeddings_pre_norm(ctx_dft)` pointer per draft decode + iteration. The 27B MTP guard held at `33.8` / `27.9 tok/s`, but the 35B A3B + repeat measured `104.3` / `88.7` and `104.3` / `89.2 tok/s`, below the + promoted `89.3 tok/s` sustained band, so the code change was removed. + - A single-sequence MTP `draft()` fast path was tested and rejected on + 2026-05-25. It removed the active-sequence bookkeeping loop for the common + `n_seq == 1` case and passed the dense 27B guard at `33.7` / `28.0 tok/s`, + but the 35B A3B sustained guard collapsed to `25.7 tok/s` despite a + passing `103.1 tok/s` short check. Reverting that path restored the 35B A3B + guard to `104.3` / `90.3 tok/s`, so the shared multi-sequence draft loop + remains the promoted implementation. + - A dual-scale-only finite-pack CPU quantizer shortcut was tested and + rejected on 2026-05-24. It passed correctness, but even after isolating + the shared scale chooser it regressed the protected FAST quant path + (`q4_0_rocmfp4_fast` normal quant rose into the `3882`-`4022` cycles / 32 + values band versus the clean `3623` cycles / 32 values baseline). The + final packing loops therefore remain on the guarded nearest-code helper. +- The Strix ROCmFP4 build script now builds the quant regression test + binaries (`test-quantize-fns`, `test-quantize-perf`, and + `test-backend-ops`) alongside `llama-cli`, `llama-quantize`, and + `llama-bench`, so quant and runtime guards work from a clean build + directory. +- `test-quantize-perf --imatrix` benchmarks the ROCmFP4 quality/coherence + path through `ggml_quantize_chunk(..., imatrix)` with synthetic importance + weights. The quant regression guard includes this mode to reject imatrix-side + CPU changes that would not show up in the normal non-imatrix timing. + +High-upside Strix-specific work: +- The next significant speed step is likely a fused ROCmFP4 decode-and-dot + ROCm matvec path. The current ROCmFP4 wins come from compact blocks and + avoiding generic decode/copy overhead; a fused path would keep Codebook10 + values and UE4M3 scales in registers/shared memory through the dot product + instead of materializing wider intermediates. +- A ROCmFP4-aware long-context attention path is the second major target. The + long-context limit is mostly KV traffic, FlashAttention shape efficiency, and + memory bandwidth. The current dual-scale layout protects coherence, but it is + not itself a large speed lever unless the attention kernels consume it + directly and efficiently. +- MTP target/draft overlap is a possible scheduler project, not a flag. The + current common MTP flow is serial on one Strix Halo ROCm device. Real overlap + would need scheduler changes plus acceptance/correctness guards, and it + should only be promoted if it beats the sustained Qwen 262k guard. +- Coherence should stay protected by tensor-aware ROCmFP4 profiles: dual-scale + for sensitive tensors and FAST only where guarded model tests show no + sustained decode or quality regression. Pure speed-only profiles are not + promoted in this tree without the serial regression gate. + +Hardware note: +- This is a special AMD-targeted ggml/llama.cpp quantization and backend + path. It includes custom Vulkan and ROCm/HIP handling for the new GGUF + types, but it is not yet a native rocWMMA FP4 tensor-core implementation. + Current speed gains come from the compact block layout and backend decode + paths; deeper rocWMMA/cooperative-matrix work is future optimization work. +- NVIDIA CUDA is disabled in the Strix-FP4 build (`-DGGML_CUDA=OFF`). Some + upstream llama.cpp HIP backend sources still live under + `ggml/src/ggml-cuda` and are compiled by HIP for AMD, but the ROCmFP4-owned + helper code and user-facing build/run path are ROCm/HIP/Vulkan targeted. + This tree also accepts `GGML_HIP_ENABLE_UNIFIED_MEMORY=1` as the AMD-named + alias for the upstream unified-memory switch. +- The bundled rocWMMA 7.1.0 headers expose gfx12 WMMA paths for FP8/BF8 and + integer 8-bit inputs, but no native FP4 input type or FP4 WMMA/MFMA builtin + is visible locally. A true matrix-core ROCmFP4 path therefore needs a + measured unpack/convert strategy first, such as ROCmFP4 Codebook10 to int8 + WMMA or FP8 WMMA tiles, before claiming native FP4 tensor-core execution. +- rocWMMA FlashAttention is intentionally opt-in via + `GGML_HIP_ROCWMMA_FATTN=ON scripts/build-strix-rocmfp4-mtp.sh`. + It currently compiles with the local rocWMMA headers in + `/path/to/third_party/rocWMMA`, but the Strix Halo benchmark + regressed sustained Qwen MTP decode (`23.3 tok/s` vs the promoted + `26.2 tok/s` default HIP FlashAttention path), so the default build keeps it + disabled. A follow-up on the 35B A3B ROCmFP4 MTP guard measured only + `99.7` / `76.1 tok/s` versus the promoted `104.4` / `89.3` band, so this + remains true for the MoE-heavy profile as well. +- TurboQuant and TriAttention are not present as runtime flags in this isolated + tree. Integrating them would require a source-level merge into `strix-fp4`; + do not claim support or promote them unless they beat the serial ROCmFP4 + regression gate. diff --git a/ggml/rocmfp4/rocmfp4.c b/ggml/rocmfp4/rocmfp4.c new file mode 100644 index 000000000000..bccefcec15da --- /dev/null +++ b/ggml/rocmfp4/rocmfp4.c @@ -0,0 +1,797 @@ +#define GGML_COMMON_DECL_C +#include "../src/ggml-common.h" + +#include "rocmfp4.h" + +#include +#include +#include + +// ggml-base is compiled architecture-neutral (no -mavx2), so SIMD for the hot +// CPU dot product is enabled per-function via a target attribute plus a runtime +// CPU check. This keeps the AVX2 path in one translation unit without exporting +// internals to ggml-cpu. Non-GNU/non-x86 builds fall back to the scalar loop. +#if defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__)) && !defined(ROCMFP4_NO_AVX2) +#include +#define ROCMFP4_X86_AVX2_DISPATCH 1 +#endif + +// ROCmFP4 stores a signed integer FP4-like codebook at half-scale. It is +// E2M1-derived, but the largest magnitude is retuned from 12 to 10 after +// sampling Qwen3 dense tensors; this reduces outlier pull without changing the +// packed 4-bit layout or integer dot-product path. +static const int8_t rocmfp4_codebook[16] = { + 0, 1, 2, 3, 4, 6, 8, 10, + 0, -1, -2, -3, -4, -6, -8,-10, +}; + +static inline int8_t rocmfp4_decode(uint8_t q) { + q &= 0x0f; + const int mag3 = q & 0x07; + const int mag = mag3 <= 4 ? mag3 : 2*mag3 - 4; + return (q & 0x08) ? -mag : mag; +} + +static inline int8_t rocmfp4_decode_table(uint8_t q) { + return rocmfp4_codebook[q & 0x0f]; +} + +// Finite unsigned E4M3 scale bytes decoded to the half-scale values used by +// ROCmFP4. Keeping this as a table avoids rebuilding identical FP32 values for +// every candidate during exhaustive scale search. +#define ROCMFP4_SCALE_SUB(M) ((M) * 0x1p-10f) +#define ROCMFP4_SCALE_E1(M) ((8 + (M)) * 0x1p-10f) +#define ROCMFP4_SCALE_E2(M) ((8 + (M)) * 0x1p-9f) +#define ROCMFP4_SCALE_E3(M) ((8 + (M)) * 0x1p-8f) +#define ROCMFP4_SCALE_E4(M) ((8 + (M)) * 0x1p-7f) +#define ROCMFP4_SCALE_E5(M) ((8 + (M)) * 0x1p-6f) +#define ROCMFP4_SCALE_E6(M) ((8 + (M)) * 0x1p-5f) +#define ROCMFP4_SCALE_E7(M) ((8 + (M)) * 0x1p-4f) +#define ROCMFP4_SCALE_E8(M) ((8 + (M)) * 0x1p-3f) +#define ROCMFP4_SCALE_E9(M) ((8 + (M)) * 0x1p-2f) +#define ROCMFP4_SCALE_E10(M) ((8 + (M)) * 0x1p-1f) +#define ROCMFP4_SCALE_E11(M) ((8 + (M)) * 0x1p0f) +#define ROCMFP4_SCALE_E12(M) ((8 + (M)) * 0x1p1f) +#define ROCMFP4_SCALE_E13(M) ((8 + (M)) * 0x1p2f) +#define ROCMFP4_SCALE_E14(M) ((8 + (M)) * 0x1p3f) +#define ROCMFP4_SCALE_E15(M) ((8 + (M)) * 0x1p4f) + +static const float rocmfp4_scale_ue4m3_half[127] = { + ROCMFP4_SCALE_SUB(0), ROCMFP4_SCALE_SUB(1), ROCMFP4_SCALE_SUB(2), ROCMFP4_SCALE_SUB(3), + ROCMFP4_SCALE_SUB(4), ROCMFP4_SCALE_SUB(5), ROCMFP4_SCALE_SUB(6), ROCMFP4_SCALE_SUB(7), + ROCMFP4_SCALE_E1(0), ROCMFP4_SCALE_E1(1), ROCMFP4_SCALE_E1(2), ROCMFP4_SCALE_E1(3), + ROCMFP4_SCALE_E1(4), ROCMFP4_SCALE_E1(5), ROCMFP4_SCALE_E1(6), ROCMFP4_SCALE_E1(7), + ROCMFP4_SCALE_E2(0), ROCMFP4_SCALE_E2(1), ROCMFP4_SCALE_E2(2), ROCMFP4_SCALE_E2(3), + ROCMFP4_SCALE_E2(4), ROCMFP4_SCALE_E2(5), ROCMFP4_SCALE_E2(6), ROCMFP4_SCALE_E2(7), + ROCMFP4_SCALE_E3(0), ROCMFP4_SCALE_E3(1), ROCMFP4_SCALE_E3(2), ROCMFP4_SCALE_E3(3), + ROCMFP4_SCALE_E3(4), ROCMFP4_SCALE_E3(5), ROCMFP4_SCALE_E3(6), ROCMFP4_SCALE_E3(7), + ROCMFP4_SCALE_E4(0), ROCMFP4_SCALE_E4(1), ROCMFP4_SCALE_E4(2), ROCMFP4_SCALE_E4(3), + ROCMFP4_SCALE_E4(4), ROCMFP4_SCALE_E4(5), ROCMFP4_SCALE_E4(6), ROCMFP4_SCALE_E4(7), + ROCMFP4_SCALE_E5(0), ROCMFP4_SCALE_E5(1), ROCMFP4_SCALE_E5(2), ROCMFP4_SCALE_E5(3), + ROCMFP4_SCALE_E5(4), ROCMFP4_SCALE_E5(5), ROCMFP4_SCALE_E5(6), ROCMFP4_SCALE_E5(7), + ROCMFP4_SCALE_E6(0), ROCMFP4_SCALE_E6(1), ROCMFP4_SCALE_E6(2), ROCMFP4_SCALE_E6(3), + ROCMFP4_SCALE_E6(4), ROCMFP4_SCALE_E6(5), ROCMFP4_SCALE_E6(6), ROCMFP4_SCALE_E6(7), + ROCMFP4_SCALE_E7(0), ROCMFP4_SCALE_E7(1), ROCMFP4_SCALE_E7(2), ROCMFP4_SCALE_E7(3), + ROCMFP4_SCALE_E7(4), ROCMFP4_SCALE_E7(5), ROCMFP4_SCALE_E7(6), ROCMFP4_SCALE_E7(7), + ROCMFP4_SCALE_E8(0), ROCMFP4_SCALE_E8(1), ROCMFP4_SCALE_E8(2), ROCMFP4_SCALE_E8(3), + ROCMFP4_SCALE_E8(4), ROCMFP4_SCALE_E8(5), ROCMFP4_SCALE_E8(6), ROCMFP4_SCALE_E8(7), + ROCMFP4_SCALE_E9(0), ROCMFP4_SCALE_E9(1), ROCMFP4_SCALE_E9(2), ROCMFP4_SCALE_E9(3), + ROCMFP4_SCALE_E9(4), ROCMFP4_SCALE_E9(5), ROCMFP4_SCALE_E9(6), ROCMFP4_SCALE_E9(7), + ROCMFP4_SCALE_E10(0), ROCMFP4_SCALE_E10(1), ROCMFP4_SCALE_E10(2), ROCMFP4_SCALE_E10(3), + ROCMFP4_SCALE_E10(4), ROCMFP4_SCALE_E10(5), ROCMFP4_SCALE_E10(6), ROCMFP4_SCALE_E10(7), + ROCMFP4_SCALE_E11(0), ROCMFP4_SCALE_E11(1), ROCMFP4_SCALE_E11(2), ROCMFP4_SCALE_E11(3), + ROCMFP4_SCALE_E11(4), ROCMFP4_SCALE_E11(5), ROCMFP4_SCALE_E11(6), ROCMFP4_SCALE_E11(7), + ROCMFP4_SCALE_E12(0), ROCMFP4_SCALE_E12(1), ROCMFP4_SCALE_E12(2), ROCMFP4_SCALE_E12(3), + ROCMFP4_SCALE_E12(4), ROCMFP4_SCALE_E12(5), ROCMFP4_SCALE_E12(6), ROCMFP4_SCALE_E12(7), + ROCMFP4_SCALE_E13(0), ROCMFP4_SCALE_E13(1), ROCMFP4_SCALE_E13(2), ROCMFP4_SCALE_E13(3), + ROCMFP4_SCALE_E13(4), ROCMFP4_SCALE_E13(5), ROCMFP4_SCALE_E13(6), ROCMFP4_SCALE_E13(7), + ROCMFP4_SCALE_E14(0), ROCMFP4_SCALE_E14(1), ROCMFP4_SCALE_E14(2), ROCMFP4_SCALE_E14(3), + ROCMFP4_SCALE_E14(4), ROCMFP4_SCALE_E14(5), ROCMFP4_SCALE_E14(6), ROCMFP4_SCALE_E14(7), + ROCMFP4_SCALE_E15(0), ROCMFP4_SCALE_E15(1), ROCMFP4_SCALE_E15(2), ROCMFP4_SCALE_E15(3), + ROCMFP4_SCALE_E15(4), ROCMFP4_SCALE_E15(5), ROCMFP4_SCALE_E15(6), +}; + +#undef ROCMFP4_SCALE_SUB +#undef ROCMFP4_SCALE_E1 +#undef ROCMFP4_SCALE_E2 +#undef ROCMFP4_SCALE_E3 +#undef ROCMFP4_SCALE_E4 +#undef ROCMFP4_SCALE_E5 +#undef ROCMFP4_SCALE_E6 +#undef ROCMFP4_SCALE_E7 +#undef ROCMFP4_SCALE_E8 +#undef ROCMFP4_SCALE_E9 +#undef ROCMFP4_SCALE_E10 +#undef ROCMFP4_SCALE_E11 +#undef ROCMFP4_SCALE_E12 +#undef ROCMFP4_SCALE_E13 +#undef ROCMFP4_SCALE_E14 +#undef ROCMFP4_SCALE_E15 + +static inline float rocmfp4_ue4m3_to_fp32_half(uint8_t e) { + return e <= 0x7e ? rocmfp4_scale_ue4m3_half[e] : 0.0f; +} + +static inline uint8_t rocmfp4_best_index_scaled_finite(float x, float inv_scale_half) { + // Exact nearest-neighbor thresholds for Codebook10: + // 0, +/-1, +/-2, +/-3, +/-4, +/-6, +/-8, +/-10 + // Ties intentionally choose the lower-magnitude code, matching the former + // linear scan because the positive codes and zero appear first. + const float a = fabsf(x * inv_scale_half); + if (a <= 0.5f) { + return 0; + } + + const bool neg = x < 0.0f; + if (a <= 1.5f) { + return neg ? 9 : 1; + } + if (a <= 2.5f) { + return neg ? 10 : 2; + } + if (a <= 3.5f) { + return neg ? 11 : 3; + } + if (a <= 5.0f) { + return neg ? 12 : 4; + } + if (a <= 7.0f) { + return neg ? 13 : 5; + } + if (a <= 9.0f) { + return neg ? 14 : 6; + } + + return neg ? 15 : 7; +} + +static inline uint8_t rocmfp4_best_index_scaled(float x, float inv_scale_half) { + if (!isfinite(x)) { + return 0; + } + + return rocmfp4_best_index_scaled_finite(x, inv_scale_half); +} + +// Fused best-index + decode used only inside the exhaustive scale search. The +// scale search re-scans every block element for every candidate scale byte, so +// avoiding the code -> decode round-trip on the hottest quantize path matters. +// Returns the same signed Codebook10 magnitude that +// rocmfp4_decode(rocmfp4_best_index_scaled_finite(x, inv_scale_half)) produces, +// so quantized output is bit-identical to the previous path. +static inline float rocmfp4_decoded_mag_scaled_finite(float x, float inv_scale_half) { + const float a = fabsf(x * inv_scale_half); + + float mag; + if (a <= 0.5f) { + mag = 0.0f; + } else if (a <= 1.5f) { + mag = 1.0f; + } else if (a <= 2.5f) { + mag = 2.0f; + } else if (a <= 3.5f) { + mag = 3.0f; + } else if (a <= 5.0f) { + mag = 4.0f; + } else if (a <= 7.0f) { + mag = 6.0f; + } else if (a <= 9.0f) { + mag = 8.0f; + } else { + mag = 10.0f; + } + + return x < 0.0f ? -mag : mag; +} + +static inline float rocmfp4_decoded_mag_scaled(float x, float inv_scale_half) { + if (!isfinite(x)) { + return 0.0f; + } + + return rocmfp4_decoded_mag_scaled_finite(x, inv_scale_half); +} + +static inline bool rocmfp4_scale_is_valid(uint8_t e) { + // ROCmFP4 scale bytes are unsigned finite E4M3 values. 0x7f is NaN in the + // unsigned encoding and values with the sign bit set are not valid scales. + return e <= 0x7e; +} + +static float rocmfp4_block_mse_for_scale_unweighted( + const float * x, int n, int e, float best_err) { + const float scale_half = rocmfp4_ue4m3_to_fp32_half((uint8_t) e); + const float inv_scale_half = 1.0f / scale_half; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float y = rocmfp4_decoded_mag_scaled(x[i], inv_scale_half) * scale_half; + const float d = x[i] - y; + + err += d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static float rocmfp4_block_mse_for_scale_unweighted_finite( + const float * x, int n, int e, float best_err) { + const float scale_half = rocmfp4_ue4m3_to_fp32_half((uint8_t) e); + const float inv_scale_half = 1.0f / scale_half; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float y = rocmfp4_decoded_mag_scaled_finite(x[i], inv_scale_half) * scale_half; + const float d = x[i] - y; + + err += d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static float rocmfp4_block_mse_for_scale_weighted( + const float * x, int n, const float * mse_weights, int e, float best_err) { + const float scale_half = rocmfp4_ue4m3_to_fp32_half((uint8_t) e); + const float inv_scale_half = 1.0f / scale_half; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float y = rocmfp4_decoded_mag_scaled(x[i], inv_scale_half) * scale_half; + const float d = x[i] - y; + + err += mse_weights[i]*d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static float rocmfp4_block_mse_for_scale_weighted_finite( + const float * x, int n, const float * mse_weights, int e, float best_err) { + const float scale_half = rocmfp4_ue4m3_to_fp32_half((uint8_t) e); + const float inv_scale_half = 1.0f / scale_half; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float y = rocmfp4_decoded_mag_scaled_finite(x[i], inv_scale_half) * scale_half; + const float d = x[i] - y; + + err += mse_weights[i]*d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static void rocmfp4_prepare_mse_weights( + float * dst, const float * x, int n, const float * quant_weights, float sigma2, + float * max_abs, float * max_abs_weight, bool * all_finite) { + *max_abs = 0.0f; + *max_abs_weight = 0.0f; + *all_finite = true; + + for (int i = 0; i < n; ++i) { + const float qw = quant_weights[i]; + const float ax = fabsf(x[i]); + const float weight = isfinite(qw) && qw > 0.0f ? qw * sqrtf(sigma2 + x[i]*x[i]) : 0.0f; + *all_finite = *all_finite && isfinite(x[i]); + + if (ax > *max_abs) { + *max_abs = ax; + *max_abs_weight = weight; + } else if (ax == *max_abs && weight > *max_abs_weight) { + *max_abs_weight = weight; + } + + // Match llama.cpp's imatrix weighting style for Q4_0: calibration + // importance is scaled by row energy so large activations remain protected. + dst[i] = weight; + } +} + +static int rocmfp4_nearest_scale_ue4m3(float target_scale_half) { + if (!(target_scale_half > 0.0f) || !isfinite(target_scale_half)) { + return 1; + } + + int lo = 1; + int hi = 126; + while (lo < hi) { + const int mid = lo + (hi - lo) / 2; + if (rocmfp4_ue4m3_to_fp32_half((uint8_t) mid) < target_scale_half) { + lo = mid + 1; + } else { + hi = mid; + } + } + + if (lo == 1) { + return 1; + } + + const float hi_scale = rocmfp4_ue4m3_to_fp32_half((uint8_t) lo); + const float lo_scale = rocmfp4_ue4m3_to_fp32_half((uint8_t) (lo - 1)); + + // Match the former ascending nearest scan: exact midpoint ties keep the + // lower scale byte. + return (target_scale_half - lo_scale <= hi_scale - target_scale_half) ? lo - 1 : lo; +} + +static uint8_t rocmfp4_choose_scale_ue4m3_exhaustive_unweighted( + const float * x, int n, float max_abs, bool all_finite) { + const int start_e = rocmfp4_nearest_scale_ue4m3(max_abs / 10.0f); + + int best_e = 0; + float best_err = FLT_MAX; + bool lower_done = false; + + for (int delta = 0; delta <= 125; ++delta) { + const int e0 = start_e - delta; + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale_half = rocmfp4_ue4m3_to_fp32_half((uint8_t) e0); + const float clip_delta = max_abs - 10.0f*scale_half; + if (clip_delta > 0.0f && clip_delta*clip_delta > best_err) { + lower_done = true; + } else { + const float err = all_finite ? + rocmfp4_block_mse_for_scale_unweighted_finite(x, n, e0, best_err) : + rocmfp4_block_mse_for_scale_unweighted(x, n, e0, best_err); + if (err < best_err || (err == best_err && e0 < best_e)) { + best_err = err; + best_e = e0; + } + } + } + + const int e1 = start_e + delta; + if (delta != 0 && e1 >= 1 && e1 <= 126) { + const float err = all_finite ? + rocmfp4_block_mse_for_scale_unweighted_finite(x, n, e1, best_err) : + rocmfp4_block_mse_for_scale_unweighted(x, n, e1, best_err); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126) { + break; + } + } + + return (uint8_t) best_e; +} + +static uint8_t rocmfp4_choose_scale_ue4m3_exhaustive_weighted( + const float * x, int n, const float * mse_weights, float max_abs, float max_abs_weight, bool all_finite) { + const int start_e = rocmfp4_nearest_scale_ue4m3(max_abs / 10.0f); + + int best_e = 0; + float best_err = FLT_MAX; + bool lower_done = false; + + for (int delta = 0; delta <= 125; ++delta) { + const int e0 = start_e - delta; + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale_half = rocmfp4_ue4m3_to_fp32_half((uint8_t) e0); + const float clip_delta = max_abs - 10.0f*scale_half; + if (max_abs_weight > 0.0f && clip_delta > 0.0f && max_abs_weight*clip_delta*clip_delta > best_err) { + lower_done = true; + } else { + const float err = all_finite ? + rocmfp4_block_mse_for_scale_weighted_finite(x, n, mse_weights, e0, best_err) : + rocmfp4_block_mse_for_scale_weighted(x, n, mse_weights, e0, best_err); + if (err < best_err || (err == best_err && e0 < best_e)) { + best_err = err; + best_e = e0; + } + } + } + + const int e1 = start_e + delta; + if (delta != 0 && e1 >= 1 && e1 <= 126) { + const float err = all_finite ? + rocmfp4_block_mse_for_scale_weighted_finite(x, n, mse_weights, e1, best_err) : + rocmfp4_block_mse_for_scale_weighted(x, n, mse_weights, e1, best_err); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126) { + break; + } + } + + return (uint8_t) best_e; +} + +static uint8_t rocmfp4_choose_scale_ue4m3(const float * x, int n, const float * quant_weights, float sigma2) { + if (quant_weights) { + assert(n <= QK_ROCMFP4); + float mse_weights_buf[QK_ROCMFP4]; + float weighted_max_abs; + float max_abs_weight; + bool all_finite; + rocmfp4_prepare_mse_weights(mse_weights_buf, x, n, quant_weights, sigma2, &weighted_max_abs, &max_abs_weight, &all_finite); + if (!(weighted_max_abs > 0.0f) || !isfinite(weighted_max_abs)) { + return 0; + } + return rocmfp4_choose_scale_ue4m3_exhaustive_weighted(x, n, mse_weights_buf, weighted_max_abs, max_abs_weight, all_finite); + } + + float max_abs = 0.0f; + bool all_finite = true; + for (int i = 0; i < n; ++i) { + all_finite = all_finite && isfinite(x[i]); + const float ax = fabsf(x[i]); + if (ax > max_abs) { + max_abs = ax; + } + } + + if (!(max_abs > 0.0f) || !isfinite(max_abs)) { + return 0; + } + + return rocmfp4_choose_scale_ue4m3_exhaustive_unweighted(x, n, max_abs, all_finite); +} + +static void rocmfp4_quantize_row_q4_0_weighted( + const float * GGML_RESTRICT x, block_rocmfp4 * GGML_RESTRICT y, int64_t k, const float * GGML_RESTRICT quant_weights) { + assert(k % QK_ROCMFP4 == 0); + + float sum_x2 = 0.0f; + for (int64_t i = 0; i < k; ++i) { + sum_x2 += x[i]*x[i]; + } + const float sigma2 = sum_x2 / (float) k; + + const int64_t nb = k / QK_ROCMFP4; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP4; + const float * qw = quant_weights ? quant_weights + ib*QK_ROCMFP4 : NULL; + const uint8_t e0 = rocmfp4_choose_scale_ue4m3(xb, QK_ROCMFP4/2, qw, sigma2); + const uint8_t e1 = rocmfp4_choose_scale_ue4m3(xb + QK_ROCMFP4/2, QK_ROCMFP4/2, qw ? qw + QK_ROCMFP4/2 : NULL, sigma2); + const float scale_half0 = rocmfp4_ue4m3_to_fp32_half(e0); + const float scale_half1 = rocmfp4_ue4m3_to_fp32_half(e1); + const float inv_scale_half0 = scale_half0 > 0.0f ? 1.0f / scale_half0 : 0.0f; + const float inv_scale_half1 = scale_half1 > 0.0f ? 1.0f / scale_half1 : 0.0f; + + y[ib].e[0] = e0; + y[ib].e[1] = e1; + + for (int j = 0; j < QK_ROCMFP4/2; ++j) { + const uint8_t q0 = rocmfp4_best_index_scaled(xb[j], inv_scale_half0); + const uint8_t q1 = rocmfp4_best_index_scaled(xb[j + QK_ROCMFP4/2], inv_scale_half1); + y[ib].qs[j] = q0 | (q1 << 4); + } + } +} + +static void rocmfp4_quantize_row_q4_0_fast_weighted( + const float * GGML_RESTRICT x, block_rocmfp4_fast * GGML_RESTRICT y, int64_t k, const float * GGML_RESTRICT quant_weights) { + assert(k % QK_ROCMFP4 == 0); + + float sum_x2 = 0.0f; + for (int64_t i = 0; i < k; ++i) { + sum_x2 += x[i]*x[i]; + } + const float sigma2 = sum_x2 / (float) k; + + const int64_t nb = k / QK_ROCMFP4; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP4; + const float * qw = quant_weights ? quant_weights + ib*QK_ROCMFP4 : NULL; + const uint8_t e = rocmfp4_choose_scale_ue4m3(xb, QK_ROCMFP4, qw, sigma2); + const float scale_half = rocmfp4_ue4m3_to_fp32_half(e); + const float inv_scale_half = scale_half > 0.0f ? 1.0f / scale_half : 0.0f; + + y[ib].e = e; + + for (int j = 0; j < QK_ROCMFP4/2; ++j) { + const uint8_t q0 = rocmfp4_best_index_scaled(xb[j], inv_scale_half); + const uint8_t q1 = rocmfp4_best_index_scaled(xb[j + QK_ROCMFP4/2], inv_scale_half); + y[ib].qs[j] = q0 | (q1 << 4); + } + } +} + +void rocmfp4_quantize_row_q4_0_ref(const float * GGML_RESTRICT x, block_rocmfp4 * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP4 == 0); + + const int64_t nb = k / QK_ROCMFP4; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP4; + const uint8_t e0 = rocmfp4_choose_scale_ue4m3(xb, QK_ROCMFP4/2, NULL, 0.0f); + const uint8_t e1 = rocmfp4_choose_scale_ue4m3(xb + QK_ROCMFP4/2, QK_ROCMFP4/2, NULL, 0.0f); + const float scale_half0 = rocmfp4_ue4m3_to_fp32_half(e0); + const float scale_half1 = rocmfp4_ue4m3_to_fp32_half(e1); + const float inv_scale_half0 = scale_half0 > 0.0f ? 1.0f / scale_half0 : 0.0f; + const float inv_scale_half1 = scale_half1 > 0.0f ? 1.0f / scale_half1 : 0.0f; + + y[ib].e[0] = e0; + y[ib].e[1] = e1; + + for (int j = 0; j < QK_ROCMFP4/2; ++j) { + const uint8_t q0 = rocmfp4_best_index_scaled(xb[j], inv_scale_half0); + const uint8_t q1 = rocmfp4_best_index_scaled(xb[j + QK_ROCMFP4/2], inv_scale_half1); + y[ib].qs[j] = q0 | (q1 << 4); + } + } +} + +void rocmfp4_quantize_row_q4_0_fast_ref(const float * GGML_RESTRICT x, block_rocmfp4_fast * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP4 == 0); + + const int64_t nb = k / QK_ROCMFP4; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP4; + const uint8_t e = rocmfp4_choose_scale_ue4m3(xb, QK_ROCMFP4, NULL, 0.0f); + const float scale_half = rocmfp4_ue4m3_to_fp32_half(e); + const float inv_scale_half = scale_half > 0.0f ? 1.0f / scale_half : 0.0f; + + y[ib].e = e; + + for (int j = 0; j < QK_ROCMFP4/2; ++j) { + const uint8_t q0 = rocmfp4_best_index_scaled(xb[j], inv_scale_half); + const uint8_t q1 = rocmfp4_best_index_scaled(xb[j + QK_ROCMFP4/2], inv_scale_half); + y[ib].qs[j] = q0 | (q1 << 4); + } + } +} + +void rocmfp4_dequantize_row_q4_0(const block_rocmfp4 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP4 == 0); + + const int64_t nb = k / QK_ROCMFP4; + for (int64_t ib = 0; ib < nb; ++ib) { + const float d0 = rocmfp4_ue4m3_to_fp32_half(x[ib].e[0]); + const float d1 = rocmfp4_ue4m3_to_fp32_half(x[ib].e[1]); + + for (int j = 0; j < QK_ROCMFP4/2; ++j) { + y[ib*QK_ROCMFP4 + j] = (float) rocmfp4_decode(x[ib].qs[j] & 0x0f) * d0; + y[ib*QK_ROCMFP4 + j + QK_ROCMFP4/2] = (float) rocmfp4_decode(x[ib].qs[j] >> 4) * d1; + } + } +} + +void rocmfp4_dequantize_row_q4_0_fast(const block_rocmfp4_fast * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP4 == 0); + + const int64_t nb = k / QK_ROCMFP4; + for (int64_t ib = 0; ib < nb; ++ib) { + const float d = rocmfp4_ue4m3_to_fp32_half(x[ib].e); + + for (int j = 0; j < QK_ROCMFP4/2; ++j) { + y[ib*QK_ROCMFP4 + j] = (float) rocmfp4_decode(x[ib].qs[j] & 0x0f) * d; + y[ib*QK_ROCMFP4 + j + QK_ROCMFP4/2] = (float) rocmfp4_decode(x[ib].qs[j] >> 4) * d; + } + } +} + +void rocmfp4_quantize_row_q4_0(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k) { + rocmfp4_quantize_row_q4_0_ref(x, (block_rocmfp4 *) y, k); +} + +void rocmfp4_quantize_row_q4_0_fast(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k) { + rocmfp4_quantize_row_q4_0_fast_ref(x, (block_rocmfp4_fast *) y, k); +} + +size_t rocmfp4_quantize_q4_0(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix) { + const size_t row_size = ggml_row_size(GGML_TYPE_Q4_0_ROCMFP4, n_per_row); + + if (!imatrix) { + rocmfp4_quantize_row_q4_0_ref(src, (block_rocmfp4 *) dst, nrows*n_per_row); + return nrows * row_size; + } + + char * qrow = (char *) dst; + for (int64_t row = 0; row < nrows; ++row) { + rocmfp4_quantize_row_q4_0_weighted(src, (block_rocmfp4 *) qrow, n_per_row, imatrix); + src += n_per_row; + qrow += row_size; + } + + return nrows * row_size; +} + +size_t rocmfp4_quantize_q4_0_fast(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix) { + const size_t row_size = ggml_row_size(GGML_TYPE_Q4_0_ROCMFP4_FAST, n_per_row); + + if (!imatrix) { + rocmfp4_quantize_row_q4_0_fast_ref(src, (block_rocmfp4_fast *) dst, nrows*n_per_row); + return nrows * row_size; + } + + char * qrow = (char *) dst; + for (int64_t row = 0; row < nrows; ++row) { + rocmfp4_quantize_row_q4_0_fast_weighted(src, (block_rocmfp4_fast *) qrow, n_per_row, imatrix); + src += n_per_row; + qrow += row_size; + } + + return nrows * row_size; +} + +bool rocmfp4_validate_row_data(const void * data, size_t nbytes) { + if (nbytes % sizeof(block_rocmfp4) != 0) { + return false; + } + + const block_rocmfp4 * blocks = (const block_rocmfp4 *) data; + const size_t nblocks = nbytes / sizeof(block_rocmfp4); + for (size_t i = 0; i < nblocks; ++i) { + if (!rocmfp4_scale_is_valid(blocks[i].e[0]) || !rocmfp4_scale_is_valid(blocks[i].e[1])) { + return false; + } + } + + return true; +} + +bool rocmfp4_validate_row_data_fast(const void * data, size_t nbytes) { + if (nbytes % sizeof(block_rocmfp4_fast) != 0) { + return false; + } + + const block_rocmfp4_fast * blocks = (const block_rocmfp4_fast *) data; + const size_t nblocks = nbytes / sizeof(block_rocmfp4_fast); + for (size_t i = 0; i < nblocks; ++i) { + if (!rocmfp4_scale_is_valid(blocks[i].e)) { + return false; + } + } + + return true; +} + +#ifdef ROCMFP4_X86_AVX2_DISPATCH +__attribute__((target("avx2"))) +static inline int rocmfp4_hsum_i32_8_avx2(__m256i v) { + __m128i s = _mm_add_epi32(_mm256_castsi256_si128(v), _mm256_extracti128_si256(v, 1)); + s = _mm_add_epi32(s, _mm_shuffle_epi32(s, _MM_SHUFFLE(1, 0, 3, 2))); + s = _mm_add_epi32(s, _mm_shuffle_epi32(s, _MM_SHUFFLE(2, 3, 0, 1))); + return _mm_cvtsi128_si32(s); +} + +// Decode one 32-weight block's low and high nibble streams through the +// Codebook10 table with a single PSHUFB, then integer-dot each against its half +// of the q8_0 block. Integer sums are order-independent, so sumi0/sumi1 match +// the scalar reference exactly and the float result is bit-identical. +__attribute__((target("avx2"))) +static inline void rocmfp4_block_isums_avx2( + const uint8_t * qs, const int8_t * q8, int * sumi0, int * sumi1) { + const __m128i tbl = _mm_loadu_si128((const __m128i *) rocmfp4_codebook); + const __m128i q = _mm_loadu_si128((const __m128i *) qs); + const __m128i lo = _mm_and_si128(q, _mm_set1_epi8(0x0F)); + const __m128i hi = _mm_and_si128(_mm_srli_epi16(q, 4), _mm_set1_epi8(0x0F)); + const __m128i dlo = _mm_shuffle_epi8(tbl, lo); + const __m128i dhi = _mm_shuffle_epi8(tbl, hi); + const __m128i ylo = _mm_loadu_si128((const __m128i *) q8); + const __m128i yhi = _mm_loadu_si128((const __m128i *) (q8 + QK_ROCMFP4/2)); + const __m256i pl = _mm256_madd_epi16(_mm256_cvtepi8_epi16(dlo), _mm256_cvtepi8_epi16(ylo)); + const __m256i ph = _mm256_madd_epi16(_mm256_cvtepi8_epi16(dhi), _mm256_cvtepi8_epi16(yhi)); + *sumi0 = rocmfp4_hsum_i32_8_avx2(pl); + *sumi1 = rocmfp4_hsum_i32_8_avx2(ph); +} + +__attribute__((target("avx2"))) +static void rocmfp4_vec_dot_q4_0_q8_0_avx2( + int nb, float * GGML_RESTRICT s, const block_rocmfp4 * GGML_RESTRICT x, const block_q8_0 * GGML_RESTRICT y) { + float sumf = 0.0f; + for (int ib = 0; ib < nb; ++ib) { + const float d0 = rocmfp4_ue4m3_to_fp32_half(x[ib].e[0]) * ggml_fp16_to_fp32(y[ib].d); + const float d1 = rocmfp4_ue4m3_to_fp32_half(x[ib].e[1]) * ggml_fp16_to_fp32(y[ib].d); + int sumi0, sumi1; + rocmfp4_block_isums_avx2(x[ib].qs, y[ib].qs, &sumi0, &sumi1); + sumf += d0 * (float) sumi0 + d1 * (float) sumi1; + } + *s = sumf; +} + +__attribute__((target("avx2"))) +static void rocmfp4_vec_dot_q4_0_fast_q8_0_avx2( + int nb, float * GGML_RESTRICT s, const block_rocmfp4_fast * GGML_RESTRICT x, const block_q8_0 * GGML_RESTRICT y) { + float sumf = 0.0f; + for (int ib = 0; ib < nb; ++ib) { + const float d = rocmfp4_ue4m3_to_fp32_half(x[ib].e) * ggml_fp16_to_fp32(y[ib].d); + int sumi0, sumi1; + rocmfp4_block_isums_avx2(x[ib].qs, y[ib].qs, &sumi0, &sumi1); + sumf += d * (float) (sumi0 + sumi1); + } + *s = sumf; +} +#endif // ROCMFP4_X86_AVX2_DISPATCH + +void rocmfp4_vec_dot_q4_0_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + GGML_UNUSED(bs); + GGML_UNUSED(bx); + GGML_UNUSED(by); + assert(nrc == 1); + GGML_UNUSED(nrc); + assert(n % QK_ROCMFP4 == 0); + assert(QK_ROCMFP4 == QK8_0); + + const block_rocmfp4 * GGML_RESTRICT x = (const block_rocmfp4 *) vx; + const block_q8_0 * GGML_RESTRICT y = (const block_q8_0 *) vy; + + const int nb = n / QK_ROCMFP4; + +#ifdef ROCMFP4_X86_AVX2_DISPATCH + if (__builtin_cpu_supports("avx2")) { + rocmfp4_vec_dot_q4_0_q8_0_avx2(nb, s, x, y); + return; + } +#endif + + float sumf = 0.0f; + + for (int ib = 0; ib < nb; ++ib) { + const float d0 = rocmfp4_ue4m3_to_fp32_half(x[ib].e[0]) * ggml_fp16_to_fp32(y[ib].d); + const float d1 = rocmfp4_ue4m3_to_fp32_half(x[ib].e[1]) * ggml_fp16_to_fp32(y[ib].d); + int sumi0 = 0; + int sumi1 = 0; + + for (int j = 0; j < QK_ROCMFP4/2; ++j) { + const uint8_t q = x[ib].qs[j]; + sumi0 += rocmfp4_decode_table(q) * y[ib].qs[j]; + sumi1 += rocmfp4_decode_table(q >> 4) * y[ib].qs[j + QK_ROCMFP4/2]; + } + + sumf += d0 * (float) sumi0 + d1 * (float) sumi1; + } + + *s = sumf; +} + +void rocmfp4_vec_dot_q4_0_fast_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + GGML_UNUSED(bs); + GGML_UNUSED(bx); + GGML_UNUSED(by); + assert(nrc == 1); + GGML_UNUSED(nrc); + assert(n % QK_ROCMFP4 == 0); + assert(QK_ROCMFP4 == QK8_0); + + const block_rocmfp4_fast * GGML_RESTRICT x = (const block_rocmfp4_fast *) vx; + const block_q8_0 * GGML_RESTRICT y = (const block_q8_0 *) vy; + + const int nb = n / QK_ROCMFP4; + +#ifdef ROCMFP4_X86_AVX2_DISPATCH + if (__builtin_cpu_supports("avx2")) { + rocmfp4_vec_dot_q4_0_fast_q8_0_avx2(nb, s, x, y); + return; + } +#endif + + float sumf = 0.0f; + + for (int ib = 0; ib < nb; ++ib) { + const float d = rocmfp4_ue4m3_to_fp32_half(x[ib].e) * ggml_fp16_to_fp32(y[ib].d); + int sumi = 0; + + for (int j = 0; j < QK_ROCMFP4/2; ++j) { + const uint8_t q = x[ib].qs[j]; + sumi += rocmfp4_decode_table(q) * y[ib].qs[j]; + sumi += rocmfp4_decode_table(q >> 4) * y[ib].qs[j + QK_ROCMFP4/2]; + } + + sumf += d * (float) sumi; + } + + *s = sumf; +} diff --git a/ggml/rocmfp4/rocmfp4.h b/ggml/rocmfp4/rocmfp4.h new file mode 100644 index 000000000000..9756f6ad4c3b --- /dev/null +++ b/ggml/rocmfp4/rocmfp4.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include "ggml.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define QK_ROCMFP4 32 +#define QR_ROCMFP4 2 +#define QI_ROCMFP4 (QK_ROCMFP4 / (4 * QR_ROCMFP4)) +#define QS_ROCMFP4 32 + +// AMD-tuned compact layout: 16 bytes of packed E2M1-derived 4-bit codes, then +// one unsigned E4M3 scale byte per 16-weight half block. +typedef struct { + uint8_t qs[QK_ROCMFP4/2]; + uint8_t e[2]; +} block_rocmfp4; + +// Speed-focused layout: same 32 packed ROCmFP4 nibbles, but one UE4M3 scale +// for the whole block. This is a separate GGUF type so fast 4.25 BPW artifacts +// never alias the safer dual-scale format above. +typedef struct { + uint8_t qs[QK_ROCMFP4/2]; + uint8_t e; +} block_rocmfp4_fast; + +#if defined(__cplusplus) +static_assert(sizeof(block_rocmfp4) == QK_ROCMFP4/2 + 2*sizeof(uint8_t), "wrong rocmfp4 block size/padding"); +static_assert(sizeof(block_rocmfp4_fast) == QK_ROCMFP4/2 + sizeof(uint8_t), "wrong rocmfp4 fast block size/padding"); +#else +_Static_assert(sizeof(block_rocmfp4) == QK_ROCMFP4/2 + 2*sizeof(uint8_t), "wrong rocmfp4 block size/padding"); +_Static_assert(sizeof(block_rocmfp4_fast) == QK_ROCMFP4/2 + sizeof(uint8_t), "wrong rocmfp4 fast block size/padding"); +#endif + +GGML_API void rocmfp4_quantize_row_q4_0_ref(const float * GGML_RESTRICT x, block_rocmfp4 * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfp4_dequantize_row_q4_0(const block_rocmfp4 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfp4_quantize_row_q4_0_fast_ref(const float * GGML_RESTRICT x, block_rocmfp4_fast * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfp4_dequantize_row_q4_0_fast(const block_rocmfp4_fast * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); + +GGML_API void rocmfp4_quantize_row_q4_0(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); +GGML_API size_t rocmfp4_quantize_q4_0(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); +GGML_API void rocmfp4_quantize_row_q4_0_fast(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); +GGML_API size_t rocmfp4_quantize_q4_0_fast(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); +GGML_API bool rocmfp4_validate_row_data(const void * data, size_t nbytes); +GGML_API bool rocmfp4_validate_row_data_fast(const void * data, size_t nbytes); + +GGML_API void rocmfp4_vec_dot_q4_0_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); +GGML_API void rocmfp4_vec_dot_q4_0_fast_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc); + +#ifdef __cplusplus +} +#endif diff --git a/ggml/rocmfp4/rocmfp4_hip.cu b/ggml/rocmfp4/rocmfp4_hip.cu new file mode 100644 index 000000000000..d2c9048c1876 --- /dev/null +++ b/ggml/rocmfp4/rocmfp4_hip.cu @@ -0,0 +1,85 @@ +#include "rocmfp4.h" + +#include + +#include "rocmfp4_hip_scale.cuh" + +// Standalone ROCm/HIP dequant kernel for integration tests and future fused +// paths. One lane owns one packed byte and writes the matching low/high +// half-block values, so each byte is read once. +extern "C" __global__ void rocmfp4_dequantize_q4_0_f32_kernel( + const block_rocmfp4 * __restrict__ x, + float * __restrict__ y, + int64_t k) { + const int64_t packed_idx = (int64_t) blockIdx.x*blockDim.x + threadIdx.x; + const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; + const int64_t packed_count = nblocks * (QK_ROCMFP4/2); + + if (packed_idx >= packed_count) { + return; + } + + const int64_t ib = packed_idx / (QK_ROCMFP4/2); + const int tid = packed_idx - ib*(QK_ROCMFP4/2); + const int64_t base = ib*QK_ROCMFP4; + const uint8_t packed = x[ib].qs[tid]; + const float d0 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[0]); + const float d1 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[1]); + + if (base + tid < k) { + y[base + tid] = (float) rocmfp4_decode_i8(packed & 0x0f) * d0; + } + if (base + tid + QK_ROCMFP4/2 < k) { + y[base + tid + QK_ROCMFP4/2] = (float) rocmfp4_decode_i8(packed >> 4) * d1; + } +} + +extern "C" __global__ void rocmfp4_dequantize_q4_0_fast_f32_kernel( + const block_rocmfp4_fast * __restrict__ x, + float * __restrict__ y, + int64_t k) { + const int64_t packed_idx = (int64_t) blockIdx.x*blockDim.x + threadIdx.x; + const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; + const int64_t packed_count = nblocks * (QK_ROCMFP4/2); + + if (packed_idx >= packed_count) { + return; + } + + const int64_t ib = packed_idx / (QK_ROCMFP4/2); + const int tid = packed_idx - ib*(QK_ROCMFP4/2); + const int64_t base = ib*QK_ROCMFP4; + const uint8_t packed = x[ib].qs[tid]; + const float d = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e); + + if (base + tid < k) { + y[base + tid] = (float) rocmfp4_decode_i8(packed & 0x0f) * d; + } + if (base + tid + QK_ROCMFP4/2 < k) { + y[base + tid + QK_ROCMFP4/2] = (float) rocmfp4_decode_i8(packed >> 4) * d; + } +} + +extern "C" void rocmfp4_hip_dequantize_q4_0_to_f32( + const void * src, + float * dst, + int64_t k, + hipStream_t stream) { + const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; + const int64_t packed_count = nblocks * (QK_ROCMFP4/2); + const dim3 block(256); + const dim3 grid((unsigned int) ((packed_count + block.x - 1) / block.x)); + rocmfp4_dequantize_q4_0_f32_kernel<<>>((const block_rocmfp4 *) src, dst, k); +} + +extern "C" void rocmfp4_hip_dequantize_q4_0_fast_to_f32( + const void * src, + float * dst, + int64_t k, + hipStream_t stream) { + const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; + const int64_t packed_count = nblocks * (QK_ROCMFP4/2); + const dim3 block(256); + const dim3 grid((unsigned int) ((packed_count + block.x - 1) / block.x)); + rocmfp4_dequantize_q4_0_fast_f32_kernel<<>>((const block_rocmfp4_fast *) src, dst, k); +} diff --git a/ggml/rocmfp4/rocmfp4_hip_codebook.cuh b/ggml/rocmfp4/rocmfp4_hip_codebook.cuh new file mode 100644 index 000000000000..1d91061129a3 --- /dev/null +++ b/ggml/rocmfp4/rocmfp4_hip_codebook.cuh @@ -0,0 +1,79 @@ +#pragma once + +#include "rocmfp4_hip_scale.cuh" + +#include +#include + +#ifndef GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD +#define GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD 1 +#endif + +static __device__ __forceinline__ int rocmfp4_get_qs_i32(const void * x, const int & i32) { +#if defined(GGML_USE_HIP) && GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD + return *((const int *) ((const uint8_t *) x + 4*i32)); +#else + const uint8_t * x8 = (const uint8_t *) x; + + int x32 = x8[4*i32 + 0] << 0; + x32 |= x8[4*i32 + 1] << 8; + x32 |= x8[4*i32 + 2] << 16; + x32 |= x8[4*i32 + 3] << 24; + + return x32; +#endif +} + +// AMD-specific fast path for expanding eight packed ROCmFP4 nibbles into two +// int32 DP4A operands. This encodes the Codebook10 table directly as four +// 32-bit constants: +// [0, 1, 2, 3], [4, 6, 8, 10], [0, -1, -2, -3], [-4, -6, -8, -10] +// Avoiding the table pointer keeps the ROCm/HIP MMVQ/MMQ hot path fully local +// to this format. Non-HIP builds still use llama.cpp's generic table expander. +static __device__ __forceinline__ int2 rocmfp4_get_int_from_codebook_16(const int & q4, const int8_t * fallback_table) { +#if defined(GGML_USE_HIP) + constexpr uint32_t values0 = 0x03020100u; + constexpr uint32_t values1 = 0x0a080604u; + constexpr uint32_t values2 = 0xfdfeff00u; + constexpr uint32_t values3 = 0xf6f8fafcu; + + const uint32_t q_even = q4; + const uint32_t q_odd = q4 >> 4; + + const uint32_t v_even_low = __builtin_amdgcn_perm(values1, values0, q_even & 0x07070707u); + const uint32_t v_odd_low = __builtin_amdgcn_perm(values1, values0, q_odd & 0x07070707u); + const uint32_t v_even_high = __builtin_amdgcn_perm(values3, values2, q_even & 0x07070707u); + const uint32_t v_odd_high = __builtin_amdgcn_perm(values3, values2, q_odd & 0x07070707u); + + const uint32_t mask_even = 0x03020100u | ((q_even & 0x08080808u) >> 1); + const uint32_t mask_odd = 0x03020100u | ((q_odd & 0x08080808u) >> 1); + + return make_int2( + __builtin_amdgcn_perm(v_even_high, v_even_low, mask_even), + __builtin_amdgcn_perm(v_odd_high, v_odd_low, mask_odd)); +#else + return get_int_from_table_16(q4, fallback_table); +#endif +} + +// Variant for call sites that already selected either the low or high nibble +// stream and only need one DP4A operand. This avoids the extra odd/even table +// expansion work in ROCmFP4 FlashAttention K/V decode. +static __device__ __forceinline__ int rocmfp4_get_low_int_from_codebook_16(const int & q4, const int8_t * fallback_table) { +#if defined(GGML_USE_HIP) + constexpr uint32_t values0 = 0x03020100u; + constexpr uint32_t values1 = 0x0a080604u; + constexpr uint32_t values2 = 0xfdfeff00u; + constexpr uint32_t values3 = 0xf6f8fafcu; + + const uint32_t q = q4; + + const uint32_t v_low = __builtin_amdgcn_perm(values1, values0, q & 0x07070707u); + const uint32_t v_high = __builtin_amdgcn_perm(values3, values2, q & 0x07070707u); + const uint32_t mask = 0x03020100u | ((q & 0x08080808u) >> 1); + + return __builtin_amdgcn_perm(v_high, v_low, mask); +#else + return get_int_from_table_16(q4, fallback_table).x; +#endif +} diff --git a/ggml/rocmfp4/rocmfp4_hip_scale.cuh b/ggml/rocmfp4/rocmfp4_hip_scale.cuh new file mode 100644 index 000000000000..19525a4fb017 --- /dev/null +++ b/ggml/rocmfp4/rocmfp4_hip_scale.cuh @@ -0,0 +1,157 @@ +#pragma once + +#include +#include +#include + +#ifndef GGML_ROCMFP4_USE_SCALE_LUT +#define GGML_ROCMFP4_USE_SCALE_LUT 0 +#endif + +#if defined(GGML_USE_HIP) && GGML_ROCMFP4_USE_SCALE_LUT +#define ROCMFP4_SCALE_SUB(M) ((M) * 0x1p-10f) +#define ROCMFP4_SCALE_E1(M) ((8 + (M)) * 0x1p-10f) +#define ROCMFP4_SCALE_E2(M) ((8 + (M)) * 0x1p-9f) +#define ROCMFP4_SCALE_E3(M) ((8 + (M)) * 0x1p-8f) +#define ROCMFP4_SCALE_E4(M) ((8 + (M)) * 0x1p-7f) +#define ROCMFP4_SCALE_E5(M) ((8 + (M)) * 0x1p-6f) +#define ROCMFP4_SCALE_E6(M) ((8 + (M)) * 0x1p-5f) +#define ROCMFP4_SCALE_E7(M) ((8 + (M)) * 0x1p-4f) +#define ROCMFP4_SCALE_E8(M) ((8 + (M)) * 0x1p-3f) +#define ROCMFP4_SCALE_E9(M) ((8 + (M)) * 0x1p-2f) +#define ROCMFP4_SCALE_E10(M) ((8 + (M)) * 0x1p-1f) +#define ROCMFP4_SCALE_E11(M) ((8 + (M)) * 0x1p0f) +#define ROCMFP4_SCALE_E12(M) ((8 + (M)) * 0x1p1f) +#define ROCMFP4_SCALE_E13(M) ((8 + (M)) * 0x1p2f) +#define ROCMFP4_SCALE_E14(M) ((8 + (M)) * 0x1p3f) +#define ROCMFP4_SCALE_E15(M) ((8 + (M)) * 0x1p4f) + +static __device__ __constant__ const float rocmfp4_scale_ue4m3_half_lut[127] = { + ROCMFP4_SCALE_SUB(0), ROCMFP4_SCALE_SUB(1), ROCMFP4_SCALE_SUB(2), ROCMFP4_SCALE_SUB(3), + ROCMFP4_SCALE_SUB(4), ROCMFP4_SCALE_SUB(5), ROCMFP4_SCALE_SUB(6), ROCMFP4_SCALE_SUB(7), + ROCMFP4_SCALE_E1(0), ROCMFP4_SCALE_E1(1), ROCMFP4_SCALE_E1(2), ROCMFP4_SCALE_E1(3), + ROCMFP4_SCALE_E1(4), ROCMFP4_SCALE_E1(5), ROCMFP4_SCALE_E1(6), ROCMFP4_SCALE_E1(7), + ROCMFP4_SCALE_E2(0), ROCMFP4_SCALE_E2(1), ROCMFP4_SCALE_E2(2), ROCMFP4_SCALE_E2(3), + ROCMFP4_SCALE_E2(4), ROCMFP4_SCALE_E2(5), ROCMFP4_SCALE_E2(6), ROCMFP4_SCALE_E2(7), + ROCMFP4_SCALE_E3(0), ROCMFP4_SCALE_E3(1), ROCMFP4_SCALE_E3(2), ROCMFP4_SCALE_E3(3), + ROCMFP4_SCALE_E3(4), ROCMFP4_SCALE_E3(5), ROCMFP4_SCALE_E3(6), ROCMFP4_SCALE_E3(7), + ROCMFP4_SCALE_E4(0), ROCMFP4_SCALE_E4(1), ROCMFP4_SCALE_E4(2), ROCMFP4_SCALE_E4(3), + ROCMFP4_SCALE_E4(4), ROCMFP4_SCALE_E4(5), ROCMFP4_SCALE_E4(6), ROCMFP4_SCALE_E4(7), + ROCMFP4_SCALE_E5(0), ROCMFP4_SCALE_E5(1), ROCMFP4_SCALE_E5(2), ROCMFP4_SCALE_E5(3), + ROCMFP4_SCALE_E5(4), ROCMFP4_SCALE_E5(5), ROCMFP4_SCALE_E5(6), ROCMFP4_SCALE_E5(7), + ROCMFP4_SCALE_E6(0), ROCMFP4_SCALE_E6(1), ROCMFP4_SCALE_E6(2), ROCMFP4_SCALE_E6(3), + ROCMFP4_SCALE_E6(4), ROCMFP4_SCALE_E6(5), ROCMFP4_SCALE_E6(6), ROCMFP4_SCALE_E6(7), + ROCMFP4_SCALE_E7(0), ROCMFP4_SCALE_E7(1), ROCMFP4_SCALE_E7(2), ROCMFP4_SCALE_E7(3), + ROCMFP4_SCALE_E7(4), ROCMFP4_SCALE_E7(5), ROCMFP4_SCALE_E7(6), ROCMFP4_SCALE_E7(7), + ROCMFP4_SCALE_E8(0), ROCMFP4_SCALE_E8(1), ROCMFP4_SCALE_E8(2), ROCMFP4_SCALE_E8(3), + ROCMFP4_SCALE_E8(4), ROCMFP4_SCALE_E8(5), ROCMFP4_SCALE_E8(6), ROCMFP4_SCALE_E8(7), + ROCMFP4_SCALE_E9(0), ROCMFP4_SCALE_E9(1), ROCMFP4_SCALE_E9(2), ROCMFP4_SCALE_E9(3), + ROCMFP4_SCALE_E9(4), ROCMFP4_SCALE_E9(5), ROCMFP4_SCALE_E9(6), ROCMFP4_SCALE_E9(7), + ROCMFP4_SCALE_E10(0), ROCMFP4_SCALE_E10(1), ROCMFP4_SCALE_E10(2), ROCMFP4_SCALE_E10(3), + ROCMFP4_SCALE_E10(4), ROCMFP4_SCALE_E10(5), ROCMFP4_SCALE_E10(6), ROCMFP4_SCALE_E10(7), + ROCMFP4_SCALE_E11(0), ROCMFP4_SCALE_E11(1), ROCMFP4_SCALE_E11(2), ROCMFP4_SCALE_E11(3), + ROCMFP4_SCALE_E11(4), ROCMFP4_SCALE_E11(5), ROCMFP4_SCALE_E11(6), ROCMFP4_SCALE_E11(7), + ROCMFP4_SCALE_E12(0), ROCMFP4_SCALE_E12(1), ROCMFP4_SCALE_E12(2), ROCMFP4_SCALE_E12(3), + ROCMFP4_SCALE_E12(4), ROCMFP4_SCALE_E12(5), ROCMFP4_SCALE_E12(6), ROCMFP4_SCALE_E12(7), + ROCMFP4_SCALE_E13(0), ROCMFP4_SCALE_E13(1), ROCMFP4_SCALE_E13(2), ROCMFP4_SCALE_E13(3), + ROCMFP4_SCALE_E13(4), ROCMFP4_SCALE_E13(5), ROCMFP4_SCALE_E13(6), ROCMFP4_SCALE_E13(7), + ROCMFP4_SCALE_E14(0), ROCMFP4_SCALE_E14(1), ROCMFP4_SCALE_E14(2), ROCMFP4_SCALE_E14(3), + ROCMFP4_SCALE_E14(4), ROCMFP4_SCALE_E14(5), ROCMFP4_SCALE_E14(6), ROCMFP4_SCALE_E14(7), + ROCMFP4_SCALE_E15(0), ROCMFP4_SCALE_E15(1), ROCMFP4_SCALE_E15(2), ROCMFP4_SCALE_E15(3), + ROCMFP4_SCALE_E15(4), ROCMFP4_SCALE_E15(5), ROCMFP4_SCALE_E15(6), +}; + +#undef ROCMFP4_SCALE_SUB +#undef ROCMFP4_SCALE_E1 +#undef ROCMFP4_SCALE_E2 +#undef ROCMFP4_SCALE_E3 +#undef ROCMFP4_SCALE_E4 +#undef ROCMFP4_SCALE_E5 +#undef ROCMFP4_SCALE_E6 +#undef ROCMFP4_SCALE_E7 +#undef ROCMFP4_SCALE_E8 +#undef ROCMFP4_SCALE_E9 +#undef ROCMFP4_SCALE_E10 +#undef ROCMFP4_SCALE_E11 +#undef ROCMFP4_SCALE_E12 +#undef ROCMFP4_SCALE_E13 +#undef ROCMFP4_SCALE_E14 +#undef ROCMFP4_SCALE_E15 +#endif + +static __device__ __forceinline__ float rocmfp4_u32_as_f32(uint32_t bits) { +#if defined(GGML_USE_HIP) + return __uint_as_float(bits); +#else + float result; + memcpy(&result, &bits, sizeof(float)); + return result; +#endif +} + +// ROCmFP4 validates scale bytes before backend execution, so HIP/ROCm hot +// paths can decode finite unsigned E4M3 half-scales directly without the +// generic FP8 NaN handling used by other formats. +static __device__ __forceinline__ float rocmfp4_ue4m3_to_fp32_half_finite(uint8_t x) { +#if defined(GGML_USE_HIP) && GGML_ROCMFP4_USE_SCALE_LUT + return x <= 0x7e ? rocmfp4_scale_ue4m3_half_lut[x] : 0.0f; +#else + const int exp = (x >> 3) & 0xF; + const int man = x & 0x7; + + if (exp == 0) { + return (float) man * (1.0f / 1024.0f); + } + + const uint32_t bits = ((uint32_t) exp + 119u) << 23 | ((uint32_t) man << 20); + return rocmfp4_u32_as_f32(bits); +#endif +} + +static __device__ __forceinline__ float rocmfpx_ue4m3_to_fp32_finite(uint8_t x) { + if (x > 0x7e) { + return 0.0f; + } + + const int exp = (x >> 3) & 0xF; + const int man = x & 0x7; + + if (exp == 0) { + return (float) man * (1.0f / 1024.0f); + } + + const uint32_t bits = ((uint32_t) exp + 119u) << 23 | ((uint32_t) man << 20); + return rocmfp4_u32_as_f32(bits); +} + +static __device__ __forceinline__ uint8_t rocmfpx_nearest_scale_ue4m3_cuda(float target_scale) { + if (!(target_scale > 0.0f) || !isfinite(target_scale)) { + return 0; + } + + uint8_t lo = 1; + uint8_t hi = 0x7e; + while (lo < hi) { + const uint8_t mid = lo + (hi - lo) / 2; + if (rocmfpx_ue4m3_to_fp32_finite(mid) < target_scale) { + lo = mid + 1; + } else { + hi = mid; + } + } + + if (lo == 1) { + return 1; + } + + const float hi_scale = rocmfpx_ue4m3_to_fp32_finite(lo); + const float lo_scale = rocmfpx_ue4m3_to_fp32_finite((uint8_t) (lo - 1)); + return (target_scale - lo_scale <= hi_scale - target_scale) ? (uint8_t) (lo - 1) : lo; +} + +static __device__ __forceinline__ int8_t rocmfp4_decode_i8(uint8_t q) { + q &= 0x0f; + const int mag3 = q & 0x07; + const int mag = mag3 <= 4 ? mag3 : 2*mag3 - 4; + return (q & 0x08) ? -mag : mag; +} diff --git a/ggml/rocmfpx/README.md b/ggml/rocmfpx/README.md new file mode 100644 index 000000000000..8d5d53c911ed --- /dev/null +++ b/ggml/rocmfpx/README.md @@ -0,0 +1,109 @@ +# ROCmFPx Experimental Formats + +This folder contains the reference layer for the proposed ROCmFP3, ROCmFP6, and +ROCmFP8 quantization family. It is intentionally separate from `ggml/rocmfp4/` +so the promoted ROCmFP4 GGUF formats and kernels are not affected while the new +layouts are evaluated. + +## ROCmFP4 Instructions To Preserve + +ROCmFPX is a sibling model-weight quant family, not a new K/V-only compression +scheme. The promoted ROCmFP4 implementation remains the template for how the +family should behave in llama.cpp: + +- Keep 32-weight blocks so CPU, HIP, and Vulkan kernels can reuse the same + Q4/Q8-style reduction shape and GGUF row-size assumptions. +- Use finite unsigned UE4M3 scale bytes only. `0x7f` and sign-bit scale bytes + are invalid, matching the ROCmFP4 validation rule. +- Prefer reconstruction-MSE scale selection over plain max-abs scaling. ROCmFP4 + searches each 16-weight half-block; ROCmFP3 and ROCmFP6 follow the same + half-block policy, while ROCmFP8 currently uses one full-block scale. +- Preserve the ROCmFP4 kernel contract: CPU reference quant/dequant/dot first, + then HIP/Vulkan `CPY`, `GET_ROWS`, `SET_ROWS`, `MUL_MAT`, and `MUL_MAT_ID` + paths, with backend-op coverage before claiming runtime support. +- Keep dequant math explicit and deterministic: integer code times decoded + UE4M3 scale. ROCmFP4 uses the Codebook10 half-scale table; ROCmFPX formats + use their own integer code ranges but must retain the same finite-scale and + integer-dot discipline. + +The ROCmFP4 Codebook10 levels are not reused by FP3/FP6/FP8 directly: +`ROCmFP3` uses `0, +/-1, +/-2, +/-4`, `ROCmFP6` uses signed-magnitude levels up +to `31`, and `ROCmFP8` uses signed int8 levels clamped to `[-127, 127]`. +What is inherited is the block/scale/kernel/dequant contract. + +Current status (June 16, 2026): +- CPU reference quantize/dequantize exists for all three formats. +- `Q3_0_ROCMFPX`, `Q6_0_ROCMFPX`, and `Q8_0_ROCMFPX` are registered as + experimental GGUF tensor types. +- ROCm/HIP and Vulkan kernels support `CPY`, `GET_ROWS`, `SET_ROWS`, and + `MUL_MAT`/`MUL_MAT_ID` for all three formats. +- Qwen3-0.6B BF16 smoke tests pass on CPU, ROCm0, and Vulkan0. +- Generic baseline quant presets now include lean coherency routing. These + preset names are not release recipe identities; released artifact mappings + and architecture-qualified recipe IDs live in + [`docs/recipes/`](../../docs/recipes/README.md): + - `Q3_0_ROCMFPX`: selective `Q5_K` on attention Q/O and early K/V, boosted + FFN-down at `Q5_K`, selective FFN-gate at `Q6_0_ROCMFPX`, bulk FFN-up on + `Q3_0_ROCMFPX`, embeddings/output at `Q4_0_ROCMFP4_FAST`. + - `Q6_0_ROCMFPX`: early attention and boosted FFN-down at `Q8_0_ROCMFPX`, + embeddings/output at `Q6_0_ROCMFPX`, bulk gate/up on `Q6_0_ROCMFPX`. + - `Q8_0_ROCMFPX`: pure FP8-family preset. +- Opt-in `*_AGENT` presets boost attention/FFN routing for tool-call / + Hermes / OpenClaw style workloads: + - `Q3_0_ROCMFPX_AGENT`, `Q6_0_ROCMFPX_AGENT`, `Q8_0_ROCMFPX_AGENT`. + - Routing is layered on top of LEAN; default presets are unchanged. +- FP3 and FP6 quantization use reconstruction-MSE scale selection per + 16-weight half-block. + +## Validation Script Index + +```text +scripts/check-rocmfpx-reference.sh # CPU reference math +scripts/check-rocmfpx-qwen-all.sh # core Qwen gates +scripts/check-rocmfpx-all.sh # qwen-all + optional smokes +scripts/check-rocmfpx-summary.sh # full JSON summary runner +scripts/sweep-rocmfpx-backend-ops.sh # test-backend-ops per backend +scripts/sweep-rocmfpx-agent-size-table.sh # LEAN vs AGENT MiB/BPW +scripts/sweep-rocmfpx-perplexity.sh # calibration PPL sweep +scripts/sweep-rocmfpx-decode-tune.sh # decode-tune matrix +scripts/build-rocmfpx-agent-fixtures.sh # proxy Hermes/OpenClaw AGENT GGUFs +``` + +## Layouts + +All formats use 32-weight blocks. + +| Format | Payload | Scale bytes | Block bytes | BPW | Purpose | +|---|---:|---:|---:|---:|---| +| `Q3_0_ROCMFPX` | 32 packed 3-bit codes | 2, one per 16 weights | 14 | 3.50 | Experimental low-bit candidate | +| `Q6_0_ROCMFPX` | 32 packed 6-bit codes | 2, one per 16 weights | 26 | 6.50 | Experimental quality candidate | +| `Q8_0_ROCMFPX` | 32 signed 8-bit codes | 1, one per 32 weights | 33 | 8.25 | Experimental high-quality reference | + +`ROCmFP3` uses a tiny signed codebook: `0, +/-1, +/-2, +/-4`. +`ROCmFP6` uses signed-magnitude integer levels up to `31`. +`ROCmFP8` uses signed int8 levels clamped to `[-127, 127]`. + +## Validation + +Reference math only: + +```bash +scripts/check-rocmfpx-reference.sh +``` + +Focused backend sweep from the experiment worktree: + +```bash +cmake --build build-strix-rocmfp4 --target test-backend-ops -j 8 +timeout 120 build-strix-rocmfp4/bin/test-backend-ops test -o MUL_MAT,GET_ROWS,CPY,SET_ROWS -b CPU +timeout 180 build-strix-rocmfp4/bin/test-backend-ops test -o MUL_MAT,GET_ROWS,CPY,SET_ROWS -b ROCm0 +timeout 180 build-strix-rocmfp4/bin/test-backend-ops test -o MUL_MAT,GET_ROWS,CPY,SET_ROWS -b Vulkan0 +``` + +Qwen3 BF16 coherency and decode-speed gates: + +```bash +MODEL=/home/caf/strix-fp4/models/rocmfpx-bf16-tests/Qwen3-0.6B-Q3_0_ROCMFPX_COHERENT-LEAN.gguf BACKEND=ROCm0 scripts/check-rocmfpx-qwen-coherency.sh +MODEL=/home/caf/strix-fp4/models/rocmfpx-bf16-tests/Qwen3-0.6B-Q3_0_ROCMFPX_COHERENT-LEAN.gguf BACKEND=ROCm0 scripts/check-rocmfpx-qwen-bench.sh +MODEL=/home/caf/strix-fp4/models/rocmfpx-bf16-tests/Qwen3-0.6B-Q3_0_ROCMFPX_COHERENT-LEAN.gguf BACKEND=ROCm0 scripts/check-rocmfpx-qwen-strict-json.sh +``` diff --git a/ggml/rocmfpx/rocmfp2_reference.c b/ggml/rocmfpx/rocmfp2_reference.c new file mode 100644 index 000000000000..87881cf64df9 --- /dev/null +++ b/ggml/rocmfpx/rocmfp2_reference.c @@ -0,0 +1,483 @@ +#include "rocmfp2_reference.h" + +#include +#include +#include + +static bool rocmfp2_p1_mapping_is_valid(rocmfp2_p1_mapping mapping) { + return mapping == ROCMFP2_P1_MAPPING_MORD || mapping == ROCMFP2_P1_MAPPING_MSM; +} + +static bool rocmfp2_p1_codebook_is_valid(const rocmfp2_p1_codebook * codebook) { + return codebook != NULL && codebook->inner > 0 && codebook->outer > codebook->inner && codebook->outer <= 127; +} + +static double rocmfp2_p1_decode_scale_valid(uint8_t scale_byte) { + const int exponent = scale_byte >> 3; + const int mantissa = scale_byte & 7; + + if (exponent == 0) { + return ldexp((double) mantissa, -10); + } + + return ldexp((double) (8 + mantissa), exponent - 11); +} + +static int8_t rocmfp2_p1_decode_code_valid( + uint8_t code, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping) { + const int inner = (int) codebook->inner; + const int outer = (int) codebook->outer; + + if (mapping == ROCMFP2_P1_MAPPING_MORD) { + static const int sign[4] = { -1, -1, 1, 1 }; + const int magnitude = (code == 0 || code == 3) ? outer : inner; + return (int8_t) (sign[code] * magnitude); + } + + static const int sign[4] = { 1, 1, -1, -1 }; + const int magnitude = (code == 1 || code == 3) ? outer : inner; + return (int8_t) (sign[code] * magnitude); +} + +static uint8_t rocmfp2_p1_encode_semantic_valid( + bool negative, + bool outer, + rocmfp2_p1_mapping mapping) { + if (mapping == ROCMFP2_P1_MAPPING_MORD) { + if (negative) { + return outer ? 0u : 1u; + } + return outer ? 3u : 2u; + } + + if (negative) { + return outer ? 3u : 2u; + } + return outer ? 1u : 0u; +} + +/* Lower ranks implement the frozen semantic tie rules. */ +static int rocmfp2_p1_code_tie_rank( + uint8_t code, + bool source_negative, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping) { + const int value = (int) rocmfp2_p1_decode_code_valid(code, codebook, mapping); + const bool code_negative = value < 0; + const bool outer = abs(value) == (int) codebook->outer; + + /* Inner magnitude wins first; input sign wins a remaining sign tie. */ + return (outer ? 2 : 0) + (code_negative != source_negative ? 1 : 0); +} + +static uint8_t rocmfp2_p1_select_code_ref_valid( + double source, + double scale, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping) { + /* Every code reconstructs exact zero; P0 canonicalizes this tie to 00. */ + if (scale == 0.0) { + return 0; + } + + const bool source_negative = signbit(source) != 0; + double best_error = INFINITY; + int best_rank = 0; + uint8_t best_code = 0; + bool have_best = false; + + for (uint8_t code = 0; code < 4; ++code) { + const double reconstructed = (double) rocmfp2_p1_decode_code_valid(code, codebook, mapping) * scale; + const double delta = source - reconstructed; + const double error = delta * delta; + const int rank = rocmfp2_p1_code_tie_rank(code, source_negative, codebook, mapping); + + if (!have_best || error < best_error || (error == best_error && rank < best_rank)) { + best_error = error; + best_rank = rank; + best_code = code; + have_best = true; + } + } + + return best_code; +} + +static uint8_t rocmfp2_p1_select_code_optimized_valid( + double source, + double scale, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping) { + /* Keep serialization identical to the exhaustive reference at scale 0. */ + if (scale == 0.0) { + return 0; + } + + const double magnitude = fabs(source); + const double inner_reconstructed = (double) codebook->inner * scale; + const double outer_reconstructed = (double) codebook->outer * scale; + const double inner_delta = magnitude - inner_reconstructed; + const double outer_delta = magnitude - outer_reconstructed; + + /* Equality is the exact inner/outer midpoint tie and keeps inner. */ + const bool outer = outer_delta * outer_delta < inner_delta * inner_delta; + return rocmfp2_p1_encode_semantic_valid(signbit(source) != 0, outer, mapping); +} + +const char * rocmfp2_p1_status_name(rocmfp2_p1_status status) { + switch (status) { + case ROCMFP2_P1_OK: return "OK"; + case ROCMFP2_P1_NONFINITE_SOURCE: return "NONFINITE_SOURCE"; + case ROCMFP2_P1_INVALID_ARGUMENT: return "INVALID_ARGUMENT"; + case ROCMFP2_P1_INVALID_CODEBOOK: return "INVALID_CODEBOOK"; + case ROCMFP2_P1_INVALID_MAPPING: return "INVALID_MAPPING"; + case ROCMFP2_P1_INVALID_SCALE_METADATA: return "INVALID_SCALE_METADATA"; + case ROCMFP2_P1_INVALID_CODE: return "INVALID_CODE"; + } + + return "UNKNOWN_STATUS"; +} + +bool rocmfp2_p1_scale_is_valid(uint8_t scale_byte) { + return scale_byte <= 0x7e; +} + +double rocmfp2_p1_ue4m3_to_binary64(uint8_t scale_byte) { + if (!rocmfp2_p1_scale_is_valid(scale_byte)) { + return NAN; + } + + return rocmfp2_p1_decode_scale_valid(scale_byte); +} + +uint8_t rocmfp2_p1_nearest_ue4m3(double target) { + if (isnan(target) || target < 0.0) { + return 0xff; + } + if (target == 0.0) { + return 0; + } + + const double maximum = rocmfp2_p1_decode_scale_valid(0x7e); + if (!isfinite(target) || target >= maximum) { + return 0x7e; + } + + for (uint8_t upper_byte = 1; upper_byte <= 0x7e; ++upper_byte) { + const double upper = rocmfp2_p1_decode_scale_valid(upper_byte); + if (target <= upper) { + const uint8_t lower_byte = (uint8_t) (upper_byte - 1); + const double lower = rocmfp2_p1_decode_scale_valid(lower_byte); + + /* Exact midpoint ties select the smaller scale byte. */ + return target - lower <= upper - target ? lower_byte : upper_byte; + } + } + + return 0x7e; +} + +rocmfp2_p1_status rocmfp2_p1_decode_code( + uint8_t code, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + int8_t * value) { + if (value == NULL) { + return ROCMFP2_P1_INVALID_ARGUMENT; + } + if (!rocmfp2_p1_codebook_is_valid(codebook)) { + return ROCMFP2_P1_INVALID_CODEBOOK; + } + if (!rocmfp2_p1_mapping_is_valid(mapping)) { + return ROCMFP2_P1_INVALID_MAPPING; + } + if (code > 3) { + return ROCMFP2_P1_INVALID_CODE; + } + + *value = rocmfp2_p1_decode_code_valid(code, codebook, mapping); + return ROCMFP2_P1_OK; +} + +rocmfp2_p1_status rocmfp2_p1_decode_packed_byte( + uint8_t packed, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + int8_t values[4]) { + if (values == NULL) { + return ROCMFP2_P1_INVALID_ARGUMENT; + } + if (!rocmfp2_p1_codebook_is_valid(codebook)) { + return ROCMFP2_P1_INVALID_CODEBOOK; + } + if (!rocmfp2_p1_mapping_is_valid(mapping)) { + return ROCMFP2_P1_INVALID_MAPPING; + } + + for (int lane = 0; lane < 4; ++lane) { + const uint8_t code = (uint8_t) ((packed >> (2 * lane)) & 3u); + values[lane] = rocmfp2_p1_decode_code_valid(code, codebook, mapping); + } + + return ROCMFP2_P1_OK; +} + +rocmfp2_p1_status rocmfp2_p1_pack_codes( + const uint8_t codes[ROCMFP2_P1_BLOCK_WEIGHTS], + const uint8_t scales[ROCMFP2_P1_SCALE_BYTES], + rocmfp2_p1_block * block) { + if (codes == NULL || scales == NULL || block == NULL) { + return ROCMFP2_P1_INVALID_ARGUMENT; + } + if (!rocmfp2_p1_scale_is_valid(scales[0]) || !rocmfp2_p1_scale_is_valid(scales[1])) { + return ROCMFP2_P1_INVALID_SCALE_METADATA; + } + + rocmfp2_p1_block temporary; + for (int packed_index = 0; packed_index < ROCMFP2_P1_DATA_BYTES; ++packed_index) { + uint8_t packed = 0; + for (int lane = 0; lane < 4; ++lane) { + const uint8_t code = codes[4 * packed_index + lane]; + if (code > 3) { + return ROCMFP2_P1_INVALID_CODE; + } + packed |= (uint8_t) (code << (2 * lane)); + } + temporary.d[packed_index] = packed; + } + temporary.s[0] = scales[0]; + temporary.s[1] = scales[1]; + + memcpy(block, &temporary, sizeof(temporary)); + return ROCMFP2_P1_OK; +} + +void rocmfp2_p1_unpack_codes( + const rocmfp2_p1_block * block, + uint8_t codes[ROCMFP2_P1_BLOCK_WEIGHTS]) { + for (int packed_index = 0; packed_index < ROCMFP2_P1_DATA_BYTES; ++packed_index) { + const uint8_t packed = block->d[packed_index]; + for (int lane = 0; lane < 4; ++lane) { + codes[4 * packed_index + lane] = (uint8_t) ((packed >> (2 * lane)) & 3u); + } + } +} + +bool rocmfp2_p1_validate_block(const rocmfp2_p1_block * block) { + return block != NULL && rocmfp2_p1_scale_is_valid(block->s[0]) && rocmfp2_p1_scale_is_valid(block->s[1]); +} + +bool rocmfp2_p1_validate_serialized(const void * data, size_t nbytes) { + if (nbytes == 0) { + return true; + } + if (data == NULL || nbytes % ROCMFP2_P1_BLOCK_BYTES != 0) { + return false; + } + + const uint8_t * bytes = (const uint8_t *) data; + const size_t blocks = nbytes / ROCMFP2_P1_BLOCK_BYTES; + for (size_t block_index = 0; block_index < blocks; ++block_index) { + const size_t base = block_index * ROCMFP2_P1_BLOCK_BYTES; + if (!rocmfp2_p1_scale_is_valid(bytes[base + 8]) || !rocmfp2_p1_scale_is_valid(bytes[base + 9])) { + return false; + } + } + + return true; +} + +rocmfp2_p1_status rocmfp2_p1_select_code_ref( + double source, + uint8_t scale_byte, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + uint8_t * code) { + if (code == NULL) { + return ROCMFP2_P1_INVALID_ARGUMENT; + } + if (!rocmfp2_p1_codebook_is_valid(codebook)) { + return ROCMFP2_P1_INVALID_CODEBOOK; + } + if (!rocmfp2_p1_mapping_is_valid(mapping)) { + return ROCMFP2_P1_INVALID_MAPPING; + } + if (!rocmfp2_p1_scale_is_valid(scale_byte)) { + return ROCMFP2_P1_INVALID_SCALE_METADATA; + } + if (!isfinite(source)) { + return ROCMFP2_P1_NONFINITE_SOURCE; + } + + *code = rocmfp2_p1_select_code_ref_valid( + source, rocmfp2_p1_decode_scale_valid(scale_byte), codebook, mapping); + return ROCMFP2_P1_OK; +} + +rocmfp2_p1_status rocmfp2_p1_select_code_optimized( + double source, + uint8_t scale_byte, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + uint8_t * code) { + if (code == NULL) { + return ROCMFP2_P1_INVALID_ARGUMENT; + } + if (!rocmfp2_p1_codebook_is_valid(codebook)) { + return ROCMFP2_P1_INVALID_CODEBOOK; + } + if (!rocmfp2_p1_mapping_is_valid(mapping)) { + return ROCMFP2_P1_INVALID_MAPPING; + } + if (!rocmfp2_p1_scale_is_valid(scale_byte)) { + return ROCMFP2_P1_INVALID_SCALE_METADATA; + } + if (!isfinite(source)) { + return ROCMFP2_P1_NONFINITE_SOURCE; + } + + *code = rocmfp2_p1_select_code_optimized_valid( + source, rocmfp2_p1_decode_scale_valid(scale_byte), codebook, mapping); + return ROCMFP2_P1_OK; +} + +typedef uint8_t (*rocmfp2_p1_selector)( + double source, + double scale, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping); + +static rocmfp2_p1_status rocmfp2_p1_quantize_block_impl( + const double source[ROCMFP2_P1_BLOCK_WEIGHTS], + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + rocmfp2_p1_block * block, + double group_sse[ROCMFP2_P1_SCALE_BYTES], + rocmfp2_p1_selector selector) { + if (source == NULL || block == NULL) { + return ROCMFP2_P1_INVALID_ARGUMENT; + } + if (!rocmfp2_p1_codebook_is_valid(codebook)) { + return ROCMFP2_P1_INVALID_CODEBOOK; + } + if (!rocmfp2_p1_mapping_is_valid(mapping)) { + return ROCMFP2_P1_INVALID_MAPPING; + } + + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + if (!isfinite(source[i])) { + return ROCMFP2_P1_NONFINITE_SOURCE; + } + } + + uint8_t best_codes[ROCMFP2_P1_BLOCK_WEIGHTS]; + uint8_t best_scales[ROCMFP2_P1_SCALE_BYTES]; + double best_group_sse[ROCMFP2_P1_SCALE_BYTES]; + + for (int group = 0; group < ROCMFP2_P1_SCALE_BYTES; ++group) { + const int source_offset = group * ROCMFP2_P1_GROUP_WEIGHTS; + double best_sse = INFINITY; + uint8_t best_scale = 0; + uint8_t candidate_codes[ROCMFP2_P1_GROUP_WEIGHTS]; + uint8_t saved_codes[ROCMFP2_P1_GROUP_WEIGHTS]; + bool have_best = false; + + /* Exact exhaustive search: every legal finite unsigned UE4M3 byte. */ + for (int scale_index = 0; scale_index <= 0x7e; ++scale_index) { + const uint8_t scale_byte = (uint8_t) scale_index; + const double scale = rocmfp2_p1_decode_scale_valid(scale_byte); + double candidate_sse = 0.0; + + for (int i = 0; i < ROCMFP2_P1_GROUP_WEIGHTS; ++i) { + const double value = source[source_offset + i]; + const uint8_t code = selector(value, scale, codebook, mapping); + const double reconstructed = (double) rocmfp2_p1_decode_code_valid(code, codebook, mapping) * scale; + const double delta = value - reconstructed; + + candidate_codes[i] = code; + candidate_sse += delta * delta; + } + + if (!have_best || candidate_sse < best_sse || + (candidate_sse == best_sse && scale_byte < best_scale)) { + best_sse = candidate_sse; + best_scale = scale_byte; + memcpy(saved_codes, candidate_codes, sizeof(saved_codes)); + have_best = true; + } + } + + memcpy(best_codes + source_offset, saved_codes, sizeof(saved_codes)); + best_scales[group] = best_scale; + best_group_sse[group] = best_sse; + } + + rocmfp2_p1_block temporary; + const rocmfp2_p1_status pack_status = rocmfp2_p1_pack_codes(best_codes, best_scales, &temporary); + if (pack_status != ROCMFP2_P1_OK) { + return pack_status; + } + + memcpy(block, &temporary, sizeof(temporary)); + if (group_sse != NULL) { + group_sse[0] = best_group_sse[0]; + group_sse[1] = best_group_sse[1]; + } + + return ROCMFP2_P1_OK; +} + +rocmfp2_p1_status rocmfp2_p1_quantize_block_ref( + const double source[ROCMFP2_P1_BLOCK_WEIGHTS], + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + rocmfp2_p1_block * block, + double group_sse[ROCMFP2_P1_SCALE_BYTES]) { + return rocmfp2_p1_quantize_block_impl( + source, codebook, mapping, block, group_sse, rocmfp2_p1_select_code_ref_valid); +} + +rocmfp2_p1_status rocmfp2_p1_quantize_block_optimized( + const double source[ROCMFP2_P1_BLOCK_WEIGHTS], + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + rocmfp2_p1_block * block, + double group_sse[ROCMFP2_P1_SCALE_BYTES]) { + return rocmfp2_p1_quantize_block_impl( + source, codebook, mapping, block, group_sse, rocmfp2_p1_select_code_optimized_valid); +} + +rocmfp2_p1_status rocmfp2_p1_dequantize_block( + const rocmfp2_p1_block * block, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + double output[ROCMFP2_P1_BLOCK_WEIGHTS]) { + if (block == NULL || output == NULL) { + return ROCMFP2_P1_INVALID_ARGUMENT; + } + if (!rocmfp2_p1_codebook_is_valid(codebook)) { + return ROCMFP2_P1_INVALID_CODEBOOK; + } + if (!rocmfp2_p1_mapping_is_valid(mapping)) { + return ROCMFP2_P1_INVALID_MAPPING; + } + if (!rocmfp2_p1_validate_block(block)) { + return ROCMFP2_P1_INVALID_SCALE_METADATA; + } + + uint8_t codes[ROCMFP2_P1_BLOCK_WEIGHTS]; + double temporary[ROCMFP2_P1_BLOCK_WEIGHTS]; + rocmfp2_p1_unpack_codes(block, codes); + + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + const int group = i / ROCMFP2_P1_GROUP_WEIGHTS; + const double scale = rocmfp2_p1_decode_scale_valid(block->s[group]); + const int8_t value = rocmfp2_p1_decode_code_valid(codes[i], codebook, mapping); + temporary[i] = (double) value * scale; + } + + memcpy(output, temporary, sizeof(temporary)); + return ROCMFP2_P1_OK; +} diff --git a/ggml/rocmfpx/rocmfp2_reference.h b/ggml/rocmfpx/rocmfp2_reference.h new file mode 100644 index 000000000000..2bc72579b322 --- /dev/null +++ b/ggml/rocmfpx/rocmfp2_reference.h @@ -0,0 +1,151 @@ +#ifndef ROCMFP2_REFERENCE_H +#define ROCMFP2_REFERENCE_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + ROCMFP2_P1_BLOCK_WEIGHTS = 32, + ROCMFP2_P1_GROUP_WEIGHTS = 16, + ROCMFP2_P1_DATA_BYTES = 8, + ROCMFP2_P1_SCALE_BYTES = 2, + ROCMFP2_P1_BLOCK_BYTES = 10, +}; + +/* + * Frozen Phase-1 P0_AOS serialization: + * + * d[0] ... d[7], s[0], s[1] + * + * Weight 4*j+i occupies bits 2*i+1:2*i of d[j]. Scale s[0] + * applies to weights 0..15 and s[1] to weights 16..31. This is a + * byte-canonical format; host word endianness is not part of serialization. + */ +typedef struct { + uint8_t d[ROCMFP2_P1_DATA_BYTES]; + uint8_t s[ROCMFP2_P1_SCALE_BYTES]; +} rocmfp2_p1_block; + +typedef struct { + uint8_t inner; + uint8_t outer; +} rocmfp2_p1_codebook; + +typedef enum { + ROCMFP2_P1_MAPPING_MORD = 0, + ROCMFP2_P1_MAPPING_MSM = 1, +} rocmfp2_p1_mapping; + +typedef enum { + ROCMFP2_P1_OK = 0, + ROCMFP2_P1_NONFINITE_SOURCE, + ROCMFP2_P1_INVALID_ARGUMENT, + ROCMFP2_P1_INVALID_CODEBOOK, + ROCMFP2_P1_INVALID_MAPPING, + ROCMFP2_P1_INVALID_SCALE_METADATA, + ROCMFP2_P1_INVALID_CODE, +} rocmfp2_p1_status; + +#if defined(__cplusplus) +static_assert(FLT_RADIX == 2 && DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024, + "ROCmFP2 Phase-1 reference requires IEEE-754 binary64 double"); +static_assert(sizeof(rocmfp2_p1_block) == ROCMFP2_P1_BLOCK_BYTES, + "ROCmFP2 P0 block has padding"); +static_assert(offsetof(rocmfp2_p1_block, d) == 0, "ROCmFP2 P0 data offset changed"); +static_assert(offsetof(rocmfp2_p1_block, s) == 8, "ROCmFP2 P0 scale offset changed"); +#else +_Static_assert(FLT_RADIX == 2 && DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024, + "ROCmFP2 Phase-1 reference requires IEEE-754 binary64 double"); +_Static_assert(sizeof(rocmfp2_p1_block) == ROCMFP2_P1_BLOCK_BYTES, + "ROCmFP2 P0 block has padding"); +_Static_assert(offsetof(rocmfp2_p1_block, d) == 0, "ROCmFP2 P0 data offset changed"); +_Static_assert(offsetof(rocmfp2_p1_block, s) == 8, "ROCmFP2 P0 scale offset changed"); +#endif + +const char * rocmfp2_p1_status_name(rocmfp2_p1_status status); + +bool rocmfp2_p1_scale_is_valid(uint8_t scale_byte); +double rocmfp2_p1_ue4m3_to_binary64(uint8_t scale_byte); +/* Returns invalid byte 0xff for NaN or a negative target. */ +uint8_t rocmfp2_p1_nearest_ue4m3(double target); + +rocmfp2_p1_status rocmfp2_p1_decode_code( + uint8_t code, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + int8_t * value); + +rocmfp2_p1_status rocmfp2_p1_decode_packed_byte( + uint8_t packed, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + int8_t values[4]); + +rocmfp2_p1_status rocmfp2_p1_pack_codes( + const uint8_t codes[ROCMFP2_P1_BLOCK_WEIGHTS], + const uint8_t scales[ROCMFP2_P1_SCALE_BYTES], + rocmfp2_p1_block * block); + +void rocmfp2_p1_unpack_codes( + const rocmfp2_p1_block * block, + uint8_t codes[ROCMFP2_P1_BLOCK_WEIGHTS]); + +bool rocmfp2_p1_validate_block(const rocmfp2_p1_block * block); +bool rocmfp2_p1_validate_serialized(const void * data, size_t nbytes); + +/* + * The code selectors are exposed so signed-zero and exact-midpoint behavior + * can be tested independently of scale selection. The reference selector + * exhaustively evaluates all four codes. The optimized selector uses the + * symmetric sign/magnitude structure. Both use binary64 arithmetic. + */ +rocmfp2_p1_status rocmfp2_p1_select_code_ref( + double source, + uint8_t scale_byte, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + uint8_t * code); + +rocmfp2_p1_status rocmfp2_p1_select_code_optimized( + double source, + uint8_t scale_byte, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + uint8_t * code); + +/* + * Both quantizers search every legal UE4M3 byte (0x00..0x7e) independently + * for each 16-weight group and minimize unweighted SSE in binary64. Exact + * scale ties select the lower byte. Outputs are committed only on success. + */ +rocmfp2_p1_status rocmfp2_p1_quantize_block_ref( + const double source[ROCMFP2_P1_BLOCK_WEIGHTS], + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + rocmfp2_p1_block * block, + double group_sse[ROCMFP2_P1_SCALE_BYTES]); + +rocmfp2_p1_status rocmfp2_p1_quantize_block_optimized( + const double source[ROCMFP2_P1_BLOCK_WEIGHTS], + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + rocmfp2_p1_block * block, + double group_sse[ROCMFP2_P1_SCALE_BYTES]); + +rocmfp2_p1_status rocmfp2_p1_dequantize_block( + const rocmfp2_p1_block * block, + const rocmfp2_p1_codebook * codebook, + rocmfp2_p1_mapping mapping, + double output[ROCMFP2_P1_BLOCK_WEIGHTS]); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ggml/rocmfpx/rocmfpx.c b/ggml/rocmfpx/rocmfpx.c new file mode 100644 index 000000000000..5aa075ef8701 --- /dev/null +++ b/ggml/rocmfpx/rocmfpx.c @@ -0,0 +1,1354 @@ +#include "rocmfpx.h" + +#include +#include +#include + +// Finite unsigned E4M3 scale bytes decoded to FP32. Precomputed from the same +// exp/mant formula rocmfpx_ue4m3_to_fp32() used to evaluate with ldexpf(): +// exp == 0 -> mant * 2^-10 ; otherwise (8 + mant) * 2^(exp - 11). +// The scale search re-decodes candidate bytes for every block, and dequant +// decodes a scale for every element, so keeping this as a table (identical to +// the former per-call ldexpf result) removes the transcendental from both hot +// paths without changing any produced value. +#define ROCMFPX_SCALE_SUB(M) ((M) * 0x1p-10f) +#define ROCMFPX_SCALE_E(B, M) ((8 + (M)) * (B)) + +static const float rocmfpx_scale_ue4m3[127] = { + ROCMFPX_SCALE_SUB(0), ROCMFPX_SCALE_SUB(1), ROCMFPX_SCALE_SUB(2), ROCMFPX_SCALE_SUB(3), + ROCMFPX_SCALE_SUB(4), ROCMFPX_SCALE_SUB(5), ROCMFPX_SCALE_SUB(6), ROCMFPX_SCALE_SUB(7), + ROCMFPX_SCALE_E(0x1p-10f,0), ROCMFPX_SCALE_E(0x1p-10f,1), ROCMFPX_SCALE_E(0x1p-10f,2), ROCMFPX_SCALE_E(0x1p-10f,3), + ROCMFPX_SCALE_E(0x1p-10f,4), ROCMFPX_SCALE_E(0x1p-10f,5), ROCMFPX_SCALE_E(0x1p-10f,6), ROCMFPX_SCALE_E(0x1p-10f,7), + ROCMFPX_SCALE_E(0x1p-9f,0), ROCMFPX_SCALE_E(0x1p-9f,1), ROCMFPX_SCALE_E(0x1p-9f,2), ROCMFPX_SCALE_E(0x1p-9f,3), + ROCMFPX_SCALE_E(0x1p-9f,4), ROCMFPX_SCALE_E(0x1p-9f,5), ROCMFPX_SCALE_E(0x1p-9f,6), ROCMFPX_SCALE_E(0x1p-9f,7), + ROCMFPX_SCALE_E(0x1p-8f,0), ROCMFPX_SCALE_E(0x1p-8f,1), ROCMFPX_SCALE_E(0x1p-8f,2), ROCMFPX_SCALE_E(0x1p-8f,3), + ROCMFPX_SCALE_E(0x1p-8f,4), ROCMFPX_SCALE_E(0x1p-8f,5), ROCMFPX_SCALE_E(0x1p-8f,6), ROCMFPX_SCALE_E(0x1p-8f,7), + ROCMFPX_SCALE_E(0x1p-7f,0), ROCMFPX_SCALE_E(0x1p-7f,1), ROCMFPX_SCALE_E(0x1p-7f,2), ROCMFPX_SCALE_E(0x1p-7f,3), + ROCMFPX_SCALE_E(0x1p-7f,4), ROCMFPX_SCALE_E(0x1p-7f,5), ROCMFPX_SCALE_E(0x1p-7f,6), ROCMFPX_SCALE_E(0x1p-7f,7), + ROCMFPX_SCALE_E(0x1p-6f,0), ROCMFPX_SCALE_E(0x1p-6f,1), ROCMFPX_SCALE_E(0x1p-6f,2), ROCMFPX_SCALE_E(0x1p-6f,3), + ROCMFPX_SCALE_E(0x1p-6f,4), ROCMFPX_SCALE_E(0x1p-6f,5), ROCMFPX_SCALE_E(0x1p-6f,6), ROCMFPX_SCALE_E(0x1p-6f,7), + ROCMFPX_SCALE_E(0x1p-5f,0), ROCMFPX_SCALE_E(0x1p-5f,1), ROCMFPX_SCALE_E(0x1p-5f,2), ROCMFPX_SCALE_E(0x1p-5f,3), + ROCMFPX_SCALE_E(0x1p-5f,4), ROCMFPX_SCALE_E(0x1p-5f,5), ROCMFPX_SCALE_E(0x1p-5f,6), ROCMFPX_SCALE_E(0x1p-5f,7), + ROCMFPX_SCALE_E(0x1p-4f,0), ROCMFPX_SCALE_E(0x1p-4f,1), ROCMFPX_SCALE_E(0x1p-4f,2), ROCMFPX_SCALE_E(0x1p-4f,3), + ROCMFPX_SCALE_E(0x1p-4f,4), ROCMFPX_SCALE_E(0x1p-4f,5), ROCMFPX_SCALE_E(0x1p-4f,6), ROCMFPX_SCALE_E(0x1p-4f,7), + ROCMFPX_SCALE_E(0x1p-3f,0), ROCMFPX_SCALE_E(0x1p-3f,1), ROCMFPX_SCALE_E(0x1p-3f,2), ROCMFPX_SCALE_E(0x1p-3f,3), + ROCMFPX_SCALE_E(0x1p-3f,4), ROCMFPX_SCALE_E(0x1p-3f,5), ROCMFPX_SCALE_E(0x1p-3f,6), ROCMFPX_SCALE_E(0x1p-3f,7), + ROCMFPX_SCALE_E(0x1p-2f,0), ROCMFPX_SCALE_E(0x1p-2f,1), ROCMFPX_SCALE_E(0x1p-2f,2), ROCMFPX_SCALE_E(0x1p-2f,3), + ROCMFPX_SCALE_E(0x1p-2f,4), ROCMFPX_SCALE_E(0x1p-2f,5), ROCMFPX_SCALE_E(0x1p-2f,6), ROCMFPX_SCALE_E(0x1p-2f,7), + ROCMFPX_SCALE_E(0x1p-1f,0), ROCMFPX_SCALE_E(0x1p-1f,1), ROCMFPX_SCALE_E(0x1p-1f,2), ROCMFPX_SCALE_E(0x1p-1f,3), + ROCMFPX_SCALE_E(0x1p-1f,4), ROCMFPX_SCALE_E(0x1p-1f,5), ROCMFPX_SCALE_E(0x1p-1f,6), ROCMFPX_SCALE_E(0x1p-1f,7), + ROCMFPX_SCALE_E(0x1p0f,0), ROCMFPX_SCALE_E(0x1p0f,1), ROCMFPX_SCALE_E(0x1p0f,2), ROCMFPX_SCALE_E(0x1p0f,3), + ROCMFPX_SCALE_E(0x1p0f,4), ROCMFPX_SCALE_E(0x1p0f,5), ROCMFPX_SCALE_E(0x1p0f,6), ROCMFPX_SCALE_E(0x1p0f,7), + ROCMFPX_SCALE_E(0x1p1f,0), ROCMFPX_SCALE_E(0x1p1f,1), ROCMFPX_SCALE_E(0x1p1f,2), ROCMFPX_SCALE_E(0x1p1f,3), + ROCMFPX_SCALE_E(0x1p1f,4), ROCMFPX_SCALE_E(0x1p1f,5), ROCMFPX_SCALE_E(0x1p1f,6), ROCMFPX_SCALE_E(0x1p1f,7), + ROCMFPX_SCALE_E(0x1p2f,0), ROCMFPX_SCALE_E(0x1p2f,1), ROCMFPX_SCALE_E(0x1p2f,2), ROCMFPX_SCALE_E(0x1p2f,3), + ROCMFPX_SCALE_E(0x1p2f,4), ROCMFPX_SCALE_E(0x1p2f,5), ROCMFPX_SCALE_E(0x1p2f,6), ROCMFPX_SCALE_E(0x1p2f,7), + ROCMFPX_SCALE_E(0x1p3f,0), ROCMFPX_SCALE_E(0x1p3f,1), ROCMFPX_SCALE_E(0x1p3f,2), ROCMFPX_SCALE_E(0x1p3f,3), + ROCMFPX_SCALE_E(0x1p3f,4), ROCMFPX_SCALE_E(0x1p3f,5), ROCMFPX_SCALE_E(0x1p3f,6), ROCMFPX_SCALE_E(0x1p3f,7), + ROCMFPX_SCALE_E(0x1p4f,0), ROCMFPX_SCALE_E(0x1p4f,1), ROCMFPX_SCALE_E(0x1p4f,2), ROCMFPX_SCALE_E(0x1p4f,3), + ROCMFPX_SCALE_E(0x1p4f,4), ROCMFPX_SCALE_E(0x1p4f,5), ROCMFPX_SCALE_E(0x1p4f,6), +}; + +#undef ROCMFPX_SCALE_SUB +#undef ROCMFPX_SCALE_E + +float rocmfpx_ue4m3_to_fp32(uint8_t e) { + return rocmfpx_scale_is_valid(e) ? rocmfpx_scale_ue4m3[e] : 0.0f; +} + +bool rocmfpx_scale_is_valid(uint8_t e) { + return e <= 0x7e; +} + +// Precomputed table of all 127 valid UE4M3 scale values (e = 0x00..0x7e). +// Avoids repeated ldexpf calls in the hot MSE inner loops and in nearest-scale +// binary search. Matches the style of rocmfp4_scale_ue4m3_half in rocmfp4.c. +#define ROCMFPX_SCALE_SUB(M) ((M) * 0x1p-10f) +#define ROCMFPX_SCALE_E1(M) ((8 + (M)) * 0x1p-10f) +#define ROCMFPX_SCALE_E2(M) ((8 + (M)) * 0x1p-9f) +#define ROCMFPX_SCALE_E3(M) ((8 + (M)) * 0x1p-8f) +#define ROCMFPX_SCALE_E4(M) ((8 + (M)) * 0x1p-7f) +#define ROCMFPX_SCALE_E5(M) ((8 + (M)) * 0x1p-6f) +#define ROCMFPX_SCALE_E6(M) ((8 + (M)) * 0x1p-5f) +#define ROCMFPX_SCALE_E7(M) ((8 + (M)) * 0x1p-4f) +#define ROCMFPX_SCALE_E8(M) ((8 + (M)) * 0x1p-3f) +#define ROCMFPX_SCALE_E9(M) ((8 + (M)) * 0x1p-2f) +#define ROCMFPX_SCALE_E10(M) ((8 + (M)) * 0x1p-1f) +#define ROCMFPX_SCALE_E11(M) ((8 + (M)) * 0x1p0f) +#define ROCMFPX_SCALE_E12(M) ((8 + (M)) * 0x1p1f) +#define ROCMFPX_SCALE_E13(M) ((8 + (M)) * 0x1p2f) +#define ROCMFPX_SCALE_E14(M) ((8 + (M)) * 0x1p3f) +#define ROCMFPX_SCALE_E15(M) ((8 + (M)) * 0x1p4f) + +static const float rocmfpx_scale_table[127] = { + ROCMFPX_SCALE_SUB(0), ROCMFPX_SCALE_SUB(1), ROCMFPX_SCALE_SUB(2), ROCMFPX_SCALE_SUB(3), + ROCMFPX_SCALE_SUB(4), ROCMFPX_SCALE_SUB(5), ROCMFPX_SCALE_SUB(6), ROCMFPX_SCALE_SUB(7), + ROCMFPX_SCALE_E1(0), ROCMFPX_SCALE_E1(1), ROCMFPX_SCALE_E1(2), ROCMFPX_SCALE_E1(3), + ROCMFPX_SCALE_E1(4), ROCMFPX_SCALE_E1(5), ROCMFPX_SCALE_E1(6), ROCMFPX_SCALE_E1(7), + ROCMFPX_SCALE_E2(0), ROCMFPX_SCALE_E2(1), ROCMFPX_SCALE_E2(2), ROCMFPX_SCALE_E2(3), + ROCMFPX_SCALE_E2(4), ROCMFPX_SCALE_E2(5), ROCMFPX_SCALE_E2(6), ROCMFPX_SCALE_E2(7), + ROCMFPX_SCALE_E3(0), ROCMFPX_SCALE_E3(1), ROCMFPX_SCALE_E3(2), ROCMFPX_SCALE_E3(3), + ROCMFPX_SCALE_E3(4), ROCMFPX_SCALE_E3(5), ROCMFPX_SCALE_E3(6), ROCMFPX_SCALE_E3(7), + ROCMFPX_SCALE_E4(0), ROCMFPX_SCALE_E4(1), ROCMFPX_SCALE_E4(2), ROCMFPX_SCALE_E4(3), + ROCMFPX_SCALE_E4(4), ROCMFPX_SCALE_E4(5), ROCMFPX_SCALE_E4(6), ROCMFPX_SCALE_E4(7), + ROCMFPX_SCALE_E5(0), ROCMFPX_SCALE_E5(1), ROCMFPX_SCALE_E5(2), ROCMFPX_SCALE_E5(3), + ROCMFPX_SCALE_E5(4), ROCMFPX_SCALE_E5(5), ROCMFPX_SCALE_E5(6), ROCMFPX_SCALE_E5(7), + ROCMFPX_SCALE_E6(0), ROCMFPX_SCALE_E6(1), ROCMFPX_SCALE_E6(2), ROCMFPX_SCALE_E6(3), + ROCMFPX_SCALE_E6(4), ROCMFPX_SCALE_E6(5), ROCMFPX_SCALE_E6(6), ROCMFPX_SCALE_E6(7), + ROCMFPX_SCALE_E7(0), ROCMFPX_SCALE_E7(1), ROCMFPX_SCALE_E7(2), ROCMFPX_SCALE_E7(3), + ROCMFPX_SCALE_E7(4), ROCMFPX_SCALE_E7(5), ROCMFPX_SCALE_E7(6), ROCMFPX_SCALE_E7(7), + ROCMFPX_SCALE_E8(0), ROCMFPX_SCALE_E8(1), ROCMFPX_SCALE_E8(2), ROCMFPX_SCALE_E8(3), + ROCMFPX_SCALE_E8(4), ROCMFPX_SCALE_E8(5), ROCMFPX_SCALE_E8(6), ROCMFPX_SCALE_E8(7), + ROCMFPX_SCALE_E9(0), ROCMFPX_SCALE_E9(1), ROCMFPX_SCALE_E9(2), ROCMFPX_SCALE_E9(3), + ROCMFPX_SCALE_E9(4), ROCMFPX_SCALE_E9(5), ROCMFPX_SCALE_E9(6), ROCMFPX_SCALE_E9(7), + ROCMFPX_SCALE_E10(0), ROCMFPX_SCALE_E10(1), ROCMFPX_SCALE_E10(2), ROCMFPX_SCALE_E10(3), + ROCMFPX_SCALE_E10(4), ROCMFPX_SCALE_E10(5), ROCMFPX_SCALE_E10(6), ROCMFPX_SCALE_E10(7), + ROCMFPX_SCALE_E11(0), ROCMFPX_SCALE_E11(1), ROCMFPX_SCALE_E11(2), ROCMFPX_SCALE_E11(3), + ROCMFPX_SCALE_E11(4), ROCMFPX_SCALE_E11(5), ROCMFPX_SCALE_E11(6), ROCMFPX_SCALE_E11(7), + ROCMFPX_SCALE_E12(0), ROCMFPX_SCALE_E12(1), ROCMFPX_SCALE_E12(2), ROCMFPX_SCALE_E12(3), + ROCMFPX_SCALE_E12(4), ROCMFPX_SCALE_E12(5), ROCMFPX_SCALE_E12(6), ROCMFPX_SCALE_E12(7), + ROCMFPX_SCALE_E13(0), ROCMFPX_SCALE_E13(1), ROCMFPX_SCALE_E13(2), ROCMFPX_SCALE_E13(3), + ROCMFPX_SCALE_E13(4), ROCMFPX_SCALE_E13(5), ROCMFPX_SCALE_E13(6), ROCMFPX_SCALE_E13(7), + ROCMFPX_SCALE_E14(0), ROCMFPX_SCALE_E14(1), ROCMFPX_SCALE_E14(2), ROCMFPX_SCALE_E14(3), + ROCMFPX_SCALE_E14(4), ROCMFPX_SCALE_E14(5), ROCMFPX_SCALE_E14(6), ROCMFPX_SCALE_E14(7), + ROCMFPX_SCALE_E15(0), ROCMFPX_SCALE_E15(1), ROCMFPX_SCALE_E15(2), ROCMFPX_SCALE_E15(3), + ROCMFPX_SCALE_E15(4), ROCMFPX_SCALE_E15(5), ROCMFPX_SCALE_E15(6), +}; + +#undef ROCMFPX_SCALE_SUB +#undef ROCMFPX_SCALE_E1 +#undef ROCMFPX_SCALE_E2 +#undef ROCMFPX_SCALE_E3 +#undef ROCMFPX_SCALE_E4 +#undef ROCMFPX_SCALE_E5 +#undef ROCMFPX_SCALE_E6 +#undef ROCMFPX_SCALE_E7 +#undef ROCMFPX_SCALE_E8 +#undef ROCMFPX_SCALE_E9 +#undef ROCMFPX_SCALE_E10 +#undef ROCMFPX_SCALE_E11 +#undef ROCMFPX_SCALE_E12 +#undef ROCMFPX_SCALE_E13 +#undef ROCMFPX_SCALE_E14 +#undef ROCMFPX_SCALE_E15 + +// O(1) table lookup — use this everywhere inside this file instead of calling +// the public rocmfpx_ue4m3_to_fp32() which goes through the ldexpf path. +static inline float rocmfpx_scale_lookup(uint8_t e) { + return e <= 0x7e ? rocmfpx_scale_table[e] : 0.0f; +} + +// Binary search for the UE4M3 entry nearest to `target`. +// rocmfpx_scale_table is monotonically increasing in [0..0x7e], so we can +// binary-search to a two-element window then pick the closer neighbor. +// This replaces the original O(126) linear scan. Matches rocmfp4_nearest_scale_ue4m3. +static uint8_t rocmfpx_nearest_scale_ue4m3(float target) { + if (!(target > 0.0f) || !isfinite(target)) { + return 0; + } + + int lo = 1; + int hi = 126; + while (lo < hi) { + const int mid = lo + (hi - lo) / 2; + if (rocmfpx_scale_table[mid] < target) { + lo = mid + 1; + } else { + hi = mid; + } + } + + if (lo == 1) { + return 1; + } + + const float hi_scale = rocmfpx_scale_table[lo]; + const float lo_scale = rocmfpx_scale_table[lo - 1]; + + // Ties keep the lower (smaller) scale byte, matching the old ascending scan. + return (target - lo_scale <= hi_scale - target) ? (uint8_t)(lo - 1) : (uint8_t) lo; +} + + +size_t rocmfpx_row_size_fp3(int64_t k) { + assert(k % QK_ROCMFP3 == 0); + return (size_t) (k / QK_ROCMFP3) * sizeof(block_rocmfp3); +} + +size_t rocmfpx_row_size_fp2(int64_t k) { + assert(k % QK_ROCMFP2 == 0); + return (size_t) (k / QK_ROCMFP2) * sizeof(block_rocmfp2); +} + +size_t rocmfpx_row_size_fp6(int64_t k) { + assert(k % QK_ROCMFP6 == 0); + return (size_t) (k / QK_ROCMFP6) * sizeof(block_rocmfp6); +} + +size_t rocmfpx_row_size_fp8(int64_t k) { + assert(k % QK_ROCMFP8 == 0); + return (size_t) (k / QK_ROCMFP8) * sizeof(block_rocmfp8); +} + + + +static float rocmfpx_max_abs(const float * x, int n) { + float max_abs = 0.0f; + + for (int i = 0; i < n; ++i) { + if (!isfinite(x[i])) { + continue; + } + + const float ax = fabsf(x[i]); + if (ax > max_abs) { + max_abs = ax; + } + } + + return max_abs; +} + +static void rocmfpx_prepare_mse_weights( + float * dst, const float * x, int n, const float * quant_weights, float sigma2, + float * max_abs, float * max_abs_weight, bool * all_finite) { + *max_abs = 0.0f; + *max_abs_weight = 0.0f; + *all_finite = true; + + for (int i = 0; i < n; ++i) { + const float ax = fabsf(x[i]); + const float qw = quant_weights[i]; + const float weight = isfinite(qw) && qw > 0.0f && isfinite(x[i]) ? qw * sqrtf(sigma2 + x[i]*x[i]) : 0.0f; + *all_finite = *all_finite && isfinite(x[i]); + + if (isfinite(x[i])) { + if (ax > *max_abs) { + *max_abs = ax; + *max_abs_weight = weight; + } else if (ax == *max_abs && weight > *max_abs_weight) { + *max_abs_weight = weight; + } + } + + // Match llama.cpp imatrix weighting style: calibration importance is + // scaled by row energy so large activations stay protected. + dst[i] = weight; + } +} + +// ROCmFP2 S40 uses the frozen MORD code order {-4, -1, +1, +4}. +static inline int rocmfpx_decode_fp2_code(uint8_t code) { + static const int8_t values[4] = { -4, -1, 1, 4 }; + return values[code & 3u]; +} + +static inline uint8_t rocmfpx_quantize_fp2_code(float x, float inv_scale) { + if (!isfinite(x) || !(inv_scale > 0.0f)) { + return 0; + } + + const float magnitude = fabsf(x * inv_scale); + const bool outer = magnitude > 2.5f; + if (signbit(x)) { + return outer ? 0u : 1u; + } + return outer ? 3u : 2u; +} + +static float rocmfpx_fp2_group_mse_for_scale( + const float * x, const float * mse_weights, int n, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + if (!isfinite(x[i])) { + continue; + } + const float reconstructed = (float) rocmfpx_decode_fp2_code(rocmfpx_quantize_fp2_code(x[i], inv_scale)) * scale; + const float delta = x[i] - reconstructed; + err += (mse_weights ? mse_weights[i] : 1.0f) * delta * delta; + if (err > best_err) { + return err; + } + } + return err; +} + +static uint8_t rocmfpx_choose_scale_fp2_mse( + const float * x, int n, const float * quant_weights, float sigma2) { + float mse_weights[QK_ROCMFP2/2]; + float max_abs = 0.0f; + float max_abs_weight = 0.0f; + bool all_finite = true; + + if (quant_weights) { + rocmfpx_prepare_mse_weights( + mse_weights, x, n, quant_weights, sigma2, + &max_abs, &max_abs_weight, &all_finite); + } else { + max_abs = rocmfpx_max_abs(x, n); + max_abs_weight = 1.0f; + } + GGML_UNUSED(all_finite); + + if (!(max_abs > 0.0f) || !isfinite(max_abs)) { + return 0; + } + + const float * weights = quant_weights ? mse_weights : NULL; + const uint8_t start_e = rocmfpx_nearest_scale_ue4m3(max_abs / 4.0f); + uint8_t best_e = start_e; + float best_err = INFINITY; + bool lower_done = false; + + for (int delta = 0; delta <= 125; ++delta) { + const int e0 = (int) start_e - delta; + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale = rocmfpx_scale_lookup((uint8_t) e0); + const float clip_delta = max_abs - 4.0f * scale; + const float clip_err = max_abs_weight * clip_delta * clip_delta; + if (clip_delta > 0.0f && clip_err > best_err) { + lower_done = true; + } else { + const float err = rocmfpx_fp2_group_mse_for_scale(x, weights, n, (uint8_t) e0, best_err); + if (err < best_err || (err == best_err && e0 < best_e)) { + best_err = err; + best_e = (uint8_t) e0; + } + } + } + + const int e1 = (int) start_e + delta; + if (delta != 0 && e1 >= 1 && e1 <= 126) { + const float err = rocmfpx_fp2_group_mse_for_scale(x, weights, n, (uint8_t) e1, best_err); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = (uint8_t) e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126) { + break; + } + } + return best_e; +} + +static void rocmfpx_quantize_row_fp2_impl( + const float * GGML_RESTRICT x, block_rocmfp2 * GGML_RESTRICT y, + int64_t k, const float * GGML_RESTRICT quant_weights) { + assert(k % QK_ROCMFP2 == 0); + + float sum_x2 = 0.0f; + for (int64_t i = 0; i < k; ++i) { + sum_x2 += isfinite(x[i]) ? x[i] * x[i] : 0.0f; + } + const float sigma2 = sum_x2 / (float) k; + + const int64_t nb = k / QK_ROCMFP2; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib * QK_ROCMFP2; + const float * qw = quant_weights ? quant_weights + ib * QK_ROCMFP2 : NULL; + block_rocmfp2 * yb = y + ib; + + for (int half = 0; half < 2; ++half) { + const int half_off = half * (QK_ROCMFP2 / 2); + const float * xh = xb + half_off; + const float * qh = qw ? qw + half_off : NULL; + yb->e[half] = rocmfpx_choose_scale_fp2_mse(xh, QK_ROCMFP2 / 2, qh, sigma2); + + const float scale = rocmfpx_scale_lookup(yb->e[half]); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + for (int packed = 0; packed < 4; ++packed) { + uint8_t byte = 0; + for (int lane = 0; lane < 4; ++lane) { + const int j = 4 * packed + lane; + byte |= (uint8_t) (rocmfpx_quantize_fp2_code(xh[j], inv_scale) << (2 * lane)); + } + yb->qs[half * 4 + packed] = byte; + } + } + } +} + +void rocmfpx_quantize_row_fp2_ref( + const float * GGML_RESTRICT x, block_rocmfp2 * GGML_RESTRICT y, int64_t k) { + rocmfpx_quantize_row_fp2_impl(x, y, k, NULL); +} + +void rocmfpx_dequantize_row_fp2( + const block_rocmfp2 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP2 == 0); + const int64_t nb = k / QK_ROCMFP2; + for (int64_t ib = 0; ib < nb; ++ib) { + const block_rocmfp2 * xb = x + ib; + float * yb = y + ib * QK_ROCMFP2; + for (int half = 0; half < 2; ++half) { + const float scale = rocmfpx_scale_lookup(xb->e[half]); + for (int j = 0; j < QK_ROCMFP2 / 2; ++j) { + const uint8_t code = (xb->qs[half * 4 + j / 4] >> (2 * (j % 4))) & 3u; + yb[half * (QK_ROCMFP2 / 2) + j] = (float) rocmfpx_decode_fp2_code(code) * scale; + } + } + } +} + +void rocmfpx_quantize_row_fp2( + const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k) { + rocmfpx_quantize_row_fp2_ref(x, (block_rocmfp2 *) y, k); +} + +size_t rocmfpx_quantize_fp2( + const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, + int64_t nrows, int64_t n_per_row, const float * imatrix) { + const size_t row_size = rocmfpx_row_size_fp2(n_per_row); + char * qrow = (char *) dst; + for (int64_t row = 0; row < nrows; ++row) { + rocmfpx_quantize_row_fp2_impl( + src + row * n_per_row, (block_rocmfp2 *) qrow, n_per_row, imatrix); + qrow += row_size; + } + return (size_t) nrows * row_size; +} + +// --------------------------------------------------------------------------- +// FP3 group pack/unpack: 8 × 3-bit codes → 3 bytes (24 bits) +// +// Bit layout across 3 bytes (LSB first within each byte): +// byte 0: [v0 2:0][v1 2:0][v2 1:0] +// byte 1: [v2.2 ][v3 2:0][v4 2:0][v5.0] +// byte 2: [v5 2:1][v6 2:0][v7 2:0] +// +// A 16-element half-block is two consecutive pack8 groups. +// Half 0 → qs[0..5], Half 1 → qs[6..11]. +// --------------------------------------------------------------------------- +static inline void rocmfpx_fp3_pack8(uint8_t * dst, const uint8_t * c) { + dst[0] = (uint8_t)( (c[0] & 7u) | ((c[1] & 7u) << 3) | ((c[2] & 3u) << 6) ); + dst[1] = (uint8_t)( ((c[2] >> 2) & 1u) | ((c[3] & 7u) << 1) | ((c[4] & 7u) << 4) | ((c[5] & 1u) << 7) ); + dst[2] = (uint8_t)( ((c[5] >> 1) & 3u) | ((c[6] & 7u) << 2) | ((c[7] & 7u) << 5) ); +} + +static inline void rocmfpx_fp3_unpack8(const uint8_t * src, uint8_t * c) { + c[0] = src[0] & 7u; + c[1] = (src[0] >> 3) & 7u; + c[2] = ((src[0] >> 6) & 3u) | ((src[1] & 1u) << 2); + c[3] = (src[1] >> 1) & 7u; + c[4] = (src[1] >> 4) & 7u; + c[5] = ((src[1] >> 7) & 1u) | ((src[2] & 3u) << 1); + c[6] = (src[2] >> 2) & 7u; + c[7] = (src[2] >> 5) & 7u; +} + +// --------------------------------------------------------------------------- +// FP6 group pack/unpack: 4 × 6-bit codes → 3 bytes (24 bits) +// +// Bit layout: +// byte 0: [v0 5:0][v1 1:0] +// byte 1: [v1 5:2][v2 3:0] +// byte 2: [v2 5:4][v3 5:0] +// +// A 16-element half-block is four consecutive pack4 groups. +// Half 0 → qs[0..11], Half 1 → qs[12..23]. +// --------------------------------------------------------------------------- +static inline void rocmfpx_fp6_pack4(uint8_t * dst, const uint8_t * c) { + dst[0] = (uint8_t)( (c[0] & 0x3fu) | ((c[1] & 0x03u) << 6) ); + dst[1] = (uint8_t)( ((c[1] >> 2) & 0x0fu) | ((c[2] & 0x0fu) << 4) ); + dst[2] = (uint8_t)( ((c[2] >> 4) & 0x03u) | ((c[3] & 0x3fu) << 2) ); +} + +static inline void rocmfpx_fp6_unpack4(const uint8_t * src, uint8_t * c) { + c[0] = src[0] & 0x3fu; + c[1] = ((src[0] >> 6) & 0x03u) | ((src[1] & 0x0fu) << 2); + c[2] = ((src[1] >> 4) & 0x0fu) | ((src[2] & 0x03u) << 4); + c[3] = (src[2] >> 2) & 0x3fu; +} + +static int rocmfpx_decode_fp3_code(uint8_t code) { + static const int mag[4] = { 0, 1, 2, 4 }; + const int value = mag[code & 3u]; + return (code & 4u) ? -value : value; +} + +static uint8_t rocmfpx_quantize_fp3_code(float x, float inv_scale) { + if (!isfinite(x) || inv_scale <= 0.0f) { + return 0; + } + + const float ax = fabsf(x * inv_scale); + uint8_t mag; + + if (ax <= 0.5f) { + mag = 0; + } else if (ax <= 1.5f) { + mag = 1; + } else if (ax <= 3.0f) { + mag = 2; + } else { + mag = 3; + } + + return mag == 0 ? 0 : (uint8_t) ((x < 0.0f ? 4u : 0u) | mag); +} + +// Fused threshold + decode for finite values in the exhaustive scale search. +// The result exactly matches quantize_fp3_code followed by decode_fp3_code. +static inline float rocmfpx_fp3_decoded_mag(float x, float inv_scale) { + const float a = fabsf(x * inv_scale); + float mag; + if (a <= 0.5f) { + return 0.0f; + } else if (a <= 1.5f) { + mag = 1.0f; + } else if (a <= 3.0f) { + mag = 2.0f; + } else { + mag = 4.0f; + } + return x < 0.0f ? -mag : mag; +} + +static float rocmfpx_fp3_block_mse_for_scale(const float * x, int n, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + if (!isfinite(x[i])) { + continue; + } + + const float y = rocmfpx_fp3_decoded_mag(x[i], inv_scale) * scale; + const float d = x[i] - y; + + err += d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +// Fast path: caller guarantees all x[i] are finite — skips isfinite() per element. +static float rocmfpx_fp3_block_mse_for_scale_finite(const float * x, int n, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float y = rocmfpx_fp3_decoded_mag(x[i], inv_scale) * scale; + const float d = x[i] - y; + + err += d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static float rocmfpx_fp3_block_weighted_mse_for_scale(const float * x, int n, const float * mse_weights, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + if (!isfinite(x[i])) { + continue; + } + + const float y = rocmfpx_fp3_decoded_mag(x[i], inv_scale) * scale; + const float d = x[i] - y; + + err += mse_weights[i]*d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +// Fast path: caller guarantees all x[i] are finite. +static float rocmfpx_fp3_block_weighted_mse_for_scale_finite(const float * x, int n, const float * mse_weights, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float y = rocmfpx_fp3_decoded_mag(x[i], inv_scale) * scale; + const float d = x[i] - y; + + err += mse_weights[i]*d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static uint8_t rocmfpx_choose_scale_fp3_mse_impl( + const float * x, int n, const float * mse_weights, + float max_abs, float max_abs_weight, bool all_finite) { + const uint8_t start_e = rocmfpx_nearest_scale_ue4m3(max_abs / 4.0f); + uint8_t best_e = start_e; + float best_err = INFINITY; + bool lower_done = false; + + for (int delta = 0; delta <= 125; ++delta) { + const int e0 = (int) start_e - delta; + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale = rocmfpx_scale_lookup((uint8_t) e0); + const float clip_delta = max_abs - 4.0f*scale; + const float clip_err = mse_weights ? max_abs_weight*clip_delta*clip_delta : clip_delta*clip_delta; + if (clip_delta > 0.0f && clip_err > best_err) { + lower_done = true; + } else { + const float err = mse_weights ? + (all_finite ? + rocmfpx_fp3_block_weighted_mse_for_scale_finite(x, n, mse_weights, (uint8_t) e0, best_err) : + rocmfpx_fp3_block_weighted_mse_for_scale(x, n, mse_weights, (uint8_t) e0, best_err)) : + (all_finite ? + rocmfpx_fp3_block_mse_for_scale_finite(x, n, (uint8_t) e0, best_err) : + rocmfpx_fp3_block_mse_for_scale(x, n, (uint8_t) e0, best_err)); + if (err < best_err || (err == best_err && e0 < best_e)) { + best_err = err; + best_e = (uint8_t) e0; + } + } + } + + const int e1 = (int) start_e + delta; + if (delta != 0 && e1 >= 1 && e1 <= 126) { + const float err = mse_weights ? + (all_finite ? + rocmfpx_fp3_block_weighted_mse_for_scale_finite(x, n, mse_weights, (uint8_t) e1, best_err) : + rocmfpx_fp3_block_weighted_mse_for_scale(x, n, mse_weights, (uint8_t) e1, best_err)) : + (all_finite ? + rocmfpx_fp3_block_mse_for_scale_finite(x, n, (uint8_t) e1, best_err) : + rocmfpx_fp3_block_mse_for_scale(x, n, (uint8_t) e1, best_err)); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = (uint8_t) e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126) { + break; + } + } + + return best_e; +} + +static uint8_t rocmfpx_choose_scale_fp3_mse(const float * x, int n) { + const float max_abs = rocmfpx_max_abs(x, n); + if (!(max_abs > 0.0f) || !isfinite(max_abs)) { + return 0; + } + // rocmfpx_max_abs skips non-finite, but we need to know if all are finite + // to select the fast path. Use a simple scan here since max_abs already ran. + bool all_finite = true; + for (int i = 0; i < n; ++i) { + all_finite = all_finite && isfinite(x[i]); + } + + return rocmfpx_choose_scale_fp3_mse_impl(x, n, NULL, max_abs, 0.0f, all_finite); +} + +static uint8_t rocmfpx_choose_scale_fp3_weighted_mse(const float * x, int n, const float * quant_weights, float sigma2) { + assert(n <= QK_ROCMFP3); + float mse_weights[QK_ROCMFP3]; + float max_abs; + float max_abs_weight; + bool all_finite; + rocmfpx_prepare_mse_weights(mse_weights, x, n, quant_weights, sigma2, &max_abs, &max_abs_weight, &all_finite); + if (!(max_abs > 0.0f) || !isfinite(max_abs)) { + return 0; + } + + return rocmfpx_choose_scale_fp3_mse_impl(x, n, mse_weights, max_abs, max_abs_weight, all_finite); +} + +static int rocmfpx_decode_fp6_code(uint8_t code) { + const int mag = code & 31u; + return (code & 32u) ? -(mag == 0 ? 32 : mag) : mag; +} + +static uint8_t rocmfpx_quantize_fp6_code(float x, float inv_scale) { + if (!isfinite(x) || !isfinite(inv_scale) || inv_scale <= 0.0f) { + return 0; + } + + int q = (int) lroundf(x * inv_scale); + if (q > 31) { + q = 31; + } else if (q < -32) { + q = -32; + } + + return q == 0 ? 0 : (uint8_t) (q < 0 ? (32u | ((uint8_t) -q & 31u)) : (uint8_t) q); +} + +// Fused round, clamp, and decode for finite values in the scale search. Keep +// current main's asymmetric signed range [-32, 31], including the encoded -32 +// endpoint, rather than the older experimental branch's [-31, 31] behavior. +static inline float rocmfpx_fp6_decoded_value(float x, float inv_scale) { + int q = (int) lroundf(x * inv_scale); + if (q > 31) { + q = 31; + } else if (q < -32) { + q = -32; + } + + return (float) q; +} + +static float rocmfpx_fp6_block_mse_for_scale(const float * x, int n, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + if (!isfinite(x[i])) { + continue; + } + const float y = rocmfpx_fp6_decoded_value(x[i], inv_scale) * scale; + const float d = x[i] - y; + err += d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +// Fast path: caller guarantees all x[i] are finite. +static float rocmfpx_fp6_block_mse_for_scale_finite(const float * x, int n, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float y = rocmfpx_fp6_decoded_value(x[i], inv_scale) * scale; + const float d = x[i] - y; + err += d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static float rocmfpx_fp6_block_weighted_mse_for_scale(const float * x, int n, const float * mse_weights, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + if (!isfinite(x[i])) { + continue; + } + const float y = rocmfpx_fp6_decoded_value(x[i], inv_scale) * scale; + const float d = x[i] - y; + err += mse_weights[i]*d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +// Fast path: caller guarantees all x[i] are finite. +static float rocmfpx_fp6_block_weighted_mse_for_scale_finite(const float * x, int n, const float * mse_weights, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float y = rocmfpx_fp6_decoded_value(x[i], inv_scale) * scale; + const float d = x[i] - y; + err += mse_weights[i]*d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static uint8_t rocmfpx_choose_scale_fp6_mse_impl( + const float * x, int n, const float * mse_weights, + float max_abs, float max_abs_weight, bool all_finite) { + const uint8_t start_e = rocmfpx_nearest_scale_ue4m3(max_abs / 31.0f); + uint8_t best_e = start_e; + float best_err = INFINITY; + bool lower_done = false; + + for (int delta = 0; delta <= 125; ++delta) { + const int e0 = (int) start_e - delta; + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale = rocmfpx_scale_lookup((uint8_t) e0); + const float clip_delta = max_abs - 31.0f*scale; + const float clip_err = mse_weights ? max_abs_weight*clip_delta*clip_delta : clip_delta*clip_delta; + if (clip_delta > 0.0f && clip_err > best_err) { + lower_done = true; + } else { + const float err = mse_weights ? + (all_finite ? + rocmfpx_fp6_block_weighted_mse_for_scale_finite(x, n, mse_weights, (uint8_t) e0, best_err) : + rocmfpx_fp6_block_weighted_mse_for_scale(x, n, mse_weights, (uint8_t) e0, best_err)) : + (all_finite ? + rocmfpx_fp6_block_mse_for_scale_finite(x, n, (uint8_t) e0, best_err) : + rocmfpx_fp6_block_mse_for_scale(x, n, (uint8_t) e0, best_err)); + if (err < best_err || (err == best_err && e0 < best_e)) { + best_err = err; + best_e = (uint8_t) e0; + } + } + } + + const int e1 = (int) start_e + delta; + if (delta != 0 && e1 >= 1 && e1 <= 126) { + const float err = mse_weights ? + (all_finite ? + rocmfpx_fp6_block_weighted_mse_for_scale_finite(x, n, mse_weights, (uint8_t) e1, best_err) : + rocmfpx_fp6_block_weighted_mse_for_scale(x, n, mse_weights, (uint8_t) e1, best_err)) : + (all_finite ? + rocmfpx_fp6_block_mse_for_scale_finite(x, n, (uint8_t) e1, best_err) : + rocmfpx_fp6_block_mse_for_scale(x, n, (uint8_t) e1, best_err)); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = (uint8_t) e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126) { + break; + } + } + + return best_e; +} + +static uint8_t rocmfpx_choose_scale_fp6_mse(const float * x, int n) { + const float max_abs = rocmfpx_max_abs(x, n); + if (!(max_abs > 0.0f) || !isfinite(max_abs)) { + return 0; + } + bool all_finite = true; + for (int i = 0; i < n; ++i) { + all_finite = all_finite && isfinite(x[i]); + } + + return rocmfpx_choose_scale_fp6_mse_impl(x, n, NULL, max_abs, 0.0f, all_finite); +} + +static uint8_t rocmfpx_choose_scale_fp6_weighted_mse(const float * x, int n, const float * quant_weights, float sigma2) { + assert(n <= QK_ROCMFP6); + float mse_weights[QK_ROCMFP6]; + float max_abs; + float max_abs_weight; + bool all_finite; + rocmfpx_prepare_mse_weights(mse_weights, x, n, quant_weights, sigma2, &max_abs, &max_abs_weight, &all_finite); + if (!(max_abs > 0.0f) || !isfinite(max_abs)) { + return 0; + } + + return rocmfpx_choose_scale_fp6_mse_impl(x, n, mse_weights, max_abs, max_abs_weight, all_finite); +} + +static int8_t rocmfpx_quantize_fp8_code(float x, float inv_scale) { + if (!isfinite(x) || inv_scale <= 0.0f) { + return 0; + } + + int q = (int) lroundf(x * inv_scale); + if (q > 127) { + q = 127; + } else if (q < -127) { + q = -127; + } + + return (int8_t) q; +} + +static float rocmfpx_fp8_block_weighted_mse_for_scale(const float * x, int n, const float * mse_weights, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + if (!isfinite(x[i])) { + continue; + } + + const int8_t code = rocmfpx_quantize_fp8_code(x[i], inv_scale); + const float y = (float) code * scale; + const float d = x[i] - y; + + err += mse_weights[i]*d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +// Fast path: caller guarantees all x[i] are finite. +static float rocmfpx_fp8_block_weighted_mse_for_scale_finite(const float * x, int n, const float * mse_weights, uint8_t e, float best_err) { + const float scale = rocmfpx_scale_lookup(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const int8_t code = rocmfpx_quantize_fp8_code(x[i], inv_scale); + const float y = (float) code * scale; + const float d = x[i] - y; + + err += mse_weights[i]*d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +static uint8_t rocmfpx_choose_scale_fp8_weighted_mse(const float * x, int n, const float * quant_weights, float sigma2) { + assert(n <= QK_ROCMFP8); + float mse_weights[QK_ROCMFP8]; + float max_abs; + float max_abs_weight; + bool all_finite; + rocmfpx_prepare_mse_weights(mse_weights, x, n, quant_weights, sigma2, &max_abs, &max_abs_weight, &all_finite); + if (!(max_abs > 0.0f) || !isfinite(max_abs)) { + return 0; + } + + const uint8_t start_e = rocmfpx_nearest_scale_ue4m3(max_abs / 127.0f); + uint8_t best_e = start_e; + float best_err = INFINITY; + bool lower_done = false; + + for (int delta = 0; delta <= 125; ++delta) { + const int e0 = (int) start_e - delta; + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale = rocmfpx_scale_lookup((uint8_t) e0); + const float clip_delta = max_abs - 127.0f*scale; + if (clip_delta > 0.0f && max_abs_weight*clip_delta*clip_delta > best_err) { + lower_done = true; + } else { + const float err = all_finite ? + rocmfpx_fp8_block_weighted_mse_for_scale_finite(x, n, mse_weights, (uint8_t) e0, best_err) : + rocmfpx_fp8_block_weighted_mse_for_scale(x, n, mse_weights, (uint8_t) e0, best_err); + if (err < best_err || (err == best_err && e0 < best_e)) { + best_err = err; + best_e = (uint8_t) e0; + } + } + } + + const int e1 = (int) start_e + delta; + if (delta != 0 && e1 >= 1 && e1 <= 126) { + const float err = all_finite ? + rocmfpx_fp8_block_weighted_mse_for_scale_finite(x, n, mse_weights, (uint8_t) e1, best_err) : + rocmfpx_fp8_block_weighted_mse_for_scale(x, n, mse_weights, (uint8_t) e1, best_err); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = (uint8_t) e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126) { + break; + } + } + + return best_e; +} + +void rocmfpx_quantize_row_fp3_ref(const float * GGML_RESTRICT x, block_rocmfp3 * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP3 == 0); + + const int64_t nb = k / QK_ROCMFP3; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP3; + block_rocmfp3 * yb = y + ib; + + for (int half = 0; half < 2; ++half) { + const int half_off = half * (QK_ROCMFP3/2); + const float * xh = xb + half_off; + yb->e[half] = rocmfpx_choose_scale_fp3_mse(xh, QK_ROCMFP3/2); + + const float scale = rocmfpx_scale_lookup(yb->e[half]); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + + // Compute 16 codes then pack into 6 bytes (2 groups of 8). + uint8_t codes[QK_ROCMFP3/2]; + for (int j = 0; j < QK_ROCMFP3/2; ++j) { + codes[j] = rocmfpx_quantize_fp3_code(xh[j], inv_scale); + } + rocmfpx_fp3_pack8(yb->qs + half_off*3/8, codes); + rocmfpx_fp3_pack8(yb->qs + half_off*3/8 + 3, codes + 8); + } + } +} + +static void rocmfpx_quantize_row_fp3_weighted( + const float * GGML_RESTRICT x, block_rocmfp3 * GGML_RESTRICT y, int64_t k, const float * GGML_RESTRICT quant_weights) { + assert(k % QK_ROCMFP3 == 0); + + float sum_x2 = 0.0f; + for (int64_t i = 0; i < k; ++i) { + sum_x2 += isfinite(x[i]) ? x[i]*x[i] : 0.0f; + } + const float sigma2 = sum_x2 / (float) k; + + const int64_t nb = k / QK_ROCMFP3; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP3; + const float * qw = quant_weights ? quant_weights + ib*QK_ROCMFP3 : NULL; + block_rocmfp3 * yb = y + ib; + + for (int half = 0; half < 2; ++half) { + const int half_off = half * (QK_ROCMFP3/2); + const float * xh = xb + half_off; + const float * qh = qw ? qw + half_off : NULL; + yb->e[half] = qh ? + rocmfpx_choose_scale_fp3_weighted_mse(xh, QK_ROCMFP3/2, qh, sigma2) : + rocmfpx_choose_scale_fp3_mse(xh, QK_ROCMFP3/2); + + const float scale = rocmfpx_scale_lookup(yb->e[half]); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + + uint8_t codes[QK_ROCMFP3/2]; + for (int j = 0; j < QK_ROCMFP3/2; ++j) { + codes[j] = rocmfpx_quantize_fp3_code(xh[j], inv_scale); + } + rocmfpx_fp3_pack8(yb->qs + half_off*3/8, codes); + rocmfpx_fp3_pack8(yb->qs + half_off*3/8 + 3, codes + 8); + } + } +} + +void rocmfpx_dequantize_row_fp3(const block_rocmfp3 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP3 == 0); + + const int64_t nb = k / QK_ROCMFP3; + for (int64_t ib = 0; ib < nb; ++ib) { + const block_rocmfp3 * xb = x + ib; + float * yb = y + ib*QK_ROCMFP3; + + // Unpack all 32 codes in 4 groups of 8 (4 × 3 bytes). + uint8_t codes[QK_ROCMFP3]; + rocmfpx_fp3_unpack8(xb->qs, codes); + rocmfpx_fp3_unpack8(xb->qs + 3, codes + 8); + rocmfpx_fp3_unpack8(xb->qs + 6, codes + 16); + rocmfpx_fp3_unpack8(xb->qs + 9, codes + 24); + + for (int half = 0; half < 2; ++half) { + const float scale = rocmfpx_scale_lookup(xb->e[half]); + for (int j = 0; j < QK_ROCMFP3/2; ++j) { + const int i = half*(QK_ROCMFP3/2) + j; + yb[i] = (float) rocmfpx_decode_fp3_code(codes[i]) * scale; + } + } + } +} + +void rocmfpx_quantize_row_fp3(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k) { + rocmfpx_quantize_row_fp3_ref(x, (block_rocmfp3 *) y, k); +} + +size_t rocmfpx_quantize_fp3(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix) { + const size_t row_size = rocmfpx_row_size_fp3(n_per_row); + char * qrow = (char *) dst; + + for (int64_t row = 0; row < nrows; ++row) { + if (imatrix) { + rocmfpx_quantize_row_fp3_weighted(src + row*n_per_row, (block_rocmfp3 *) qrow, n_per_row, imatrix); + } else { + rocmfpx_quantize_row_fp3_ref(src + row*n_per_row, (block_rocmfp3 *) qrow, n_per_row); + } + qrow += row_size; + } + + return (size_t) nrows * row_size; +} + +static void rocmfpx_quantize_row_fp6_weighted( + const float * GGML_RESTRICT x, block_rocmfp6 * GGML_RESTRICT y, int64_t k, const float * GGML_RESTRICT quant_weights) { + assert(k % QK_ROCMFP6 == 0); + + float sum_x2 = 0.0f; + for (int64_t i = 0; i < k; ++i) { + sum_x2 += isfinite(x[i]) ? x[i]*x[i] : 0.0f; + } + const float sigma2 = sum_x2 / (float) k; + + const int64_t nb = k / QK_ROCMFP6; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP6; + const float * qw = quant_weights ? quant_weights + ib*QK_ROCMFP6 : NULL; + block_rocmfp6 * yb = y + ib; + + for (int half = 0; half < 2; ++half) { + const int half_off = half * (QK_ROCMFP6/2); + const float * xh = xb + half_off; + const float * qh = qw ? qw + half_off : NULL; + yb->e[half] = qh ? + rocmfpx_choose_scale_fp6_weighted_mse(xh, QK_ROCMFP6/2, qh, sigma2) : + rocmfpx_choose_scale_fp6_mse(xh, QK_ROCMFP6/2); + + const float scale = rocmfpx_scale_lookup(yb->e[half]); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + + // 16 codes packed into 4 groups of 4 (4 × 3 bytes = 12 bytes per half). + uint8_t codes[QK_ROCMFP6/2]; + for (int j = 0; j < QK_ROCMFP6/2; ++j) { + codes[j] = rocmfpx_quantize_fp6_code(xh[j], inv_scale); + } + uint8_t * qsdst = yb->qs + half_off * 6 / 8; + rocmfpx_fp6_pack4(qsdst, codes); + rocmfpx_fp6_pack4(qsdst + 3, codes + 4); + rocmfpx_fp6_pack4(qsdst + 6, codes + 8); + rocmfpx_fp6_pack4(qsdst + 9, codes + 12); + } + } +} + +void rocmfpx_quantize_row_fp6_ref(const float * GGML_RESTRICT x, block_rocmfp6 * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP6 == 0); + + const int64_t nb = k / QK_ROCMFP6; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP6; + block_rocmfp6 * yb = y + ib; + + for (int half = 0; half < 2; ++half) { + const int half_off = half * (QK_ROCMFP6/2); + const float * xh = xb + half_off; + yb->e[half] = rocmfpx_choose_scale_fp6_mse(xh, QK_ROCMFP6/2); + + const float scale = rocmfpx_scale_lookup(yb->e[half]); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + + uint8_t codes[QK_ROCMFP6/2]; + for (int j = 0; j < QK_ROCMFP6/2; ++j) { + codes[j] = rocmfpx_quantize_fp6_code(xh[j], inv_scale); + } + uint8_t * qsdst = yb->qs + half_off * 6 / 8; + rocmfpx_fp6_pack4(qsdst, codes); + rocmfpx_fp6_pack4(qsdst + 3, codes + 4); + rocmfpx_fp6_pack4(qsdst + 6, codes + 8); + rocmfpx_fp6_pack4(qsdst + 9, codes + 12); + } + } +} + +void rocmfpx_dequantize_row_fp6(const block_rocmfp6 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP6 == 0); + + const int64_t nb = k / QK_ROCMFP6; + for (int64_t ib = 0; ib < nb; ++ib) { + const block_rocmfp6 * xb = x + ib; + float * yb = y + ib*QK_ROCMFP6; + + // Unpack all 32 codes in 8 groups of 4 (8 × 3 bytes = 24 bytes). + uint8_t codes[QK_ROCMFP6]; + rocmfpx_fp6_unpack4(xb->qs, codes); + rocmfpx_fp6_unpack4(xb->qs + 3, codes + 4); + rocmfpx_fp6_unpack4(xb->qs + 6, codes + 8); + rocmfpx_fp6_unpack4(xb->qs + 9, codes + 12); + rocmfpx_fp6_unpack4(xb->qs + 12, codes + 16); + rocmfpx_fp6_unpack4(xb->qs + 15, codes + 20); + rocmfpx_fp6_unpack4(xb->qs + 18, codes + 24); + rocmfpx_fp6_unpack4(xb->qs + 21, codes + 28); + + for (int half = 0; half < 2; ++half) { + const float scale = rocmfpx_scale_lookup(xb->e[half]); + for (int j = 0; j < QK_ROCMFP6/2; ++j) { + const int i = half*(QK_ROCMFP6/2) + j; + yb[i] = (float) rocmfpx_decode_fp6_code(codes[i]) * scale; + } + } + } +} + +void rocmfpx_quantize_row_fp6(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k) { + rocmfpx_quantize_row_fp6_ref(x, (block_rocmfp6 *) y, k); +} + +size_t rocmfpx_quantize_fp6(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix) { + const size_t row_size = rocmfpx_row_size_fp6(n_per_row); + char * qrow = (char *) dst; + + for (int64_t row = 0; row < nrows; ++row) { + if (imatrix) { + rocmfpx_quantize_row_fp6_weighted(src + row*n_per_row, (block_rocmfp6 *) qrow, n_per_row, imatrix); + } else { + rocmfpx_quantize_row_fp6_ref(src + row*n_per_row, (block_rocmfp6 *) qrow, n_per_row); + } + qrow += row_size; + } + + return (size_t) nrows * row_size; +} + +static void rocmfpx_quantize_row_fp8_weighted( + const float * GGML_RESTRICT x, block_rocmfp8 * GGML_RESTRICT y, int64_t k, const float * GGML_RESTRICT quant_weights) { + assert(k % QK_ROCMFP8 == 0); + + float sum_x2 = 0.0f; + for (int64_t i = 0; i < k; ++i) { + sum_x2 += isfinite(x[i]) ? x[i]*x[i] : 0.0f; + } + const float sigma2 = sum_x2 / (float) k; + + const int64_t nb = k / QK_ROCMFP8; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP8; + const float * qw = quant_weights ? quant_weights + ib*QK_ROCMFP8 : NULL; + block_rocmfp8 * yb = y + ib; + + yb->e = qw ? rocmfpx_choose_scale_fp8_weighted_mse(xb, QK_ROCMFP8, qw, sigma2) : + rocmfpx_nearest_scale_ue4m3(rocmfpx_max_abs(xb, QK_ROCMFP8) / 127.0f); + + const float scale = rocmfpx_scale_lookup(yb->e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + + for (int i = 0; i < QK_ROCMFP8; ++i) { + yb->qs[i] = rocmfpx_quantize_fp8_code(xb[i], inv_scale); + } + } +} + +void rocmfpx_quantize_row_fp8_ref(const float * GGML_RESTRICT x, block_rocmfp8 * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP8 == 0); + + const int64_t nb = k / QK_ROCMFP8; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP8; + block_rocmfp8 * yb = y + ib; + + const float max_abs = rocmfpx_max_abs(xb, QK_ROCMFP8); + yb->e = rocmfpx_nearest_scale_ue4m3(max_abs / 127.0f); + + const float scale = rocmfpx_scale_lookup(yb->e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + + for (int i = 0; i < QK_ROCMFP8; ++i) { + yb->qs[i] = rocmfpx_quantize_fp8_code(xb[i], inv_scale); + } + } +} + +void rocmfpx_dequantize_row_fp8(const block_rocmfp8 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + assert(k % QK_ROCMFP8 == 0); + + const int64_t nb = k / QK_ROCMFP8; + for (int64_t ib = 0; ib < nb; ++ib) { + const block_rocmfp8 * xb = x + ib; + float * yb = y + ib*QK_ROCMFP8; + + const float scale = rocmfpx_scale_lookup(xb->e); + for (int i = 0; i < QK_ROCMFP8; ++i) { + yb[i] = (float) xb->qs[i] * scale; + } + } +} + +void rocmfpx_quantize_row_fp8(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k) { + rocmfpx_quantize_row_fp8_ref(x, (block_rocmfp8 *) y, k); +} + +size_t rocmfpx_quantize_fp8(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix) { + const size_t row_size = rocmfpx_row_size_fp8(n_per_row); + char * qrow = (char *) dst; + + for (int64_t row = 0; row < nrows; ++row) { + if (imatrix) { + rocmfpx_quantize_row_fp8_weighted(src + row*n_per_row, (block_rocmfp8 *) qrow, n_per_row, imatrix); + } else { + rocmfpx_quantize_row_fp8_ref(src + row*n_per_row, (block_rocmfp8 *) qrow, n_per_row); + } + qrow += row_size; + } + + return (size_t) nrows * row_size; +} + +bool rocmfpx_validate_row_data_fp2(const void * data, size_t nbytes) { + if (nbytes % sizeof(block_rocmfp2) != 0) { + return false; + } + + const block_rocmfp2 * blocks = (const block_rocmfp2 *) data; + const size_t nb = nbytes / sizeof(block_rocmfp2); + for (size_t i = 0; i < nb; ++i) { + if (!rocmfpx_scale_is_valid(blocks[i].e[0]) || !rocmfpx_scale_is_valid(blocks[i].e[1])) { + return false; + } + } + return true; +} + +bool rocmfpx_validate_row_data_fp3(const void * data, size_t nbytes) { + if (nbytes % sizeof(block_rocmfp3) != 0) { + return false; + } + + const block_rocmfp3 * blocks = (const block_rocmfp3 *) data; + const size_t nb = nbytes / sizeof(block_rocmfp3); + + for (size_t i = 0; i < nb; ++i) { + if (!rocmfpx_scale_is_valid(blocks[i].e[0]) || !rocmfpx_scale_is_valid(blocks[i].e[1])) { + return false; + } + } + + return true; +} + +bool rocmfpx_validate_row_data_fp6(const void * data, size_t nbytes) { + if (nbytes % sizeof(block_rocmfp6) != 0) { + return false; + } + + const block_rocmfp6 * blocks = (const block_rocmfp6 *) data; + const size_t nb = nbytes / sizeof(block_rocmfp6); + + for (size_t i = 0; i < nb; ++i) { + if (!rocmfpx_scale_is_valid(blocks[i].e[0]) || !rocmfpx_scale_is_valid(blocks[i].e[1])) { + return false; + } + } + + return true; +} + +bool rocmfpx_validate_row_data_fp8(const void * data, size_t nbytes) { + if (nbytes % sizeof(block_rocmfp8) != 0) { + return false; + } + + const block_rocmfp8 * blocks = (const block_rocmfp8 *) data; + const size_t nb = nbytes / sizeof(block_rocmfp8); + + for (size_t i = 0; i < nb; ++i) { + if (!rocmfpx_scale_is_valid(blocks[i].e)) { + return false; + } + } + + return true; +} diff --git a/ggml/rocmfpx/rocmfpx.h b/ggml/rocmfpx/rocmfpx.h new file mode 100644 index 000000000000..579d7e448b59 --- /dev/null +++ b/ggml/rocmfpx/rocmfpx.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include + +#include "ggml.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define QK_ROCMFPX 32 + +#define QK_ROCMFP2 QK_ROCMFPX +#define QK_ROCMFP3 QK_ROCMFPX +#define QK_ROCMFP6 QK_ROCMFPX +#define QK_ROCMFP8 QK_ROCMFPX + +#define QS_ROCMFP2 ((QK_ROCMFP2 * 2) / 8) +#define QR_ROCMFP2 1 +#define QI_ROCMFP2 (QK_ROCMFP2 / (4 * QR_ROCMFP2)) +#define QS_ROCMFP3 ((QK_ROCMFP3 * 3) / 8) +#define QS_ROCMFP6 ((QK_ROCMFP6 * 6) / 8) +#define QS_ROCMFP8 QK_ROCMFP8 + +#define QR_ROCMFP3 1 +#define QI_ROCMFP3 (QK_ROCMFP3 / (4 * QR_ROCMFP3)) + +#define QR_ROCMFP6 1 +#define QI_ROCMFP6 (QK_ROCMFP6 / (4 * QR_ROCMFP6)) + +#define QR_ROCMFP8 1 +#define QI_ROCMFP8 (QK_ROCMFP8 / (4 * QR_ROCMFP8)) + +// AMD-native experimental family layouts. The GGUF types are registered, but +// the layouts stay isolated from the promoted ROCmFP4 formats while evaluated. +typedef struct { + uint8_t qs[QS_ROCMFP2]; + uint8_t e[2]; +} block_rocmfp2; + +typedef struct { + uint8_t qs[QS_ROCMFP3]; + uint8_t e[2]; +} block_rocmfp3; + +typedef struct { + uint8_t qs[QS_ROCMFP6]; + uint8_t e[2]; +} block_rocmfp6; + +typedef struct { + int8_t qs[QS_ROCMFP8]; + uint8_t e; +} block_rocmfp8; + +#if defined(__cplusplus) +static_assert(sizeof(block_rocmfp2) == QS_ROCMFP2 + 2*sizeof(uint8_t), "wrong rocmfp2 block size/padding"); +static_assert(sizeof(block_rocmfp3) == QS_ROCMFP3 + 2*sizeof(uint8_t), "wrong rocmfp3 block size/padding"); +static_assert(sizeof(block_rocmfp6) == QS_ROCMFP6 + 2*sizeof(uint8_t), "wrong rocmfp6 block size/padding"); +static_assert(sizeof(block_rocmfp8) == QS_ROCMFP8 + sizeof(uint8_t), "wrong rocmfp8 block size/padding"); +#else +_Static_assert(sizeof(block_rocmfp2) == QS_ROCMFP2 + 2*sizeof(uint8_t), "wrong rocmfp2 block size/padding"); +_Static_assert(sizeof(block_rocmfp3) == QS_ROCMFP3 + 2*sizeof(uint8_t), "wrong rocmfp3 block size/padding"); +_Static_assert(sizeof(block_rocmfp6) == QS_ROCMFP6 + 2*sizeof(uint8_t), "wrong rocmfp6 block size/padding"); +_Static_assert(sizeof(block_rocmfp8) == QS_ROCMFP8 + sizeof(uint8_t), "wrong rocmfp8 block size/padding"); +#endif + +GGML_API float rocmfpx_ue4m3_to_fp32(uint8_t e); +GGML_API bool rocmfpx_scale_is_valid(uint8_t e); +GGML_API size_t rocmfpx_row_size_fp2(int64_t k); +GGML_API size_t rocmfpx_row_size_fp3(int64_t k); +GGML_API size_t rocmfpx_row_size_fp6(int64_t k); +GGML_API size_t rocmfpx_row_size_fp8(int64_t k); + +GGML_API void rocmfpx_quantize_row_fp2_ref(const float * GGML_RESTRICT x, block_rocmfp2 * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfpx_dequantize_row_fp2(const block_rocmfp2 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfpx_quantize_row_fp2(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); +GGML_API size_t rocmfpx_quantize_fp2(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); + +GGML_API void rocmfpx_quantize_row_fp3_ref(const float * GGML_RESTRICT x, block_rocmfp3 * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfpx_dequantize_row_fp3(const block_rocmfp3 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfpx_quantize_row_fp3(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); +GGML_API size_t rocmfpx_quantize_fp3(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); + +GGML_API void rocmfpx_quantize_row_fp6_ref(const float * GGML_RESTRICT x, block_rocmfp6 * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfpx_dequantize_row_fp6(const block_rocmfp6 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfpx_quantize_row_fp6(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); +GGML_API size_t rocmfpx_quantize_fp6(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); + +GGML_API void rocmfpx_quantize_row_fp8_ref(const float * GGML_RESTRICT x, block_rocmfp8 * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfpx_dequantize_row_fp8(const block_rocmfp8 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void rocmfpx_quantize_row_fp8(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); +GGML_API size_t rocmfpx_quantize_fp8(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); + +GGML_API bool rocmfpx_validate_row_data_fp2(const void * data, size_t nbytes); +GGML_API bool rocmfpx_validate_row_data_fp3(const void * data, size_t nbytes); +GGML_API bool rocmfpx_validate_row_data_fp6(const void * data, size_t nbytes); +GGML_API bool rocmfpx_validate_row_data_fp8(const void * data, size_t nbytes); + +#ifdef __cplusplus +} +#endif diff --git a/ggml/rocmfpx/rocmfpx_hip_codebook.cuh b/ggml/rocmfpx/rocmfpx_hip_codebook.cuh new file mode 100644 index 000000000000..2faa9c3caeb1 --- /dev/null +++ b/ggml/rocmfpx/rocmfpx_hip_codebook.cuh @@ -0,0 +1,45 @@ +#pragma once + +#include + +static __device__ __constant__ const int8_t rocmfpx_fp3_codebook[8] = { + 0, 1, 2, 4, 0, -1, -2, -4, +}; + +static __device__ __constant__ const int8_t rocmfpx_fp6_codebook[64] = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + 0, -1, -2, -3, -4, -5, -6, -7, + -8, -9, -10, -11, -12, -13, -14, -15, + -16, -17, -18, -19, -20, -21, -22, -23, + -24, -25, -26, -27, -28, -29, -30, -31, +}; + +static __device__ __forceinline__ int rocmfpx_pack4_fp3_codes(const uint32_t bits12) { + const uint32_t c0 = (bits12 >> 0) & 7u; + const uint32_t c1 = (bits12 >> 3) & 7u; + const uint32_t c2 = (bits12 >> 6) & 7u; + const uint32_t c3 = (bits12 >> 9) & 7u; + const char4 v = make_char4( + rocmfpx_fp3_codebook[c0], + rocmfpx_fp3_codebook[c1], + rocmfpx_fp3_codebook[c2], + rocmfpx_fp3_codebook[c3]); + return *((const int *) &v); +} + +static __device__ __forceinline__ int rocmfpx_pack4_fp6_codes(const uint32_t bits24) { + const uint32_t c0 = (bits24 >> 0) & 63u; + const uint32_t c1 = (bits24 >> 6) & 63u; + const uint32_t c2 = (bits24 >> 12) & 63u; + const uint32_t c3 = (bits24 >> 18) & 63u; + + const char4 v = make_char4( + rocmfpx_fp6_codebook[c0], + rocmfpx_fp6_codebook[c1], + rocmfpx_fp6_codebook[c2], + rocmfpx_fp6_codebook[c3]); + return *((const int *) &v); +} diff --git a/ggml/rocmfpx/test_rocmfp2_reference.c b/ggml/rocmfpx/test_rocmfp2_reference.c new file mode 100644 index 000000000000..96201fc1e54a --- /dev/null +++ b/ggml/rocmfpx/test_rocmfp2_reference.c @@ -0,0 +1,565 @@ +#include "rocmfp2_reference.h" + +#include +#include +#include +#include + +static unsigned long long test_checks = 0; + +#define CHECK(condition) do { \ + ++test_checks; \ + if (!(condition)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + return false; \ + } \ +} while (0) + +#define CHECK_STATUS(expression, expected) do { \ + const rocmfp2_p1_status actual_status_ = (expression); \ + ++test_checks; \ + if (actual_status_ != (expected)) { \ + fprintf(stderr, "FAIL %s:%d: %s returned %s, expected %s\n", \ + __FILE__, __LINE__, #expression, rocmfp2_p1_status_name(actual_status_), \ + rocmfp2_p1_status_name(expected)); \ + return false; \ + } \ +} while (0) + +static int8_t expected_code_value( + uint8_t code, + rocmfp2_p1_codebook codebook, + rocmfp2_p1_mapping mapping) { + if (mapping == ROCMFP2_P1_MAPPING_MORD) { + const int values[4] = { + -(int) codebook.outer, + -(int) codebook.inner, + (int) codebook.inner, + (int) codebook.outer, + }; + return (int8_t) values[code]; + } + + const int values[4] = { + (int) codebook.inner, + (int) codebook.outer, + -(int) codebook.inner, + -(int) codebook.outer, + }; + return (int8_t) values[code]; +} + +static uint8_t expected_semantic_code(bool negative, bool outer, rocmfp2_p1_mapping mapping) { + if (mapping == ROCMFP2_P1_MAPPING_MORD) { + if (negative) { + return outer ? 0u : 1u; + } + return outer ? 3u : 2u; + } + + if (negative) { + return outer ? 3u : 2u; + } + return outer ? 1u : 0u; +} + +static bool test_layout_and_p0_packing(void) { + CHECK(sizeof(rocmfp2_p1_block) == 10); + CHECK(offsetof(rocmfp2_p1_block, d) == 0); + CHECK(offsetof(rocmfp2_p1_block, s) == 8); + + uint8_t codes[ROCMFP2_P1_BLOCK_WEIGHTS]; + uint8_t unpacked[ROCMFP2_P1_BLOCK_WEIGHTS]; + const uint8_t scales[2] = { 0x12, 0x7e }; + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + codes[i] = (uint8_t) (i & 3); + } + + rocmfp2_p1_block block; + memset(&block, 0xa5, sizeof(block)); + CHECK_STATUS(rocmfp2_p1_pack_codes(codes, scales, &block), ROCMFP2_P1_OK); + + const uint8_t expected_bytes[ROCMFP2_P1_BLOCK_BYTES] = { + 0xe4, 0xe4, 0xe4, 0xe4, 0xe4, 0xe4, 0xe4, 0xe4, 0x12, 0x7e, + }; + CHECK(memcmp(&block, expected_bytes, sizeof(expected_bytes)) == 0); + + rocmfp2_p1_unpack_codes(&block, unpacked); + CHECK(memcmp(codes, unpacked, sizeof(codes)) == 0); + + rocmfp2_p1_block unchanged; + rocmfp2_p1_block before; + memset(&unchanged, 0x5a, sizeof(unchanged)); + memcpy(&before, &unchanged, sizeof(before)); + codes[31] = 4; + CHECK_STATUS(rocmfp2_p1_pack_codes(codes, scales, &unchanged), ROCMFP2_P1_INVALID_CODE); + CHECK(memcmp(&unchanged, &before, sizeof(before)) == 0); + codes[31] = 3; + + const uint8_t invalid_scales[2] = { 0x7f, 0x00 }; + CHECK_STATUS(rocmfp2_p1_pack_codes(codes, invalid_scales, &unchanged), + ROCMFP2_P1_INVALID_SCALE_METADATA); + CHECK(memcmp(&unchanged, &before, sizeof(before)) == 0); + + puts("PASS layout_p0: block=10 data=8 scales=2 bpw=2.50 golden=e4e4e4e4e4e4e4e4127e"); + return true; +} + +static bool test_all_packed_bytes(void) { + const rocmfp2_p1_codebook codebook = { 3, 10 }; + + for (int mapping_index = 0; mapping_index < 2; ++mapping_index) { + const rocmfp2_p1_mapping mapping = (rocmfp2_p1_mapping) mapping_index; + for (int packed_value = 0; packed_value <= 0xff; ++packed_value) { + int8_t decoded[4]; + CHECK_STATUS(rocmfp2_p1_decode_packed_byte( + (uint8_t) packed_value, &codebook, mapping, decoded), ROCMFP2_P1_OK); + + for (int lane = 0; lane < 4; ++lane) { + const uint8_t code = (uint8_t) ((packed_value >> (2 * lane)) & 3); + CHECK(decoded[lane] == expected_code_value(code, codebook, mapping)); + + int8_t scalar = 0; + CHECK_STATUS(rocmfp2_p1_decode_code(code, &codebook, mapping, &scalar), ROCMFP2_P1_OK); + CHECK(scalar == decoded[lane]); + } + } + } + + int8_t value = 0; + CHECK_STATUS(rocmfp2_p1_decode_code(4, &codebook, ROCMFP2_P1_MAPPING_MORD, &value), + ROCMFP2_P1_INVALID_CODE); + + puts("PASS packed_byte_decode: mappings=2 bytes=256 lanes=2048"); + return true; +} + +static double expected_scale(uint8_t scale_byte) { + const int exponent = scale_byte >> 3; + const int mantissa = scale_byte & 7; + return exponent == 0 ? scalbn((double) mantissa, -10) : scalbn((double) (8 + mantissa), exponent - 11); +} + +static bool test_scale_encoding_and_boundaries(void) { + double previous = -1.0; + for (int scale_index = 0; scale_index <= 0xff; ++scale_index) { + const uint8_t scale_byte = (uint8_t) scale_index; + const bool valid = scale_index <= 0x7e; + CHECK(rocmfp2_p1_scale_is_valid(scale_byte) == valid); + + const double decoded = rocmfp2_p1_ue4m3_to_binary64(scale_byte); + if (valid) { + CHECK(decoded == expected_scale(scale_byte)); + CHECK(decoded > previous); + CHECK(rocmfp2_p1_nearest_ue4m3(decoded) == scale_byte); + previous = decoded; + } else { + CHECK(isnan(decoded)); + } + } + + CHECK(rocmfp2_p1_ue4m3_to_binary64(0x00) == 0.0); + CHECK(rocmfp2_p1_ue4m3_to_binary64(0x01) == 0x1p-10); + CHECK(rocmfp2_p1_ue4m3_to_binary64(0x07) == 7.0 * 0x1p-10); + CHECK(rocmfp2_p1_ue4m3_to_binary64(0x08) == 8.0 * 0x1p-10); + CHECK(rocmfp2_p1_ue4m3_to_binary64(0x7e) == 224.0); + + for (int upper_index = 1; upper_index <= 0x7e; ++upper_index) { + const uint8_t upper_byte = (uint8_t) upper_index; + const uint8_t lower_byte = (uint8_t) (upper_index - 1); + const double lower = expected_scale(lower_byte); + const double upper = expected_scale(upper_byte); + const double midpoint = (lower + upper) * 0.5; + + CHECK(rocmfp2_p1_nearest_ue4m3(midpoint) == lower_byte); + CHECK(rocmfp2_p1_nearest_ue4m3(nextafter(midpoint, -INFINITY)) == lower_byte); + CHECK(rocmfp2_p1_nearest_ue4m3(nextafter(midpoint, INFINITY)) == upper_byte); + } + + CHECK(rocmfp2_p1_nearest_ue4m3(-1.0) == 0xff); + CHECK(rocmfp2_p1_nearest_ue4m3(NAN) == 0xff); + CHECK(rocmfp2_p1_nearest_ue4m3(nextafter(224.0, INFINITY)) == 0x7e); + CHECK(rocmfp2_p1_nearest_ue4m3(INFINITY) == 0x7e); + + puts("PASS ue4m3_boundaries: legal=127 invalid=129 adjacent_midpoints=126 max=224"); + return true; +} + +static bool test_metadata_validation(void) { + rocmfp2_p1_block block; + memset(&block, 0, sizeof(block)); + + for (int first = 0; first <= 0x7e; ++first) { + for (int second = 0; second <= 0x7e; ++second) { + block.s[0] = (uint8_t) first; + block.s[1] = (uint8_t) second; + CHECK(rocmfp2_p1_validate_block(&block)); + } + } + + for (int invalid = 0x7f; invalid <= 0xff; ++invalid) { + block.s[0] = (uint8_t) invalid; + block.s[1] = 0; + CHECK(!rocmfp2_p1_validate_block(&block)); + block.s[0] = 0; + block.s[1] = (uint8_t) invalid; + CHECK(!rocmfp2_p1_validate_block(&block)); + } + + uint8_t serialized[2 * ROCMFP2_P1_BLOCK_BYTES]; + memset(serialized, 0, sizeof(serialized)); + serialized[8] = 0x7e; + serialized[9] = 0x00; + serialized[18] = 0x01; + serialized[19] = 0x7e; + CHECK(rocmfp2_p1_validate_serialized(serialized, sizeof(serialized))); + CHECK(rocmfp2_p1_validate_serialized(NULL, 0)); + CHECK(!rocmfp2_p1_validate_serialized(NULL, sizeof(rocmfp2_p1_block))); + + for (size_t length = 1; length < sizeof(serialized); ++length) { + if (length != ROCMFP2_P1_BLOCK_BYTES) { + CHECK(!rocmfp2_p1_validate_serialized(serialized, length)); + } + } + + for (int invalid = 0x7f; invalid <= 0xff; ++invalid) { + serialized[8] = (uint8_t) invalid; + CHECK(!rocmfp2_p1_validate_serialized(serialized, sizeof(serialized))); + serialized[8] = 0x00; + serialized[19] = (uint8_t) invalid; + CHECK(!rocmfp2_p1_validate_serialized(serialized, sizeof(serialized))); + serialized[19] = 0x7e; + } + + const rocmfp2_p1_codebook codebook = { 3, 10 }; + double output[ROCMFP2_P1_BLOCK_WEIGHTS]; + double before[ROCMFP2_P1_BLOCK_WEIGHTS]; + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + output[i] = 1234.0 + (double) i; + } + memcpy(before, output, sizeof(before)); + block.s[0] = 0x7f; + block.s[1] = 0; + CHECK_STATUS(rocmfp2_p1_dequantize_block( + &block, &codebook, ROCMFP2_P1_MAPPING_MORD, output), ROCMFP2_P1_INVALID_SCALE_METADATA); + CHECK(memcmp(output, before, sizeof(output)) == 0); + + puts("PASS metadata_validation: legal_pairs=16129 invalid_bytes=129 malformed_lengths=18"); + return true; +} + +static bool test_zero_midpoint_and_scale_ties(void) { + const rocmfp2_p1_codebook codebook = { 3, 10 }; + const uint8_t scale_byte = 0x31; + const double scale = expected_scale(scale_byte); + const double midpoint = ((double) codebook.inner + (double) codebook.outer) * scale * 0.5; + + for (int mapping_index = 0; mapping_index < 2; ++mapping_index) { + const rocmfp2_p1_mapping mapping = (rocmfp2_p1_mapping) mapping_index; + const double probes[] = { + 0.0, + -0.0, + midpoint, + -midpoint, + nextafter(midpoint, INFINITY), + nextafter(-midpoint, -INFINITY), + }; + const uint8_t expected[] = { + expected_semantic_code(false, false, mapping), + expected_semantic_code(true, false, mapping), + expected_semantic_code(false, false, mapping), + expected_semantic_code(true, false, mapping), + expected_semantic_code(false, true, mapping), + expected_semantic_code(true, true, mapping), + }; + + for (size_t i = 0; i < sizeof(probes) / sizeof(probes[0]); ++i) { + uint8_t reference_code = 0xff; + uint8_t optimized_code = 0xff; + CHECK_STATUS(rocmfp2_p1_select_code_ref( + probes[i], scale_byte, &codebook, mapping, &reference_code), ROCMFP2_P1_OK); + CHECK_STATUS(rocmfp2_p1_select_code_optimized( + probes[i], scale_byte, &codebook, mapping, &optimized_code), ROCMFP2_P1_OK); + CHECK(reference_code == expected[i]); + CHECK(optimized_code == expected[i]); + } + + uint8_t zero_scale_positive = 0xff; + uint8_t zero_scale_negative = 0xff; + CHECK_STATUS(rocmfp2_p1_select_code_ref( + 1.0, 0, &codebook, mapping, &zero_scale_positive), ROCMFP2_P1_OK); + CHECK_STATUS(rocmfp2_p1_select_code_optimized( + -1.0, 0, &codebook, mapping, &zero_scale_negative), ROCMFP2_P1_OK); + CHECK(zero_scale_positive == 0); + CHECK(zero_scale_negative == 0); + } + + const rocmfp2_p1_codebook tie_codebook = { 1, 2 }; + double source[ROCMFP2_P1_BLOCK_WEIGHTS]; + const double group0_value = 2.0 * expected_scale(0x20); + const double group1_value = 2.0 * expected_scale(0x21); + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + const double magnitude = i < ROCMFP2_P1_GROUP_WEIGHTS ? group0_value : group1_value; + source[i] = (i & 1) ? -magnitude : magnitude; + } + + for (int mapping_index = 0; mapping_index < 2; ++mapping_index) { + const rocmfp2_p1_mapping mapping = (rocmfp2_p1_mapping) mapping_index; + rocmfp2_p1_block reference; + rocmfp2_p1_block optimized; + double reference_sse[2]; + double optimized_sse[2]; + CHECK_STATUS(rocmfp2_p1_quantize_block_ref( + source, &tie_codebook, mapping, &reference, reference_sse), ROCMFP2_P1_OK); + CHECK_STATUS(rocmfp2_p1_quantize_block_optimized( + source, &tie_codebook, mapping, &optimized, optimized_sse), ROCMFP2_P1_OK); + CHECK(memcmp(&reference, &optimized, sizeof(reference)) == 0); + CHECK(reference.s[0] == 0x20); + CHECK(reference.s[1] == 0x21); + CHECK(reference_sse[0] == 0.0 && reference_sse[1] == 0.0); + CHECK(optimized_sse[0] == 0.0 && optimized_sse[1] == 0.0); + } + + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + source[i] = (i & 1) ? -0.0 : 0.0; + } + for (int mapping_index = 0; mapping_index < 2; ++mapping_index) { + const rocmfp2_p1_mapping mapping = (rocmfp2_p1_mapping) mapping_index; + rocmfp2_p1_block reference; + rocmfp2_p1_block optimized; + uint8_t codes[ROCMFP2_P1_BLOCK_WEIGHTS]; + CHECK_STATUS(rocmfp2_p1_quantize_block_ref( + source, &codebook, mapping, &reference, NULL), ROCMFP2_P1_OK); + CHECK_STATUS(rocmfp2_p1_quantize_block_optimized( + source, &codebook, mapping, &optimized, NULL), ROCMFP2_P1_OK); + CHECK(memcmp(&reference, &optimized, sizeof(reference)) == 0); + CHECK(reference.s[0] == 0 && reference.s[1] == 0); + rocmfp2_p1_unpack_codes(&reference, codes); + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + CHECK(codes[i] == 0); + } + const uint8_t canonical_zero[ROCMFP2_P1_BLOCK_BYTES] = { 0 }; + CHECK(memcmp(&reference, canonical_zero, sizeof(canonical_zero)) == 0); + } + + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + source[i] = (i & 1) ? -10000.0 : 10000.0; + } + rocmfp2_p1_block saturated; + CHECK_STATUS(rocmfp2_p1_quantize_block_ref( + source, &codebook, ROCMFP2_P1_MAPPING_MORD, &saturated, NULL), ROCMFP2_P1_OK); + CHECK(saturated.s[0] == 0x7e && saturated.s[1] == 0x7e); + + puts("PASS deterministic_ties: signed_zero=2 magnitude_midpoint=4 scale_tuple=2 saturation=0x7e"); + return true; +} + +static bool test_python_golden_vectors(void) { + const rocmfp2_p1_codebook codebook = { 3, 10 }; + double source[ROCMFP2_P1_BLOCK_WEIGHTS]; + static const double values[4] = { -10.0, -3.0, 3.0, 10.0 }; + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + source[i] = values[i & 3] * (i < ROCMFP2_P1_GROUP_WEIGHTS ? 1.0 : 2.0); + } + + static const uint8_t expected[2][ROCMFP2_P1_BLOCK_BYTES] = { + { 0xe4, 0xe4, 0xe4, 0xe4, 0xe4, 0xe4, 0xe4, 0xe4, 0x40, 0x48 }, + { 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x40, 0x48 }, + }; + + for (int mapping_index = 0; mapping_index < 2; ++mapping_index) { + const rocmfp2_p1_mapping mapping = (rocmfp2_p1_mapping) mapping_index; + rocmfp2_p1_block reference; + rocmfp2_p1_block optimized; + double output[ROCMFP2_P1_BLOCK_WEIGHTS]; + CHECK_STATUS(rocmfp2_p1_quantize_block_ref( + source, &codebook, mapping, &reference, NULL), ROCMFP2_P1_OK); + CHECK_STATUS(rocmfp2_p1_quantize_block_optimized( + source, &codebook, mapping, &optimized, NULL), ROCMFP2_P1_OK); + CHECK(memcmp(&reference, expected[mapping_index], sizeof(reference)) == 0); + CHECK(memcmp(&optimized, expected[mapping_index], sizeof(optimized)) == 0); + CHECK_STATUS(rocmfp2_p1_dequantize_block( + &reference, &codebook, mapping, output), ROCMFP2_P1_OK); + CHECK(memcmp(output, source, sizeof(source)) == 0); + } + + puts("PASS python_golden_vectors: vectors=2 mord=e4x8+4048 msm=4bx8+4048"); + return true; +} + +static uint64_t splitmix64_next(uint64_t * state) { + uint64_t value = (*state += UINT64_C(0x9e3779b97f4a7c15)); + value = (value ^ (value >> 30)) * UINT64_C(0xbf58476d1ce4e5b9); + value = (value ^ (value >> 27)) * UINT64_C(0x94d049bb133111eb); + return value ^ (value >> 31); +} + +static uint64_t fnv1a64(uint64_t hash, const void * data, size_t size) { + const uint8_t * bytes = (const uint8_t *) data; + for (size_t i = 0; i < size; ++i) { + hash ^= bytes[i]; + hash *= UINT64_C(1099511628211); + } + return hash; +} + +static void fill_fixed_random_block(double source[ROCMFP2_P1_BLOCK_WEIGHTS], uint64_t * state, int block_index) { + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + const uint64_t bits = splitmix64_next(state); + const double unit = (double) (bits >> 11) * 0x1p-53; + const int exponent = (int) ((bits >> 3) % 18) - 10; + source[i] = scalbn(2.0 * unit - 1.0, exponent); + } + + source[(block_index * 7) & 31] = (block_index & 1) ? -0.0 : 0.0; + source[(block_index * 11 + 3) & 31] *= 13.0; +} + +static bool test_fixed_random_reference_optimized_match(void) { + static const rocmfp2_p1_codebook codebooks[8] = { + { 1, 2 }, { 2, 5 }, { 1, 3 }, { 3, 10 }, + { 2, 7 }, { 1, 4 }, { 3, 13 }, { 1, 5 }, + }; + enum { BLOCKS_PER_CODEBOOK = 24 }; + + uint64_t random_state = UINT64_C(0x726f636d66703231); + uint64_t fingerprint = UINT64_C(1469598103934665603); + + for (int codebook_index = 0; codebook_index < 8; ++codebook_index) { + const rocmfp2_p1_codebook * codebook = &codebooks[codebook_index]; + for (int block_index = 0; block_index < BLOCKS_PER_CODEBOOK; ++block_index) { + double source[ROCMFP2_P1_BLOCK_WEIGHTS]; + rocmfp2_p1_block mapping_blocks[2]; + double mapping_outputs[2][ROCMFP2_P1_BLOCK_WEIGHTS]; + double mapping_sse[2][2]; + fill_fixed_random_block(source, &random_state, block_index); + + for (int mapping_index = 0; mapping_index < 2; ++mapping_index) { + const rocmfp2_p1_mapping mapping = (rocmfp2_p1_mapping) mapping_index; + rocmfp2_p1_block reference; + rocmfp2_p1_block optimized; + rocmfp2_p1_block reference_repeat; + rocmfp2_p1_block optimized_repeat; + double reference_sse[2]; + double optimized_sse[2]; + double repeat_sse[2]; + + CHECK_STATUS(rocmfp2_p1_quantize_block_ref( + source, codebook, mapping, &reference, reference_sse), ROCMFP2_P1_OK); + CHECK_STATUS(rocmfp2_p1_quantize_block_optimized( + source, codebook, mapping, &optimized, optimized_sse), ROCMFP2_P1_OK); + CHECK(memcmp(&reference, &optimized, sizeof(reference)) == 0); + CHECK(reference_sse[0] == optimized_sse[0]); + CHECK(reference_sse[1] == optimized_sse[1]); + + CHECK_STATUS(rocmfp2_p1_quantize_block_ref( + source, codebook, mapping, &reference_repeat, repeat_sse), ROCMFP2_P1_OK); + CHECK(memcmp(&reference, &reference_repeat, sizeof(reference)) == 0); + CHECK(reference_sse[0] == repeat_sse[0] && reference_sse[1] == repeat_sse[1]); + + CHECK_STATUS(rocmfp2_p1_quantize_block_optimized( + source, codebook, mapping, &optimized_repeat, repeat_sse), ROCMFP2_P1_OK); + CHECK(memcmp(&optimized, &optimized_repeat, sizeof(optimized)) == 0); + CHECK(optimized_sse[0] == repeat_sse[0] && optimized_sse[1] == repeat_sse[1]); + CHECK(rocmfp2_p1_validate_block(&reference)); + + CHECK_STATUS(rocmfp2_p1_dequantize_block( + &reference, codebook, mapping, mapping_outputs[mapping_index]), ROCMFP2_P1_OK); + for (int group = 0; group < 2; ++group) { + double recomputed_sse = 0.0; + for (int i = 0; i < ROCMFP2_P1_GROUP_WEIGHTS; ++i) { + const int index = group * ROCMFP2_P1_GROUP_WEIGHTS + i; + const double delta = source[index] - mapping_outputs[mapping_index][index]; + recomputed_sse += delta * delta; + } + CHECK(recomputed_sse == reference_sse[group]); + mapping_sse[mapping_index][group] = reference_sse[group]; + } + + memcpy(&mapping_blocks[mapping_index], &reference, sizeof(reference)); + fingerprint = fnv1a64(fingerprint, &reference, sizeof(reference)); + } + + CHECK(mapping_blocks[0].s[0] == mapping_blocks[1].s[0]); + CHECK(mapping_blocks[0].s[1] == mapping_blocks[1].s[1]); + CHECK(mapping_sse[0][0] == mapping_sse[1][0]); + CHECK(mapping_sse[0][1] == mapping_sse[1][1]); + CHECK(memcmp(mapping_outputs[0], mapping_outputs[1], sizeof(mapping_outputs[0])) == 0); + } + } + + printf("PASS fixed_random: codebooks=8 blocks_each=%d mappings=2 ref_opt_equal=384 fingerprint=%016" PRIx64 "\n", + BLOCKS_PER_CODEBOOK, fingerprint); + return true; +} + +static bool test_nonfinite_rejection_and_output_atomicity(void) { + const rocmfp2_p1_codebook codebook = { 3, 10 }; + double source[ROCMFP2_P1_BLOCK_WEIGHTS]; + for (int i = 0; i < ROCMFP2_P1_BLOCK_WEIGHTS; ++i) { + source[i] = (double) i * 0.125 - 2.0; + } + + const double nonfinite_values[3] = { NAN, INFINITY, -INFINITY }; + for (int mapping_index = 0; mapping_index < 2; ++mapping_index) { + const rocmfp2_p1_mapping mapping = (rocmfp2_p1_mapping) mapping_index; + for (int position = 0; position < ROCMFP2_P1_BLOCK_WEIGHTS; ++position) { + const double saved = source[position]; + for (int variant = 0; variant < 3; ++variant) { + rocmfp2_p1_block reference; + rocmfp2_p1_block optimized; + rocmfp2_p1_block before_reference; + rocmfp2_p1_block before_optimized; + double reference_sse[2] = { 123.0, 456.0 }; + double optimized_sse[2] = { 789.0, 987.0 }; + const double reference_sse_before[2] = { 123.0, 456.0 }; + const double optimized_sse_before[2] = { 789.0, 987.0 }; + + source[position] = nonfinite_values[variant]; + memset(&reference, 0x3c, sizeof(reference)); + memset(&optimized, 0xc3, sizeof(optimized)); + memcpy(&before_reference, &reference, sizeof(reference)); + memcpy(&before_optimized, &optimized, sizeof(optimized)); + + CHECK_STATUS(rocmfp2_p1_quantize_block_ref( + source, &codebook, mapping, &reference, reference_sse), + ROCMFP2_P1_NONFINITE_SOURCE); + CHECK_STATUS(rocmfp2_p1_quantize_block_optimized( + source, &codebook, mapping, &optimized, optimized_sse), + ROCMFP2_P1_NONFINITE_SOURCE); + CHECK(memcmp(&reference, &before_reference, sizeof(reference)) == 0); + CHECK(memcmp(&optimized, &before_optimized, sizeof(optimized)) == 0); + CHECK(memcmp(reference_sse, reference_sse_before, sizeof(reference_sse)) == 0); + CHECK(memcmp(optimized_sse, optimized_sse_before, sizeof(optimized_sse)) == 0); + } + source[position] = saved; + } + } + + uint8_t code = 0; + CHECK_STATUS(rocmfp2_p1_select_code_ref( + NAN, 0x10, &codebook, ROCMFP2_P1_MAPPING_MORD, &code), ROCMFP2_P1_NONFINITE_SOURCE); + CHECK_STATUS(rocmfp2_p1_select_code_optimized( + INFINITY, 0x10, &codebook, ROCMFP2_P1_MAPPING_MSM, &code), ROCMFP2_P1_NONFINITE_SOURCE); + + puts("PASS nonfinite_rejection: positions=32 values=3 mappings=2 paths=2 status=NONFINITE_SOURCE"); + return true; +} + +int main(void) { + if (!test_layout_and_p0_packing() || + !test_all_packed_bytes() || + !test_scale_encoding_and_boundaries() || + !test_metadata_validation() || + !test_zero_midpoint_and_scale_ties() || + !test_python_golden_vectors() || + !test_fixed_random_reference_optimized_match() || + !test_nonfinite_rejection_and_output_atomicity()) { + fprintf(stderr, "ROCmFP2 Phase-1 reference tests FAILED after %llu checks\n", test_checks); + return 1; + } + + printf("PASS rocmfp2_phase1_reference: checks=%llu arithmetic=IEEE754-binary64 search=127-scales exhaustive\n", + test_checks); + return 0; +} diff --git a/ggml/rocmfpx/test_rocmfpx.c b/ggml/rocmfpx/test_rocmfpx.c new file mode 100644 index 000000000000..daf58ccc3da7 --- /dev/null +++ b/ggml/rocmfpx/test_rocmfpx.c @@ -0,0 +1,177 @@ +#include "rocmfpx.h" + +#include +#include +#include + +static void fill_row(float * x, int n) { + for (int i = 0; i < n; ++i) { + const float wave = 0.75f*sinf((float) i * 0.37f) + 0.25f*cosf((float) i * 0.13f); + const float ramp = ((float) (i % 11) - 5.0f) * 0.035f; + x[i] = wave + ramp; + } + + x[7] = 3.25f; + x[19] = -2.75f; + x[43] = 1.875f; +} + +static float mse(const float * a, const float * b, int n) { + float err = 0.0f; + + for (int i = 0; i < n; ++i) { + const float d = a[i] - b[i]; + err += d*d; + } + + return err / (float) n; +} + +static float weighted_mse(const float * a, const float * b, const float * w, int n) { + float err = 0.0f; + float sum_w = 0.0f; + + for (int i = 0; i < n; ++i) { + const float d = a[i] - b[i]; + err += w[i]*d*d; + sum_w += w[i]; + } + + return sum_w > 0.0f ? err / sum_w : 0.0f; +} + +static void fill_imatrix_case(float * src, float * imatrix, int n, float base, float outlier) { + for (int i = 0; i < n; ++i) { + const float sign = (i % 2) ? 1.0f : -1.0f; + src[i] = sign * base * (1.0f + 0.03f*(float)(i % 5)); + imatrix[i] = 100.0f; + } + + src[0] = outlier; + imatrix[0] = 0.0f; +} + +static void check_weighted_imatrix_fp3(void) { + enum { N = QK_ROCMFP3 }; + + float src[N]; + float imatrix[N]; + float plain[N]; + float weighted[N]; + block_rocmfp3 q_plain[N / QK_ROCMFP3]; + block_rocmfp3 q_weighted[N / QK_ROCMFP3]; + + fill_imatrix_case(src, imatrix, N, 0.21f, 9.0f); + + rocmfpx_quantize_fp3(src, q_plain, 1, N, NULL); + rocmfpx_quantize_fp3(src, q_weighted, 1, N, imatrix); + rocmfpx_dequantize_row_fp3(q_plain, plain, N); + rocmfpx_dequantize_row_fp3(q_weighted, weighted, N); + + const float plain_err = weighted_mse(src, plain, imatrix, N); + const float weighted_err = weighted_mse(src, weighted, imatrix, N); + + printf("ROCmFP3 imatrix weighted_mse: plain=%g weighted=%g\n", plain_err, weighted_err); + assert(weighted_err < plain_err); +} + +static void check_weighted_imatrix_fp6(void) { + enum { N = QK_ROCMFP6 }; + + float src[N]; + float imatrix[N]; + float plain[N]; + float weighted[N]; + block_rocmfp6 q_plain[N / QK_ROCMFP6]; + block_rocmfp6 q_weighted[N / QK_ROCMFP6]; + + fill_imatrix_case(src, imatrix, N, 0.045f, 6.0f); + + rocmfpx_quantize_fp6(src, q_plain, 1, N, NULL); + rocmfpx_quantize_fp6(src, q_weighted, 1, N, imatrix); + rocmfpx_dequantize_row_fp6(q_plain, plain, N); + rocmfpx_dequantize_row_fp6(q_weighted, weighted, N); + + const float plain_err = weighted_mse(src, plain, imatrix, N); + const float weighted_err = weighted_mse(src, weighted, imatrix, N); + + printf("ROCmFP6 imatrix weighted_mse: plain=%g weighted=%g\n", plain_err, weighted_err); + assert(weighted_err < plain_err); +} + +static void check_weighted_imatrix_fp8(void) { + enum { N = QK_ROCMFP8 }; + + float src[N]; + float imatrix[N]; + float plain[N]; + float weighted[N]; + block_rocmfp8 q_plain[N / QK_ROCMFP8]; + block_rocmfp8 q_weighted[N / QK_ROCMFP8]; + + fill_imatrix_case(src, imatrix, N, 0.008f, 8.0f); + + rocmfpx_quantize_fp8(src, q_plain, 1, N, NULL); + rocmfpx_quantize_fp8(src, q_weighted, 1, N, imatrix); + rocmfpx_dequantize_row_fp8(q_plain, plain, N); + rocmfpx_dequantize_row_fp8(q_weighted, weighted, N); + + const float plain_err = weighted_mse(src, plain, imatrix, N); + const float weighted_err = weighted_mse(src, weighted, imatrix, N); + + printf("ROCmFP8 imatrix weighted_mse: plain=%g weighted=%g\n", plain_err, weighted_err); + assert(weighted_err < plain_err); +} + +int main(void) { + enum { N = 64 }; + + float src[N]; + float fp3[N]; + float fp6[N]; + float fp8[N]; + + block_rocmfp3 q3[N / QK_ROCMFP3]; + block_rocmfp6 q6[N / QK_ROCMFP6]; + block_rocmfp8 q8[N / QK_ROCMFP8]; + + fill_row(src, N); + + rocmfpx_quantize_row_fp3_ref(src, q3, N); + rocmfpx_quantize_row_fp6_ref(src, q6, N); + rocmfpx_quantize_row_fp8_ref(src, q8, N); + + assert(rocmfpx_validate_row_data_fp3(q3, sizeof(q3))); + assert(rocmfpx_validate_row_data_fp6(q6, sizeof(q6))); + assert(rocmfpx_validate_row_data_fp8(q8, sizeof(q8))); + + rocmfpx_dequantize_row_fp3(q3, fp3, N); + rocmfpx_dequantize_row_fp6(q6, fp6, N); + rocmfpx_dequantize_row_fp8(q8, fp8, N); + + const float mse3 = mse(src, fp3, N); + const float mse6 = mse(src, fp6, N); + const float mse8 = mse(src, fp8, N); + + printf("ROCmFP3: block=%zu row=%zu bpw=%.2f mse=%g\n", + sizeof(block_rocmfp3), rocmfpx_row_size_fp3(N), + 8.0f*(float) sizeof(block_rocmfp3)/(float) QK_ROCMFP3, mse3); + printf("ROCmFP6: block=%zu row=%zu bpw=%.2f mse=%g\n", + sizeof(block_rocmfp6), rocmfpx_row_size_fp6(N), + 8.0f*(float) sizeof(block_rocmfp6)/(float) QK_ROCMFP6, mse6); + printf("ROCmFP8: block=%zu row=%zu bpw=%.2f mse=%g\n", + sizeof(block_rocmfp8), rocmfpx_row_size_fp8(N), + 8.0f*(float) sizeof(block_rocmfp8)/(float) QK_ROCMFP8, mse8); + + assert(isfinite(mse3)); + assert(isfinite(mse6)); + assert(isfinite(mse8)); + assert(mse8 < mse6); + assert(mse6 < mse3); + + check_weighted_imatrix_fp3(); + check_weighted_imatrix_fp6(); + check_weighted_imatrix_fp8(); + + return 0; +} diff --git a/ggml/src/CMakeLists.txt b/ggml/src/CMakeLists.txt index 947732000219..6633c01ced1e 100644 --- a/ggml/src/CMakeLists.txt +++ b/ggml/src/CMakeLists.txt @@ -206,6 +206,10 @@ add_library(ggml-base ggml-threading.h ggml-quants.c ggml-quants.h + ../rocmfp4/rocmfp4.c + ../rocmfp4/rocmfp4.h + ../rocmfpx/rocmfpx.c + ../rocmfpx/rocmfpx.h gguf.cpp) set_target_properties(ggml-base PROPERTIES diff --git a/ggml/src/ggml-common.h b/ggml/src/ggml-common.h index 1dbbe326d0fc..7a94403a3256 100644 --- a/ggml/src/ggml-common.h +++ b/ggml/src/ggml-common.h @@ -1128,6 +1128,14 @@ GGML_TABLE_BEGIN(int8_t, kvalues_fp4, 16) GGML_TABLE_END() #define kvalues_mxfp4 kvalues_fp4 +// ROCmFP4 uses an E2M1-derived value set with the largest level retuned from +// 12 to 10, plus dual half-block UE4M3 scales. Keeping this separate from +// MXFP4/NVFP4 lets the experimental Strix Halo format evolve without changing +// stock MXFP4/NVFP4 behavior. +GGML_TABLE_BEGIN(int8_t, kvalues_rocmfp4, 16) + 0, 1, 2, 3, 4, 6, 8, 10, 0, -1, -2, -3, -4, -6, -8, -10, +GGML_TABLE_END() + #define NGRID_IQ1S 2048 #define IQ1S_DELTA 0.125f #define IQ1M_DELTA 0.125f diff --git a/ggml/src/ggml-cpu/ggml-cpu.c b/ggml/src/ggml-cpu/ggml-cpu.c index 87a329f26975..b1a2e8d05ca3 100644 --- a/ggml/src/ggml-cpu/ggml-cpu.c +++ b/ggml/src/ggml-cpu/ggml-cpu.c @@ -16,6 +16,10 @@ #include "ggml.h" #include "common.h" +// ROCmFPx experimental AMD-native quant formats +#include "../../rocmfp4/rocmfp4.h" +#include "../../rocmfpx/rocmfpx.h" + #if defined(_MSC_VER) || defined(__MINGW32__) #include // using malloc.h with MSC/MINGW #elif !defined(__FreeBSD__) && !defined(__NetBSD__) && !defined(__OpenBSD__) @@ -212,6 +216,166 @@ typedef pthread_t ggml_thread_t; #include #endif +static inline uint32_t ggml_rocmfpx_get_bits_cpu(const uint8_t * src, int bit_pos, int nbits) { + const int byte_pos = bit_pos >> 3; + const int shift = bit_pos & 7; + uint32_t v = src[byte_pos]; + + v |= (uint32_t) src[byte_pos + 1] << 8; + v |= (uint32_t) src[byte_pos + 2] << 16; + + return (v >> shift) & ((1u << nbits) - 1u); +} + +static inline int ggml_rocmfpx_decode_fp3_cpu(uint32_t code) { + static const int8_t table[8] = { 0, 1, 2, 4, 0, -1, -2, -4 }; + return table[code & 7u]; +} + +static inline int ggml_rocmfpx_decode_fp2_cpu(uint32_t code) { + static const int8_t table[4] = { -4, -1, 1, 4 }; + return table[code & 3u]; +} + +static void ggml_vec_dot_rocmfpx_fp2_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + GGML_UNUSED(bs); + GGML_UNUSED(bx); + GGML_UNUSED(by); + assert(nrc == 1); + GGML_UNUSED(nrc); + assert(n % QK_ROCMFP2 == 0); + assert(QK_ROCMFP2 == QK8_0); + + const block_rocmfp2 * GGML_RESTRICT x = (const block_rocmfp2 *) vx; + const block_q8_0 * GGML_RESTRICT y = (const block_q8_0 *) vy; + const int nb = n / QK_ROCMFP2; + float sumf = 0.0f; + + for (int ib = 0; ib < nb; ++ib) { + const float dy = GGML_CPU_FP16_TO_FP32(y[ib].d); + int sumi0 = 0; + int sumi1 = 0; + for (int j = 0; j < QK_ROCMFP2/2; ++j) { + const uint8_t c0 = (x[ib].qs[j/4] >> (2*(j % 4))) & 3u; + const uint8_t c1 = (x[ib].qs[4 + j/4] >> (2*(j % 4))) & 3u; + sumi0 += ggml_rocmfpx_decode_fp2_cpu(c0) * (int) y[ib].qs[j]; + sumi1 += ggml_rocmfpx_decode_fp2_cpu(c1) * (int) y[ib].qs[j + QK_ROCMFP2/2]; + } + sumf += dy * ( + rocmfpx_ue4m3_to_fp32(x[ib].e[0]) * (float) sumi0 + + rocmfpx_ue4m3_to_fp32(x[ib].e[1]) * (float) sumi1); + } + *s = sumf; +} + +static inline int ggml_rocmfpx_decode_fp6_cpu(uint32_t code) { + // asymmetric signed range [-32, 31]: sign|0 encodes -32, matching + // rocmfpx_decode_fp6_code() in ggml/rocmfpx/rocmfpx.c + const int mag = (int) (code & 31u); + return (code & 32u) ? -(mag == 0 ? 32 : mag) : mag; +} + +static void ggml_vec_dot_rocmfpx_fp3_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + GGML_UNUSED(bs); + GGML_UNUSED(bx); + GGML_UNUSED(by); + assert(nrc == 1); + GGML_UNUSED(nrc); + assert(n % QK_ROCMFP3 == 0); + assert(QK_ROCMFP3 == QK8_0); + + const block_rocmfp3 * GGML_RESTRICT x = (const block_rocmfp3 *) vx; + const block_q8_0 * GGML_RESTRICT y = (const block_q8_0 *) vy; + + const int nb = n / QK_ROCMFP3; + float sumf = 0.0f; + + for (int ib = 0; ib < nb; ++ib) { + const float dy = GGML_CPU_FP16_TO_FP32(y[ib].d); + + int sumi0 = 0; + int sumi1 = 0; + for (int j = 0; j < QK_ROCMFP3/2; ++j) { + const int q0 = ggml_rocmfpx_decode_fp3_cpu(ggml_rocmfpx_get_bits_cpu(x[ib].qs, j*3, 3)); + const int q1 = ggml_rocmfpx_decode_fp3_cpu(ggml_rocmfpx_get_bits_cpu(x[ib].qs, (j + QK_ROCMFP3/2)*3, 3)); + sumi0 += q0 * (int) y[ib].qs[j]; + sumi1 += q1 * (int) y[ib].qs[j + QK_ROCMFP3/2]; + } + + sumf += dy * ( + rocmfpx_ue4m3_to_fp32(x[ib].e[0]) * (float) sumi0 + + rocmfpx_ue4m3_to_fp32(x[ib].e[1]) * (float) sumi1); + } + + *s = sumf; +} + +static void ggml_vec_dot_rocmfpx_fp6_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + GGML_UNUSED(bs); + GGML_UNUSED(bx); + GGML_UNUSED(by); + assert(nrc == 1); + GGML_UNUSED(nrc); + assert(n % QK_ROCMFP6 == 0); + assert(QK_ROCMFP6 == QK8_0); + + const block_rocmfp6 * GGML_RESTRICT x = (const block_rocmfp6 *) vx; + const block_q8_0 * GGML_RESTRICT y = (const block_q8_0 *) vy; + + const int nb = n / QK_ROCMFP6; + float sumf = 0.0f; + + for (int ib = 0; ib < nb; ++ib) { + const float dy = GGML_CPU_FP16_TO_FP32(y[ib].d); + + int sumi0 = 0; + int sumi1 = 0; + for (int j = 0; j < QK_ROCMFP6/2; ++j) { + const int q0 = ggml_rocmfpx_decode_fp6_cpu(ggml_rocmfpx_get_bits_cpu(x[ib].qs, j*6, 6)); + const int q1 = ggml_rocmfpx_decode_fp6_cpu(ggml_rocmfpx_get_bits_cpu(x[ib].qs, (j + QK_ROCMFP6/2)*6, 6)); + sumi0 += q0 * (int) y[ib].qs[j]; + sumi1 += q1 * (int) y[ib].qs[j + QK_ROCMFP6/2]; + } + + sumf += dy * ( + rocmfpx_ue4m3_to_fp32(x[ib].e[0]) * (float) sumi0 + + rocmfpx_ue4m3_to_fp32(x[ib].e[1]) * (float) sumi1); + } + + *s = sumf; +} + +static void ggml_vec_dot_rocmfpx_fp8_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) { + GGML_UNUSED(bs); + GGML_UNUSED(bx); + GGML_UNUSED(by); + assert(nrc == 1); + GGML_UNUSED(nrc); + assert(n % QK_ROCMFP8 == 0); + assert(QK_ROCMFP8 == QK8_0); + + const block_rocmfp8 * GGML_RESTRICT x = (const block_rocmfp8 *) vx; + const block_q8_0 * GGML_RESTRICT y = (const block_q8_0 *) vy; + + const int nb = n / QK_ROCMFP8; + float sumf = 0.0f; + + for (int ib = 0; ib < nb; ++ib) { + const float dx = rocmfpx_ue4m3_to_fp32(x[ib].e); + const float dy = GGML_CPU_FP16_TO_FP32(y[ib].d); + const float d = dx * dy; + + int sumi = 0; + for (int j = 0; j < QK_ROCMFP8; ++j) { + sumi += (int) x[ib].qs[j] * (int) y[ib].qs[j]; + } + + sumf += d * (float) sumi; + } + + *s = sumf; +} + static const struct ggml_type_traits_cpu type_traits_cpu[GGML_TYPE_COUNT] = { [GGML_TYPE_F32] = { .from_float = (ggml_from_float_t) ggml_cpu_fp32_to_fp32, @@ -247,6 +411,42 @@ static const struct ggml_type_traits_cpu type_traits_cpu[GGML_TYPE_COUNT] = { .nrows = 1, #endif }, + [GGML_TYPE_Q4_0_ROCMFP4] = { + .from_float = rocmfp4_quantize_row_q4_0, + .vec_dot = rocmfp4_vec_dot_q4_0_q8_0, + .vec_dot_type = GGML_TYPE_Q8_0, + .nrows = 1, + }, + [GGML_TYPE_Q4_0_ROCMFP4_FAST] = { + .from_float = rocmfp4_quantize_row_q4_0_fast, + .vec_dot = rocmfp4_vec_dot_q4_0_fast_q8_0, + .vec_dot_type = GGML_TYPE_Q8_0, + .nrows = 1, + }, + [GGML_TYPE_Q3_0_ROCMFPX] = { + .from_float = rocmfpx_quantize_row_fp3, + .vec_dot = ggml_vec_dot_rocmfpx_fp3_q8_0, + .vec_dot_type = GGML_TYPE_Q8_0, + .nrows = 1, + }, + [GGML_TYPE_Q2_0_ROCMFPX] = { + .from_float = rocmfpx_quantize_row_fp2, + .vec_dot = ggml_vec_dot_rocmfpx_fp2_q8_0, + .vec_dot_type = GGML_TYPE_Q8_0, + .nrows = 1, + }, + [GGML_TYPE_Q6_0_ROCMFPX] = { + .from_float = rocmfpx_quantize_row_fp6, + .vec_dot = ggml_vec_dot_rocmfpx_fp6_q8_0, + .vec_dot_type = GGML_TYPE_Q8_0, + .nrows = 1, + }, + [GGML_TYPE_Q8_0_ROCMFPX] = { + .from_float = rocmfpx_quantize_row_fp8, + .vec_dot = ggml_vec_dot_rocmfpx_fp8_q8_0, + .vec_dot_type = GGML_TYPE_Q8_0, + .nrows = 1, + }, [GGML_TYPE_Q4_1] = { .from_float = quantize_row_q4_1, .vec_dot = ggml_vec_dot_q4_1_q8_1, diff --git a/ggml/src/ggml-cpu/ops.cpp b/ggml/src/ggml-cpu/ops.cpp index 266261c5e5a4..66e3659f943c 100644 --- a/ggml/src/ggml-cpu/ops.cpp +++ b/ggml/src/ggml-cpu/ops.cpp @@ -673,6 +673,12 @@ void ggml_compute_forward_add( case GGML_TYPE_Q8_0: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q2_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -1125,6 +1131,12 @@ void ggml_compute_forward_add1( case GGML_TYPE_Q8_1: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q2_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -1256,6 +1268,12 @@ void ggml_compute_forward_acc( case GGML_TYPE_Q8_1: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q2_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -4658,6 +4676,12 @@ void ggml_compute_forward_out_prod( case GGML_TYPE_Q8_0: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q2_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -4935,6 +4959,12 @@ void ggml_compute_forward_set( case GGML_TYPE_Q8_1: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q2_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -5160,6 +5190,12 @@ void ggml_compute_forward_get_rows( case GGML_TYPE_Q8_1: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q2_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -5917,6 +5953,12 @@ void ggml_compute_forward_clamp( case GGML_TYPE_Q8_1: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q2_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: diff --git a/ggml/src/ggml-quants.c b/ggml/src/ggml-quants.c index 1ebc50a763f1..80cdb3050db3 100644 --- a/ggml/src/ggml-quants.c +++ b/ggml/src/ggml-quants.c @@ -2,6 +2,8 @@ #include "ggml-common.h" #include "ggml-quants.h" +#include "../rocmfp4/rocmfp4.h" +#include "../rocmfpx/rocmfpx.h" #include "ggml-impl.h" #include "ggml-cpu/ggml-cpu-impl.h" #include "ggml-cpu.h" @@ -5541,6 +5543,48 @@ bool ggml_validate_row_data(enum ggml_type type, const void * data, size_t nbyte { VALIDATE_ROW_DATA_D_F16_IMPL(block_q4_0, data, nb); } break; + case GGML_TYPE_Q4_0_ROCMFP4: + { + if (!rocmfp4_validate_row_data(data, nbytes)) { + fprintf(stderr, "%s: invalid ROCmFP4 row data\n", __func__); + return false; + } + } break; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + { + if (!rocmfp4_validate_row_data_fast(data, nbytes)) { + fprintf(stderr, "%s: invalid ROCmFP4 fast row data\n", __func__); + return false; + } + } break; + case GGML_TYPE_Q3_0_ROCMFPX: + { + if (!rocmfpx_validate_row_data_fp3(data, nbytes)) { + fprintf(stderr, "%s: invalid ROCmFPx FP3 row data\n", __func__); + return false; + } + } break; + case GGML_TYPE_Q2_0_ROCMFPX: + { + if (!rocmfpx_validate_row_data_fp2(data, nbytes)) { + fprintf(stderr, "%s: invalid ROCmFPx FP2 row data\n", __func__); + return false; + } + } break; + case GGML_TYPE_Q6_0_ROCMFPX: + { + if (!rocmfpx_validate_row_data_fp6(data, nbytes)) { + fprintf(stderr, "%s: invalid ROCmFPx FP6 row data\n", __func__); + return false; + } + } break; + case GGML_TYPE_Q8_0_ROCMFPX: + { + if (!rocmfpx_validate_row_data_fp8(data, nbytes)) { + fprintf(stderr, "%s: invalid ROCmFPx FP8 row data\n", __func__); + return false; + } + } break; case GGML_TYPE_Q4_1: { VALIDATE_ROW_DATA_DM_F16_IMPL(block_q4_1, data, nb, d, m); diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index ce66a10d6458..1e0c622f5026 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4081,6 +4081,17 @@ static bool ggml_vk_matmul_shmem_support(const vk_device& device, const std::vec // Same kvalues budget as MXFP4 plus ue4m3_fp32_lut[128] (types.glsl, DATA_A_NVFP4). lut_size = 4*16 + 128u * (uint32_t)sizeof(float); break; + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + // kvalues_rocmfp4[16] (int8) plus rocmfp_ue4m3_fp32_lut[128] (types.glsl). + lut_size = 16 + 128u * (uint32_t)sizeof(float); + break; + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: + // rocmfp_ue4m3_fp32_lut[128] only; the fp3 codebook is a const array. + lut_size = 128u * (uint32_t)sizeof(float); + break; default: break; } @@ -4782,6 +4793,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_IQ3_S], matmul_iq3_s_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_IQ4_XS], matmul_iq4_xs_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_IQ4_NL], matmul_iq4_nl_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) + CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q4_0_ROCMFP4], matmul_rocmfp4_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) + CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_rocmfp4_fast_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) + CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q3_0_ROCMFPX], matmul_rocmfpx_fp3_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) + CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q6_0_ROCMFPX], matmul_rocmfpx_fp6_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) + CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q8_0_ROCMFPX], matmul_rocmfpx_fp8_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) #if defined(GGML_VULKAN_FLOAT_E2M1_GLSLC_SUPPORT) && defined(GGML_VULKAN_FLOAT_E4M3_GLSLC_SUPPORT) if (device->ocp_fp4) { CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_MXFP4], matmul_mxfp4_f16_ocp, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3) @@ -4823,6 +4839,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_S], matmul_id_subgroup_iq3_s_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_XS], matmul_id_subgroup_iq4_xs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_NL], matmul_id_subgroup_iq4_nl_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4], matmul_id_subgroup_rocmfp4_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_id_subgroup_rocmfp4_fast_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp3_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp6_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp8_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) #if defined(GGML_VULKAN_FLOAT_E2M1_GLSLC_SUPPORT) && defined(GGML_VULKAN_FLOAT_E4M3_GLSLC_SUPPORT) if (device->ocp_fp4) { CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f16_ocp, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) @@ -4896,6 +4917,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ3_S], matmul_iq3_s_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ4_XS], matmul_iq4_xs_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ4_NL], matmul_iq4_nl_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q4_0_ROCMFP4], matmul_rocmfp4_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_rocmfp4_fast_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q3_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_0_ROCMFPX], matmul_rocmfpx_fp3_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q6_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q6_0_ROCMFPX], matmul_rocmfpx_fp6_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q8_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q8_0_ROCMFPX], matmul_rocmfpx_fp8_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); #if defined(GGML_VULKAN_FLOAT_E2M1_GLSLC_SUPPORT) && defined(GGML_VULKAN_FLOAT_E4M3_GLSLC_SUPPORT) if (device->ocp_fp4) { @@ -4941,6 +4967,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_S], matmul_id_subgroup_iq3_s_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); CREATE_MM2(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_XS], matmul_id_subgroup_iq4_xs_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); CREATE_MM2(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_NL], matmul_id_subgroup_iq4_nl_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4], matmul_id_subgroup_rocmfp4_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_id_subgroup_rocmfp4_fast_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q3_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp3_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q6_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp6_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q8_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp8_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); #if defined(GGML_VULKAN_FLOAT_E2M1_GLSLC_SUPPORT) && defined(GGML_VULKAN_FLOAT_E4M3_GLSLC_SUPPORT) if (device->ocp_fp4) { CREATE_MM2(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f32_ocp, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); @@ -5032,6 +5063,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ3_S], matmul_iq3_s_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM2(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ4_XS], matmul_iq4_xs_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM2(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ4_NL], matmul_iq4_nl_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q4_0_ROCMFP4], matmul_rocmfp4_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_rocmfp4_fast_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM2(GGML_TYPE_Q3_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_0_ROCMFPX], matmul_rocmfpx_fp3_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM2(GGML_TYPE_Q6_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q6_0_ROCMFPX], matmul_rocmfpx_fp6_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM2(GGML_TYPE_Q8_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q8_0_ROCMFPX], matmul_rocmfpx_fp8_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM2(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat[GGML_TYPE_MXFP4], matmul_mxfp4_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM2(GGML_TYPE_NVFP4, pipeline_dequant_mul_mat_mat[GGML_TYPE_NVFP4], matmul_nvfp4_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); @@ -5081,6 +5117,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_S], matmul_id_subgroup_iq3_s_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MM2(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_XS], matmul_id_subgroup_iq4_xs_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MM2(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_NL], matmul_id_subgroup_iq4_nl_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4], matmul_id_subgroup_rocmfp4_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_id_subgroup_rocmfp4_fast_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM2(GGML_TYPE_Q3_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp3_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM2(GGML_TYPE_Q6_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp6_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM2(GGML_TYPE_Q8_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp8_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MM2(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MM2(GGML_TYPE_NVFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4], matmul_id_subgroup_nvfp4_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); @@ -5129,6 +5170,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_S], matmul_id_iq3_s_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MM2(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_XS], matmul_id_iq4_xs_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MM2(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_NL], matmul_id_iq4_nl_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4], matmul_id_rocmfp4_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM2(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_id_rocmfp4_fast_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM2(GGML_TYPE_Q3_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_0_ROCMFPX], matmul_id_rocmfpx_fp3_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM2(GGML_TYPE_Q6_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_0_ROCMFPX], matmul_id_rocmfpx_fp6_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM2(GGML_TYPE_Q8_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0_ROCMFPX], matmul_id_rocmfpx_fp8_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MM2(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_mxfp4_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MM2(GGML_TYPE_NVFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4], matmul_id_nvfp4_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); @@ -5209,6 +5255,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ3_S].f32acc, matmul_iq3_s_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ4_XS].f32acc, matmul_iq4_xs_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ4_NL].f32acc, matmul_iq4_nl_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q4_0_ROCMFP4].f32acc, matmul_rocmfp4_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q4_0_ROCMFP4_FAST].f32acc, matmul_rocmfp4_fast_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM(GGML_TYPE_Q3_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_0_ROCMFPX].f32acc, matmul_rocmfpx_fp3_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM(GGML_TYPE_Q6_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q6_0_ROCMFPX].f32acc, matmul_rocmfpx_fp6_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); + CREATE_MM(GGML_TYPE_Q8_0_ROCMFPX, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q8_0_ROCMFPX].f32acc, matmul_rocmfpx_fp8_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat[GGML_TYPE_MXFP4].f32acc, matmul_mxfp4_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); CREATE_MM(GGML_TYPE_NVFP4, pipeline_dequant_mul_mat_mat[GGML_TYPE_NVFP4].f32acc, matmul_nvfp4_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0); @@ -5257,6 +5308,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_S].f32acc, matmul_id_subgroup_iq3_s_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MM(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_XS].f32acc, matmul_id_subgroup_iq4_xs_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MM(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_NL].f32acc, matmul_id_subgroup_iq4_nl_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4].f32acc, matmul_id_subgroup_rocmfp4_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4_FAST].f32acc, matmul_id_subgroup_rocmfp4_fast_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM(GGML_TYPE_Q3_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_0_ROCMFPX].f32acc, matmul_id_subgroup_rocmfpx_fp3_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM(GGML_TYPE_Q6_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_0_ROCMFPX].f32acc, matmul_id_subgroup_rocmfpx_fp6_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MM(GGML_TYPE_Q8_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0_ROCMFPX].f32acc, matmul_id_subgroup_rocmfpx_fp8_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MM(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4].f32acc, matmul_id_subgroup_mxfp4_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MM(GGML_TYPE_NVFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4].f32acc, matmul_id_subgroup_nvfp4_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); } else { @@ -5287,6 +5343,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_S].f32acc, matmul_id_iq3_s_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MM(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_XS].f32acc, matmul_id_iq4_xs_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MM(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_NL].f32acc, matmul_id_iq4_nl_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4].f32acc, matmul_id_rocmfp4_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4_FAST].f32acc, matmul_id_rocmfp4_fast_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM(GGML_TYPE_Q3_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_0_ROCMFPX].f32acc, matmul_id_rocmfpx_fp3_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM(GGML_TYPE_Q6_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_0_ROCMFPX].f32acc, matmul_id_rocmfpx_fp6_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MM(GGML_TYPE_Q8_0_ROCMFPX, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0_ROCMFPX].f32acc, matmul_id_rocmfpx_fp8_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MM(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4].f32acc, matmul_id_mxfp4_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MM(GGML_TYPE_NVFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4].f32acc, matmul_id_nvfp4_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); } @@ -5396,6 +5457,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_IQ3_S][i], "mul_mat_vec_iq3_s_f32_f32", arr_dmmv_iq3_s_f32_f32_len[reduc16], arr_dmmv_iq3_s_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_IQ4_XS][i], "mul_mat_vec_iq4_xs_f32_f32", arr_dmmv_iq4_xs_f32_f32_len[reduc16], arr_dmmv_iq4_xs_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_IQ4_NL][i], "mul_mat_vec_iq4_nl_f32_f32", arr_dmmv_iq4_nl_f32_f32_len[reduc16], arr_dmmv_iq4_nl_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q4_0_ROCMFP4][i], "mul_mat_vec_rocmfp4_f32_f32", arr_dmmv_rocmfp4_f32_f32_len[reduc16], arr_dmmv_rocmfp4_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q4_0_ROCMFP4_FAST][i], "mul_mat_vec_rocmfp4_fast_f32_f32", arr_dmmv_rocmfp4_fast_f32_f32_len[reduc16], arr_dmmv_rocmfp4_fast_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q3_0_ROCMFPX][i], "mul_mat_vec_rocmfpx_fp3_f32_f32", arr_dmmv_rocmfpx_fp3_f32_f32_len[reduc16], arr_dmmv_rocmfpx_fp3_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q6_0_ROCMFPX][i], "mul_mat_vec_rocmfpx_fp6_f32_f32", arr_dmmv_rocmfpx_fp6_f32_f32_len[reduc16], arr_dmmv_rocmfpx_fp6_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q8_0_ROCMFPX][i], "mul_mat_vec_rocmfpx_fp8_f32_f32", arr_dmmv_rocmfpx_fp8_f32_f32_len[reduc16], arr_dmmv_rocmfpx_fp8_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_MXFP4][i], "mul_mat_vec_mxfp4_f32_f32", OCP_DMMV_LEN(arr_dmmv_mxfp4_f32_f32, reduc16), OCP_DMMV_DATA(arr_dmmv_mxfp4_f32_f32, reduc16), "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_NVFP4][i], "mul_mat_vec_nvfp4_f32_f32", OCP_DMMV_LEN(arr_dmmv_nvfp4_f32_f32, reduc16), OCP_DMMV_DATA(arr_dmmv_nvfp4_f32_f32, reduc16), "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); @@ -5424,6 +5490,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_IQ3_S][i], "mul_mat_vec_iq3_s_f16_f32", arr_dmmv_iq3_s_f16_f32_len[reduc16], arr_dmmv_iq3_s_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_IQ4_XS][i], "mul_mat_vec_iq4_xs_f16_f32", arr_dmmv_iq4_xs_f16_f32_len[reduc16], arr_dmmv_iq4_xs_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_IQ4_NL][i], "mul_mat_vec_iq4_nl_f16_f32", arr_dmmv_iq4_nl_f16_f32_len[reduc16], arr_dmmv_iq4_nl_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q4_0_ROCMFP4][i], "mul_mat_vec_rocmfp4_f16_f32", arr_dmmv_rocmfp4_f16_f32_len[reduc16], arr_dmmv_rocmfp4_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q4_0_ROCMFP4_FAST][i], "mul_mat_vec_rocmfp4_fast_f16_f32", arr_dmmv_rocmfp4_fast_f16_f32_len[reduc16], arr_dmmv_rocmfp4_fast_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q3_0_ROCMFPX][i], "mul_mat_vec_rocmfpx_fp3_f16_f32", arr_dmmv_rocmfpx_fp3_f16_f32_len[reduc16], arr_dmmv_rocmfpx_fp3_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q6_0_ROCMFPX][i], "mul_mat_vec_rocmfpx_fp6_f16_f32", arr_dmmv_rocmfpx_fp6_f16_f32_len[reduc16], arr_dmmv_rocmfpx_fp6_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q8_0_ROCMFPX][i], "mul_mat_vec_rocmfpx_fp8_f16_f32", arr_dmmv_rocmfpx_fp8_f16_f32_len[reduc16], arr_dmmv_rocmfpx_fp8_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_MXFP4][i], "mul_mat_vec_mxfp4_f16_f32", OCP_DMMV_LEN(arr_dmmv_mxfp4_f16_f32, reduc16), OCP_DMMV_DATA(arr_dmmv_mxfp4_f16_f32, reduc16), "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_NVFP4][i], "mul_mat_vec_nvfp4_f16_f32", OCP_DMMV_LEN(arr_dmmv_nvfp4_f16_f32, reduc16), OCP_DMMV_DATA(arr_dmmv_nvfp4_f16_f32, reduc16), "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq, i+1}, 1, true, use_subgroups16, force_subgroup_size16); @@ -5479,6 +5550,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_IQ3_S], "mul_mat_vec_id_iq3_s_f32", arr_dmmv_id_iq3_s_f32_f32_len[reduc16], arr_dmmv_id_iq3_s_f32_f32_data[reduc16], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_IQ4_XS], "mul_mat_vec_id_iq4_xs_f32", arr_dmmv_id_iq4_xs_f32_f32_len[reduc16], arr_dmmv_id_iq4_xs_f32_f32_data[reduc16], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_IQ4_NL], "mul_mat_vec_id_iq4_nl_f32", arr_dmmv_id_iq4_nl_f32_f32_len[reduc16], arr_dmmv_id_iq4_nl_f32_f32_data[reduc16], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q4_0_ROCMFP4], "mul_mat_vec_id_rocmfp4_f32", arr_dmmv_id_rocmfp4_f32_f32_len[reduc16], arr_dmmv_id_rocmfp4_f32_f32_data[reduc16], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q4_0_ROCMFP4_FAST], "mul_mat_vec_id_rocmfp4_fast_f32", arr_dmmv_id_rocmfp4_fast_f32_f32_len[reduc16], arr_dmmv_id_rocmfp4_fast_f32_f32_data[reduc16], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q3_0_ROCMFPX], "mul_mat_vec_id_rocmfpx_fp3_f32", arr_dmmv_id_rocmfpx_fp3_f32_f32_len[reduc16], arr_dmmv_id_rocmfpx_fp3_f32_f32_data[reduc16], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q6_0_ROCMFPX], "mul_mat_vec_id_rocmfpx_fp6_f32", arr_dmmv_id_rocmfpx_fp6_f32_f32_len[reduc16], arr_dmmv_id_rocmfpx_fp6_f32_f32_data[reduc16], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q8_0_ROCMFPX], "mul_mat_vec_id_rocmfpx_fp8_f32", arr_dmmv_id_rocmfpx_fp8_f32_f32_len[reduc16], arr_dmmv_id_rocmfpx_fp8_f32_f32_data[reduc16], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_MXFP4], "mul_mat_vec_id_mxfp4_f32", OCP_DMMV_LEN(arr_dmmv_id_mxfp4_f32_f32, reduc16), OCP_DMMV_DATA(arr_dmmv_id_mxfp4_f32_f32, reduc16), "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_NVFP4], "mul_mat_vec_id_nvfp4_f32", OCP_DMMV_LEN(arr_dmmv_id_nvfp4_f32_f32, reduc16), OCP_DMMV_DATA(arr_dmmv_id_nvfp4_f32_f32, reduc16), "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_iq, 1, 1}, {wg_size_subgroup16, rm_iq}, 1, true, use_subgroups16, force_subgroup_size16); @@ -5545,6 +5621,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_IQ3_S], "dequant_iq3_s", dequant_iq3_s_len, dequant_iq3_s_data, "main", 2, 5 * sizeof(uint32_t), {256 * 32, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_IQ4_XS], "dequant_iq4_xs", dequant_iq4_xs_len, dequant_iq4_xs_data, "main", 2, 5 * sizeof(uint32_t), {256 * 32, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_IQ4_NL], "dequant_iq4_nl", dequant_iq4_nl_len, dequant_iq4_nl_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q4_0_ROCMFP4], "dequant_rocmfp4", dequant_rocmfp4_len, dequant_rocmfp4_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q4_0_ROCMFP4_FAST], "dequant_rocmfp4_fast", dequant_rocmfp4_fast_len, dequant_rocmfp4_fast_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q3_0_ROCMFPX], "dequant_rocmfpx_fp3", dequant_rocmfpx_fp3_len, dequant_rocmfpx_fp3_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q6_0_ROCMFPX], "dequant_rocmfpx_fp6", dequant_rocmfpx_fp6_len, dequant_rocmfpx_fp6_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q8_0_ROCMFPX], "dequant_rocmfpx_fp8", dequant_rocmfpx_fp8_len, dequant_rocmfpx_fp8_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_MXFP4], "dequant_mxfp4", dequant_mxfp4_len, dequant_mxfp4_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_NVFP4], "dequant_nvfp4", dequant_nvfp4_len, dequant_nvfp4_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); @@ -5574,6 +5655,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_IQ3_S], "get_rows_iq3_s", get_rows_iq3_s_len, get_rows_iq3_s_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_IQ4_XS], "get_rows_iq4_xs", get_rows_iq4_xs_len, get_rows_iq4_xs_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_IQ4_NL], "get_rows_iq4_nl", get_rows_iq4_nl_len, get_rows_iq4_nl_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_Q4_0_ROCMFP4], "get_rows_rocmfp4", get_rows_rocmfp4_len, get_rows_rocmfp4_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_Q4_0_ROCMFP4_FAST], "get_rows_rocmfp4_fast", get_rows_rocmfp4_fast_len, get_rows_rocmfp4_fast_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_Q3_0_ROCMFPX], "get_rows_rocmfpx_fp3", get_rows_rocmfpx_fp3_len, get_rows_rocmfpx_fp3_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_Q6_0_ROCMFPX], "get_rows_rocmfpx_fp6", get_rows_rocmfpx_fp6_len, get_rows_rocmfpx_fp6_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_Q8_0_ROCMFPX], "get_rows_rocmfpx_fp8", get_rows_rocmfpx_fp8_len, get_rows_rocmfpx_fp8_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_MXFP4], "get_rows_mxfp4", get_rows_mxfp4_len, get_rows_mxfp4_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_NVFP4], "get_rows_nvfp4", get_rows_nvfp4_len, get_rows_nvfp4_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_I32], "get_rows_i32", get_rows_i32_len, get_rows_i32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); @@ -5603,6 +5689,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_IQ3_S], "get_rows_iq3_s_f32", get_rows_iq3_s_f32_len, get_rows_iq3_s_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_IQ4_XS], "get_rows_iq4_xs_f32", get_rows_iq4_xs_f32_len, get_rows_iq4_xs_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_IQ4_NL], "get_rows_iq4_nl_f32", get_rows_iq4_nl_f32_len, get_rows_iq4_nl_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_Q4_0_ROCMFP4], "get_rows_rocmfp4_f32", get_rows_rocmfp4_f32_len, get_rows_rocmfp4_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_Q4_0_ROCMFP4_FAST], "get_rows_rocmfp4_fast_f32", get_rows_rocmfp4_fast_f32_len, get_rows_rocmfp4_fast_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_Q3_0_ROCMFPX], "get_rows_rocmfpx_fp3_f32", get_rows_rocmfpx_fp3_f32_len, get_rows_rocmfpx_fp3_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_Q6_0_ROCMFPX], "get_rows_rocmfpx_fp6_f32", get_rows_rocmfpx_fp6_f32_len, get_rows_rocmfpx_fp6_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_Q8_0_ROCMFPX], "get_rows_rocmfpx_fp8_f32", get_rows_rocmfpx_fp8_f32_len, get_rows_rocmfpx_fp8_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_MXFP4], "get_rows_mxfp4_f32", get_rows_mxfp4_f32_len, get_rows_mxfp4_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_NVFP4], "get_rows_nvfp4_f32", get_rows_nvfp4_f32_len, get_rows_nvfp4_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_get_rows_back_f32, "get_rows_back_f32", get_rows_back_f32_len, get_rows_back_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {256, 1, 1}, {}, 1, true); @@ -5677,6 +5768,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_cpy_f32_quant[GGML_TYPE_Q5_1], "cpy_f32_q5_1", cpy_f32_q5_1_len, cpy_f32_q5_1_data, "main", 2, sizeof(vk_op_unary_push_constants), {32, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_cpy_f32_quant[GGML_TYPE_Q8_0], "cpy_f32_q8_0", cpy_f32_q8_0_len, cpy_f32_q8_0_data, "main", 2, sizeof(vk_op_unary_push_constants), {32, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_cpy_f32_quant[GGML_TYPE_IQ4_NL], "cpy_f32_iq4_nl", cpy_f32_iq4_nl_len, cpy_f32_iq4_nl_data, "main", 2, sizeof(vk_op_unary_push_constants), {32, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_f32_quant[GGML_TYPE_Q4_0_ROCMFP4], "cpy_f32_rocmfp4", cpy_f32_rocmfp4_len, cpy_f32_rocmfp4_data, "main", 2, sizeof(vk_op_unary_push_constants), {32, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_f32_quant[GGML_TYPE_Q4_0_ROCMFP4_FAST], "cpy_f32_rocmfp4_fast", cpy_f32_rocmfp4_fast_len, cpy_f32_rocmfp4_fast_data, "main", 2, sizeof(vk_op_unary_push_constants), {32, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_f32_quant[GGML_TYPE_Q3_0_ROCMFPX], "cpy_f32_rocmfpx_fp3", cpy_f32_rocmfpx_fp3_len, cpy_f32_rocmfpx_fp3_data, "main", 2, sizeof(vk_op_unary_push_constants), {32, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_f32_quant[GGML_TYPE_Q6_0_ROCMFPX], "cpy_f32_rocmfpx_fp6", cpy_f32_rocmfpx_fp6_len, cpy_f32_rocmfpx_fp6_data, "main", 2, sizeof(vk_op_unary_push_constants), {32, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_f32_quant[GGML_TYPE_Q8_0_ROCMFPX], "cpy_f32_rocmfpx_fp8", cpy_f32_rocmfpx_fp8_len, cpy_f32_rocmfpx_fp8_data, "main", 2, sizeof(vk_op_unary_push_constants), {32, 1, 1}, {}, 1); #define SET_ROWS(src_idx, src, itype) \ ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_F32], "set_rows_" #src "_f32" #itype, set_rows_ ## src ## _f32 ## itype ## _len, set_rows_ ## src ## _f32 ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ @@ -5689,7 +5785,12 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_Q5_0], "set_rows_" #src "_q5_0" #itype, set_rows_ ## src ## _q5_0 ## itype ## _len, set_rows_ ## src ## _q5_0 ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_Q5_1], "set_rows_" #src "_q5_1" #itype, set_rows_ ## src ## _q5_1 ## itype ## _len, set_rows_ ## src ## _q5_1 ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_Q8_0], "set_rows_" #src "_q8_0" #itype, set_rows_ ## src ## _q8_0 ## itype ## _len, set_rows_ ## src ## _q8_0 ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ - ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_IQ4_NL], "set_rows_" #src "_iq4_nl" #itype, set_rows_ ## src ## _iq4_nl ## itype ## _len, set_rows_ ## src ## _iq4_nl ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); + ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_IQ4_NL], "set_rows_" #src "_iq4_nl" #itype, set_rows_ ## src ## _iq4_nl ## itype ## _len, set_rows_ ## src ## _iq4_nl ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ + ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_Q4_0_ROCMFP4], "set_rows_" #src "_rocmfp4" #itype, set_rows_ ## src ## _rocmfp4 ## itype ## _len, set_rows_ ## src ## _rocmfp4 ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ + ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_Q4_0_ROCMFP4_FAST], "set_rows_" #src "_rocmfp4_fast" #itype, set_rows_ ## src ## _rocmfp4_fast ## itype ## _len, set_rows_ ## src ## _rocmfp4_fast ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ + ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_Q3_0_ROCMFPX], "set_rows_" #src "_rocmfpx_fp3" #itype, set_rows_ ## src ## _rocmfpx_fp3 ## itype ## _len, set_rows_ ## src ## _rocmfpx_fp3 ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ + ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_Q6_0_ROCMFPX], "set_rows_" #src "_rocmfpx_fp6" #itype, set_rows_ ## src ## _rocmfpx_fp6 ## itype ## _len, set_rows_ ## src ## _rocmfpx_fp6 ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); \ + ggml_vk_create_pipeline(device, device->pipeline_set_rows ## itype [src_idx][GGML_TYPE_Q8_0_ROCMFPX], "set_rows_" #src "_rocmfpx_fp8" #itype, set_rows_ ## src ## _rocmfpx_fp8 ## itype ## _len, set_rows_ ## src ## _rocmfpx_fp8 ## itype ## _data, "main", 3, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {1}, 1, true); SET_ROWS(0, f32, _i32) SET_ROWS(0, f32, _i64) @@ -5706,6 +5807,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_cpy_quant_f32[GGML_TYPE_Q5_1], "cpy_q5_1_f32", cpy_q5_1_f32_len, cpy_q5_1_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {(uint32_t)ggml_blck_size(GGML_TYPE_Q5_1), 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_cpy_quant_f32[GGML_TYPE_Q8_0], "cpy_q8_0_f32", cpy_q8_0_f32_len, cpy_q8_0_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {(uint32_t)ggml_blck_size(GGML_TYPE_Q8_0), 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_cpy_quant_f32[GGML_TYPE_IQ4_NL], "cpy_iq4_nl_f32", cpy_iq4_nl_f32_len, cpy_iq4_nl_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {(uint32_t)ggml_blck_size(GGML_TYPE_IQ4_NL), 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_quant_f32[GGML_TYPE_Q4_0_ROCMFP4], "cpy_rocmfp4_f32", cpy_rocmfp4_f32_len, cpy_rocmfp4_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {(uint32_t)ggml_blck_size(GGML_TYPE_Q4_0_ROCMFP4), 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_quant_f32[GGML_TYPE_Q4_0_ROCMFP4_FAST], "cpy_rocmfp4_fast_f32", cpy_rocmfp4_fast_f32_len, cpy_rocmfp4_fast_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {(uint32_t)ggml_blck_size(GGML_TYPE_Q4_0_ROCMFP4_FAST), 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_quant_f32[GGML_TYPE_Q3_0_ROCMFPX], "cpy_rocmfpx_fp3_f32", cpy_rocmfpx_fp3_f32_len, cpy_rocmfpx_fp3_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {(uint32_t)ggml_blck_size(GGML_TYPE_Q3_0_ROCMFPX), 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_quant_f32[GGML_TYPE_Q6_0_ROCMFPX], "cpy_rocmfpx_fp6_f32", cpy_rocmfpx_fp6_f32_len, cpy_rocmfpx_fp6_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {(uint32_t)ggml_blck_size(GGML_TYPE_Q6_0_ROCMFPX), 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_cpy_quant_f32[GGML_TYPE_Q8_0_ROCMFPX], "cpy_rocmfpx_fp8_f32", cpy_rocmfpx_fp8_f32_len, cpy_rocmfpx_fp8_f32_data, "main", 2, sizeof(vk_op_unary_push_constants), {(uint32_t)ggml_blck_size(GGML_TYPE_Q8_0_ROCMFPX), 1, 1}, {}, 1); auto get_suffix = [](bool src0_f16, bool src1_f16, bool dst_f16) { std::string s; @@ -7828,6 +7934,11 @@ static vk_pipeline ggml_vk_get_to_fp16(ggml_backend_vk_context * ctx, ggml_type case GGML_TYPE_IQ4_NL: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_TQ2_0: break; default: @@ -7903,6 +8014,11 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_pipeline(ggml_backend_vk_conte case GGML_TYPE_IQ4_NL: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_TQ2_0: break; default: @@ -7973,6 +8089,11 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec(ggml_backend_vk_context * case GGML_TYPE_IQ4_NL: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_TQ2_0: break; default: @@ -8067,6 +8188,11 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_id_pipeline(ggml_backend_vk_co case GGML_TYPE_IQ4_NL: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_TQ2_0: break; default: @@ -8140,6 +8266,11 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec_id(ggml_backend_vk_context case GGML_TYPE_IQ4_NL: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_TQ2_0: break; default: @@ -9181,6 +9312,11 @@ static vk_pipeline ggml_vk_get_cpy_pipeline(ggml_backend_vk_context * ctx, const case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: case GGML_TYPE_IQ4_NL: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: return ctx->device->pipeline_cpy_f32_quant[to]; default: break; @@ -9197,6 +9333,11 @@ static vk_pipeline ggml_vk_get_cpy_pipeline(ggml_backend_vk_context * ctx, const case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: case GGML_TYPE_IQ4_NL: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: return ctx->device->pipeline_cpy_quant_f32[src->type]; default: break; @@ -18803,6 +18944,11 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_TYPE_IQ4_NL: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_TQ2_0: break; default: @@ -18909,6 +19055,11 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_TYPE_IQ4_NL: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_TQ2_0: case GGML_TYPE_I32: return true; @@ -18936,6 +19087,11 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: case GGML_TYPE_IQ4_NL: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: return true; default: return false; @@ -18961,6 +19117,11 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: case GGML_TYPE_IQ4_NL: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: return true; default: break; @@ -18978,6 +19139,11 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: case GGML_TYPE_IQ4_NL: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: return true; default: break; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/copy_from_quant.comp b/ggml/src/ggml-vulkan/vulkan-shaders/copy_from_quant.comp index 6a6921474781..75bdfe47231f 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/copy_from_quant.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/copy_from_quant.comp @@ -4,7 +4,7 @@ #include "generic_unary_head.glsl" #include "dequant_funcs.glsl" -#if defined(DATA_A_IQ4_NL) || defined(DATA_A_MXFP4) || defined(DATA_A_NVFP4) +#if defined(DATA_A_IQ4_NL) || defined(DATA_A_MXFP4) || defined(DATA_A_NVFP4) || defined(DATA_A_ROCMFP_ANY) // 16 invocations needed for init_iq_shmem layout(local_size_x = 16, local_size_y = 1, local_size_z = 1) in; #else diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/copy_quant_same.comp b/ggml/src/ggml-vulkan/vulkan-shaders/copy_quant_same.comp new file mode 100644 index 000000000000..4b28c42a31ed --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/copy_quant_same.comp @@ -0,0 +1,55 @@ +#version 450 + +#include "types.glsl" +#include "generic_unary_head.glsl" + +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +#if !defined(BLOCK_BYTES) +#error BLOCK_BYTES must be defined +#endif + +uint src0_block_idx(uint idx) { + const uint ne00b = p.ne00 / QUANT_K; + const uint nb02 = p.ne01 * ne00b; + const uint nb03 = p.ne02 * nb02; + + const uint i03 = idx / nb03; + const uint r03 = idx - i03 * nb03; + const uint i02 = r03 / nb02; + const uint r02 = r03 - i02 * nb02; + const uint i01 = r02 / ne00b; + const uint i00b = r02 - i01 * ne00b; + + return i03*p.nb03 + i02*p.nb02 + i01*p.nb01 + i00b*p.nb00; +} + +uint dst_block_idx(uint idx) { + const uint ne10b = p.ne10 / QUANT_K; + const uint nb12 = p.ne11 * ne10b; + const uint nb13 = p.ne12 * nb12; + + const uint i13 = idx / nb13; + const uint r13 = idx - i13 * nb13; + const uint i12 = r13 / nb12; + const uint r12 = r13 - i12 * nb12; + const uint i11 = r12 / ne10b; + const uint i10b = r12 - i11 * ne10b; + + return i13*p.nb13 + i12*p.nb12 + i11*p.nb11 + i10b*p.nb10; +} + +void main() { + const uint idx = get_idx(); + + if (idx >= p.ne) { + return; + } + + const uint src_base = get_aoffset() + src0_block_idx(idx) * BLOCK_BYTES; + const uint dst_base = get_doffset() + dst_block_idx(idx) * BLOCK_BYTES; + + [[unroll]] for (uint j = 0; j < BLOCK_BYTES; ++j) { + data_d[dst_base + j] = data_a[src_base + j]; + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp b/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp index 776e9b8a5572..481f9a8ca135 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp @@ -12,6 +12,10 @@ const uint BLOCK_SIZE = 32; layout (binding = 0) readonly buffer S {S_TYPE data_s[];}; +float load_s(uint idx) { + return float(data_s[idx]); +} + #if defined(SET_ROWS) #include "generic_binary_head.glsl" layout (binding = 1) readonly buffer C {B_TYPE data_i[];}; @@ -286,6 +290,481 @@ void quantize(uint dst_idx, uint src_idx) } #endif +#if defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) +float rocmfp4_code_value(uint q) { + switch (q & 0xFu) { + case 0u: return 0.0; + case 1u: return 1.0; + case 2u: return 2.0; + case 3u: return 3.0; + case 4u: return 4.0; + case 5u: return 6.0; + case 6u: return 8.0; + case 7u: return 10.0; + case 8u: return 0.0; + case 9u: return -1.0; + case 10u: return -2.0; + case 11u: return -3.0; + case 12u: return -4.0; + case 13u: return -6.0; + case 14u: return -8.0; + default: return -10.0; + } +} + +uint rocmfp4_best_index_scaled(float x, float inv_scale_half) { + const float a = abs(x * inv_scale_half); + if (a <= 0.5) { + return 0u; + } + + const bool neg = x < 0.0; + if (a <= 1.5) return neg ? 9u : 1u; + if (a <= 2.5) return neg ? 10u : 2u; + if (a <= 3.5) return neg ? 11u : 3u; + if (a <= 5.0) return neg ? 12u : 4u; + if (a <= 7.0) return neg ? 13u : 5u; + if (a <= 9.0) return neg ? 14u : 6u; + + return neg ? 15u : 7u; +} + +float rocmfp4_block_mse_for_scale(uint src_idx, uint src_off, uint n, uint e, float best_err) { + const float scale_half = ue4m3_to_fp32(uint8_t(e)); + const float inv_scale_half = scale_half > 0.0 ? 1.0 / scale_half : 0.0; + float err = 0.0; + + for (uint j = 0; j < n; ++j) { + const float x = load_s(src_idx + src_off + j); + const uint q = rocmfp4_best_index_scaled(x, inv_scale_half); + const float y = rocmfp4_code_value(q) * scale_half; + const float d = x - y; + err += d * d; + if (err > best_err) { + return err; + } + } + + return err; +} + +uint rocmfp4_nearest_scale_ue4m3(float target_scale_half) { + if (target_scale_half <= 0.0) { + return 1u; + } + + uint lo = 1u; + uint hi = 126u; + while (lo < hi) { + const uint mid = lo + (hi - lo) / 2u; + if (ue4m3_to_fp32(uint8_t(mid)) < target_scale_half) { + lo = mid + 1u; + } else { + hi = mid; + } + } + + if (lo == 1u) { + return 1u; + } + + const float hi_scale = ue4m3_to_fp32(uint8_t(lo)); + const float lo_scale = ue4m3_to_fp32(uint8_t(lo - 1u)); + + // Match the previous ascending nearest scan: exact midpoint ties keep the + // lower scale byte. + return (target_scale_half - lo_scale <= hi_scale - target_scale_half) ? lo - 1u : lo; +} + +uint rocmfp4_choose_scale_ue4m3(uint src_idx, uint src_off, uint n) { + float max_abs = 0.0; + for (uint j = 0; j < n; ++j) { + max_abs = max(max_abs, abs(load_s(src_idx + src_off + j))); + } + if (max_abs <= 0.0) { + return 0u; + } + + const uint start_e = rocmfp4_nearest_scale_ue4m3(max_abs / 10.0); + uint best_e = 0u; + float best_err = 3.4028234663852886e38; + bool lower_done = false; + + for (uint delta = 0u; delta <= 125u; ++delta) { + const int e0 = int(start_e) - int(delta); + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale_half = ue4m3_to_fp32(uint8_t(e0)); + const float clip_delta = max_abs - 10.0 * scale_half; + if (clip_delta > 0.0 && clip_delta * clip_delta > best_err) { + lower_done = true; + } else { + const uint e0u = uint(e0); + const float err = rocmfp4_block_mse_for_scale(src_idx, src_off, n, e0u, best_err); + if (err < best_err || (err == best_err && e0u < best_e)) { + best_err = err; + best_e = e0u; + } + } + } + + const uint e1 = start_e + delta; + if (delta != 0u && e1 <= 126u) { + const float err = rocmfp4_block_mse_for_scale(src_idx, src_off, n, e1, best_err); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126u) { + break; + } + } + + return best_e; +} + +#endif + +#if defined(DATA_A_ROCMFP4) +void quantize(uint dst_idx, uint src_idx) +{ + const uint e0 = rocmfp4_choose_scale_ue4m3(src_idx, 0u, QUANT_K_ROCMFP4 / 2); + const uint e1 = rocmfp4_choose_scale_ue4m3(src_idx, QUANT_K_ROCMFP4 / 2, QUANT_K_ROCMFP4 / 2); + const float d0 = ue4m3_to_fp32(uint8_t(e0)); + const float d1 = ue4m3_to_fp32(uint8_t(e1)); + const float id0 = d0 > 0.0 ? 1.0 / d0 : 0.0; + const float id1 = d1 > 0.0 ? 1.0 / d1 : 0.0; + + data_q[dst_idx].e[0] = uint8_t(e0); + data_q[dst_idx].e[1] = uint8_t(e1); + + [[unroll]] for (uint j = 0u; j < QUANT_K_ROCMFP4 / 2; ++j) { + const uint q0 = rocmfp4_best_index_scaled(load_s(src_idx + j), id0); + const uint q1 = rocmfp4_best_index_scaled(load_s(src_idx + QUANT_K_ROCMFP4 / 2 + j), id1); + data_q[dst_idx].qs[j] = uint8_t(q0 | (q1 << 4)); + } +} +#endif + +#if defined(DATA_A_ROCMFP4_FAST) +void quantize(uint dst_idx, uint src_idx) +{ + const uint e = rocmfp4_choose_scale_ue4m3(src_idx, 0u, QUANT_K_ROCMFP4); + const float d = ue4m3_to_fp32(uint8_t(e)); + const float id = d > 0.0 ? 1.0 / d : 0.0; + + data_q[dst_idx].e = uint8_t(e); + + [[unroll]] for (uint j = 0u; j < QUANT_K_ROCMFP4 / 2; ++j) { + const uint q0 = rocmfp4_best_index_scaled(load_s(src_idx + j), id); + const uint q1 = rocmfp4_best_index_scaled(load_s(src_idx + QUANT_K_ROCMFP4 / 2 + j), id); + data_q[dst_idx].qs[j] = uint8_t(q0 | (q1 << 4)); + } +} +#endif + +#if defined(DATA_A_ROCMFPX_FAMILY) +uint rocmfpx_nearest_scale_ue4m3(float target_scale) { + if (target_scale <= 0.0) { + return 0u; + } + + uint lo = 1u; + uint hi = 126u; + while (lo < hi) { + const uint mid = lo + (hi - lo) / 2u; + if (ue4m3_to_fp32(uint8_t(mid)) < target_scale) { + lo = mid + 1u; + } else { + hi = mid; + } + } + + if (lo == 1u) { + return 1u; + } + + const float hi_scale = ue4m3_to_fp32(uint8_t(lo)); + const float lo_scale = ue4m3_to_fp32(uint8_t(lo - 1u)); + return (target_scale - lo_scale <= hi_scale - target_scale) ? lo - 1u : lo; +} +#endif + +#if defined(DATA_A_ROCMFPX_FP3) +uint rocmfpx_fp3_quantize_code(float x, float inv_scale) { + if (inv_scale <= 0.0) { + return 0u; + } + + const float ax = abs(x * inv_scale); + uint mag; + if (ax <= 0.5) { + mag = 0u; + } else if (ax <= 1.5) { + mag = 1u; + } else if (ax <= 3.0) { + mag = 2u; + } else { + mag = 3u; + } + + return mag == 0u ? 0u : ((x < 0.0 ? 4u : 0u) | mag); +} + +int rocmfpx_fp3_decode_value(uint code) { + const int mag = (code & 3u) == 3u ? 4 : int(code & 3u); + return (code & 4u) != 0u ? -mag : mag; +} + +float rocmfpx_fp3_mse_for_scale(uint src_idx, uint offset, uint e, float best_err) { + const float scale = ue4m3_to_fp32(uint8_t(e)); + const float inv_scale = scale > 0.0 ? 1.0 / scale : 0.0; + float err = 0.0; + + [[unroll]] for (uint j = 0u; j < 16u; ++j) { + const float x = load_s(src_idx + offset + j); + const uint code = rocmfpx_fp3_quantize_code(x, inv_scale); + const float y = float(rocmfpx_fp3_decode_value(code)) * scale; + const float d = x - y; + + err += d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +uint rocmfpx_choose_scale_fp3_mse(uint src_idx, uint offset) { + float max_abs = 0.0; + [[unroll]] for (uint j = 0u; j < 16u; ++j) { + max_abs = max(max_abs, abs(load_s(src_idx + offset + j))); + } + + if (max_abs <= 0.0) { + return 0u; + } + + const uint start_e = rocmfpx_nearest_scale_ue4m3(max_abs / 4.0); + uint best_e = start_e; + float best_err = 3.402823466e+38; + bool lower_done = false; + + for (uint delta = 0u; delta <= 125u; ++delta) { + const int e0 = int(start_e) - int(delta); + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale = ue4m3_to_fp32(uint8_t(uint(e0))); + const float clip_delta = max_abs - 4.0 * scale; + if (clip_delta > 0.0 && clip_delta*clip_delta > best_err) { + lower_done = true; + } else { + const float err = rocmfpx_fp3_mse_for_scale(src_idx, offset, uint(e0), best_err); + if (err < best_err || (err == best_err && uint(e0) < best_e)) { + best_err = err; + best_e = uint(e0); + } + } + } + + const uint e1 = start_e + delta; + if (delta != 0u && e1 >= 1u && e1 <= 126u) { + const float err = rocmfpx_fp3_mse_for_scale(src_idx, offset, e1, best_err); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126u) { + break; + } + } + + return best_e; +} + +void rocmfpx_fp3_set_bits(inout uint qs[12], uint bit_pos, uint code) { + [[unroll]] for (uint bit = 0u; bit < 3u; ++bit) { + const uint dst_bit = bit_pos + bit; + const uint byte_index = dst_bit >> 3u; + const uint mask = 1u << (dst_bit & 7u); + uint v = qs[byte_index]; + v = (code & (1u << bit)) != 0u ? (v | mask) : (v & ~mask); + qs[byte_index] = v; + } +} + +void quantize(uint dst_idx, uint src_idx) +{ + uint qs[12]; + [[unroll]] for (uint j = 0u; j < 12u; ++j) { + qs[j] = 0u; + } + + [[unroll]] for (uint part = 0u; part < 2u; ++part) { + const uint offset = part * 16u; + const uint e = rocmfpx_choose_scale_fp3_mse(src_idx, offset); + const float d = ue4m3_to_fp32(uint8_t(e)); + const float id = d > 0.0 ? 1.0 / d : 0.0; + data_q[dst_idx].e[part] = uint8_t(e); + + [[unroll]] for (uint j = 0u; j < 16u; ++j) { + const uint i = offset + j; + rocmfpx_fp3_set_bits(qs, i * 3u, rocmfpx_fp3_quantize_code(load_s(src_idx + i), id)); + } + } + + [[unroll]] for (uint j = 0u; j < 12u; ++j) { + data_q[dst_idx].qs[j] = uint8_t(qs[j]); + } +} +#endif + +#if defined(DATA_A_ROCMFPX_FP6) +uint rocmfpx_fp6_quantize_code(float x, float inv_scale) { + if (inv_scale <= 0.0) { + return 0u; + } + + // asymmetric signed range [-32, 31]; -32 is encoded as sign|0 + const int q = clamp(int(round(x * inv_scale)), -32, 31); + if (q == 0) { + return 0u; + } + return q < 0 ? (32u | (uint(-q) & 31u)) : uint(q); +} + +int rocmfpx_fp6_decode_value(uint code) { + const int mag = int(code & 31u); + return (code & 32u) != 0u ? -(mag == 0 ? 32 : mag) : mag; +} + +float rocmfpx_fp6_mse_for_scale(uint src_idx, uint offset, uint e, float best_err) { + const float scale = ue4m3_to_fp32(uint8_t(e)); + const float inv_scale = scale > 0.0 ? 1.0 / scale : 0.0; + float err = 0.0; + + [[unroll]] for (uint j = 0u; j < 16u; ++j) { + const float x = load_s(src_idx + offset + j); + const uint code = rocmfpx_fp6_quantize_code(x, inv_scale); + const float y = float(rocmfpx_fp6_decode_value(code)) * scale; + const float d = x - y; + + err += d*d; + if (err > best_err) { + return err; + } + } + + return err; +} + +uint rocmfpx_choose_scale_fp6_mse(uint src_idx, uint offset) { + float max_abs = 0.0; + [[unroll]] for (uint j = 0u; j < 16u; ++j) { + max_abs = max(max_abs, abs(load_s(src_idx + offset + j))); + } + + if (max_abs <= 0.0) { + return 0u; + } + + const uint start_e = rocmfpx_nearest_scale_ue4m3(max_abs / 31.0); + uint best_e = start_e; + float best_err = 3.402823466e+38; + bool lower_done = false; + + for (uint delta = 0u; delta <= 125u; ++delta) { + const int e0 = int(start_e) - int(delta); + if (!lower_done && e0 >= 1 && e0 <= 126) { + const float scale = ue4m3_to_fp32(uint8_t(uint(e0))); + const float clip_delta = max_abs - 31.0 * scale; + if (clip_delta > 0.0 && clip_delta*clip_delta > best_err) { + lower_done = true; + } else { + const float err = rocmfpx_fp6_mse_for_scale(src_idx, offset, uint(e0), best_err); + if (err < best_err || (err == best_err && uint(e0) < best_e)) { + best_err = err; + best_e = uint(e0); + } + } + } + + const uint e1 = start_e + delta; + if (delta != 0u && e1 >= 1u && e1 <= 126u) { + const float err = rocmfpx_fp6_mse_for_scale(src_idx, offset, e1, best_err); + if (err < best_err || (err == best_err && e1 < best_e)) { + best_err = err; + best_e = e1; + } + } + + if ((lower_done || e0 <= 1) && e1 >= 126u) { + break; + } + } + + return best_e; +} + +void quantize(uint dst_idx, uint src_idx) +{ + [[unroll]] for (uint part = 0u; part < 2u; ++part) { + const uint offset = part * 16u; + const uint e = rocmfpx_choose_scale_fp6_mse(src_idx, offset); + const float d = ue4m3_to_fp32(uint8_t(e)); + const float id = d > 0.0 ? 1.0 / d : 0.0; + data_q[dst_idx].e[part] = uint8_t(e); + + // pack four 6-bit codes into three bytes, little-endian + [[unroll]] for (uint g = 0u; g < 4u; ++g) { + const uint i0 = offset + g * 4u; + const uint c0 = rocmfpx_fp6_quantize_code(load_s(src_idx + i0 + 0u), id); + const uint c1 = rocmfpx_fp6_quantize_code(load_s(src_idx + i0 + 1u), id); + const uint c2 = rocmfpx_fp6_quantize_code(load_s(src_idx + i0 + 2u), id); + const uint c3 = rocmfpx_fp6_quantize_code(load_s(src_idx + i0 + 3u), id); + + const uint b = (offset >> 4u) * 12u + g * 3u; + data_q[dst_idx].qs[b + 0u] = uint8_t(( c0 & 0x3Fu) | ((c1 & 0x03u) << 6)); + data_q[dst_idx].qs[b + 1u] = uint8_t(((c1 >> 2u) & 0x0Fu) | ((c2 & 0x0Fu) << 4)); + data_q[dst_idx].qs[b + 2u] = uint8_t(((c2 >> 4u) & 0x03u) | ((c3 & 0x3Fu) << 2)); + } + } +} +#endif + +#if defined(DATA_A_ROCMFPX_FP8) + +int rocmfpx_fp8_quantize_code(float x, float inv_scale) { + if (inv_scale <= 0.0) { + return 0; + } + + int q = int(round(x * inv_scale)); + return clamp(q, -127, 127); +} + +void quantize(uint dst_idx, uint src_idx) +{ + float max_abs = 0.0; + [[unroll]] for (uint j = 0u; j < QUANT_K_ROCMFPX_FP8; ++j) { + max_abs = max(max_abs, abs(load_s(src_idx + j))); + } + + const uint e = rocmfpx_nearest_scale_ue4m3(max_abs / 127.0); + const float d = ue4m3_to_fp32(uint8_t(e)); + const float id = d > 0.0 ? 1.0 / d : 0.0; + + data_q[dst_idx].e = uint8_t(e); + + [[unroll]] for (uint j = 0u; j < QUANT_K_ROCMFPX_FP8; ++j) { + data_q[dst_idx].qs[j] = int8_t(rocmfpx_fp8_quantize_code(load_s(src_idx + j), id)); + } +} +#endif + #if defined(DATA_A_F32) || defined(DATA_A_F16) void quantize(uint dst_idx, uint src_idx) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl index 627932bd3547..a8275564b808 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl @@ -510,6 +510,142 @@ vec4 dequantize4(uint ib, uint iqs, uint a_offset) { } #endif +#if defined(DATA_A_ROCMFP4) +vec2 dequantize(uint ib, uint iqs, uint a_offset) { + const uint vui = uint(data_a[a_offset + ib].qs[iqs]); + const float d0 = ue4m3_to_fp32(data_a[a_offset + ib].e[0]); + const float d1 = ue4m3_to_fp32(data_a[a_offset + ib].e[1]); + return vec2(float(kvalues_rocmfp4[vui & 0xF]) * d0, + float(kvalues_rocmfp4[vui >> 4]) * d1); +} +vec4 dequantize4(uint ib, uint iqs, uint a_offset) { + const uint vui0 = uint(data_a[a_offset + ib].qs[iqs]); + const uint vui1 = uint(data_a[a_offset + ib].qs[iqs + 1]); + const float d0 = ue4m3_to_fp32(data_a[a_offset + ib].e[0]); + const float d1 = ue4m3_to_fp32(data_a[a_offset + ib].e[1]); + return vec4(float(kvalues_rocmfp4[vui0 & 0xF]) * d0, + float(kvalues_rocmfp4[vui0 >> 4]) * d1, + float(kvalues_rocmfp4[vui1 & 0xF]) * d0, + float(kvalues_rocmfp4[vui1 >> 4]) * d1); +} +#endif + +#if defined(DATA_A_ROCMFP4_FAST) +vec2 dequantize(uint ib, uint iqs, uint a_offset) { + const uint vui = uint(data_a[a_offset + ib].qs[iqs]); + const float d = ue4m3_to_fp32(data_a[a_offset + ib].e); + return vec2(float(kvalues_rocmfp4[vui & 0xF]) * d, + float(kvalues_rocmfp4[vui >> 4]) * d); +} +vec4 dequantize4(uint ib, uint iqs, uint a_offset) { + const uint vui0 = uint(data_a[a_offset + ib].qs[iqs]); + const uint vui1 = uint(data_a[a_offset + ib].qs[iqs + 1]); + const float d = ue4m3_to_fp32(data_a[a_offset + ib].e); + return vec4(float(kvalues_rocmfp4[vui0 & 0xF]) * d, + float(kvalues_rocmfp4[vui0 >> 4]) * d, + float(kvalues_rocmfp4[vui1 & 0xF]) * d, + float(kvalues_rocmfp4[vui1 >> 4]) * d); +} +#endif + +#if defined(DATA_A_ROCMFPX_FP3) +uint rocmfpx_fp3_get_bits(uint ib, uint bit_pos, uint a_offset) { + uint code = 0u; + [[unroll]] for (uint bit = 0u; bit < 3u; ++bit) { + const uint src_bit = bit_pos + bit; + code |= ((uint(data_a[a_offset + ib].qs[src_bit >> 3u]) >> (src_bit & 7u)) & 1u) << bit; + } + return code; +} + +int rocmfpx_fp3_decode_code(uint code) { + return int(kvalues_rocmfpx_fp3_const[code & 7u]); +} + +float rocmfpx_fp3_dequant(uint ib, uint idx, uint a_offset) { + const float d = ue4m3_to_fp32(data_a[a_offset + ib].e[idx >= 16u ? 1u : 0u]); + return float(rocmfpx_fp3_decode_code(rocmfpx_fp3_get_bits(ib, idx * 3u, a_offset))) * d; +} + +int32_t rocmfpx_fp3_pack4_window(uint ib, uint idx, uint a_offset) { + const uint bit_pos = idx * 3u; + const uint byte_pos = bit_pos >> 3u; + const uint sh = bit_pos & 7u; + uint bits = uint(data_a[a_offset + ib].qs[byte_pos]) | + (uint(data_a[a_offset + ib].qs[byte_pos + 1u]) << 8); + if (sh > 4u) { + bits |= uint(data_a[a_offset + ib].qs[byte_pos + 2u]) << 16; + } + bits = (bits >> sh) & 0xFFFu; + return pack32(i8vec4(kvalues_rocmfpx_fp3_const[ bits & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 3) & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 6) & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 9) & 7u])); +} + +vec4 rocmfpx_fp3_dequant4(uint ib, uint idx, uint a_offset) { + const vec4 q = vec4(unpack8(rocmfpx_fp3_pack4_window(ib, idx, a_offset))); + return q * vec4(ue4m3_to_fp32(data_a[a_offset + ib].e[(idx + 0u) >= 16u ? 1u : 0u]), + ue4m3_to_fp32(data_a[a_offset + ib].e[(idx + 1u) >= 16u ? 1u : 0u]), + ue4m3_to_fp32(data_a[a_offset + ib].e[(idx + 2u) >= 16u ? 1u : 0u]), + ue4m3_to_fp32(data_a[a_offset + ib].e[(idx + 3u) >= 16u ? 1u : 0u])); +} + +vec2 dequantize(uint ib, uint iqs, uint a_offset) { + return vec2(rocmfpx_fp3_dequant(ib, iqs + 0u, a_offset), + rocmfpx_fp3_dequant(ib, iqs + 1u, a_offset)); +} + +vec4 dequantize4(uint ib, uint iqs, uint a_offset) { + return rocmfpx_fp3_dequant4(ib, iqs, a_offset); +} +#endif + +#if defined(DATA_A_ROCMFPX_FP6) +uint rocmfpx_fp6_get_bits(uint ib, uint idx, uint a_offset) { + const uint bit_pos = idx * 6u; + const uint byte_pos = bit_pos >> 3u; + const uint sh = bit_pos & 7u; + uint bits = uint(data_a[a_offset + ib].qs[byte_pos]); + if (sh > 2u) { + bits |= uint(data_a[a_offset + ib].qs[byte_pos + 1u]) << 8; + } + return (bits >> sh) & 0x3Fu; +} + +float rocmfpx_fp6_dequant(uint ib, uint idx, uint a_offset) { + const float d = ue4m3_to_fp32(data_a[a_offset + ib].e[idx >= 16u ? 1u : 0u]); + return float(rocmfpx_fp6_decode_code(rocmfpx_fp6_get_bits(ib, idx, a_offset))) * d; +} + +vec2 dequantize(uint ib, uint iqs, uint a_offset) { + return vec2(rocmfpx_fp6_dequant(ib, iqs + 0u, a_offset), + rocmfpx_fp6_dequant(ib, iqs + 1u, a_offset)); +} + +vec4 dequantize4(uint ib, uint iqs, uint a_offset) { + return vec4(rocmfpx_fp6_dequant(ib, iqs + 0u, a_offset), + rocmfpx_fp6_dequant(ib, iqs + 1u, a_offset), + rocmfpx_fp6_dequant(ib, iqs + 2u, a_offset), + rocmfpx_fp6_dequant(ib, iqs + 3u, a_offset)); +} +#endif + +#if defined(DATA_A_ROCMFPX_FP8) +vec2 dequantize(uint ib, uint iqs, uint a_offset) { + const float d = ue4m3_to_fp32(data_a[a_offset + ib].e); + return vec2(float(int(data_a[a_offset + ib].qs[iqs + 0u])) * d, + float(int(data_a[a_offset + ib].qs[iqs + 1u])) * d); +} +vec4 dequantize4(uint ib, uint iqs, uint a_offset) { + const float d = ue4m3_to_fp32(data_a[a_offset + ib].e); + return vec4(float(int(data_a[a_offset + ib].qs[iqs + 0u])) * d, + float(int(data_a[a_offset + ib].qs[iqs + 1u])) * d, + float(int(data_a[a_offset + ib].qs[iqs + 2u])) * d, + float(int(data_a[a_offset + ib].qs[iqs + 3u])) * d); +} +#endif + #if defined(DATA_A_NVFP4) vec2 dequantize(uint ib, uint iqs, uint a_offset) { const uint sub = iqs >> 4; @@ -577,6 +713,13 @@ vec2 get_dm(uint ib, uint a_offset) { } #endif +#if defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) || defined(DATA_A_ROCMFPX_FAMILY) +// the ROCm FP formats fold their UE4M3 scales into dequantize() directly +vec2 get_dm(uint ib, uint a_offset) { + return vec2(1, 0); +} +#endif + #if defined(DATA_A_NVFP4) vec2 get_dm(uint ib, uint a_offset) { return vec2(1.0, 0.0); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl index 46cc69cb26ed..0d729a25dbd0 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl @@ -1333,6 +1333,157 @@ f16vec4 dequantFuncMXFP4_v(const in decodeBufMXFP4 bl, const in uint blockCoords } #endif +#if defined(DATA_A_ROCMFP4) +layout(buffer_reference, std430, buffer_reference_align = 1) buffer decodeBufROCMFP4 { + block_rocmfp4 block; +}; + +float16_t dequantFuncROCMFP4(const in decodeBufROCMFP4 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const uint idx = coordInBlock[1]; + const uint iqs = idx & 0xF; + const uint shift = (idx & 0x10) >> 2; + const uint sub = idx >> 4; + const float d = ue4m3_to_fp32(bl.block.e[sub]); + uint32_t qs = bl.block.qs[iqs]; + qs >>= shift; + qs &= 0xF; + float16_t ret = float16_t(float(kvalues_rocmfp4[qs]) * d); + return ret; +} +#endif + +#if defined(DATA_A_ROCMFP4_FAST) +layout(buffer_reference, std430, buffer_reference_align = 1) buffer decodeBufROCMFP4Fast { + block_rocmfp4_fast block; +}; + +float16_t dequantFuncROCMFP4Fast(const in decodeBufROCMFP4Fast bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const uint idx = coordInBlock[1]; + const uint iqs = idx & 0xF; + const uint shift = (idx & 0x10) >> 2; + const float d = ue4m3_to_fp32(bl.block.e); + uint32_t qs = bl.block.qs[iqs]; + qs >>= shift; + qs &= 0xF; + float16_t ret = float16_t(float(kvalues_rocmfp4[qs]) * d); + return ret; +} +#endif + +#if defined(DATA_A_ROCMFPX_FP3) +layout(buffer_reference, std430, buffer_reference_align = 1) buffer decodeBufROCMFPXFP3 { + block_rocmfpx_fp3 block; +}; + +uint rocmfpx_cm2_fp3_get_bits(const in decodeBufROCMFPXFP3 bl, uint bit_pos) +{ + uint code = 0u; + [[unroll]] for (uint bit = 0u; bit < 3u; ++bit) { + const uint src_bit = bit_pos + bit; + code |= ((uint(bl.block.qs[src_bit >> 3u]) >> (src_bit & 7u)) & 1u) << bit; + } + return code; +} + +int rocmfpx_cm2_fp3_decode(uint code) +{ + return int(kvalues_rocmfpx_fp3_const[code & 7u]); +} + +float16_t dequantFuncROCMFPXFP3(const in decodeBufROCMFPXFP3 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const uint idx = coordInBlock[1]; + const float d = ue4m3_to_fp32(bl.block.e[idx >= 16u ? 1u : 0u]); + return float16_t(float(rocmfpx_cm2_fp3_decode(rocmfpx_cm2_fp3_get_bits(bl, idx * 3u))) * d); +} + +int32_t rocmfpx_cm2_fp3_pack4_window(const in decodeBufROCMFPXFP3 bl, uint idx) +{ + const uint bit_pos = idx * 3u; + const uint byte_pos = bit_pos >> 3u; + const uint sh = bit_pos & 7u; + uint bits = uint(bl.block.qs[byte_pos]) | + (uint(bl.block.qs[byte_pos + 1u]) << 8); + if (sh > 4u) { + bits |= uint(bl.block.qs[byte_pos + 2u]) << 16; + } + bits = (bits >> sh) & 0xFFFu; + return pack32(i8vec4(kvalues_rocmfpx_fp3_const[ bits & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 3) & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 6) & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 9) & 7u])); +} + +f16vec4 dequantFuncROCMFPXFP3_v(const in decodeBufROCMFPXFP3 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const uint idx = coordInBlock[1]; + const vec4 q = vec4(unpack8(rocmfpx_cm2_fp3_pack4_window(bl, idx))); + return f16vec4(q * vec4(ue4m3_to_fp32(bl.block.e[(idx + 0u) >= 16u ? 1u : 0u]), + ue4m3_to_fp32(bl.block.e[(idx + 1u) >= 16u ? 1u : 0u]), + ue4m3_to_fp32(bl.block.e[(idx + 2u) >= 16u ? 1u : 0u]), + ue4m3_to_fp32(bl.block.e[(idx + 3u) >= 16u ? 1u : 0u]))); +} +#endif + +#if defined(DATA_A_ROCMFPX_FP6) +layout(buffer_reference, std430, buffer_reference_align = 1) buffer decodeBufROCMFPXFP6 { + block_rocmfpx_fp6 block; +}; + +uint rocmfpx_cm2_fp6_get_bits(const in decodeBufROCMFPXFP6 bl, uint idx) +{ + const uint bit_pos = idx * 6u; + const uint byte_pos = bit_pos >> 3u; + const uint sh = bit_pos & 7u; + uint bits = uint(bl.block.qs[byte_pos]); + if (sh > 2u) { + bits |= uint(bl.block.qs[byte_pos + 1u]) << 8; + } + return (bits >> sh) & 0x3Fu; +} + +float16_t dequantFuncROCMFPXFP6(const in decodeBufROCMFPXFP6 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const uint idx = coordInBlock[1]; + const float d = ue4m3_to_fp32(bl.block.e[idx >= 16u ? 1u : 0u]); + return float16_t(float(rocmfpx_fp6_decode_code(rocmfpx_cm2_fp6_get_bits(bl, idx))) * d); +} + +f16vec4 dequantFuncROCMFPXFP6_v(const in decodeBufROCMFPXFP6 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const uint idx = coordInBlock[1]; + return f16vec4(float16_t(float(rocmfpx_fp6_decode_code(rocmfpx_cm2_fp6_get_bits(bl, idx + 0u))) * ue4m3_to_fp32(bl.block.e[(idx + 0u) >= 16u ? 1u : 0u])), + float16_t(float(rocmfpx_fp6_decode_code(rocmfpx_cm2_fp6_get_bits(bl, idx + 1u))) * ue4m3_to_fp32(bl.block.e[(idx + 1u) >= 16u ? 1u : 0u])), + float16_t(float(rocmfpx_fp6_decode_code(rocmfpx_cm2_fp6_get_bits(bl, idx + 2u))) * ue4m3_to_fp32(bl.block.e[(idx + 2u) >= 16u ? 1u : 0u])), + float16_t(float(rocmfpx_fp6_decode_code(rocmfpx_cm2_fp6_get_bits(bl, idx + 3u))) * ue4m3_to_fp32(bl.block.e[(idx + 3u) >= 16u ? 1u : 0u]))); +} +#endif + +#if defined(DATA_A_ROCMFPX_FP8) +layout(buffer_reference, std430, buffer_reference_align = 1) buffer decodeBufROCMFPXFP8 { + block_rocmfpx_fp8 block; +}; + +float16_t dequantFuncROCMFPXFP8(const in decodeBufROCMFPXFP8 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const uint idx = coordInBlock[1]; + const float d = ue4m3_to_fp32(bl.block.e); + return float16_t(float(int(bl.block.qs[idx])) * d); +} + +f16vec4 dequantFuncROCMFPXFP8_v(const in decodeBufROCMFPXFP8 bl, const in uint blockCoords[2], const in uint coordInBlock[2]) +{ + const uint idx = coordInBlock[1]; + const float d = ue4m3_to_fp32(bl.block.e); + return f16vec4(vec4(float(int(bl.block.qs[idx + 0u])), + float(int(bl.block.qs[idx + 1u])), + float(int(bl.block.qs[idx + 2u])), + float(int(bl.block.qs[idx + 3u]))) * d); +} +#endif + #if defined(DATA_A_NVFP4) layout(buffer_reference, std430, buffer_reference_align = 4) buffer decodeBufNVFP4 { block_nvfp4 block; @@ -1458,6 +1609,19 @@ f16vec4 dequantFuncNVFP4_v(const in decodeBufNVFP4 bl, const in uint blockCoords #elif defined(DATA_A_MXFP4) #define dequantFuncA dequantFuncMXFP4 #define dequantFuncA_v dequantFuncMXFP4_v +#elif defined(DATA_A_ROCMFP4) +#define dequantFuncA dequantFuncROCMFP4 +#elif defined(DATA_A_ROCMFP4_FAST) +#define dequantFuncA dequantFuncROCMFP4Fast +#elif defined(DATA_A_ROCMFPX_FP3) +#define dequantFuncA dequantFuncROCMFPXFP3 +#define dequantFuncA_v dequantFuncROCMFPXFP3_v +#elif defined(DATA_A_ROCMFPX_FP6) +#define dequantFuncA dequantFuncROCMFPXFP6 +#define dequantFuncA_v dequantFuncROCMFPXFP6_v +#elif defined(DATA_A_ROCMFPX_FP8) +#define dequantFuncA dequantFuncROCMFPXFP8 +#define dequantFuncA_v dequantFuncROCMFPXFP8_v #elif defined(DATA_A_NVFP4) #define dequantFuncA dequantFuncNVFP4 #define dequantFuncA_v dequantFuncNVFP4_v diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfp4.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfp4.comp new file mode 100644 index 000000000000..46cc1bd7dc5b --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfp4.comp @@ -0,0 +1,33 @@ +#version 450 + +#include "dequant_head.glsl" + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {block_rocmfp4 data_a[];}; +layout (binding = 1) writeonly buffer D {D_TYPE data_b[];}; + +void main() { + const uint i = gl_WorkGroupID.x * 4 + gl_LocalInvocationID.x / 64; + + init_iq_shmem(gl_WorkGroupSize); + + const uint tid = gl_LocalInvocationID.x % 64; + const uint il = tid/32; + const uint ir = tid%32; + const uint ib = 32*i + ir; + if (ib >= p.nel / 32) { + return; + } + + const uint q_idx = 8*il; + const uint b_idx = 1024*i + 32*ir + q_idx; + + const float d0 = ue4m3_to_fp32(data_a[ib].e[0]); + const float d1 = ue4m3_to_fp32(data_a[ib].e[1]); + + [[unroll]] for (uint l = 0; l < 8; ++l) { + data_b[b_idx + l + 0] = D_TYPE(d0 * float(kvalues_rocmfp4[data_a[ib].qs[q_idx + l] & 0xF])); + data_b[b_idx + l + 16] = D_TYPE(d1 * float(kvalues_rocmfp4[data_a[ib].qs[q_idx + l] >> 4])); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfp4_fast.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfp4_fast.comp new file mode 100644 index 000000000000..360ff679f398 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfp4_fast.comp @@ -0,0 +1,31 @@ +#version 450 + +#include "dequant_head.glsl" + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {block_rocmfp4_fast data_a[];}; +layout (binding = 1) writeonly buffer D {D_TYPE data_b[];}; + +void main() { + const uint i = gl_WorkGroupID.x * 4 + gl_LocalInvocationID.x / 64; + + init_iq_shmem(gl_WorkGroupSize); + + const uint tid = gl_LocalInvocationID.x % 64; + const uint il = tid/32; + const uint ir = tid%32; + const uint ib = 32*i + ir; + if (ib >= p.nel / 32) { + return; + } + + const uint q_idx = 8*il; + const uint b_idx = 1024*i + 32*ir + q_idx; + const float d = ue4m3_to_fp32(data_a[ib].e); + + [[unroll]] for (uint l = 0; l < 8; ++l) { + data_b[b_idx + l + 0] = D_TYPE(d * float(kvalues_rocmfp4[data_a[ib].qs[q_idx + l] & 0xF])); + data_b[b_idx + l + 16] = D_TYPE(d * float(kvalues_rocmfp4[data_a[ib].qs[q_idx + l] >> 4])); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp3.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp3.comp new file mode 100644 index 000000000000..e3aab966d769 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp3.comp @@ -0,0 +1,46 @@ +#version 450 + +#include "dequant_head.glsl" + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {block_rocmfpx_fp3 data_a[];}; +layout (binding = 1) writeonly buffer D {D_TYPE data_b[];}; + +uint rocmfpx_fp3_get_bits(uint ib, uint bit_pos) { + uint code = 0u; + [[unroll]] for (uint bit = 0u; bit < 3u; ++bit) { + const uint src_bit = bit_pos + bit; + code |= ((uint(data_a[ib].qs[src_bit >> 3u]) >> (src_bit & 7u)) & 1u) << bit; + } + return code; +} + +int rocmfpx_fp3_decode_code(uint code) { + const uint mag_code = code & 3u; + const int mag = mag_code == 3u ? 4 : int(mag_code); + return (code & 4u) != 0u ? -mag : mag; +} + +void main() { + const uint i = gl_WorkGroupID.x * 4 + gl_LocalInvocationID.x / 64; + + init_iq_shmem(gl_WorkGroupSize); + + const uint tid = gl_LocalInvocationID.x % 64; + const uint il = tid/32; + const uint ir = tid%32; + const uint ib = 32*i + ir; + if (ib >= p.nel / 32) { + return; + } + + const uint b_idx = 1024*i + 32*ir + 16*il; + const uint q_idx = 16*il; + const float d = ue4m3_to_fp32(data_a[ib].e[il]); + + [[unroll]] for (uint l = 0; l < 16; ++l) { + const uint idx = q_idx + l; + data_b[b_idx + l] = D_TYPE(d * float(rocmfpx_fp3_decode_code(rocmfpx_fp3_get_bits(ib, idx * 3u)))); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp6.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp6.comp new file mode 100644 index 000000000000..5da0e9e8cf98 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp6.comp @@ -0,0 +1,44 @@ +#version 450 + +#include "dequant_head.glsl" + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {block_rocmfpx_fp6 data_a[];}; +layout (binding = 1) writeonly buffer D {D_TYPE data_b[];}; + +uint rocmfpx_fp6_get_bits(uint ib, uint idx) { + const uint bit_pos = idx * 6u; + const uint byte_pos = bit_pos >> 3u; + const uint sh = bit_pos & 7u; + uint bits = uint(data_a[ib].qs[byte_pos]); + if (sh > 2u) { + bits |= uint(data_a[ib].qs[byte_pos + 1u]) << 8; + } + return (bits >> sh) & 0x3Fu; +} + +void main() { + const uint i = gl_WorkGroupID.x * 4 + gl_LocalInvocationID.x / 64; + +#ifdef NEEDS_INIT_IQ_SHMEM + init_iq_shmem(gl_WorkGroupSize); +#endif + + const uint tid = gl_LocalInvocationID.x % 64; + const uint il = tid/32; + const uint ir = tid%32; + const uint ib = 32*i + ir; + if (ib >= p.nel / 32) { + return; + } + + const uint b_idx = 1024*i + 32*ir + 16*il; + const uint q_idx = 16*il; + const float d = ue4m3_to_fp32(data_a[ib].e[il]); + + [[unroll]] for (uint l = 0; l < 16; ++l) { + const uint idx = q_idx + l; + data_b[b_idx + l] = D_TYPE(d * float(rocmfpx_fp6_decode_code(rocmfpx_fp6_get_bits(ib, idx)))); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp8.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp8.comp new file mode 100644 index 000000000000..7052de542470 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_rocmfpx_fp8.comp @@ -0,0 +1,31 @@ +#version 450 + +#include "dequant_head.glsl" + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {block_rocmfpx_fp8 data_a[];}; +layout (binding = 1) writeonly buffer D {D_TYPE data_b[];}; + +void main() { + const uint i = gl_WorkGroupID.x * 4 + gl_LocalInvocationID.x / 64; + + init_iq_shmem(gl_WorkGroupSize); + + const uint tid = gl_LocalInvocationID.x % 64; + const uint il = tid/32; + const uint ir = tid%32; + const uint ib = 32*i + ir; + if (ib >= p.nel / 32) { + return; + } + + const uint b_idx = 1024*i + 32*ir + 16*il; + const uint q_idx = 16*il; + const float d = ue4m3_to_fp32(data_a[ib].e); + + [[unroll]] for (uint l = 0; l < 16; l += 2) { + data_b[b_idx + l ] = D_TYPE(d * float(data_a[ib].qs[q_idx + l ])); + data_b[b_idx + l + 1] = D_TYPE(d * float(data_a[ib].qs[q_idx + l + 1])); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl index 7d852dced8ab..9ea477e896f9 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl @@ -17,6 +17,54 @@ void store_a(uint m, uint k_pair, FLOAT_TYPEV2 value) { buf_a[a_shmem_index(m, k_pair)] = value; } +#if defined(DATA_A_ROCMFPX_FP3) +int32_t rocmfpx_mm_fp3_pack4_window(uint ib, uint idx) { + const uint bit_pos = idx * 3u; + const uint byte_pos = bit_pos >> 3u; + const uint sh = bit_pos & 7u; + uint bits = uint(data_a[ib].qs[byte_pos]) | + (uint(data_a[ib].qs[byte_pos + 1u]) << 8); + if (sh > 4u) { + bits |= uint(data_a[ib].qs[byte_pos + 2u]) << 16; + } + bits = (bits >> sh) & 0xFFFu; + return pack32(i8vec4(kvalues_rocmfpx_fp3_const[ bits & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 3) & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 6) & 7u], + kvalues_rocmfpx_fp3_const[(bits >> 9) & 7u])); +} + +vec4 rocmfpx_mm_fp3_vec4(uint ib, uint idx) { + const float d = ue4m3_to_fp32(data_a[ib].e[idx >= 16u ? 1u : 0u]); + return vec4(unpack8(rocmfpx_mm_fp3_pack4_window(ib, idx))) * d; +} +#endif + +#if defined(DATA_A_ROCMFPX_FP6) +uint rocmfpx_mm_fp6_get_bits(uint ib, uint idx) { + const uint bit_pos = idx * 6u; + const uint byte_pos = bit_pos >> 3u; + const uint sh = bit_pos & 7u; + uint bits = uint(data_a[ib].qs[byte_pos]); + if (sh > 2u) { + bits |= uint(data_a[ib].qs[byte_pos + 1u]) << 8; + } + return (bits >> sh) & 0x3Fu; +} + +float rocmfpx_mm_fp6_value(uint ib, uint idx) { + const float d = ue4m3_to_fp32(data_a[ib].e[idx >= 16u ? 1u : 0u]); + return float(rocmfpx_fp6_decode_code(rocmfpx_mm_fp6_get_bits(ib, idx))) * d; +} + +vec4 rocmfpx_mm_fp6_vec4(uint ib, uint idx) { + return vec4(rocmfpx_mm_fp6_value(ib, idx + 0u), + rocmfpx_mm_fp6_value(ib, idx + 1u), + rocmfpx_mm_fp6_value(ib, idx + 2u), + rocmfpx_mm_fp6_value(ib, idx + 3u)); +} +#endif + void load_a_to_shmem(const uint pos_a, const uint row, const uint col, const uint idx_m, const uint block, const uint end_k) { #if defined(DATA_A_F32) || defined(DATA_A_F16) #if LOAD_VEC_A == 8 @@ -557,6 +605,63 @@ void load_a_to_shmem(const uint pos_a, const uint row, const uint col, const uin store_a(col, row + 8, FLOAT_TYPEV2(kvalues_mxfp4[vui >> 4] * d, kvalues_mxfp4[vui2 >> 4] * d)); #endif +#elif defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) + const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; + + const uint ib = idx / 8; + const uint iqs = (idx & 0x07) * 2; + + const uint vui = uint(data_a[ib].qs[iqs]); + const uint vui2 = uint(data_a[ib].qs[iqs+1]); + + // low nibbles hold the first half of the block (scale e[0]), + // high nibbles the second half (scale e[1]) +#if defined(DATA_A_ROCMFP4) + const float d0 = ue4m3_to_fp32(data_a[ib].e[0]); + const float d1 = ue4m3_to_fp32(data_a[ib].e[1]); +#else + const float d0 = ue4m3_to_fp32(data_a[ib].e); + const float d1 = d0; +#endif + store_a(col, row, FLOAT_TYPEV2(float(kvalues_rocmfp4[vui & 0xF]) * d0, + float(kvalues_rocmfp4[vui2 & 0xF]) * d0)); + store_a(col, row + 8, FLOAT_TYPEV2(float(kvalues_rocmfp4[vui >> 4]) * d1, + float(kvalues_rocmfp4[vui2 >> 4]) * d1)); +#elif defined(DATA_A_ROCMFPX_FP3) + const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; + + const uint ib = idx / 8; + const uint iqs = (idx & 0x07) * 4; + const vec4 v = rocmfpx_mm_fp3_vec4(ib, iqs); + + const uint k_pair = row * LOAD_VEC_A / 2; + store_a(col, k_pair, FLOAT_TYPEV2(v.xy)); + store_a(col, k_pair + 1, FLOAT_TYPEV2(v.zw)); +#elif defined(DATA_A_ROCMFPX_FP6) + const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; + + const uint ib = idx / 8; + const uint iqs = (idx & 0x07) * 4; + const vec4 v = rocmfpx_mm_fp6_vec4(ib, iqs); + + const uint k_pair = row * LOAD_VEC_A / 2; + store_a(col, k_pair, FLOAT_TYPEV2(v.xy)); + store_a(col, k_pair + 1, FLOAT_TYPEV2(v.zw)); +#elif defined(DATA_A_ROCMFPX_FP8) + const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; + + const uint ib = idx / 8; + const uint iqs = (idx & 0x07) * 4; + + const float d = ue4m3_to_fp32(data_a[ib].e); + const vec4 v = vec4(float(data_a[ib].qs[iqs + 0u]), + float(data_a[ib].qs[iqs + 1u]), + float(data_a[ib].qs[iqs + 2u]), + float(data_a[ib].qs[iqs + 3u])) * d; + + const uint k_pair = row * LOAD_VEC_A / 2; + store_a(col, k_pair, FLOAT_TYPEV2(v.xy)); + store_a(col, k_pair + 1, FLOAT_TYPEV2(v.zw)); #elif defined(DATA_A_NVFP4) const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; const uint ib = idx / 16u; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl index adb1bb8b32b5..983b07320657 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl @@ -1804,6 +1804,87 @@ struct block_nvfp4_packed32 #define A_TYPE_PACKED32 block_nvfp4_packed32 #endif +#define QUANT_K_ROCMFP4 32 +#define QUANT_R_ROCMFP4 2 + +struct block_rocmfp4 +{ + uint8_t qs[QUANT_K_ROCMFP4/2]; + uint8_t e[2]; +}; + +struct block_rocmfp4_fast +{ + uint8_t qs[QUANT_K_ROCMFP4/2]; + uint8_t e; +}; + +#if defined(DATA_A_ROCMFP4) +#define QUANT_K QUANT_K_ROCMFP4 +#define QUANT_R QUANT_R_ROCMFP4 +#define QUANT_AUXF 1 +#define A_TYPE block_rocmfp4 +#endif + +#if defined(DATA_A_ROCMFP4_FAST) +#define QUANT_K QUANT_K_ROCMFP4 +#define QUANT_R QUANT_R_ROCMFP4 +#define QUANT_AUXF 1 +#define A_TYPE block_rocmfp4_fast +#endif + +#define QUANT_K_ROCMFPX_FP8 32 +#define QUANT_R_ROCMFPX_FP8 1 + +struct block_rocmfpx_fp3 +{ + uint8_t qs[12]; + uint8_t e[2]; +}; + +// 32 codes of 6 bits, little-endian packed into 24 bytes, plus two UE4M3 +// half-block scales. Matches block_rocmfp6 in ggml/rocmfpx/rocmfpx.h. +struct block_rocmfpx_fp6 +{ + uint8_t qs[24]; + uint8_t e[2]; +}; + +struct block_rocmfpx_fp8 +{ + int8_t qs[QUANT_K_ROCMFPX_FP8]; + uint8_t e; +}; + +#if defined(DATA_A_ROCMFPX_FP3) +#define QUANT_K QUANT_K_ROCMFPX_FP8 +#define QUANT_R QUANT_R_ROCMFPX_FP8 +#define QUANT_AUXF 1 +#define A_TYPE block_rocmfpx_fp3 +#endif + +#if defined(DATA_A_ROCMFPX_FP6) +#define QUANT_K QUANT_K_ROCMFPX_FP8 +#define QUANT_R QUANT_R_ROCMFPX_FP8 +#define QUANT_AUXF 1 +#define A_TYPE block_rocmfpx_fp6 +#endif + +#if defined(DATA_A_ROCMFPX_FP8) +#define QUANT_K QUANT_K_ROCMFPX_FP8 +#define QUANT_R QUANT_R_ROCMFPX_FP8 +#define QUANT_AUXF 1 +#define A_TYPE block_rocmfpx_fp8 +#endif + +#if defined(DATA_A_ROCMFPX_FP3) || defined(DATA_A_ROCMFPX_FP6) || defined(DATA_A_ROCMFPX_FP8) +#define DATA_A_ROCMFPX_FAMILY +#endif + +#if defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) || defined(DATA_A_ROCMFPX_FAMILY) +#define DATA_A_ROCMFP_ANY +#endif + #if defined(DATA_A_IQ4_NL) || defined(DATA_A_IQ4_XS) const int8_t kvalues_iq4nl_const[16] = { int8_t(-127), int8_t(-104), int8_t(-83), int8_t(-65), int8_t(-49), int8_t(-35), int8_t(-22), int8_t(-10), @@ -1823,6 +1904,71 @@ void init_iq_shmem(uvec3 wgsize) } #endif +#if defined(DATA_A_ROCMFP_ANY) || defined(FA_ROCMFPX_FAMILY) +// The ROCm FP formats share one UE4M3 scale encoding. It differs from the +// NVFP4 encoding: the exponent bias is one lower and subnormals are scaled by +// 1/1024, which keeps the halved ROCmFP4 codebook multiply-free at call sites. +shared float rocmfp_ue4m3_fp32_lut[128]; + +float rocmfp_ue4m3_to_fp32_build(uint u) { + if (u == 0u || u == 127u) { + return 0.0; + } + const uint exp = (u >> 3) & 15u; + const uint man = u & 7u; + if (exp == 0u) { + return float(man) * (1.0 / 1024.0); + } + const uint bits = (exp + 119u) << 23 | (man << 20); + return uintBitsToFloat(bits); +} + +float ue4m3_to_fp32(uint8_t x) { + return rocmfp_ue4m3_fp32_lut[min(uint(x), 127u)]; +} +#endif + +#if defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) +// E2M1-derived codebook with the top level retuned from 12 to 10. +const int8_t kvalues_rocmfp4_const[16] = { + int8_t(0), int8_t(1), int8_t(2), int8_t(3), int8_t(4), int8_t(6), int8_t(8), int8_t(10), + int8_t(0), int8_t(-1), int8_t(-2), int8_t(-3), int8_t(-4), int8_t(-6), int8_t(-8), int8_t(-10), +}; + +shared int8_t kvalues_rocmfp4[16]; +#endif + +#if defined(DATA_A_ROCMFPX_FP6) +int rocmfpx_fp6_decode_code(uint code) { + const int mag = int(code & 31u); + return (code & 32u) != 0u ? -(mag == 0 ? 32 : mag) : mag; +} +#endif + +#if defined(DATA_A_ROCMFPX_FAMILY) +const int8_t kvalues_rocmfpx_fp3_const[8] = { + int8_t(0), int8_t(1), int8_t(2), int8_t(4), + int8_t(0), int8_t(-1), int8_t(-2), int8_t(-4) +}; +#endif + +#if defined(DATA_A_ROCMFP_ANY) +#define NEEDS_INIT_IQ_SHMEM +void init_iq_shmem(uvec3 wgsize) +{ + // copy the tables into shared memory and sync +#if defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) + for (uint i = gl_LocalInvocationIndex.x; i < kvalues_rocmfp4.length(); i += wgsize.x) { + kvalues_rocmfp4[i] = kvalues_rocmfp4_const[i]; + } +#endif + for (uint i = gl_LocalInvocationIndex.x; i < rocmfp_ue4m3_fp32_lut.length(); i += wgsize.x) { + rocmfp_ue4m3_fp32_lut[i] = rocmfp_ue4m3_to_fp32_build(i); + } + barrier(); +} +#endif + #if defined(DATA_A_MXFP4) || defined(DATA_A_NVFP4) #if !defined(USE_OCP_FP4) const int8_t kvalues_mxfp4_const[16] = { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 27ff68c10d5b..dede9ad041b2 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -71,6 +71,11 @@ const std::vector type_names = { "iq4_xs", "iq4_nl", "mxfp4", + "rocmfp4", + "rocmfp4_fast", + "rocmfpx_fp3", + "rocmfpx_fp6", + "rocmfpx_fp8", "nvfp4", "tq2_0", "bf16", @@ -585,7 +590,9 @@ void matmul_shaders(bool fp16, MatMulIdType matmul_id_type, bool coopmat, bool c std::string load_vec_quant = "2"; if ((tname == "q1_0") || (tname == "q4_0") || (tname == "q4_1") || (tname == "q5_1") || (tname == "iq1_s") || (tname == "iq1_m") || (tname == "iq2_xxs") || (tname == "iq2_xs") || (tname == "iq2_s")) load_vec_quant = "8"; - else if ((tname == "q2_0") || (tname == "q5_0") || (tname == "q8_0") || (tname == "q2_k") || (tname == "q4_k") || (tname == "q5_k") || (tname == "iq3_xxs") || (tname == "iq3_s") || (tname == "iq4_xs") || (tname == "iq4_nl") || (tname == "mxfp4") || (tname == "nvfp4")) + else if ((tname == "q2_0") || (tname == "q5_0") || (tname == "q8_0") || (tname == "q2_k") || (tname == "q4_k") || (tname == "q5_k") || (tname == "iq3_xxs") || (tname == "iq3_s") || (tname == "iq4_xs") || (tname == "iq4_nl") || (tname == "mxfp4") || (tname == "nvfp4") || + (tname == "rocmfp4") || (tname == "rocmfp4_fast") || + (tname == "rocmfpx_fp3") || (tname == "rocmfpx_fp6") || (tname == "rocmfpx_fp8")) load_vec_quant = "4"; if (tname == "bf16") { @@ -833,13 +840,21 @@ void process_shaders() { string_to_spv("cpy_transpose_02_16", "copy_transpose_02.comp", {{"A_TYPE", "uint16_t"}, {"D_TYPE", "uint16_t"}}); string_to_spv("cpy_transpose_02_32", "copy_transpose_02.comp", {{"A_TYPE", "uint"}, {"D_TYPE", "uint"}}); - for (std::string t : {"q1_0", "q2_0", "q4_0", "q4_1", "q5_0", "q5_1", "q8_0", "iq4_nl"}) { + string_to_spv("cpy_rocmfp4_rocmfp4", "copy_quant_same.comp", {{"A_TYPE", "uint8_t"}, {"D_TYPE", "uint8_t"}, {"QUANT_K", "32"}, {"BLOCK_BYTES", "18"}}); + string_to_spv("cpy_rocmfp4_fast_rocmfp4_fast", "copy_quant_same.comp", {{"A_TYPE", "uint8_t"}, {"D_TYPE", "uint8_t"}, {"QUANT_K", "32"}, {"BLOCK_BYTES", "17"}}); + string_to_spv("cpy_rocmfpx_fp3_rocmfpx_fp3", "copy_quant_same.comp", {{"A_TYPE", "uint8_t"}, {"D_TYPE", "uint8_t"}, {"QUANT_K", "32"}, {"BLOCK_BYTES", "14"}}); + string_to_spv("cpy_rocmfpx_fp6_rocmfpx_fp6", "copy_quant_same.comp", {{"A_TYPE", "uint8_t"}, {"D_TYPE", "uint8_t"}, {"QUANT_K", "32"}, {"BLOCK_BYTES", "26"}}); + string_to_spv("cpy_rocmfpx_fp8_rocmfpx_fp8", "copy_quant_same.comp", {{"A_TYPE", "uint8_t"}, {"D_TYPE", "uint8_t"}, {"QUANT_K", "32"}, {"BLOCK_BYTES", "33"}}); + + for (std::string t : {"q1_0", "q2_0", "q4_0", "q4_1", "q5_0", "q5_1", "q8_0", "iq4_nl", + "rocmfp4", "rocmfp4_fast", "rocmfpx_fp3", "rocmfpx_fp6", "rocmfpx_fp8"}) { string_to_spv("cpy_f32_" + t, "copy_to_quant.comp", {{"DATA_A_" + to_uppercase(t), "1"}, {"S_TYPE", "float"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}}); string_to_spv("cpy_" + t + "_f32", "copy_from_quant.comp", {{"DATA_A_" + to_uppercase(t), "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}}); } for (auto src : {std::pair{"f32", "float"}, std::pair{"f16", "float16_t"}}) { - for (std::string dst : {"f32", "f16", "bf16", "q1_0", "q2_0", "q4_0", "q4_1", "q5_0", "q5_1", "q8_0", "iq4_nl"}) { + for (std::string dst : {"f32", "f16", "bf16", "q1_0", "q2_0", "q4_0", "q4_1", "q5_0", "q5_1", "q8_0", "iq4_nl", + "rocmfp4", "rocmfp4_fast", "rocmfpx_fp3", "rocmfpx_fp6", "rocmfpx_fp8"}) { string_to_spv("set_rows_" + std::string(src.first) + "_" + dst + "_i32", "copy_to_quant.comp", {{"SET_ROWS", "1"}, {"DATA_A_" + to_uppercase(dst), "1"}, {"B_TYPE", "uint"}, {"B_SIZE", "32"}, {"S_TYPE", src.second}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}}); string_to_spv("set_rows_" + std::string(src.first) + "_" + dst + "_i64", "copy_to_quant.comp", {{"SET_ROWS", "1"}, {"DATA_A_" + to_uppercase(dst), "1"}, {"B_TYPE", "uvec2"}, {"B_SIZE", "64"}, {"S_TYPE", src.second}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}}); } diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c index 6257cdbe5821..5fcb6c2f300a 100644 --- a/ggml/src/ggml.c +++ b/ggml/src/ggml.c @@ -11,6 +11,10 @@ // FIXME: required here for quantization functions #include "ggml-quants.h" +// ROCmFPx experimental AMD-native quant formats +#include "../rocmfp4/rocmfp4.h" +#include "../rocmfpx/rocmfpx.h" + #ifdef GGML_USE_CPU_HBM #include #endif @@ -698,6 +702,54 @@ static const struct ggml_type_traits type_traits[GGML_TYPE_COUNT] = { .to_float = (ggml_to_float_t) dequantize_row_q4_0, .from_float_ref = (ggml_from_float_t) quantize_row_q4_0_ref, }, + [GGML_TYPE_Q4_0_ROCMFP4] = { + .type_name = "q4_0_rocmfp4", + .blck_size = QK_ROCMFP4, + .type_size = sizeof(block_rocmfp4), + .is_quantized = true, + .to_float = (ggml_to_float_t) rocmfp4_dequantize_row_q4_0, + .from_float_ref = (ggml_from_float_t) rocmfp4_quantize_row_q4_0_ref, + }, + [GGML_TYPE_Q4_0_ROCMFP4_FAST] = { + .type_name = "q4_0_rocmfp4_fast", + .blck_size = QK_ROCMFP4, + .type_size = sizeof(block_rocmfp4_fast), + .is_quantized = true, + .to_float = (ggml_to_float_t) rocmfp4_dequantize_row_q4_0_fast, + .from_float_ref = (ggml_from_float_t) rocmfp4_quantize_row_q4_0_fast_ref, + }, + [GGML_TYPE_Q3_0_ROCMFPX] = { + .type_name = "q3_0_rocmfpx", + .blck_size = QK_ROCMFP3, + .type_size = sizeof(block_rocmfp3), + .is_quantized = true, + .to_float = (ggml_to_float_t) rocmfpx_dequantize_row_fp3, + .from_float_ref = (ggml_from_float_t) rocmfpx_quantize_row_fp3_ref, + }, + [GGML_TYPE_Q2_0_ROCMFPX] = { + .type_name = "q2_0_rocmfpx", + .blck_size = QK_ROCMFP2, + .type_size = sizeof(block_rocmfp2), + .is_quantized = true, + .to_float = (ggml_to_float_t) rocmfpx_dequantize_row_fp2, + .from_float_ref = (ggml_from_float_t) rocmfpx_quantize_row_fp2_ref, + }, + [GGML_TYPE_Q6_0_ROCMFPX] = { + .type_name = "q6_0_rocmfpx", + .blck_size = QK_ROCMFP6, + .type_size = sizeof(block_rocmfp6), + .is_quantized = true, + .to_float = (ggml_to_float_t) rocmfpx_dequantize_row_fp6, + .from_float_ref = (ggml_from_float_t) rocmfpx_quantize_row_fp6_ref, + }, + [GGML_TYPE_Q8_0_ROCMFPX] = { + .type_name = "q8_0_rocmfpx", + .blck_size = QK_ROCMFP8, + .type_size = sizeof(block_rocmfp8), + .is_quantized = true, + .to_float = (ggml_to_float_t) rocmfpx_dequantize_row_fp8, + .from_float_ref = (ggml_from_float_t) rocmfpx_quantize_row_fp8_ref, + }, [GGML_TYPE_Q4_1] = { .type_name = "q4_1", .blck_size = QK4_1, @@ -1435,6 +1487,19 @@ enum ggml_type ggml_ftype_to_ggml_type(enum ggml_ftype ftype) { case GGML_FTYPE_MOSTLY_Q4_1: wtype = GGML_TYPE_Q4_1; break; case GGML_FTYPE_MOSTLY_Q1_0: wtype = GGML_TYPE_Q1_0; break; case GGML_FTYPE_MOSTLY_Q2_0: wtype = GGML_TYPE_Q2_0; break; + case GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4: + case GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN: + case GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_COHERENT: + wtype = GGML_TYPE_Q4_0_ROCMFP4; break; + case GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST: + case GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT: + case GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX: + case GGML_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN: + wtype = GGML_TYPE_Q4_0_ROCMFP4_FAST; break; + case GGML_FTYPE_MOSTLY_Q3_0_ROCMFPX: wtype = GGML_TYPE_Q3_0_ROCMFPX; break; + case GGML_FTYPE_MOSTLY_Q2_0_ROCMFPX: wtype = GGML_TYPE_Q2_0_ROCMFPX; break; + case GGML_FTYPE_MOSTLY_Q6_0_ROCMFPX: wtype = GGML_TYPE_Q6_0_ROCMFPX; break; + case GGML_FTYPE_MOSTLY_Q8_0_ROCMFPX: wtype = GGML_TYPE_Q8_0_ROCMFPX; break; case GGML_FTYPE_MOSTLY_Q5_0: wtype = GGML_TYPE_Q5_0; break; case GGML_FTYPE_MOSTLY_Q5_1: wtype = GGML_TYPE_Q5_1; break; case GGML_FTYPE_MOSTLY_Q8_0: wtype = GGML_TYPE_Q8_0; break; @@ -8003,6 +8068,24 @@ size_t ggml_quantize_chunk( case GGML_TYPE_Q1_0: result = quantize_q1_0 (src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_Q2_0: result = quantize_q2_0 (src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_Q4_0: result = quantize_q4_0 (src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; + case GGML_TYPE_Q4_0_ROCMFP4: + result = rocmfp4_quantize_q4_0(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); + break; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + result = rocmfp4_quantize_q4_0_fast(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); + break; + case GGML_TYPE_Q3_0_ROCMFPX: + result = rocmfpx_quantize_fp3(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); + break; + case GGML_TYPE_Q2_0_ROCMFPX: + result = rocmfpx_quantize_fp2(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); + break; + case GGML_TYPE_Q6_0_ROCMFPX: + result = rocmfpx_quantize_fp6(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); + break; + case GGML_TYPE_Q8_0_ROCMFPX: + result = rocmfpx_quantize_fp8(src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); + break; case GGML_TYPE_Q4_1: result = quantize_q4_1 (src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_Q5_0: result = quantize_q5_0 (src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; case GGML_TYPE_Q5_1: result = quantize_q5_1 (src + start, (char *) dst + start_row * row_size, nrows, n_per_row, imatrix); break; diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index b1e053d1d008..6711e325cda9 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -5575,6 +5575,12 @@ class GGMLQuantizationType(IntEnum): MXFP4 = 39 NVFP4 = 40 Q1_0 = 41 + Q4_0_ROCMFP4 = 100 + Q4_0_ROCMFP4_FAST = 101 + Q6_0_ROCMFPX = 102 + Q8_0_ROCMFPX = 103 + Q3_0_ROCMFPX = 104 + Q2_0_ROCMFPX = 107 Q2_0 = 42 @@ -5633,6 +5639,23 @@ class LlamaFileType(IntEnum): MOSTLY_Q1_0 = 40 # except 1d tensors MOSTLY_Q2_0 = 41 # except 1d tensors + MOSTLY_Q4_0_ROCMFP4 = 100 + MOSTLY_Q4_0_ROCMFP4_LEAN = 101 + MOSTLY_Q4_0_ROCMFP4_COHERENT = 102 + MOSTLY_Q4_0_ROCMFP4_FAST = 103 + MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT = 104 + MOSTLY_Q4_0_ROCMFP4_STRIX = 105 + MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN = 106 + MOSTLY_Q6_0_ROCMFPX = 110 + MOSTLY_Q8_0_ROCMFPX = 111 + MOSTLY_Q3_0_ROCMFPX = 112 + MOSTLY_Q3_0_ROCMFPX_AGENT = 113 + MOSTLY_Q6_0_ROCMFPX_AGENT = 114 + MOSTLY_Q8_0_ROCMFPX_AGENT = 115 + MOSTLY_Q6_0_ROCMFPX_LEAN = 116 + MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN = 117 + MOSTLY_Q2_0_ROCMFPX = 119 # except 1d tensors + GUESSED = 1024 # not specified in the model file @@ -5768,6 +5791,12 @@ class VisionProjectorType: GGMLQuantizationType.MXFP4: (32, 1 + 16), GGMLQuantizationType.NVFP4: (64, 4 + 32), GGMLQuantizationType.Q1_0: (128, 2 + 16), + GGMLQuantizationType.Q4_0_ROCMFP4: (32, 2 + 16), + GGMLQuantizationType.Q4_0_ROCMFP4_FAST: (32, 1 + 16), + GGMLQuantizationType.Q6_0_ROCMFPX: (32, 24 + 2), + GGMLQuantizationType.Q8_0_ROCMFPX: (32, 32 + 1), + GGMLQuantizationType.Q3_0_ROCMFPX: (32, 12 + 2), + GGMLQuantizationType.Q2_0_ROCMFPX: (32, 8 + 2), GGMLQuantizationType.Q2_0: (64, 2 + 16), } diff --git a/gguf-py/gguf/quants.py b/gguf-py/gguf/quants.py index 80966b6ef151..ff9dc992687a 100644 --- a/gguf-py/gguf/quants.py +++ b/gguf-py/gguf/quants.py @@ -402,6 +402,78 @@ def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: return (x * d) +def _rocmfpx_ue4m3_to_fp32(e: np.ndarray) -> np.ndarray: + e = e.astype(np.uint8, copy=False) + exp = (e >> np.uint8(3)).astype(np.int32) + mant = (e & np.uint8(7)).astype(np.float32) + subnormal = mant * np.float32(2.0**-10) + normal = np.ldexp(8.0 + mant, exp - 11).astype(np.float32) + return np.where( + e <= np.uint8(0x7E), + np.where(exp == 0, subnormal, normal), + np.float32(0.0), + ).astype(np.float32) + + +def _rocmfpx_unpack_codes(qs: np.ndarray, nbits: int, block_size: int) -> np.ndarray: + bit_pos = np.arange(block_size, dtype=np.uint16) * np.uint16(nbits) + byte_index = bit_pos // np.uint16(8) + bit_index = bit_pos % np.uint16(8) + padded = np.pad(qs, ((0, 0), (0, 1)), mode="constant") + words = ( + padded[:, byte_index].astype(np.uint16) + | (padded[:, byte_index + 1].astype(np.uint16) << np.uint16(8)) + ) + return ((words >> bit_index) & np.uint16((1 << nbits) - 1)).astype(np.uint8) + + +class Q3_0_ROCMFPX(__Quant, qtype=GGMLQuantizationType.Q3_0_ROCMFPX): + _mag = np.array((0, 1, 2, 4), dtype=np.int8) + + @classmethod + def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: + n_blocks = blocks.shape[0] + + qs, e = np.hsplit(blocks, [12]) + + codes = _rocmfpx_unpack_codes(qs, 3, cls.block_size) + mag = np.take(cls._mag, codes & np.uint8(3)) + vals = np.where((codes & np.uint8(4)) != 0, -mag, mag).astype(np.float32) + + scales = _rocmfpx_ue4m3_to_fp32(e).reshape(n_blocks, 2, 1) + vals = vals.reshape(n_blocks, 2, cls.block_size // 2) + + return (vals * scales).reshape(n_blocks, cls.block_size) + + +class Q6_0_ROCMFPX(__Quant, qtype=GGMLQuantizationType.Q6_0_ROCMFPX): + @classmethod + def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: + n_blocks = blocks.shape[0] + + qs, e = np.hsplit(blocks, [24]) + + codes = _rocmfpx_unpack_codes(qs, 6, cls.block_size) + mag = (codes & np.uint8(31)).astype(np.int8) + vals = np.where((codes & np.uint8(32)) != 0, -mag, mag).astype(np.float32) + + scales = _rocmfpx_ue4m3_to_fp32(e).reshape(n_blocks, 2, 1) + vals = vals.reshape(n_blocks, 2, cls.block_size // 2) + + return (vals * scales).reshape(n_blocks, cls.block_size) + + +class Q8_0_ROCMFPX(__Quant, qtype=GGMLQuantizationType.Q8_0_ROCMFPX): + @classmethod + def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: + qs, e = np.hsplit(blocks, [32]) + + scales = _rocmfpx_ue4m3_to_fp32(e) + qs = qs.view(np.int8).astype(np.float32) + + return qs * scales + + class Q2_K(__Quant, qtype=GGMLQuantizationType.Q2_K): @classmethod def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: diff --git a/include/llama.h b/include/llama.h index d0dda88ffd2e..0468ab2f8d15 100644 --- a/include/llama.h +++ b/include/llama.h @@ -157,6 +157,24 @@ extern "C" { LLAMA_FTYPE_MOSTLY_Q1_0 = 40, // except 1d tensors LLAMA_FTYPE_MOSTLY_Q2_0 = 41, // except 1d tensors + // ROCmFPx experimental AMD-native recipes (high ID range, see ggml_ftype) + LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4 = 100, // except 1d tensors + LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN = 101, // ROCmFP4 with Q5_K token embeddings + LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_COHERENT = 102, // ROCmFP4 with Q6_K token embeddings + LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST = 103, // ROCmFP4 single-scale speed layout + LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT = 104, // ROCmFP4 fast with Q6_K token embeddings + LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX = 105, // ROCmFP4 Strix Halo quality/speed recipe + LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN = 106, // ROCmFP4 Strix Halo size-biased K/V recipe + LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX = 110, // ROCmFPx experimental 6-bit reference layout + LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX = 111, // ROCmFPx experimental 8-bit reference layout + LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX = 112, // ROCmFPx experimental 3-bit reference layout + LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX_AGENT = 113, // ROCmFPx 3-bit agent/tool-call coherent routing + LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT = 114, // ROCmFPx 6-bit agent/tool-call coherent routing + LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX_AGENT = 115, // ROCmFPx 8-bit agent/tool-call coherent routing + LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_LEAN = 116, // ROCmFPx 6-bit size/speed-biased routing + LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN = 117, // ROCmFPx 6-bit agent routing without Q8-heavy boosts + LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX = 119, // ROCmFPx 2-bit S40 codebook + dual UE4M3 scales + LLAMA_FTYPE_GUESSED = 1024, // not specified in the model file }; diff --git a/src/llama-kv-cache.cpp b/src/llama-kv-cache.cpp index 328c84da93d9..6effd8dfb5f6 100644 --- a/src/llama-kv-cache.cpp +++ b/src/llama-kv-cache.cpp @@ -323,8 +323,18 @@ llama_kv_cache::llama_kv_cache( LLAMA_LOG_WARN("%s: attention rotation force disabled (LLAMA_ATTN_ROT_DISABLE)\n", __func__); } + // fp3 ROCmFPX carries its own scale layout and is incompatible with + // the shared attention rotation path. + const bool attn_rot_k_rocmfpx_fp3 = type_k == GGML_TYPE_Q3_0_ROCMFPX; + const bool attn_rot_v_rocmfpx_fp3 = type_v == GGML_TYPE_Q3_0_ROCMFPX; + if (attn_rot_k_rocmfpx_fp3 || attn_rot_v_rocmfpx_fp3) { + LLAMA_LOG_WARN("%s: attention rotation disabled for fp3 ROCmFPX KV cache (K=%s, V=%s)\n", __func__, + ggml_type_name(type_k), ggml_type_name(type_v)); + } + attn_rot_k = !attn_rot_disable && + !attn_rot_k_rocmfpx_fp3 && n_embd_head_k_all > 0 && ggml_is_quantized(type_k) && hparams.n_embd_head_k() % 64 == 0; @@ -338,6 +348,7 @@ llama_kv_cache::llama_kv_cache( attn_rot_v = !attn_rot_disable && + !attn_rot_v_rocmfpx_fp3 && n_embd_head_v_all > 0 && ggml_is_quantized(type_v) && hparams.n_embd_head_v() % 64 == 0; diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp index 89ae60d2d571..2dfcd6eb9074 100644 --- a/src/llama-model-loader.cpp +++ b/src/llama-model-loader.cpp @@ -40,6 +40,22 @@ const char * llama_ftype_name(llama_ftype ftype) { case LLAMA_FTYPE_MOSTLY_Q1_0: name = LLAMA_FTYPE_PREFIX "Q1_0"; break; case LLAMA_FTYPE_MOSTLY_Q2_0: name = LLAMA_FTYPE_PREFIX "Q2_0"; break; case LLAMA_FTYPE_MOSTLY_Q4_0: name = LLAMA_FTYPE_PREFIX "Q4_0"; break; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4: name = LLAMA_FTYPE_PREFIX "Q4_0_ROCMFP4"; break; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN: name = LLAMA_FTYPE_PREFIX "Q4_0_ROCMFP4_LEAN"; break; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_COHERENT: name = LLAMA_FTYPE_PREFIX "Q4_0_ROCMFP4_COHERENT"; break; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST: name = LLAMA_FTYPE_PREFIX "Q4_0_ROCMFP4_FAST"; break; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT: name = LLAMA_FTYPE_PREFIX "Q4_0_ROCMFP4_FAST_COHERENT"; break; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX: name = LLAMA_FTYPE_PREFIX "Q4_0_ROCMFP4_STRIX"; break; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN: name = LLAMA_FTYPE_PREFIX "Q4_0_ROCMFP4_STRIX_LEAN"; break; + case LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX: name = LLAMA_FTYPE_PREFIX "Q3_0_ROCMFPX"; break; + case LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX: name = LLAMA_FTYPE_PREFIX "Q2_0_ROCMFPX"; break; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX: name = LLAMA_FTYPE_PREFIX "Q6_0_ROCMFPX"; break; + case LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX: name = LLAMA_FTYPE_PREFIX "Q8_0_ROCMFPX"; break; + case LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX_AGENT: name = LLAMA_FTYPE_PREFIX "Q3_0_ROCMFPX_AGENT"; break; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT: name = LLAMA_FTYPE_PREFIX "Q6_0_ROCMFPX_AGENT"; break; + case LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX_AGENT: name = LLAMA_FTYPE_PREFIX "Q8_0_ROCMFPX_AGENT"; break; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_LEAN: name = LLAMA_FTYPE_PREFIX "Q6_0_ROCMFPX_LEAN"; break; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN: name = LLAMA_FTYPE_PREFIX "Q6_0_ROCMFPX_AGENT_LEAN"; break; case LLAMA_FTYPE_MOSTLY_Q4_1: name = LLAMA_FTYPE_PREFIX "Q4_1"; break; case LLAMA_FTYPE_MOSTLY_Q5_0: name = LLAMA_FTYPE_PREFIX "Q5_0"; break; case LLAMA_FTYPE_MOSTLY_Q5_1: name = LLAMA_FTYPE_PREFIX "Q5_1"; break; @@ -752,6 +768,12 @@ llama_model_loader::llama_model_loader( case GGML_TYPE_F16: ftype = LLAMA_FTYPE_MOSTLY_F16; break; case GGML_TYPE_BF16: ftype = LLAMA_FTYPE_MOSTLY_BF16; break; case GGML_TYPE_Q4_0: ftype = LLAMA_FTYPE_MOSTLY_Q4_0; break; + case GGML_TYPE_Q4_0_ROCMFP4: ftype = LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4; break; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: ftype = LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST; break; + case GGML_TYPE_Q3_0_ROCMFPX: ftype = LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX; break; + case GGML_TYPE_Q2_0_ROCMFPX: ftype = LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX; break; + case GGML_TYPE_Q6_0_ROCMFPX: ftype = LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX; break; + case GGML_TYPE_Q8_0_ROCMFPX: ftype = LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX; break; case GGML_TYPE_Q4_1: ftype = LLAMA_FTYPE_MOSTLY_Q4_1; break; case GGML_TYPE_Q5_0: ftype = LLAMA_FTYPE_MOSTLY_Q5_0; break; case GGML_TYPE_Q5_1: ftype = LLAMA_FTYPE_MOSTLY_Q5_1; break; diff --git a/src/llama-quant.cpp b/src/llama-quant.cpp index 34ff25db57e6..1c7da63bc0af 100644 --- a/src/llama-quant.cpp +++ b/src/llama-quant.cpp @@ -13,6 +13,7 @@ #include #include #include +#include // result of parsing --tensor-type option // (changes to this struct must be reflected in tools/quantize/quantize.cpp) @@ -395,6 +396,12 @@ static ggml_type tensor_type_fallback(quantize_state_impl & qs, const ggml_tenso case GGML_TYPE_Q3_K: case GGML_TYPE_TQ1_0: case GGML_TYPE_TQ2_0: return_type = GGML_TYPE_Q4_0; break; + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: return_type = GGML_TYPE_Q4_0; break; + case GGML_TYPE_Q3_0_ROCMFPX: + case GGML_TYPE_Q2_0_ROCMFPX: + case GGML_TYPE_Q6_0_ROCMFPX: + case GGML_TYPE_Q8_0_ROCMFPX: return_type = GGML_TYPE_Q8_0; break; case GGML_TYPE_Q4_K: return_type = GGML_TYPE_Q5_0; break; case GGML_TYPE_Q5_K: return_type = GGML_TYPE_Q5_1; break; case GGML_TYPE_Q6_K: return_type = GGML_TYPE_Q8_0; break; @@ -450,6 +457,118 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type } return std::make_pair(i_layer, n_layer); }; + auto rocmfpx_is_q3_agent = [] (llama_ftype ftype) { + return ftype == LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX_AGENT; + }; + auto rocmfpx_is_q6_agent = [] (llama_ftype ftype) { + return ftype == LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT; + }; + auto rocmfpx_is_q6_agent_lean = [] (llama_ftype ftype) { + return ftype == LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN; + }; + auto rocmfpx_is_q6_lean = [] (llama_ftype ftype) { + return ftype == LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_LEAN || + ftype == LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN; + }; + auto rocmfpx_is_q8_agent = [] (llama_ftype ftype) { + return ftype == LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX_AGENT; + }; + auto rocmfpx_is_q3_family = [&] (llama_ftype ftype) { + return ftype == LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX || rocmfpx_is_q3_agent(ftype); + }; + auto rocmfpx_is_q6_family = [&] (llama_ftype ftype) { + return ftype == LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX || + rocmfpx_is_q6_agent(ftype) || + rocmfpx_is_q6_lean(ftype); + }; + auto rocmfpx_is_q8_family = [&] (llama_ftype ftype) { + return ftype == LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX || rocmfpx_is_q8_agent(ftype); + }; + auto rocmfpx_is_family = [&] (llama_ftype ftype) { + return rocmfpx_is_q3_family(ftype) || rocmfpx_is_q6_family(ftype) || rocmfpx_is_q8_family(ftype); + }; + auto rocmfpx_sensitive_tensor_type = [&] (llama_ftype ftype) { + switch (ftype) { + case LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX: return GGML_TYPE_Q4_0_ROCMFP4_FAST; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX: return GGML_TYPE_Q6_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_LEAN: return GGML_TYPE_Q6_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN: return GGML_TYPE_Q6_K; + case LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX: return GGML_TYPE_Q8_0_ROCMFPX; + default: + if (rocmfpx_is_q3_agent(ftype)) { + return GGML_TYPE_Q6_0_ROCMFPX; + } + if (rocmfpx_is_q6_agent(ftype)) { + return GGML_TYPE_Q8_0_ROCMFPX; + } + if (rocmfpx_is_q8_agent(ftype)) { + return GGML_TYPE_Q8_0; + } + return GGML_TYPE_COUNT; + } + }; + auto rocmfpx_q3_needs_down_boost = [&](int i_layer, int n_layer, llama_ftype ftype) -> bool { + if (rocmfpx_is_q3_agent(ftype)) { + return i_layer < n_layer/8 || use_more_bits(i_layer, n_layer) || i_layer >= n_layer/2; + } + return i_layer < n_layer/16 || use_more_bits(i_layer, n_layer) || i_layer >= (2*n_layer)/3; + }; + auto rocmfpx_q3_attn_kv_type = [&](int i_layer, int n_layer, llama_ftype ftype) -> ggml_type { + if (rocmfpx_is_q3_agent(ftype)) { + return i_layer < n_layer/2 ? GGML_TYPE_Q6_K : GGML_TYPE_Q5_K; + } + return i_layer < n_layer/2 ? GGML_TYPE_Q5_K : GGML_TYPE_Q4_K; + }; + auto rocmfpx_q6_needs_down_boost = [&](int i_layer, int n_layer, llama_ftype ftype) -> bool { + if (rocmfpx_is_q6_agent_lean(ftype)) { + return i_layer < n_layer/16 || i_layer >= 15*n_layer/16; + } + if (rocmfpx_is_q6_lean(ftype)) { + return false; + } + if (rocmfpx_is_q6_agent(ftype)) { + return i_layer < n_layer/8 || i_layer >= 3*n_layer/4 || use_more_bits(i_layer, n_layer); + } + return i_layer < n_layer/16 || i_layer >= 7*n_layer/8; + }; + auto rocmfpx_q6_needs_attn_boost = [&](int i_layer, int n_layer, llama_ftype ftype) -> bool { + if (rocmfpx_is_q6_agent_lean(ftype)) { + return i_layer < n_layer/16; + } + if (rocmfpx_is_q6_lean(ftype)) { + return false; + } + if (rocmfpx_is_q6_agent(ftype)) { + return i_layer < n_layer/8 || use_more_bits(i_layer, n_layer); + } + return i_layer < n_layer/16; + }; + auto rocmfpx_q8_needs_attn_boost = [&](int i_layer, int n_layer, llama_ftype ftype) -> bool { + if (rocmfpx_is_q8_agent(ftype)) { + return i_layer < n_layer/8 || use_more_bits(i_layer, n_layer) || i_layer >= n_layer/2; + } + return false; + }; + auto rocmfpx_q8_needs_down_boost = [&](int i_layer, int n_layer, llama_ftype ftype) -> bool { + if (rocmfpx_is_q8_agent(ftype)) { + return i_layer < n_layer/8 || i_layer >= 3*n_layer/4 || use_more_bits(i_layer, n_layer); + } + return false; + }; + auto layer_from_name = [&](const std::string & tensor_name, int n_layer) -> std::pair { + int i_layer = 0; + if (sscanf(tensor_name.c_str(), "blk.%d.", &i_layer) == 1) { + return { i_layer, n_layer }; + } + static std::mutex warn_mtx; + static std::unordered_set warned; + std::lock_guard lock(warn_mtx); + if (warned.emplace(tensor_name).second) { + LLAMA_LOG_WARN("%s: could not parse layer index from tensor '%s', assuming layer 0\n", + __func__, tensor_name.c_str()); + } + return { 0, n_layer }; + }; // for arches that share the same tensor between the token embeddings and the output, we quantize the token embeddings // with the quantization of the output tensor @@ -460,7 +579,25 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type const int64_t nx = tensor->ne[0]; const int64_t qk_k = ggml_blck_size(new_type); - if (ftype == LLAMA_FTYPE_MOSTLY_MXFP4_MOE) { + if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST) { + new_type = GGML_TYPE_Q4_0_ROCMFP4_FAST; + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_COHERENT || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN) { + new_type = category == tensor_category::TOKEN_EMBD ? + ((ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN) ? GGML_TYPE_Q5_K : GGML_TYPE_Q6_K) : + ((ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN) ? GGML_TYPE_Q4_0_ROCMFP4_FAST : GGML_TYPE_Q4_0_ROCMFP4); + } + else if (ggml_type rocmfpx_type = rocmfpx_sensitive_tensor_type(ftype); rocmfpx_type < GGML_TYPE_COUNT) { + new_type = rocmfpx_type; + } + else if (ftype == LLAMA_FTYPE_MOSTLY_MXFP4_MOE) { new_type = GGML_TYPE_Q8_0; } else if (arch == LLM_ARCH_FALCON || nx % qk_k != 0) { @@ -492,7 +629,21 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type if (qs.params->token_embedding_type < GGML_TYPE_COUNT) { new_type = qs.params->token_embedding_type; } else { - if (ftype == LLAMA_FTYPE_MOSTLY_IQ2_XXS || ftype == LLAMA_FTYPE_MOSTLY_IQ2_XS || + if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_COHERENT || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN) { + // Token embeddings are not hot during decode. Spending a + // little more here preserves coherence while keeping the + // transformer layers on the fast ROCmFP4 path. + new_type = (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN) ? GGML_TYPE_Q5_K : GGML_TYPE_Q6_K; + } + else if (ggml_type rocmfpx_type = rocmfpx_sensitive_tensor_type(ftype); rocmfpx_type < GGML_TYPE_COUNT) { + new_type = rocmfpx_type; + } + else if (ftype == LLAMA_FTYPE_MOSTLY_IQ2_XXS || ftype == LLAMA_FTYPE_MOSTLY_IQ2_XS || ftype == LLAMA_FTYPE_MOSTLY_IQ1_S || ftype == LLAMA_FTYPE_MOSTLY_IQ1_M) { new_type = GGML_TYPE_Q2_K; } @@ -531,6 +682,35 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type } } } else if (category_is_attn_v(category)) { + if (rocmfpx_is_q3_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + new_type = rocmfpx_q3_attn_kv_type(info.first, info.second, ftype); + } + else if (rocmfpx_is_q6_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + if (rocmfpx_q6_needs_attn_boost(info.first, info.second, ftype)) { + new_type = rocmfpx_is_q6_agent_lean(ftype) ? GGML_TYPE_Q6_K : GGML_TYPE_Q8_0_ROCMFPX; + } + } + else if (rocmfpx_is_q8_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + if (rocmfpx_q8_needs_attn_boost(info.first, info.second, ftype)) { + new_type = GGML_TYPE_Q8_0; + } + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN) { + // Strix Halo quality/speed recipe: keep attention V in the + // dual-scale ROCmFP4 layout. Together with ATTENTION_K below this + // costs about 0.02 BPW on Qwen3-4B and improves the short + // WikiText-2 pass while preserving the speed win over stock Q4_0. + new_type = GGML_TYPE_Q4_0_ROCMFP4; + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4) { + // Attention V is coherence-sensitive, but on Qwen3-4B Q5_K tested + // better than Q6_K while also shaving a little size from the preset. + new_type = GGML_TYPE_Q5_K; + } if (ftype == LLAMA_FTYPE_MOSTLY_Q2_K) { new_type = qs.model.hparams.n_gqa() >= 4 ? GGML_TYPE_Q4_K : GGML_TYPE_Q3_K; } @@ -562,14 +742,37 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type // nearly negligible increase in model size by quantizing this tensor with more bits: if (new_type == GGML_TYPE_Q3_K || new_type == GGML_TYPE_Q4_K) new_type = GGML_TYPE_Q5_K; } - if (qs.model.hparams.n_expert == 8) { + if (qs.model.hparams.n_expert == 8 && !rocmfpx_is_family(ftype)) { // for the 8-expert model, bumping this to Q8_0 trades just ~128MB // TODO: explore better strategies new_type = GGML_TYPE_Q8_0; } ++qs.i_attention_wv; } else if (category == tensor_category::ATTENTION_K) { - if (qs.model.hparams.n_expert == 8) { + if (rocmfpx_is_q3_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + new_type = rocmfpx_q3_attn_kv_type(info.first, info.second, ftype); + } + else if (rocmfpx_is_q6_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + if (rocmfpx_q6_needs_attn_boost(info.first, info.second, ftype)) { + new_type = rocmfpx_is_q6_agent_lean(ftype) ? GGML_TYPE_Q6_K : GGML_TYPE_Q8_0_ROCMFPX; + } + } + else if (rocmfpx_is_q8_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + if (rocmfpx_q8_needs_attn_boost(info.first, info.second, ftype)) { + new_type = GGML_TYPE_Q8_0; + } + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX || + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN) { + // Matching K/V in the dual-scale custom layout improved PPL more + // than attention-V alone while keeping the large tensors on the + // faster single-scale ROCmFP4 path. + new_type = GGML_TYPE_Q4_0_ROCMFP4; + } + else if (qs.model.hparams.n_expert == 8 && !rocmfpx_is_family(ftype)) { // for the 8-expert model, bumping this to Q8_0 trades just ~128MB // TODO: explore better strategies new_type = GGML_TYPE_Q8_0; @@ -581,7 +784,24 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type new_type = GGML_TYPE_IQ2_S; } } else if (category == tensor_category::ATTENTION_Q) { - if (ftype == LLAMA_FTYPE_MOSTLY_IQ3_XS) { + if (rocmfpx_is_q3_family(ftype)) { + // Q projection carries arithmetic/reasoning detail; Q5_K is a + // small size premium that restores strict JSON following. + new_type = rocmfpx_is_q3_agent(ftype) ? GGML_TYPE_Q6_K : GGML_TYPE_Q5_K; + } + else if (rocmfpx_is_q6_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + if (rocmfpx_q6_needs_attn_boost(info.first, info.second, ftype)) { + new_type = rocmfpx_is_q6_agent_lean(ftype) ? GGML_TYPE_Q6_K : GGML_TYPE_Q8_0_ROCMFPX; + } + } + else if (rocmfpx_is_q8_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + if (rocmfpx_q8_needs_attn_boost(info.first, info.second, ftype)) { + new_type = GGML_TYPE_Q8_0; + } + } + else if (ftype == LLAMA_FTYPE_MOSTLY_IQ3_XS) { new_type = GGML_TYPE_IQ3_XXS; } else if (ftype == LLAMA_FTYPE_MOSTLY_IQ3_XXS) { @@ -590,7 +810,29 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type } else if (category == tensor_category::FFN_DOWN) { auto info = layer_info(qs.i_ffn_down, qs.n_ffn_down, name.c_str()); int i_layer = info.first, n_layer = info.second; - if (ftype == LLAMA_FTYPE_MOSTLY_Q2_K) new_type = GGML_TYPE_Q3_K; + if (rocmfpx_is_q3_family(ftype)) { + if (rocmfpx_q3_needs_down_boost(i_layer, n_layer, ftype)) { + new_type = rocmfpx_is_q3_agent(ftype) ? GGML_TYPE_Q6_K : GGML_TYPE_Q5_K; + } + } + else if (rocmfpx_is_q6_family(ftype)) { + if (rocmfpx_q6_needs_down_boost(i_layer, n_layer, ftype)) { + new_type = rocmfpx_is_q6_agent_lean(ftype) ? GGML_TYPE_Q6_K : GGML_TYPE_Q8_0_ROCMFPX; + } + } + else if (rocmfpx_is_q8_family(ftype)) { + if (rocmfpx_q8_needs_down_boost(i_layer, n_layer, ftype)) { + new_type = GGML_TYPE_Q8_0; + } + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4) { + // Down projections carry residual-stream detail. Spend extra bits + // here so the experimental FP4 tensors do not dominate perplexity. + // The final third is less sensitive on Qwen3-4B in the Strix FP4 + // quality pass, so Q5_K buys back size and a little decode speed. + new_type = (n_layer > 0 && i_layer >= (2 * n_layer) / 3) ? GGML_TYPE_Q5_K : GGML_TYPE_Q6_K; + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q2_K) new_type = GGML_TYPE_Q3_K; else if (ftype == LLAMA_FTYPE_MOSTLY_Q2_K_S) { if (i_layer < n_layer/8) new_type = GGML_TYPE_Q4_K; } @@ -633,6 +875,26 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type } ++qs.i_ffn_down; } else if (category == tensor_category::ATTENTION_OUTPUT) { + if (rocmfpx_is_q3_family(ftype)) { + new_type = rocmfpx_is_q3_agent(ftype) ? GGML_TYPE_Q6_K : GGML_TYPE_Q5_K; + } + else if (rocmfpx_is_q6_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + if (rocmfpx_q6_needs_attn_boost(info.first, info.second, ftype)) { + new_type = rocmfpx_is_q6_agent_lean(ftype) ? GGML_TYPE_Q6_K : GGML_TYPE_Q8_0_ROCMFPX; + } + } + else if (rocmfpx_is_q8_family(ftype)) { + auto info = layer_from_name(name, qs.model.hparams.n_layer()); + if (rocmfpx_q8_needs_attn_boost(info.first, info.second, ftype)) { + new_type = GGML_TYPE_Q8_0; + } + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4) { + // Output projections are another high-leverage location; Q5_K is + // a modest size increase that helps preserve chat coherence. + new_type = GGML_TYPE_Q5_K; + } if (arch != LLM_ARCH_FALCON) { if (qs.model.hparams.n_expert == 8) { if (ftype == LLAMA_FTYPE_MOSTLY_Q2_K || ftype == LLAMA_FTYPE_MOSTLY_IQ3_XS || ftype == LLAMA_FTYPE_MOSTLY_IQ3_XXS || @@ -653,7 +915,17 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type } } else if (category == tensor_category::ATTENTION_QKV) { - if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_M || ftype == LLAMA_FTYPE_MOSTLY_Q3_K_L || ftype == LLAMA_FTYPE_MOSTLY_IQ3_M) { + if (rocmfpx_is_q3_family(ftype)) { + new_type = rocmfpx_is_q3_agent(ftype) ? GGML_TYPE_Q5_K : GGML_TYPE_Q4_K; + } + else if (rocmfpx_is_q6_family(ftype)) { + new_type = rocmfpx_is_q6_agent_lean(ftype) ? GGML_TYPE_Q6_K : + (rocmfpx_is_q6_lean(ftype) ? GGML_TYPE_Q6_0_ROCMFPX : GGML_TYPE_Q8_0_ROCMFPX); + } + else if (rocmfpx_is_q8_family(ftype)) { + new_type = GGML_TYPE_Q8_0; + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_M || ftype == LLAMA_FTYPE_MOSTLY_Q3_K_L || ftype == LLAMA_FTYPE_MOSTLY_IQ3_M) { new_type = GGML_TYPE_Q4_K; } else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_M) new_type = GGML_TYPE_Q5_K; @@ -662,7 +934,33 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type else if (category == tensor_category::FFN_GATE) { auto info = layer_info(qs.i_ffn_gate, qs.n_ffn_gate, name.c_str()); int i_layer = info.first, n_layer = info.second; - if (ftype == LLAMA_FTYPE_MOSTLY_IQ3_XS && (i_layer >= n_layer/8 && i_layer < 7*n_layer/8)) { + if (rocmfpx_is_q3_family(ftype)) { + if (rocmfpx_is_q3_agent(ftype) || i_layer < n_layer/16 || use_more_bits(i_layer, n_layer)) { + new_type = GGML_TYPE_Q6_0_ROCMFPX; + } + } + else if (rocmfpx_is_q6_family(ftype)) { + // Keep gate projections on FP6; selective Q8 boosts here were + // the main reason the Q6 preset overshot stock Q6_K size. + if (rocmfpx_is_q6_agent_lean(ftype) && use_more_bits(i_layer, n_layer)) { + new_type = GGML_TYPE_Q6_K; + } + else if (rocmfpx_is_q6_agent(ftype) && !rocmfpx_is_q6_lean(ftype) && use_more_bits(i_layer, n_layer)) { + new_type = GGML_TYPE_Q8_0_ROCMFPX; + } + } + else if (rocmfpx_is_q8_family(ftype)) { + if (rocmfpx_is_q8_agent(ftype) && (i_layer < n_layer/8 || use_more_bits(i_layer, n_layer))) { + new_type = GGML_TYPE_Q8_0; + } + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4 && use_more_bits(i_layer, n_layer)) { + // Gate projections are the cheapest place to buy back coherence in + // this preset. Keep selected layers at Q5_K, but leave FFN-up in + // ROCmFP4 to avoid sliding back to a bulky/slower mostly-Q5 mix. + new_type = GGML_TYPE_Q5_K; + } + else if (ftype == LLAMA_FTYPE_MOSTLY_IQ3_XS && (i_layer >= n_layer/8 && i_layer < 7*n_layer/8)) { new_type = GGML_TYPE_IQ3_XXS; } ++qs.i_ffn_gate; @@ -670,7 +968,29 @@ static ggml_type llama_tensor_get_type_impl(quantize_state_impl & qs, ggml_type else if (category == tensor_category::FFN_UP) { auto info = layer_info(qs.i_ffn_up, qs.n_ffn_up, name.c_str()); int i_layer = info.first, n_layer = info.second; - if (ftype == LLAMA_FTYPE_MOSTLY_IQ3_XS && (i_layer >= n_layer/8 && i_layer < 7*n_layer/8)) { + if (rocmfpx_is_q3_agent(ftype)) { + if (i_layer < n_layer/8 || i_layer >= 3*n_layer/4 || use_more_bits(i_layer, n_layer)) { + new_type = GGML_TYPE_Q6_0_ROCMFPX; + } + } + else if (rocmfpx_is_q6_family(ftype)) { + if (rocmfpx_is_q6_agent_lean(ftype) && use_more_bits(i_layer, n_layer)) { + new_type = GGML_TYPE_Q6_K; + } + else if (rocmfpx_is_q6_agent(ftype) && !rocmfpx_is_q6_lean(ftype) && use_more_bits(i_layer, n_layer)) { + new_type = GGML_TYPE_Q8_0_ROCMFPX; + } + } + else if (rocmfpx_is_q8_family(ftype)) { + if (rocmfpx_is_q8_agent(ftype) && (i_layer < n_layer/8 || use_more_bits(i_layer, n_layer))) { + new_type = GGML_TYPE_Q8_0; + } + } + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4) { + GGML_UNUSED(i_layer); + GGML_UNUSED(n_layer); + } + else if (ftype == LLAMA_FTYPE_MOSTLY_IQ3_XS && (i_layer >= n_layer/8 && i_layer < 7*n_layer/8)) { new_type = GGML_TYPE_IQ3_XXS; } ++qs.i_ffn_up; @@ -815,6 +1135,33 @@ static size_t llama_tensor_quantize_impl(enum ggml_type new_type, const float * return new_size; } +static bool llama_tensor_allows_requantize_to_rocmfp4(const ggml_type src_type, const ggml_type dst_type) { + // ROCmFP4 targets are permitted to requantize (without the global + // --allow-requantize flag) from a small set of well-behaved 4-bit sources: + // - NVFP4: cross-vendor FP4 exchange format, and the closest-matching + // source. NVFP4 and ROCmFP4 share the same UE4M3 scale encoding + // applied per 16-element sub-block, and 7 of 8 integer codebook + // levels {0,1,2,3,4,6,8} are identical. The only mismatch is the + // saturating top level: NVFP4 uses 12, ROCmFP4 was retuned to 10, + // so NVFP4's largest-magnitude bucket cannot be represented + // exactly at any scale. The dequant->requant MSE search handles + // this well but is not lossless (measured ~7% rel-RMSE vs the + // NVFP4 weights on Gaussian data, concentrated at that top + // bucket). Targeting Q4_0_ROCMFP4 keeps the exact 4.5 bpw size; + // Q4_0_ROCMFP4_FAST drops to 4.25 bpw (smaller) by merging the + // two sub-scales into one. A/B recommended, but expect it close. + // - Q4_0 : the format Google ships QAT (quantization-aware-trained) weights + // in. Dequantizing Q4_0 recovers the QAT-optimized weights, which + // can then be re-mapped onto the ROCmFP4 kernel path. Unlike NVFP4, + // Q4_0 uses a uniform int4 grid with an fp16 per-32 scale, so this + // re-mapping is NOT lossless and may erode QAT quality — always A/B + // the result. Prefer running QAT models natively as Q4_0 for a + // quality-free speedup; requantize only when an A/B test passes. + const bool src_ok = src_type == GGML_TYPE_NVFP4 || src_type == GGML_TYPE_Q4_0; + const bool dst_ok = dst_type == GGML_TYPE_Q4_0_ROCMFP4 || dst_type == GGML_TYPE_Q4_0_ROCMFP4_FAST; + return src_ok && dst_ok; +} + // // imatrix requirement check // @@ -847,6 +1194,22 @@ static bool tensor_requires_imatrix(const char * tensor_name, const ggml_type ds ggml_type llama_ftype_get_default_type(llama_ftype ftype) { switch (ftype) { case LLAMA_FTYPE_MOSTLY_Q4_0: return GGML_TYPE_Q4_0; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4: return GGML_TYPE_Q4_0_ROCMFP4; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN: return GGML_TYPE_Q4_0_ROCMFP4; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_COHERENT: return GGML_TYPE_Q4_0_ROCMFP4; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST: return GGML_TYPE_Q4_0_ROCMFP4_FAST; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT: return GGML_TYPE_Q4_0_ROCMFP4_FAST; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX: return GGML_TYPE_Q4_0_ROCMFP4_FAST; + case LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN: return GGML_TYPE_Q4_0_ROCMFP4_FAST; + case LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX: return GGML_TYPE_Q3_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX: return GGML_TYPE_Q2_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX: return GGML_TYPE_Q6_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_LEAN: return GGML_TYPE_Q6_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX: return GGML_TYPE_Q8_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX_AGENT: return GGML_TYPE_Q3_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT: return GGML_TYPE_Q6_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN: return GGML_TYPE_Q6_0_ROCMFPX; + case LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX_AGENT: return GGML_TYPE_Q8_0_ROCMFPX; case LLAMA_FTYPE_MOSTLY_Q4_1: return GGML_TYPE_Q4_1; case LLAMA_FTYPE_MOSTLY_Q5_0: return GGML_TYPE_Q5_0; case LLAMA_FTYPE_MOSTLY_Q5_1: return GGML_TYPE_Q5_1; @@ -1269,7 +1632,8 @@ 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)); } - if (ggml_is_quantized(tensor->type) && !params->allow_requantize) { + if (ggml_is_quantized(tensor->type) && !params->allow_requantize && + !llama_tensor_allows_requantize_to_rocmfp4(tensor->type, new_type)) { throw std::runtime_error(format("requantizing from type %s is disabled", ggml_type_name(tensor->type))); } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 436ba35ff7c1..3919e555273f 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -2524,6 +2524,14 @@ struct test_set_rows : public test_case { } double max_nmse_err() override { + if ((type_dst == GGML_TYPE_Q4_0_ROCMFP4 || type_dst == GGML_TYPE_Q4_0_ROCMFP4_FAST || + type_dst == GGML_TYPE_Q3_0_ROCMFPX || type_dst == GGML_TYPE_Q6_0_ROCMFPX || type_dst == GGML_TYPE_Q8_0_ROCMFPX) && + (type_src == GGML_TYPE_F16 || type_src == GGML_TYPE_BF16)) { + // The ROCmFPx encoders run an exhaustive UE4M3 scale search. It is + // the same search as the CPU path, but a half/bfloat source can tie + // differently and select a neighbouring scale/code pair. + return 5e-4; + } if (type_dst == GGML_TYPE_Q2_0 || type_dst == GGML_TYPE_Q4_0 || type_dst == GGML_TYPE_Q4_1 || type_dst == GGML_TYPE_IQ4_NL || type_dst == GGML_TYPE_Q5_0 || type_dst == GGML_TYPE_Q5_1 || type_dst == GGML_TYPE_Q8_0) { @@ -3058,6 +3066,14 @@ struct test_cpy : public test_case { if (type_src == type_dst) { return 0.0; } + if ((type_dst == GGML_TYPE_Q4_0_ROCMFP4 || type_dst == GGML_TYPE_Q4_0_ROCMFP4_FAST || + type_dst == GGML_TYPE_Q3_0_ROCMFPX || type_dst == GGML_TYPE_Q6_0_ROCMFPX || type_dst == GGML_TYPE_Q8_0_ROCMFPX) && + (type_src == GGML_TYPE_F16 || type_src == GGML_TYPE_BF16)) { + // Vulkan quantizes half/bfloat sources directly on device. The + // exhaustive ROCmFP4 scale search matches the CPU path, but source + // precision and tie points can pick a nearby scale/code pair. + return 5e-4; + } if (type_dst == GGML_TYPE_Q4_0 || type_dst == GGML_TYPE_Q4_1 || type_dst == GGML_TYPE_IQ4_NL || type_dst == GGML_TYPE_Q5_0 || type_dst == GGML_TYPE_Q5_1 || type_dst == GGML_TYPE_Q8_0) { // estimate what the max nmse error would be if one quantized value is @@ -9070,7 +9086,8 @@ static const ggml_type all_types[] = { GGML_TYPE_Q8_0, GGML_TYPE_Q1_0, GGML_TYPE_Q2_0, - GGML_TYPE_MXFP4, GGML_TYPE_NVFP4, + GGML_TYPE_MXFP4, GGML_TYPE_Q4_0_ROCMFP4, GGML_TYPE_Q4_0_ROCMFP4_FAST, GGML_TYPE_NVFP4, + GGML_TYPE_Q2_0_ROCMFPX, GGML_TYPE_Q3_0_ROCMFPX, GGML_TYPE_Q6_0_ROCMFPX, GGML_TYPE_Q8_0_ROCMFPX, GGML_TYPE_Q2_K, GGML_TYPE_Q3_K, GGML_TYPE_Q4_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, @@ -9089,7 +9106,8 @@ static const ggml_type base_types[] = { GGML_TYPE_Q4_0, GGML_TYPE_Q4_1, // for I8MM tests GGML_TYPE_Q4_K, - GGML_TYPE_MXFP4, GGML_TYPE_NVFP4, // TODO: or "other" + GGML_TYPE_MXFP4, GGML_TYPE_Q4_0_ROCMFP4, GGML_TYPE_Q4_0_ROCMFP4_FAST, GGML_TYPE_NVFP4, // TODO: or "other" + GGML_TYPE_Q2_0_ROCMFPX, GGML_TYPE_Q3_0_ROCMFPX, GGML_TYPE_Q6_0_ROCMFPX, GGML_TYPE_Q8_0_ROCMFPX, GGML_TYPE_IQ2_XXS }; diff --git a/tools/quantize/quantize.cpp b/tools/quantize/quantize.cpp index 38950036cd82..f14788a71e0c 100644 --- a/tools/quantize/quantize.cpp +++ b/tools/quantize/quantize.cpp @@ -35,6 +35,24 @@ static const std::vector QUANT_OPTIONS = { { "Q1_0", LLAMA_FTYPE_MOSTLY_Q1_0, " 1.125 bpw quantization", }, { "Q2_0", LLAMA_FTYPE_MOSTLY_Q2_0, " 2.25 bpw quantization (group 64)", }, { "Q4_0", LLAMA_FTYPE_MOSTLY_Q4_0, " 4.34G, +0.4685 ppl @ Llama-3-8B", }, + { "Q4_0_ROCMFP4", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4, " 4.50 bpw ROCmFP4 UE4M3-scale experimental", }, + { "Q4_0_ROCMFP4_EVEN", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4, " 4.50 bpw ROCmFP4 even tensor conversion; implies --pure", }, + { "Q4_0_ROCMFP4_LEAN", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_LEAN, " 4.60 bpw ROCmFP4 + Q5_K token embeddings", }, + { "Q4_0_ROCMFP4_COHERENT", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_COHERENT, " 4.70 bpw ROCmFP4 + Q6_K token embeddings", }, + { "Q4_0_ROCMFP4_FAST", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST, " 4.25 bpw ROCmFP4 single-scale speed layout", }, + { "Q4_0_ROCMFP4_FAST_EVEN", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST, " 4.25 bpw ROCmFP4 fast even tensor conversion; implies --pure", }, + { "Q4_0_ROCMFP4_FAST_COHERENT", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_FAST_COHERENT, " ~4.45 bpw ROCmFP4 fast + Q6_K token embeddings", }, + { "Q4_0_ROCMFP4_STRIX", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX, " ~4.49 bpw ROCmFP4 Strix Halo attn-K/V quality recipe", }, + { "Q4_0_ROCMFP4_STRIX_LEAN", LLAMA_FTYPE_MOSTLY_Q4_0_ROCMFP4_STRIX_LEAN, " ~4.38 bpw ROCmFP4 Strix K/V + Q5_K token embeddings", }, + { "Q3_0_ROCMFPX", LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX, " 3.50 bpw ROCmFPx experimental, ROCm/Vulkan staging", }, + { "Q2_0_ROCMFPX", LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX, " 2.50 bpw ROCmFPx S40 codebook + dual UE4M3 scales", }, + { "Q6_0_ROCMFPX", LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX, " 6.50 bpw ROCmFPx experimental, ROCm/Vulkan staging", }, + { "Q8_0_ROCMFPX", LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX, " 8.25 bpw ROCmFPx experimental, ROCm/Vulkan staging", }, + { "Q3_0_ROCMFPX_AGENT", LLAMA_FTYPE_MOSTLY_Q3_0_ROCMFPX_AGENT, " agent/tool-call coherent ROCmFPx Q3 routing", }, + { "Q6_0_ROCMFPX_AGENT", LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT, " agent/tool-call coherent ROCmFPx Q6 routing", }, + { "Q8_0_ROCMFPX_AGENT", LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX_AGENT, " agent/tool-call coherent ROCmFPx Q8 routing", }, + { "Q6_0_ROCMFPX_LEAN", LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_LEAN, " size/speed-biased ROCmFPx Q6 routing", }, + { "Q6_0_ROCMFPX_AGENT_LEAN", LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN, " agent ROCmFPx Q6 routing without Q8-heavy boosts", }, { "Q4_1", LLAMA_FTYPE_MOSTLY_Q4_1, " 4.78G, +0.4511 ppl @ Llama-3-8B", }, { "MXFP4_MOE",LLAMA_FTYPE_MOSTLY_MXFP4_MOE," MXFP4 MoE", }, { "Q5_0", LLAMA_FTYPE_MOSTLY_Q5_0, " 5.21G, +0.1316 ppl @ Llama-3-8B", }, @@ -584,6 +602,9 @@ int llama_quantize(int argc, char ** argv) { if (ftype_str == "COPY") { params.only_copy = true; } + if (ftype_str == "Q4_0_ROCMFP4_EVEN" || ftype_str == "Q4_0_ROCMFP4_FAST_EVEN") { + params.pure = true; + } } else { // argv[arg_idx] is not a valid ftype, so treat it as output path: fname_out = argv[arg_idx]; @@ -603,6 +624,9 @@ int llama_quantize(int argc, char ** argv) { if (ftype_str == "COPY") { params.only_copy = true; } + if (ftype_str == "Q4_0_ROCMFP4_EVEN" || ftype_str == "Q4_0_ROCMFP4_FAST_EVEN") { + params.pure = true; + } arg_idx++; } From 344943ae4a3e44c71709ec7f54ad72e8fd11409f Mon Sep 17 00:00:00 2001 From: Marshall Date: Thu, 20 Aug 2026 23:56:03 +0100 Subject: [PATCH 070/212] vulkan: integer-dot (MMQ/MMVQ) paths for the ROCmFPx types Adds the q8_1 integer-dot-product paths that the first ROCmFPx port left out. The activations are quantized to q8_1 and the weight codes decoded to int8, so the dot product runs on dotPacked4x8EXT with the UE4M3 scales factored out of the inner loop. mul_mat_vecq (decode): Q4_0_ROCMFP4, Q4_0_ROCMFP4_FAST, Q8_0_ROCMFPX mul_mmq (prefill): Q4_0_ROCMFP4, Q4_0_ROCMFP4_FAST Q4_0_ROCMFP4 carries two UE4M3 scales per 32-element block, so its low and high nibbles cannot share an int32 accumulator. Both the mat-vec and the matmul path keep separate accumulators for the two halves and apply the scales independently; the single-scale _FAST variant uses one accumulator. fp3 and fp6 are left on the float path for now: fp3 needs a bit-window gather that does not fit the current cache_b layout, and the fork's fp6 integer path assumes the unpacked block layout that this tree already corrects. Measured on Radeon 8060S (RADV STRIX_HALO), FastContext-4B ROCmFP4, interleaved runs on an idle GPU: batched decode b=1 +0.6% b=8 +4.6% (b=2/4 within noise) tg128 81.0 -> 82.4 t/s pp512 no change MMQ for prefill is neutral on this device, but a control run with stock q8_0 (which has had MMQ upstream all along) behaves the same way, so this is the coopmat matmul already winning on Strix Halo rather than anything specific to these types. The path is kept because it should help devices without coopmat. test-backend-ops passes in full both with the default heuristic and with GGML_VK_FORCE_MMVQ=1: 857 ROCmFPx cases, no failures. Co-Authored-By: Claude Opus 5 (cherry picked from commit e282dfd6dfbaaaadc293bf2cb5d11108088400a2) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 20 +++++++ .../vulkan-shaders/mul_mat_vecq.comp | 2 +- .../vulkan-shaders/mul_mat_vecq_funcs.glsl | 58 +++++++++++++++++++ .../vulkan-shaders/mul_mmq_funcs.glsl | 56 ++++++++++++++++++ .../vulkan-shaders/mul_mmq_shmem_types.glsl | 12 ++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 11 +++- 6 files changed, 155 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 1e0c622f5026..53bc5e412864 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4150,6 +4150,8 @@ static bool ggml_vk_matmul_int_shmem_support(const vk_device& device, const std: case GGML_TYPE_Q5_1: block_a_size = std430_size({{16, 4}, {4, 4}, {fp2_size, fp2_align}}); break; // qs[16/4] + qh + dm(vec2) case GGML_TYPE_Q8_0: block_a_size = std430_size({{32, 4}, {fp_size, fp_align}}); break; // qs[8] + dm case GGML_TYPE_MXFP4: block_a_size = std430_size({{32, 4}, {fp_size, fp_align}}); break; // qs[8] + d + case GGML_TYPE_Q4_0_ROCMFP4: block_a_size = std430_size({{32, 4}, {fp2_size, fp2_align}}); break; // qs[8] + d(vec2) + case GGML_TYPE_Q4_0_ROCMFP4_FAST: block_a_size = std430_size({{32, 4}, {fp_size, fp_align}}); break; // qs[8] + d case GGML_TYPE_Q2_K: block_a_size = std430_size({{ 8, 4}, {2, 2}, {fp2_size, fp2_align}}); break; // qs[2] + scales(u8vec2) + dm(vec2) case GGML_TYPE_Q3_K: block_a_size = std430_size({{16, 4}, {fp2_size, fp2_align}}); break; // qs[4] + d_scales(vec2) case GGML_TYPE_Q4_K: block_a_size = std430_size({{16, 4}, {fp2_size, fp2_align}}); break; // qs[4] + dm(vec2) @@ -5081,6 +5083,8 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MMQ(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_q8_1[GGML_TYPE_Q8_0], matmul_q8_0_q8_1, mmq_wg_denoms, warptile_mmq_int, vk_mat_mat_push_constants, 3, , 0); CREATE_MMQ(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_q8_1[GGML_TYPE_MXFP4], matmul_mxfp4_q8_1, mmq_wg_denoms, warptile_mmq_int, vk_mat_mat_push_constants, 3, , 0); + CREATE_MMQ(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat_q8_1[GGML_TYPE_Q4_0_ROCMFP4], matmul_rocmfp4_q8_1, mmq_wg_denoms, warptile_mmq_int, vk_mat_mat_push_constants, 3, , 0); + CREATE_MMQ(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat_q8_1[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_rocmfp4_fast_q8_1, mmq_wg_denoms, warptile_mmq_int, vk_mat_mat_push_constants, 3, , 0); CREATE_MMQ(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_q8_1[GGML_TYPE_Q2_K], matmul_q2_k_q8_1, mmq_wg_denoms, warptile_mmq_int_k, vk_mat_mat_push_constants, 3, , 0); CREATE_MMQ(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_q8_1[GGML_TYPE_Q3_K], matmul_q3_k_q8_1, mmq_wg_denoms, warptile_mmq_int_k, vk_mat_mat_push_constants, 3, , 0); @@ -5135,6 +5139,8 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MMQ(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_q8_1, mmq_wg_denoms, warptile_mmqid_int, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MMQ(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_q8_1, mmq_wg_denoms, warptile_mmqid_int, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MMQ(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q4_0_ROCMFP4], matmul_id_subgroup_rocmfp4_q8_1, mmq_wg_denoms, warptile_mmqid_int, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); + CREATE_MMQ(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_id_subgroup_rocmfp4_fast_q8_1, mmq_wg_denoms, warptile_mmqid_int, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size); CREATE_MMQ(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_q8_1, mmq_wg_denoms, warptile_mmqid_int_k, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size_16); CREATE_MMQ(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_q8_1, mmq_wg_denoms, warptile_mmqid_int_k, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size_16); @@ -5188,6 +5194,8 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MMQ(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q8_0], matmul_id_q8_0_q8_1, mmq_wg_denoms, warptile_mmqid_int, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MMQ(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_MXFP4], matmul_id_mxfp4_q8_1, mmq_wg_denoms, warptile_mmqid_int, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MMQ(GGML_TYPE_Q4_0_ROCMFP4, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q4_0_ROCMFP4], matmul_id_rocmfp4_q8_1, mmq_wg_denoms, warptile_mmqid_int, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); + CREATE_MMQ(GGML_TYPE_Q4_0_ROCMFP4_FAST, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_id_rocmfp4_fast_q8_1, mmq_wg_denoms, warptile_mmqid_int, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MMQ(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q2_K], matmul_id_q2_k_q8_1, mmq_wg_denoms, warptile_mmqid_int_k, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); CREATE_MMQ(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_Q3_K], matmul_id_q3_k_q8_1, mmq_wg_denoms, warptile_mmqid_int_k, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0); @@ -5511,6 +5519,9 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q8_0][i], "mul_mat_vec_q8_0_q8_1_f32", arr_dmmv_q8_0_q8_1_f32_len[reduc], arr_dmmv_q8_0_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_int_n(1*rm_stdq_int, i), 1, 1}, {wg_size_subgroup_int, rm_int_n(1*rm_stdq_int, i), i+1}, 1, true, use_subgroups, subgroup_size_int); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_MXFP4][i], "mul_mat_vec_mxfp4_q8_1_f32", arr_dmmv_mxfp4_q8_1_f32_len[reduc], arr_dmmv_mxfp4_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_int_n(2*rm_stdq_int, i), 1, 1}, {wg_size_subgroup_int, rm_int_n(2*rm_stdq_int, i), i+1}, 1, true, use_subgroups, subgroup_size_int); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q4_0_ROCMFP4][i], "mul_mat_vec_rocmfp4_q8_1_f32", arr_dmmv_rocmfp4_q8_1_f32_len[reduc], arr_dmmv_rocmfp4_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_int_n(2*rm_stdq_int, i), 1, 1}, {wg_size_subgroup_int, rm_int_n(2*rm_stdq_int, i), i+1}, 1, true, use_subgroups, subgroup_size_int); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q4_0_ROCMFP4_FAST][i], "mul_mat_vec_rocmfp4_fast_q8_1_f32", arr_dmmv_rocmfp4_fast_q8_1_f32_len[reduc], arr_dmmv_rocmfp4_fast_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_int_n(2*rm_stdq_int, i), 1, 1}, {wg_size_subgroup_int, rm_int_n(2*rm_stdq_int, i), i+1}, 1, true, use_subgroups, subgroup_size_int); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q8_0_ROCMFPX][i], "mul_mat_vec_rocmfpx_fp8_q8_1_f32", arr_dmmv_rocmfpx_fp8_q8_1_f32_len[reduc], arr_dmmv_rocmfpx_fp8_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_int_n(2*rm_stdq_int, i), 1, 1}, {wg_size_subgroup_int, rm_int_n(2*rm_stdq_int, i), i+1}, 1, true, use_subgroups, subgroup_size_int); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q2_K][i], "mul_mat_vec_q2_k_q8_1_f32", arr_dmmv_q2_k_q8_1_f32_len[reduc], arr_dmmv_q2_k_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_int_n(2*rm_kq_int, i), 1, 1}, {wg_size_subgroup_int, rm_int_n(2*rm_kq_int, i), i+1}, 1, true, use_subgroups, subgroup_size_int); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_q8_1_f32[w][GGML_TYPE_Q3_K][i], "mul_mat_vec_q3_k_q8_1_f32", arr_dmmv_q3_k_q8_1_f32_len[reduc], arr_dmmv_q3_k_q8_1_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_int_n(1*rm_kq_int, i), 1, 1}, {wg_size_subgroup_int, rm_int_n(1*rm_kq_int, i), i+1}, 1, true, use_subgroups, subgroup_size_int); @@ -5571,6 +5582,9 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_q8_1_f32[w][GGML_TYPE_Q8_0], "mul_mat_vec_id_q8_0_q8_1_f32", arr_dmmv_id_q8_0_q8_1_f32_len[reduc], arr_dmmv_id_q8_0_q8_1_f32_data[reduc], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_id(1*rm_stdq_int), 1, 1}, {wg_size_subgroup_int, rm_id(1*rm_stdq_int)}, 1, true, use_subgroups, subgroup_size_int); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_q8_1_f32[w][GGML_TYPE_MXFP4], "mul_mat_vec_id_mxfp4_q8_1_f32", arr_dmmv_id_mxfp4_q8_1_f32_len[reduc], arr_dmmv_id_mxfp4_q8_1_f32_data[reduc], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_id(2*rm_stdq_int), 1, 1}, {wg_size_subgroup_int, rm_id(2*rm_stdq_int)}, 1, true, use_subgroups, subgroup_size_int); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_q8_1_f32[w][GGML_TYPE_Q4_0_ROCMFP4], "mul_mat_vec_id_rocmfp4_q8_1_f32", arr_dmmv_id_rocmfp4_q8_1_f32_len[reduc], arr_dmmv_id_rocmfp4_q8_1_f32_data[reduc], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_id(2*rm_stdq_int), 1, 1}, {wg_size_subgroup_int, rm_id(2*rm_stdq_int)}, 1, true, use_subgroups, subgroup_size_int); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_q8_1_f32[w][GGML_TYPE_Q4_0_ROCMFP4_FAST], "mul_mat_vec_id_rocmfp4_fast_q8_1_f32", arr_dmmv_id_rocmfp4_fast_q8_1_f32_len[reduc], arr_dmmv_id_rocmfp4_fast_q8_1_f32_data[reduc], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_id(2*rm_stdq_int), 1, 1}, {wg_size_subgroup_int, rm_id(2*rm_stdq_int)}, 1, true, use_subgroups, subgroup_size_int); + ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_q8_1_f32[w][GGML_TYPE_Q8_0_ROCMFPX], "mul_mat_vec_id_rocmfpx_fp8_q8_1_f32", arr_dmmv_id_rocmfpx_fp8_q8_1_f32_len[reduc], arr_dmmv_id_rocmfpx_fp8_q8_1_f32_data[reduc], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_id(2*rm_stdq_int), 1, 1}, {wg_size_subgroup_int, rm_id(2*rm_stdq_int)}, 1, true, use_subgroups, subgroup_size_int); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_q8_1_f32[w][GGML_TYPE_Q2_K], "mul_mat_vec_id_q2_k_q8_1_f32", arr_dmmv_id_q2_k_q8_1_f32_len[reduc], arr_dmmv_id_q2_k_q8_1_f32_data[reduc], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_id(2*rm_kq_int), 1, 1}, {wg_size_subgroup_int, rm_id(2*rm_kq_int)}, 1, true, use_subgroups, subgroup_size_int); ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_q8_1_f32[w][GGML_TYPE_Q3_K], "mul_mat_vec_id_q3_k_q8_1_f32", arr_dmmv_id_q3_k_q8_1_f32_len[reduc], arr_dmmv_id_q3_k_q8_1_f32_data[reduc], "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_id(1*rm_kq_int), 1, 1}, {wg_size_subgroup_int, rm_id(1*rm_kq_int)}, 1, true, use_subgroups, subgroup_size_int); @@ -8049,6 +8063,9 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec(ggml_backend_vk_context * case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: case GGML_TYPE_MXFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -8226,6 +8243,9 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec_id(ggml_backend_vk_context case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: case GGML_TYPE_MXFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + case GGML_TYPE_Q8_0_ROCMFPX: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp index 18d441ead40e..f2f52560e674 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp @@ -13,7 +13,7 @@ layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; #if defined(DATA_A_Q2_0) || defined(DATA_A_QUANT_K) #define K_PER_ITER 16 -#elif defined(DATA_A_QUANT_LEGACY) || defined(DATA_A_MXFP4) +#elif defined(DATA_A_QUANT_LEGACY) || defined(DATA_A_MXFP4) || defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) || defined(DATA_A_ROCMFPX_FP8) #define K_PER_ITER 8 #elif defined(DATA_A_IQ1_S) || defined(DATA_A_IQ1_M) #define K_PER_ITER 32 diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl index a5403ac82121..2bbf6ff6cd59 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl @@ -137,6 +137,64 @@ FLOAT_TYPE mul_q8_1(const int32_t q_sum, const float da, const vec2 dsb, const i } #endif +#if defined(DATA_A_ROCMFP4) +// dual UE4M3 scales: e[0] covers elements 0-15, e[1] covers 16-31 +FLOAT_TYPEV2 get_dm2(uint ib) { + return FLOAT_TYPEV2(ue4m3_to_fp32(data_a[ib].e[0]), ue4m3_to_fp32(data_a[ib].e[1])); +} +#endif + +#if defined(DATA_A_ROCMFP4_FAST) +FLOAT_TYPEV2 get_dm2(uint ib) { + const FLOAT_TYPE d = FLOAT_TYPE(ue4m3_to_fp32(data_a[ib].e)); + return FLOAT_TYPEV2(d, d); +} +#endif + +#if defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) +// 1-byte quant loads for ROCmFP4 blocks (18 / 17 bytes). Low nibbles are the +// first half of the block, high nibbles the second half. +i32vec2 repack(uint ib, uint iqs) { + const uint32_t qs = pack32(u8vec4(data_a[ib].qs[iqs * 4 ], + data_a[ib].qs[iqs * 4 + 1], + data_a[ib].qs[iqs * 4 + 2], + data_a[ib].qs[iqs * 4 + 3])); + + const u8vec4 i_a0 = unpack8( qs & 0x0F0F0F0F); + const u8vec4 i_a1 = unpack8((qs >> 4) & 0x0F0F0F0F); + + return i32vec2(pack32(i8vec4(kvalues_rocmfp4[i_a0.x], kvalues_rocmfp4[i_a0.y], kvalues_rocmfp4[i_a0.z], kvalues_rocmfp4[i_a0.w])), + pack32(i8vec4(kvalues_rocmfp4[i_a1.x], kvalues_rocmfp4[i_a1.y], kvalues_rocmfp4[i_a1.z], kvalues_rocmfp4[i_a1.w]))); +} + +FLOAT_TYPE mmvq_dot_product(const uint ib_a, const uint iqs) { + const i32vec2 data_a_qs = repack(ib_a, iqs); + + // the two halves carry different scales, so they cannot share an accumulator + const int32_t q_sum0 = dotPacked4x8EXT(data_a_qs.x, cache_b_qs[0]); + const int32_t q_sum1 = dotPacked4x8EXT(data_a_qs.y, cache_b_qs[1]); + + const FLOAT_TYPEV2 d = get_dm2(ib_a); + return FLOAT_TYPE(cache_b_ds.x * (float(q_sum0) * float(d.x) + float(q_sum1) * float(d.y))); +} +#endif + +#if defined(DATA_A_ROCMFPX_FP8) +// int8 values with one UE4M3 scale per 32-element block +FLOAT_TYPE mmvq_dot_product(const uint ib_a, const uint iqs) { + const int32_t qs0 = pack32(i8vec4(data_a[ib_a].qs[iqs * 8 ], data_a[ib_a].qs[iqs * 8 + 1], + data_a[ib_a].qs[iqs * 8 + 2], data_a[ib_a].qs[iqs * 8 + 3])); + const int32_t qs1 = pack32(i8vec4(data_a[ib_a].qs[iqs * 8 + 4], data_a[ib_a].qs[iqs * 8 + 5], + data_a[ib_a].qs[iqs * 8 + 6], data_a[ib_a].qs[iqs * 8 + 7])); + + const int32_t q_sum = dotPacked4x8EXT(qs0, cache_b_qs[0]) + + dotPacked4x8EXT(qs1, cache_b_qs[1]); + + const FLOAT_TYPE d = FLOAT_TYPE(ue4m3_to_fp32(data_a[ib_a].e)); + return FLOAT_TYPE(cache_b_ds.x * float(d) * float(q_sum)); +} +#endif + #if defined(DATA_A_MXFP4) // 1-byte loads for mxfp4 blocks (17 bytes) i32vec2 repack(uint ib, uint iqs) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.glsl index 24da4f715f83..478a65843b26 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_funcs.glsl @@ -178,6 +178,62 @@ ACC_TYPE mmq_dot_product(const uint ib_a) { } #endif +#if defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) +// 1-byte loads for ROCmFP4 blocks (18 / 17 bytes). Low nibbles are the first +// half of the block, high nibbles the second half. +void block_a_to_shmem(const uint buf_ib, const uint ib, const uint iqs) { + const uint32_t qs = pack32(u8vec4(data_a[ib].qs[iqs * 4 ], + data_a[ib].qs[iqs * 4 + 1], + data_a[ib].qs[iqs * 4 + 2], + data_a[ib].qs[iqs * 4 + 3])); + + const u8vec4 i_a0 = unpack8( qs & 0x0F0F0F0F); + const u8vec4 i_a1 = unpack8((qs >> 4) & 0x0F0F0F0F); + + buf_a[buf_ib].qs[iqs ] = pack32(i8vec4(kvalues_rocmfp4[i_a0.x], kvalues_rocmfp4[i_a0.y], kvalues_rocmfp4[i_a0.z], kvalues_rocmfp4[i_a0.w])); + buf_a[buf_ib].qs[iqs + 4] = pack32(i8vec4(kvalues_rocmfp4[i_a1.x], kvalues_rocmfp4[i_a1.y], kvalues_rocmfp4[i_a1.z], kvalues_rocmfp4[i_a1.w])); + + if (iqs == 0) { +#if defined(DATA_A_ROCMFP4) + buf_a[buf_ib].d = FLOAT_TYPEV2(ue4m3_to_fp32(data_a[ib].e[0]), + ue4m3_to_fp32(data_a[ib].e[1])); +#else + buf_a[buf_ib].d = FLOAT_TYPE(ue4m3_to_fp32(data_a[ib].e)); +#endif + } +} + +void block_a_to_registers(const uint reg_ib, const uint buf_ib) { + cache_a[reg_ib].d = buf_a[buf_ib].d; + + [[unroll]] for (uint iqs = 0; iqs < 8; iqs++) { + cache_a[reg_ib].qs[iqs] = buf_a[buf_ib].qs[iqs]; + } +} + +ACC_TYPE mmq_dot_product(const uint ib_a) { +#if defined(DATA_A_ROCMFP4) + // the halves carry different scales, so they need separate accumulators + int32_t q_sum0 = 0; + int32_t q_sum1 = 0; + [[unroll]] for (uint iqs = 0; iqs < 4; iqs++) { + q_sum0 += dotPacked4x8EXT(cache_a[ib_a].qs[iqs], cache_b.qs[iqs]); + q_sum1 += dotPacked4x8EXT(cache_a[ib_a].qs[iqs + 4], cache_b.qs[iqs + 4]); + } + + return ACC_TYPE((float(q_sum0) * float(cache_a[ib_a].d.x) + + float(q_sum1) * float(cache_a[ib_a].d.y)) * float(cache_b.ds.x)); +#else + int32_t q_sum = 0; + [[unroll]] for (uint iqs = 0; iqs < 8; iqs++) { + q_sum += dotPacked4x8EXT(cache_a[ib_a].qs[iqs], cache_b.qs[iqs]); + } + + return ACC_TYPE(float(q_sum) * float(cache_a[ib_a].d) * float(cache_b.ds.x)); +#endif +} +#endif + #if defined(DATA_A_MXFP4) // 1-byte loads for mxfp4 blocks (17 bytes) void block_a_to_shmem(const uint buf_ib, const uint ib, const uint iqs) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_shmem_types.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_shmem_types.glsl index 2b7adcb6c2fc..1995acaf358f 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_shmem_types.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq_shmem_types.glsl @@ -59,6 +59,18 @@ struct block_a_cache { int32_t qs[8]; FLOAT_TYPE d; }; +#elif defined(DATA_A_ROCMFP4) +#define QUANT_R_MMQ 2 +struct block_a_cache { + int32_t qs[8]; + FLOAT_TYPEV2 d; // one UE4M3 scale per half block +}; +#elif defined(DATA_A_ROCMFP4_FAST) +#define QUANT_R_MMQ 2 +struct block_a_cache { + int32_t qs[8]; + FLOAT_TYPE d; +}; #elif defined(DATA_A_Q2_K) #define QUANT_R_MMQ 4 struct block_a_cache { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index dede9ad041b2..1a57fdc24e8c 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -630,7 +630,8 @@ void matmul_shaders(bool fp16, MatMulIdType matmul_id_type, bool coopmat, bool c #if defined(GGML_VULKAN_INTEGER_DOT_GLSLC_SUPPORT) // Integer dot mmq performs better with f32 accumulators (different shader, skip for dot2) - if (!f16acc && !coopmat && !coopmat2 && !dot2 && (is_legacy_quant(tname) || is_k_quant(tname) || tname == "mxfp4")) { + if (!f16acc && !coopmat && !coopmat2 && !dot2 && (is_legacy_quant(tname) || is_k_quant(tname) || tname == "mxfp4" || + tname == "rocmfp4" || tname == "rocmfp4_fast")) { string_to_spv(shader_name + "_" + tname + "_q8_1", "mul_mmq.comp", merge_maps(merge_maps(base_dict, float_type_dict), {{data_a_key, "1"}, {"D_TYPE", "float"},}), fp16, coopmat, coopmat2, f16acc); } #endif @@ -772,7 +773,9 @@ void process_shaders() { // mul mat vec with integer dot product #if defined(GGML_VULKAN_INTEGER_DOT_GLSLC_SUPPORT) - if (is_legacy_quant(tname) || tname == "mxfp4" || is_k_quant(tname) || tname == "iq1_s" || tname == "iq1_m") { + if (is_legacy_quant(tname) || tname == "mxfp4" || + tname == "rocmfp4" || tname == "rocmfp4_fast" || tname == "rocmfpx_fp8" || + is_k_quant(tname) || tname == "iq1_s" || tname == "iq1_m") { string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32", "mul_mat_vecq.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPEV2", "vec2"}, {"ACC_TYPE", "float"}})); string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32_subgroup", "mul_mat_vecq.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPEV2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}})); string_to_spv("mul_mat_vec_" + tname + "_q8_1_f32_subgroup_no_shmem", "mul_mat_vecq.comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float"}, {"FLOAT_TYPE", "float"}, {"FLOAT_TYPEV2", "vec2"}, {"ACC_TYPE", "float"}, {"USE_SUBGROUP_ADD_NO_SHMEM", "1"}})); @@ -1279,7 +1282,9 @@ void write_output_files() { for (const std::string& btype : btypes) { for (const auto& tname : type_names) { - if (btype == "q8_1" && !is_legacy_quant(tname) && tname != "mxfp4" && !is_k_quant(tname) && tname != "iq1_s" && tname != "iq1_m") { + if (btype == "q8_1" && !is_legacy_quant(tname) && tname != "mxfp4" && + tname != "rocmfp4" && tname != "rocmfp4_fast" && tname != "rocmfpx_fp8" && + !is_k_quant(tname) && tname != "iq1_s" && tname != "iq1_m") { continue; } hdr << "extern const void * arr_dmmv_" << tname << "_" << btype << "_f32_data[3];\n"; From 1fc5ee3bec0956fa1945eff0599d171c8fa43a8c Mon Sep 17 00:00:00 2001 From: Marshall Date: Fri, 21 Aug 2026 10:13:40 +0100 Subject: [PATCH 071/212] vulkan: fix ROCmFPx mat-vec cost at batch 3-8 Speculative decoding verifies at batch 3-8, and both ROCmFPx mat-vec paths were mis-tuned for exactly that range, so the FP4 27B lost to a K-quant of the same model under DFlash2 despite being 12% smaller. mul_mat_vecq.comp: K_PER_ITER 8 -> 32 for ROCMFP4/_FAST. Each MMVQ call handled a quarter of a block and re-decoded both UE4M3 scales every time. A whole block per call amortises them 4x and lets the eight B dwords load as two dwordx4 instead of two scattered dwords. dequant_funcs.glsl: dequantize4 for fp6 and fp3 gathered each weight through its own bit window with a branch, and did one scale-table lookup per weight. Every caller passes iqs % 4 == 0, so four codes are three whole bytes (fp6) or twelve bits inside two (fp3) and all four share one scale. The branch was also stopping the decode being hoisted out of the NUM_COLS loop, which is why the old cost scaled linearly with batch size. fp6 matters for an "FP4" model because the GGUFs are mixed: the 27B carries output.weight as Q6_0_ROCMFPX, ~22 ms/step at batch 4 on its own. Radeon 8060S, m=4096 k=14336, n=8, us/run: q4_0_rocmfp4 312 -> 173 q6_0_rocmfpx 2236 -> 402 q3_0_rocmfpx 1942 -> 463 Qwen3.8-27B ROCMFPX-MQ-Q4 end to end: batched-bench TG at B=8 36.3 -> 60.0 t/s, DFlash2 spec decoding 16.8 -> 24.6 t/s at n-max 3. test-backend-ops 17920/17920, also with GGML_VK_FORCE_MMVQ=1. Co-Authored-By: Claude Opus 5 (cherry picked from commit f0a2bd6b34e83b4282d62b085e2d21ef2d241864) --- .../vulkan-shaders/dequant_funcs.glsl | 32 ++++++++++++------- .../vulkan-shaders/mul_mat_vecq.comp | 16 ++++++++-- .../vulkan-shaders/mul_mat_vecq_funcs.glsl | 14 +++++--- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl index a8275564b808..a10636d5fdbb 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl @@ -567,15 +567,15 @@ float rocmfpx_fp3_dequant(uint ib, uint idx, uint a_offset) { return float(rocmfpx_fp3_decode_code(rocmfpx_fp3_get_bits(ib, idx * 3u, a_offset))) * d; } +// idx is a multiple of 4 at every call site, so bit_pos is a multiple of 12 and +// the shift is 0 or 4: the twelve bits always fit in two bytes, no third byte +// and no branch. int32_t rocmfpx_fp3_pack4_window(uint ib, uint idx, uint a_offset) { const uint bit_pos = idx * 3u; const uint byte_pos = bit_pos >> 3u; const uint sh = bit_pos & 7u; uint bits = uint(data_a[a_offset + ib].qs[byte_pos]) | (uint(data_a[a_offset + ib].qs[byte_pos + 1u]) << 8); - if (sh > 4u) { - bits |= uint(data_a[a_offset + ib].qs[byte_pos + 2u]) << 16; - } bits = (bits >> sh) & 0xFFFu; return pack32(i8vec4(kvalues_rocmfpx_fp3_const[ bits & 7u], kvalues_rocmfpx_fp3_const[(bits >> 3) & 7u], @@ -585,10 +585,9 @@ int32_t rocmfpx_fp3_pack4_window(uint ib, uint idx, uint a_offset) { vec4 rocmfpx_fp3_dequant4(uint ib, uint idx, uint a_offset) { const vec4 q = vec4(unpack8(rocmfpx_fp3_pack4_window(ib, idx, a_offset))); - return q * vec4(ue4m3_to_fp32(data_a[a_offset + ib].e[(idx + 0u) >= 16u ? 1u : 0u]), - ue4m3_to_fp32(data_a[a_offset + ib].e[(idx + 1u) >= 16u ? 1u : 0u]), - ue4m3_to_fp32(data_a[a_offset + ib].e[(idx + 2u) >= 16u ? 1u : 0u]), - ue4m3_to_fp32(data_a[a_offset + ib].e[(idx + 3u) >= 16u ? 1u : 0u])); + // idx is a multiple of 4 and the halves split at element 16, so all four + // weights share one scale + return q * ue4m3_to_fp32(data_a[a_offset + ib].e[idx >= 16u ? 1u : 0u]); } vec2 dequantize(uint ib, uint iqs, uint a_offset) { @@ -623,11 +622,22 @@ vec2 dequantize(uint ib, uint iqs, uint a_offset) { rocmfpx_fp6_dequant(ib, iqs + 1u, a_offset)); } +// Every caller passes a multiple of 4, which makes this the fast case: four +// 6-bit codes are exactly three whole bytes, so the gather needs no per-weight +// bit window and no branch, and all four share one scale (the halves split at +// element 16). rocmfpx_fp6_dequant() above stays for the unaligned callers. vec4 dequantize4(uint ib, uint iqs, uint a_offset) { - return vec4(rocmfpx_fp6_dequant(ib, iqs + 0u, a_offset), - rocmfpx_fp6_dequant(ib, iqs + 1u, a_offset), - rocmfpx_fp6_dequant(ib, iqs + 2u, a_offset), - rocmfpx_fp6_dequant(ib, iqs + 3u, a_offset)); + const uint byte_pos = (iqs >> 2u) * 3u; + const uint bits = uint(data_a[a_offset + ib].qs[byte_pos ]) + | uint(data_a[a_offset + ib].qs[byte_pos + 1u]) << 8 + | uint(data_a[a_offset + ib].qs[byte_pos + 2u]) << 16; + + const float d = ue4m3_to_fp32(data_a[a_offset + ib].e[iqs >= 16u ? 1u : 0u]); + + return vec4(float(rocmfpx_fp6_decode_code( bits & 0x3Fu)), + float(rocmfpx_fp6_decode_code((bits >> 6u) & 0x3Fu)), + float(rocmfpx_fp6_decode_code((bits >> 12u) & 0x3Fu)), + float(rocmfpx_fp6_decode_code((bits >> 18u) & 0x3Fu))) * d; } #endif diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp index f2f52560e674..0ebdde36cd71 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq.comp @@ -13,7 +13,12 @@ layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; #if defined(DATA_A_Q2_0) || defined(DATA_A_QUANT_K) #define K_PER_ITER 16 -#elif defined(DATA_A_QUANT_LEGACY) || defined(DATA_A_MXFP4) || defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) || defined(DATA_A_ROCMFPX_FP8) +#elif defined(DATA_A_ROCMFP4) || defined(DATA_A_ROCMFP4_FAST) +// A whole block per call. The ROCmFP4 scales are UE4M3 and cost a byte load plus +// a table lookup each, so decoding them once per 32 weights instead of once per +// 8 is worth the extra B registers. +#define K_PER_ITER 32 +#elif defined(DATA_A_QUANT_LEGACY) || defined(DATA_A_MXFP4) || defined(DATA_A_ROCMFPX_FP8) #define K_PER_ITER 8 #elif defined(DATA_A_IQ1_S) || defined(DATA_A_IQ1_M) #define K_PER_ITER 32 @@ -37,9 +42,16 @@ void iter(inout FLOAT_TYPE temp[NUM_COLS][NUM_ROWS], const uint first_row, const cache_b_ds = vec2(data_b[b_block_idx_outer].ds[b_block_idx_inner]); #if QUANT_R == 2 - // Assumes K_PER_ITER == 8 +#if K_PER_ITER == 8 cache_b_qs[0] = data_b[b_block_idx_outer].qs[b_block_idx_inner * 8 + b_qs_idx]; cache_b_qs[1] = data_b[b_block_idx_outer].qs[b_block_idx_inner * 8 + b_qs_idx + 4]; +#elif K_PER_ITER == 32 + [[unroll]] for (uint k = 0; k < 8; ++k) { + cache_b_qs[k] = data_b[b_block_idx_outer].qs[b_block_idx_inner * 8 + k]; + } +#else +#error unimplemented +#endif #else #if K_PER_ITER == 8 cache_b_qs[0] = data_b[b_block_idx_outer].qs[b_block_idx_inner * 8 + b_qs_idx * 2]; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl index 2bbf6ff6cd59..bded206808c6 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vecq_funcs.glsl @@ -167,12 +167,18 @@ i32vec2 repack(uint ib, uint iqs) { pack32(i8vec4(kvalues_rocmfp4[i_a1.x], kvalues_rocmfp4[i_a1.y], kvalues_rocmfp4[i_a1.z], kvalues_rocmfp4[i_a1.w]))); } +// K_PER_ITER is 32 for these types: one call consumes a whole block, so the +// UE4M3 scales are decoded once per block rather than once per 8 weights. FLOAT_TYPE mmvq_dot_product(const uint ib_a, const uint iqs) { - const i32vec2 data_a_qs = repack(ib_a, iqs); - // the two halves carry different scales, so they cannot share an accumulator - const int32_t q_sum0 = dotPacked4x8EXT(data_a_qs.x, cache_b_qs[0]); - const int32_t q_sum1 = dotPacked4x8EXT(data_a_qs.y, cache_b_qs[1]); + int32_t q_sum0 = 0; + int32_t q_sum1 = 0; + + [[unroll]] for (uint k = 0; k < 4; ++k) { + const i32vec2 data_a_qs = repack(ib_a, k); + q_sum0 += dotPacked4x8EXT(data_a_qs.x, cache_b_qs[k]); + q_sum1 += dotPacked4x8EXT(data_a_qs.y, cache_b_qs[k + 4]); + } const FLOAT_TYPEV2 d = get_dm2(ib_a); return FLOAT_TYPE(cache_b_ds.x * (float(q_sum0) * float(d.x) + float(q_sum1) * float(d.y))); From a99005f6192472b5b473921c99b38a12466c4b76 Mon Sep 17 00:00:00 2001 From: Marshall Date: Mon, 31 Aug 2026 20:02:40 +0100 Subject: [PATCH 072/212] tests : set quantization error budgets for the ROCmFPx 2/3-bit types q3_0_rocmfpx and q2_0_rocmfpx had no entry in either threshold table, so both fell through to the defaults meant for 4-bit and wider types and failed. Budget them by width instead: q3_0_rocmfpx with the other 3-bit types, q2_0_rocmfpx with q2_0. The measured errors are in family for their widths - q2_0_rocmfpx (0.004768 quant / 0.059435 dot) is better than q2_0 (0.008672 / 0.140697) and tq2_0 (0.008681 / 0.141345), which already pass. Reference implementation error is 0.000000 for both, and test-backend-ops MUL_MAT on CPU passes 11/11 cases for each, so the vec_dot path is fine. Assisted-by: Claude Opus 5 --- tests/test-quantize-fns.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test-quantize-fns.cpp b/tests/test-quantize-fns.cpp index 9510ac14ce00..8a109dcbe12c 100644 --- a/tests/test-quantize-fns.cpp +++ b/tests/test-quantize-fns.cpp @@ -164,7 +164,10 @@ static int test_vec_dot_q(bool verbose) { type == GGML_TYPE_Q3_K ? MAX_QUANTIZATION_TOTAL_ERROR_3BITS : type == GGML_TYPE_IQ3_S ? MAX_QUANTIZATION_TOTAL_ERROR_3BITS : type == GGML_TYPE_IQ3_XXS ? MAX_QUANTIZATION_TOTAL_ERROR_3BITS_XXS : - type == GGML_TYPE_NVFP4 ? MAX_QUANTIZATION_TOTAL_ERROR_FP4 : MAX_QUANTIZATION_TOTAL_ERROR; + type == GGML_TYPE_NVFP4 ? MAX_QUANTIZATION_TOTAL_ERROR_FP4 : + // ROCmFPx sub-4-bit types: same budget as the other types of their width + type == GGML_TYPE_Q3_0_ROCMFPX ? MAX_QUANTIZATION_TOTAL_ERROR_3BITS : + type == GGML_TYPE_Q2_0_ROCMFPX ? MAX_QUANTIZATION_TOTAL_ERROR_2BITS : MAX_QUANTIZATION_TOTAL_ERROR; bool failed = !(total_error < max_quantization_error); num_failed += failed; if (failed || verbose) { @@ -180,11 +183,13 @@ static int test_vec_dot_q(bool verbose) { const float vec_dot_error = dot_product_error(qfns, qfns_cpu, test_size, test_data.data(), test_data2.data()); const float max_allowed_error = type == GGML_TYPE_Q2_K || type == GGML_TYPE_IQ2_XS || type == GGML_TYPE_IQ2_XXS || - type == GGML_TYPE_IQ3_XXS || type == GGML_TYPE_IQ3_S || type == GGML_TYPE_IQ2_S + type == GGML_TYPE_IQ3_XXS || type == GGML_TYPE_IQ3_S || type == GGML_TYPE_IQ2_S || + type == GGML_TYPE_Q3_0_ROCMFPX ? MAX_DOT_PRODUCT_ERROR_LOWBIT : type == GGML_TYPE_Q1_0 ? MAX_DOT_PRODUCT_ERROR_BINARY - : type == GGML_TYPE_TQ1_0 || type == GGML_TYPE_TQ2_0 || type == GGML_TYPE_Q2_0 + : type == GGML_TYPE_TQ1_0 || type == GGML_TYPE_TQ2_0 || type == GGML_TYPE_Q2_0 || + type == GGML_TYPE_Q2_0_ROCMFPX ? MAX_DOT_PRODUCT_ERROR_TERNARY : type == GGML_TYPE_NVFP4 ? MAX_DOT_PRODUCT_ERROR_FP4 From c00341d63b4762c3b75a13828e94480dc987bb37 Mon Sep 17 00:00:00 2001 From: Marshall Date: Mon, 31 Aug 2026 20:10:21 +0100 Subject: [PATCH 073/212] rocmfpx : credit charlie12345/ROCmFPX as the origin of the format The ROCmFPx sources here were hand-ported from ciru-ai/ROCmFPX, which is itself a fork of charlie12345/ROCmFPX, where the ROCmFP4 format originates. Adds the MIT attribution header to each ported source and records both projects in the README credits and ROCMFPX-NOTES.md. Assisted-by: Claude Opus 5 --- README.md | 3 +++ ggml/rocmfp4/rocmfp4.c | 8 ++++++++ ggml/rocmfp4/rocmfp4.h | 8 ++++++++ ggml/rocmfp4/rocmfp4_hip.cu | 8 ++++++++ ggml/rocmfp4/rocmfp4_hip_codebook.cuh | 8 ++++++++ ggml/rocmfp4/rocmfp4_hip_scale.cuh | 8 ++++++++ ggml/rocmfpx/rocmfp2_reference.c | 8 ++++++++ ggml/rocmfpx/rocmfp2_reference.h | 8 ++++++++ ggml/rocmfpx/rocmfpx.c | 8 ++++++++ ggml/rocmfpx/rocmfpx.h | 8 ++++++++ ggml/rocmfpx/rocmfpx_hip_codebook.cuh | 8 ++++++++ ggml/rocmfpx/test_rocmfp2_reference.c | 8 ++++++++ ggml/rocmfpx/test_rocmfpx.c | 8 ++++++++ 13 files changed, 99 insertions(+) diff --git a/README.md b/README.md index 623d80a55975..65236f8387d9 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ Everything else is upstream `llama.cpp`. The additions currently carried here: | ROCm/HIP grouped decode matvecs | `GGML_CUDA_DISABLE_MMV_GROUP=1` | Consecutive single-column matvecs that read the same activation (gate/up pairs, hyper-connection projections) launch as one kernel | | ROCm/HIP flash attention on RDNA 3.5 | | WMMA path for D=256 prefill at depth, Q8_0 KV tile kernel for decode | | Speculative checkpoints on device | | `llama-server` keeps speculative-decoding checkpoints in device memory instead of copying them to the host | +| ROCmFPx quant types | `llama-quantize` types `Q4_0_ROCMFP4`, `Q4_0_ROCMFP4_FAST`, `Q2/Q3/Q6/Q8_0_ROCMFPX` and the `_LEAN`/`_COHERENT`/`_STRIX` recipes | Loads the ROCmFP4 GGUFs published for Strix Halo. CPU codecs plus Vulkan dequant, mat-vec, matmul and integer-dot kernels; the coopmat2 flash-attention path does not decode them yet | | Repeatable output at depth | | Freed KV cells are zeroed so masked-out rows never carry stale K/V, and the Vulkan radix top-k assigns output slots deterministically | Every ROCm/HIP change above is guarded on architecture, shape and layout, so other devices see upstream behaviour. @@ -175,6 +176,8 @@ CI runs the standard llama.cpp suite plus a self-hosted `gfx1151` ROCm job on re This project is a fork and owes everything to the people who built what it forks: - [ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp) and [ggml](https://github.com/ggml-org/ggml) - Georgi Gerganov and the llama.cpp contributors - MIT license +- [charlie12345/ROCmFPX](https://github.com/charlie12345/ROCmFPX) - the origin of the ROCmFPx project and the creator of the ROCmFP4 format - MIT license +- [ciru-ai/ROCmFPX](https://github.com/ciru-ai/ROCmFPX) - a fork of the above, and the tree the ROCmFPx quant formats and reference codecs here were hand-ported from (it shares no git history with llama.cpp, so it cannot be merged) - MIT license - [yhirose/cpp-httplib](https://github.com/yhirose/cpp-httplib) - Single-header HTTP server, used by `llama-server` - MIT license - [nothings/stb](https://github.com/nothings/stb) - Single-header image format decoder, used by multimodal subsystem - Public domain - [nlohmann/json](https://github.com/nlohmann/json) - Single-header JSON library, used by various tools/examples - MIT License diff --git a/ggml/rocmfp4/rocmfp4.c b/ggml/rocmfp4/rocmfp4.c index bccefcec15da..c957966e16aa 100644 --- a/ggml/rocmfp4/rocmfp4.c +++ b/ggml/rocmfp4/rocmfp4.c @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #define GGML_COMMON_DECL_C #include "../src/ggml-common.h" diff --git a/ggml/rocmfp4/rocmfp4.h b/ggml/rocmfp4/rocmfp4.h index 9756f6ad4c3b..a4fb24761cfe 100644 --- a/ggml/rocmfp4/rocmfp4.h +++ b/ggml/rocmfp4/rocmfp4.h @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #pragma once #include diff --git a/ggml/rocmfp4/rocmfp4_hip.cu b/ggml/rocmfp4/rocmfp4_hip.cu index d2c9048c1876..3174743cc41d 100644 --- a/ggml/rocmfp4/rocmfp4_hip.cu +++ b/ggml/rocmfp4/rocmfp4_hip.cu @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #include "rocmfp4.h" #include diff --git a/ggml/rocmfp4/rocmfp4_hip_codebook.cuh b/ggml/rocmfp4/rocmfp4_hip_codebook.cuh index 1d91061129a3..7fa1357f8e8d 100644 --- a/ggml/rocmfp4/rocmfp4_hip_codebook.cuh +++ b/ggml/rocmfp4/rocmfp4_hip_codebook.cuh @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #pragma once #include "rocmfp4_hip_scale.cuh" diff --git a/ggml/rocmfp4/rocmfp4_hip_scale.cuh b/ggml/rocmfp4/rocmfp4_hip_scale.cuh index 19525a4fb017..a783be2e9493 100644 --- a/ggml/rocmfp4/rocmfp4_hip_scale.cuh +++ b/ggml/rocmfp4/rocmfp4_hip_scale.cuh @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #pragma once #include diff --git a/ggml/rocmfpx/rocmfp2_reference.c b/ggml/rocmfpx/rocmfp2_reference.c index 87881cf64df9..039672f5d750 100644 --- a/ggml/rocmfpx/rocmfp2_reference.c +++ b/ggml/rocmfpx/rocmfp2_reference.c @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #include "rocmfp2_reference.h" #include diff --git a/ggml/rocmfpx/rocmfp2_reference.h b/ggml/rocmfpx/rocmfp2_reference.h index 2bc72579b322..42c7e1ecfc15 100644 --- a/ggml/rocmfpx/rocmfp2_reference.h +++ b/ggml/rocmfpx/rocmfp2_reference.h @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #ifndef ROCMFP2_REFERENCE_H #define ROCMFP2_REFERENCE_H diff --git a/ggml/rocmfpx/rocmfpx.c b/ggml/rocmfpx/rocmfpx.c index 5aa075ef8701..b169cd96a88b 100644 --- a/ggml/rocmfpx/rocmfpx.c +++ b/ggml/rocmfpx/rocmfpx.c @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #include "rocmfpx.h" #include diff --git a/ggml/rocmfpx/rocmfpx.h b/ggml/rocmfpx/rocmfpx.h index 579d7e448b59..30f831ee6a43 100644 --- a/ggml/rocmfpx/rocmfpx.h +++ b/ggml/rocmfpx/rocmfpx.h @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #pragma once #include diff --git a/ggml/rocmfpx/rocmfpx_hip_codebook.cuh b/ggml/rocmfpx/rocmfpx_hip_codebook.cuh index 2faa9c3caeb1..b9e36528c2fc 100644 --- a/ggml/rocmfpx/rocmfpx_hip_codebook.cuh +++ b/ggml/rocmfpx/rocmfpx_hip_codebook.cuh @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #pragma once #include diff --git a/ggml/rocmfpx/test_rocmfp2_reference.c b/ggml/rocmfpx/test_rocmfp2_reference.c index 96201fc1e54a..09bd04bdb0c3 100644 --- a/ggml/rocmfpx/test_rocmfp2_reference.c +++ b/ggml/rocmfpx/test_rocmfp2_reference.c @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #include "rocmfp2_reference.h" #include diff --git a/ggml/rocmfpx/test_rocmfpx.c b/ggml/rocmfpx/test_rocmfpx.c index daf58ccc3da7..bb4c6b949561 100644 --- a/ggml/rocmfpx/test_rocmfpx.c +++ b/ggml/rocmfpx/test_rocmfpx.c @@ -1,3 +1,11 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + #include "rocmfpx.h" #include From 3e779bbcbc487b073af670b9bc9f43da4f6de64f Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 7 Sep 2026 00:12:38 +0100 Subject: [PATCH 074/212] llama : give Q2_0_ROCMFPX the ftype id ciru-ai/ROCmFPX uses (122) The first port numbered LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX as 119, but in ciru-ai/ROCmFPX 119 is Q7_0_ROCMFPX. Files quantized there with the 2-bit type carry general.file_type 122, so use that id here too, in llama.h and in gguf-py, or the loader prints the wrong file type for them. The tensor type ids were already aligned. Assisted-by: Claude Fable 5.1 --- gguf-py/gguf/constants.py | 2 +- include/llama.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index 6711e325cda9..538f6e4a4890 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -5654,7 +5654,7 @@ class LlamaFileType(IntEnum): MOSTLY_Q8_0_ROCMFPX_AGENT = 115 MOSTLY_Q6_0_ROCMFPX_LEAN = 116 MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN = 117 - MOSTLY_Q2_0_ROCMFPX = 119 # except 1d tensors + MOSTLY_Q2_0_ROCMFPX = 122 # matches ciru-ai/ROCmFPX upstream id; 119 is their Q7_0_ROCMFPX GUESSED = 1024 # not specified in the model file diff --git a/include/llama.h b/include/llama.h index 0468ab2f8d15..1ee38bcaec7d 100644 --- a/include/llama.h +++ b/include/llama.h @@ -173,7 +173,7 @@ extern "C" { LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX_AGENT = 115, // ROCmFPx 8-bit agent/tool-call coherent routing LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_LEAN = 116, // ROCmFPx 6-bit size/speed-biased routing LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN = 117, // ROCmFPx 6-bit agent routing without Q8-heavy boosts - LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX = 119, // ROCmFPx 2-bit S40 codebook + dual UE4M3 scales + LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX = 122, // ROCmFPx 2-bit S40 codebook + dual UE4M3 scales; id matches ciru-ai/ROCmFPX (119 is their Q7_0_ROCMFPX) LLAMA_FTYPE_GUESSED = 1024, // not specified in the model file }; From 2c1cdc431052ab7a5824b478f49a1b23eacaa288 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 7 Sep 2026 00:21:29 +0100 Subject: [PATCH 075/212] tests : run the ROCmFPx codec self-tests under ctest The port brought ggml/rocmfpx/test_rocmfpx.c and test_rocmfp2_reference.c along but never built them, so nothing ran. Register both as ctest targets next to test-quantize-fns: test-rocmfpx covers the ROCmFP3/6/8 codecs and their imatrix weighting, test-rocmfp2-reference the standalone ROCmFP2 reference encoder (layout, scale encoding, golden vectors, non-finite input). Cast the float printf arguments in test_rocmfpx.c so it builds clean under the project's -Wdouble-promotion. Assisted-by: Claude Fable 5.1 --- ggml/rocmfpx/test_rocmfpx.c | 12 ++++++------ tests/CMakeLists.txt | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ggml/rocmfpx/test_rocmfpx.c b/ggml/rocmfpx/test_rocmfpx.c index bb4c6b949561..ff1db79fdd50 100644 --- a/ggml/rocmfpx/test_rocmfpx.c +++ b/ggml/rocmfpx/test_rocmfpx.c @@ -79,7 +79,7 @@ static void check_weighted_imatrix_fp3(void) { const float plain_err = weighted_mse(src, plain, imatrix, N); const float weighted_err = weighted_mse(src, weighted, imatrix, N); - printf("ROCmFP3 imatrix weighted_mse: plain=%g weighted=%g\n", plain_err, weighted_err); + printf("ROCmFP3 imatrix weighted_mse: plain=%g weighted=%g\n", (double) plain_err, (double) weighted_err); assert(weighted_err < plain_err); } @@ -103,7 +103,7 @@ static void check_weighted_imatrix_fp6(void) { const float plain_err = weighted_mse(src, plain, imatrix, N); const float weighted_err = weighted_mse(src, weighted, imatrix, N); - printf("ROCmFP6 imatrix weighted_mse: plain=%g weighted=%g\n", plain_err, weighted_err); + printf("ROCmFP6 imatrix weighted_mse: plain=%g weighted=%g\n", (double) plain_err, (double) weighted_err); assert(weighted_err < plain_err); } @@ -127,7 +127,7 @@ static void check_weighted_imatrix_fp8(void) { const float plain_err = weighted_mse(src, plain, imatrix, N); const float weighted_err = weighted_mse(src, weighted, imatrix, N); - printf("ROCmFP8 imatrix weighted_mse: plain=%g weighted=%g\n", plain_err, weighted_err); + printf("ROCmFP8 imatrix weighted_mse: plain=%g weighted=%g\n", (double) plain_err, (double) weighted_err); assert(weighted_err < plain_err); } @@ -163,13 +163,13 @@ int main(void) { printf("ROCmFP3: block=%zu row=%zu bpw=%.2f mse=%g\n", sizeof(block_rocmfp3), rocmfpx_row_size_fp3(N), - 8.0f*(float) sizeof(block_rocmfp3)/(float) QK_ROCMFP3, mse3); + 8.0*(double) sizeof(block_rocmfp3)/(double) QK_ROCMFP3, (double) mse3); printf("ROCmFP6: block=%zu row=%zu bpw=%.2f mse=%g\n", sizeof(block_rocmfp6), rocmfpx_row_size_fp6(N), - 8.0f*(float) sizeof(block_rocmfp6)/(float) QK_ROCMFP6, mse6); + 8.0*(double) sizeof(block_rocmfp6)/(double) QK_ROCMFP6, (double) mse6); printf("ROCmFP8: block=%zu row=%zu bpw=%.2f mse=%g\n", sizeof(block_rocmfp8), rocmfpx_row_size_fp8(N), - 8.0f*(float) sizeof(block_rocmfp8)/(float) QK_ROCMFP8, mse8); + 8.0*(double) sizeof(block_rocmfp8)/(double) QK_ROCMFP8, (double) mse8); assert(isfinite(mse3)); assert(isfinite(mse6)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c46377c7623d..ec1b142eba85 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -325,6 +325,18 @@ if (NOT GGML_BACKEND_DL) llama_build_and_test(test-barrier.cpp) llama_build_and_test(test-quantize-fns.cpp) llama_build_and_test(test-quantize-perf.cpp) + + # ROCmFPx codec self-tests, kept next to the codecs under ggml/rocmfpx. + # test-rocmfpx exercises the ROCmFP3/6/8 codecs; test-rocmfp2-reference + # checks the standalone ROCmFP2 reference encoder. + llama_build_and_test(${PROJECT_SOURCE_DIR}/ggml/rocmfpx/test_rocmfpx.c + ${PROJECT_SOURCE_DIR}/ggml/rocmfpx/rocmfpx.c + NAME test-rocmfpx) + target_include_directories(test-rocmfpx PRIVATE ${PROJECT_SOURCE_DIR}/ggml/rocmfpx) + llama_build_and_test(${PROJECT_SOURCE_DIR}/ggml/rocmfpx/test_rocmfp2_reference.c + ${PROJECT_SOURCE_DIR}/ggml/rocmfpx/rocmfp2_reference.c + NAME test-rocmfp2-reference) + target_include_directories(test-rocmfp2-reference PRIVATE ${PROJECT_SOURCE_DIR}/ggml/rocmfpx) llama_build_and_test(test-rope.cpp) llama_build_and_test(test-col2im-1d.cpp) endif() From 659e0f1fba0c6b1714a2630858f647978dfbe97e Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 7 Sep 2026 00:21:29 +0100 Subject: [PATCH 076/212] gguf-py : dequantize every ROCmFPx type in Python, and test it against C gguf-py/tests/test_quants.py resolves the C dequantizer by type name, so it could not reach the ROCmFPx codecs (rocmfp4_dequantize_row_q4_0 and friends) and silently skipped them. Add dequantize_row_ wrappers in ggml-quants for the six types so the test exercises them like any other quant. Python side: add dequantizers for Q4_0_ROCMFP4, Q4_0_ROCMFP4_FAST and Q2_0_ROCMFPX, which had none, and fix Q6_0_ROCMFPX, which decoded code 32 (sign bit set, zero magnitude) as -0 where the C codec yields -32. All six types now match the C dequantizer bit for bit on the random and quantize-then-dequantize paths. Assisted-by: Claude Fable 5.1 --- ggml/src/ggml-quants.c | 24 +++++++++++++++++ ggml/src/ggml-quants.h | 14 ++++++++++ gguf-py/gguf/quants.py | 60 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-quants.c b/ggml/src/ggml-quants.c index 80cdb3050db3..dd28eea5266a 100644 --- a/ggml/src/ggml-quants.c +++ b/ggml/src/ggml-quants.c @@ -613,6 +613,30 @@ void dequantize_row_nvfp4(const block_nvfp4 * GGML_RESTRICT x, float * GGML_REST } } +void dequantize_row_q4_0_rocmfp4(const block_rocmfp4 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + rocmfp4_dequantize_row_q4_0(x, y, k); +} + +void dequantize_row_q4_0_rocmfp4_fast(const block_rocmfp4_fast * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + rocmfp4_dequantize_row_q4_0_fast(x, y, k); +} + +void dequantize_row_q2_0_rocmfpx(const block_rocmfp2 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + rocmfpx_dequantize_row_fp2(x, y, k); +} + +void dequantize_row_q3_0_rocmfpx(const block_rocmfp3 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + rocmfpx_dequantize_row_fp3(x, y, k); +} + +void dequantize_row_q6_0_rocmfpx(const block_rocmfp6 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + rocmfpx_dequantize_row_fp6(x, y, k); +} + +void dequantize_row_q8_0_rocmfpx(const block_rocmfp8 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) { + rocmfpx_dequantize_row_fp8(x, y, k); +} + // // 2-6 bit quantization in super-blocks // diff --git a/ggml/src/ggml-quants.h b/ggml/src/ggml-quants.h index 75188f1af180..9282654d059c 100644 --- a/ggml/src/ggml-quants.h +++ b/ggml/src/ggml-quants.h @@ -5,6 +5,10 @@ #include "ggml.h" +// ROCmFPx block layouts, for the dequantize_row_* wrappers below +#include "../rocmfp4/rocmfp4.h" +#include "../rocmfpx/rocmfpx.h" + // GGML internal header #ifdef __cplusplus @@ -55,6 +59,16 @@ GGML_API void dequantize_row_q8_0(const block_q8_0 * GGML_RESTRICT x, float * GG GGML_API void dequantize_row_mxfp4(const block_mxfp4 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); GGML_API void dequantize_row_nvfp4(const block_nvfp4 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +// ROCmFPx types under the ggml naming convention (dequantize_row_), +// so tooling that resolves dequantizers by type name (gguf-py/tests/test_quants.py) +// reaches them. The codecs themselves live in ggml/rocmfp4 and ggml/rocmfpx. +GGML_API void dequantize_row_q4_0_rocmfp4 (const block_rocmfp4 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void dequantize_row_q4_0_rocmfp4_fast(const block_rocmfp4_fast * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void dequantize_row_q2_0_rocmfpx (const block_rocmfp2 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void dequantize_row_q3_0_rocmfpx (const block_rocmfp3 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void dequantize_row_q6_0_rocmfpx (const block_rocmfp6 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); +GGML_API void dequantize_row_q8_0_rocmfpx (const block_rocmfp8 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); + GGML_API void dequantize_row_q2_K(const block_q2_K * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); GGML_API void dequantize_row_q3_K(const block_q3_K * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); GGML_API void dequantize_row_q4_K(const block_q4_K * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); diff --git a/gguf-py/gguf/quants.py b/gguf-py/gguf/quants.py index ff9dc992687a..957314b4155f 100644 --- a/gguf-py/gguf/quants.py +++ b/gguf-py/gguf/quants.py @@ -427,6 +427,61 @@ def _rocmfpx_unpack_codes(qs: np.ndarray, nbits: int, block_size: int) -> np.nda return ((words >> bit_index) & np.uint16((1 << nbits) - 1)).astype(np.uint8) +_ROCMFP4_MAG = np.array((0, 1, 2, 3, 4, 6, 8, 10), dtype=np.int8) + + +def _rocmfp4_decode_nibbles(qs: np.ndarray) -> np.ndarray: + # 32 nibbles per block: the low nibbles are the first 16 weights, the high + # nibbles the last 16. Codes are sign (bit 3) + a 3-bit index into the + # {0,1,2,3,4,6,8,10} magnitude table. + codes = np.concatenate((qs & np.uint8(0x0F), qs >> np.uint8(4)), axis=1) + mag = np.take(_ROCMFP4_MAG, codes & np.uint8(7)) + return np.where((codes & np.uint8(8)) != 0, -mag, mag).astype(np.float32) + + +class Q4_0_ROCMFP4(__Quant, qtype=GGMLQuantizationType.Q4_0_ROCMFP4): + @classmethod + def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: + n_blocks = blocks.shape[0] + + qs, e = np.hsplit(blocks, [16]) + + vals = _rocmfp4_decode_nibbles(qs).reshape(n_blocks, 2, cls.block_size // 2) + scales = _rocmfpx_ue4m3_to_fp32(e).reshape(n_blocks, 2, 1) + + return (vals * scales).reshape(n_blocks, cls.block_size) + + +class Q4_0_ROCMFP4_FAST(__Quant, qtype=GGMLQuantizationType.Q4_0_ROCMFP4_FAST): + @classmethod + def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: + qs, e = np.hsplit(blocks, [16]) + + vals = _rocmfp4_decode_nibbles(qs) + scales = _rocmfpx_ue4m3_to_fp32(e) + + return vals * scales + + +class Q2_0_ROCMFPX(__Quant, qtype=GGMLQuantizationType.Q2_0_ROCMFPX): + _vals = np.array((-4, -1, 1, 4), dtype=np.int8) + + @classmethod + def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: + n_blocks = blocks.shape[0] + + qs, e = np.hsplit(blocks, [8]) + + # 2-bit codes, LSB first within each byte; bytes 0-3 are the first half + # block, bytes 4-7 the second, each with its own UE4M3 scale. + shifts = np.array((0, 2, 4, 6), dtype=np.uint8).reshape(1, 1, 4) + codes = (qs.reshape(n_blocks, 8, 1) >> shifts) & np.uint8(3) + vals = np.take(cls._vals, codes).astype(np.float32).reshape(n_blocks, 2, cls.block_size // 2) + scales = _rocmfpx_ue4m3_to_fp32(e).reshape(n_blocks, 2, 1) + + return (vals * scales).reshape(n_blocks, cls.block_size) + + class Q3_0_ROCMFPX(__Quant, qtype=GGMLQuantizationType.Q3_0_ROCMFPX): _mag = np.array((0, 1, 2, 4), dtype=np.int8) @@ -454,8 +509,11 @@ def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray: qs, e = np.hsplit(blocks, [24]) codes = _rocmfpx_unpack_codes(qs, 6, cls.block_size) + neg = (codes & np.uint8(32)) != 0 mag = (codes & np.uint8(31)).astype(np.int8) - vals = np.where((codes & np.uint8(32)) != 0, -mag, mag).astype(np.float32) + # code 32 (sign set, zero magnitude) is -32, not -0, in the C decoder + mag = np.where(neg & (mag == 0), np.int8(32), mag) + vals = np.where(neg, -mag, mag).astype(np.float32) scales = _rocmfpx_ue4m3_to_fp32(e).reshape(n_blocks, 2, 1) vals = vals.reshape(n_blocks, 2, cls.block_size // 2) From 9eec24459fd876c748f7770df24da12f3787d3ab Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 7 Sep 2026 00:37:04 +0100 Subject: [PATCH 077/212] llama : keep Q2_0_ROCMFPX ftype 119, matching the ROCmFPX origin and its GGUFs Undo the 119 -> 122 change from earlier on this branch. The two ROCmFPX upstreams disagree: charlie12345/ROCmFPX (the origin, and kingjones30's fork that the ROCm builds come from) has Q2_0_ROCMFPX at tensor type 107 and ftype 119, while ciru-ai/ROCmFPX moved it to 108 / 122 and put Q7_0_ROCMFPX at 107 / 119. Our tensor type ids follow charlie12345 (107), and every Q2_0 GGUF checked so far carries 107 + 119, so the ftype has to follow the same tree. Document the neighbouring ids (105/106 TURBO, 108 ROCMI4) in ggml.h so nobody reuses them for something else and silently misreads those files. Assisted-by: Claude Fable 5.1 --- ggml/include/ggml.h | 3 +++ gguf-py/gguf/constants.py | 2 +- include/llama.h | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h index 00f682f7bf80..f0eda79a1ae2 100644 --- a/ggml/include/ggml.h +++ b/ggml/include/ggml.h @@ -437,6 +437,9 @@ extern "C" { GGML_TYPE_Q6_0_ROCMFPX = 102, // ROCmFPx experimental 6-bit UE4M3-scale reference layout GGML_TYPE_Q8_0_ROCMFPX = 103, // ROCmFPx experimental 8-bit UE4M3-scale reference layout GGML_TYPE_Q3_0_ROCMFPX = 104, // ROCmFPx experimental 3-bit UE4M3-scale reference layout + // 105, 106 are TURBO3_0 / TURBO4_0 and 108 is Q4_0_ROCMI4 in charlie12345/ROCmFPX; + // left unassigned here so a GGUF from that tree is rejected rather than misread. + // Note ciru-ai/ROCmFPX diverges: it puts Q7_0_ROCMFPX at 107 and Q2_0_ROCMFPX at 108. GGML_TYPE_Q2_0_ROCMFPX = 107, // ROCmFPx experimental 2-bit S40 codebook + dual UE4M3 scales GGML_TYPE_COUNT = 108, }; diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index 538f6e4a4890..5116e63845d1 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -5654,7 +5654,7 @@ class LlamaFileType(IntEnum): MOSTLY_Q8_0_ROCMFPX_AGENT = 115 MOSTLY_Q6_0_ROCMFPX_LEAN = 116 MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN = 117 - MOSTLY_Q2_0_ROCMFPX = 122 # matches ciru-ai/ROCmFPX upstream id; 119 is their Q7_0_ROCMFPX + MOSTLY_Q2_0_ROCMFPX = 119 # as in charlie12345/ROCmFPX (ciru-ai uses 122 and puts Q7_0_ROCMFPX here) GUESSED = 1024 # not specified in the model file diff --git a/include/llama.h b/include/llama.h index 1ee38bcaec7d..9663fd87c2d5 100644 --- a/include/llama.h +++ b/include/llama.h @@ -173,7 +173,7 @@ extern "C" { LLAMA_FTYPE_MOSTLY_Q8_0_ROCMFPX_AGENT = 115, // ROCmFPx 8-bit agent/tool-call coherent routing LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_LEAN = 116, // ROCmFPx 6-bit size/speed-biased routing LLAMA_FTYPE_MOSTLY_Q6_0_ROCMFPX_AGENT_LEAN = 117, // ROCmFPx 6-bit agent routing without Q8-heavy boosts - LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX = 122, // ROCmFPx 2-bit S40 codebook + dual UE4M3 scales; id matches ciru-ai/ROCmFPX (119 is their Q7_0_ROCMFPX) + LLAMA_FTYPE_MOSTLY_Q2_0_ROCMFPX = 119, // ROCmFPx 2-bit S40 codebook + dual UE4M3 scales; id as in charlie12345/ROCmFPX (ciru-ai uses 122 and puts Q7_0 here) LLAMA_FTYPE_GUESSED = 1024, // not specified in the model file }; From da65acb8fe2fb463d4accd2381058457e71041d4 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 7 Sep 2026 00:38:11 +0100 Subject: [PATCH 078/212] rocmfpx : drop the unused HIP helpers and describe what is actually here rocmfp4_hip.cu, rocmfp4_hip_codebook.cuh, rocmfp4_hip_scale.cuh and rocmfpx_hip_codebook.cuh came along with the directory copy from ciru-ai/ROCmFPX. Nothing builds or includes them: they are not in any CMakeLists and the HIP backend in this tree has no ROCmFPx paths. Remove them. Both READMEs were ciru-ai's tuning logs (1000+ lines for rocmfp4) and claimed ROCm/HIP support, validation scripts and paths that do not exist in this repository. Replace them with the layouts, the GGUF type ids (including where the two ROCmFPX upstreams disagree on Q2_0), what is implemented on CPU and Vulkan, what is not, and how to test it. Assisted-by: Claude Fable 5.1 --- ggml/rocmfp4/README.md | 1056 +------------------------ ggml/rocmfp4/rocmfp4_hip.cu | 93 --- ggml/rocmfp4/rocmfp4_hip_codebook.cuh | 87 -- ggml/rocmfp4/rocmfp4_hip_scale.cuh | 165 ---- ggml/rocmfpx/README.md | 155 ++-- ggml/rocmfpx/rocmfpx_hip_codebook.cuh | 53 -- 6 files changed, 93 insertions(+), 1516 deletions(-) delete mode 100644 ggml/rocmfp4/rocmfp4_hip.cu delete mode 100644 ggml/rocmfp4/rocmfp4_hip_codebook.cuh delete mode 100644 ggml/rocmfp4/rocmfp4_hip_scale.cuh delete mode 100644 ggml/rocmfpx/rocmfpx_hip_codebook.cuh diff --git a/ggml/rocmfp4/README.md b/ggml/rocmfp4/README.md index 39bb79302cb2..bb966f950761 100644 --- a/ggml/rocmfp4/README.md +++ b/ggml/rocmfp4/README.md @@ -1,1020 +1,48 @@ # ROCmFP4 -ROCmFP4 is the experimental `Q4_0_ROCMFP4` GGUF tensor type for Strix Halo. -The current AMD-tuned variant stores 32 weights per block as packed -E2M1-derived 4-bit values plus two unsigned E4M3 scale bytes, one per -16-weight half, for 18 bytes per block. The codebook stores half-scale signed -integer levels up to `10`, representing `5.0` raw-scale units after the scale -factor is applied. This keeps outlier pull lower than the original wider test -range while preserving a fast integer-dot backend shape. +`Q4_0_ROCMFP4` and `Q4_0_ROCMFP4_FAST` are the 4-bit ROCmFPx tensor types. +The format originates in [charlie12345/ROCmFPX](https://github.com/charlie12345/ROCmFPX) +(MIT); the codec here was hand-ported via [ciru-ai/ROCmFPX](https://github.com/ciru-ai/ROCmFPX). -This directory owns the format-specific implementation. The rest of ggml only -registers and dispatches the type. +This directory owns the CPU codec (`rocmfp4.c`): quantize, dequantize, imatrix +weighting and row validation. The rest of ggml registers and dispatches the type. -Current status: -- The format runs on CPU, Vulkan, and ROCm/HIP in this custom tree. -- `Q4_0_ROCMFP4` is the pure 4.50 BPW dual-scale path. -- `Q4_0_ROCMFP4_LEAN` keeps ROCmFP4 for dense tensors but protects token - embeddings with `Q5_K`. On the Qwen3-4B Strix test pass this closed most of - the Q4_0 coherence gap while staying around normal Q4 size. -- `Q4_0_ROCMFP4_COHERENT` protects token embeddings with `Q6_K` and is the - quality-first ROCmFP4 preset. -- `Q4_0_ROCMFP4_FAST` is the 4.25 BPW single-scale speed path. It is the - smallest and fastest decode variant, but the pure version gives up too much - PPL to be the default quality target. -- `Q4_0_ROCMFP4_FAST_COHERENT` combines the fast 4.25 BPW transformer layout - with `Q6_K` token embeddings. On the Qwen3-4B Strix test pass it is the - current balanced AMD target: smaller than `Q4_0`, faster than `Q4_0` on - decode on both Vulkan and ROCm, and close to `Q4_0` PPL on the short - WikiText-2 check. -- `Q4_0_ROCMFP4_STRIX` is the current quality-biased Strix Halo preset. It - keeps most transformer tensors on `Q4_0_ROCMFP4_FAST`, protects token - embeddings with `Q6_K`, and uses the dual-scale `Q4_0_ROCMFP4` layout for - attention-K and attention-V tensors. On Qwen3-4B it improved the short - WikiText-2 PPL to `13.8865` at `4.49 BPW` while still beating the - same-flags stock `Q4_0` decode baselines on both Vulkan and ROCm. -- `Q4_0_ROCMFP4_STRIX_LEAN` is the compact Strix Halo preset. It keeps the - STRIX all-layer dual-scale attention-K/V protection, uses the FAST - single-scale transformer layout for the dense tensors, and protects token - embeddings/output with `Q5_K` instead of `Q6_K`. On the Qwen3-4B validation - pass it landed at `4.38 BPW`, improved short WikiText-2 PPL versus - `FAST_COHERENT`, and kept Vulkan decode in the `81 tok/s` band. -- A smaller first/last-layer-only K/V protection recipe was tested but not - promoted. It reached `4.48 BPW`, `80.13` Vulkan decode, and `69.85` ROCm - decode, but PPL regressed to `14.0167`, so the all-layer K/V STRIX preset - remains the quality target. -- The ROCm/HIP MMQ path for `Q4_0_ROCMFP4_FAST` uses one scale per 32-weight - block, matching the actual FAST layout instead of duplicating the scale into - two half-block slots. -- The ROCm/HIP vector-dot and MMQ loaders use a ROCmFP4-owned Codebook10 - expander backed by AMD `amdgcn_perm` constants. This avoids the generic - table-load helper on the hot ROCm path. -- The ROCm/HIP hot paths use a ROCmFP4-owned unaligned 32-bit quant-byte load - for packed nibble bytes. ROCmFP4 blocks are 17 or 18 bytes wide, so the older - byte-safe assembly path was conservative but expensive on Strix Halo HIP. - Direct unaligned dword loads improved ROCm `MUL_MAT` and FlashAttention - microbenchmarks while the Qwen3.6 27B MTP guard held `33.4 tok/s` short and - `27.7 tok/s` sustained. It is enabled by default and can be disabled with - `-DGGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD=0` for isolation. -- The ROCm/HIP FAST MMVQ/MMQ path now uses the same ROCmFP4-owned unaligned - quant-byte load instead of the generic byte-assembly helper. The full - promoted gate measured FAST ROCm `MUL_MAT` at `45.17`, `58.38`, `90.54`, - and `157.83` us for `n=1/2/4/8`, and the Qwen3.6 27B MTP guard improved to - `33.6 tok/s` short and `28.0 tok/s` sustained. The ROCm runtime guard now - tightens the FAST ceilings to protect this band. -- Qwen3.6 35B A3B MTP ROCmFP4 STRIX_LEAN was checked separately at native - `262144` context on ROCm0. In the reasoning-off discovery sweep, `n-max 1` - was best sustained at `72.2 tok/s`, while `n-max 5` was best burst at - `107.3 tok/s` but slower sustained at `64.9 tok/s`. With reasoning on, a - follow-up sweep found `n-max 2` best sustained at `92.6 tok/s` short and - `80.6 tok/s` sustained; `n-max 3` was close at `104.3` short and `80.1` - sustained, while `n-max 5` stayed burst-only at `98.6` short and `73.3` - sustained. KV-cache isolation then found that q8 main KV with q4 draft KV - improved the reasoning-on `n-max 2` profile to `93.7 tok/s` short and - `85.6 tok/s` sustained; full q8 main/draft KV reached `93.2` / `85.2`, - draft-only q8 stayed at `80.5` sustained, and K-only / V-only q8 main KV - regressed to `71.8` / `73.3` sustained. Rechecking draft depth under q8 main - KV moved the best sustained profile to `n-max 3` at `104.3 tok/s` short and - `89.3 tok/s` sustained, while `n-max 4` became the best burst-only profile - at `111.2 tok/s` short and `78.7 tok/s` sustained. The Pi sustained profile - therefore uses `n-max 3` with q8 main KV and q4 draft KV, while a separate - `n-max 4` burst profile is available for short-response experimentation. - The older `n-max 5` burst alias remains available for comparison. - Batch and CPU thread follow-ups did not beat the promoted runtime shape: - `-b 1024 -ub 512`, `-b 2048 -ub 512`, `-b 512 -ub 256`, - `-t 24 -tb 32`, and `-t 12 -tb 32` all measured in the `88.7`-`89.1 tok/s` - sustained band, so `-b 512 -ub 512 -t 16 -tb 32` stays promoted. - Current KV isolation rechecked the promoted shape at `104.3 tok/s` short and - `90.1 tok/s` sustained with q8 main KV, q4 draft KV, `n-max 3`, and - `p-min 0.25`; full q8 draft KV tied at `104.5` / `90.0`, while draft-only - q8 reached only `82.2` sustained. K-only and V-only q8 main KV regressed to - `70.5` and `74.6` sustained, so both accepted K and V need q8 and the draft - KV can stay q4. The updated default 35B guard then passed at `103.3 tok/s` - short and `90.1 tok/s` sustained. - Sampler-chain trims and backend sampling did not beat the promoted sustained - profile: `top_k;top_p;temperature` reached `104.2` / `87.5`, the milder - `penalties;top_k;top_p;min_p;temperature` chain reached `104.1` / `88.9`, - and `--backend-sampling` reached `104.3` / `89.2`, so the default sampler - path stays promoted. - A single-sequence MTP `process()` fast path for `-np 1` was also prototyped, - built, and rejected because the guard dropped to `104.1 tok/s` short and - `88.5 tok/s` sustained; the change was removed. - MoE `rows_per_block` compile-time variants were checked against the same 35B - guard and also rejected: `rows_per_block=4` measured `103.8` short / - `89.1 tok/s` sustained, `rows_per_block=3` measured `86.8 tok/s` sustained, - and `rows_per_block=1` measured `103.6` / `88.7 tok/s`, none beating the - promoted `104.3` / `89.3` band. - The Pi server profile was also started and stopped successfully with - `n_ctx = 262144`, `draft-mtp` initialized, built-in tools enabled, and - `thinking = 1`; ROCm reported no KFD PIDs after shutdown. The promoted - profile is now covered by the focused - `scripts/check-rocmfp4-qwen35-a3b-mtp-regression.sh` guard, which defaults - to `n-max 3`, q8 main KV, q4 draft KV, and `p-min 0.25` with `100.0` short - and `85.0 tok/s` sustained floors. - Follow-up `p-min 0.25/0.50/0.75`, `p-split 0.05/0.20`, and `n-min 1` - checks on the q4-KV baseline all tied the same `80.5`-`80.8 tok/s` sustained - band, while `p-min 0.25` on the q8-main/q4-draft path tied at `85.4 tok/s` - sustained, so the - conservative `n-min 0`, `p-min 0.0`, `p-split 0.10` defaults remained - promoted until the MTP internal sampler was changed from `top_k=1` to - `top_k=10`, making the `p-min` cutoff operate on a meaningful candidate - distribution while the draft loop still selects the top sorted candidate. - A pre-change p-min sweep on the actual promoted `n-max 3`, q8-main/q4-draft - profile measured `104.2` / `89.1` at `p-min 0.25`, `104.1` / `89.0` at - `0.50`, `104.0` / `89.3` at `0.75`, and `104.2` / `89.1` at `0.90`. - Follow-up p-split checks measured `103.7` / `88.9` at `0.05` and - `103.8` / `89.3` at `0.20`; n-min checks measured `103.8` / `88.9` for - `1` and `104.2` / `89.2` for `2`. None beat the promoted - sustained-plus-short profile. Combined `p-min`/`p-split` follow-ups also - tied rather than beat the default: `p-min 0.25` with `p-split 0.30` - repeated at `104.6` / `89.5`, `p-split 0.40` and `0.50` measured - `104.6` / `89.5` and `104.4` / `89.5`, and `p-split 0.70` / `0.90` - measured `104.8` / `89.5` and `104.7` / `89.5`; the same-session default - tied at `104.7` / `89.5`. With `top_k=10`, the 35B A3B profile now promotes - `--spec-draft-p-min 0.25`: default `p-min 0.0` measured `104.3` / `89.3`, - while `p-min 0.25` measured `103.9` / `90.0` after a first sustained run at - `89.8 tok/s`. The dense 27B profile should stay at `p-min 0.0`; the same - `p-min 0.25` filter regressed it to `24.6 tok/s` sustained. The full - all-regression harness can include the 35B guard with - `INCLUDE_QWEN35_A3B_GUARD=1`. - The MTP draft sampler now uses a probability-only top-10 helper for this - path. The draft loop only consumes the sorted top candidate and its - probability, so skipping the unused final RNG sampler selection preserved the - output path while improving the 35B A3B guard to `104.6 tok/s` short and - `90.2 tok/s` sustained. The dense 27B guard remained stable at `33.9` / - `28.1 tok/s`. - A fixed insertion top-10 replacement for `std::partial_sort` was tested and - rejected after the 35B A3B short guard dropped to `73.0 tok/s`, below the - `100.0 tok/s` floor. A `std::nth_element` plus top-k slice sort variant was - also rejected after the same guard dropped to `71.5 tok/s`. - Narrowing the MTP top-k probability accumulator from `double` to `float` was - also rejected: dense 27B still passed at `34.0` / `28.1 tok/s`, but the 35B - A3B short guard repeated below floor at `84.1` then `93.4 tok/s`; restoring - the `double` accumulator recovered the short guard to `104.4 tok/s`. - A `std::partial_sort_copy` top-k buffer variant built and passed the same - guard at `104.4 tok/s` short and `90.2 tok/s` sustained, but a same-session - promoted-build comparison measured `104.1` / `90.2`, so it was not promoted: - it added sampler buffer/clone complexity without a sustained decode gain. - Jackrong Qwopus3.6 27B v2 MTP BF16 was converted to STRIX_LEAN ROCmFP4 at - `4.34 BPW`. At native `262144` context with reasoning on, q4 main/draft KV, - and `n-max 4`, ROCm0 initially measured `34.9` / `29.6 tok/s`; increasing - only the batch to `-b 1024 -ub 512` moved sustained decode to `29.9 tok/s`. - Follow-up batch shapes `1280/512`, `1536/512`, and `1536/768` measured - `29.8`, `29.9`, and `29.8 tok/s` sustained respectively, so the smaller - `1024/512` profile remains promoted. Light acceptance filters `p-min 0.05` - and `0.10` also tied at `29.9 tok/s` sustained without beating the default. - Lower draft-depth checks with the same promoted batch rejected `n-max 1`, - `2`, and `3`, which measured only `19.9`, `26.6`, and `27.3 tok/s` - sustained. Qwopus therefore stays on `n-max 4`, unlike the 35B A3B profile - where `n-max 3` plus q8 main KV is best. - `--backend-sampling` tied sustained decode at `29.9 tok/s` but lowered prompt - throughput, so it is not promoted. Thread split checks at target/draft - `12/32` and `24/32` also tied `29.9 tok/s` sustained; the simpler default - `16/32` thread shape remains the recommended Qwopus profile. - KV isolation confirmed this is not like the 35B A3B q8-main profile: - draft-only q8 KV measured `35.1` / `29.8 tok/s`, and full q8 main plus q8 - draft KV measured `36.6` / `26.0 tok/s`. The full-q8 path improves burst - only and regresses sustained decode. Split accepted-KV checks also regressed: - q8 K only measured `35.7` / `22.3 tok/s`, and q8 V only measured `34.2` / - `24.7 tok/s`. Qwopus therefore keeps q4 main and q4 draft KV. - Vulkan0 measured `40.0` / `27.7 tok/s`, with Vulkan `n-max 3` and `n-max 5` - slower sustained; Vulkan `-b 1024 -ub 512` repeated the same `27.7 tok/s` - sustained band. q8 main KV regressed this model on both ROCm0 and Vulkan0. - A normal-path shortcut that only normalized `data[0].p` and filled the rest - of the top-k probabilities only for debug logging was rejected after the - 35B A3B short guard fell to `92.7 tok/s`, below the `100.0 tok/s` floor. - Disabling internal sampler timing with `sparams.no_perf = true` was also - tested and rejected after the same short guard dropped to `96.2 tok/s`. - Skipping the per-draft `common_sampler_reset()` in the MTP path was rejected - after the 35B A3B short guard dropped to `68.3 tok/s`; that reset remains - required to preserve the expected sampler/logit state for this helper. - A small MTP host-path cleanup now delays `llama_get_embeddings_pre_norm_ith()` - until the draft loop has confirmed that another draft token will be queued. - This avoids unused embedding-row pointer fetches on p-min rejects and final - `n-max` draft tokens. It passed the dense 27B guard at `34.0` / `28.1 tok/s`, - the 35B A3B guard at `104.4` / `90.1 tok/s`, and the Qwopus best ROCm0 - profile at `35.0` / `29.8 tok/s`. The default all-regression gate also - passed after this cleanup and ended with no KFD PIDs running. The 2026-05-25 - serial pass measured Qwen3.6 27B MTP at `33.9` / `27.9 tok/s`, ROCm runtime - FAST `45.66` / `57.81` / `88.27` / `155.05` us and dual-scale `49.16` / - `51.58` / `83.34` / `151.42` us for `n=1/2/4/8`, ROCm FlashAttention - `70.86` / `66.51` us for 64d dual-scale / FAST and `189.45` / `172.73` us - for Qwen-style 128d dual-scale / FAST, and ROCm CPY source-to-dual - `1106.89` / `1008.56` / `1006.60` us with source-to-FAST `1050.49` / - `958.98` / `950.50` us for F32/F16/BF16. - A second attempt to skip the final `n-max` `common_sampler_accept()` call was - rejected: the 35B A3B short check still reached `104.5 tok/s`, but sustained - decode fell to `81.1 tok/s`, below the `85.0 tok/s` floor. Reverting only - that sampler-accept change recovered the guard to `104.3` / `90.0 tok/s`. - Retesting the internal MTP sampler candidate count on the 35B A3B - reasoning-on profile rejected both directions around the promoted `top_k=10` - setting: `top_k=5` fell to `77.3 tok/s` sustained and `top_k=20` fell to - `69.6 tok/s` sustained. -- Reasoning-off checks on the final 35B q8-main/q4-draft profile are a - separate lower-throughput mode. `n-max 1/2/3/4` measured `77.7` / `73.9`, - `90.3` / `75.5`, `100.3` / `71.9`, and `85.7` / `66.1` short/sustained - tok/s respectively. If reasoning is disabled, `n-max 2` is currently the - best sustained profile in this bracket; the promoted fastest profile remains - reasoning-on `n-max 3`. -- Draft-thread-only checks on the promoted 35B profile also tied below the - promoted band. Keeping target threads at `16/32`, draft `8/16`, `16/16`, - and `24/32` all measured around `104.1`-`104.2 tok/s` short and - `89.1 tok/s` sustained, so the default matching draft thread counts remain. -- ROCm/HIP single-token MMVQ uses a full-block vector-dot ratio for the - dual-scale layout (`VDR_ROCMFP4_Q8_1_MMVQ=4`) while keeping the FAST layout - on the previous half-block ratio (`VDR_ROCMFP4_FAST_Q8_1_MMVQ=2`). This - lets dual-scale ROCmFP4 consume one full 32-value block per vector-dot call - without slowing the FAST dense-tensor path used by STRIX_LEAN. The focused - ROCm0 `MUL_MAT` guard improved dual-scale from the prior `78.81` us/run - serial pass to `54.89` us/run, and the Qwen3.6 27B MTP sustained guard - improved from `26.2` to `27.8 tok/s`. A broader version that also moved - FAST to `vdr=4` was rejected because sustained Qwen MTP dropped to - `24.2 tok/s`. A later FAST-only retest after the packed-byte load improved - the focused FAST ROCm guard to `41.37`, `49.29`, `80.91`, and `139.58` - us for `n=1/2/4/8`, but was still rejected because sustained Qwen MTP - dropped to `24.7 tok/s`. The remaining narrower FAST setting, - `GGML_ROCMFP4_FAST_Q8_1_MMVQ_VDR=1`, was also rejected after it failed the - focused FAST ROCm `n=1` guard at `60.18` us/run. The knob now rejects - invalid FAST MMVQ values at compile time; only `1`, `2`, and `4` are valid. -- A direct ROCmFP4 `vec_dot_q_cuda_dispatch` wrapper was tested in the - MMVQ kernels to bypass the generic constexpr function pointer call. It built - and passed the focused ROCm guard, measuring FAST `45.16` / `57.52` / - `89.44` / `156.49` us and dual-scale `50.83` / `51.11` / `84.51` / - `143.27` us for `n=1/2/4/8`, but Qwen3.6 27B MTP only reached `33.7 tok/s` - short and `27.9 tok/s` sustained. Because it did not beat the promoted - sustained band, the code change was removed. -- ROCm/HIP batched MMQ keeps the upstream-style `vdr=8` default for both - ROCmFP4 layouts, with ROCmFP4-owned compile-time test knobs - `GGML_ROCMFP4_Q8_1_MMQ_VDR` and `GGML_ROCMFP4_FAST_Q8_1_MMQ_VDR`. - FAST-only `vdr=4`, FAST-only `vdr=16`, and dual-scale `vdr=16` were tested - against the Qwen3.6 27B STRIX_LEAN ROCmFP4 bench and did not improve decode; - the tested runs all stayed at `13.56 tok/s` generation, while dual-scale - `vdr=16` slightly reduced prompt throughput. The default remains `8`. -- ROCm/HIP RDNA3.5 small-batch MMVQ now uses a ROCmFP4-specific two-warp - launch geometry for the ROCmFP4 layouts through `n=2`. Strix Halo previously - inherited the older RDNA2 one-warp table for this path. The promoted - `GGML_ROCMFP4_RDNA35_NWARPS=2` and - `GGML_ROCMFP4_RDNA35_NWARPS_MAX_NCOLS=2` defaults keep the single-token - microbench win and improve the guarded ROCm0 `n=2` shape: the latest serial - pass measured FAST/dual `66.56` / `58.40` us/run for `n=2`, versus the prior - `68.85` / `60.98` us/run band before extending the two-warp route. The - Qwen3.6 27B MTP guard held `33.5 tok/s` short and `27.7 tok/s` sustained, so - this is promoted as a backend micro-optimization with no sustained decode - regression. `GGML_ROCMFP4_RDNA35_NWARPS=4` was rejected because dual-scale - regressed to `57.87` us/run. The remaining 8-warp candidate was also rejected - because FAST `n=1` regressed to `59.33` us/run and failed the focused ROCm - runtime guard before multi-column checks. Extending the promoted two-warp - launch from `n=1` to `n=1..4` improved some focused multi-column ROCm - microbench rows (`dual n=2` reached `57.14` us/run), but Qwen3.6 27B MTP - sustained decode fell to `23.6 tok/s`, so the promoted upper bound stops at - `n=2`. A midpoint `GGML_ROCMFP4_RDNA35_NWARPS_MAX_NCOLS=3` build was also - checked on the Qwen3.6 35B A3B reasoning-on 262k profile; it reached - `87.5 tok/s` sustained versus `89.6 tok/s` for the same-session promoted - build, so `n=2` remains the default. -- ROCm/HIP RDNA3.5 wide-column rows-per-block now has ROCmFP4-owned compile-time - test knobs: `GGML_ROCMFP4_RDNA35_RPB_WIDE`, - `GGML_ROCMFP4_RDNA35_RPB_WIDE_DUAL`, and - `GGML_ROCMFP4_RDNA35_RPB_WIDE_FAST`. The defaults stay at `1`. A full - `RPB_WIDE=2` test improved FAST `n=8` `MUL_MAT` from `167.90` to - `131.72` us/run, but dual-scale collapsed from `148.11` to `1382.72` - us/run. A FAST-only `RPB_WIDE_FAST=2` build kept dual-scale safe - (`145.75` us/run) and improved FAST to `135.31` us/run, but Qwen3.6 - 27B MTP did not improve (`33.3 tok/s` short and `27.6 tok/s` sustained - at `n-max 4`; `n-max 5` remained burst-only at `45.5` short and `24.9` - sustained). Keep the knob off by default until a real decode guard benefits. -- ROCm/HIP RDNA3.5 `MUL_MAT_ID` routing now has a ROCmFP4-only compile-time - guard knob, `GGML_ROCMFP4_RDNA35_MMID_MAX_BATCH`, for testing whether MTP/MoE - batches should leave MMVQ earlier. The default keeps the accepted generic - RDNA3 behavior (`MMVQ_MAX_BATCH_SIZE`). A Strix test with the knob set to `1` - tied the promoted Qwen3.6 27B MTP path at `33.4 tok/s` short and `27.7 tok/s` - sustained. Follow-up threshold `2`, `3`, and `4` builds also tied at - `33.6` / `33.8` / `33.7 tok/s` short and `27.7` / `27.9` / `27.9 tok/s` - sustained. The same `3` threshold was then checked on the MoE-heavy - Qwen3.6 35B A3B MTP ROCmFP4 path and regressed to `95.8 tok/s` short and - `74.0 tok/s` sustained versus the promoted `104.4` / `89.3` band. Reusing - the older threshold `4` exploratory build on the same 35B guard measured - `104.2` / `89.2`, a near tie but still below the sustained promoted band. - No lower routing threshold is promoted. -- Target/draft "dual-stream" MTP overlap was inspected and is not a promoted - optimization. The current common MTP implementation verifies with the target - context, mirrors pre-norm embeddings into the MTP context, then drafts through - serial `llama_decode(ctx_dft, ...)` calls. ggml exposes async graph execution - and pipeline parallelism, but llama enables pipeline parallelism only for - multi-device layer-split offload cases. On single-device Strix Halo ROCm0, - target/draft overlap would require speculative scheduler changes and new - correctness guards rather than a runtime flag. The long-context optimization - focus remains KV-cache traffic, FlashAttention, memory bandwidth, and MTP - acceptance, while dual-scale ROCmFP4 mainly protects coherence. -- The MTP host loop now avoids two small sources of scheduler overhead. It - reuses a per-sequence drafting-state buffer instead of allocating a - `std::vector` on every draft call, and it tracks verify-batch sequence - bounds with one pass over token seq-ids instead of scanning every sequence for - every token. It also avoids copying all target verify rows when no previous - draft is pending, keeping only the carryover hidden row needed for the next - MTP step. The MTP path also uses a single-sequence batch append helper - instead of constructing a temporary sequence vector for each `common_batch_add` - call. It stores only non-final verify rows for partial-accept rollback, - copying the final row directly to `pending_h` for the next MTP step. It also - skips the draft-model decode call entirely when no sequence is drafting. - Debug candidate token formatting is skipped unless debug logging is enabled, - and the debug verbosity state is hoisted once per draft call instead of - rechecked inside the per-token loop. This is mainly a cleanup for - multi-sequence and long-running MTP sessions; the latest single-sequence - Qwen3.6 27B guard stayed in range at `33.7 tok/s` short and `27.9 tok/s` - sustained. -- The simple draft path also reuses an object-owned `uint8_t` drafting-state - buffer instead of allocating `std::vector` on every draft call. This - keeps both draft implementations on the same host-side allocation pattern; - the full serial gate stayed clean, with Qwen3.6 27B MTP at `33.8 tok/s` - short and `27.9 tok/s` sustained. -- The all-in-one regression harness now accepts a candidate `BUILD_DIR` and - derives separate `TEST_BACKEND_OPS_BIN` and `LLAMA_CLI_BIN` paths, so backend - microbench guards and Qwen CLI guards can run from the same candidate build. - DeepSeek remains opt-in only through `INCLUDE_DEEPSEEK_SMOKE=1`. -- ROCm/HIP vector FlashAttention now uses a ROCmFP4-only RDNA K/Q thread-group - default of `1` instead of the generic quantized-K RDNA default of `2`. This - affects only `Q4_0_ROCMFP4` and `Q4_0_ROCMFP4_FAST` K-cache cases; other - quantized FA paths keep the existing backend default. On the guarded ROCm0 - `FLASH_ATTN_EXT` shape, dual-scale improved from `122.33` to `113.03` - us/run, and FAST improved from `115.41` to `109.33` us/run. The Qwen3.6 - 27B MTP guard remained in the promoted band at `33.5 tok/s` short and - `27.6 tok/s` sustained. A wider `GGML_ROCMFP4_FATTN_KQ_NTHREADS=4` variant - was rejected because it regressed the same FA guard to `136.79` us/run - dual-scale and `124.37` us/run FAST. After the V-side default moved to `2`, - retesting `KQ_NTHREADS=2,V_NTHREADS=2` and `KQ_NTHREADS=4,V_NTHREADS=2` - still regressed the focused FA guard, so K/Q remains `1`. -- ROCm/HIP vector FlashAttention also uses a ROCmFP4-only V thread-group - default of `2`, down from the generic `D/4` path used by other quantized V - types. On the same guarded ROCm0 `FLASH_ATTN_EXT` shape, this moved - dual-scale from `113.03` to `85.63` us/run and FAST from `109.33` to - `80.74` us/run. The Qwen3.6 27B MTP guard stayed in range at `33.4 tok/s` - short and `27.6 tok/s` sustained, so this is promoted as the new default. - `GGML_ROCMFP4_FATTN_V_NTHREADS=4` also passed but was slower on the focused - FA guard; `1` was rejected because gfx1151 HIP compilation exceeded the 64 - KiB local-memory limit for ROCmFP4 FA instances. The FA guard now also - includes a Qwen3.6-style 128-head-dim, 8-KV-head, 12x-GQA ROCmFP4 shape - (`hsk=128,hsv=128,nh=8,nr23=[12,1],kv=7680,nb=1`). The accepted default - measured `246.91` us dual-scale and `219.23` us FAST on that shape. - Retesting `V_NTHREADS=4` on this wider shape produced a tiny dual-scale - improvement (`244.01` us) but regressed FAST (`224.21` us) and the existing - 64d guard, so it remains rejected. Retesting `KQ_NTHREADS=2,V_NTHREADS=2` - regressed the Qwen-style shape to `268.53` us dual-scale and `226.32` us - FAST, so K/Q remains `1`. -- ROCm/HIP vector FlashAttention now uses a ROCmFP4-owned single-half - Codebook10 expander in K/Q and V decode paths. These FA call sites already - know whether they need the low or high nibble stream, so they no longer pay - to expand both streams and discard one. The focused ROCm0 FA guard improved - from `86.13` / `81.23` us to `82.24` / `78.15` us for the 64d dual-scale / - FAST shapes, and from `247.06` / `221.19` us to `237.12` / `206.50` us for - the Qwen-style 128d shapes. The Qwen3.6 27B MTP guard held at `33.5 tok/s` - short and `27.7 tok/s` sustained in the full serial gate, so this is - promoted. -- ROCm/HIP vector FlashAttention also specializes the ROCmFP4 K/Q path when - `GGML_ROCMFP4_FATTN_KQ_NTHREADS=1`. Each thread owns the full head dot in - this promoted setting, so the kernel now expands both low/high packed - Codebook10 streams once per ROCmFP4 block and accumulates both half-blocks - together instead of loading the same packed bytes twice. The full serial gate - measured 64d FlashAttention at `81.62` / `78.13` us for dual-scale / FAST, - and the wider Qwen-style 128d guard improved from `237.12` / `206.50` us to - `228.58` / `199.32` us. Qwen3.6 27B MTP held `33.2 tok/s` short and - `27.7 tok/s` sustained, so this is promoted as a real Qwen-relevant FA - micro-optimization. - A post-specialization retest of `GGML_ROCMFP4_FATTN_V_NTHREADS=4` was - rejected. It passed the tightened FA guard and nudged the Qwen-style - dual-scale microbench to `228.33` us, but regressed 64d dual-scale to - `86.30` us and Qwen-style FAST to `201.81` us. Qwen3.6 27B MTP stayed at - `27.7 tok/s` sustained, so the promoted V-side default remains `2`. -- ROCm/HIP vector FlashAttention now uses a ROCmFP4-only V dequant rows-per- - thread default of `8`. The ROCmFP4 V helper supports 8-value chunks, and the - post-unaligned-load retest produced a real FA win: 64d dual-scale / FAST - `68.82` / `66.32` us and Qwen-style 128d dual-scale / FAST `201.13` / - `172.22` us. Qwen3.6 27B MTP held `33.5 tok/s` short and `27.7 tok/s` - sustained, so this is now promoted. It can be isolated with - `-DGGML_ROCMFP4_FATTN_V_ROWS_PER_THREAD=4` to return to the prior setting. - Retesting `GGML_ROCMFP4_FATTN_V_NTHREADS=4` on top of the 8-row default - improved only the Qwen-style dual-scale FA microbench (`187.72` us) while - regressing 64d dual-scale / FAST to `77.35` / `77.25` us and Qwen-style FAST - to `182.56` us. Qwen3.6 27B MTP tied at `33.5` / `27.7 tok/s`, so the - shipped V thread-group default remains `2`. - Retesting `GGML_ROCMFP4_FATTN_V_NTHREADS=8` on top of the 8-row default - regressed every focused FA row, including Qwen-style 128d dual-scale / FAST - at `208.94` / `200.45` us, so it was rejected without a longer MTP run. - Retesting `GGML_ROCMFP4_FATTN_KQ_NTHREADS=2` on top of the 8-row default - also regressed the focused FA guard: 64d dual-scale / FAST measured - `80.68` / `78.27` us and Qwen-style 128d measured `245.84` / `207.04` us. - Because the Qwen-style dual-scale row failed the guard, it was rejected - without a longer MTP run and K/Q remains `1`. -- ROCm/HIP vector FlashAttention now has a narrow dual-scale 128d V-thread - specialization: `GGML_ROCMFP4_FATTN_V_NTHREADS_D128_DUAL=4`. This keeps the - promoted 64d default and all FAST paths on `V_NTHREADS=2`, but uses the - previously promising 4-thread V grouping only for the Qwen-style dual-scale - 128d shape. The full serial gate measured FA at `70.37` / `66.31` us for - 64d dual-scale / FAST and `188.75` / `171.89` us for Qwen-style 128d - dual-scale / FAST, while Qwen3.6 27B MTP held `33.7 tok/s` short and - `27.9 tok/s` sustained with no KFD PIDs left running. - Retesting `GGML_ROCMFP4_FATTN_V_NTHREADS_D128_DUAL=8` was rejected. It - slightly improved the 64d dual-scale row (`69.40` us versus a same-session - `70.92` us promoted build), but regressed the Qwen-style 128d dual-scale row - from `194.40` to `211.10` us/run. Because long-context Qwen is the relevant - guard, the promoted D128 dual-scale V grouping remains `4`. - `GGML_ROCMFP4_FATTN_V_ROWS_PER_THREAD=16` was rejected during compilation: - the ROCmFP4 V dequantizer intentionally supports only `2`, `4`, and `8` - rows per thread, and the fixed-copy helper does not support the resulting - 32-byte move. The source now rejects unsupported values with a direct - ROCmFP4 compile-time error. - Retesting `GGML_ROCMFP4_FATTN_V_ROWS_PER_THREAD=2` on the promoted - D128-specialized build was rejected by the focused ROCm FlashAttention - guard. It regressed 64d dual-scale / FAST to `94.31` / `88.77` us and - Qwen-style 128d dual-scale / FAST to `270.00` / `223.88` us, so the - promoted rows-per-thread default remains `8`. -- Vulkan ROCmFP4 scale decode now uses a shared UE4M3 lookup table with - ROCmFP4's half-scale semantics. This moved the focused Vulkan dual-scale - `MUL_MAT` guard from `82.86`, `120.77`, and `181.28` us/run to `65.05`, - `83.07`, and `122.70` us/run for `n=1/2/4`. On Qwen3.6 27B MTP at 262k - context, the same change moved Vulkan sustained output from the older - `20.4 tok/s` at `--spec-draft-n-max 4` to `25.0 tok/s`; `n-max 3` now - reaches `25.3 tok/s` sustained. ROCm0 remains the promoted backend because - it still holds `33.5 tok/s` short and `27.7 tok/s` sustained. - A post-LUT Vulkan runtime sweep found a better fallback profile with q8 KV - and q8 draft KV: `--spec-draft-n-max 4` reached `34.8 tok/s` short and - `27.0 tok/s` sustained. q8 with `n-max 5` improved only the short burst - (`47.8 tok/s`) and regressed sustained output to `23.0 tok/s`; f16 KV - regressed sustained output to `22.5 tok/s`. - Follow-up isolation showed the Vulkan sustained gain comes from the main KV - cache rather than the draft KV cache: q4 main KV with q8 draft KV reached - only `34.6 tok/s` short and `25.0 tok/s` sustained, while q8 main KV with - q4 draft KV reached `34.7 tok/s` short and `26.9 tok/s` sustained. For a - Vulkan fallback, q8 main KV plus q4 draft KV is therefore the leaner near-tie - profile. Splitting q8 across only one accepted KV side was also rejected: - q8 main K with q4 main V reached `34.8 tok/s` short and `25.4 tok/s` - sustained, while q4 main K with q8 main V reached `34.6 tok/s` short and - `23.7 tok/s` sustained. The Vulkan fallback needs both accepted K and V at - q8. Adding mild acceptance filtering to the lean fallback - (`--spec-draft-p-min 0.25`) tied full q8/q8 at `27.0 tok/s` sustained while - keeping draft KV at q4; adding `--spec-draft-n-min 1`, stricter `p-min 0.75`, - and `p-split` checks stayed at `26.9 tok/s`. Retrying `n-max 5` with q8 main - KV, q4 draft KV, and - `p-min 0.25` still produced only a burst win (`47.8 tok/s` short) while - sustained output stayed at `23.0 tok/s`, so `n-max 4` remains the Vulkan - fallback setting. ROCm0 q4/q4 remains the overall promoted backend/profile. - Replacing the shared Codebook10 table with inline integer decode was tested - and rejected because it regressed the focused Vulkan FAST `n=1` guard to - `99.43` us/run. -- ROCm/HIP fallback dequant, copy, get-rows, GPU-side quantization scoring, - and standalone dequant helpers use ROCmFP4-owned HIP helpers for finite - scales and Codebook10 nibbles instead of relying on generic FP8 handling. - This keeps non-MMQ conversion paths aligned with the custom AMD format. -- A branchless HIP scalar Codebook10 nibble decoder was tested and rejected. - It passed the focused ROCm CPY, FlashAttention, and Qwen MTP guards, but - did not improve end-to-end decode. Qwen3.6 27B MTP measured `33.4 tok/s` - short and `27.7 tok/s` sustained, and the focused FlashAttention guard - slowed to `86.38` / `81.13` us for dual-scale / FAST, so the original - scalar decoder remains the default. -- CPU-side Codebook10 table decode in the quantizer/dequantizer was tested - and rejected. The full table variant failed the CPU quant guard with - dequantization at `49.04` cycles/32 for dual-scale and `84.23` cycles/32 for - FAST. The MSE-loop-only variant passed but slowed normal quantization to - `4183.33` / `4018.68` cycles/32 versus the restored arithmetic baseline at - `4034.33` / `3738.85` cycles/32, so the arithmetic CPU decoder remains the - default. -- UE4M3 scale decode in the ROCm/HIP software path uses a ROCmFP4-owned finite - scale decoder. It avoids `ldexpf`, builds normal FP32 values directly from - exponent/mantissa bits, and skips the generic FP8 NaN handling because - ROCmFP4 row validation already rejects non-finite scale bytes. -- A constant-memory HIP lookup table for the finite UE4M3 scale values was - tested and rejected. The latest isolated `GGML_ROCMFP4_USE_SCALE_LUT=1` - pass failed the focused ROCm runtime guard because FAST `n=1` regressed to - `69.48` us/run; prior checks also regressed ROCm CPY source-to-quant paths - and FlashAttention. The arithmetic finite-scale decoder remains the default. -- ROCm/HIP dequant conversion kernels use the same ROCmFP4 finite scale - decoder, keeping tensor conversion aligned with the hot MMQ/MMVQ backend - path instead of falling back to the generic FP8 helper. -- ROCm/HIP backend CPY now advertises and executes quantized - `Q4_0_ROCMFP4 -> F32` and `Q4_0_ROCMFP4_FAST -> F32` conversion paths. - This keeps diagnostic graph ops and fallback tensor conversion inside the - custom AMD decoder instead of being rejected by backend capability checks. - Contiguous q-to-f32 copies now use a ROCmFP4-specific packed-byte kernel: - one HIP thread reads one packed byte and writes the matching low/high - half-block output values. This gives coalesced output writes for normal - contiguous graph copies while the existing stride-aware block kernel remains - the fallback for views and non-contiguous tensors. -- ROCm/HIP backend CPY also supports `F16 -> Q4_0_ROCMFP4`, - `F16 -> Q4_0_ROCMFP4_FAST`, `BF16 -> Q4_0_ROCMFP4`, and - `BF16 -> Q4_0_ROCMFP4_FAST`. The kernels convert each 32-value half/bfloat - block to local FP32 and then run the same exhaustive ROCmFP4 scale search, - so runtime graph copies keep the coherence-first quantizer instead of - falling back to unsupported behavior. -- ROCm/HIP ROCmFP4 source-to-quant and quant-to-F32 CPY wrappers now use - normal multi-thread HIP launch geometry instead of launching one active - thread per quant block. This keeps F32/F16/BF16 runtime quantization and - ROCmFP4 dequantization on the same exact conversion math while removing a - launch-shape bottleneck in helper/fallback graph paths. -- ROCm/HIP ROCmFP4 quant-to-F32 CPY uses ROCmFP4-specific block dequant - helpers that decode each block scale once before unpacking all 32 values. - This avoids repeatedly decoding identical scale bytes through the generic - two-value dequant helper while preserving the same output values. For the - guarded contiguous `8192x512x2` ROCm0 CPY shape, the packed-byte path moved - dual-scale `Q4_0_ROCMFP4 -> F32` from the old `740` us/run band to - `184.48` us/run and FAST `Q4_0_ROCMFP4_FAST -> F32` to `169.99` us/run. - A split launch geometry is now used: source-to-ROCmFP4 quantization uses - 128-thread workgroups, while quant-to-F32 keeps the accepted 64-thread - packed-byte launch. A whole-path 128-thread launch was roughly tied overall - but slightly regressed quant-to-F32; isolating 128 threads to source - quantization preserves the dequant copy win while shaving the source paths. - A 256-thread launch regressed F16 source-to-quant paths and remains rejected. - After the FAST direct-value scoring win, FAST-only 256-thread and 64-thread - source launch splits were rechecked and rejected as well. The 256-thread - split regressed FAST F16 source-to-quant to `1047.24` us/run, and the - 64-thread split measured FAST F32/F16/BF16 `1055.32`, `955.35`, and - `954.78` us/run, so the shared 128-thread source launch remains promoted for - both ROCmFP4 layouts. -- ROCm/HIP contiguous quant-to-F32 CPY shared-scale staging was tested and - rejected. Decoding one block scale into shared memory and synchronizing each - 64-thread launch reduced duplicate scale decode work, but the synchronization - overhead regressed the guarded `8192x512x2` shape: dual-scale - `Q4_0_ROCMFP4 -> F32` moved from `181.84` to `188.63` us/run, and FAST - `Q4_0_ROCMFP4_FAST -> F32` moved from `170.65` to `180.03` us/run. The - direct packed-byte kernel remains promoted. -- ROCm/HIP backend CPY supports same-type packed-block copies for - `Q4_0_ROCMFP4 -> Q4_0_ROCMFP4` and - `Q4_0_ROCMFP4_FAST -> Q4_0_ROCMFP4_FAST`, including block-aligned views. - The kernel copies the packed 18-byte or 17-byte ROCmFP4 blocks directly, so - graph/view copies preserve exact bytes and avoid dequantize/requantize - fallback behavior. The launcher uses normal multi-thread HIP workgroups - rather than one-thread launches, so large packed-view copies scale with the - number of ROCmFP4 blocks. -- ROCm/HIP runtime quantization now finds the finite UE4M3 candidate nearest - `max_abs / 10` with a monotonic binary search, matching the CPU/Vulkan - reference tie behavior while avoiding the older 126-step linear nearest-scale - scan in the HIP copy utility. It also uses the same conservative lower-scale - pruning as the CPU quantizer, so runtime F32/F16/BF16-to-ROCmFP4 copies skip - smaller scales once their unavoidable clipped max-value error cannot beat the - current best exact scale. The HIP max scan uses the same plain absolute - compare as the CPU quantizer, preserving NaN/Inf handling while avoiding - `fmaxf` in the runtime quantization hot loop. -- ROCm/HIP runtime quantization specializes the exact scale search for the - only block shapes used by ROCmFP4 CPY: `0..15`, `16..31`, and `0..31`. - This keeps the same candidate order and tie behavior while letting HIP - compile the dual-scale and FAST source-to-quant paths with fixed offsets. -- ROCm/HIP dual-scale source-to-quant scoring uses a direct Codebook10 value - helper for 16-value half blocks. Final packed quantization still uses the - normal code-index helper, but scale scoring no longer has to map the selected - nibble back through the generic decode helper on the dual-scale path. -- ROCm/HIP FAST source-to-quant scoring now uses the same direct Codebook10 - value helper during scale search. This preserves the exact nearest-value - thresholds and final packed output, but avoids index-then-decode work in the - 32-value FAST scoring path. On the guarded `8192x512x2` ROCm0 CPY shape, - FAST source quantization improved from F32/F16/BF16 `1218.69`, `1138.32`, - and `1138.78` us/run to a repeat guard pass of `1047.21`, `950.93`, and - `951.00` us/run. -- ROCm/HIP `F32 -> ROCmFP4` runtime CPY stages each 32-value block into local - FP32 before running the exhaustive scale search, matching the existing - F16/BF16 source paths. This preserves exact F32 input values but avoids - rereading the global source pointer throughout scale scoring. On the guarded - `8192x512x2` ROCm0 CPY shape, dual-scale F32 source quantization improved - from roughly `9916` to `1117` us/run, and FAST improved from roughly - `10671` to `1231` us/run. -- ROCm/HIP contiguous-only source-to-quant CPY kernels for - `F32 -> ROCmFP4` were tested and rejected. They avoided the generic - multidimensional offset math, but on the guarded `8192x512x2` shape they - did not beat the existing multi-thread generic CPY path and slightly - regressed `F32 -> Q4_0_ROCMFP4_FAST`, so source-to-quant ROCmFP4 CPY keeps - the same guarded implementation for view and contiguous tensors. -- ROCm/HIP backend GET_ROWS supports both ROCmFP4 layouts. This gives pure - ROCmFP4 tensors the same direct row-gather coverage as stock small-block - quants on ROCm and keeps embedding-row access on the custom finite-scale - decoder. -- ROCm/HIP `MUL_MAT` support now covers `F16` activation tensors for both - ROCmFP4 layouts. The backend stages half activations to contiguous FP32 on - the GPU, including non-contiguous/views, then feeds the existing Q8 - activation quantizer and ROCmFP4 MMVQ/MMQ kernels. This keeps the forward - path on the AMD backend instead of rejecting the op and falling through to a - slower dequantized matrix path. The generic matmul runtime guard explicitly - allows this ROCmFP4 x F16 forward-inference case, so the support probe and - execution wrapper agree for batched activations. -- The standalone HIP dequant skeleton covers both the dual-scale and FAST - single-scale layouts, so future fused ROCm kernels can target the current - balanced FAST artifact without reintroducing the older scale path. -- The standalone HIP dequant launch now maps one thread to one packed - ROCmFP4 byte across a 256-thread global grid. This avoids the older - one-16-thread-block-per-32-values launch shape and gives future fused or - diagnostic dequant paths normal GPU occupancy scaling. -- Vulkan ROCmFP4 shaders also decode UE4M3 scales directly to the half-scale - value used by the codebook, matching CPU/HIP and avoiding repeated `* 0.5` - fixups at dequant and matmul call sites. -- Vulkan ROCmFP4 shaders keep a shared `kvalues_rocmfp4` Codebook10 table. - Arithmetic/direct Codebook10 decode variants compiled and preserved - coherence, but measured slower on Strix Halo Vulkan, so the table path - remains the active backend implementation. -- Vulkan `dequantize4()` for ROCmFP4 and ROCmFP4_FAST decodes each block's - UE4M3 scale once per 4-value vector instead of calling the 2-value helper - twice. This improves tested dual-scale ROCmFP4 small-batch matvec shapes - while keeping the promoted Vulkan runtime guard clean. -- Vulkan `Q4_0_ROCMFP4_FAST` matvec/MMQ kernels have a single-scale dot - specialization. They combine the two half-block dot sums and apply the one - FAST scale once, instead of taking the dual-scale path used by - `Q4_0_ROCMFP4`. -- Vulkan `Q4_0_ROCMFP4_FAST` MMQ stores its block scale as a scalar in - shared/register cache instead of duplicating it into a `vec2`. The focused - Strix Halo Vulkan `MUL_MAT` microbench for - `m=4096,n=1,k=14336,type_a=q4_0_rocmfp4_fast` improved from `62.18` to - `61.45` us/run. -- A Vulkan packed16 view for ROCmFP4 quant bytes was tested and rejected. It - compiled, but the focused FAST MMQ guard regressed to `83.03` us/run from the - accepted `61`-`62` us/run range, so ROCmFP4 Vulkan keeps the byte-view load - path. -- Vulkan backend CPY/SET_ROWS now has generated `F32 -> Q4_0_ROCMFP4`, - `F32 -> Q4_0_ROCMFP4_FAST`, `Q4_0_ROCMFP4 -> F32`, - `Q4_0_ROCMFP4_FAST -> F32`, and indexed SET_ROWS shaders. The SET_ROWS path - uses the same exhaustive finite UE4M3 scale search as the CPU reference, so - quantized K/V cache writes favor coherence over a cheap max-abs shortcut. -- Vulkan `F32 -> ROCmFP4` runtime quantization now uses the same exact ordered - UE4M3 scale search as CPU/HIP: find the scale nearest `max_abs / 10`, expand - outward, and stop a candidate once its partial error cannot beat the current - best scale. The candidate set is unchanged, so this avoids a slower linear - scan without falling back to a lower-quality shortcut. -- Vulkan source-to-ROCmFP4 runtime quantization now also prunes lower scale - candidates once clipping the block max alone cannot beat the current best - error. This mirrors the CPU/HIP scale search bound and keeps the candidate - set exact. The 2026-05-25 Vulkan CPY guard passed after this shader change: - F32/F16/BF16-to-dual measured `9525.39`, `2350.54`, and `2418.09` us/run, - dual-to-F32 `516.65` us/run, F32/F16/BF16-to-FAST `10111.85`, `2923.67`, - and `2949.42` us/run, and FAST-to-F32 `509.65` us/run. The full promoted - gate passed after rebuild with Qwen3.6 27B MTP at `33.9` / `28.0 tok/s` and - no KFD PIDs left running. -- Vulkan backend CPY also supports `F16 -> Q4_0_ROCMFP4`, - `F16 -> Q4_0_ROCMFP4_FAST`, `BF16 -> Q4_0_ROCMFP4`, and - `BF16 -> Q4_0_ROCMFP4_FAST`. The runtime quantization shader can load - `float`, `float16_t`, and BF16 source bits and then runs the same exact - ordered finite UE4M3 scale search. Backend tests keep a bounded NMSE - tolerance only for the half/bfloat runtime quantization cases because those - paths are inherently lossy around source-precision tie points; the - `F32 -> ROCmFP4` checks remain strict. -- Vulkan same-type CPY supports packed-block copies for - `Q4_0_ROCMFP4 -> Q4_0_ROCMFP4` and - `Q4_0_ROCMFP4_FAST -> Q4_0_ROCMFP4_FAST`, including block-aligned - non-contiguous/permuted/view copies. The non-contiguous path uses a - byte-addressed block shader and preserves exact 18-byte or 17-byte - ROCmFP4 blocks. Contiguous same-type dual-scale and FAST copies now both use - the direct byte-copy path, avoiding the generic halfword copy route for - ROCmFP4's custom 18-byte and 17-byte block layouts. -- Vulkan CPY is now covered by a dedicated regression guard so copy-path - changes cannot silently fall back or regress outside the ROCm-only CPY gate. - The guard runs same-type and source/dequant CPY correctness before measuring - the large copy performance shape, and now streams the performance phase - through `tee` so long Vulkan runs do not look idle to the command runner. - The ceilings were tightened after the lower-scale pruning gain, so the old - slow source-to-quant path no longer passes this guard. - On Strix Halo RADV Vulkan, the large guarded shape currently measures - F32/F16/BF16-to-dual at `9525.39`, `2350.54`, and `2418.09` us/run, - dual-to-F32 at `516.65` us/run, F32/F16/BF16-to-FAST at `10111.85`, - `2923.67`, and `2949.42` us/run, and FAST-to-F32 at `509.65` us/run. -- Vulkan scalar FlashAttention can now decode ROCmFP4 and ROCmFP4_FAST K/V - cache blocks. ROCmFP4 K/V is forced to the scalar FA path because the current - custom decode is not a coopmat/native matrix-core FP4 path. -- Vulkan scalar FlashAttention can use the integer-dot MMQ K path for both - ROCmFP4 K-cache layouts. The FAST layout expands each 4-bit Codebook10 value - into packed signed int8 lanes and uses its single UE4M3 scale as the K block - multiplier. The dual-scale layout also uses packed signed int8 lanes, but - splits the dot accumulation by half-block so each 16-value half uses its own - UE4M3 scale. This keeps the quality-biased STRIX K-cache path fast without - applying one scale to a two-scale block. -- Build and runtime verification generated the Vulkan SPIR-V entries for - ROCmFP4 copy/SET_ROWS shaders, linked `libggml-vulkan.so`, and passed - Vulkan ROCmFP4 CPY plus MUL_MAT smoke tests on Strix Halo. -- Row validation rejects invalid scale bytes outside finite unsigned UE4M3 - (`0x00` through `0x7e`) so corrupted custom GGUF tensors fail early. -- Quantization keeps the exhaustive 126-scale UE4M3 search for both normal and - imatrix paths. Candidate-window scale search was tested and improved GGUF - creation speed, but it regressed the Qwen3-4B short WikiText-2 PPL on the - compact FAST path, so it was rejected for coherence. -- The exhaustive scale search now visits the UE4M3 candidate nearest the - block's `max_abs / 10` first using a monotonic binary search, expands - outward, and exits a candidate scale once its partial error cannot beat the - current best scale. This remains exact because every finite scale is still - evaluated; on the Qwen3-4B FAST_COHERENT artifact it produced a - byte-identical GGUF while cutting FAST quantization cost sharply. -- CPU scale decode now uses a 127-entry finite UE4M3 half-scale table for the - exhaustive scale search and row dequantization. The table preserves the same - exact FP32 values as the former bit-construction decoder, but avoids - rebuilding them for every scale candidate. -- CPU scale search now has separate unweighted and imatrix-weighted MSE - scoring paths. The normal path avoids per-value weight branches, while the - imatrix path precomputes the row-energy calibration weight once per - 16/32-value block instead of recomputing the same `sqrtf` term for every - candidate scale. -- CPU normal quantization scans each block once for finite values and uses a - finite-only nearest-Codebook10 helper in the exhaustive MSE loop when the - block is clean. Non-finite blocks still use the guarded helper. This - promoted conversion-speed path measured focused dual-scale / FAST normal - quantization at `3844.38` / `3582.57` cycles per 32 values in the latest - guard. -- CPU imatrix-weighted scale-MSE scoring now uses the same finite-only - nearest-Codebook10 helper when the source block is finite. This preserves the - guarded path for non-finite input while improving FAST imatrix GGUF creation: - same-session pre-candidate FAST imatrix was `5258.07` cycles / 32 values, and - two guarded candidate passes measured `4448.73` and `4447.32`. Dual imatrix - stayed in the noisy guarded band, so the claim is limited to FAST imatrix. -- The weighted and unweighted exhaustive scale loops are split as well, so - the normal quantizer does not branch on imatrix state for every finite - UE4M3 candidate. This keeps the exact candidate order and tie behavior while - improving CPU GGUF creation speed for both ROCmFP4 layouts. -- The CPU quantizers fill every packed nibble byte in each ROCmFP4 block, so - they no longer clear `qs` before packing. This is an exact store cleanup: - correctness stayed clean, `q4_0_rocmfp4` quantize improved to `5244.46` - cycles / 32 values, `q4_0_rocmfp4_fast` improved to `4668.28` cycles / 32 - values. -- CPU scale selection scans block maxima with a plain absolute-value compare - instead of `fmaxf`. NaNs still do not raise the maximum, infinities are still - rejected before scale search, and finite output is unchanged. This moved - `q4_0_rocmfp4` quantize to `5228.64` cycles / 32 values and FAST to - `4516.45` cycles / 32 values. -- CPU dequantization and quantization-error scoring use the ROCmFP4 arithmetic - Codebook10 decoder, which avoids a table fetch per unpacked nibble. CPU - fallback vec-dot keeps the 16-entry codebook table because measured Strix - Halo fallback dot speed was better with the table there. This hybrid keeps - the dequant win without regressing CPU vec-dot. -- CPU fallback vec-dot now reuses each packed ROCmFP4 byte for both low and - high nibbles before table decode. This preserves exact output, moved focused - dual-scale vec-dot from `31.86` to the `29.77`-`29.82` cycles / 32 values - band, and kept FAST in the `27.04`-`27.06` cycles / 32 values band. -- Two additional CPU decode shortcuts were rechecked on 2026-05-24 and - rejected. Replacing arithmetic Codebook10 decode with table decode in - row dequantization and scale-MSE scoring slowed CPU dequantization from the - guarded `33`-cycle band to `51`-`84` cycles / 32 values. Returning a direct - decoded value during MSE scoring instead of index-then-decode also passed - correctness but regressed normal quantization in the measured guard, so the - original arithmetic decoder remains the CPU path. A narrower full-block - weighted-MSE-only retry was also rejected because it regressed the focused - FAST imatrix timing to `5007.73` cycles / 32 values. -- A direct finite-scale table helper for CPU quantizer scale-search candidates - was also rejected. It helped dual-scale normal quantization, but repeat - guard runs regressed FAST normal quantization to `4043.60` and - `4289.66` cycles / 32 values. The guarded scale helper remains in place - because the compact STRIX_LEAN preset depends heavily on FAST tensors. -- NaN-only and branchless finite-scan variants were also rejected for default. - The NaN-only scan was noisy and the same-session `isfinite()` scan measured - better dual-scale normal quantization (`3735.41` vs `3776.53` cycles / 32 - values), while the branchless boolean-and scan regressed FAST normal - quantization to `3773.44` cycles / 32 values. -- Per-value Codebook10 quantization uses exact nearest-neighbor thresholds - instead of a 16-entry scan. The hot quantizer path uses one reciprocal scale - per candidate/block and multiplies each value by that reciprocal instead of - dividing per value. On the Qwen3-4B FAST_COHERENT check this kept PPL tied - with the accepted artifact while cutting GGUF creation time further. -- CPU exhaustive scale search now prunes lower-scale candidates after the - clipped max-value error alone cannot beat the current best error. The - imatrix path uses the positive calibration weight of the max element for the - same conservative bound. This does not change accepted values because every - skipped smaller scale has a larger unavoidable clip error. The weighted path - also combines imatrix weight preparation, max-absolute scan, and max-value - pruning-weight selection into one pre-scan, while both exhaustive loops stop - once no valid lower or upper scale candidates remain. Latest 40-iteration - quant guard run: `q4_0_rocmfp4` normal `4049.28` cycles / 32 values, FAST - normal `3721.48`, imatrix dual-scale `5251.76`, imatrix FAST `4898.75`. -- Adding `GGML_RESTRICT` to the local CPU scale-scoring helper pointers was - tested and rejected. It compiled and passed the quant guard, but did not - improve normal quantization and made the imatrix guard noisier/slower in - the measured pass, so the existing helper signatures were kept. -- Regression guards: - - `scripts/check-rocmfp4-deepseek-regression.sh` is an optional compatibility - smoke guard for a second ROCmFP4-converted model family. It is not part of - the promoted-gain gate because no reproducible DeepSeek speedup has been - established yet. The default all-regression script skips it unless - `INCLUDE_DEEPSEEK_SMOKE=1` is set. - - `scripts/check-rocmfp4-qwen-mtp-regression.sh` verifies the promoted - Qwen3.6 27B MTP ROCmFP4 262k-context `draft-mtp` path. The stable guard - uses ROCm0 and `--spec-draft-n-max 4`. On ROCm0, `n-max 4` improved the - short guard prompt from `27.7` to `33.6 tok/s` versus `n-max 3`, and the - longer forced-output prompt from `25.6` to `26.3 tok/s`; the dual-scale - MMVQ VDR tune then moved the longer prompt to `27.8 tok/s`. `n-max 5` - and `n-max 6` improved only the bursty short prompt after the MMVQ tune - but regressed sustained output to `24.8` and `24.0 tok/s`, respectively. - Follow-up `n-max 5` checks with `--spec-draft-p-min 0.75`, - `--spec-draft-p-min 0.90`, and `--spec-draft-n-min 1` still held the - short prompt at `45.0`-`45.1 tok/s` but kept sustained output at only - `24.7 tok/s`, so `n-max 4` remains promoted. - The guard now checks both short and sustained prompts so high-acceptance - best-case gains do not hide sustained-output regressions. - Additional ROCm0 checks rejected larger batch sizes, smaller ubatch, - q8/f16 KV cache, `--swa-full`, `--no-host`, `--no-op-offload`, - `--no-repack`, polling changes, lower and higher CPU thread counts, - greedy sampling, backend sampling, p-split changes, sampler chain trimming, - and combined MTP+ngram speculation because none improved the sustained - prompt. After the MMVQ VDR tune, `-b 1024 -ub 512`, `-b 2048 -ub 512`, - and `-b 512 -ub 256` still tied the promoted `27.8 tok/s` sustained - result, while `-b 1024 -ub 1024` and - `--poll-batch 1 --spec-draft-poll-batch 1` stayed at `27.7 tok/s`. - q8 and f16 KV cache dropped sustained decode to `25.4` and `26.0 tok/s`. - The MTP draft loop now honors `--spec-draft-p-min`. With that cutoff active, - `n-max 4, p-min 0.75` tied the promoted path at `33.5 tok/s` short and - `27.6 tok/s` sustained, while `n-max 5` stayed burst-only at `45.0`-`45.1` - tok/s short and `24.6`-`24.7 tok/s` sustained for `p-min 0.75` and `0.90`. - Follow-up ROCm0 KV isolation confirmed this is not a draft-cache issue: - q8 main KV with q4 draft KV measured `33.5 tok/s` short and `25.3 tok/s` - sustained, q8 main K only measured `31.5` and `21.4 tok/s`, and q8 main V - only measured `31.1` and `21.9 tok/s`. The promoted ROCm0 path therefore - keeps both accepted and draft KV at q4. - `--spec-draft-p-split 0.05` and `0.20` both tied the `27.7 tok/s` - sustained guard, `-t 24 -tb 32 --spec-draft-threads 24 - --spec-draft-threads-batch 32` tied, and trimming the sampler chain to - `top_k;top_p;temperature` regressed sustained decode to `26.6 tok/s`. - Draft-only q8 KV (`SPEC_DRAFT_TYPE_K=q8_0`, - `SPEC_DRAFT_TYPE_V=q8_0`) with main KV left at q4 tied the promoted path - at `33.6 tok/s` short and `27.7 tok/s` sustained, so the leaner q4 draft - KV default remains promoted. - The heavier Qwen3.6 27B MTP `STRIX_MTP_Q6` ROCmFP4 profile was also - checked with the same ROCm0 guard. It reached `30.1 tok/s` on the short - prompt but only `21.3 tok/s` sustained, below the guard floor, so the - Q6/quality-biased profile is not promoted for throughput. - The guard script supports `SPEC_DRAFT_N_MAX`, `SPEC_DRAFT_N_MIN`, - `SPEC_DRAFT_P_MIN`, `SPEC_DRAFT_P_SPLIT`, `THREADS`, `THREADS_BATCH`, - `SPEC_DRAFT_THREADS`, `SPEC_DRAFT_THREADS_BATCH`, `SAMPLERS`, - `BATCH_SIZE`, `UBATCH_SIZE`, `CACHE_TYPE_K`, `CACHE_TYPE_V`, - `SPEC_DRAFT_TYPE_K`, `SPEC_DRAFT_TYPE_V`, `EXTRA_ARGS`, and - `SPEC_EXTRA_ARGS` environment overrides for - controlled sweeps while defaulting to the promoted `n-max 4`, `n-min 0`, - `p-min 0.0`, `p-split 0.10`, q4 KV, `-t 16`, `-tb 32`, `-b 512`, and - `-ub 512` settings. - - `scripts/check-rocmfp4-quant-regression.sh` runs quant correctness plus - CPU quantizer, dequantizer, and vec-dot cycle ceilings for both ROCmFP4 - block layouts. It checks normal and imatrix quantization, and now also - protects CPU fallback dequant/vec-dot paths so decode-helper experiments - cannot pass while slowing non-GPU fallbacks. Recent pass after keeping the - `isfinite()` finite-block scoring promotion and adding the weighted/imatrix - finite scorer: dual-scale quant `3844.38`, FAST quant `3582.57`, dual - dequant `33.59`, FAST dequant `33.13`, dual vec-dot `29.96`, FAST vec-dot - `27.03`, dual imatrix `5587.43`, and FAST imatrix `4447.32` cycles / 32 - values. - - `scripts/check-rocmfp4-vulkan-runtime-regression.sh` measures focused - Vulkan `MUL_MAT` runtime for ROCmFP4 FAST and dual-scale layouts, catching - shader regressions that can be hidden by end-to-end decode noise. The - guard covers `n=1`, `n=2`, `n=4`, and `n=8` for both layouts. Recent - tightened serial pass measured FAST `55.82`, `71.59`, `105.10`, and - `163.41` us/run for `n=1/2/4/8`, and dual-scale `64.87`, `83.24`, - `118.56`, and `194.27` us/run for `n=1/2/4/8`. The previous serial pass - after adding the shared ROCmFP4 UE4M3 - scale LUT was FAST `53.66`, `71.67`, `105.14` us/run and dual-scale - `65.05`, `83.07`, `122.70` us/run for the same shapes. - - `scripts/check-rocmfp4-vulkan-cpy-regression.sh` measures Vulkan0 CPY for - `F32/F16/BF16 -> Q4_0_ROCMFP4`, - `F32/F16/BF16 -> Q4_0_ROCMFP4_FAST`, and the matching quant-to-F32 - dequant copy paths on the same large shape used by the ROCm CPY guard. - It is included in the all-regression harness to catch accidental Vulkan - copy fallback or shader-routing regressions. - - `scripts/check-rocmfp4-rocm-runtime-regression.sh` measures the same - focused `MUL_MAT` shapes on ROCm0. The guard covers `n=1`, `n=2`, `n=4`, - and `n=8` for both FAST and dual-scale ROCmFP4 so MTP-style multi-column - regressions are caught instead of only protecting single-token matvec. - Recent tightened serial pass after extending the RDNA3.5 two-warp route - through `n=2` measured FAST `51.66`, `66.56`, `101.27`, and `168.84` - us/run for `n=1/2/4/8`, and dual-scale `53.66`, `58.40`, `87.32`, and - `148.26` us/run for `n=1/2/4/8`. The `n=8` guard was added after a - rejected wide-rows candidate regressed dual-scale `n=8` to `1382.72` - us/run. - - `scripts/check-rocmfp4-rocm-cpy-regression.sh` measures ROCm0 CPY for - `F32/F16/BF16 -> Q4_0_ROCMFP4`, - `F32/F16/BF16 -> Q4_0_ROCMFP4_FAST`, and the matching quant-to-F32 - dequant copy paths on a large `8192x512x2` shape. Recent serial-gate pass - after direct FAST value scoring: F32/F16/BF16-to-dual `1111.73`, - `1008.69`, `1006.93` us/run, dual-to-F32 `182.25` us/run, - F32/F16/BF16-to-FAST `1047.21`, `950.93`, `951.00` us/run, and - FAST-to-F32 `170.36` us/run. - - `scripts/check-rocmfp4-all-regression.sh` runs the promoted-gain gate: - quant, Vulkan runtime, Vulkan CPY, ROCm runtime, ROCm FlashAttention, - ROCm CPY, and Qwen MTP guards serially, then checks ROCm KFD PIDs. Runtime - microbenchmarks should use this serial path rather than being run in - parallel with other GPU/UMA workloads. DeepSeek is not part of this - promoted-gain gate because no reproducible DeepSeek speedup has been - established; run the separate compatibility smoke only when explicitly - needed with `INCLUDE_DEEPSEEK_SMOKE=1`. The K/Q block-pair FlashAttention - serial pass held Qwen3.6 27B MTP at `33.2 tok/s` short and `27.7 tok/s` - sustained, with no KFD PIDs left running. The same pass measured ROCm - runtime `MUL_MAT` at FAST `54.16` / `66.13` / `101.73` / `178.36` us and - dual-scale `51.65` / `57.98` / `88.33` / `148.48` us for `n=1/2/4/8`, - and ROCm FlashAttention at `81.62` / `78.13` us for 64d dual-scale / FAST - and `228.58` / `199.32` us for Qwen-style 128d dual-scale / FAST. - - The MTP `accept()` path now skips re-copying `pending_h` when acceptance - lands on the final verify row already staged by `process()`. The focused - Qwen MTP guard held `33.8 tok/s` short and `27.9 tok/s` sustained after - this host-side cleanup. - - The MTP `process()` path now stores only the non-final verify rows needed - for partial-accept rollback, while copying the final target hidden row - directly to `pending_h` for the next MTP step. The focused Qwen MTP guard - held `33.9 tok/s` short and `27.9 tok/s` sustained, and the full serial - gate held `33.7 tok/s` short and `27.9 tok/s` sustained after this cleanup. - - The simple draft and MTP draft paths now return before - `llama_decode(ctx_dft, batch)` when no sequence is actively drafting. The - focused Qwen MTP guard held `33.8 tok/s` short and `28.0 tok/s` sustained, - and the full serial gate held `33.7 tok/s` short and `27.9 tok/s` - sustained after this idle/no-draft cleanup. - - The simple draft path now uses a reusable `uint8_t` drafting-state buffer - instead of allocating `std::vector` per draft call. The focused Qwen - MTP guard held `33.7 tok/s` short and `28.0 tok/s` sustained, and the full - serial gate held `33.8 tok/s` short and `27.9 tok/s` sustained. - - The MTP draft loop now skips its debug-candidate loop entirely unless - debug logging is enabled, hoists the debug verbosity state once per draft - call, and uses direct vector indexing in the hot per-token path. The - focused Qwen MTP guard held `33.8 tok/s` short and `27.9 tok/s` sustained - after this cleanup, so it is kept as a low-risk host-side simplification - rather than claimed as a decode-speed gain. - - The simple and MTP draft paths now use a shared direct one-sequence batch - append helper instead of `common_batch_add(..., { seq_id }, ...)` in the - hot speculative path. This avoids a temporary sequence-id container without - changing multi-sequence batch behavior elsewhere. The focused Qwen MTP - guard held `33.9 tok/s` short and `27.9 tok/s` sustained, and the full - serial gate held `33.8 tok/s` short and `27.9 tok/s` sustained. - - The MTP verify-row buffer now reserves for the configured draft depth and - only grows when needed instead of shrinking/resizing on every verification - pass. The focused Qwen MTP guard held `33.8 tok/s` short and `27.9 tok/s` - sustained, and the full serial gate held `33.9 tok/s` short and - `27.9 tok/s` sustained with no KFD PIDs left running. - - The MTP `process()` path now copies retained verification hidden rows from - the target embedding buffer in one contiguous `memcpy()` instead of one - `llama_get_embeddings_pre_norm_ith()` plus `memcpy()` per row. Rollback - behavior is unchanged. The focused Qwen3.6 27B MTP guard held - `33.7 tok/s` short and `27.9 tok/s` sustained, and the 35B A3B guard held - `104.1 tok/s` short and `89.2 tok/s` sustained. The same build later - passed the full serial all-regression gate with `INCLUDE_QWEN35_A3B_GUARD=1`, - including Qwen3.6 27B MTP at `33.8` / `27.9 tok/s` and Qwen3.6 35B A3B MTP - at `104.1` / `89.3 tok/s`. - - A matching contiguous-pointer cleanup inside the MTP `draft()` loop was - tested and rejected. It replaced per-row - `llama_get_embeddings_pre_norm_ith(ctx_dft, i_batch)` calls with one - `llama_get_embeddings_pre_norm(ctx_dft)` pointer per draft decode - iteration. The 27B MTP guard held at `33.8` / `27.9 tok/s`, but the 35B A3B - repeat measured `104.3` / `88.7` and `104.3` / `89.2 tok/s`, below the - promoted `89.3 tok/s` sustained band, so the code change was removed. - - A single-sequence MTP `draft()` fast path was tested and rejected on - 2026-05-25. It removed the active-sequence bookkeeping loop for the common - `n_seq == 1` case and passed the dense 27B guard at `33.7` / `28.0 tok/s`, - but the 35B A3B sustained guard collapsed to `25.7 tok/s` despite a - passing `103.1 tok/s` short check. Reverting that path restored the 35B A3B - guard to `104.3` / `90.3 tok/s`, so the shared multi-sequence draft loop - remains the promoted implementation. - - A dual-scale-only finite-pack CPU quantizer shortcut was tested and - rejected on 2026-05-24. It passed correctness, but even after isolating - the shared scale chooser it regressed the protected FAST quant path - (`q4_0_rocmfp4_fast` normal quant rose into the `3882`-`4022` cycles / 32 - values band versus the clean `3623` cycles / 32 values baseline). The - final packing loops therefore remain on the guarded nearest-code helper. -- The Strix ROCmFP4 build script now builds the quant regression test - binaries (`test-quantize-fns`, `test-quantize-perf`, and - `test-backend-ops`) alongside `llama-cli`, `llama-quantize`, and - `llama-bench`, so quant and runtime guards work from a clean build - directory. -- `test-quantize-perf --imatrix` benchmarks the ROCmFP4 quality/coherence - path through `ggml_quantize_chunk(..., imatrix)` with synthetic importance - weights. The quant regression guard includes this mode to reject imatrix-side - CPU changes that would not show up in the normal non-imatrix timing. +## Layout -High-upside Strix-specific work: -- The next significant speed step is likely a fused ROCmFP4 decode-and-dot - ROCm matvec path. The current ROCmFP4 wins come from compact blocks and - avoiding generic decode/copy overhead; a fused path would keep Codebook10 - values and UE4M3 scales in registers/shared memory through the dot product - instead of materializing wider intermediates. -- A ROCmFP4-aware long-context attention path is the second major target. The - long-context limit is mostly KV traffic, FlashAttention shape efficiency, and - memory bandwidth. The current dual-scale layout protects coherence, but it is - not itself a large speed lever unless the attention kernels consume it - directly and efficiently. -- MTP target/draft overlap is a possible scheduler project, not a flag. The - current common MTP flow is serial on one Strix Halo ROCm device. Real overlap - would need scheduler changes plus acceptance/correctness guards, and it - should only be promoted if it beats the sustained Qwen 262k guard. -- Coherence should stay protected by tensor-aware ROCmFP4 profiles: dual-scale - for sensitive tensors and FAST only where guarded model tests show no - sustained decode or quality regression. Pure speed-only profiles are not - promoted in this tree without the serial regression gate. +32 weights per block. Each weight is a 4-bit code: sign bit plus a 3-bit index +into the magnitude table `0, 1, 2, 3, 4, 6, 8, 10`. The low nibbles of the 16 +payload bytes are weights 0-15, the high nibbles weights 16-31. Scales are +unsigned E4M3 bytes (`e <= 0x7E`, values above are treated as 0) decoded as +`mant * 2^-10` for exponent 0 and `(8 + mant) * 2^(exp - 11)` otherwise. -Hardware note: -- This is a special AMD-targeted ggml/llama.cpp quantization and backend - path. It includes custom Vulkan and ROCm/HIP handling for the new GGUF - types, but it is not yet a native rocWMMA FP4 tensor-core implementation. - Current speed gains come from the compact block layout and backend decode - paths; deeper rocWMMA/cooperative-matrix work is future optimization work. -- NVIDIA CUDA is disabled in the Strix-FP4 build (`-DGGML_CUDA=OFF`). Some - upstream llama.cpp HIP backend sources still live under - `ggml/src/ggml-cuda` and are compiled by HIP for AMD, but the ROCmFP4-owned - helper code and user-facing build/run path are ROCm/HIP/Vulkan targeted. - This tree also accepts `GGML_HIP_ENABLE_UNIFIED_MEMORY=1` as the AMD-named - alias for the upstream unified-memory switch. -- The bundled rocWMMA 7.1.0 headers expose gfx12 WMMA paths for FP8/BF8 and - integer 8-bit inputs, but no native FP4 input type or FP4 WMMA/MFMA builtin - is visible locally. A true matrix-core ROCmFP4 path therefore needs a - measured unpack/convert strategy first, such as ROCmFP4 Codebook10 to int8 - WMMA or FP8 WMMA tiles, before claiming native FP4 tensor-core execution. -- rocWMMA FlashAttention is intentionally opt-in via - `GGML_HIP_ROCWMMA_FATTN=ON scripts/build-strix-rocmfp4-mtp.sh`. - It currently compiles with the local rocWMMA headers in - `/path/to/third_party/rocWMMA`, but the Strix Halo benchmark - regressed sustained Qwen MTP decode (`23.3 tok/s` vs the promoted - `26.2 tok/s` default HIP FlashAttention path), so the default build keeps it - disabled. A follow-up on the 35B A3B ROCmFP4 MTP guard measured only - `99.7` / `76.1 tok/s` versus the promoted `104.4` / `89.3` band, so this - remains true for the MoE-heavy profile as well. -- TurboQuant and TriAttention are not present as runtime flags in this isolated - tree. Integrating them would require a source-level merge into `strix-fp4`; - do not claim support or promote them unless they beat the serial ROCmFP4 - regression gate. +| Type | GGUF type id | Payload | Scales | Block bytes | BPW | +|---|---:|---|---|---:|---:| +| `Q4_0_ROCMFP4` | 100 | 16 bytes, 32 nibbles | 2, one per 16-weight half | 18 | 4.50 | +| `Q4_0_ROCMFP4_FAST` | 101 | 16 bytes, 32 nibbles | 1 per block | 17 | 4.25 | + +Type ids match charlie12345/ROCmFPX, so GGUFs quantized there (the published +`*-ROCmFP4-FAST*`, `*-ROCmFP4-STRIX*` files) load here. + +## What is implemented in this tree + +- CPU: quantize / dequantize / vec_dot through the ggml type traits. +- Vulkan: dequant, mat-vec, matmul, and q8_1 integer-dot (MMQ / MMVQ) kernels + under `ggml/src/ggml-vulkan/vulkan-shaders/`. +- Not implemented: ROCm/HIP kernels for these types. The HIP backend does not + know them, so on ROCm they run through the CPU codec. Flash attention does + not accept them as KV-cache types. + +## Recipes in `llama-quantize` + +`Q4_0_ROCMFP4`, `_LEAN` (Q5_K token embeddings), `_COHERENT` (Q6_K token +embeddings), `_FAST`, `_FAST_COHERENT`, `_STRIX` and `_STRIX_LEAN` (Strix Halo +attention K/V recipes). See `tools/quantize/quantize.cpp` and +`src/llama-quant.cpp` for the exact per-tensor routing. + +## Tests + +- `test-quantize-fns` covers both types with the 4-bit error budget. +- `test-backend-ops` includes both in its type lists (MUL_MAT, MUL_MAT_ID, + GET_ROWS, CPY, SET_ROWS); run with `-b Vulkan0` for the Vulkan kernels. +- `gguf-py/tests/test_quants.py --type Q4_0_ROCMFP4` (and `_FAST`) checks the + Python dequantizer against the C one bit for bit. diff --git a/ggml/rocmfp4/rocmfp4_hip.cu b/ggml/rocmfp4/rocmfp4_hip.cu deleted file mode 100644 index 3174743cc41d..000000000000 --- a/ggml/rocmfp4/rocmfp4_hip.cu +++ /dev/null @@ -1,93 +0,0 @@ -// ROCmFPx quant formats, hand-ported into this fork. -// -// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. -// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. -// -// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors -// retain their authorship and MIT license credit. See LICENSE. - -#include "rocmfp4.h" - -#include - -#include "rocmfp4_hip_scale.cuh" - -// Standalone ROCm/HIP dequant kernel for integration tests and future fused -// paths. One lane owns one packed byte and writes the matching low/high -// half-block values, so each byte is read once. -extern "C" __global__ void rocmfp4_dequantize_q4_0_f32_kernel( - const block_rocmfp4 * __restrict__ x, - float * __restrict__ y, - int64_t k) { - const int64_t packed_idx = (int64_t) blockIdx.x*blockDim.x + threadIdx.x; - const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; - const int64_t packed_count = nblocks * (QK_ROCMFP4/2); - - if (packed_idx >= packed_count) { - return; - } - - const int64_t ib = packed_idx / (QK_ROCMFP4/2); - const int tid = packed_idx - ib*(QK_ROCMFP4/2); - const int64_t base = ib*QK_ROCMFP4; - const uint8_t packed = x[ib].qs[tid]; - const float d0 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[0]); - const float d1 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[1]); - - if (base + tid < k) { - y[base + tid] = (float) rocmfp4_decode_i8(packed & 0x0f) * d0; - } - if (base + tid + QK_ROCMFP4/2 < k) { - y[base + tid + QK_ROCMFP4/2] = (float) rocmfp4_decode_i8(packed >> 4) * d1; - } -} - -extern "C" __global__ void rocmfp4_dequantize_q4_0_fast_f32_kernel( - const block_rocmfp4_fast * __restrict__ x, - float * __restrict__ y, - int64_t k) { - const int64_t packed_idx = (int64_t) blockIdx.x*blockDim.x + threadIdx.x; - const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; - const int64_t packed_count = nblocks * (QK_ROCMFP4/2); - - if (packed_idx >= packed_count) { - return; - } - - const int64_t ib = packed_idx / (QK_ROCMFP4/2); - const int tid = packed_idx - ib*(QK_ROCMFP4/2); - const int64_t base = ib*QK_ROCMFP4; - const uint8_t packed = x[ib].qs[tid]; - const float d = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e); - - if (base + tid < k) { - y[base + tid] = (float) rocmfp4_decode_i8(packed & 0x0f) * d; - } - if (base + tid + QK_ROCMFP4/2 < k) { - y[base + tid + QK_ROCMFP4/2] = (float) rocmfp4_decode_i8(packed >> 4) * d; - } -} - -extern "C" void rocmfp4_hip_dequantize_q4_0_to_f32( - const void * src, - float * dst, - int64_t k, - hipStream_t stream) { - const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; - const int64_t packed_count = nblocks * (QK_ROCMFP4/2); - const dim3 block(256); - const dim3 grid((unsigned int) ((packed_count + block.x - 1) / block.x)); - rocmfp4_dequantize_q4_0_f32_kernel<<>>((const block_rocmfp4 *) src, dst, k); -} - -extern "C" void rocmfp4_hip_dequantize_q4_0_fast_to_f32( - const void * src, - float * dst, - int64_t k, - hipStream_t stream) { - const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; - const int64_t packed_count = nblocks * (QK_ROCMFP4/2); - const dim3 block(256); - const dim3 grid((unsigned int) ((packed_count + block.x - 1) / block.x)); - rocmfp4_dequantize_q4_0_fast_f32_kernel<<>>((const block_rocmfp4_fast *) src, dst, k); -} diff --git a/ggml/rocmfp4/rocmfp4_hip_codebook.cuh b/ggml/rocmfp4/rocmfp4_hip_codebook.cuh deleted file mode 100644 index 7fa1357f8e8d..000000000000 --- a/ggml/rocmfp4/rocmfp4_hip_codebook.cuh +++ /dev/null @@ -1,87 +0,0 @@ -// ROCmFPx quant formats, hand-ported into this fork. -// -// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. -// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. -// -// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors -// retain their authorship and MIT license credit. See LICENSE. - -#pragma once - -#include "rocmfp4_hip_scale.cuh" - -#include -#include - -#ifndef GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD -#define GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD 1 -#endif - -static __device__ __forceinline__ int rocmfp4_get_qs_i32(const void * x, const int & i32) { -#if defined(GGML_USE_HIP) && GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD - return *((const int *) ((const uint8_t *) x + 4*i32)); -#else - const uint8_t * x8 = (const uint8_t *) x; - - int x32 = x8[4*i32 + 0] << 0; - x32 |= x8[4*i32 + 1] << 8; - x32 |= x8[4*i32 + 2] << 16; - x32 |= x8[4*i32 + 3] << 24; - - return x32; -#endif -} - -// AMD-specific fast path for expanding eight packed ROCmFP4 nibbles into two -// int32 DP4A operands. This encodes the Codebook10 table directly as four -// 32-bit constants: -// [0, 1, 2, 3], [4, 6, 8, 10], [0, -1, -2, -3], [-4, -6, -8, -10] -// Avoiding the table pointer keeps the ROCm/HIP MMVQ/MMQ hot path fully local -// to this format. Non-HIP builds still use llama.cpp's generic table expander. -static __device__ __forceinline__ int2 rocmfp4_get_int_from_codebook_16(const int & q4, const int8_t * fallback_table) { -#if defined(GGML_USE_HIP) - constexpr uint32_t values0 = 0x03020100u; - constexpr uint32_t values1 = 0x0a080604u; - constexpr uint32_t values2 = 0xfdfeff00u; - constexpr uint32_t values3 = 0xf6f8fafcu; - - const uint32_t q_even = q4; - const uint32_t q_odd = q4 >> 4; - - const uint32_t v_even_low = __builtin_amdgcn_perm(values1, values0, q_even & 0x07070707u); - const uint32_t v_odd_low = __builtin_amdgcn_perm(values1, values0, q_odd & 0x07070707u); - const uint32_t v_even_high = __builtin_amdgcn_perm(values3, values2, q_even & 0x07070707u); - const uint32_t v_odd_high = __builtin_amdgcn_perm(values3, values2, q_odd & 0x07070707u); - - const uint32_t mask_even = 0x03020100u | ((q_even & 0x08080808u) >> 1); - const uint32_t mask_odd = 0x03020100u | ((q_odd & 0x08080808u) >> 1); - - return make_int2( - __builtin_amdgcn_perm(v_even_high, v_even_low, mask_even), - __builtin_amdgcn_perm(v_odd_high, v_odd_low, mask_odd)); -#else - return get_int_from_table_16(q4, fallback_table); -#endif -} - -// Variant for call sites that already selected either the low or high nibble -// stream and only need one DP4A operand. This avoids the extra odd/even table -// expansion work in ROCmFP4 FlashAttention K/V decode. -static __device__ __forceinline__ int rocmfp4_get_low_int_from_codebook_16(const int & q4, const int8_t * fallback_table) { -#if defined(GGML_USE_HIP) - constexpr uint32_t values0 = 0x03020100u; - constexpr uint32_t values1 = 0x0a080604u; - constexpr uint32_t values2 = 0xfdfeff00u; - constexpr uint32_t values3 = 0xf6f8fafcu; - - const uint32_t q = q4; - - const uint32_t v_low = __builtin_amdgcn_perm(values1, values0, q & 0x07070707u); - const uint32_t v_high = __builtin_amdgcn_perm(values3, values2, q & 0x07070707u); - const uint32_t mask = 0x03020100u | ((q & 0x08080808u) >> 1); - - return __builtin_amdgcn_perm(v_high, v_low, mask); -#else - return get_int_from_table_16(q4, fallback_table).x; -#endif -} diff --git a/ggml/rocmfp4/rocmfp4_hip_scale.cuh b/ggml/rocmfp4/rocmfp4_hip_scale.cuh deleted file mode 100644 index a783be2e9493..000000000000 --- a/ggml/rocmfp4/rocmfp4_hip_scale.cuh +++ /dev/null @@ -1,165 +0,0 @@ -// ROCmFPx quant formats, hand-ported into this fork. -// -// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. -// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. -// -// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors -// retain their authorship and MIT license credit. See LICENSE. - -#pragma once - -#include -#include -#include - -#ifndef GGML_ROCMFP4_USE_SCALE_LUT -#define GGML_ROCMFP4_USE_SCALE_LUT 0 -#endif - -#if defined(GGML_USE_HIP) && GGML_ROCMFP4_USE_SCALE_LUT -#define ROCMFP4_SCALE_SUB(M) ((M) * 0x1p-10f) -#define ROCMFP4_SCALE_E1(M) ((8 + (M)) * 0x1p-10f) -#define ROCMFP4_SCALE_E2(M) ((8 + (M)) * 0x1p-9f) -#define ROCMFP4_SCALE_E3(M) ((8 + (M)) * 0x1p-8f) -#define ROCMFP4_SCALE_E4(M) ((8 + (M)) * 0x1p-7f) -#define ROCMFP4_SCALE_E5(M) ((8 + (M)) * 0x1p-6f) -#define ROCMFP4_SCALE_E6(M) ((8 + (M)) * 0x1p-5f) -#define ROCMFP4_SCALE_E7(M) ((8 + (M)) * 0x1p-4f) -#define ROCMFP4_SCALE_E8(M) ((8 + (M)) * 0x1p-3f) -#define ROCMFP4_SCALE_E9(M) ((8 + (M)) * 0x1p-2f) -#define ROCMFP4_SCALE_E10(M) ((8 + (M)) * 0x1p-1f) -#define ROCMFP4_SCALE_E11(M) ((8 + (M)) * 0x1p0f) -#define ROCMFP4_SCALE_E12(M) ((8 + (M)) * 0x1p1f) -#define ROCMFP4_SCALE_E13(M) ((8 + (M)) * 0x1p2f) -#define ROCMFP4_SCALE_E14(M) ((8 + (M)) * 0x1p3f) -#define ROCMFP4_SCALE_E15(M) ((8 + (M)) * 0x1p4f) - -static __device__ __constant__ const float rocmfp4_scale_ue4m3_half_lut[127] = { - ROCMFP4_SCALE_SUB(0), ROCMFP4_SCALE_SUB(1), ROCMFP4_SCALE_SUB(2), ROCMFP4_SCALE_SUB(3), - ROCMFP4_SCALE_SUB(4), ROCMFP4_SCALE_SUB(5), ROCMFP4_SCALE_SUB(6), ROCMFP4_SCALE_SUB(7), - ROCMFP4_SCALE_E1(0), ROCMFP4_SCALE_E1(1), ROCMFP4_SCALE_E1(2), ROCMFP4_SCALE_E1(3), - ROCMFP4_SCALE_E1(4), ROCMFP4_SCALE_E1(5), ROCMFP4_SCALE_E1(6), ROCMFP4_SCALE_E1(7), - ROCMFP4_SCALE_E2(0), ROCMFP4_SCALE_E2(1), ROCMFP4_SCALE_E2(2), ROCMFP4_SCALE_E2(3), - ROCMFP4_SCALE_E2(4), ROCMFP4_SCALE_E2(5), ROCMFP4_SCALE_E2(6), ROCMFP4_SCALE_E2(7), - ROCMFP4_SCALE_E3(0), ROCMFP4_SCALE_E3(1), ROCMFP4_SCALE_E3(2), ROCMFP4_SCALE_E3(3), - ROCMFP4_SCALE_E3(4), ROCMFP4_SCALE_E3(5), ROCMFP4_SCALE_E3(6), ROCMFP4_SCALE_E3(7), - ROCMFP4_SCALE_E4(0), ROCMFP4_SCALE_E4(1), ROCMFP4_SCALE_E4(2), ROCMFP4_SCALE_E4(3), - ROCMFP4_SCALE_E4(4), ROCMFP4_SCALE_E4(5), ROCMFP4_SCALE_E4(6), ROCMFP4_SCALE_E4(7), - ROCMFP4_SCALE_E5(0), ROCMFP4_SCALE_E5(1), ROCMFP4_SCALE_E5(2), ROCMFP4_SCALE_E5(3), - ROCMFP4_SCALE_E5(4), ROCMFP4_SCALE_E5(5), ROCMFP4_SCALE_E5(6), ROCMFP4_SCALE_E5(7), - ROCMFP4_SCALE_E6(0), ROCMFP4_SCALE_E6(1), ROCMFP4_SCALE_E6(2), ROCMFP4_SCALE_E6(3), - ROCMFP4_SCALE_E6(4), ROCMFP4_SCALE_E6(5), ROCMFP4_SCALE_E6(6), ROCMFP4_SCALE_E6(7), - ROCMFP4_SCALE_E7(0), ROCMFP4_SCALE_E7(1), ROCMFP4_SCALE_E7(2), ROCMFP4_SCALE_E7(3), - ROCMFP4_SCALE_E7(4), ROCMFP4_SCALE_E7(5), ROCMFP4_SCALE_E7(6), ROCMFP4_SCALE_E7(7), - ROCMFP4_SCALE_E8(0), ROCMFP4_SCALE_E8(1), ROCMFP4_SCALE_E8(2), ROCMFP4_SCALE_E8(3), - ROCMFP4_SCALE_E8(4), ROCMFP4_SCALE_E8(5), ROCMFP4_SCALE_E8(6), ROCMFP4_SCALE_E8(7), - ROCMFP4_SCALE_E9(0), ROCMFP4_SCALE_E9(1), ROCMFP4_SCALE_E9(2), ROCMFP4_SCALE_E9(3), - ROCMFP4_SCALE_E9(4), ROCMFP4_SCALE_E9(5), ROCMFP4_SCALE_E9(6), ROCMFP4_SCALE_E9(7), - ROCMFP4_SCALE_E10(0), ROCMFP4_SCALE_E10(1), ROCMFP4_SCALE_E10(2), ROCMFP4_SCALE_E10(3), - ROCMFP4_SCALE_E10(4), ROCMFP4_SCALE_E10(5), ROCMFP4_SCALE_E10(6), ROCMFP4_SCALE_E10(7), - ROCMFP4_SCALE_E11(0), ROCMFP4_SCALE_E11(1), ROCMFP4_SCALE_E11(2), ROCMFP4_SCALE_E11(3), - ROCMFP4_SCALE_E11(4), ROCMFP4_SCALE_E11(5), ROCMFP4_SCALE_E11(6), ROCMFP4_SCALE_E11(7), - ROCMFP4_SCALE_E12(0), ROCMFP4_SCALE_E12(1), ROCMFP4_SCALE_E12(2), ROCMFP4_SCALE_E12(3), - ROCMFP4_SCALE_E12(4), ROCMFP4_SCALE_E12(5), ROCMFP4_SCALE_E12(6), ROCMFP4_SCALE_E12(7), - ROCMFP4_SCALE_E13(0), ROCMFP4_SCALE_E13(1), ROCMFP4_SCALE_E13(2), ROCMFP4_SCALE_E13(3), - ROCMFP4_SCALE_E13(4), ROCMFP4_SCALE_E13(5), ROCMFP4_SCALE_E13(6), ROCMFP4_SCALE_E13(7), - ROCMFP4_SCALE_E14(0), ROCMFP4_SCALE_E14(1), ROCMFP4_SCALE_E14(2), ROCMFP4_SCALE_E14(3), - ROCMFP4_SCALE_E14(4), ROCMFP4_SCALE_E14(5), ROCMFP4_SCALE_E14(6), ROCMFP4_SCALE_E14(7), - ROCMFP4_SCALE_E15(0), ROCMFP4_SCALE_E15(1), ROCMFP4_SCALE_E15(2), ROCMFP4_SCALE_E15(3), - ROCMFP4_SCALE_E15(4), ROCMFP4_SCALE_E15(5), ROCMFP4_SCALE_E15(6), -}; - -#undef ROCMFP4_SCALE_SUB -#undef ROCMFP4_SCALE_E1 -#undef ROCMFP4_SCALE_E2 -#undef ROCMFP4_SCALE_E3 -#undef ROCMFP4_SCALE_E4 -#undef ROCMFP4_SCALE_E5 -#undef ROCMFP4_SCALE_E6 -#undef ROCMFP4_SCALE_E7 -#undef ROCMFP4_SCALE_E8 -#undef ROCMFP4_SCALE_E9 -#undef ROCMFP4_SCALE_E10 -#undef ROCMFP4_SCALE_E11 -#undef ROCMFP4_SCALE_E12 -#undef ROCMFP4_SCALE_E13 -#undef ROCMFP4_SCALE_E14 -#undef ROCMFP4_SCALE_E15 -#endif - -static __device__ __forceinline__ float rocmfp4_u32_as_f32(uint32_t bits) { -#if defined(GGML_USE_HIP) - return __uint_as_float(bits); -#else - float result; - memcpy(&result, &bits, sizeof(float)); - return result; -#endif -} - -// ROCmFP4 validates scale bytes before backend execution, so HIP/ROCm hot -// paths can decode finite unsigned E4M3 half-scales directly without the -// generic FP8 NaN handling used by other formats. -static __device__ __forceinline__ float rocmfp4_ue4m3_to_fp32_half_finite(uint8_t x) { -#if defined(GGML_USE_HIP) && GGML_ROCMFP4_USE_SCALE_LUT - return x <= 0x7e ? rocmfp4_scale_ue4m3_half_lut[x] : 0.0f; -#else - const int exp = (x >> 3) & 0xF; - const int man = x & 0x7; - - if (exp == 0) { - return (float) man * (1.0f / 1024.0f); - } - - const uint32_t bits = ((uint32_t) exp + 119u) << 23 | ((uint32_t) man << 20); - return rocmfp4_u32_as_f32(bits); -#endif -} - -static __device__ __forceinline__ float rocmfpx_ue4m3_to_fp32_finite(uint8_t x) { - if (x > 0x7e) { - return 0.0f; - } - - const int exp = (x >> 3) & 0xF; - const int man = x & 0x7; - - if (exp == 0) { - return (float) man * (1.0f / 1024.0f); - } - - const uint32_t bits = ((uint32_t) exp + 119u) << 23 | ((uint32_t) man << 20); - return rocmfp4_u32_as_f32(bits); -} - -static __device__ __forceinline__ uint8_t rocmfpx_nearest_scale_ue4m3_cuda(float target_scale) { - if (!(target_scale > 0.0f) || !isfinite(target_scale)) { - return 0; - } - - uint8_t lo = 1; - uint8_t hi = 0x7e; - while (lo < hi) { - const uint8_t mid = lo + (hi - lo) / 2; - if (rocmfpx_ue4m3_to_fp32_finite(mid) < target_scale) { - lo = mid + 1; - } else { - hi = mid; - } - } - - if (lo == 1) { - return 1; - } - - const float hi_scale = rocmfpx_ue4m3_to_fp32_finite(lo); - const float lo_scale = rocmfpx_ue4m3_to_fp32_finite((uint8_t) (lo - 1)); - return (target_scale - lo_scale <= hi_scale - target_scale) ? (uint8_t) (lo - 1) : lo; -} - -static __device__ __forceinline__ int8_t rocmfp4_decode_i8(uint8_t q) { - q &= 0x0f; - const int mag3 = q & 0x07; - const int mag = mag3 <= 4 ? mag3 : 2*mag3 - 4; - return (q & 0x08) ? -mag : mag; -} diff --git a/ggml/rocmfpx/README.md b/ggml/rocmfpx/README.md index 8d5d53c911ed..8d122c5b7ec2 100644 --- a/ggml/rocmfpx/README.md +++ b/ggml/rocmfpx/README.md @@ -1,109 +1,56 @@ -# ROCmFPx Experimental Formats +# ROCmFPx -This folder contains the reference layer for the proposed ROCmFP3, ROCmFP6, and -ROCmFP8 quantization family. It is intentionally separate from `ggml/rocmfp4/` -so the promoted ROCmFP4 GGUF formats and kernels are not affected while the new -layouts are evaluated. +The 2/3/6/8-bit ROCmFPx tensor types. The formats originate in +[charlie12345/ROCmFPX](https://github.com/charlie12345/ROCmFPX) (MIT); the codecs +here were hand-ported via [ciru-ai/ROCmFPX](https://github.com/ciru-ai/ROCmFPX). +The 4-bit types live in `../rocmfp4/`. -## ROCmFP4 Instructions To Preserve - -ROCmFPX is a sibling model-weight quant family, not a new K/V-only compression -scheme. The promoted ROCmFP4 implementation remains the template for how the -family should behave in llama.cpp: - -- Keep 32-weight blocks so CPU, HIP, and Vulkan kernels can reuse the same - Q4/Q8-style reduction shape and GGUF row-size assumptions. -- Use finite unsigned UE4M3 scale bytes only. `0x7f` and sign-bit scale bytes - are invalid, matching the ROCmFP4 validation rule. -- Prefer reconstruction-MSE scale selection over plain max-abs scaling. ROCmFP4 - searches each 16-weight half-block; ROCmFP3 and ROCmFP6 follow the same - half-block policy, while ROCmFP8 currently uses one full-block scale. -- Preserve the ROCmFP4 kernel contract: CPU reference quant/dequant/dot first, - then HIP/Vulkan `CPY`, `GET_ROWS`, `SET_ROWS`, `MUL_MAT`, and `MUL_MAT_ID` - paths, with backend-op coverage before claiming runtime support. -- Keep dequant math explicit and deterministic: integer code times decoded - UE4M3 scale. ROCmFP4 uses the Codebook10 half-scale table; ROCmFPX formats - use their own integer code ranges but must retain the same finite-scale and - integer-dot discipline. - -The ROCmFP4 Codebook10 levels are not reused by FP3/FP6/FP8 directly: -`ROCmFP3` uses `0, +/-1, +/-2, +/-4`, `ROCmFP6` uses signed-magnitude levels up -to `31`, and `ROCmFP8` uses signed int8 levels clamped to `[-127, 127]`. -What is inherited is the block/scale/kernel/dequant contract. - -Current status (June 16, 2026): -- CPU reference quantize/dequantize exists for all three formats. -- `Q3_0_ROCMFPX`, `Q6_0_ROCMFPX`, and `Q8_0_ROCMFPX` are registered as - experimental GGUF tensor types. -- ROCm/HIP and Vulkan kernels support `CPY`, `GET_ROWS`, `SET_ROWS`, and - `MUL_MAT`/`MUL_MAT_ID` for all three formats. -- Qwen3-0.6B BF16 smoke tests pass on CPU, ROCm0, and Vulkan0. -- Generic baseline quant presets now include lean coherency routing. These - preset names are not release recipe identities; released artifact mappings - and architecture-qualified recipe IDs live in - [`docs/recipes/`](../../docs/recipes/README.md): - - `Q3_0_ROCMFPX`: selective `Q5_K` on attention Q/O and early K/V, boosted - FFN-down at `Q5_K`, selective FFN-gate at `Q6_0_ROCMFPX`, bulk FFN-up on - `Q3_0_ROCMFPX`, embeddings/output at `Q4_0_ROCMFP4_FAST`. - - `Q6_0_ROCMFPX`: early attention and boosted FFN-down at `Q8_0_ROCMFPX`, - embeddings/output at `Q6_0_ROCMFPX`, bulk gate/up on `Q6_0_ROCMFPX`. - - `Q8_0_ROCMFPX`: pure FP8-family preset. -- Opt-in `*_AGENT` presets boost attention/FFN routing for tool-call / - Hermes / OpenClaw style workloads: - - `Q3_0_ROCMFPX_AGENT`, `Q6_0_ROCMFPX_AGENT`, `Q8_0_ROCMFPX_AGENT`. - - Routing is layered on top of LEAN; default presets are unchanged. -- FP3 and FP6 quantization use reconstruction-MSE scale selection per - 16-weight half-block. - -## Validation Script Index - -```text -scripts/check-rocmfpx-reference.sh # CPU reference math -scripts/check-rocmfpx-qwen-all.sh # core Qwen gates -scripts/check-rocmfpx-all.sh # qwen-all + optional smokes -scripts/check-rocmfpx-summary.sh # full JSON summary runner -scripts/sweep-rocmfpx-backend-ops.sh # test-backend-ops per backend -scripts/sweep-rocmfpx-agent-size-table.sh # LEAN vs AGENT MiB/BPW -scripts/sweep-rocmfpx-perplexity.sh # calibration PPL sweep -scripts/sweep-rocmfpx-decode-tune.sh # decode-tune matrix -scripts/build-rocmfpx-agent-fixtures.sh # proxy Hermes/OpenClaw AGENT GGUFs -``` +This directory owns the CPU codecs (`rocmfpx.c`) and the standalone ROCmFP2 +reference encoder (`rocmfp2_reference.c`) that the optimized FP2 encoder is +checked against. The rest of ggml registers and dispatches the types. ## Layouts -All formats use 32-weight blocks. - -| Format | Payload | Scale bytes | Block bytes | BPW | Purpose | -|---|---:|---:|---:|---:|---| -| `Q3_0_ROCMFPX` | 32 packed 3-bit codes | 2, one per 16 weights | 14 | 3.50 | Experimental low-bit candidate | -| `Q6_0_ROCMFPX` | 32 packed 6-bit codes | 2, one per 16 weights | 26 | 6.50 | Experimental quality candidate | -| `Q8_0_ROCMFPX` | 32 signed 8-bit codes | 1, one per 32 weights | 33 | 8.25 | Experimental high-quality reference | - -`ROCmFP3` uses a tiny signed codebook: `0, +/-1, +/-2, +/-4`. -`ROCmFP6` uses signed-magnitude integer levels up to `31`. -`ROCmFP8` uses signed int8 levels clamped to `[-127, 127]`. - -## Validation - -Reference math only: - -```bash -scripts/check-rocmfpx-reference.sh -``` - -Focused backend sweep from the experiment worktree: - -```bash -cmake --build build-strix-rocmfp4 --target test-backend-ops -j 8 -timeout 120 build-strix-rocmfp4/bin/test-backend-ops test -o MUL_MAT,GET_ROWS,CPY,SET_ROWS -b CPU -timeout 180 build-strix-rocmfp4/bin/test-backend-ops test -o MUL_MAT,GET_ROWS,CPY,SET_ROWS -b ROCm0 -timeout 180 build-strix-rocmfp4/bin/test-backend-ops test -o MUL_MAT,GET_ROWS,CPY,SET_ROWS -b Vulkan0 -``` - -Qwen3 BF16 coherency and decode-speed gates: - -```bash -MODEL=/home/caf/strix-fp4/models/rocmfpx-bf16-tests/Qwen3-0.6B-Q3_0_ROCMFPX_COHERENT-LEAN.gguf BACKEND=ROCm0 scripts/check-rocmfpx-qwen-coherency.sh -MODEL=/home/caf/strix-fp4/models/rocmfpx-bf16-tests/Qwen3-0.6B-Q3_0_ROCMFPX_COHERENT-LEAN.gguf BACKEND=ROCm0 scripts/check-rocmfpx-qwen-bench.sh -MODEL=/home/caf/strix-fp4/models/rocmfpx-bf16-tests/Qwen3-0.6B-Q3_0_ROCMFPX_COHERENT-LEAN.gguf BACKEND=ROCm0 scripts/check-rocmfpx-qwen-strict-json.sh -``` +All formats use 32-weight blocks and the same unsigned E4M3 scale byte as +ROCmFP4 (see `../rocmfp4/README.md`). + +| Type | GGUF type id | Payload | Scales | Block bytes | BPW | +|---|---:|---|---|---:|---:| +| `Q2_0_ROCMFPX` | 107 | 32 packed 2-bit codes, LSB first | 2, one per 16-weight half | 10 | 2.50 | +| `Q3_0_ROCMFPX` | 104 | 32 packed 3-bit codes, LSB first | 2, one per 16-weight half | 14 | 3.50 | +| `Q6_0_ROCMFPX` | 102 | 32 packed 6-bit codes, LSB first | 2, one per 16-weight half | 26 | 6.50 | +| `Q8_0_ROCMFPX` | 103 | 32 signed int8 | 1 per block | 33 | 8.25 | + +- ROCmFP2 codes map to `-4, -1, 1, 4`. +- ROCmFP3 codes are sign + 2-bit magnitude index into `0, 1, 2, 4`. +- ROCmFP6 codes are sign + 5-bit magnitude; code 32 (sign set, magnitude 0) + decodes to `-32`. +- ROCmFP8 is signed int8 clamped to `[-127, 127]`. + +### Type ids and the two upstreams + +Ids 100-104 are the same in every ROCmFPX tree. For `Q2_0_ROCMFPX` the +upstreams disagree: charlie12345/ROCmFPX (and the kingjones30 fork the ROCm +builds come from) use tensor type 107 and file type 119; ciru-ai/ROCmFPX moved +it to 108 / 122 and put `Q7_0_ROCMFPX` at 107 / 119. This tree follows +charlie12345, which is what the published GGUFs carry. Ids 105, 106 (TURBO3_0, +TURBO4_0) and 108 (ROCMI4 or ciru-ai's Q2_0) are deliberately left unassigned +so files using them are rejected instead of misread. + +## What is implemented in this tree + +- CPU: quantize / dequantize / vec_dot for all four types, imatrix weighting. +- Vulkan: dequant, mat-vec and matmul for Q3/Q6/Q8; q8_1 integer-dot mat-vec + for Q8. Q2 runs on CPU. +- Not implemented: ROCm/HIP kernels. On ROCm these types run through the CPU + codecs. + +## Tests + +- `ctest -R rocmfp` runs `test-rocmfpx` (codecs and imatrix weighting) and + `test-rocmfp2-reference` (the FP2 reference encoder: layout, scale encoding, + golden vectors, non-finite input). +- `test-quantize-fns` covers all four types with per-width error budgets. +- `test-backend-ops` includes them in its type lists. +- `gguf-py/tests/test_quants.py --type Q6_0_ROCMFPX` (etc.) checks the Python + dequantizers against C. diff --git a/ggml/rocmfpx/rocmfpx_hip_codebook.cuh b/ggml/rocmfpx/rocmfpx_hip_codebook.cuh deleted file mode 100644 index b9e36528c2fc..000000000000 --- a/ggml/rocmfpx/rocmfpx_hip_codebook.cuh +++ /dev/null @@ -1,53 +0,0 @@ -// ROCmFPx quant formats, hand-ported into this fork. -// -// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. -// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. -// -// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors -// retain their authorship and MIT license credit. See LICENSE. - -#pragma once - -#include - -static __device__ __constant__ const int8_t rocmfpx_fp3_codebook[8] = { - 0, 1, 2, 4, 0, -1, -2, -4, -}; - -static __device__ __constant__ const int8_t rocmfpx_fp6_codebook[64] = { - 0, 1, 2, 3, 4, 5, 6, 7, - 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, - 24, 25, 26, 27, 28, 29, 30, 31, - 0, -1, -2, -3, -4, -5, -6, -7, - -8, -9, -10, -11, -12, -13, -14, -15, - -16, -17, -18, -19, -20, -21, -22, -23, - -24, -25, -26, -27, -28, -29, -30, -31, -}; - -static __device__ __forceinline__ int rocmfpx_pack4_fp3_codes(const uint32_t bits12) { - const uint32_t c0 = (bits12 >> 0) & 7u; - const uint32_t c1 = (bits12 >> 3) & 7u; - const uint32_t c2 = (bits12 >> 6) & 7u; - const uint32_t c3 = (bits12 >> 9) & 7u; - const char4 v = make_char4( - rocmfpx_fp3_codebook[c0], - rocmfpx_fp3_codebook[c1], - rocmfpx_fp3_codebook[c2], - rocmfpx_fp3_codebook[c3]); - return *((const int *) &v); -} - -static __device__ __forceinline__ int rocmfpx_pack4_fp6_codes(const uint32_t bits24) { - const uint32_t c0 = (bits24 >> 0) & 63u; - const uint32_t c1 = (bits24 >> 6) & 63u; - const uint32_t c2 = (bits24 >> 12) & 63u; - const uint32_t c3 = (bits24 >> 18) & 63u; - - const char4 v = make_char4( - rocmfpx_fp6_codebook[c0], - rocmfpx_fp6_codebook[c1], - rocmfpx_fp6_codebook[c2], - rocmfpx_fp6_codebook[c3]); - return *((const int *) &v); -} From 525d2b373b75d1e4237b24baf47d4c9eaaeedf89 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Mon, 7 Sep 2026 00:14:58 +0000 Subject: [PATCH 079/212] common : do not offer ROCmFPx types as KV cache types The ROCmFPx types are weight formats in this tree. Flash attention has no decode path for them: fa_kv_ok in ggml-vulkan.cpp rejects all of them, and flash_attn_dequant.glsl has no aliased view for their blocks (block_rocmfp4_fast is 17 bytes, so a *_packed16 view is not expressible for it). Listing them in kv_cache_types made them selectable with -ctk/-ctv and printed them in --help. That combination does not fall back the way an unsupported KV type normally would: all of these types are quantized, a quantized V cache sets flash_attn_type to ENABLED in llama_init_from_model, and that clears auto_fa, which is the guard on the flash attention support probe. The probe that would turn flash attention back off never runs. Also drops the K-type override that silently replaced a requested q3_0_rocmfpx K cache with q6_0_rocmfpx: it changed an explicit user choice behind a warning, and applied to K only, so -ctk q3_0_rocmfpx -ctv q3_0_rocmfpx produced mismatched K and V types. Co-Authored-By: Claude Opus 5 --- common/arg.cpp | 9 ++++----- common/common.cpp | 4 ---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index 47dacc5857a1..a016606a5514 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -311,11 +311,10 @@ const std::vector kv_cache_types = { GGML_TYPE_IQ4_NL, GGML_TYPE_Q5_0, GGML_TYPE_Q5_1, - GGML_TYPE_Q4_0_ROCMFP4, - GGML_TYPE_Q4_0_ROCMFP4_FAST, - GGML_TYPE_Q3_0_ROCMFPX, - GGML_TYPE_Q6_0_ROCMFPX, - GGML_TYPE_Q8_0_ROCMFPX, + // ROCmFPx types are weight formats here, not KV cache types: flash attention + // has no decode path for them (fa_kv_ok in ggml-vulkan.cpp rejects them), and + // a quantized V cache force-enables flash attention in llama_init_from_model, + // which skips the support probe that would otherwise turn it back off. }; static ggml_type kv_cache_type_from_str(const std::string & s) { diff --git a/common/common.cpp b/common/common.cpp index 2588cd6ee182..a6a5364163bc 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -1755,10 +1755,6 @@ struct llama_context_params common_context_params_to_llama(const common_params & cparams.type_k = params.cache_type_k; cparams.type_v = params.cache_type_v; - if (cparams.type_k == GGML_TYPE_Q3_0_ROCMFPX) { - LOG_WRN("%s: q3_0_rocmfpx K cache is below the coherency floor; using q6_0_rocmfpx for K\n", __func__); - cparams.type_k = GGML_TYPE_Q6_0_ROCMFPX; - } return cparams; } From 4a539f3e4ca875a932414620492f3ac976b1c4e0 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Mon, 7 Sep 2026 09:29:28 +0100 Subject: [PATCH 080/212] readme : ROCmFPx types are weight formats, not KV-cache types Follow-up to the previous commit, which removed them from the -ctk/-ctv list. Assisted-by: Claude Fable 5.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65236f8387d9..3067a7a4e71e 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Everything else is upstream `llama.cpp`. The additions currently carried here: | ROCm/HIP grouped decode matvecs | `GGML_CUDA_DISABLE_MMV_GROUP=1` | Consecutive single-column matvecs that read the same activation (gate/up pairs, hyper-connection projections) launch as one kernel | | ROCm/HIP flash attention on RDNA 3.5 | | WMMA path for D=256 prefill at depth, Q8_0 KV tile kernel for decode | | Speculative checkpoints on device | | `llama-server` keeps speculative-decoding checkpoints in device memory instead of copying them to the host | -| ROCmFPx quant types | `llama-quantize` types `Q4_0_ROCMFP4`, `Q4_0_ROCMFP4_FAST`, `Q2/Q3/Q6/Q8_0_ROCMFPX` and the `_LEAN`/`_COHERENT`/`_STRIX` recipes | Loads the ROCmFP4 GGUFs published for Strix Halo. CPU codecs plus Vulkan dequant, mat-vec, matmul and integer-dot kernels; the coopmat2 flash-attention path does not decode them yet | +| ROCmFPx quant types | `llama-quantize` types `Q4_0_ROCMFP4`, `Q4_0_ROCMFP4_FAST`, `Q2/Q3/Q6/Q8_0_ROCMFPX` and the `_LEAN`/`_COHERENT`/`_STRIX` recipes | Loads the ROCmFP4 GGUFs published for Strix Halo. CPU codecs plus Vulkan dequant, mat-vec, matmul and integer-dot kernels. Weight formats only: not accepted as KV-cache types | | Repeatable output at depth | | Freed KV cells are zeroed so masked-out rows never carry stale K/V, and the Vulkan radix top-k assigns output slots deterministically | Every ROCm/HIP change above is guarded on architecture, shape and layout, so other devices see upstream behaviour. From eedd906417af3a3cd23fbc52909bc80d61cabff0 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Tue, 8 Sep 2026 00:06:34 +0100 Subject: [PATCH 081/212] tests : keep the test-rocmfpx assertions alive in Release builds Every check in test_rocmfpx.c is a plain assert(), including the rocmfpx_validate_row_data_*() calls and the mse ordering comparisons. The Release configuration the PR builds defines NDEBUG, so all of them compiled away and the test still exited 0 - it validated nothing. Undefine NDEBUG before including , the way test-quantize-fns.cpp and test-quantize-perf.cpp already do. test_rocmfp2_reference.c does not need this: it uses its own always-active check machinery rather than assert(). Assisted-by: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VcXid9TGNfxqxxNWFQGES8 --- ggml/rocmfpx/test_rocmfpx.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ggml/rocmfpx/test_rocmfpx.c b/ggml/rocmfpx/test_rocmfpx.c index ff1db79fdd50..75dc6a0d614d 100644 --- a/ggml/rocmfpx/test_rocmfpx.c +++ b/ggml/rocmfpx/test_rocmfpx.c @@ -8,6 +8,11 @@ #include "rocmfpx.h" +// Every check below is an assert(), so keep them alive in Release builds - +// otherwise the test would validate nothing and still exit 0. +#ifdef NDEBUG +#undef NDEBUG +#endif #include #include #include From 14a5b92ff0408b913d82fd9a8e9bac6d84db9785 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Tue, 8 Sep 2026 00:06:34 +0100 Subject: [PATCH 082/212] tests : link rocmfpx from ggml-base instead of compiling a second copy rocmfpx.c is already a source of ggml-base, and everything test_rocmfpx.c calls is declared GGML_API. Compiling it into the test as well breaks MSVC shared-library builds: with BUILD_SHARED_LIBS=ON and GGML_BACKEND_DL=OFF the test inherits GGML_SHARED through llama -> ggml -> ggml-base but not GGML_BUILD, so the codec declarations become __declspec(dllimport) in the same translation unit that defines them, which MSVC rejects. Drop the extra source and let the test link the library implementation. test-rocmfp2-reference keeps compiling rocmfp2_reference.c directly: it is a standalone reference that is not part of ggml-base and carries no GGML_API. Assisted-by: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VcXid9TGNfxqxxNWFQGES8 --- tests/CMakeLists.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ec1b142eba85..a222bc7e4053 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -329,8 +329,14 @@ if (NOT GGML_BACKEND_DL) # ROCmFPx codec self-tests, kept next to the codecs under ggml/rocmfpx. # test-rocmfpx exercises the ROCmFP3/6/8 codecs; test-rocmfp2-reference # checks the standalone ROCmFP2 reference encoder. + # + # rocmfpx.c is already part of ggml-base and its entry points are GGML_API, + # so link it rather than compiling a second copy: in a shared build the + # inherited GGML_SHARED would make those declarations __declspec(dllimport) + # in the very translation unit that defines them, which MSVC rejects. + # rocmfp2_reference.c is standalone and carries no GGML_API, so it is still + # compiled straight into its test. llama_build_and_test(${PROJECT_SOURCE_DIR}/ggml/rocmfpx/test_rocmfpx.c - ${PROJECT_SOURCE_DIR}/ggml/rocmfpx/rocmfpx.c NAME test-rocmfpx) target_include_directories(test-rocmfpx PRIVATE ${PROJECT_SOURCE_DIR}/ggml/rocmfpx) llama_build_and_test(${PROJECT_SOURCE_DIR}/ggml/rocmfpx/test_rocmfp2_reference.c From df563b74e371f912a832aa1d988d483aae5693f1 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Tue, 8 Sep 2026 00:06:49 +0100 Subject: [PATCH 083/212] rocmfpx : let the ROCmFP2 encoder pick the zero scale rocmfpx_choose_scale_fp2_mse() only searched scale bytes 1..126, returning 0 solely when the block was all zeros. ROCmFP2's S40 codebook is {-4, -1, +1, +4}, which has no zero code, so at any nonzero scale the smallest magnitude it can emit is one scale step. A block that sits below half of the smallest UE4M3 scale is then amplified rather than approximated. A block filled with 2^-14 reconstructed as 2^-10, 16x too large, for a squared error 225x greater than encoding it as zero. rocmfp2_p1_quantize_block_ref(), which searches every legal byte 0x00..0x7e, picked scale byte 0 for the same input - so the two encoders in this tree disagreed. Seed the search with the zero-scale candidate so bytes 1..126 have to beat it. Ties keep byte 0, matching both the lower-byte rule the rest of the search uses and the standalone reference. before: e=(1,1) sse=2.68e-05 after: e=(0,0) sse=1.19e-07 (reference: 5.96e-08 per 16-weight group) FP3, FP6 and FP8 all have a zero code and so can already match the zero scale by rounding; only FP2 needs this. Assisted-by: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VcXid9TGNfxqxxNWFQGES8 --- ggml/rocmfpx/rocmfpx.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ggml/rocmfpx/rocmfpx.c b/ggml/rocmfpx/rocmfpx.c index b169cd96a88b..99dc13231a0b 100644 --- a/ggml/rocmfpx/rocmfpx.c +++ b/ggml/rocmfpx/rocmfpx.c @@ -306,9 +306,17 @@ static uint8_t rocmfpx_choose_scale_fp2_mse( } const float * weights = quant_weights ? mse_weights : NULL; + + // The S40 codebook {-4, -1, +1, +4} has no zero code, so at any nonzero scale + // the smallest magnitude ROCmFP2 can emit is one scale step. A block that sits + // below half of the smallest UE4M3 scale is therefore encoded better by the + // zero scale than by e = 1, which would amplify every value. Seed the search + // with that candidate so scales 1..126 have to beat it; ties keep byte 0, + // matching the lower-byte rule the rest of the search uses. + uint8_t best_e = 0; + float best_err = rocmfpx_fp2_group_mse_for_scale(x, weights, n, 0, INFINITY); + const uint8_t start_e = rocmfpx_nearest_scale_ue4m3(max_abs / 4.0f); - uint8_t best_e = start_e; - float best_err = INFINITY; bool lower_done = false; for (int delta = 0; delta <= 125; ++delta) { From 8c9dc1ca9028e9934c4fa21eb9759ed9d9f31b80 Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Tue, 8 Sep 2026 00:07:04 +0100 Subject: [PATCH 084/212] rocmfpx : bound FP6 scale pruning by 32, the magnitude it can reach The FP6 scale search stops descending once the unavoidable clipping error at a scale already exceeds the best error found so far. That bound was computed as max_abs - 31*scale, but rocmfpx_decode_fp6_code() maps code 32 to -32: the representable range is the asymmetric [-32, 31]. For a block whose largest magnitude is negative the bound therefore overestimates the error that cannot be avoided, and the search gives up before reaching the scale that wins. Sweeping 20000 half-blocks with a single dominant outlier: before: 324 suboptimal with a negative outlier, 0 with a positive one after: 0 suboptimal in either case worst observed case: byte 12 at sse 1.783e-04 where byte 11 gives 1.367e-04. The asymmetry between the two columns is exactly what the wrong bound predicts. The Vulkan encoder carried the same bound, so fix both together - the two encoders have to agree on the scale they choose. Assisted-by: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VcXid9TGNfxqxxNWFQGES8 --- ggml/rocmfpx/rocmfpx.c | 6 +++++- ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ggml/rocmfpx/rocmfpx.c b/ggml/rocmfpx/rocmfpx.c index 99dc13231a0b..de573338bc21 100644 --- a/ggml/rocmfpx/rocmfpx.c +++ b/ggml/rocmfpx/rocmfpx.c @@ -812,7 +812,11 @@ static uint8_t rocmfpx_choose_scale_fp6_mse_impl( const int e0 = (int) start_e - delta; if (!lower_done && e0 >= 1 && e0 <= 126) { const float scale = rocmfpx_scale_lookup((uint8_t) e0); - const float clip_delta = max_abs - 31.0f*scale; + // ROCmFP6 reaches -32, not just 31, so bound the unavoidable clipping + // error by 32: at 31 the bound is too pessimistic for a block whose + // largest magnitude is negative, and the search stops before reaching + // the scale that actually wins. + const float clip_delta = max_abs - 32.0f*scale; const float clip_err = mse_weights ? max_abs_weight*clip_delta*clip_delta : clip_delta*clip_delta; if (clip_delta > 0.0f && clip_err > best_err) { lower_done = true; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp b/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp index 481f9a8ca135..bed84cef00bc 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp @@ -680,7 +680,11 @@ uint rocmfpx_choose_scale_fp6_mse(uint src_idx, uint offset) { const int e0 = int(start_e) - int(delta); if (!lower_done && e0 >= 1 && e0 <= 126) { const float scale = ue4m3_to_fp32(uint8_t(uint(e0))); - const float clip_delta = max_abs - 31.0 * scale; + // ROCmFP6 reaches -32, not just 31, so bound the unavoidable clipping + // error by 32 - matching rocmfpx_choose_scale_fp6_mse_impl() on the CPU. + // At 31 the bound is too pessimistic for a block whose largest magnitude + // is negative and the search stops before the scale that actually wins. + const float clip_delta = max_abs - 32.0 * scale; if (clip_delta > 0.0 && clip_delta*clip_delta > best_err) { lower_done = true; } else { From ff9cbe4ba7a061924a8f3444c54980df5fafb67e Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk Date: Tue, 8 Sep 2026 00:07:21 +0100 Subject: [PATCH 085/212] rocmfpx : clamp before the integer conversion in the FP6/FP8 encoders rocmfpx_quantize_fp6_code(), rocmfpx_fp6_decoded_value() and rocmfpx_quantize_fp8_code() rounded to int first and clamped afterwards. The isfinite() guard keeps NaN and inf out, but a large finite weight still overflows the conversion, so the value that gets clamped is already wrong: 1e12 -> fp6 -0.03125 sign flipped 1e30 -> fp6 0, fp8 0 magnitude lost entirely Clamp the float to the representable range first, then convert. For in-range values this is identical to the old order - rounding cannot push a value past a bound it was not already past - so ordinary weights encode bit for bit as before, and the extremes now saturate: 1e12 -> fp6 31*scale, fp8 127*scale 1e30 -> fp6 31*scale, fp8 127*scale The Vulkan encoder had the same shape, and int() of an out-of-range float is undefined in GLSL, so fix it in the same commit to keep the two encoders in agreement. This is extreme-input robustness, not a defect seen on ordinary weights. Assisted-by: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VcXid9TGNfxqxxNWFQGES8 --- ggml/rocmfpx/rocmfpx.c | 34 +++++++------------ .../vulkan-shaders/copy_to_quant.comp | 7 ++-- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/ggml/rocmfpx/rocmfpx.c b/ggml/rocmfpx/rocmfpx.c index de573338bc21..8582a2038539 100644 --- a/ggml/rocmfpx/rocmfpx.c +++ b/ggml/rocmfpx/rocmfpx.c @@ -690,6 +690,15 @@ static uint8_t rocmfpx_choose_scale_fp3_weighted_mse(const float * x, int n, con return rocmfpx_choose_scale_fp3_mse_impl(x, n, mse_weights, max_abs, max_abs_weight, all_finite); } +// Round to the nearest integer inside [lo, hi], clamping *before* the conversion. +// Converting first and clamping afterwards overflows int (and, for large enough +// inputs, long) so an extreme finite weight could flip sign or collapse to zero. +// For in-range values this is identical to rounding then clamping. +static inline int rocmfpx_round_clamp(float v, float lo, float hi) { + const float c = v < lo ? lo : (v > hi ? hi : v); + return (int) lroundf(c); +} + static int rocmfpx_decode_fp6_code(uint8_t code) { const int mag = code & 31u; return (code & 32u) ? -(mag == 0 ? 32 : mag) : mag; @@ -700,12 +709,7 @@ static uint8_t rocmfpx_quantize_fp6_code(float x, float inv_scale) { return 0; } - int q = (int) lroundf(x * inv_scale); - if (q > 31) { - q = 31; - } else if (q < -32) { - q = -32; - } + const int q = rocmfpx_round_clamp(x * inv_scale, -32.0f, 31.0f); return q == 0 ? 0 : (uint8_t) (q < 0 ? (32u | ((uint8_t) -q & 31u)) : (uint8_t) q); } @@ -714,14 +718,7 @@ static uint8_t rocmfpx_quantize_fp6_code(float x, float inv_scale) { // current main's asymmetric signed range [-32, 31], including the encoded -32 // endpoint, rather than the older experimental branch's [-31, 31] behavior. static inline float rocmfpx_fp6_decoded_value(float x, float inv_scale) { - int q = (int) lroundf(x * inv_scale); - if (q > 31) { - q = 31; - } else if (q < -32) { - q = -32; - } - - return (float) q; + return (float) rocmfpx_round_clamp(x * inv_scale, -32.0f, 31.0f); } static float rocmfpx_fp6_block_mse_for_scale(const float * x, int n, uint8_t e, float best_err) { @@ -890,14 +887,7 @@ static int8_t rocmfpx_quantize_fp8_code(float x, float inv_scale) { return 0; } - int q = (int) lroundf(x * inv_scale); - if (q > 127) { - q = 127; - } else if (q < -127) { - q = -127; - } - - return (int8_t) q; + return (int8_t) rocmfpx_round_clamp(x * inv_scale, -127.0f, 127.0f); } static float rocmfpx_fp8_block_weighted_mse_for_scale(const float * x, int n, const float * mse_weights, uint8_t e, float best_err) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp b/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp index bed84cef00bc..a0707c145958 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/copy_to_quant.comp @@ -629,7 +629,9 @@ uint rocmfpx_fp6_quantize_code(float x, float inv_scale) { } // asymmetric signed range [-32, 31]; -32 is encoded as sign|0 - const int q = clamp(int(round(x * inv_scale)), -32, 31); + // Clamp in float before the int conversion: int() of an out-of-range float is + // undefined in GLSL, so an extreme finite weight could flip sign or vanish. + const int q = int(clamp(round(x * inv_scale), -32.0, 31.0)); if (q == 0) { return 0u; } @@ -746,8 +748,7 @@ int rocmfpx_fp8_quantize_code(float x, float inv_scale) { return 0; } - int q = int(round(x * inv_scale)); - return clamp(q, -127, 127); + return int(clamp(round(x * inv_scale), -127.0, 127.0)); } void quantize(uint dst_idx, uint src_idx) From b1f86ee1bd8cb702576f432b8a6fcd563ad01a62 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Mon, 13 Jul 2026 11:09:53 +0000 Subject: [PATCH 086/212] experiment: dequant-once FA scratch for all KV quant types (q4_0/q4_1/q5_0/q5_1/iq4_nl) Evidence branch only - NOT for upstream. Extends the q8_0 dequant-once FA path to every KV-eligible quant type via per-type fused dequant+transpose shaders, plus a prefill-only fa_kv_ok gate for iq4_nl (no native coopmat1 path) and a GGML_VK_FA_DEQUANT env toggle. Correctness: dequant-once == native FA bit-exact for q4_0/q4_1/q5_0/q5_1; iq4_nl matches CPU. Finding: prefill is quant-type independent (all dequant to identical f16 scratch); iq4_nl is a poor KV type (ppl ~2x q4_0 at equal bits). Retained as gating evidence. Assisted-by: Claude Opus 4.8 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 9 +++++++- .../vulkan-shaders/dequant_iq4_nl.comp | 21 +++++++++++++++---- .../vulkan-shaders/dequant_q4_0.comp | 12 +++++++++++ .../vulkan-shaders/dequant_q4_1.comp | 11 ++++++++++ .../vulkan-shaders/dequant_q5_0.comp | 11 ++++++++++ .../vulkan-shaders/dequant_q5_1.comp | 11 ++++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 2 +- 7 files changed, 71 insertions(+), 6 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 8147894b0e47..0b24dff2d881 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -5618,6 +5618,10 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q4_1], "dequant_q4_1", dequant_q4_1_len, dequant_q4_1_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q5_0], "dequant_q5_0", dequant_q5_0_len, dequant_q5_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q5_1], "dequant_q5_1", dequant_q5_1_len, dequant_q5_1_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_Q4_0], "dequant_q4_0_transpose", dequant_q4_0_transpose_len, dequant_q4_0_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_Q4_1], "dequant_q4_1_transpose", dequant_q4_1_transpose_len, dequant_q4_1_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_Q5_0], "dequant_q5_0_transpose", dequant_q5_0_transpose_len, dequant_q5_0_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_Q5_1], "dequant_q5_1_transpose", dequant_q5_1_transpose_len, dequant_q5_1_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q8_0], "dequant_q8_0", dequant_q8_0_len, dequant_q8_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_Q8_0], "dequant_q8_0_transpose", dequant_q8_0_transpose_len, dequant_q8_0_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q2_K], "dequant_q2_k", dequant_q2_k_len, dequant_q2_k_data, "main", 2, 5 * sizeof(uint32_t), {256 * 64, 1, 1}, {}, 1); @@ -5640,6 +5644,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q3_0_ROCMFPX], "dequant_rocmfpx_fp3", dequant_rocmfpx_fp3_len, dequant_rocmfpx_fp3_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q6_0_ROCMFPX], "dequant_rocmfpx_fp6", dequant_rocmfpx_fp6_len, dequant_rocmfpx_fp6_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q8_0_ROCMFPX], "dequant_rocmfpx_fp8", dequant_rocmfpx_fp8_len, dequant_rocmfpx_fp8_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_IQ4_NL], "dequant_iq4_nl_transpose", dequant_iq4_nl_transpose_len, dequant_iq4_nl_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_MXFP4], "dequant_mxfp4", dequant_mxfp4_len, dequant_mxfp4_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_NVFP4], "dequant_nvfp4", dequant_nvfp4_len, dequant_nvfp4_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); @@ -11262,7 +11267,9 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx }; const bool k_quant = k->type != GGML_TYPE_F16 && k->type != GGML_TYPE_BF16 && k->type != GGML_TYPE_F32; const bool v_quant = v->type != GGML_TYPE_F16 && v->type != GGML_TYPE_BF16 && v->type != GGML_TYPE_F32; - const bool use_dequant_kv = k_quant && v_quant && neq1 >= 64 && + static const char * fa_dequant_env = getenv("GGML_VK_FA_DEQUANT"); + const bool fa_dequant_off = fa_dequant_env && fa_dequant_env[0] == '0'; + const bool use_dequant_kv = !fa_dequant_off && k_quant && v_quant && neq1 >= 64 && is_dense_kv_cache(k) && is_dense_kv_cache(v) && (uint64_t)ggml_nelements(k) * sizeof(ggml_fp16_t) <= ctx->device->properties.limits.maxStorageBufferRange && (uint64_t)ggml_nelements(v) * sizeof(ggml_fp16_t) <= ctx->device->properties.limits.maxStorageBufferRange && diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_iq4_nl.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_iq4_nl.comp index 8f7833eab2e7..befcdc1c5100 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_iq4_nl.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_iq4_nl.comp @@ -10,8 +10,6 @@ layout (binding = 1) writeonly buffer D {D_TYPE data_b[];}; void main() { const uint i = gl_WorkGroupID.x * 4 + gl_LocalInvocationID.x / 64; - init_iq_shmem(gl_WorkGroupSize); - const uint tid = gl_LocalInvocationID.x % 64; const uint il = tid/32; const uint ir = tid%32; @@ -21,12 +19,27 @@ void main() { } const uint q_idx = 8*il; + +#ifdef DEQUANT_TRANSPOSE + // Fused dequant+transpose for FA quant-KV: source physical is [HS, NH, KV, NS] + // (p.M=HS, p.K=NH, p.stride_a=KV); write to per-head-contiguous dest [HS, KV, NH, NS] so the + // f16 FA reads KV coalesced. An iq4_nl block = 32 consecutive HS elements at fixed (head,kv) -> + // 32 contiguous dest positions (intra-block nibble offsets unchanged). + const uint HS = p.M, NH = p.K, KVn = p.stride_a; + const uint e0 = ib * 32; + const uint b_idx = (e0 % HS) + + ((e0 / (HS * NH)) % KVn) * HS + + ((e0 / HS) % NH) * (HS * KVn) + + (e0 / (HS * NH * KVn)) * (HS * KVn * NH) + + q_idx; +#else const uint b_idx = 1024*i + 32*ir + q_idx; +#endif const float d = float(data_a[ib].d); [[unroll]] for (uint l = 0; l < 8; ++l) { - data_b[b_idx + l + 0] = D_TYPE(d * kvalues_iq4nl[data_a[ib].qs[q_idx + l] & 0xF]); - data_b[b_idx + l + 16] = D_TYPE(d * kvalues_iq4nl[data_a[ib].qs[q_idx + l] >> 4]); + data_b[b_idx + l + 0] = D_TYPE(d * float(kvalues_iq4nl_const[data_a[ib].qs[q_idx + l] & 0xF])); + data_b[b_idx + l + 16] = D_TYPE(d * float(kvalues_iq4nl_const[data_a[ib].qs[q_idx + l] >> 4])); } } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q4_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q4_0.comp index b92b292135b4..51a6c89a60c8 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q4_0.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q4_0.comp @@ -19,7 +19,19 @@ void main() { } const uint q_idx = 8*il; + +#ifdef DEQUANT_TRANSPOSE + // fused dequant+transpose for FA quant-KV: per-head-contiguous f16 scratch (see dequant_q8_0.comp) + const uint HS = p.M, NH = p.K, KVn = p.stride_a; + const uint e0 = ib * 32; + const uint b_idx = (e0 % HS) + + ((e0 / (HS * NH)) % KVn) * HS + + ((e0 / HS) % NH) * (HS * KVn) + + (e0 / (HS * NH * KVn)) * (HS * KVn * NH) + + q_idx; +#else const uint b_idx = 1024*i + 32*ir + q_idx; +#endif const float d = float(data_a[ib].d); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q4_1.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q4_1.comp index 6b63cbe5833b..76f2d958cbab 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q4_1.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q4_1.comp @@ -18,7 +18,18 @@ void main() { return; } +#ifdef DEQUANT_TRANSPOSE + // fused dequant+transpose for FA quant-KV: per-head-contiguous f16 scratch (see dequant_q8_0.comp) + const uint HS = p.M, NH = p.K, KVn = p.stride_a; + const uint e0 = ib * 32; + const uint b_idx = (e0 % HS) + + ((e0 / (HS * NH)) % KVn) * HS + + ((e0 / HS) % NH) * (HS * KVn) + + (e0 / (HS * NH * KVn)) * (HS * KVn * NH) + + 8*il; +#else const uint b_idx = 1024*i + 32*ir + 8*il; +#endif const float d = float(data_a[ib].d); const float m = float(data_a[ib].m); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q5_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q5_0.comp index f1b0bac87271..1402fa4292d8 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q5_0.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q5_0.comp @@ -18,7 +18,18 @@ void main() { return; } +#ifdef DEQUANT_TRANSPOSE + // fused dequant+transpose for FA quant-KV: per-head-contiguous f16 scratch (see dequant_q8_0.comp) + const uint HS = p.M, NH = p.K, KVn = p.stride_a; + const uint e0 = ib * 32; + const uint b_idx = (e0 % HS) + + ((e0 / (HS * NH)) % KVn) * HS + + ((e0 / HS) % NH) * (HS * KVn) + + (e0 / (HS * NH * KVn)) * (HS * KVn * NH) + + 8*il; +#else const uint b_idx = 1024*i + 32*ir + 8*il; +#endif const float d = float(data_a[ib].d); const uint qh = uint(data_a[ib].qh[1]) << 16 | data_a[ib].qh[0]; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q5_1.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q5_1.comp index c495b31f1754..1fd6e2552af7 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q5_1.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_q5_1.comp @@ -18,7 +18,18 @@ void main() { return; } +#ifdef DEQUANT_TRANSPOSE + // fused dequant+transpose for FA quant-KV: per-head-contiguous f16 scratch (see dequant_q8_0.comp) + const uint HS = p.M, NH = p.K, KVn = p.stride_a; + const uint e0 = ib * 32; + const uint b_idx = (e0 % HS) + + ((e0 / (HS * NH)) % KVn) * HS + + ((e0 / HS) % NH) * (HS * KVn) + + (e0 / (HS * NH * KVn)) * (HS * KVn * NH) + + 8*il; +#else const uint b_idx = 1024*i + 32*ir + 8*il; +#endif const float d = float(data_a[ib].d); const float m = float(data_a[ib].m); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 1a57fdc24e8c..8baf0f2ef4bc 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -791,7 +791,7 @@ void process_shaders() { string_to_spv("dequant_" + tname, "dequant_" + tname + ".comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float16_t"}})); } // Fused dequant+transpose variant for FA quant-KV (per-head-contiguous f16 scratch). - if (tname == "q8_0") { + if (tname == "q8_0" || tname == "iq4_nl" || tname == "q4_0" || tname == "q4_1" || tname == "q5_0" || tname == "q5_1") { string_to_spv("dequant_" + tname + "_transpose", "dequant_" + tname + ".comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float16_t"}, {"DEQUANT_TRANSPOSE", "1"}})); } From 6dc9116a0528831bb5d24771cd823cb19bc4679f Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 14 Jul 2026 14:09:02 +0000 Subject: [PATCH 087/212] vulkan : gate FA dequant-once scratch on device-local capacity The dequant-once path materializes a per-layer f16 K/V scratch (~2 KiB/token). On discrete devices that resident footprint can push the working set past free VRAM, at which point the driver silently pages device-local memory: measured ~15x prefill regression on an 8 GB card (RTX 3070, driver 591.86) at long context, with no error reported. Integrated/UMA devices have no separate device pool to overflow and are unaffected. Gate the path on this process's device-local usage against the physical heap size, keeping a conservative reserve for memory not observable in-process. heapBudget is deliberately not used as the signal: ggml_backend_vk_get_device_memory computes heapBudget - heapUsage in unsigned arithmetic, which wraps to a huge value exactly when the device is oversubscribed. The allocation cannot gate itself either - on WDDM vkAllocateMemory only fails at roughly physical heap size, which is above the free-VRAM level where paging begins, so a successful allocation is not evidence of a resident fit. Also fix the scratch size check: K and V are bound as one storage buffer, so their sum must fit maxStorageBufferRange, not each half independently. GGML_VK_FA_DEQUANT=0 forces the path off and =1 skips the capacity check; GGML_VK_FA_DEQUANT_RESERVE_MB overrides the reserve. Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 87 ++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 0b24dff2d881..096aefa97307 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -2416,6 +2416,10 @@ struct ggml_backend_vk_context { ggml_vk_garbage_collector gc; size_t prealloc_size_x, prealloc_size_y, prealloc_size_split_k, prealloc_size_add_rms_partials, prealloc_size_add_rms_partials_offset; vk_buffer prealloc_x, prealloc_y, prealloc_split_k, prealloc_add_rms_partials, sync_staging; + // memoized capacity decision for the FA dequant-once scratch, see ggml_vk_fa_dequant_scratch_fits + uint64_t fa_dequant_gate_sz; + bool fa_dequant_gate_fits; + bool fa_dequant_gate_logged; vk::Fence fence, almost_ready_fence; bool submit_pending {}; bool almost_ready_fence_pending {}; @@ -7897,6 +7901,9 @@ static void ggml_vk_init(ggml_backend_vk_context * ctx, size_t idx) { ctx->prealloc_size_x = 0; ctx->prealloc_size_y = 0; ctx->prealloc_size_split_k = 0; + ctx->fa_dequant_gate_sz = 0; + ctx->fa_dequant_gate_fits = false; + ctx->fa_dequant_gate_logged = false; // Fixed size of 1KB, for deterministic behavior ctx->prealloc_size_add_rms_partials = 1024; @@ -11199,6 +11206,69 @@ static bool ggml_vk_flash_attn_coopmat_shmem_support(const vk_device& device, co return supported; } +// Capacity gate for the dequant-once f16 K/V scratch. On discrete devices the scratch can push the +// working set past free VRAM, and the driver then silently pages device-local memory (~15x prefill +// regression measured on an 8 GB card at long context). UMA has no separate pool to overflow. +// +// Gates our own device-local usage against the physical heap size, less a reserve for memory not +// visible in-process. heapBudget is deliberately not the signal: ggml_backend_vk_get_device_memory +// computes heapBudget - heapUsage unsigned, which wraps when the device is oversubscribed. Nor can +// the allocation gate itself: on WDDM vkAllocateMemory only fails near physical heap size, above +// the free-VRAM level where paging starts. The reserve is necessarily conservative because other +// processes' VRAM use is invisible to us. GGML_VK_FA_DEQUANT=0/1 forces the path off/on; +// GGML_VK_FA_DEQUANT_RESERVE_MB overrides the reserve. +static bool ggml_vk_fa_dequant_scratch_fits(ggml_backend_vk_context * ctx, uint64_t scratch_sz) { + const vk_device& device = ctx->device; + + if (device->uma) { + return true; + } + + // Decided once per scratch size, so the decision cannot flip between layers as usage grows. + if (ctx->fa_dequant_gate_sz == scratch_sz) { + return ctx->fa_dequant_gate_fits; + } + + static const uint64_t reserve = [] { + const char * env = getenv("GGML_VK_FA_DEQUANT_RESERVE_MB"); + return (uint64_t)(env ? atoi(env) : 1024) * 1024 * 1024; + }(); + + bool fits = false; + + // Without VK_EXT_memory_budget our usage is unknowable, so leave the path disabled. + if (vk_instance.device_supports_membudget[device->idx]) { + vk::PhysicalDeviceMemoryBudgetPropertiesEXT budgetprops; + vk::PhysicalDeviceMemoryProperties2 memprops = {}; + memprops.pNext = &budgetprops; + device->physical_device.getMemoryProperties2(&memprops); + + uint64_t heap_size = 0; + uint64_t heap_used = 0; + for (uint32_t i = 0; i < memprops.memoryProperties.memoryHeapCount; ++i) { + const vk::MemoryHeap & heap = memprops.memoryProperties.memoryHeaps[i]; + if (heap.flags & vk::MemoryHeapFlagBits::eDeviceLocal) { + heap_size += heap.size; + heap_used += budgetprops.heapUsage[i]; + } + } + // heap_used already covers scratch allocated on a previous ubatch, so counting scratch_sz + // in full is conservative by up to the current scratch size. + fits = heap_size > reserve && heap_used + scratch_sz + reserve <= heap_size; + } + + if (!fits && !ctx->fa_dequant_gate_logged) { + ctx->fa_dequant_gate_logged = true; + GGML_LOG_INFO("ggml_vulkan: flash attention dequant-once disabled: %llu MiB K/V scratch does not fit " + "device-local memory with a %llu MiB reserve. Set GGML_VK_FA_DEQUANT=1 to force it on.\n", + (unsigned long long)(scratch_sz >> 20), (unsigned long long)(reserve >> 20)); + } + + ctx->fa_dequant_gate_sz = scratch_sz; + ctx->fa_dequant_gate_fits = fits; + return fits; +} + static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * q, const ggml_tensor * k, const ggml_tensor * v, const ggml_tensor * mask, const ggml_tensor * sinks, ggml_tensor * dst) { VK_LOG_DEBUG("ggml_vk_flash_attn((" << q << ", name=" << q->name << ", type=" << q->type << ", ne0=" << q->ne[0] << ", ne1=" << q->ne[1] << ", ne2=" << q->ne[2] << ", ne3=" << q->ne[3] << ", nb0=" << q->nb[0] << ", nb1=" << q->nb[1] << ", nb2=" << q->nb[2] << ", nb3=" << q->nb[3]; std::cerr << "), (" << k << ", name=" << k->name << ", type=" << k->type << ", ne0=" << k->ne[0] << ", ne1=" << k->ne[1] << ", ne2=" << k->ne[2] << ", ne3=" << k->ne[3] << ", nb0=" << k->nb[0] << ", nb1=" << k->nb[1] << ", nb2=" << k->nb[2] << ", nb3=" << k->nb[3]; @@ -11258,7 +11328,14 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx const bool f32acc = !ctx->device->fp16 || dst->op_params[3] == GGML_PREC_F32 || k->type == GGML_TYPE_BF16; - // dequant K/V once into an f16 scratch, reordered KV layout so FA can read without a stride + // For prefill with quantized K/V, dequantize+transpose K/V once into a per-head-contiguous + // f16 scratch and run the f16 FA path, instead of the coopmat1 shader re-dequantizing the + // whole KV inside every Q-workgroup. The KV-cache view reaching FA is [0,2,1,3]-permuted but + // dense, so we require dense allocation (not ggml_is_contiguous) and block-contiguous dim0, + // and only engage where a fused dequant-transpose shader exists. Prefill only + // (n_rows >= 64); measured neutral at shallow depth and up to ~2x at long context. + // The scratch is bound as a single storage buffer holding K and V back to back, so the SUM of + // the two must fit maxStorageBufferRange, not each half independently. auto is_dense_kv_cache = [](const ggml_tensor * t) { return t->nb[0] == ggml_type_size(t->type) && t->nb[2] == ggml_row_size(t->type, t->ne[0]) && @@ -11267,19 +11344,21 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx }; const bool k_quant = k->type != GGML_TYPE_F16 && k->type != GGML_TYPE_BF16 && k->type != GGML_TYPE_F32; const bool v_quant = v->type != GGML_TYPE_F16 && v->type != GGML_TYPE_BF16 && v->type != GGML_TYPE_F32; + const uint64_t kv_f16_sz = ((uint64_t)ggml_nelements(k) + (uint64_t)ggml_nelements(v)) * sizeof(ggml_fp16_t); static const char * fa_dequant_env = getenv("GGML_VK_FA_DEQUANT"); const bool fa_dequant_off = fa_dequant_env && fa_dequant_env[0] == '0'; + const bool fa_dequant_on = fa_dequant_env && fa_dequant_env[0] == '1'; const bool use_dequant_kv = !fa_dequant_off && k_quant && v_quant && neq1 >= 64 && is_dense_kv_cache(k) && is_dense_kv_cache(v) && - (uint64_t)ggml_nelements(k) * sizeof(ggml_fp16_t) <= ctx->device->properties.limits.maxStorageBufferRange && - (uint64_t)ggml_nelements(v) * sizeof(ggml_fp16_t) <= ctx->device->properties.limits.maxStorageBufferRange && + kv_f16_sz <= ctx->device->properties.limits.maxStorageBufferRange && ctx->device->pipeline_dequant_transpose[k->type] != nullptr && ctx->device->pipeline_dequant_transpose[v->type] != nullptr && // coopmat2 path does not benefit from the f16 scratch !ctx->device->coopmat2 && // Intel Xe1 regresses, see PR 25494 (ctx->device->vendor_id != VK_VENDOR_ID_INTEL || - (ctx->device->coopmat_support && ctx->device->architecture != vk_device_architecture::INTEL_XE1)); + (ctx->device->coopmat_support && ctx->device->architecture != vk_device_architecture::INTEL_XE1)) && + (fa_dequant_on || ggml_vk_fa_dequant_scratch_fits(ctx, kv_f16_sz)); const ggml_type k_type_eff = use_dequant_kv ? GGML_TYPE_F16 : k->type; const ggml_type v_type_eff = use_dequant_kv ? GGML_TYPE_F16 : v->type; From e52cc7cee0f318fb13e1eb3006faa5fc7d9bf5ca Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 28 Jul 2026 10:21:44 +0000 Subject: [PATCH 088/212] vulkan: contiguize strided f16 KV for FA prefill (GGML_VK_FA_KV_CONTIG, env-gated) The KV-cache view reaching FA has head-interleaved rows ([HS, NH, KV] physically), and the cm1 shader's direct-from-global coopMatLoads run ~2x slower on that stride than on per-head-contiguous K/V: a 16x16 tile touches 16 distant cache lines instead of 4. Measured on Strix Halo (RADV gfx1151), hd128/GQA8/kv10240/nb2048 f16: 29.8ms contiguous vs 63.1ms dense-permuted (the model layout; matches the in-model 59.9ms from the perf logger, where FLASH_ATTN_EXT was 72.6% of the graph at pp2048@d8192). GGML_VK_FA_KV_CONTIG=1 extends the dequant-once FA scratch to f16 K/V: dequant_f16_transpose.comp is a pure strided copy ([HS,NH,KV] -> [HS,KV,NH], same push-constant ABI and dispatch as the quant transpose shaders), engaged only when the rows are actually strided, prefill only (neq1 >= 64). FA op 63.1 -> 30.5ms (2.07x) incl. copy cost. Model-level (Qwen3-Coder-30B Q6_K_XL, ub2048, f16 KV, r=3, vs ROCm 571d0d5 nowmma): pp8192 877 -> 1199 t/s (ROCm 1216, was -28% now parity); pp2048@d4096/8192/16384: 776/513/300 -> 1120/850/580. Shallow prefill unchanged-to-better (pp2048 1542 -> 1633). Not the fix: shmem staging on AMD (loses on occupancy, 29.8 -> 54.4ms contiguous), bigger-tile/GQA-packed streaming (1-KV-head L2-resident probe runs identical -> kernel is issue-bound, not bandwidth-bound). test-backend-ops -o FLASH_ATTN_EXT green with the flag off and on (pre-existing iq4_nl+sinks failures unchanged). Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 14 +++++++- .../vulkan-shaders/dequant_f16_transpose.comp | 32 +++++++++++++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 4 +++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dequant_f16_transpose.comp diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 096aefa97307..5f353e5630da 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -5628,6 +5628,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_Q5_1], "dequant_q5_1_transpose", dequant_q5_1_transpose_len, dequant_q5_1_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q8_0], "dequant_q8_0", dequant_q8_0_len, dequant_q8_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_Q8_0], "dequant_q8_0_transpose", dequant_q8_0_transpose_len, dequant_q8_0_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 16, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dequant_transpose[GGML_TYPE_F16], "dequant_f16_transpose", dequant_f16_transpose_len, dequant_f16_transpose_data, "main", 2, 5 * sizeof(uint32_t), {256 * 8, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q2_K], "dequant_q2_k", dequant_q2_k_len, dequant_q2_k_data, "main", 2, 5 * sizeof(uint32_t), {256 * 64, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_TQ2_0], "dequant_tq2_0", dequant_tq2_0_len, dequant_tq2_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 64, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q3_K], "dequant_q3_k", dequant_q3_k_len, dequant_q3_k_data, "main", 2, 5 * sizeof(uint32_t), {256 * 64, 1, 1}, {}, 1); @@ -11348,7 +11349,18 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx static const char * fa_dequant_env = getenv("GGML_VK_FA_DEQUANT"); const bool fa_dequant_off = fa_dequant_env && fa_dequant_env[0] == '0'; const bool fa_dequant_on = fa_dequant_env && fa_dequant_env[0] == '1'; - const bool use_dequant_kv = !fa_dequant_off && k_quant && v_quant && neq1 >= 64 && + // EXPERIMENT (GGML_VK_FA_KV_CONTIG=1): run the same contiguize pass for f16 K/V. The + // KV-cache view reaching FA is head-interleaved ([HS, NH, KV] physically), and the cm1 + // direct-from-global coopMatLoads run ~2-5x slower on those strided rows than on + // per-head-contiguous K/V. Copy K/V once into the scratch instead (dequant_f16_transpose + // is a pure strided copy). Only engages when the rows are actually strided. + static const char * fa_kv_contig_env = getenv("GGML_VK_FA_KV_CONTIG"); + const bool fa_kv_contig = fa_kv_contig_env && fa_kv_contig_env[0] == '1'; + const bool kv_f16_strided = k->type == GGML_TYPE_F16 && v->type == GGML_TYPE_F16 && + (k->nb[1] != (uint64_t)HSK * sizeof(ggml_fp16_t) || + v->nb[1] != (uint64_t)HSV * sizeof(ggml_fp16_t)) && + (HSK % 8) == 0 && (HSV % 8) == 0; + const bool use_dequant_kv = !fa_dequant_off && ((k_quant && v_quant) || (fa_kv_contig && kv_f16_strided)) && neq1 >= 64 && is_dense_kv_cache(k) && is_dense_kv_cache(v) && kv_f16_sz <= ctx->device->properties.limits.maxStorageBufferRange && ctx->device->pipeline_dequant_transpose[k->type] != nullptr && diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_f16_transpose.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_f16_transpose.comp new file mode 100644 index 000000000000..dda53d749acc --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_f16_transpose.comp @@ -0,0 +1,32 @@ +#version 450 + +#include "dequant_head.glsl" + +#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout (binding = 0) readonly buffer A {f16vec4 data_a[];}; +layout (binding = 1) writeonly buffer D {f16vec4 data_b[];}; + +// Strided-copy counterpart of the fused dequant+transpose shaders for FA f16 KV: +// source physical is [HS, NH, KV, NS] (p.M=HS, p.K=NH, p.stride_a=KV); write to +// per-head-contiguous dest [HS, KV, NH, NS] so the f16 FA reads KV coalesced. +// HS stays innermost in both layouts, so each invocation moves 8 HS-contiguous +// elements (two f16vec4) requiring HS % 8 == 0 (enforced by the host gate). +void main() { + const uint i = gl_GlobalInvocationID.x; + const uint e0 = i * 8; + if (e0 >= p.nel) { + return; + } + + const uint HS = p.M, NH = p.K, KVn = p.stride_a; + const uint dst = (e0 % HS) + + ((e0 / (HS * NH)) % KVn) * HS + + ((e0 / HS) % NH) * (HS * KVn) + + (e0 / (HS * NH * KVn)) * (HS * KVn * NH); + + data_b[dst / 4 ] = data_a[e0 / 4 ]; + data_b[dst / 4 + 1] = data_a[e0 / 4 + 1]; +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 8baf0f2ef4bc..81e556b0c78e 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -794,6 +794,10 @@ void process_shaders() { if (tname == "q8_0" || tname == "iq4_nl" || tname == "q4_0" || tname == "q4_1" || tname == "q5_0" || tname == "q5_1") { string_to_spv("dequant_" + tname + "_transpose", "dequant_" + tname + ".comp", merge_maps(base_dict, {{data_a_key, "1"}, {"D_TYPE", "float16_t"}, {"DEQUANT_TRANSPOSE", "1"}})); } + // Strided-copy counterpart for f16 KV (contiguize the head-interleaved cache layout). + if (tname == "f16") { + string_to_spv("dequant_f16_transpose", "dequant_f16_transpose.comp", {}); + } shader = (tname == "f32" || tname == "f16" || tname == "bf16") ? "get_rows.comp" : "get_rows_quant.comp"; From d61a7fd6c4b9c9e4abe0041bba7d33039ef8bd68 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 28 Jul 2026 10:21:56 +0000 Subject: [PATCH 089/212] tests: dense-permuted K/V option + Strix FA prefill perf/probe cases test_flash_attn_ext always built K/V as sparse views (physical dim1 doubled, half viewed), which can never satisfy the contiguize path's ggml_is_contiguously_allocated gate - so no permuted correctness case exercised it. Add a kv_view parameter (default true = unchanged) and dense-permuted eval cases matching the real KV-cache layout, including ALiBi and logit-softcap variants; all pass vs CPU with GGML_VK_FA_KV_CONTIG=1. Perf additions: Qwen3-Coder-30B prefill-at-depth shapes (hd128, 4 KV heads, GQA 8, kv up to 10240, nb 512/2048), the dense-permuted variant (model layout), and the probe set used to establish that the contiguous cm1 kernel is issue-bound: 32-distinct-KV-head MALL-spill (flat), 1-KV-head L2-resident (flat), mask=0 (-5.5%), f16 acc (-3%). Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- tests/test-backend-ops.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 3919e555273f..606ecc53b956 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -7915,7 +7915,7 @@ struct test_flash_attn_ext : public test_case { const ggml_type type_K; const ggml_type type_V; std::array permute; - const bool kv_view; // create K/V as views of a larger buffer (like a KV cache) + const bool kv_view; // create K/V as views of a larger buffer (like a KV cache); false = dense permuted like the model KV cache const bool v_is_view_of_k; const int64_t n_kv_max; @@ -10986,6 +10986,12 @@ static std::vector> make_test_cases_eval() { } } + // dense-permuted K/V (model KV-cache layout, engages the f16 contiguize path at nb>=64) + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 1024, 128, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 2, 1, 3}, false)); + test_cases.emplace_back(new test_flash_attn_ext(96, 96, 8, {4, 1}, 512, 80, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 2, 1, 3}, false)); + test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {8, 1}, 512, 75, true, false, 8.0f, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 2, 1, 3}, false)); + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 512, 96, true, false, 0, 30.0f, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 2, 1, 3}, false)); + // mixed quant and Q1_0 test cases test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {1, 1}, 128, 2, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q4_0)); test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {1, 1}, 128, 2, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q4_0, GGML_TYPE_F16)); @@ -11534,6 +11540,23 @@ static std::vector> make_test_cases_perf() { // Qwen3-VL-8B https://github.com/ggml-org/llama.cpp/issues/17012 test_cases.emplace_back(new test_flash_attn_ext(72, 72, 16, {1, 1}, 5776, 5776, false, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + // Qwen3-Coder-30B-A3B prefill at depth: hd128, 4 KV heads, GQA 8, ub 2048 + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 2048, 2048, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 6144, 2048, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 10240, 2048, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 10240, 512, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + // MALL-spill probe: same FLOPs, 32 distinct KV heads (no GQA) -> K/V footprint 8x (168MB > 32MB MALL) + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 32, {1, 1}, 10240, 2048, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + // KV-cache layout probe: same shape, K/V strided token-major like the real cache (heads interleaved) + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 10240, 2048, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 2, 1, 3})); + // Same, dense-permuted (exact model KV-cache layout; eligible for the f16 contiguize path) + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 10240, 2048, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 2, 1, 3}, false)); + // L2-residence probe: 1 KV head x GQA 32 (K/V 5.2MB fits L2) - distinguishes cache-BW-bound from issue-bound + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 1, {32, 1}, 10240, 2048, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + // cost-partition probes: no mask; f16 accumulate + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 10240, 2048, false, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 10240, 2048, true, false, 0, 0, GGML_PREC_DEFAULT, GGML_TYPE_F16, GGML_TYPE_F16)); + test_cases.emplace_back(new test_flash_attn_ext(64, 64, 8, {8, 1}, 7680, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); test_cases.emplace_back(new test_flash_attn_ext(64, 64, 8, {8, 1}, 7680, 4, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); test_cases.emplace_back(new test_flash_attn_ext(64, 64, 8, {8, 1}, 7680, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q4_0, GGML_TYPE_Q4_0)); From 6d056bf9db9854e6f1c31907f00c412d1ccbfd87 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 28 Jul 2026 13:25:49 +0000 Subject: [PATCH 090/212] vulkan: enable the f16 KV contiguize pass by default (GGML_VK_FA_KV_CONTIG=0 opts out) Flip 442d7df from opt-in to default-on, matching the quant dequant-once path (GGML_VK_FA_DEQUANT) convention. The pass still self-gates: f16 K/V only, prefill only (neq1 >= 64), only when rows are actually strided, dense allocation, and the shared scratch-capacity check. Validated on Strix Halo (RADV gfx1151): FLASH_ATTN_EXT suite green with default env (dense-permuted cases exercise the pass) and with the opt-out; model-level pp2048@d8192 with no FA env matches the explicit GGML_VK_FA_KV_CONTIG=1 validation run (846.6 vs 847.9 t/s). Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 5f353e5630da..6d24357bfb42 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11349,13 +11349,14 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx static const char * fa_dequant_env = getenv("GGML_VK_FA_DEQUANT"); const bool fa_dequant_off = fa_dequant_env && fa_dequant_env[0] == '0'; const bool fa_dequant_on = fa_dequant_env && fa_dequant_env[0] == '1'; - // EXPERIMENT (GGML_VK_FA_KV_CONTIG=1): run the same contiguize pass for f16 K/V. The - // KV-cache view reaching FA is head-interleaved ([HS, NH, KV] physically), and the cm1 + // Contiguize pass for f16 K/V (GGML_VK_FA_KV_CONTIG=0 opts out). The KV-cache view + // reaching FA is head-interleaved ([HS, NH, KV] physically), and the cm1 // direct-from-global coopMatLoads run ~2-5x slower on those strided rows than on // per-head-contiguous K/V. Copy K/V once into the scratch instead (dequant_f16_transpose - // is a pure strided copy). Only engages when the rows are actually strided. + // is a pure strided copy). Only engages when the rows are actually strided, and shares + // the quant path's prefill/allocation/scratch-capacity gates below. static const char * fa_kv_contig_env = getenv("GGML_VK_FA_KV_CONTIG"); - const bool fa_kv_contig = fa_kv_contig_env && fa_kv_contig_env[0] == '1'; + const bool fa_kv_contig = !(fa_kv_contig_env && fa_kv_contig_env[0] == '0'); const bool kv_f16_strided = k->type == GGML_TYPE_F16 && v->type == GGML_TYPE_F16 && (k->nb[1] != (uint64_t)HSK * sizeof(ggml_fp16_t) || v->nb[1] != (uint64_t)HSV * sizeof(ggml_fp16_t)) && From 82c3fe972128ff3fc04225065957359eea9c5ec3 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 28 Jul 2026 23:40:27 +0000 Subject: [PATCH 091/212] vulkan: single source of truth for native FA K/V types + non-native hard-gate Rebased adaptation of the iq4_nl routing fix: upstream 8161641 made iq4_nl a native FA type, so the original motivation (iq4_nl had no native shader and silently read garbage outside the dequant-once path) no longer applies to any currently-admitted type. Keep the machinery as hardening: ggml_vk_fa_kv_native() is the one list, supports_op mirrors every hard condition of the dispatch-time dequant gate for any future non-native type, and dispatch asserts the invariant instead of falling back to a garbage-reading shader. Native list synced with upstream (iq4_nl in, q1_0 out to match current admission). Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 80 +++++++++++++++++++++------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 6d24357bfb42..752d1f3e9d3e 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11207,6 +11207,27 @@ static bool ggml_vk_flash_attn_coopmat_shmem_support(const vk_device& device, co return supported; } +// K/V types the FA shaders can read directly (scalar/coopmat1 select the dequant code via the +// FaTypeK/FaTypeV spec constants; a type outside this list silently reads garbage). Types that +// are FA-supported but not listed here (iq4_nl) are only correct through the dequant-once +// scratch path, so supports_op and the dispatch-time gate must agree on when that path runs. +static bool ggml_vk_fa_kv_native(ggml_type t, bool coopmat2) { + switch (t) { + case GGML_TYPE_F32: + case GGML_TYPE_F16: + case GGML_TYPE_BF16: + case GGML_TYPE_Q4_0: + case GGML_TYPE_Q4_1: + case GGML_TYPE_Q5_0: + case GGML_TYPE_Q5_1: + case GGML_TYPE_Q8_0: + case GGML_TYPE_IQ4_NL: // native FA support since upstream 8161641 + return true; + default: + return false; + } +} + // Capacity gate for the dequant-once f16 K/V scratch. On discrete devices the scratch can push the // working set past free VRAM, and the driver then silently pages device-local memory (~15x prefill // regression measured on an 8 GB card at long context). UMA has no separate pool to overflow. @@ -11361,17 +11382,27 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx (k->nb[1] != (uint64_t)HSK * sizeof(ggml_fp16_t) || v->nb[1] != (uint64_t)HSV * sizeof(ggml_fp16_t)) && (HSK % 8) == 0 && (HSV % 8) == 0; - const bool use_dequant_kv = !fa_dequant_off && ((k_quant && v_quant) || (fa_kv_contig && kv_f16_strided)) && neq1 >= 64 && + // A K/V type the FA shaders cannot read directly (iq4_nl) is only correct through the + // dequant path. supports_op only admits such types when the hard conditions below hold, + // and the VRAM heuristic must not veto them (there is no fallback), so force the path on. + const bool kv_needs_dequant = !ggml_vk_fa_kv_native(k->type, ctx->device->coopmat2) || + !ggml_vk_fa_kv_native(v->type, ctx->device->coopmat2); + const bool use_dequant_kv = !fa_dequant_off && + ((k_quant && v_quant) || kv_needs_dequant || (fa_kv_contig && kv_f16_strided)) && neq1 >= 64 && is_dense_kv_cache(k) && is_dense_kv_cache(v) && kv_f16_sz <= ctx->device->properties.limits.maxStorageBufferRange && ctx->device->pipeline_dequant_transpose[k->type] != nullptr && ctx->device->pipeline_dequant_transpose[v->type] != nullptr && - // coopmat2 path does not benefit from the f16 scratch - !ctx->device->coopmat2 && + // coopmat2 reads its native types directly; non-native still needs the scratch + (kv_needs_dequant || !ctx->device->coopmat2) && // Intel Xe1 regresses, see PR 25494 - (ctx->device->vendor_id != VK_VENDOR_ID_INTEL || + (kv_needs_dequant || + ctx->device->vendor_id != VK_VENDOR_ID_INTEL || (ctx->device->coopmat_support && ctx->device->architecture != vk_device_architecture::INTEL_XE1)) && - (fa_dequant_on || ggml_vk_fa_dequant_scratch_fits(ctx, kv_f16_sz)); + (fa_dequant_on || kv_needs_dequant || ggml_vk_fa_dequant_scratch_fits(ctx, kv_f16_sz)); + // If this fires, supports_op admitted a non-native K/V type the gate then rejected; the + // native shader would return garbage rather than fail, so abort instead. + GGML_ASSERT(use_dequant_kv || !kv_needs_dequant); const ggml_type k_type_eff = use_dequant_kv ? GGML_TYPE_F16 : k->type; const ggml_type v_type_eff = use_dequant_kv ? GGML_TYPE_F16 : v->type; @@ -19120,25 +19151,34 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm if (op->src[3] && op->src[3]->type != GGML_TYPE_F16) { return false; } - auto fa_kv_ok = [](ggml_type t) { - switch (t) { - case GGML_TYPE_F32: - case GGML_TYPE_F16: - case GGML_TYPE_BF16: - case GGML_TYPE_Q8_0: - case GGML_TYPE_Q5_1: - case GGML_TYPE_Q5_0: - case GGML_TYPE_Q4_1: - case GGML_TYPE_Q4_0: - case GGML_TYPE_IQ4_NL: - return true; - default: - return false; - } + auto fa_kv_ok = [&](ggml_type t) { + // ggml_vk_fa_kv_native is the single source of truth; on this base every + // admitted type is native, and the non-native hard-gate below is dormant + // hardening for any future type routed through the dequant-once scratch. + return ggml_vk_fa_kv_native(t, coopmat2); }; if (!fa_kv_ok(op->src[1]->type) || !fa_kv_ok(op->src[2]->type)) { return false; } + if (!ggml_vk_fa_kv_native(op->src[1]->type, coopmat2) || !ggml_vk_fa_kv_native(op->src[2]->type, coopmat2)) { + // Only correct through the dequant-once scratch path; admit only when every + // hard condition of the dispatch-time gate holds, so dispatch can never fall + // back to the native shader (it reads garbage for these types, not an error). + const ggml_tensor * k = op->src[1]; + const ggml_tensor * v = op->src[2]; + static const char * fa_dequant_env = getenv("GGML_VK_FA_DEQUANT"); + const bool fa_dequant_off = fa_dequant_env && fa_dequant_env[0] == '0'; + const uint64_t kv_f16_sz = ((uint64_t)ggml_nelements(k) + (uint64_t)ggml_nelements(v)) * sizeof(ggml_fp16_t); + if (fa_dequant_off || + op->src[0]->ne[1] < 64 || + device->pipeline_dequant_transpose[k->type] == nullptr || + device->pipeline_dequant_transpose[v->type] == nullptr || + k->nb[0] != ggml_type_size(k->type) || v->nb[0] != ggml_type_size(v->type) || + !ggml_is_contiguously_allocated(k) || !ggml_is_contiguously_allocated(v) || + kv_f16_sz > device->properties.limits.maxStorageBufferRange) { + return false; + } + } if ((op->src[1]->type == GGML_TYPE_BF16) != (op->src[2]->type == GGML_TYPE_BF16)) { return false; } From fa117d1d055f20467a87844368b7a62f050ebf0b Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 28 Jul 2026 23:51:32 +0000 Subject: [PATCH 092/212] tests: dense-permuted iq4_nl FA cases (dequant-once route coverage) iq4_nl has no native Vulkan FA shader; the dequant-once scratch path is its only route (1abdd92). The sweep's iq4_nl cases are all sparse-view, which that path correctly rejects, so iq4_nl had zero passing FA coverage. Add model-layout (dense [0,2,1,3]-permuted) cases at prefill batch size: iq4_nl/iq4_nl with sinks off and on, hd72+GQA with sinks, and mixed K=iq4_nl/V=f16. Validated on RADV gfx1151 at 146fb73: FLASH_ATTN_EXT 4765/4765 with default env and with GGML_VK_FA_KV_CONTIG=0/1; 4761/4761 with GGML_VK_FA_DEQUANT=0 (new cases correctly report unsupported); full test-backend-ops suite 15538/15538. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- tests/test-backend-ops.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 606ecc53b956..5b7893d45129 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -10992,6 +10992,13 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {8, 1}, 512, 75, true, false, 8.0f, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 2, 1, 3}, false)); test_cases.emplace_back(new test_flash_attn_ext(128, 128, 4, {8, 1}, 512, 96, true, false, 0, 30.0f, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 2, 1, 3}, false)); + // dense-permuted iq4_nl K/V at prefill batch sizes: iq4_nl has no native FA shader, so these + // exercise the only supported route (the dequant-once path), incl. sinks and mixed-with-f16 + test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {1, 1}, 512, 75, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_IQ4_NL, GGML_TYPE_IQ4_NL, {0, 2, 1, 3}, false)); + test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {1, 1}, 512, 75, true, true, 0, 0, GGML_PREC_F32, GGML_TYPE_IQ4_NL, GGML_TYPE_IQ4_NL, {0, 2, 1, 3}, false)); + test_cases.emplace_back(new test_flash_attn_ext(72, 72, 4, {4, 1}, 113, 75, true, true, 0, 0, GGML_PREC_F32, GGML_TYPE_IQ4_NL, GGML_TYPE_IQ4_NL, {0, 2, 1, 3}, false)); + test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {1, 1}, 512, 75, true, true, 0, 0, GGML_PREC_F32, GGML_TYPE_IQ4_NL, GGML_TYPE_F16, {0, 2, 1, 3}, false)); + // mixed quant and Q1_0 test cases test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {1, 1}, 128, 2, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q4_0)); test_cases.emplace_back(new test_flash_attn_ext(64, 64, 4, {1, 1}, 128, 2, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q4_0, GGML_TYPE_F16)); From 5d5759ac92f12a117987814b12a8533df54860e1 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Fri, 31 Jul 2026 01:25:46 +0000 Subject: [PATCH 093/212] vulkan: bill the FA K/V contiguize pass on its own perf-logger line The contiguize/dequant pass is dispatched inside the FLASH_ATTN_EXT node handler, and the perf logger writes one timestamp per graph node, so its cost was charged to FLASH_ATTN_EXT with no way to separate the two. That made the copy invisible and the kernel look correspondingly slower. Adds a sub-node timestamp: a handler can close an interval mid-node and have it logged under its own name. Measured on Coder-30B UD-Q6_K_XL, pp2048/ub2048 at d32768, f16 KV: graph total 4612.3 ms FLASH_ATTN_EXT 3490.6 ms 75.68% of graph FA_KV_CONTIGUIZE 33.1 ms 0.72% of graph (0.95% of FA) so the copy is under 1% of the graph and FA is 75.7% of it at that depth. Bench throughput is unchanged with the instrumentation compiled in (441.09 vs 442.14 t/s), since the marks are only emitted when the logger is on in per-op mode. Two latent bugs in the query-pool handling fall out of this and are fixed here: - The pool is created with n_nodes+100 slots but only the first n_nodes+1 were reset each graph, so anything using the headroom would read stale results. - The results buffer was sized n_nodes+1 while getQueryPoolResults was asked for query_idx entries. Equal today, but it is an overflow waiting for the first caller that writes an extra timestamp. Sub-op intervals log no flops, since they move bytes rather than doing math; attributing the node's flop count to them would corrupt the GFLOPS column for both halves. Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 50 +++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 752d1f3e9d3e..990a90729d5a 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -2383,6 +2383,13 @@ class vk_perf_logger { timings[name].push_back(time); } + // Log a sub-node interval under a caller-supplied name. Used for work a node's handler + // dispatches before the op itself (e.g. the FA K/V contiguize/dequant pass), which would + // otherwise be billed to the op and invisible. No flops: these move bytes, not math. + void log_timing_named(const char *name, uint64_t time) { + timings[std::string(name)].push_back(time); + } + void log_timing(const std::vector &nodes, const std::vector &names, uint64_t time) { uint64_t total_flops = 0; std::string name; @@ -2479,6 +2486,9 @@ struct ggml_backend_vk_context { std::vector query_fusion_node_count; std::vector query_nodes; std::vector query_node_idx; + // non-null => this query slot closes a sub-node interval logged under this literal name, + // not a graph node. See ggml_vk_perf_mark_subop. + std::vector query_sub_names; int32_t num_queries {}; int32_t query_idx {}; }; @@ -11291,6 +11301,22 @@ static bool ggml_vk_fa_dequant_scratch_fits(ggml_backend_vk_context * ctx, uint6 return fits; } +// Close a timestamp interval mid-node so work a handler dispatches before its op is billed +// separately instead of being folded into the op's own time. `name` must be a string literal +// (stored by pointer). No-op unless the perf logger is on in per-op mode. +static void ggml_vk_perf_mark_subop(ggml_backend_vk_context * ctx, vk_context& subctx, const char * name) { + if (!vk_perf_logger_enabled || vk_perf_logger_concurrent || ctx->query_pool == VK_NULL_HANDLE) { + return; + } + if (ctx->query_idx >= (int)ctx->num_queries) { + return; // pool headroom exhausted; drop the mark rather than overflow + } + ctx->query_nodes[ctx->query_idx] = nullptr; + ctx->query_fusion_names[ctx->query_idx] = nullptr; + ctx->query_sub_names[ctx->query_idx] = name; + subctx->s->buffer->buf.writeTimestamp(vk::PipelineStageFlagBits::eAllCommands, ctx->query_pool, ctx->query_idx++); +} + static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * q, const ggml_tensor * k, const ggml_tensor * v, const ggml_tensor * mask, const ggml_tensor * sinks, ggml_tensor * dst) { VK_LOG_DEBUG("ggml_vk_flash_attn((" << q << ", name=" << q->name << ", type=" << q->type << ", ne0=" << q->ne[0] << ", ne1=" << q->ne[1] << ", ne2=" << q->ne[2] << ", ne3=" << q->ne[3] << ", nb0=" << q->nb[0] << ", nb1=" << q->nb[1] << ", nb2=" << q->nb[2] << ", nb3=" << q->nb[3]; std::cerr << "), (" << k << ", name=" << k->name << ", type=" << k->type << ", ne0=" << k->ne[0] << ", ne1=" << k->ne[1] << ", ne2=" << k->ne[2] << ", ne3=" << k->ne[3] << ", nb0=" << k->nb[0] << ", nb1=" << k->nb[1] << ", nb2=" << k->nb[2] << ", nb3=" << k->nb[3]; @@ -11602,6 +11628,11 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx ggml_vk_sync_buffers(ctx, subctx); k_buf = k_dst; v_buf = v_dst; + // Bill the K/V contiguize/dequant pass on its own line. Without this it is charged to + // FLASH_ATTN_EXT, which makes the copy invisible and the kernel look slower than it is. + ggml_vk_perf_mark_subop(ctx, subctx, kv_needs_dequant || (k_quant && v_quant) + ? "FA_KV_DEQUANT (sub-op)" + : "FA_KV_CONTIGUIZE (sub-op)"); } uint32_t mask_n_head_log2 = ((sinks != nullptr) << 24) | n_head_log2; @@ -18014,9 +18045,11 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg ctx->query_fusion_node_count.resize(ctx->num_queries); ctx->query_nodes.resize(ctx->num_queries); ctx->query_node_idx.resize(ctx->num_queries); + ctx->query_sub_names.resize(ctx->num_queries); } - ctx->device->device.resetQueryPool(ctx->query_pool, 0, cgraph->n_nodes+1); + // Reset the whole pool, not just n_nodes+1: sub-op marks consume slots past that. + ctx->device->device.resetQueryPool(ctx->query_pool, 0, ctx->num_queries); std::fill(ctx->query_fusion_names.begin(), ctx->query_fusion_names.end(), nullptr); std::fill(ctx->query_fusion_node_count.begin(), ctx->query_fusion_node_count.end(), 0); std::fill(ctx->query_nodes.begin(), ctx->query_nodes.end(), nullptr); @@ -18352,6 +18385,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg // track a single node/fusion for the current query ctx->query_nodes[ctx->query_idx] = cgraph->nodes[i]; ctx->query_fusion_names[ctx->query_idx] = fusion_string; + ctx->query_sub_names[ctx->query_idx] = nullptr; compute_ctx->s->buffer->buf.writeTimestamp(vk::PipelineStageFlagBits::eAllCommands, ctx->query_pool, ctx->query_idx++); ggml_vk_sync_buffers(ctx, compute_ctx); } else { @@ -18393,14 +18427,22 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg ctx->compute_ctx.reset(); // Get the results and pass them to the logger - std::vector timestamps(cgraph->n_nodes + 1); - VK_CHECK(ctx->device->device.getQueryPoolResults(ctx->query_pool, 0, ctx->query_idx, (cgraph->n_nodes + 1)*sizeof(uint64_t), timestamps.data(), sizeof(uint64_t), vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait), "get timestamp results", ctx->device); + // Sized to the pool, not n_nodes+1: sub-op marks push query_idx past the node count. + std::vector timestamps(ctx->num_queries); + VK_CHECK(ctx->device->device.getQueryPoolResults(ctx->query_pool, 0, ctx->query_idx, ctx->num_queries*sizeof(uint64_t), timestamps.data(), sizeof(uint64_t), vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait), "get timestamp results", ctx->device); if (!vk_perf_logger_concurrent) { // Log each op separately for (int i = 1; i < ctx->query_idx; i++) { + const uint64_t dt = uint64_t((timestamps[i] - timestamps[i-1]) * ctx->device->properties.limits.timestampPeriod); + if (ctx->query_sub_names[i] != nullptr) { + // sub-node interval (e.g. the FA K/V contiguize pass) - billed separately so + // it is not silently folded into the op that dispatched it + ctx->perf_logger->log_timing_named(ctx->query_sub_names[i], dt); + continue; + } auto node = ctx->query_nodes[i]; auto name = ctx->query_fusion_names[i]; - ctx->perf_logger->log_timing(node, name, uint64_t((timestamps[i] - timestamps[i-1]) * ctx->device->properties.limits.timestampPeriod)); + ctx->perf_logger->log_timing(node, name, dt); } } else { // Log each group of nodes From 8f178b8477543fe2cebed12057d9387b4bfcdd5f Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 9 Aug 2026 10:43:34 +0000 Subject: [PATCH 094/212] vulkan: scale the FA MMQ dot product in fp32 before narrowing acc is an int32 sum of dotPacked4x8EXT results. With q8_0 both operands are full int8, so its bound is d_per_step*4*127*127, which overflows f16 when ACC_TYPE is f16 (GGML_PREC_DEFAULT): the score goes +inf, softmax is destroyed and the output comes back -FLT_MAX. Nibble types bound at ~30480 and stay in range, which is why only q8_0 tripped it. Apply the scales in fp32, then narrow. Identical arithmetic when ACC_TYPE is float, so the f32acc path is untouched. This is an upstream bug, not a fork regression, and it was fixed here once before - 61e77f4 carried it and the rebase onto b10133 dropped it. Only the scalar shader has MMQ, so the failing shape is hsk=128 + q8_0 K + prec=def + nb=1 + nr23=[1,1]; GQA>1 and nb>1 route to coopmat1 and escape. test-backend-ops -o FLASH_ATTN_EXT on gfx1151, quiet box: 13295/13295 twice, up from 13257/13295. All 38 failures were type_K=q8_0 prec=def. Both ggml_flash_attn_ext call sites in the tree force GGML_PREC_F32, so no model path reaches this - it is a landmine for the next person touching precision, not a live bug. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp index 0c1b6d0673e9..18a37add9bf8 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp @@ -420,7 +420,11 @@ void main() { acc += dotPacked4x8EXT(Qf[qib].qs[qiqs + d], k_quants[d]); } - Sf[r][c] += ACC_TYPE(acc) * ACC_TYPE(Qf[qib].ds.x) * k_dm.x; + // scale in fp32 before narrowing: acc is an int32 sum of dotPacked4x8EXT + // results, bounded by d_per_step*4*127*127 with q8_0 on both sides, which + // overflows f16 when ACC_TYPE is f16 (GGML_PREC_DEFAULT). Identical + // arithmetic when ACC_TYPE is float. + Sf[r][c] += ACC_TYPE(float(acc) * float(Qf[qib].ds.x) * float(k_dm.x)); if ((d_tid * (HSK_per_thread / 4) + d_block) % 8 == 0) { Sf[r][c] += k_dot_correction(qib, k_dm); } From bacb0d313605781cc9eeaade4f417e0ace08f02f Mon Sep 17 00:00:00 2001 From: Jerome Coste Date: Mon, 7 Sep 2026 23:26:14 -0700 Subject: [PATCH 095/212] docs : align issue templates with the fork's AI policy Assisted-by: GitHub Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/010-bug-compilation.yml | 2 +- .github/ISSUE_TEMPLATE/011-bug-results.yml | 2 +- .github/ISSUE_TEMPLATE/019-bug-misc.yml | 2 +- .github/ISSUE_TEMPLATE/020-enhancement.yml | 2 +- .github/ISSUE_TEMPLATE/030-research.yml | 2 +- .github/ISSUE_TEMPLATE/040-refactor.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/010-bug-compilation.yml b/.github/ISSUE_TEMPLATE/010-bug-compilation.yml index 51ef87a59068..991b83c9325d 100644 --- a/.github/ISSUE_TEMPLATE/010-bug-compilation.yml +++ b/.github/ISSUE_TEMPLATE/010-bug-compilation.yml @@ -13,7 +13,7 @@ body: If the compilation succeeds with ccache disabled you should be able to permanently fix the issue by clearing `~/.cache/ccache` (on Linux). - Please fill out this template yourself, copypasting language model outputs is [strictly prohibited](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). + AI assistance is allowed in this fork. Disclose how it was used, verify the information in your report, and take responsibility for its accuracy. See [our AI usage policy](https://github.com/halo-box/strix-llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). - type: textarea id: commit attributes: diff --git a/.github/ISSUE_TEMPLATE/011-bug-results.yml b/.github/ISSUE_TEMPLATE/011-bug-results.yml index 23150d0b619d..dc333e7bb543 100644 --- a/.github/ISSUE_TEMPLATE/011-bug-results.yml +++ b/.github/ISSUE_TEMPLATE/011-bug-results.yml @@ -13,7 +13,7 @@ body: please reproduce your issue using one of the examples/binaries in this repository. The `llama-completion` binary can be used for simple and reproducible model inference. - Please fill out this template yourself, copypasting language model outputs is [strictly prohibited](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). + AI assistance is allowed in this fork. Disclose how it was used, verify the information in your report, and take responsibility for its accuracy. See [our AI usage policy](https://github.com/halo-box/strix-llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). - type: textarea id: version attributes: diff --git a/.github/ISSUE_TEMPLATE/019-bug-misc.yml b/.github/ISSUE_TEMPLATE/019-bug-misc.yml index 041a7cdb2eeb..99862b83d857 100644 --- a/.github/ISSUE_TEMPLATE/019-bug-misc.yml +++ b/.github/ISSUE_TEMPLATE/019-bug-misc.yml @@ -11,7 +11,7 @@ body: If you encountered the issue while using an external UI (e.g. ollama), please reproduce your issue using one of the examples/binaries in this repository. - Please fill out this template yourself, copypasting language model outputs is [strictly prohibited](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). + AI assistance is allowed in this fork. Disclose how it was used, verify the information in your report, and take responsibility for its accuracy. See [our AI usage policy](https://github.com/halo-box/strix-llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). - type: textarea id: version attributes: diff --git a/.github/ISSUE_TEMPLATE/020-enhancement.yml b/.github/ISSUE_TEMPLATE/020-enhancement.yml index fdca622ae9db..eb8da1bfa3c4 100644 --- a/.github/ISSUE_TEMPLATE/020-enhancement.yml +++ b/.github/ISSUE_TEMPLATE/020-enhancement.yml @@ -8,7 +8,7 @@ body: value: | [Please post your idea first in Discussion if there is not yet a consensus for this enhancement request. This will help to keep this issue tracker focused on enhancements that the community has agreed needs to be implemented.](https://github.com/ggml-org/llama.cpp/discussions/categories/ideas) - Please fill out this template yourself, copypasting language model outputs is [strictly prohibited](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). + AI assistance is allowed in this fork. Disclose how it was used, verify the information in your report, and take responsibility for its accuracy. See [our AI usage policy](https://github.com/halo-box/strix-llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). - type: checkboxes id: prerequisites diff --git a/.github/ISSUE_TEMPLATE/030-research.yml b/.github/ISSUE_TEMPLATE/030-research.yml index 172edc1326a4..c4639f0c8c7b 100644 --- a/.github/ISSUE_TEMPLATE/030-research.yml +++ b/.github/ISSUE_TEMPLATE/030-research.yml @@ -8,7 +8,7 @@ body: value: | Don't forget to check for any [duplicate research issue tickets](https://github.com/ggml-org/llama.cpp/issues?q=is%3Aopen+is%3Aissue+label%3A%22research+%F0%9F%94%AC%22) - Please fill out this template yourself, copypasting language model outputs is [strictly prohibited](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). + AI assistance is allowed in this fork. Disclose how it was used, verify the information in your report, and take responsibility for its accuracy. See [our AI usage policy](https://github.com/halo-box/strix-llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). - type: checkboxes id: research-stage diff --git a/.github/ISSUE_TEMPLATE/040-refactor.yml b/.github/ISSUE_TEMPLATE/040-refactor.yml index efd10d79d99a..55c5c2ad984c 100644 --- a/.github/ISSUE_TEMPLATE/040-refactor.yml +++ b/.github/ISSUE_TEMPLATE/040-refactor.yml @@ -9,7 +9,7 @@ body: Don't forget to [check for existing refactor issue tickets](https://github.com/ggml-org/llama.cpp/issues?q=is%3Aopen+is%3Aissue+label%3Arefactoring) in case it's already covered. Also you may want to check [Pull request refactor label as well](https://github.com/ggml-org/llama.cpp/pulls?q=is%3Aopen+is%3Apr+label%3Arefactoring) for duplicates too. - Please fill out this template yourself, copypasting language model outputs is [strictly prohibited](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). + AI assistance is allowed in this fork. Disclose how it was used, verify the information in your report, and take responsibility for its accuracy. See [our AI usage policy](https://github.com/halo-box/strix-llama.cpp/blob/master/CONTRIBUTING.md#ai-usage-policy). - type: textarea id: background-description From fc349bd785d8bd878235039d8e35a186d872d0cb Mon Sep 17 00:00:00 2001 From: ARC <261443955+arc-uri-el@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:54:30 +0200 Subject: [PATCH 096/212] context : size device checkpoint views in quantized elements A quantized type size describes a block rather than one tensor element. Include the block size when converting saved bytes into view dimensions in the device writer, reader and fragmented-copy fallback. Extend the existing save/load regression with fragmented-to-compact restoration. Assisted-by: GPT 6 Astra --- src/llama-context.cpp | 7 +++---- tests/test-save-load-state.cpp | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/llama-context.cpp b/src/llama-context.cpp index e202c68400f6..31750f071e12 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -2788,7 +2788,7 @@ class llama_io_write_device : public llama_io_write_i { for (const auto & winfo : winfos) { auto * buft = ggml_backend_buffer_get_type(winfo.tensor->buffer); - const int64_t n = winfo.size/ggml_element_size(winfo.tensor); + const int64_t n = (winfo.size / ggml_type_size(winfo.tensor->type)) * ggml_blck_size(winfo.tensor->type); auto & mbuf = mbufs_new[buft]; @@ -2919,7 +2919,7 @@ class llama_io_read_device : public llama_io_read_i { for (const auto & rinfo : rinfos) { auto * buft = ggml_backend_buffer_get_type(rinfo.tensor->buffer); - const int64_t n = rinfo.size/ggml_element_size(rinfo.tensor); + const int64_t n = (rinfo.size / ggml_type_size(rinfo.tensor->type)) * ggml_blck_size(rinfo.tensor->type); auto & mbuf = mbufs_new[buft]; @@ -2986,8 +2986,7 @@ class llama_io_read_device : public llama_io_read_i { 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); + const int64_t n_el = (n_copy / ggml_type_size(src_t->type)) * ggml_blck_size(src_t->type); auto * src_v = ggml_view_1d(ctx_scratch, src_t, n_el, src_off); ggml_backend_view_init(src_v); diff --git a/tests/test-save-load-state.cpp b/tests/test-save-load-state.cpp index 6179e6c10848..618b6635e988 100644 --- a/tests/test-save-load-state.cpp +++ b/tests/test-save-load-state.cpp @@ -360,14 +360,14 @@ static bool test_seq_cp_device(struct llama_model * model, const struct common_p // - save the seq 1 state, free the interleaved seq 0 cells, and restore via the given io path // - the restore destination is non-contiguous: scatter reads are batched per contiguous run // - save again on the host and compare the two blobs byte for byte -static bool test_seq_cp_scatter(struct llama_model * model, const struct common_params & params, const llama_tokens & tokens, int test_num, bool on_device) { +static bool test_seq_cp_scatter(struct llama_model * model, const struct common_params & params, const llama_tokens & tokens, int test_num, bool on_device, bool compact = false) { auto params_ctx = common_context_params_to_llama(params); params_ctx.n_ctx = 256; params_ctx.n_seq_max = 2; params_ctx.kv_unified = true; auto ctx = llama_context_ptr{llama_init_from_model(model, params_ctx)}; - LOG("\n=== Test %d: seq copy (%s, scatter) ===\n", test_num, on_device ? "device" : "host"); + LOG("\n=== Test %d: seq copy (%s, %s) ===\n", test_num, on_device ? "device" : "host", compact ? "scatter to compact" : "scatter"); const uint32_t flags = on_device ? LLAMA_STATE_SEQ_FLAGS_ON_DEVICE : LLAMA_STATE_SEQ_FLAGS_NONE; @@ -425,6 +425,11 @@ static bool test_seq_cp_scatter(struct llama_model * model, const struct common_ return false; } + if (compact) { + // Saved ranges [2, 1] must restore into one contiguous range, including quantized KV blocks. + llama_memory_clear(llama_get_memory(ctx.get()), true); + } + // restore via the io path under test const size_t nset = llama_state_seq_set_data_ext(ctx.get(), state_save.data(), state_save.size(), 1, flags); if (nset != state_save.size()) { @@ -508,7 +513,7 @@ static bool test_state_roundtrip(struct llama_model * model, const struct common } -// Run the full save/load test suite (tests 1-8) for a single model. +// Run the full save/load test suite (tests 1-9) 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; @@ -590,6 +595,11 @@ static bool run_save_load_tests_for_model(const std::string & model_path, const return false; } + // Test 9: on-device restore with different source and destination chunking. + if (!test_seq_cp_scatter(model, params, tokens, 9, true, true)) { + return false; + } + LOG("\nAll tests passed.\n"); return true; From f8668dd354c829a92e0e4d58a6c4eaec64ec8ddd Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sat, 22 Aug 2026 08:06:54 +0000 Subject: [PATCH 097/212] server : do not re-verify replayed draft tokens after a checkpoint restore With full-checkpoint rollback, a partial draft acceptance restores the pre-round state and re-decodes the accepted tokens to rebuild it. The replay went through the same verification as a fresh draft. On backends where logits change with batch shape or memory layout (Vulkan), that re-verification can reject a token the original verification accepted; the rejection restores the same checkpoint and replays again, and the slot loops on one position without emitting anything. qwen35moe with --spec-type draft-mtp stalled this way a few hundred tokens into long generations (the v0.6.8 MTP hang): the loop repeated "accepted 2/3, restore at pos 995" every 27 ms with the GPU at 90 percent. Accept the replayed tokens without re-verifying and sample only the continuation from the final position. The replayed prefix was accepted by the verification that triggered the restore; the replay exists to rebuild state. On backends with batch-shape invariant logits the re-verification always agreed, so behavior there is unchanged (verified bit-identical on CPU with and without this change, 800-token greedy pair, 118 restore rounds). Assisted-by: Claude Fable 5 (cherry picked from commit 9c5d899ff7966179f56e49edd5c7a57f7b6172e6) --- tools/server/server-context.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 7d636c7923b8..81dac470622e 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -4132,11 +4132,24 @@ struct server_context_impl { GGML_ASSERT(slot.spec_i_batch.size() == n_draft + 1); 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( + std::vector accepted; + if (!synth_probs.empty()) { + accepted = 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); + } else if (slot.spec_is_replay) { + // replayed tokens were accepted before the restore; re-verifying them can + // disagree when logits depend on batch shape, and each disagreement restores + // the same checkpoint again - the slot stops making progress + accepted = slot.spec_draft; + for (const llama_token id : accepted) { + common_sampler_accept(slot.smpl.get(), id, true); + } + accepted.push_back(common_sampler_sample(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch.back())); + common_sampler_accept(slot.smpl.get(), accepted.back(), true); + } else { + accepted = common_sampler_sample_and_accept_n(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft); + } slot.spec_i_batch.clear(); GGML_ASSERT(accepted.size() >= 1); From e72ffec16eb59e9b024288c9f669963effb096f2 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:57:00 +0200 Subject: [PATCH 098/212] vulkan: DeepSeek V4 lightning indexer kernels + indexed sparse FA Implements GGML_OP_LIGHTNING_INDEXER on Vulkan (scalar subgroup shader for small batches, coopmat 16x16 tiles for prefill, dedicated decode variant) and an indexed sparse flash-attention path that consumes the indexer's top-k selection directly via a new ggml_flash_attn_ext_add_top_k() API (FA src[5] + op_param[4] = n_kv_raw dense prefix), instead of attending densely over the full compressed KV. The sparse path engages only for V4's CSA shape (hd 512, 64 heads, MQA, f16 K==V latent) when dense_kv >= 3x active_kv; everything else falls through to the dense path, which stays correct because the kq_mask still carries the top-k selection. Dropped from the original: the mul_mat_id tokens-per-expert pipeline selection, which duplicates GGML_VK_MMID_SMALLN already on this branch. Originally by Gaetan Puleo (llama-cpp-nathan-toolbox-deepseek-v4-poc, branch deepseek-v4-flash-strix-halo); cherry-picked with the mmid hunk dropped. --- ggml/include/ggml.h | 5 + ggml/src/ggml-vulkan/ggml-vulkan.cpp | 170 ++++++++++++++++++ .../vulkan-shaders/flash_attn_top_k.comp | 144 +++++++++++++++ .../vulkan-shaders/lightning_indexer_cm.comp | 125 +++++++++++++ .../lightning_indexer_decode_cm.comp | 110 ++++++++++++ .../lightning_indexer_scalar64.comp | 92 ++++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 7 + ggml/src/ggml.c | 14 ++ src/llama-graph.cpp | 7 +- src/llama-graph.h | 4 +- src/models/deepseek4.cpp | 2 +- tests/test-backend-ops.cpp | 10 +- 12 files changed, 686 insertions(+), 4 deletions(-) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_scalar64.comp diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h index f0eda79a1ae2..2ff12d00295d 100644 --- a/ggml/include/ggml.h +++ b/ggml/include/ggml.h @@ -2486,6 +2486,11 @@ extern "C" { struct ggml_tensor * a, struct ggml_tensor * sinks); + GGML_API void ggml_flash_attn_ext_add_top_k( + struct ggml_tensor * a, + struct ggml_tensor * top_k, + int64_t n_kv_raw); + // TODO: needs to be adapted to ggml_flash_attn_ext GGML_API struct ggml_tensor * ggml_flash_attn_back( struct ggml_context * ctx, diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 990a90729d5a..44efa5c4897c 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1104,6 +1104,10 @@ struct vk_device_struct { 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_lightning_indexer_f16; + vk_pipeline pipeline_lightning_indexer_cm_f16; + vk_pipeline pipeline_lightning_indexer_decode_cm_f16; + vk_pipeline pipeline_flash_attn_top_k_f16; vk_pipeline pipeline_ssm_scan_f32_d128; vk_pipeline pipeline_ssm_scan_f32_d256; vk_pipeline pipeline_ssm_conv_f32; @@ -1933,6 +1937,29 @@ struct vk_op_gated_delta_net_push_constants { uint32_t K; }; +// push constants for the fork's wave64 f16 lightning-indexer kernels (scalar-64 + CM family) +struct vk_op_lightning_indexer_cm_push_constants { + uint32_t n_kv, n_batch, n_stream, nem3; + uint32_t nb1, nb3; + uint32_t nbq1, nbq2, nbq3; + uint32_t nbk2, nbk3; + uint32_t nbw1, nbw3; + uint32_t nbm1, nbm3; +}; +static_assert(sizeof(vk_op_lightning_indexer_cm_push_constants) <= 128); + +struct vk_op_flash_attn_top_k_push_constants { + uint32_t n_batch, n_kv, n_kv_raw, n_top_k, n_head; + uint32_t nbq1, nbq2, nbq3; + uint32_t nbk1, nbk3; + uint32_t nbm1, nbm3; + uint32_t nbt1, nbt3; + uint32_t nb1, nb2, nb3; + float scale; + uint32_t has_sinks; +}; +static_assert(sizeof(vk_op_flash_attn_top_k_push_constants) <= 128); + struct vk_op_ssm_scan_push_constants { uint32_t nb02, nb03, nb12, nb13; uint32_t nb21, nb22, nb31; @@ -6205,6 +6232,29 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { } } + if (device->subgroup_arithmetic && device->subgroup_size == 64) { + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_f16, + "lightning_indexer_f16", lightning_indexer_f16_len, lightning_indexer_f16_data, "main", 5, + sizeof(vk_op_lightning_indexer_cm_push_constants), {8, 1, 1}, {device->subgroup_size}, 1, true, true, + device->subgroup_size); +#if defined(VK_KHR_cooperative_matrix) && defined(GGML_VULKAN_COOPMAT_GLSLC_SUPPORT) + if (device->coopmat_support && device->coopmat_support_16x16x16_f32acc && device->subgroup_size_control) { + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_cm_f16, + "lightning_indexer_cm_f16", lightning_indexer_cm_f16_len, lightning_indexer_cm_f16_data, "main", 5, + sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 16, 1}, {device->subgroup_size}, 1, true, true, + device->subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_decode_cm_f16, + "lightning_indexer_decode_cm_f16", lightning_indexer_decode_cm_f16_len, lightning_indexer_decode_cm_f16_data, "main", 5, + sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 1, 1}, {device->subgroup_size}, 1, true, true, + device->subgroup_size); + } +#endif + ggml_vk_create_pipeline(device, device->pipeline_flash_attn_top_k_f16, + "flash_attn_top_k_f16", flash_attn_top_k_f16_len, flash_attn_top_k_f16_data, "main", 6, + sizeof(vk_op_flash_attn_top_k_push_constants), {1, 1, 1}, {512, device->subgroup_size}, 1, true, true, + device->subgroup_size); + } + if (device->subgroup_arithmetic && device->subgroup_require_full_support) { ggml_vk_create_pipeline(device, device->pipeline_ssm_scan_f32_d128, "ssm_scan_128_f32", ssm_scan_subgroup_f32_len, ssm_scan_subgroup_f32_data, "main", 8, sizeof(vk_op_ssm_scan_push_constants), {1, 1, 1}, {128, device->subgroup_size}, 1, true, true); ggml_vk_create_pipeline(device, device->pipeline_ssm_scan_f32_d256, "ssm_scan_256_f32", ssm_scan_subgroup_f32_len, ssm_scan_subgroup_f32_data, "main", 8, sizeof(vk_op_ssm_scan_push_constants), {1, 1, 1}, {256, device->subgroup_size}, 1, true, true); @@ -11317,6 +11367,69 @@ static void ggml_vk_perf_mark_subop(ggml_backend_vk_context * ctx, vk_context& s subctx->s->buffer->buf.writeTimestamp(vk::PipelineStageFlagBits::eAllCommands, ctx->query_pool, ctx->query_idx++); } +static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & subctx, + const ggml_tensor * q, const ggml_tensor * k, const ggml_tensor * v, + const ggml_tensor * mask, const ggml_tensor * sinks, ggml_tensor * dst) { + const ggml_tensor * top_k = dst->src[5]; + if (!top_k || !ctx->device->pipeline_flash_attn_top_k_f16 || + q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || v->type != GGML_TYPE_F16 || + !mask || mask->type != GGML_TYPE_F16 || top_k->type != GGML_TYPE_I32 || + q->ne[0] != 512 || q->ne[1] < 64 || k->ne[0] != 512 || v->ne[0] != 512 || + q->ne[2] != 64 || k->ne[2] != 1 || v->ne[2] != 1 || + q->ne[1] != top_k->ne[1] || q->ne[3] != top_k->ne[3] || + k->ne[1] != v->ne[1] || k->buffer != v->buffer || k->data != v->data || + !ggml_is_contiguous(mask) || !ggml_is_contiguous(top_k)) { + return false; + } + + float scale = 0.0f; + float max_bias = 0.0f; + float logit_softcap = 0.0f; + memcpy(&scale, (const float *) dst->op_params + 0, sizeof(float)); + memcpy(&max_bias, (const float *) dst->op_params + 1, sizeof(float)); + memcpy(&logit_softcap, (const float *) dst->op_params + 2, sizeof(float)); + if (max_bias != 0.0f || logit_softcap != 0.0f) { + return false; + } + + const int32_t n_kv_raw = ggml_get_op_params_i32(dst, 4); + if (n_kv_raw < 0 || n_kv_raw > k->ne[1] || top_k->ne[0] > k->ne[1] - n_kv_raw) { + return false; + } + const int64_t n_kv_active = n_kv_raw + top_k->ne[0]; + if (k->ne[1] < 3 * n_kv_active) { + return false; + } + + const vk_op_flash_attn_top_k_push_constants pc = { + (uint32_t) q->ne[1], (uint32_t) k->ne[1], (uint32_t) n_kv_raw, + (uint32_t) top_k->ne[0], (uint32_t) q->ne[2], + (uint32_t) (q->nb[1] / sizeof(float)), + (uint32_t) (q->nb[2] / sizeof(float)), + (uint32_t) (q->nb[3] / sizeof(float)), + (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)), + (uint32_t) (k->nb[3] / sizeof(ggml_fp16_t)), + (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)), + (uint32_t) (mask->nb[3] / sizeof(ggml_fp16_t)), + (uint32_t) (top_k->nb[1] / sizeof(int32_t)), + (uint32_t) (top_k->nb[3] / sizeof(int32_t)), + (uint32_t) (dst->nb[1] / sizeof(float)), + (uint32_t) (dst->nb[2] / sizeof(float)), + (uint32_t) (dst->nb[3] / sizeof(float)), + scale, sinks != nullptr, + }; + + const vk_subbuffer q_buf = ggml_vk_tensor_subbuffer(ctx, q); + const vk_subbuffer sinks_buf = sinks ? ggml_vk_tensor_subbuffer(ctx, sinks) : q_buf; + vk_pipeline pipeline = ctx->device->pipeline_flash_attn_top_k_f16; + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, + {q_buf, ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, mask), sinks_buf, + ggml_vk_tensor_subbuffer(ctx, top_k), ggml_vk_tensor_subbuffer(ctx, dst)}, + pc, {(uint32_t) q->ne[1], (uint32_t) CEIL_DIV(q->ne[2], 8), (uint32_t) q->ne[3]}); + return true; +} + static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * q, const ggml_tensor * k, const ggml_tensor * v, const ggml_tensor * mask, const ggml_tensor * sinks, ggml_tensor * dst) { VK_LOG_DEBUG("ggml_vk_flash_attn((" << q << ", name=" << q->name << ", type=" << q->type << ", ne0=" << q->ne[0] << ", ne1=" << q->ne[1] << ", ne2=" << q->ne[2] << ", ne3=" << q->ne[3] << ", nb0=" << q->nb[0] << ", nb1=" << q->nb[1] << ", nb2=" << q->nb[2] << ", nb3=" << q->nb[3]; std::cerr << "), (" << k << ", name=" << k->name << ", type=" << k->type << ", ne0=" << k->ne[0] << ", ne1=" << k->ne[1] << ", ne2=" << k->ne[2] << ", ne3=" << k->ne[3] << ", nb0=" << k->nb[0] << ", nb1=" << k->nb[1] << ", nb2=" << k->nb[2] << ", nb3=" << k->nb[3]; @@ -11368,6 +11481,9 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx assert(dst->type == GGML_TYPE_F32); assert(q->type == GGML_TYPE_F32); + if (ggml_vk_flash_attn_top_k(ctx, subctx, q, k, v, mask, sinks, dst)) { + return; + } uint32_t gqa_ratio = 1; uint32_t qk_ratio = neq2 / nek2; uint32_t workgroups_x = (uint32_t)neq1; @@ -12275,6 +12391,17 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const } return nullptr; case GGML_OP_LIGHTNING_INDEXER: + // fork fast path: f16 K on wave64 subgroup-arithmetic devices routes to the tuned + // scalar-64/CM kernels; anything else falls through to the generic pipeline table + if (src0->type == GGML_TYPE_F32 && src1->type == GGML_TYPE_F16 && dst->type == GGML_TYPE_F32 && + src0->ne[0] == 128 && src0->ne[1] == 64 && src1->ne[1] == 1 && + ctx->device->pipeline_lightning_indexer_f16) { + if (ctx->device->pipeline_lightning_indexer_decode_cm_f16 && src0->ne[2] == 1) { + return ctx->device->pipeline_lightning_indexer_decode_cm_f16; + } + return ctx->device->pipeline_lightning_indexer_cm_f16 && src0->ne[2] >= 16 ? + ctx->device->pipeline_lightning_indexer_cm_f16 : ctx->device->pipeline_lightning_indexer_f16; + } // 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]; @@ -13355,6 +13482,8 @@ 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_cm(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst, vk_pipeline pipeline); + 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]; @@ -13364,6 +13493,14 @@ static void ggml_vk_lightning_indexer(ggml_backend_vk_context * ctx, vk_context& vk_pipeline pipeline = ggml_vk_op_get_pipeline(ctx, q, k, w, dst, dst->op); GGML_ASSERT(pipeline != nullptr); + // the fork's wave64 f16 kernels take their own push-constant layout + if (pipeline == ctx->device->pipeline_lightning_indexer_f16 || + pipeline == ctx->device->pipeline_lightning_indexer_cm_f16 || + pipeline == ctx->device->pipeline_lightning_indexer_decode_cm_f16) { + ggml_vk_lightning_indexer_cm(ctx, subctx, dst, pipeline); + return; + } + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); const uint32_t n_kv = k->ne[2]; @@ -13461,6 +13598,39 @@ static void ggml_vk_gated_delta_net(ggml_backend_vk_context * ctx, vk_context& s pc, { H, n_seqs, S_v }); } +static void ggml_vk_lightning_indexer_cm(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst, vk_pipeline pipeline) { + 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]; + + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + + const vk_op_lightning_indexer_cm_push_constants pc = { + (uint32_t) k->ne[2], + (uint32_t) q->ne[2], + (uint32_t) q->ne[3], + (uint32_t) m->ne[3], + (uint32_t) (dst->nb[1] / sizeof(float)), + (uint32_t) (dst->nb[3] / sizeof(float)), + (uint32_t) (q->nb[1] / sizeof(float)), + (uint32_t) (q->nb[2] / sizeof(float)), + (uint32_t) (q->nb[3] / sizeof(float)), + (uint32_t) (k->nb[2] / sizeof(ggml_fp16_t)), + (uint32_t) (k->nb[3] / sizeof(ggml_fp16_t)), + (uint32_t) (w->nb[1] / sizeof(float)), + (uint32_t) (w->nb[3] / sizeof(float)), + (uint32_t) (m->nb[1] / sizeof(ggml_fp16_t)), + (uint32_t) (m->nb[3] / sizeof(ggml_fp16_t)), + }; + + 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, {(uint32_t) k->ne[2], (uint32_t) q->ne[2], (uint32_t) q->ne[3]}); +} + static void ggml_vk_ssm_scan(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) { const ggml_tensor * src0 = dst->src[0]; const ggml_tensor * src1 = dst->src[1]; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp new file mode 100644 index 000000000000..7f82266781f1 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp @@ -0,0 +1,144 @@ +#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_arithmetic : require +#extension GL_KHR_shader_subgroup_basic : require + +layout(constant_id = 0) const uint WORKGROUP_SIZE = 512; +layout(constant_id = 1) const uint SUBGROUP_SIZE = 64; +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer QBuf { float data_q[]; }; +layout(binding = 1) readonly buffer KBuf { float16_t data_k[]; }; +layout(binding = 2) readonly buffer MaskBuf { float16_t data_m[]; }; +layout(binding = 3) readonly buffer SinkBuf { float data_s[]; }; +layout(binding = 4) readonly buffer TopBuf { int data_top[]; }; +layout(binding = 5) writeonly buffer DstBuf { float data_dst[]; }; + +layout(push_constant) uniform Parameters { + uint n_batch; + uint n_kv; + uint n_kv_raw; + uint n_top_k; + uint n_head; + uint nbq1; + uint nbq2; + uint nbq3; + uint nbk1; + uint nbk3; + uint nbm1; + uint nbm3; + uint nbt1; + uint nbt3; + uint nb1; + uint nb2; + uint nb3; + float scale; + uint has_sinks; +} p; + +const uint HEAD_SIZE = 512; +const uint HEADS_PER_GROUP = 8; +const uint KEYS_PER_BLOCK = 16; + +shared float16_t key_sh[KEYS_PER_BLOCK * HEAD_SIZE]; +shared uint key_idx[KEYS_PER_BLOCK]; + +void main() { + const uint tid = gl_LocalInvocationIndex; + const uint lane = gl_SubgroupInvocationID; + const uint head = gl_WorkGroupID.y * HEADS_PER_GROUP + gl_SubgroupID; + const uint token = gl_WorkGroupID.x; + const uint stream = gl_WorkGroupID.z; + + if (token >= p.n_batch || head >= p.n_head) { + return; + } + + float accum[HEAD_SIZE / SUBGROUP_SIZE]; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { + accum[i] = 0.0; + } + + float row_max = uintBitsToFloat(0xff800000); + float row_sum = 0.0; + const uint q_base = stream * p.nbq3 + head * p.nbq2 + token * p.nbq1; + const uint mask_base = stream * p.nbm3 + token * p.nbm1; + const uint top_base = stream * p.nbt3 + token * p.nbt1; + const uint total_keys = p.n_kv_raw + p.n_top_k; + + for (uint kb = 0; kb < total_keys; kb += KEYS_PER_BLOCK) { + if (tid < KEYS_PER_BLOCK) { + const uint selected = kb + tid; + uint key = p.n_kv; + if (selected < p.n_kv_raw) { + key = selected; + } else if (selected < total_keys) { + const int compressed = data_top[top_base + selected - p.n_kv_raw]; + if (compressed >= 0 && uint(compressed) < p.n_kv - p.n_kv_raw) { + key = p.n_kv_raw + uint(compressed); + } + } + key_idx[tid] = key; + } + barrier(); + + for (uint idx = tid; idx < KEYS_PER_BLOCK * HEAD_SIZE; idx += WORKGROUP_SIZE) { + const uint col = idx / HEAD_SIZE; + const uint dim = idx % HEAD_SIZE; + const uint key = key_idx[col]; + key_sh[idx] = key < p.n_kv ? data_k[stream * p.nbk3 + key * p.nbk1 + dim] : float16_t(0.0); + } + barrier(); + + [[unroll]] for (uint col = 0; col < KEYS_PER_BLOCK; ++col) { + const uint selected = kb + col; + const uint key = key_idx[col]; + if (selected >= total_keys || key >= p.n_kv) { + continue; + } + + float partial = 0.0; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { + const uint dim = lane + i * SUBGROUP_SIZE; + partial += data_q[q_base + dim] * float(key_sh[col * HEAD_SIZE + dim]); + } + const float mask = float(data_m[mask_base + key]); + const float score = subgroupAdd(partial) * p.scale + mask; + if (mask < -65500.0) { + continue; + } + + const float new_max = max(row_max, score); + const float old_scale = row_sum == 0.0 ? 0.0 : exp(row_max - new_max); + const float value_scale = exp(score - new_max); + row_sum = row_sum * old_scale + value_scale; + row_max = new_max; + + [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { + const uint dim = lane + i * SUBGROUP_SIZE; + accum[i] = accum[i] * old_scale + value_scale * float(key_sh[col * HEAD_SIZE + dim]); + } + } + barrier(); + } + + if (p.has_sinks != 0) { + const float sink = data_s[head]; + const float new_max = max(row_max, sink); + const float old_scale = row_sum == 0.0 ? 0.0 : exp(row_max - new_max); + const float sink_scale = exp(sink - new_max); + row_sum = row_sum * old_scale + sink_scale; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { + accum[i] *= old_scale; + } + } + + const uint dst_base = stream * p.nb3 + token * p.nb2 + head * p.nb1; + const float inv_sum = row_sum == 0.0 ? 0.0 : 1.0 / row_sum; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { + data_dst[dst_base + lane + i * SUBGROUP_SIZE] = accum[i] * inv_sum; + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp new file mode 100644 index 000000000000..a0a3639d2546 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp @@ -0,0 +1,125 @@ +#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_cooperative_matrix : require +#extension GL_KHR_memory_scope_semantics : require + +layout(constant_id = 0) const uint SUBGROUP_SIZE = 64; +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer QBuf { float data_q[]; }; +layout(binding = 1) readonly buffer KBuf { float16_t data_k[]; }; +layout(binding = 2) readonly buffer WBuf { float data_w[]; }; +layout(binding = 3) readonly buffer MBuf { float16_t data_m[]; }; +layout(binding = 4) writeonly buffer DstBuf { float data_dst[]; }; + +layout(push_constant) uniform Parameters { + uint n_kv; + uint n_batch; + uint n_stream; + uint nem3; + uint nb1; + uint nb3; + uint nbq1; + uint nbq2; + uint nbq3; + uint nbk2; + uint nbk3; + uint nbw1; + uint nbw3; + uint nbm1; + uint nbm3; +} p; + +const uint TILE = 16; +const uint HEAD_SIZE = 128; +const uint N_HEAD = 64; +const uint VEC_PER_HEAD = HEAD_SIZE / 4; +const uint TILE_STRIDE = VEC_PER_HEAD + 2; +const uint SCORE_STRIDE = TILE / 4 + 1; + +shared f16vec4 q_sh[TILE * TILE_STRIDE]; +shared f16vec4 k_sh[TILE * TILE_STRIDE]; +shared vec4 score_sh[TILE * SCORE_STRIDE]; + +void main() { + const uint tid = gl_LocalInvocationIndex; + const uint kv_base = gl_WorkGroupID.x * TILE; + const uint token_base = gl_WorkGroupID.y * TILE; + const uint stream = gl_WorkGroupID.z; + + float totals[4]; + [[unroll]] for (uint i = 0; i < 4; ++i) { + totals[i] = 0.0; + } + + for (uint idx = tid; idx < TILE * VEC_PER_HEAD; idx += SUBGROUP_SIZE) { + const uint key = idx / VEC_PER_HEAD; + const uint d4 = idx % VEC_PER_HEAD; + const uint kv = kv_base + key; + f16vec4 value = f16vec4(0.0); + if (kv < p.n_kv) { + const uint offset = stream * p.nbk3 + kv * p.nbk2 + d4 * 4; + value = f16vec4(data_k[offset], data_k[offset + 1], data_k[offset + 2], data_k[offset + 3]); + } + k_sh[key * TILE_STRIDE + d4] = value; + } + barrier(); + + for (uint head = 0; head < N_HEAD; ++head) { + for (uint idx = tid; idx < TILE * VEC_PER_HEAD; idx += SUBGROUP_SIZE) { + const uint token_local = idx / VEC_PER_HEAD; + const uint d4 = idx % VEC_PER_HEAD; + const uint token = token_base + token_local; + f16vec4 value = f16vec4(0.0); + if (token < p.n_batch) { + const uint offset = stream * p.nbq3 + token * p.nbq2 + head * p.nbq1 + d4 * 4; + value = f16vec4(data_q[offset], data_q[offset + 1], data_q[offset + 2], data_q[offset + 3]); + } + q_sh[token_local * TILE_STRIDE + d4] = value; + } + barrier(); + + coopmat scores = + coopmat(0.0); + coopmat kmat; + coopmat qmat; + + [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { + coopMatLoad(kmat, k_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + coopMatLoad(qmat, q_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutColumnMajor); + scores = coopMatMulAdd(kmat, qmat, scores); + } + + coopMatStore(scores, score_sh, 0, SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + barrier(); + + [[unroll]] for (uint i = 0; i < 4; ++i) { + const uint idx = tid + i * SUBGROUP_SIZE; + const uint key = idx / TILE; + const uint token_local = idx % TILE; + const uint token = token_base + token_local; + if (token < p.n_batch && kv_base + key < p.n_kv) { + const float score = score_sh[key * SCORE_STRIDE + token_local / 4][token_local % 4]; + const float weight = data_w[stream * p.nbw3 + token * p.nbw1 + head]; + totals[i] += max(score, 0.0) * weight; + } + } + barrier(); + } + + [[unroll]] for (uint i = 0; i < 4; ++i) { + const uint idx = tid + i * SUBGROUP_SIZE; + const uint key = idx / TILE; + const uint token_local = idx % TILE; + const uint kv = kv_base + key; + const uint token = token_base + token_local; + if (kv < p.n_kv && token < p.n_batch) { + const uint mask_base = (stream % p.nem3) * p.nbm3 + token * p.nbm1; + const uint dst_base = stream * p.nb3 + token * p.nb1; + data_dst[dst_base + kv] = totals[i] + float(data_m[mask_base + kv]); + } + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp new file mode 100644 index 000000000000..fd555c76806d --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp @@ -0,0 +1,110 @@ +#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_cooperative_matrix : require +#extension GL_KHR_memory_scope_semantics : require + +layout(constant_id = 0) const uint SUBGROUP_SIZE = 64; +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer QBuf { float data_q[]; }; +layout(binding = 1) readonly buffer KBuf { float16_t data_k[]; }; +layout(binding = 2) readonly buffer WBuf { float data_w[]; }; +layout(binding = 3) readonly buffer MBuf { float16_t data_m[]; }; +layout(binding = 4) writeonly buffer DstBuf { float data_dst[]; }; + +layout(push_constant) uniform Parameters { + uint n_kv; + uint n_batch; + uint n_stream; + uint nem3; + uint nb1; + uint nb3; + uint nbq1; + uint nbq2; + uint nbq3; + uint nbk2; + uint nbk3; + uint nbw1; + uint nbw3; + uint nbm1; + uint nbm3; +} p; + +const uint TILE = 16; +const uint HEAD_SIZE = 128; +const uint N_HEAD = 64; +const uint VEC_PER_HEAD = HEAD_SIZE / 4; +const uint TILE_STRIDE = VEC_PER_HEAD + 2; +const uint SCORE_STRIDE = TILE / 4 + 1; + +shared f16vec4 q_sh[TILE * TILE_STRIDE]; +shared f16vec4 k_sh[TILE * TILE_STRIDE]; +shared vec4 score_sh[TILE * SCORE_STRIDE]; + +void main() { + const uint tid = gl_LocalInvocationIndex; + const uint kv_base = gl_WorkGroupID.x * TILE; + const uint token = gl_WorkGroupID.y; + const uint stream = gl_WorkGroupID.z; + + for (uint idx = tid; idx < TILE * VEC_PER_HEAD; idx += SUBGROUP_SIZE) { + const uint key = idx / VEC_PER_HEAD; + const uint d4 = idx % VEC_PER_HEAD; + const uint kv = kv_base + key; + f16vec4 value = f16vec4(0.0); + if (kv < p.n_kv) { + const uint offset = stream * p.nbk3 + kv * p.nbk2 + d4 * 4; + value = f16vec4(data_k[offset], data_k[offset + 1], data_k[offset + 2], data_k[offset + 3]); + } + k_sh[key * TILE_STRIDE + d4] = value; + } + barrier(); + + float total = 0.0; + for (uint head_base = 0; head_base < N_HEAD; head_base += TILE) { + for (uint idx = tid; idx < TILE * VEC_PER_HEAD; idx += SUBGROUP_SIZE) { + const uint head_local = idx / VEC_PER_HEAD; + const uint d4 = idx % VEC_PER_HEAD; + const uint head = head_base + head_local; + const uint offset = stream * p.nbq3 + token * p.nbq2 + head * p.nbq1 + d4 * 4; + q_sh[head_local * TILE_STRIDE + d4] = + f16vec4(data_q[offset], data_q[offset + 1], data_q[offset + 2], data_q[offset + 3]); + } + barrier(); + + coopmat scores = + coopmat(0.0); + coopmat kmat; + coopmat qmat; + + [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { + coopMatLoad(kmat, k_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + coopMatLoad(qmat, q_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutColumnMajor); + scores = coopMatMulAdd(kmat, qmat, scores); + } + + coopMatStore(scores, score_sh, 0, SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + barrier(); + + if (tid < TILE && kv_base + tid < p.n_kv) { + [[unroll]] for (uint head_local = 0; head_local < TILE; ++head_local) { + const float score = score_sh[tid * SCORE_STRIDE + head_local / 4][head_local % 4]; + const float weight = data_w[stream * p.nbw3 + token * p.nbw1 + head_base + head_local]; + total += max(score, 0.0) * weight; + } + } + barrier(); + } + + if (tid < TILE) { + const uint kv = kv_base + tid; + if (kv < p.n_kv) { + const uint mask_base = (stream % p.nem3) * p.nbm3 + token * p.nbm1; + const uint dst_base = stream * p.nb3 + token * p.nb1; + data_dst[dst_base + kv] = total + float(data_m[mask_base + kv]); + } + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_scalar64.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_scalar64.comp new file mode 100644 index 000000000000..693bb3ece8e8 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_scalar64.comp @@ -0,0 +1,92 @@ +#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_arithmetic : require +#extension GL_KHR_shader_subgroup_basic : require + +layout(constant_id = 0) const uint SUBGROUP_SIZE = 64; +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer QBuf { float data_q[]; }; +layout(binding = 1) readonly buffer KBuf { float16_t data_k[]; }; +layout(binding = 2) readonly buffer WBuf { float data_w[]; }; +layout(binding = 3) readonly buffer MaskBuf { float16_t data_m[]; }; +layout(binding = 4) writeonly buffer DstBuf { float data_dst[]; }; + +layout(push_constant) uniform Parameters { + uint n_kv; + uint n_batch; + uint n_stream; + uint nem3; + uint nb1; + uint nb3; + uint nbq1; + uint nbq2; + uint nbq3; + uint nbk2; + uint nbk3; + uint nbw1; + uint nbw3; + uint nbm1; + uint nbm3; +} p; + +const uint K_PER_GROUP = 8; +const uint N_HEAD = 64; + +void main() { + const uint lane = gl_SubgroupInvocationID; + const uint token = gl_WorkGroupID.y; + const uint stream = gl_WorkGroupID.z; + const uint kv_base = gl_WorkGroupID.x * K_PER_GROUP; + + if (token >= p.n_batch || stream >= p.n_stream) { + return; + } + + float k0[K_PER_GROUP]; + float k1[K_PER_GROUP]; + [[unroll]] for (uint j = 0; j < K_PER_GROUP; ++j) { + const uint kv = kv_base + j; + if (kv < p.n_kv) { + const uint k_base = stream * p.nbk3 + kv * p.nbk2; + k0[j] = float(data_k[k_base + lane]); + k1[j] = float(data_k[k_base + lane + SUBGROUP_SIZE]); + } else { + k0[j] = 0.0; + k1[j] = 0.0; + } + } + + float score[K_PER_GROUP]; + [[unroll]] for (uint j = 0; j < K_PER_GROUP; ++j) { + score[j] = 0.0; + } + + for (uint head = 0; head < N_HEAD; ++head) { + const uint q_base = stream * p.nbq3 + token * p.nbq2 + head * p.nbq1; + const float q0 = data_q[q_base + lane]; + const float q1 = data_q[q_base + lane + SUBGROUP_SIZE]; + const float weight = data_w[stream * p.nbw3 + token * p.nbw1 + head]; + + [[unroll]] for (uint j = 0; j < K_PER_GROUP; ++j) { + const float qk = subgroupAdd(q0 * k0[j] + q1 * k1[j]); + if (lane == 0) { + score[j] += max(qk, 0.0) * weight; + } + } + } + + if (lane == 0) { + const uint mask_base = (stream % p.nem3) * p.nbm3 + token * p.nbm1; + const uint dst_base = stream * p.nb3 + token * p.nb1; + [[unroll]] for (uint j = 0; j < K_PER_GROUP; ++j) { + const uint kv = kv_base + j; + if (kv < p.n_kv) { + data_dst[dst_base + kv] = score[j] + float(data_m[mask_base + kv]); + } + } + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 81e556b0c78e..9b485311baa8 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -811,6 +811,13 @@ void process_shaders() { string_to_spv("get_rows_i32", "get_rows.comp", {{"TEMP_TYPE", "uint"}, {"A_TYPE", "uint"}, {"B_TYPE", "int"}, {"D_TYPE", "uint"}}); + string_to_spv("lightning_indexer_f16", "lightning_indexer_scalar64.comp", {}); +#if defined(GGML_VULKAN_COOPMAT_GLSLC_SUPPORT) + string_to_spv("lightning_indexer_cm_f16", "lightning_indexer_cm.comp", {}); + string_to_spv("lightning_indexer_decode_cm_f16", "lightning_indexer_decode_cm.comp", {}); +#endif + string_to_spv("flash_attn_top_k_f16", "flash_attn_top_k.comp", {}); + string_to_spv("mul_mat_vec_p021_f16_f32_subgroup_add", "mul_mat_vec_p021.comp", {{"A_TYPE", "float16_t"}, {"A_TYPEV4", "f16vec4"}, {"B_TYPE", "float"}, {"B_TYPEV4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}}); string_to_spv("mul_mat_vec_p021_f16_f32", "mul_mat_vec_p021.comp", {{"A_TYPE", "float16_t"}, {"A_TYPEV4", "f16vec4"}, {"B_TYPE", "float"}, {"B_TYPEV4", "vec4"}, {"D_TYPE", "float"}}); string_to_spv("mul_mat_vec_nc_f16_f32", "mul_mat_vec_nc.comp", {{"A_TYPE", "float16_t"}, {"A_TYPEV4", "f16vec4"}, {"B_TYPE", "float"}, {"B_TYPEV4", "vec4"}, {"D_TYPE", "float"}}); diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c index 5fcb6c2f300a..c5093a039aa5 100644 --- a/ggml/src/ggml.c +++ b/ggml/src/ggml.c @@ -5597,6 +5597,20 @@ void ggml_flash_attn_ext_add_sinks( a->src[4] = sinks; } +void ggml_flash_attn_ext_add_top_k( + struct ggml_tensor * a, + struct ggml_tensor * top_k, + int64_t n_kv_raw) { + GGML_ASSERT(a->op == GGML_OP_FLASH_ATTN_EXT); + GGML_ASSERT(a->src[5] == NULL); + GGML_ASSERT(top_k->type == GGML_TYPE_I32); + GGML_ASSERT(top_k->ne[1] == a->src[0]->ne[1]); + GGML_ASSERT(n_kv_raw >= 0 && n_kv_raw <= a->src[1]->ne[1]); + + a->src[5] = top_k; + ggml_set_op_params_i32(a, 4, (int32_t) n_kv_raw); +} + // ggml_flash_attn_back struct ggml_tensor * ggml_flash_attn_back( diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index b137c6cfe8a0..ed56fdbb1040 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -2554,7 +2554,9 @@ ggml_tensor * llm_graph_context::build_attn_mha( ggml_tensor * v_mla, int64_t n_kv_max, float kq_scale, - int il) const { + int il, + ggml_tensor * top_k, + int64_t n_kv_raw) const { const bool v_trans = v->nb[1] > v->nb[2]; // split the batch into streams if needed @@ -2592,6 +2594,9 @@ ggml_tensor * llm_graph_context::build_attn_mha( ggml_flash_attn_ext_add_sinks(cur, sinks); GGML_ASSERT(n_kv_max >= 0 && n_kv_max <= INT32_MAX); ggml_flash_attn_ext_set_n_kv_max(cur, static_cast(n_kv_max)); + if (top_k) { + ggml_flash_attn_ext_add_top_k(cur, top_k, n_kv_raw); + } ggml_flash_attn_ext_set_prec (cur, GGML_PREC_F32); if (v_mla) { diff --git a/src/llama-graph.h b/src/llama-graph.h index dddfdac7b51e..862142b4fd7b 100644 --- a/src/llama-graph.h +++ b/src/llama-graph.h @@ -1173,7 +1173,9 @@ struct llm_graph_context { ggml_tensor * v_mla, // [n_embd_head_v_mla, n_embd_head_v, n_head_v] int64_t n_kv_max, float kq_scale, - int il) const; + int il, + ggml_tensor * top_k = nullptr, + int64_t n_kv_raw = 0) const; llm_graph_input_attn_no_cache * build_attn_inp_no_cache() const; diff --git a/src/models/deepseek4.cpp b/src/models/deepseek4.cpp index 6bf9d3444942..8588d069eed6 100644 --- a/src/models/deepseek4.cpp +++ b/src/models/deepseek4.cpp @@ -758,7 +758,7 @@ ggml_tensor * llama_model_deepseek4::graph::build_csa_lid_attention( cb(kq_mask, "csa_lid_kq_mask", il); const int64_t n_kv_max = std::min(raw_mask->ne[0], hparams.n_swa) + top_k->ne[0]; - ggml_tensor * out = build_attn_mha(q, k_all, k_all, nullptr, kq_mask, sinks, nullptr, n_kv_max, kq_scale, il); + ggml_tensor * out = build_attn_mha(q, k_all, k_all, nullptr, kq_mask, sinks, nullptr, n_kv_max, kq_scale, il, top_k, raw_k->ne[2]); if (k_rot) { out = llama_mul_mat_hadamard(ctx0, out, k_rot); } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 5b7893d45129..13d8ae928fd1 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11260,6 +11260,9 @@ static std::vector> make_test_cases_eval() { } } } + test_cases.emplace_back(new test_lightning_indexer(128, 64, 257, 1, 1, 1, GGML_TYPE_F16)); + test_cases.emplace_back(new test_lightning_indexer(128, 64, 257, 17, 1, 1, GGML_TYPE_F16)); + test_cases.emplace_back(new test_lightning_indexer(128, 64, 512, 512, 1, 1, GGML_TYPE_F16)); for (int kv : { 1, 7, 8, 63, 64, 65 }) { for (ggml_type type_K : {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}) { @@ -11302,6 +11305,11 @@ static std::vector> make_test_cases_perf() { GGML_TYPE_F32, {n_kv, 512, 64, 1}, false, {2, 1, 0, 3})); } + for (ggml_type type_a : { GGML_TYPE_IQ2_XS, GGML_TYPE_IQ3_XXS }) { + test_cases.emplace_back(new test_mul_mat_id(type_a, GGML_TYPE_F32, 256, 6, false, 2048, 512, 4096)); + test_cases.emplace_back(new test_mul_mat_id(type_a, GGML_TYPE_F32, 256, 6, false, 4096, 512, 2048)); + } + // Conv2d: K=CRS=NPQ=4096 matmul performance uint32_t iwh_idx = 0; uint32_t kwh_idx = 1; @@ -11740,7 +11748,7 @@ static std::vector> make_test_cases_perf() { test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 2048, 1, 1, false, true)); // lightning_indexer - for (int kv : { 256, 4096, 65536 }) { + for (int kv : { 256, 512, 4096, 65536 }) { for (int bs : { 1, 512, 2048 }) { for (int nh : { 32, 64 }) { for (int ns : { 1, 4 }) { From 56a1843fd9b8f4737ec4ac162a6f1a76a1f5affa Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sat, 1 Aug 2026 13:30:31 +0000 Subject: [PATCH 099/212] vulkan: harden the sparse-FA shader and document the top-k API - flash_attn_top_k.comp: remove the dead bounds check that would skip barriers for part of the workgroup if it ever fired (barrier divergence is UB; the dispatch gate sizes the grid exactly), pin per-lane sizing to LANES=64 instead of the SUBGROUP_SIZE spec constant (array bounds fold from the spec default at compile time), name the f16 mask threshold constant - ggml.h: document ggml_flash_attn_ext_add_top_k semantics (index base, dense prefix, backends-may-ignore contract) - tests: cover the scalar indexer variant (batch 4 and boundary 15), previously only the cm and decode-cm variants had eval parity cases Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/include/ggml.h | 5 +++ .../vulkan-shaders/flash_attn_top_k.comp | 36 ++++++++++++------- tests/test-backend-ops.cpp | 4 +++ 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h index 2ff12d00295d..fb83a874e877 100644 --- a/ggml/include/ggml.h +++ b/ggml/include/ggml.h @@ -2486,6 +2486,11 @@ extern "C" { struct ggml_tensor * a, struct ggml_tensor * sinks); + // sparse attention hint: attend only to the first n_kv_raw keys (dense prefix) plus the + // keys selected by top_k. top_k is I32 [n_top_k, n_tokens, 1, n_streams]; each index i + // selects absolute key n_kv_raw + i. Negative or out-of-range indices are ignored. + // Backends may ignore the hint: the kq_mask must still encode the same selection, so a + // dense fallback computes the identical result. GGML_API void ggml_flash_attn_ext_add_top_k( struct ggml_tensor * a, struct ggml_tensor * top_k, diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp index 7f82266781f1..b8b49c677bd5 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp @@ -39,9 +39,18 @@ layout(push_constant) uniform Parameters { uint has_sinks; } p; +// Shape constants pinned by the dispatch gate in ggml_vk_flash_attn_top_k: DeepSeek V4 +// CSA attention only (hd 512, 64 heads MQA over an f16 K==V latent). The gate also pins +// the pipeline to subgroup size 64, so per-lane sizing uses LANES rather than the +// SUBGROUP_SIZE specialization constant (a spec-constant array bound would be folded at +// compile time from the default and silently break under a different runtime subgroup). const uint HEAD_SIZE = 512; const uint HEADS_PER_GROUP = 8; const uint KEYS_PER_BLOCK = 16; +const uint LANES = 64; +// f16 -inf (or the lowest f16 normal some mask writers use in its place) marks a +// masked-out key. +const float MASK_NEG_INF = -65500.0; shared float16_t key_sh[KEYS_PER_BLOCK * HEAD_SIZE]; shared uint key_idx[KEYS_PER_BLOCK]; @@ -53,12 +62,13 @@ void main() { const uint token = gl_WorkGroupID.x; const uint stream = gl_WorkGroupID.z; - if (token >= p.n_batch || head >= p.n_head) { - return; - } + // No bounds check: the grid is sized exactly (x = n_batch, y * HEADS_PER_GROUP covers + // n_head == 64). An early return here would skip the barriers below for part of the + // workgroup, which is undefined behavior — do not reintroduce one without restructuring + // the barrier flow. - float accum[HEAD_SIZE / SUBGROUP_SIZE]; - [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { + float accum[HEAD_SIZE / LANES]; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { accum[i] = 0.0; } @@ -101,13 +111,13 @@ void main() { } float partial = 0.0; - [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { - const uint dim = lane + i * SUBGROUP_SIZE; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { + const uint dim = lane + i * LANES; partial += data_q[q_base + dim] * float(key_sh[col * HEAD_SIZE + dim]); } const float mask = float(data_m[mask_base + key]); const float score = subgroupAdd(partial) * p.scale + mask; - if (mask < -65500.0) { + if (mask < MASK_NEG_INF) { continue; } @@ -117,8 +127,8 @@ void main() { row_sum = row_sum * old_scale + value_scale; row_max = new_max; - [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { - const uint dim = lane + i * SUBGROUP_SIZE; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { + const uint dim = lane + i * LANES; accum[i] = accum[i] * old_scale + value_scale * float(key_sh[col * HEAD_SIZE + dim]); } } @@ -131,14 +141,14 @@ void main() { const float old_scale = row_sum == 0.0 ? 0.0 : exp(row_max - new_max); const float sink_scale = exp(sink - new_max); row_sum = row_sum * old_scale + sink_scale; - [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { accum[i] *= old_scale; } } const uint dst_base = stream * p.nb3 + token * p.nb2 + head * p.nb1; const float inv_sum = row_sum == 0.0 ? 0.0 : 1.0 / row_sum; - [[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) { - data_dst[dst_base + lane + i * SUBGROUP_SIZE] = accum[i] * inv_sum; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { + data_dst[dst_base + lane + i * LANES] = accum[i] * inv_sum; } } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 13d8ae928fd1..16a8e382356b 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11260,7 +11260,11 @@ static std::vector> make_test_cases_eval() { } } } + // batch 1 = Vulkan decode-cm variant, 4/15 = scalar subgroup variant (below the cm + // threshold of 16, 15 is the boundary), 17/512 = cm prefill variant test_cases.emplace_back(new test_lightning_indexer(128, 64, 257, 1, 1, 1, GGML_TYPE_F16)); + test_cases.emplace_back(new test_lightning_indexer(128, 64, 257, 4, 1, 1, GGML_TYPE_F16)); + test_cases.emplace_back(new test_lightning_indexer(128, 64, 257, 15, 1, 1, GGML_TYPE_F16)); test_cases.emplace_back(new test_lightning_indexer(128, 64, 257, 17, 1, 1, GGML_TYPE_F16)); test_cases.emplace_back(new test_lightning_indexer(128, 64, 512, 512, 1, 1, GGML_TYPE_F16)); From 79149d1bacf0ae127a6ee5e1d2249228f1f80e3e Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sat, 1 Aug 2026 14:55:05 +0000 Subject: [PATCH 100/212] tests: sparse top-k FA parity + perf coverage (V4 CSA shape) Adds test_flash_attn_ext_top_k: builds the DeepSeek V4 CSA attention shape (hd 512, 64-head MQA, V as a view of K) with a consistent per-token top-k/mask pair, one deliberately invalid index, and cases on both sides of the Vulkan engagement gates. nb >= 64 cases are the first numerical parity coverage the sparse prefill shader has had; nb < 64 and sub-3x-kv cases pin the dense-fallback contract. Perf cases sweep kv 8k/32k/64k at nb 1/8/64/512 with a fixed active set. Measured on gfx1151: the sparse shader is flat vs kv at prefill (~2.2 TFLOPS active-only) while nb < 64 falls back to dense and scales with kv (1326 us at 64k, nb=1) - the gap a sparse decode path needs to close. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- tests/test-backend-ops.cpp | 134 +++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 16a8e382356b..248fbd0555f7 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -8028,6 +8028,117 @@ struct test_flash_attn_ext : public test_case { } }; +// GGML_OP_FLASH_ATTN_EXT with a top-k sparse selection hint (DeepSeek V4 CSA shape). +// The kq_mask encodes the same selection as the top_k indices, so a backend that ignores +// the hint (CPU) computes the identical result densely — this is exactly the contract +// that keeps sparse and dense paths interchangeable, and what this test verifies. +struct test_flash_attn_ext_top_k : public test_case { + const int64_t kv; // total KV size (compressed region + dense prefix) + const int64_t nb; // batch size (query tokens) + const int64_t n_kv_raw; // dense prefix always attended + const int64_t n_top_k; // selected keys per query token + const bool sinks; + + static constexpr int64_t hs = 512; // V4 CSA head size, K == V latent + static constexpr int64_t nh = 64; // V4 CSA query heads (MQA) + + std::string vars() override { + return VARS_TO_STR5(kv, nb, n_kv_raw, n_top_k, sinks); + } + + double max_nmse_err() override { + return 5e-4; + } + + uint64_t op_flops(ggml_tensor * t) override { + GGML_UNUSED(t); + // only the active keys contribute compute on a sparse backend; count those so + // perf mode reports the useful-work rate + return 2 * nh * nb * (hs + hs) * (n_kv_raw + n_top_k); + } + + test_flash_attn_ext_top_k(int64_t kv = 768, int64_t nb = 8, int64_t n_kv_raw = 64, int64_t n_top_k = 128, bool sinks = false) + : kv(kv), nb(nb), n_kv_raw(n_kv_raw), n_top_k(n_top_k), sinks(sinks) {} + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * q = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, hs, nb, nh, 1); + ggml_set_name(q, "q"); + + ggml_tensor * k = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, hs, kv, 1, 1); + ggml_set_name(k, "k"); + + // V4 CSA attends over the K latent itself: V is the same cache tensor + ggml_tensor * v = ggml_view_4d(ctx, k, hs, kv, 1, 1, k->nb[1], k->nb[2], k->nb[3], 0); + ggml_set_name(v, "v"); + + ggml_tensor * m = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, kv, nb, 1, 1); + ggml_set_name(m, "m"); + + ggml_tensor * t = ggml_new_tensor_4d(ctx, GGML_TYPE_I32, n_top_k, nb, 1, 1); + ggml_set_name(t, "top_k"); + + ggml_tensor * s = nullptr; + if (sinks) { + s = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, nh); + ggml_set_name(s, "s"); + } + + ggml_tensor * out = ggml_flash_attn_ext(ctx, q, k, v, m, 1.0f/sqrtf(hs), 0.0f, 0.0f); + ggml_flash_attn_ext_add_sinks(out, s); + ggml_flash_attn_ext_add_top_k(out, t, n_kv_raw); + ggml_flash_attn_ext_set_prec (out, GGML_PREC_F32); + ggml_set_name(out, "out"); + + return out; + } + + void initialize_tensors(ggml_context * ctx) override { + const int64_t range = kv - n_kv_raw; // size of the selectable compressed region + + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { + if (strcmp(t->name, "top_k") == 0 || strcmp(t->name, "m") == 0) { + continue; // filled together below + } + if (strcmp(t->name, "s") == 0) { + init_tensor_uniform(t, -10.0f, 10.0f); + } else { + init_tensor_uniform(t); + } + } + + // build a consistent (top_k, mask) pair: a deterministic per-token selection, + // strided so adjacent tokens select overlapping-but-different keys, with one + // deliberately invalid index (-1) whose mask slot stays -inf + std::vector top(n_top_k * nb); + std::vector mask(kv * nb); + const ggml_fp16_t minus_inf = ggml_fp32_to_fp16(-INFINITY); + const ggml_fp16_t zero = ggml_fp32_to_fp16(0.0f); + + for (int64_t b = 0; b < nb; ++b) { + for (int64_t i = 0; i < kv; ++i) { + mask[b * kv + i] = i < n_kv_raw ? zero : minus_inf; + } + for (int64_t j = 0; j < n_top_k; ++j) { + int32_t idx = (int32_t) ((j * range) / n_top_k + b) % (int32_t) range; + if (j == n_top_k - 1 && b == 0) { + idx = -1; // exercise the ignore-invalid-index path + } else { + mask[b * kv + n_kv_raw + idx] = zero; + } + top[b * n_top_k + j] = idx; + } + } + + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { + if (strcmp(t->name, "top_k") == 0) { + ggml_backend_tensor_set(t, top.data(), 0, top.size() * sizeof(int32_t)); + } else if (strcmp(t->name, "m") == 0) { + ggml_backend_tensor_set(t, mask.data(), 0, mask.size() * sizeof(ggml_fp16_t)); + } + } + } +}; + // GGML_OP_CROSS_ENTROPY_LOSS struct test_cross_entropy_loss : public test_case { const ggml_type type; @@ -11274,6 +11385,18 @@ static std::vector> make_test_cases_eval() { } } + // sparse top-k FA: (kv, nb, n_kv_raw, n_top_k, sinks). The Vulkan sparse path engages + // when kv >= 3*(n_kv_raw + n_top_k) AND nb >= 64 (prefill-only); the nb < 64 cases + // and the kv=512 case verify dense-fallback parity with the hint attached, the + // nb=64/128 cases exercise the sparse shader itself. + test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 1, 256, 512, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 8, 64, 128, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 17, 64, 128, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k( 512, 4, 64, 128, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, true)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 128, 256, 512, false)); + return test_cases; } #ifdef _MSC_VER @@ -11764,6 +11887,17 @@ static std::vector> make_test_cases_perf() { } } + // sparse top-k FA at V4 decode/prefill shapes — the A/B instrument for the + // gather-to-compact work (n_active = n_kv_raw + n_top_k stays fixed as kv grows). + // nb 1/8 currently takes the DENSE path (the sparse shader gates on nb >= 64): + // those rows measure the decode cost gather-to-compact must beat. nb 64/512 + // measures the existing sparse prefill shader. + for (int kv : { 8192, 32768, 65536 }) { + for (int nb : { 1, 8, 64, 512 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, nb, 1024, 512, false)); + } + } + return test_cases; } From 5d1180a63265cc1394d31aaed6d2062a25f65a51 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sat, 1 Aug 2026 15:08:58 +0000 Subject: [PATCH 101/212] vulkan: gather-to-compact sparse decode FA for DeepSeek V4 top-k selection The sparse prefill shader gates on q->ne[1] >= 64, so single-token decode attends densely over the whole compressed KV and its cost grows with context. This adds a gather pass (flash_attn_gather.comp): copy the active rows (dense prefix + top-k selection; MQA, so all 64 query heads share one set) plus their mask values into a compact contiguous scratch in prealloc_y, then run the ordinary dense FA over the compacted K/V/mask. V is the K latent, so one gather serves both. Invalid indices and padding get zeroed K and -inf mask. The FA function itself only has its inputs swapped: KV, mask geometry, strides and the K/V/mask bindings are overridden up front and every downstream decision (pipeline choice, split-k, workgroup sizing, use_mask_opt) sizes itself to the compact KV unchanged. Engages for the V4 CSA decode shape when kv >= 2x the padded active set; GGML_VK_FA_TOPK_GATHER=0 disables. Measured (gfx1151, test-backend-ops perf, active set 1536): kv=8192 nb=1: 255.6 us -> 55.8 us (4.6x) kv=32768 nb=1: 986.4 us -> 56.1 us (17.6x) kv=65536 nb=1: 1333.8 us -> 58.7 us (22.7x) Time is flat vs context. nb>1 still falls back to dense pending a union gather. FLASH_ATTN_EXT eval suite green incl. the top-k parity cases (the kv=4096 nb=1 case exercises this path end-to-end vs CPU). Co-Authored-By: Claude Fable 5 Fold-in note for the toolbox branch: use_dequant_kv is additionally gated on !fa_compact.active (the compact scratch is already contiguous f16, and the two scratch layers must not stack), and the compact stride overrides chain through the toolbox's nb*_eff values so the contiguize/dequant path keeps its strides when the gather is inactive. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 143 +++++++++++++++++- .../vulkan-shaders/flash_attn_gather.comp | 70 +++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 1 + 3 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 44efa5c4897c..012b18a44ba1 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1108,6 +1108,7 @@ struct vk_device_struct { vk_pipeline pipeline_lightning_indexer_cm_f16; vk_pipeline pipeline_lightning_indexer_decode_cm_f16; vk_pipeline pipeline_flash_attn_top_k_f16; + vk_pipeline pipeline_flash_attn_gather_f16; vk_pipeline pipeline_ssm_scan_f32_d128; vk_pipeline pipeline_ssm_scan_f32_d256; vk_pipeline pipeline_ssm_conv_f32; @@ -1948,6 +1949,12 @@ struct vk_op_lightning_indexer_cm_push_constants { }; static_assert(sizeof(vk_op_lightning_indexer_cm_push_constants) <= 128); +struct vk_op_flash_attn_gather_push_constants { + uint32_t n_kv, n_kv_raw, n_top_k, kv_c; + uint32_t nbk1, nbk3, nbt3, nbm3, nem3; +}; +static_assert(sizeof(vk_op_flash_attn_gather_push_constants) <= 128); + struct vk_op_flash_attn_top_k_push_constants { uint32_t n_batch, n_kv, n_kv_raw, n_top_k, n_head; uint32_t nbq1, nbq2, nbq3; @@ -6253,6 +6260,10 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { "flash_attn_top_k_f16", flash_attn_top_k_f16_len, flash_attn_top_k_f16_data, "main", 6, sizeof(vk_op_flash_attn_top_k_push_constants), {1, 1, 1}, {512, device->subgroup_size}, 1, true, true, device->subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_flash_attn_gather_f16, + "flash_attn_gather_f16", flash_attn_gather_f16_len, flash_attn_gather_f16_data, "main", 5, + sizeof(vk_op_flash_attn_gather_push_constants), {1, 1, 1}, {}, 1, true, true, + device->subgroup_size); } if (device->subgroup_arithmetic && device->subgroup_require_full_support) { @@ -11430,6 +11441,95 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & return true; } +struct vk_fa_compact_state { + bool active = false; + uint32_t kv_c = 0; + vk_subbuffer kc_buf, mc_buf; +}; + +// V4 sparse decode (gather-to-compact): the sparse prefill shader above gates on +// q->ne[1] >= 64, so single-token decode otherwise attends densely over the whole +// compressed KV, at a cost that grows with context. Instead, gather the active rows +// (dense prefix + top-k selection; MQA, so all query heads share one set) into a +// compact contiguous scratch in prealloc_y, and let the ordinary dense FA below run +// over the compacted K/V/mask. Correct by the same contract as the sparse shader: +// the source mask carries the selection, and the gathered mask preserves it. +static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_context & subctx, + const ggml_tensor * q, const ggml_tensor * k, const ggml_tensor * v, + const ggml_tensor * mask, ggml_tensor * dst, vk_fa_compact_state & st) { + const ggml_tensor * top_k = dst->src[5]; + static const char * gather_env = getenv("GGML_VK_FA_TOPK_GATHER"); + if ((gather_env && gather_env[0] == '0') || + !top_k || !ctx->device->pipeline_flash_attn_gather_f16 || + q->ne[1] != 1 || // single-token decode only; batched queries need a union gather + q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || v->type != GGML_TYPE_F16 || + !mask || mask->type != GGML_TYPE_F16 || top_k->type != GGML_TYPE_I32 || + q->ne[0] != 512 || k->ne[0] != 512 || v->ne[0] != 512 || q->ne[2] != 64 || + k->ne[2] != 1 || v->ne[2] != 1 || + q->ne[1] != top_k->ne[1] || q->ne[3] != top_k->ne[3] || + k->ne[1] != v->ne[1] || k->buffer != v->buffer || k->data != v->data || + !ggml_is_contiguous(mask) || !ggml_is_contiguous(top_k)) { + return false; + } + + float max_bias = 0.0f; + float logit_softcap = 0.0f; + memcpy(&max_bias, (const float *) dst->op_params + 1, sizeof(float)); + memcpy(&logit_softcap, (const float *) dst->op_params + 2, sizeof(float)); + if (max_bias != 0.0f || logit_softcap != 0.0f) { + return false; + } + + const int32_t n_kv_raw = ggml_get_op_params_i32(dst, 4); + if (n_kv_raw < 0 || n_kv_raw > k->ne[1] || top_k->ne[0] > k->ne[1] - n_kv_raw) { + return false; + } + + const uint32_t kv_c = GGML_PAD((uint32_t)(n_kv_raw + top_k->ne[0]), 256u); + // the gather writes then re-reads ~the active bytes; dense reads the source KV once, + // so compaction only pays when the source is comfortably larger than the active set + if ((uint64_t) k->ne[1] < 2ull * kv_c) { + return false; + } + + const uint32_t ns = (uint32_t) q->ne[3]; + const size_t kc_sz = (size_t) ns * kv_c * 512 * sizeof(ggml_fp16_t); + const size_t mc_sz = (size_t) ns * kv_c * sizeof(ggml_fp16_t); + + if (ctx->prealloc_size_y < kc_sz + mc_sz) { + ctx->prealloc_size_y = kc_sz + mc_sz; + ggml_vk_preallocate_buffers(ctx, subctx); + } + if (ctx->prealloc_y_need_sync) { + ggml_vk_sync_buffers(ctx, subctx); + } + + vk_pipeline pipeline = ctx->device->pipeline_flash_attn_gather_f16; + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + + const vk_op_flash_attn_gather_push_constants pc = { + (uint32_t) k->ne[1], (uint32_t) n_kv_raw, (uint32_t) top_k->ne[0], kv_c, + (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)), + (uint32_t) (k->nb[3] / sizeof(ggml_fp16_t)), + (uint32_t) (top_k->nb[3] / sizeof(int32_t)), + (uint32_t) (mask->nb[3] / sizeof(ggml_fp16_t)), + (uint32_t) mask->ne[3], + }; + + st.kc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0); + st.mc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, kc_sz); + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, + { ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, top_k), + ggml_vk_tensor_subbuffer(ctx, mask), st.kc_buf, st.mc_buf }, + pc, { kv_c, 1, ns }); + ggml_vk_sync_buffers(ctx, subctx); + ctx->prealloc_y_need_sync = true; + + st.active = true; + st.kv_c = kv_c; + return true; +} + static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * q, const ggml_tensor * k, const ggml_tensor * v, const ggml_tensor * mask, const ggml_tensor * sinks, ggml_tensor * dst) { VK_LOG_DEBUG("ggml_vk_flash_attn((" << q << ", name=" << q->name << ", type=" << q->type << ", ne0=" << q->ne[0] << ", ne1=" << q->ne[1] << ", ne2=" << q->ne[2] << ", ne3=" << q->ne[3] << ", nb0=" << q->nb[0] << ", nb1=" << q->nb[1] << ", nb2=" << q->nb[2] << ", nb3=" << q->nb[3]; std::cerr << "), (" << k << ", name=" << k->name << ", type=" << k->type << ", ne0=" << k->ne[0] << ", ne1=" << k->ne[1] << ", ne2=" << k->ne[2] << ", ne3=" << k->ne[3] << ", nb0=" << k->nb[0] << ", nb1=" << k->nb[1] << ", nb2=" << k->nb[2] << ", nb3=" << k->nb[3]; @@ -11449,15 +11549,15 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx GGML_TENSOR_LOCALS(int64_t, ne, dst, ne) GGML_TENSOR_LOCALS(size_t, nb, dst, nb) - const uint32_t nem0 = mask ? mask->ne[0] : 0; - const uint32_t nem1 = mask ? mask->ne[1] : 0; - const uint32_t nem2 = mask ? mask->ne[2] : 0; - const uint32_t nem3 = mask ? mask->ne[3] : 0; + uint32_t nem0 = mask ? mask->ne[0] : 0; + uint32_t nem1 = mask ? mask->ne[1] : 0; + uint32_t nem2 = mask ? mask->ne[2] : 0; + uint32_t nem3 = mask ? mask->ne[3] : 0; const uint32_t HSK = nek0; const uint32_t HSV = nev0; uint32_t N = neq1; - const uint32_t KV = nek1; + uint32_t KV = nek1; GGML_ASSERT(ne0 == HSV); GGML_ASSERT(ne2 == N); @@ -11484,6 +11584,17 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx if (ggml_vk_flash_attn_top_k(ctx, subctx, q, k, v, mask, sinks, dst)) { return; } + // V4 sparse decode: gather the active set into a compact scratch and run the dense + // FA below on it. Overrides KV, the mask geometry, and (further down) the K/V/mask + // bindings and strides; every other decision then sizes itself to the compact KV. + vk_fa_compact_state fa_compact; + if (ggml_vk_flash_attn_gather_compact(ctx, subctx, q, k, v, mask, dst, fa_compact)) { + KV = fa_compact.kv_c; + nem0 = fa_compact.kv_c; + nem1 = N; + nem2 = 1; + nem3 = (uint32_t) q->ne[3]; + } uint32_t gqa_ratio = 1; uint32_t qk_ratio = neq2 / nek2; uint32_t workgroups_x = (uint32_t)neq1; @@ -11530,6 +11641,9 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx const bool kv_needs_dequant = !ggml_vk_fa_kv_native(k->type, ctx->device->coopmat2) || !ggml_vk_fa_kv_native(v->type, ctx->device->coopmat2); const bool use_dequant_kv = !fa_dequant_off && + // the gather-to-compact scratch is already contiguous f16; the + // dequant/contiguize pass must not run on top of it + !fa_compact.active && ((k_quant && v_quant) || kv_needs_dequant || (fa_kv_contig && kv_f16_strided)) && neq1 >= 64 && is_dense_kv_cache(k) && is_dense_kv_cache(v) && kv_f16_sz <= ctx->device->properties.limits.maxStorageBufferRange && @@ -11568,6 +11682,10 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx const uint32_t q_stride = (uint32_t)(nbq1 / ggml_type_size(q->type)); uint32_t k_stride = (uint32_t)(nbk1 / ggml_type_size(k->type)); uint32_t v_stride = (uint32_t)(nbv1 / ggml_type_size(v->type)); + if (fa_compact.active) { + k_stride = 512; + v_stride = 512; + } // For F32, the shader treats it as a block of size 4 (for vec4 loads) if (k->type == GGML_TYPE_F32) { @@ -11715,6 +11833,11 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx vk_subbuffer v_buf = ggml_vk_tensor_subbuffer(ctx, v); vk_subbuffer dst_buf = ggml_vk_tensor_subbuffer(ctx, dst); vk_subbuffer mask_buf = mask ? ggml_vk_tensor_subbuffer(ctx, mask) : q_buf; + if (fa_compact.active) { + k_buf = fa_compact.kc_buf; + v_buf = fa_compact.kc_buf; // V is the K latent; one gather serves both + mask_buf = fa_compact.mc_buf; + } vk_subbuffer sinks_buf = sinks ? ggml_vk_tensor_subbuffer(ctx, sinks) : q_buf; vk_subbuffer mask_opt_buf = use_mask_opt ? ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0) : q_buf; @@ -11773,6 +11896,12 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx ggml_vk_sync_buffers(ctx, subctx); } + // compact scratch layout: [512, kv_c, 1, ns] f16, tightly packed + const uint32_t eff_nbk2 = fa_compact.active ? fa_compact.kv_c * 512 * (uint32_t)sizeof(ggml_fp16_t) : nbk2_eff; + const uint32_t eff_nbk3 = fa_compact.active ? fa_compact.kv_c * 512 * (uint32_t)sizeof(ggml_fp16_t) : nbk3_eff; + const uint32_t eff_nbv2 = fa_compact.active ? eff_nbk2 : nbv2_eff; + const uint32_t eff_nbv3 = fa_compact.active ? eff_nbk3 : nbv3_eff; + const vk_flash_attn_push_constants pc = { N, KV, (uint32_t)ne1, (uint32_t)ne2, (uint32_t)ne3, (uint32_t)neq2, (uint32_t)neq3, @@ -11780,8 +11909,8 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx (uint32_t)nev2, (uint32_t)nev3, nem1, nem2, nem3, q_stride, (uint32_t)nbq2, (uint32_t)nbq3, - k_stride, nbk2_eff, nbk3_eff, - v_stride, nbv2_eff, nbv3_eff, + k_stride, eff_nbk2, eff_nbk3, + v_stride, eff_nbv2, eff_nbv3, scale, max_bias, logit_softcap, mask_n_head_log2, m0, m1, gqa_ratio, split_kv, split_k }; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp new file mode 100644 index 000000000000..4e3dc1c42623 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp @@ -0,0 +1,70 @@ +#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 + +// Gathers the active KV rows of a top-k sparse attention (DeepSeek V4 CSA decode) into a +// compact contiguous scratch: rows [0, n_kv_raw) of the source (the dense prefix), then the +// n_top_k selected rows, then zero padding up to kv_c. The gathered mask row keeps the +// per-key mask values so causality/validity survive compaction; invalid top-k indices and +// padding get -inf mask and zeroed K (softmax-neutral either way, zeroed so no NaN*0). +// One workgroup per compact row; V is the K latent (V==K), so a single gather serves both. + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer KBuf { float16_t data_k[]; }; +layout(binding = 1) readonly buffer TopBuf { int data_top[]; }; +layout(binding = 2) readonly buffer MBuf { float16_t data_m[]; }; +layout(binding = 3) writeonly buffer KcBuf { float16_t data_kc[]; }; +layout(binding = 4) writeonly buffer McBuf { float16_t data_mc[]; }; + +layout(push_constant) uniform Parameters { + uint n_kv; // total source KV rows + uint n_kv_raw; // dense prefix length + uint n_top_k; // selected rows for the (single) query token + uint kv_c; // padded compact row count == dispatch row range + uint nbk1; // K source row stride, elements + uint nbk3; // K source stream stride, elements + uint nbt3; // top_k stream stride, elements + uint nbm3; // mask source stream stride, elements + uint nem3; // mask ne[3], for stream broadcast +} p; + +const uint HEAD_SIZE = 512; +const uint LANES = 64; + +void main() { + const uint row = gl_WorkGroupID.x; + const uint stream = gl_WorkGroupID.z; + const uint tid = gl_LocalInvocationIndex; + + // map compact row -> source row; p.n_kv is the invalid sentinel + uint src = p.n_kv; + if (row < p.n_kv_raw) { + src = row; + } else if (row < p.n_kv_raw + p.n_top_k) { + const int idx = data_top[stream * p.nbt3 + (row - p.n_kv_raw)]; + if (idx >= 0 && uint(idx) < p.n_kv - p.n_kv_raw) { + src = p.n_kv_raw + uint(idx); + } + } + + const uint dst_base = (stream * p.kv_c + row) * HEAD_SIZE; + if (src < p.n_kv) { + const uint src_base = stream * p.nbk3 + src * p.nbk1; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { + data_kc[dst_base + tid + i * LANES] = data_k[src_base + tid + i * LANES]; + } + if (tid == 0) { + data_mc[stream * p.kv_c + row] = data_m[(stream % p.nem3) * p.nbm3 + src]; + } + } else { + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { + data_kc[dst_base + tid + i * LANES] = float16_t(0.0); + } + if (tid == 0) { + data_mc[stream * p.kv_c + row] = float16_t(uintBitsToFloat(0xff800000)); + } + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 9b485311baa8..5c06f8222927 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -817,6 +817,7 @@ void process_shaders() { string_to_spv("lightning_indexer_decode_cm_f16", "lightning_indexer_decode_cm.comp", {}); #endif string_to_spv("flash_attn_top_k_f16", "flash_attn_top_k.comp", {}); + string_to_spv("flash_attn_gather_f16", "flash_attn_gather.comp", {}); string_to_spv("mul_mat_vec_p021_f16_f32_subgroup_add", "mul_mat_vec_p021.comp", {{"A_TYPE", "float16_t"}, {"A_TYPEV4", "f16vec4"}, {"B_TYPE", "float"}, {"B_TYPEV4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}}); string_to_spv("mul_mat_vec_p021_f16_f32", "mul_mat_vec_p021.comp", {{"A_TYPE", "float16_t"}, {"A_TYPEV4", "f16vec4"}, {"B_TYPE", "float"}, {"B_TYPEV4", "vec4"}, {"D_TYPE", "float"}}); From 554519bbfe3251dc7fe7023a9e8bbf82ae10a65b Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 2 Aug 2026 00:04:55 +0000 Subject: [PATCH 102/212] vulkan: fused DeepSeek V4 hyper-connection ops (HC pre / comb / post) Ports ggml-cuda/dsv4-hc.cu to Vulkan: hc_pre (mix the HC input streams down to one embedding), hc_comb (per-token 4x4 stream-mixing matrix - per-source softmax then eps-stabilized alternating column/row sinkhorn normalization, whole matrix in registers, one thread per token), and hc_post (redistribute the layer output back into the streams with the mixed residual). Same launch geometry as the CUDA kernels; plain f32 compute, no subgroup or coopmat requirements, so the pipelines are created unconditionally. The value at decode is dispatch-count collapse: the unfused fallback runs the decomposed graph (measured on gfx1151 config-a partial offload: SUM_ROWS alone 79 dispatches x 39.7us = 3.1ms per graph, plus DIV/MUL/ ADD shares at 4x4 shapes) where the fused form is 3 dispatches per layer. resolve_fused_ops now keeps all three fusions enabled on Vulkan instead of printing 'not supported, set to disabled'. Parity: upstream test-backend-ops DSV4_HC_PRE/COMB/POST cases green vs CPU on first build. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 153 ++++++++++++++++++ .../vulkan-shaders/dsv4_hc_comb.comp | 101 ++++++++++++ .../vulkan-shaders/dsv4_hc_post.comp | 44 +++++ .../vulkan-shaders/dsv4_hc_pre.comp | 37 +++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 3 + 5 files changed, 338 insertions(+) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_comb.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_post.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_pre.comp diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 012b18a44ba1..8a2d6fe99545 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1109,6 +1109,9 @@ struct vk_device_struct { vk_pipeline pipeline_lightning_indexer_decode_cm_f16; vk_pipeline pipeline_flash_attn_top_k_f16; vk_pipeline pipeline_flash_attn_gather_f16; + vk_pipeline pipeline_dsv4_hc_pre_f32; + vk_pipeline pipeline_dsv4_hc_comb_f32; + vk_pipeline pipeline_dsv4_hc_post_f32; vk_pipeline pipeline_ssm_scan_f32_d128; vk_pipeline pipeline_ssm_scan_f32_d256; vk_pipeline pipeline_ssm_conv_f32; @@ -1949,6 +1952,35 @@ struct vk_op_lightning_indexer_cm_push_constants { }; static_assert(sizeof(vk_op_lightning_indexer_cm_push_constants) <= 128); +struct vk_op_dsv4_hc_pre_push_constants { + uint32_t n_embd, hc, nr; + uint32_t sx0, sx1, sx2; + uint32_t sw0, sw1; + uint32_t sd0, sd1; +}; +static_assert(sizeof(vk_op_dsv4_hc_pre_push_constants) <= 128); + +struct vk_op_dsv4_hc_comb_push_constants { + uint32_t n_tokens; + uint32_t sm0, sm1; + uint32_t ss0; + uint32_t sb0; + uint32_t sd0, sd1, sd2; + float eps; + int32_t n_iter; +}; +static_assert(sizeof(vk_op_dsv4_hc_comb_push_constants) <= 128); + +struct vk_op_dsv4_hc_post_push_constants { + uint32_t n_embd, hc, nr; + uint32_t sx0, sx1; + uint32_t sr0, sr1, sr2; + uint32_t sp0, sp1; + uint32_t sc0, sc1, sc2; + uint32_t sd0, sd1, sd2; +}; +static_assert(sizeof(vk_op_dsv4_hc_post_push_constants) <= 128); + struct vk_op_flash_attn_gather_push_constants { uint32_t n_kv, n_kv_raw, n_top_k, kv_c; uint32_t nbk1, nbk3, nbt3, nbm3, nem3; @@ -6266,6 +6298,17 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { device->subgroup_size); } + // DSv4 fused hyper-connection ops: plain f32 compute, no subgroup/coopmat requirements + ggml_vk_create_pipeline(device, device->pipeline_dsv4_hc_pre_f32, + "dsv4_hc_pre_f32", dsv4_hc_pre_f32_len, dsv4_hc_pre_f32_data, "main", 3, + sizeof(vk_op_dsv4_hc_pre_push_constants), {256, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dsv4_hc_comb_f32, + "dsv4_hc_comb_f32", dsv4_hc_comb_f32_len, dsv4_hc_comb_f32_data, "main", 4, + sizeof(vk_op_dsv4_hc_comb_push_constants), {256, 1, 1}, {}, 1); + ggml_vk_create_pipeline(device, device->pipeline_dsv4_hc_post_f32, + "dsv4_hc_post_f32", dsv4_hc_post_f32_len, dsv4_hc_post_f32_data, "main", 5, + sizeof(vk_op_dsv4_hc_post_push_constants), {256, 1, 1}, {}, 1); + if (device->subgroup_arithmetic && device->subgroup_require_full_support) { ggml_vk_create_pipeline(device, device->pipeline_ssm_scan_f32_d128, "ssm_scan_128_f32", ssm_scan_subgroup_f32_len, ssm_scan_subgroup_f32_data, "main", 8, sizeof(vk_op_ssm_scan_push_constants), {1, 1, 1}, {128, device->subgroup_size}, 1, true, true); ggml_vk_create_pipeline(device, device->pipeline_ssm_scan_f32_d256, "ssm_scan_256_f32", ssm_scan_subgroup_f32_len, ssm_scan_subgroup_f32_data, "main", 8, sizeof(vk_op_ssm_scan_push_constants), {1, 1, 1}, {256, device->subgroup_size}, 1, true, true); @@ -13760,6 +13803,91 @@ static void ggml_vk_lightning_indexer_cm(ggml_backend_vk_context * ctx, vk_conte pc, {(uint32_t) k->ne[2], (uint32_t) q->ne[2], (uint32_t) q->ne[3]}); } +// DSv4 fused hyper-connection ops — ports of ggml-cuda/dsv4-hc.cu. Strides are passed in +// f32 elements; grids mirror the CUDA launch geometry (flat 1D, 256 threads per workgroup). + +static void ggml_vk_dsv4_hc_pre(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) { + const ggml_tensor * x = dst->src[0]; + const ggml_tensor * w = dst->src[1]; + + const uint32_t n_embd = (uint32_t) x->ne[0]; + const uint32_t hc = (uint32_t) x->ne[1]; + const uint32_t n_tokens = (uint32_t) x->ne[2]; + const uint32_t nr = n_embd * n_tokens; + + vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_pre_f32; + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + + const vk_op_dsv4_hc_pre_push_constants pc = { + n_embd, hc, nr, + (uint32_t)(x->nb[0] / sizeof(float)), (uint32_t)(x->nb[1] / sizeof(float)), (uint32_t)(x->nb[2] / sizeof(float)), + (uint32_t)(w->nb[0] / sizeof(float)), (uint32_t)(w->nb[1] / sizeof(float)), + (uint32_t)(dst->nb[0] / sizeof(float)), (uint32_t)(dst->nb[1] / sizeof(float)), + }; + + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, + {ggml_vk_tensor_subbuffer(ctx, x), ggml_vk_tensor_subbuffer(ctx, w), + ggml_vk_tensor_subbuffer(ctx, dst)}, + pc, {nr, 1, 1}); +} + +static void ggml_vk_dsv4_hc_comb(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) { + const ggml_tensor * mixes = dst->src[0]; + const ggml_tensor * scale = dst->src[1]; + const ggml_tensor * base = dst->src[2]; + + const uint32_t n_tokens = (uint32_t) mixes->ne[1]; + const float eps = ggml_get_op_params_f32(dst, 0); + const int32_t n_iter = ggml_get_op_params_i32(dst, 1); + + vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_comb_f32; + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + + const vk_op_dsv4_hc_comb_push_constants pc = { + n_tokens, + (uint32_t)(mixes->nb[0] / sizeof(float)), (uint32_t)(mixes->nb[1] / sizeof(float)), + (uint32_t)(scale->nb[0] / sizeof(float)), + (uint32_t)(base->nb[0] / sizeof(float)), + (uint32_t)(dst->nb[0] / sizeof(float)), (uint32_t)(dst->nb[1] / sizeof(float)), (uint32_t)(dst->nb[2] / sizeof(float)), + eps, n_iter, + }; + + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, + {ggml_vk_tensor_subbuffer(ctx, mixes), ggml_vk_tensor_subbuffer(ctx, scale), + ggml_vk_tensor_subbuffer(ctx, base), ggml_vk_tensor_subbuffer(ctx, dst)}, + pc, {n_tokens, 1, 1}); +} + +static void ggml_vk_dsv4_hc_post(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) { + const ggml_tensor * x = dst->src[0]; + const ggml_tensor * residual = dst->src[1]; + const ggml_tensor * post = dst->src[2]; + const ggml_tensor * comb = dst->src[3]; + + const uint32_t n_embd = (uint32_t) x->ne[0]; + const uint32_t n_tokens = (uint32_t) x->ne[1]; + const uint32_t hc = (uint32_t) residual->ne[1]; + const uint32_t nr = n_embd * hc * n_tokens; + + vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_post_f32; + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + + const vk_op_dsv4_hc_post_push_constants pc = { + n_embd, hc, nr, + (uint32_t)(x->nb[0] / sizeof(float)), (uint32_t)(x->nb[1] / sizeof(float)), + (uint32_t)(residual->nb[0] / sizeof(float)), (uint32_t)(residual->nb[1] / sizeof(float)), (uint32_t)(residual->nb[2] / sizeof(float)), + (uint32_t)(post->nb[0] / sizeof(float)), (uint32_t)(post->nb[1] / sizeof(float)), + (uint32_t)(comb->nb[0] / sizeof(float)), (uint32_t)(comb->nb[1] / sizeof(float)), (uint32_t)(comb->nb[2] / sizeof(float)), + (uint32_t)(dst->nb[0] / sizeof(float)), (uint32_t)(dst->nb[1] / sizeof(float)), (uint32_t)(dst->nb[2] / sizeof(float)), + }; + + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, + {ggml_vk_tensor_subbuffer(ctx, x), ggml_vk_tensor_subbuffer(ctx, residual), + ggml_vk_tensor_subbuffer(ctx, post), ggml_vk_tensor_subbuffer(ctx, comb), + ggml_vk_tensor_subbuffer(ctx, dst)}, + pc, {nr, 1, 1}); +} + static void ggml_vk_ssm_scan(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) { const ggml_tensor * src0 = dst->src[0]; const ggml_tensor * src1 = dst->src[1]; @@ -16921,6 +17049,21 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr break; + case GGML_OP_DSV4_HC_PRE: + ggml_vk_dsv4_hc_pre(ctx, compute_ctx, node); + + break; + + case GGML_OP_DSV4_HC_COMB: + ggml_vk_dsv4_hc_comb(ctx, compute_ctx, node); + + break; + + case GGML_OP_DSV4_HC_POST: + ggml_vk_dsv4_hc_post(ctx, compute_ctx, node); + + break; + case GGML_OP_SSM_SCAN: ggml_vk_ssm_scan(ctx, compute_ctx, node); @@ -19858,6 +20001,16 @@ 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_DSV4_HC_PRE: + return op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32 && + op->type == GGML_TYPE_F32; + case GGML_OP_DSV4_HC_COMB: + return op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32 && + op->src[2]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32; + case GGML_OP_DSV4_HC_POST: + return op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32 && + op->src[2]->type == GGML_TYPE_F32 && op->src[3]->type == GGML_TYPE_F32 && + op->type == GGML_TYPE_F32; case GGML_OP_LIGHTNING_INDEXER: { const ggml_tensor * q = op->src[0]; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_comb.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_comb.comp new file mode 100644 index 000000000000..0449715bf458 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_comb.comp @@ -0,0 +1,101 @@ +#version 450 + +// DeepSeek V4 fused hyper-connection "comb": build the 4x4 stream-mixing matrix per token — +// per-source softmax over scaled+biased logits, then eps-stabilized alternating column/row +// (sinkhorn) normalization. Port of ggml-cuda/dsv4-hc.cu (hc_comb): one THREAD per token, +// the whole 4x4 lives in registers. At decode this is a single active thread by design — +// the fusion's value is collapsing the ~dozen decomposed graph ops (and their intermediate +// tensors) into one dispatch, not throughput. + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer MBuf { float data_mixes[]; }; +layout(binding = 1) readonly buffer SBuf { float data_scale[]; }; +layout(binding = 2) readonly buffer BBuf { float data_base[]; }; +layout(binding = 3) writeonly buffer DBuf { float data_d[]; }; + +layout(push_constant) uniform Parameters { + uint n_tokens; + uint sm0, sm1; + uint ss0; + uint sb0; + uint sd0, sd1, sd2; + float eps; + int n_iter; +} p; + +const uint HC = 4; +const uint COMB_OFFSET = 2 * HC; // comb logits start after the pre/post blocks of the mix vector + +void norm_cols(inout float comb[HC * HC]) { + for (uint idst = 0; idst < HC; ++idst) { + float sum = p.eps; + for (uint isrc = 0; isrc < HC; ++isrc) { + sum += comb[idst + HC * isrc]; + } + const float inv_sum = 1.0 / sum; + for (uint isrc = 0; isrc < HC; ++isrc) { + comb[idst + HC * isrc] *= inv_sum; + } + } +} + +void norm_rows(inout float comb[HC * HC]) { + for (uint isrc = 0; isrc < HC; ++isrc) { + float sum = p.eps; + for (uint idst = 0; idst < HC; ++idst) { + sum += comb[idst + HC * isrc]; + } + const float inv_sum = 1.0 / sum; + for (uint idst = 0; idst < HC; ++idst) { + comb[idst + HC * isrc] *= inv_sum; + } + } +} + +void main() { + const uint it = gl_GlobalInvocationID.x; + if (it >= p.n_tokens) { + return; + } + + const float scale_comb = data_scale[2 * p.ss0]; + float comb[HC * HC]; + + for (uint isrc = 0; isrc < HC; ++isrc) { + float vmax = uintBitsToFloat(0xff800000); // -inf + for (uint idst = 0; idst < HC; ++idst) { + const uint idx = idst + HC * isrc; + const float v = data_mixes[(COMB_OFFSET + idx) * p.sm0 + it * p.sm1] * scale_comb + + data_base[(COMB_OFFSET + idx) * p.sb0]; + comb[idx] = v; + vmax = max(vmax, v); + } + + float sum = 0.0; + for (uint idst = 0; idst < HC; ++idst) { + const uint idx = idst + HC * isrc; + const float v = exp(comb[idx] - vmax); + comb[idx] = v; + sum += v; + } + + const float inv_sum = 1.0 / sum; + for (uint idst = 0; idst < HC; ++idst) { + const uint idx = idst + HC * isrc; + comb[idx] = comb[idx] * inv_sum + p.eps; + } + } + + norm_cols(comb); + for (int i = 1; i < p.n_iter; ++i) { + norm_rows(comb); + norm_cols(comb); + } + + for (uint isrc = 0; isrc < HC; ++isrc) { + for (uint idst = 0; idst < HC; ++idst) { + data_d[idst * p.sd0 + isrc * p.sd1 + it * p.sd2] = comb[idst + HC * isrc]; + } + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_post.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_post.comp new file mode 100644 index 000000000000..212109dfd434 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_post.comp @@ -0,0 +1,44 @@ +#version 450 + +// DeepSeek V4 fused hyper-connection "post": redistribute the layer output back into the HC +// streams with the sinkhorn-mixed residual, +// dst[i0, idst, it] = x[i0, it] * post[idst, it] + sum_isrc residual[i0, isrc, it] * comb[idst, isrc, it]. +// Port of ggml-cuda/dsv4-hc.cu (hc_post). Flat elementwise over n_embd * hc * n_tokens. + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer XBuf { float data_x[]; }; +layout(binding = 1) readonly buffer RBuf { float data_r[]; }; +layout(binding = 2) readonly buffer PBuf { float data_p[]; }; +layout(binding = 3) readonly buffer CBuf { float data_c[]; }; +layout(binding = 4) writeonly buffer DBuf { float data_d[]; }; + +layout(push_constant) uniform Parameters { + uint n_embd; + uint hc; + uint nr; // n_embd * hc * n_tokens + uint sx0, sx1; + uint sr0, sr1, sr2; + uint sp0, sp1; + uint sc0, sc1, sc2; + uint sd0, sd1, sd2; +} p; + +void main() { + const uint ir = gl_GlobalInvocationID.x; + if (ir >= p.nr) { + return; + } + + const uint i0 = ir % p.n_embd; + const uint idst = (ir / p.n_embd) % p.hc; + const uint it = ir / (p.n_embd * p.hc); + + float sum = data_x[i0 * p.sx0 + it * p.sx1] * data_p[idst * p.sp0 + it * p.sp1]; + for (uint isrc = 0; isrc < p.hc; ++isrc) { + sum += data_r[i0 * p.sr0 + isrc * p.sr1 + it * p.sr2] + * data_c[idst * p.sc0 + isrc * p.sc1 + it * p.sc2]; + } + + data_d[i0 * p.sd0 + idst * p.sd1 + it * p.sd2] = sum; +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_pre.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_pre.comp new file mode 100644 index 000000000000..b6cc09fe2cfa --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_pre.comp @@ -0,0 +1,37 @@ +#version 450 + +// DeepSeek V4 fused hyper-connection "pre": mix the HC input streams down to one embedding, +// dst[i0, it] = sum_ih x[i0, ih, it] * w[ih, it]. Port of ggml-cuda/dsv4-hc.cu (hc_pre). +// Flat elementwise kernel over n_embd * n_tokens; strides are in f32 elements. + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer XBuf { float data_x[]; }; +layout(binding = 1) readonly buffer WBuf { float data_w[]; }; +layout(binding = 2) writeonly buffer DBuf { float data_d[]; }; + +layout(push_constant) uniform Parameters { + uint n_embd; + uint hc; + uint nr; // n_embd * n_tokens + uint sx0, sx1, sx2; + uint sw0, sw1; + uint sd0, sd1; +} p; + +void main() { + const uint ir = gl_GlobalInvocationID.x; + if (ir >= p.nr) { + return; + } + + const uint i0 = ir % p.n_embd; + const uint it = ir / p.n_embd; + + float sum = data_x[i0 * p.sx0 + it * p.sx2] * data_w[it * p.sw1]; + for (uint ih = 1; ih < p.hc; ++ih) { + sum += data_x[i0 * p.sx0 + ih * p.sx1 + it * p.sx2] * data_w[ih * p.sw0 + it * p.sw1]; + } + + data_d[i0 * p.sd0 + it * p.sd1] = sum; +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 5c06f8222927..3232278e5881 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -818,6 +818,9 @@ void process_shaders() { #endif string_to_spv("flash_attn_top_k_f16", "flash_attn_top_k.comp", {}); string_to_spv("flash_attn_gather_f16", "flash_attn_gather.comp", {}); + string_to_spv("dsv4_hc_pre_f32", "dsv4_hc_pre.comp", {}); + string_to_spv("dsv4_hc_comb_f32", "dsv4_hc_comb.comp", {}); + string_to_spv("dsv4_hc_post_f32", "dsv4_hc_post.comp", {}); string_to_spv("mul_mat_vec_p021_f16_f32_subgroup_add", "mul_mat_vec_p021.comp", {{"A_TYPE", "float16_t"}, {"A_TYPEV4", "f16vec4"}, {"B_TYPE", "float"}, {"B_TYPEV4", "vec4"}, {"D_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}}); string_to_spv("mul_mat_vec_p021_f16_f32", "mul_mat_vec_p021.comp", {{"A_TYPE", "float16_t"}, {"A_TYPEV4", "f16vec4"}, {"B_TYPE", "float"}, {"B_TYPEV4", "vec4"}, {"D_TYPE", "float"}}); From b8d2503264dfd6d13d12c5c56b3ffdd979d01c19 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 2 Aug 2026 16:07:19 +0000 Subject: [PATCH 103/212] llama: keep DeepSeek lightning-indexer key cache f16 under quantized -ctk The fused indexer kernels read f16 keys only, so quantizing the small (128-dim) indexer cache silently disables them and falls back to the decomposed full-KV indexer path, whose contiguize cost grows superlinearly with depth (measured 0.8ms -> 97ms per dispatch by 12k context on Vulkan). Pin the indexer key cache to f16 in both DSA cache variants; the memory cost vs q8_0 is ~120 bytes per token per layer. Assisted-by: Claude Fable 5 --- src/llama-kv-cache-dsa.cpp | 5 ++++- src/llama-kv-cache-dsv4.cpp | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/llama-kv-cache-dsa.cpp b/src/llama-kv-cache-dsa.cpp index 96cb045d2e5d..e926e34314b4 100644 --- a/src/llama-kv-cache-dsa.cpp +++ b/src/llama-kv-cache-dsa.cpp @@ -47,8 +47,11 @@ llama_kv_cache_dsa::llama_kv_cache_dsa( LLAMA_LOG_INFO("%s: creating indexer KV cache, size = %u cells\n", __func__, kv_size); + // keep indexer keys f16 regardless of type_k: the fused indexer kernels read + // f16 only, and quantizing this small cache (128 dims) saves little while + // forcing the much slower decomposed indexer path kv_lid = std::make_unique( - model, hparams_lid, type_k, type_v, + model, hparams_lid, GGML_TYPE_F16, type_v, v_trans, offload, unified, kv_size, n_seq_max, n_pad, n_swa, swa_type, nullptr, filter_lid, reuse, nullptr); } diff --git a/src/llama-kv-cache-dsv4.cpp b/src/llama-kv-cache-dsv4.cpp index 9e82a6198a09..2097271a0f18 100644 --- a/src/llama-kv-cache-dsv4.cpp +++ b/src/llama-kv-cache-dsv4.cpp @@ -1305,8 +1305,11 @@ llama_kv_cache_dsv4::llama_kv_cache_dsv4( LLAMA_LOG_INFO("%s: creating DSV4 lightning-indexer KV cache, size = %u cells\n", __func__, dsv4_comp_size(kv_size, DSV4_CSA_RATIO)); + // keep indexer keys f16 regardless of type_k: the fused indexer kernels read + // f16 only, and quantizing this small cache (128 dims) saves little while + // forcing the much slower decomposed indexer path kv_lid = std::make_unique( - model, hparams_lid, type_k, type_v, + model, hparams_lid, GGML_TYPE_F16, type_v, v_trans, offload, unified_compressed, GGML_PAD(dsv4_comp_size(kv_size, DSV4_CSA_RATIO), 256u), n_seq_max, n_pad, 0, LLAMA_SWA_TYPE_NONE, nullptr, filter_csa, nullptr, nullptr); From 9cb8c288889ac32765211bf3026b262e36010459 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Mon, 3 Aug 2026 01:48:02 +0000 Subject: [PATCH 104/212] llama: contiguize grouped o-proj input for small multi-token batches (DSv4) The permuted view feeding the grouped wo_a matmul gives B a 128 KiB power-of-2 token stride. Single-token decode never touches it, but small multi-token batches (speculative verify, n=2-4) hit a strided-B matmul path that runs ~11x slower than contiguous (40 vs 460 GFLOPS measured on gfx1151, ~54% of GPU time in a draft-verify window). A contiguous copy for 2..8 tokens is far cheaper than the stride tax; n=1 and large prefill batches are unaffected. Assisted-by: Claude Fable 5 --- src/models/deepseek4.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/models/deepseek4.cpp b/src/models/deepseek4.cpp index 8588d069eed6..494e643276c5 100644 --- a/src/models/deepseek4.cpp +++ b/src/models/deepseek4.cpp @@ -1209,6 +1209,11 @@ ggml_tensor * llama_model_deepseek4::graph::build_attention_impl( out = ggml_reshape_3d(ctx0, out, o_group_dim, n_groups, nt); out = ggml_permute(ctx0, out, 0, 2, 1, 3); + // small multi-token batches (speculative verify) hit a pathological strided-B + // path in the grouped matmul below; a contiguous copy is much cheaper + if (nt > 1 && nt <= 8) { + out = ggml_cont(ctx0, out); + } ggml_tensor * oa = ggml_mul_mat(ctx0, layer.wo_a, out); cb(oa, "attn_wo_a", il); oa = ggml_permute(ctx0, oa, 0, 2, 1, 3); From 78e31af07176d46c6792b08bdba3d0b4f4b574ec Mon Sep 17 00:00:00 2001 From: Jaap Buurman Date: Wed, 12 Aug 2026 22:25:40 +0200 Subject: [PATCH 105/212] vulkan: accelerate DeepSeek V4 sparse prefill FA Assisted-by: OpenAI Codex --- .../DSV4-vulkan-sparse-prefill-progress.md | 259 +++++++++++++++++ ggml/src/ggml-vulkan/ggml-vulkan.cpp | 16 +- .../vulkan-shaders/flash_attn_top_k_cm.comp | 269 ++++++++++++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 3 + tests/test-backend-ops.cpp | 1 + 5 files changed, 545 insertions(+), 3 deletions(-) create mode 100644 docs/development/DSV4-vulkan-sparse-prefill-progress.md create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp diff --git a/docs/development/DSV4-vulkan-sparse-prefill-progress.md b/docs/development/DSV4-vulkan-sparse-prefill-progress.md new file mode 100644 index 000000000000..80bf562a0623 --- /dev/null +++ b/docs/development/DSV4-vulkan-sparse-prefill-progress.md @@ -0,0 +1,259 @@ +# DeepSeek V4 Vulkan sparse prefill progress + +This file is a self-contained handoff for the DeepSeek V4 sparse-attention prompt-processing optimization on AMD Strix Halo. Read the repository `AGENTS.md` and `CONTRIBUTING.md` before continuing. + +## Repository state + +- Repository: `https://github.com/Nathanw1014/llama.cpp` +- Branch: `strix-halo-vulkan` +- Starting commit: `baf0025de861c6f6ea3720fa81c52ae1b2e6c078` +- Target GPU: AMD Radeon 8060S / gfx1151, RADV, Vulkan, wave64 +- The device reports `GL_KHR_cooperative_matrix`, f16 inputs with f32 accumulation, 64 KiB shared memory, and a maximum 512-thread workgroup used by this path. + +The implementation is not upstream `ggml-org/llama.cpp`. It builds on this branch's DeepSeek V4 graph, Lightning Indexer, sparse top-K hint, decode gather path, fused HC kernels, and Vulkan profiler changes. + +## Important execution constraints + +The system is an APU. CPU compilation and GPU benchmarking share power and memory bandwidth. Never build and benchmark at the same time. Serialize all builds, correctness tests, and performance tests. + +GPU commands must run with host GPU access. In an agent sandbox, request elevated/out-of-sandbox execution. A sandboxed benchmark showed only CPU activity and is invalid. + +Redirect the full model benchmark to a log. Inspect only the last profiler block with `tail`; do not load the full log into agent context. + +## Build and ccache + +The build directory is `build`, configured as Release with Vulkan enabled. The default ccache directory was read-only in the agent environment, so use a writable directory: + +```bash +cmake -S . -B build \ + -DGGML_VULKAN=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + +CCACHE_DIR=/tmp/llama-cpp-ccache cmake --build build --config Release \ + --target llama-bench test-backend-ops -j "$(nproc)" + +CCACHE_DIR=/tmp/llama-cpp-ccache ccache -s +``` + +ccache was verified active. The final build reported direct hits. Note that an incremental change to `ggml-vulkan.cpp` is one large C++ translation unit and therefore uses one compiler core even with `-j`. Shader object regeneration can run in parallel. + +## Canonical benchmark command + +Do not change or omit switches for the 32k acceptance run: + +```bash +GGML_VK_PERF_LOGGER=1 ./build/bin/llama-bench \ + -m ~/Projects/docker/localLLaMA/models/models--unsloth--DeepSeek-V4-Flash-0731-GGUF/snapshots/109848da2469efe1f1aab9e11acea08a065ccd4f/UD-IQ3_XXS/DeepSeek-V4-Flash-0731-UD-IQ3_XXS-00001-of-00004.gguf \ + -r 1 -d 32768 -p 2048 -ub 2048 -fa 1 -n 0 \ + > /tmp/dsv4-vulkan-32k.log 2>&1 + +tail -n 180 /tmp/dsv4-vulkan-32k.log +``` + +The known model path exists on the target system. + +## Graph and dispatch findings + +DeepSeek V4 builds the Lightning Indexer and sparse attention in `src/models/deepseek4.cpp`: + +1. `build_lid_top_k()` creates indexer Q/K/weights and calls `ggml_lightning_indexer()`. +2. `ggml_top_k()` selects up to `hparams.indexer_top_k` compressed-cache indices for every query token. +3. `build_csa_lid_attention()` concatenates the raw SWA K prefix with compressed CSA K, builds a dense mask carrying the same sparse selection, and calls `build_attn_mha(..., top_k, raw_k->ne[2])`. +4. `build_attn_mha()` attaches `top_k` and `n_kv_raw` to `GGML_OP_FLASH_ATTN_EXT` through `ggml_flash_attn_ext_add_top_k()`. + +At the final 32k PP2048 batch: + +- total K rows: 11,008 +- `n_kv_raw`: 2,304 raw SWA rows, always attended subject to the mask +- `n_top_k`: 512 selected compressed rows per query token +- active rows: 2,816 +- selectable compressed region: 8,704 + +The custom Vulkan path is selected by `ggml_vk_flash_attn_top_k()` before ordinary FA. Its gate requires the DeepSeek V4 shape and `total_k >= 3 * (n_kv_raw + n_top_k)`. The final shape satisfies `11008 >= 3 * 2816`. + +The old `flash_attn_top_k.comp` shader is scalar/subgroup code. One 512-thread workgroup covers eight heads for one query token. It stages 16 selected 512-wide K/V rows, computes QK with scalar FMAs and `subgroupAdd`, updates online softmax one key at a time, and accumulates PV manually. It does not use cooperative matrices. + +The top-K set differs by query token but is shared by all 64 query heads for that token. This makes the attention for one token a regular matrix problem across heads and selected keys despite sparse per-token indexing. + +## Root cause evidence + +The existing Vulkan timestamp infrastructure was extended with `ggml_vk_perf_mark_subop()` after the sparse dispatch. This reports the sparse kernel separately as `FA_TOP_K_SPARSE (sub-op)` or `FA_TOP_K_CM (sub-op)`. + +Focused test shape: + +```bash +GGML_VK_PERF_LOGGER=1 ./build/bin/test-backend-ops perf \ + -b Vulkan0 -o FLASH_ATTN_EXT \ + -p 'kv=32768,nb=512,n_kv_raw=1024,n_top_k=512,sinks=0' +``` + +Results: + +- old scalar sparse kernel: 61.42 ms, 1.68 TFLOPS of useful active-set work +- ordinary dense FA diagnostic (`GGML_VK_FA_TOPK=0`): 255.97 ms, about 8.7 TFLOPS over the full dense work +- final cooperative sparse kernel: 32.55 ms, 3.17 TFLOPS of useful active-set work + +The residual `FLASH_ATTN_EXT` interval after the sparse timestamp is only about 4-7 us. The cost is inside the shader, not dispatch or surrounding synchronization. + +A temporary uniform stage-profiling mode was used and removed. For the final 32-head tile: + +- selected K gather + cooperative QK: 14.30 ms +- gather + QK + serial softmax: 26.34 ms +- gather + QK + parallel softmax: 15.53 ms +- full kernel: 32.55 ms +- the remaining cooperative PV/output portion is about 17.0 ms + +The old scalar shader was compute/issue inefficient. Dense FA proved matrix hardware is much faster but was still too expensive because it processes all K rows. The final implementation preserves sparsity and uses the matrix hardware for both QK and PV. + +## Implementation + +Files changed: + +- `ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp` + - New cooperative-matrix sparse prefill shader. + - One 512-thread workgroup covers 32 query heads for one token. + - Eight wave64 subgroups cover two 16-head tiles by four 16-key or 16-output-dimension tiles. + - Processes 64 selected keys per online-softmax block. + - Stages only indexed selected K/V tiles, never the full K range. + - Uses f16 cooperative-matrix inputs and f32 accumulation for QK and PV. + - Uses a 16-lane segmented softmax per head. XOR subgroup shuffles reduce max and sum for four independent heads per wave without workgroup barriers. + - Keeps f32 output accumulators and normalizes after all active blocks. + - Preserves the raw prefix, top-K index validation, mask, sinks, stream strides, and K == V latent behavior. +- `ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp` + - Embeds the new shader when cooperative-matrix shader support is available. +- `ggml/src/ggml-vulkan/ggml-vulkan.cpp` + - Adds the cooperative sparse pipeline when the device supports the required 16x16x16 f16/f32 cooperative matrix shape. + - Selects it by capability and keeps the scalar shader as fallback. + - Adds `GGML_VK_FA_TOPK=0` to force ordinary dense FA for diagnostics. + - Adds `GGML_VK_FA_TOPK_CM=0` to force the old scalar sparse shader for A/B tests. + - Adds sparse sub-operation timestamps through the existing profiler. + +The path is capability-based, not hardcoded to Strix Halo. The current sparse shape gate remains DeepSeek V4-specific. Devices without the required cooperative matrix support keep the correct scalar sparse or dense fallback. + +Two discarded prototypes are useful context: + +- A 16-head, 64-key streaming cooperative tile was correct and reduced the focused test from 61.4 to 44.6 ms. +- Keeping 32 complete 512-wide K/V rows in LDS grew shared memory to about 41 KiB, reduced residency, doubled block/barrier count, and regressed to 73.8 ms. Do not retry full-row LDS staging without solving occupancy. +- A 32-head, 64-key streaming tile halved irregular row loads but initially stayed near 44.7 ms because serial softmax cost about 11.5 ms. Parallel segmented softmax produced the final 32.55 ms result. + +## Correctness validation + +Run: + +```bash +./build/bin/test-backend-ops test -b Vulkan0 -o FLASH_ATTN_EXT -p 'n_top_k=' +``` + +Final result: 8/8 sparse top-K FA cases passed against the CPU reference. Cases include: + +- decode and short batches that use dense/gather fallback +- sparse prefill batch sizes 64 and 128 +- `n_kv_raw` plus top-K selection +- invalid top-K index handling from the test fixture +- sinks enabled and disabled +- sparse threshold transitions +- an active-key count of 193, which exercises a partial final 64-key block + +The test uses the existing FA tolerance of NMSE <= `5e-4`. No NaN or Inf failure occurred. The implementation changes Q and probability inputs to f16 cooperative-matrix operands with f32 accumulation, matching the precision strategy of ordinary Vulkan cooperative FA. + +Still desirable before broader submission: + +- compare model logits on controlled prompts between `GGML_VK_FA_TOPK_CM=0` and the default cooperative path + +## Canonical 32k results + +Exact clean runs, same command and machine, no concurrent build: + +```text +32k context, PP 2048, ub 2048 + +Before (commit baf0025de): +112.29 tok/s +Total Vulkan: 18.1957 s +Sparse FA: 8.84496 s, 421.189 ms/layer +Lightning Indexer: 1.19438 s +TOP_K: 0.076948 s + +After: +152.32 tok/s +Total Vulkan: 13.4032 s +Sparse FA: 4.44701 s, 211.762 ms/layer +Lightning Indexer: 1.13156 s +TOP_K: 0.073945 s + +Change: +Throughput: +35.65% +Total Vulkan time: -26.34% +Sparse FA time: -49.72% +Sparse FA saved: 4.398 s +Total GPU time saved: 4.793 s +``` + +The profiler now lists the optimized dispatch as `FA_TOP_K_CM (sub-op)`. The following residual `FLASH_ATTN_EXT` line is only the post-mark interval and must not be interpreted as the kernel time. + +## Context-depth measurements + +All points use PP2048, ub2048, FA enabled, one repetition, and no token generation. They were run sequentially with no compiler active: + +```text +Existing depth tok/s Total Vulkan Final large FA Lightning Indexer TOP_K +0 253.44 8.041 s 0.294 s 0.095 s 0.001 s +8192 211.01 9.666 s 1.625 s 0.379 s 0.022 s +16384 177.19 11.518 s 3.031 s 0.678 s 0.044 s +32768 152.32 13.403 s 4.447 s 1.132 s 0.074 s +``` + +At 0, 8k, and 16k, total K is below the existing sparse-path gate `total_k >= 3 * (n_kv_raw + n_top_k)`. These points use the unchanged ordinary dense FA implementation, so the cooperative sparse change does not affect or regress them. At 32k, total K is 11,008 and the cooperative sparse path engages. The 32k `Final large FA` value is the `FA_TOP_K_CM (sub-op)` total; the lower-depth values are the large ordinary `FLASH_ATTN_EXT` totals. + +Logs: + +- `/tmp/dsv4-vulkan-cm-0k.log` +- `/tmp/dsv4-vulkan-cm-8k.log` +- `/tmp/dsv4-vulkan-cm-16k.log` +- `/tmp/dsv4-vulkan-cm-32k.log` +- `/tmp/dsv4-vulkan-baseline-clean.log` +- `/tmp/dsv4-fa-cm-correctness-final.log` + +## Next optimization target + +The cooperative sparse FA remains the largest context-dependent cost at about 4.45 s total. Stage profiling indicates approximately 14.3 ms of focused-test time in gather/QK, about 1.2 ms in parallel softmax, and about 17 ms in PV/output. + +The next useful work is PV and output accumulation, not TOP_K. Investigate: + +- reducing repeated selected V staging across the two 32-head workgroups per token without increasing LDS enough to lose occupancy +- reducing the eight output-dimension passes or retaining more PV state in cooperative fragments/registers +- checking register count and spills for the 32 f32 output accumulators per invocation using RADV shader statistics +- alternate 32-head layouts that keep the same eight-wave occupancy but improve PV scheduling +- query-tile overlap/union gathering only if measured top-K overlap is high enough; a whole-2048-query union is unlikely to help + +Do not optimize TOP_K first. At 32k it is only about 74 ms total. Lightning Indexer is about 1.13 s and is the next context-dependent target only after sparse FA improves further. + +## Useful diagnostics + +Force old scalar sparse path: + +```bash +GGML_VK_FA_TOPK_CM=0 GGML_VK_PERF_LOGGER=1 ./build/bin/test-backend-ops perf \ + -b Vulkan0 -o FLASH_ATTN_EXT \ + -p 'kv=32768,nb=512,n_kv_raw=1024,n_top_k=512,sinks=0' +``` + +Force ordinary dense FA: + +```bash +GGML_VK_FA_TOPK=0 GGML_VK_PERF_LOGGER=1 ./build/bin/test-backend-ops perf \ + -b Vulkan0 -o FLASH_ATTN_EXT \ + -p 'kv=32768,nb=512,n_kv_raw=1024,n_top_k=512,sinks=0' +``` + +Default cooperative sparse path: + +```bash +GGML_VK_PERF_LOGGER=1 ./build/bin/test-backend-ops perf \ + -b Vulkan0 -o FLASH_ATTN_EXT \ + -p 'kv=32768,nb=512,n_kv_raw=1024,n_top_k=512,sinks=0' +``` + +Always run these sequentially. Do not run a compiler concurrently on this APU. diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 8a2d6fe99545..6c2dcef824b8 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1108,6 +1108,7 @@ struct vk_device_struct { vk_pipeline pipeline_lightning_indexer_cm_f16; vk_pipeline pipeline_lightning_indexer_decode_cm_f16; vk_pipeline pipeline_flash_attn_top_k_f16; + vk_pipeline pipeline_flash_attn_top_k_cm_f16; vk_pipeline pipeline_flash_attn_gather_f16; vk_pipeline pipeline_dsv4_hc_pre_f32; vk_pipeline pipeline_dsv4_hc_comb_f32; @@ -6286,6 +6287,10 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { "lightning_indexer_decode_cm_f16", lightning_indexer_decode_cm_f16_len, lightning_indexer_decode_cm_f16_data, "main", 5, sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 1, 1}, {device->subgroup_size}, 1, true, true, device->subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_flash_attn_top_k_cm_f16, + "flash_attn_top_k_cm_f16", flash_attn_top_k_cm_f16_len, flash_attn_top_k_cm_f16_data, "main", 6, + sizeof(vk_op_flash_attn_top_k_push_constants), {1, 1, 1}, {512, device->subgroup_size}, 1, true, true, + device->subgroup_size); } #endif ggml_vk_create_pipeline(device, device->pipeline_flash_attn_top_k_f16, @@ -11425,7 +11430,9 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & const ggml_tensor * q, const ggml_tensor * k, const ggml_tensor * v, const ggml_tensor * mask, const ggml_tensor * sinks, ggml_tensor * dst) { const ggml_tensor * top_k = dst->src[5]; - if (!top_k || !ctx->device->pipeline_flash_attn_top_k_f16 || + static const char * top_k_env = getenv("GGML_VK_FA_TOPK"); + if ((top_k_env && top_k_env[0] == '0') || + !top_k || (!ctx->device->pipeline_flash_attn_top_k_f16 && !ctx->device->pipeline_flash_attn_top_k_cm_f16) || q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || v->type != GGML_TYPE_F16 || !mask || mask->type != GGML_TYPE_F16 || top_k->type != GGML_TYPE_I32 || q->ne[0] != 512 || q->ne[1] < 64 || k->ne[0] != 512 || v->ne[0] != 512 || @@ -11475,12 +11482,15 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & const vk_subbuffer q_buf = ggml_vk_tensor_subbuffer(ctx, q); const vk_subbuffer sinks_buf = sinks ? ggml_vk_tensor_subbuffer(ctx, sinks) : q_buf; - vk_pipeline pipeline = ctx->device->pipeline_flash_attn_top_k_f16; + static const char * top_k_cm_env = getenv("GGML_VK_FA_TOPK_CM"); + const bool use_cm = (!top_k_cm_env || top_k_cm_env[0] != '0') && ctx->device->pipeline_flash_attn_top_k_cm_f16; + vk_pipeline pipeline = use_cm ? ctx->device->pipeline_flash_attn_top_k_cm_f16 : ctx->device->pipeline_flash_attn_top_k_f16; ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, {q_buf, ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, mask), sinks_buf, ggml_vk_tensor_subbuffer(ctx, top_k), ggml_vk_tensor_subbuffer(ctx, dst)}, - pc, {(uint32_t) q->ne[1], (uint32_t) CEIL_DIV(q->ne[2], 8), (uint32_t) q->ne[3]}); + pc, {(uint32_t) q->ne[1], (uint32_t) CEIL_DIV(q->ne[2], use_cm ? 32 : 8), (uint32_t) q->ne[3]}); + ggml_vk_perf_mark_subop(ctx, subctx, use_cm ? "FA_TOP_K_CM (sub-op)" : "FA_TOP_K_SPARSE (sub-op)"); return true; } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp new file mode 100644 index 000000000000..420a05375a7b --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp @@ -0,0 +1,269 @@ +#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_cooperative_matrix : require +#extension GL_KHR_memory_scope_semantics : require +#extension GL_KHR_shader_subgroup_basic : require +#extension GL_KHR_shader_subgroup_shuffle : require + +layout(constant_id = 0) const uint WORKGROUP_SIZE = 512; +layout(constant_id = 1) const uint SUBGROUP_SIZE = 64; +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer QBuf { float data_q[]; }; +layout(binding = 1) readonly buffer KBuf { float16_t data_k[]; }; +layout(binding = 2) readonly buffer MaskBuf { float16_t data_m[]; }; +layout(binding = 3) readonly buffer SinkBuf { float data_s[]; }; +layout(binding = 4) readonly buffer TopBuf { int data_top[]; }; +layout(binding = 5) writeonly buffer DstBuf { float data_dst[]; }; + +layout(push_constant) uniform Parameters { + uint n_batch; + uint n_kv; + uint n_kv_raw; + uint n_top_k; + uint n_head; + uint nbq1; + uint nbq2; + uint nbq3; + uint nbk1; + uint nbk3; + uint nbm1; + uint nbm3; + uint nbt1; + uint nbt3; + uint nb1; + uint nb2; + uint nb3; + float scale; + uint has_sinks; +} p; + +const uint TILE = 16; +const uint HEAD_SIZE = 512; +const uint HEADS_PER_GROUP = 32; +const uint KEYS_PER_BLOCK = 64; +const uint DIMS_PER_BLOCK = 64; +const uint QK_STRIDE = TILE / 4 + 2; +const uint SCORE_STRIDE = HEADS_PER_GROUP / 4 + 1; +const uint P_STRIDE = KEYS_PER_BLOCK / 4 + 2; +const uint V_STRIDE = DIMS_PER_BLOCK / 4 + 2; +const uint PV_STRIDE = DIMS_PER_BLOCK / 4; +const float MASK_NEG_INF = -65500.0; + +shared uint key_idx[KEYS_PER_BLOCK]; +shared f16vec4 q_sh[HEADS_PER_GROUP * QK_STRIDE]; +shared f16vec4 k_sh[KEYS_PER_BLOCK * QK_STRIDE]; +shared vec4 score_sh[KEYS_PER_BLOCK * SCORE_STRIDE]; +shared f16vec4 p_sh[HEADS_PER_GROUP * P_STRIDE]; +shared f16vec4 v_sh[KEYS_PER_BLOCK * V_STRIDE]; +shared vec4 pv_sh[HEADS_PER_GROUP * PV_STRIDE]; +shared float old_scale_sh[HEADS_PER_GROUP]; +shared float row_max_sh[HEADS_PER_GROUP]; +shared float row_sum_sh[HEADS_PER_GROUP]; + +void main() { + const uint tid = gl_LocalInvocationIndex; + const uint token = gl_WorkGroupID.x; + const uint head_base = gl_WorkGroupID.y * HEADS_PER_GROUP; + const uint stream = gl_WorkGroupID.z; + const uint mask_base = stream * p.nbm3 + token * p.nbm1; + const uint top_base = stream * p.nbt3 + token * p.nbt1; + const uint total_keys = p.n_kv_raw + p.n_top_k; + + float accum[HEADS_PER_GROUP * HEAD_SIZE / WORKGROUP_SIZE]; + [[unroll]] for (uint i = 0; i < accum.length(); ++i) { + accum[i] = 0.0; + } + + if (tid < HEADS_PER_GROUP) { + row_max_sh[tid] = uintBitsToFloat(0xff800000); + row_sum_sh[tid] = 0.0; + } + barrier(); + + for (uint kb = 0; kb < total_keys; kb += KEYS_PER_BLOCK) { + if (tid < KEYS_PER_BLOCK) { + const uint selected = kb + tid; + uint key = p.n_kv; + if (selected < p.n_kv_raw) { + key = selected; + } else if (selected < total_keys) { + const int compressed = data_top[top_base + selected - p.n_kv_raw]; + if (compressed >= 0 && uint(compressed) < p.n_kv - p.n_kv_raw) { + key = p.n_kv_raw + uint(compressed); + } + } + key_idx[tid] = key; + } + barrier(); + + coopmat scores = + coopmat(0.0); + coopmat kmat; + coopmat qmat; + + [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { + if (tid < KEYS_PER_BLOCK * (TILE / 4)) { + const uint key_local = tid / (TILE / 4); + const uint d4 = tid % (TILE / 4); + const uint key = key_idx[key_local]; + f16vec4 value = f16vec4(0.0); + if (key < p.n_kv) { + const uint offset = stream * p.nbk3 + key * p.nbk1 + d + d4 * 4; + value = f16vec4(data_k[offset], data_k[offset + 1], data_k[offset + 2], data_k[offset + 3]); + } + k_sh[key_local * QK_STRIDE + d4] = value; + } + if (tid < HEADS_PER_GROUP * (TILE / 4)) { + const uint head_local = tid / (TILE / 4); + const uint d4 = tid % (TILE / 4); + const uint head = head_base + head_local; + const uint offset = stream * p.nbq3 + head * p.nbq2 + token * p.nbq1 + d + d4 * 4; + q_sh[head_local * QK_STRIDE + d4] = f16vec4( + data_q[offset], data_q[offset + 1], data_q[offset + 2], data_q[offset + 3]); + } + barrier(); + + const uint key_chunk = gl_SubgroupID % (KEYS_PER_BLOCK / TILE); + const uint head_tile = gl_SubgroupID / (KEYS_PER_BLOCK / TILE); + coopMatLoad(kmat, k_sh, key_chunk * TILE * QK_STRIDE, + QK_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + coopMatLoad(qmat, q_sh, head_tile * TILE * QK_STRIDE, + QK_STRIDE, gl_CooperativeMatrixLayoutColumnMajor); + scores = coopMatMulAdd(kmat, qmat, scores); + barrier(); + } + + const uint score_key_chunk = gl_SubgroupID % (KEYS_PER_BLOCK / TILE); + const uint score_head_tile = gl_SubgroupID / (KEYS_PER_BLOCK / TILE); + coopMatStore(scores, score_sh, + score_key_chunk * TILE * SCORE_STRIDE + score_head_tile * (TILE / 4), + SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + barrier(); + + { + const uint head_local = tid / (SUBGROUP_SIZE / 4); + const uint softmax_lane = tid % (SUBGROUP_SIZE / 4); + float block_max = uintBitsToFloat(0xff800000); + [[unroll]] for (uint i = 0; i < KEYS_PER_BLOCK / (SUBGROUP_SIZE / 4); ++i) { + const uint key_local = softmax_lane + i * (SUBGROUP_SIZE / 4); + const uint key = key_idx[key_local]; + const float mask = key < p.n_kv ? float(data_m[mask_base + key]) : uintBitsToFloat(0xff800000); + const float score = float(score_sh[key_local * SCORE_STRIDE + head_local / 4][head_local % 4]) * p.scale + mask; + block_max = mask < MASK_NEG_INF ? block_max : max(block_max, score); + } + [[unroll]] for (uint delta = 1; delta < SUBGROUP_SIZE / 4; delta *= 2) { + block_max = max(block_max, subgroupShuffleXor(block_max, delta)); + } + + const float old_row_max = row_max_sh[head_local]; + const float old_row_sum = row_sum_sh[head_local]; + const float new_max = max(old_row_max, block_max); + const float old_scale = old_row_sum == 0.0 ? 0.0 : exp(old_row_max - new_max); + float block_sum = 0.0; + [[unroll]] for (uint i = 0; i < KEYS_PER_BLOCK / (SUBGROUP_SIZE / 4); ++i) { + const uint key_local = softmax_lane + i * (SUBGROUP_SIZE / 4); + const uint key = key_idx[key_local]; + const float mask = key < p.n_kv ? float(data_m[mask_base + key]) : uintBitsToFloat(0xff800000); + float weight = 0.0; + if (mask >= MASK_NEG_INF) { + const float score = float(score_sh[key_local * SCORE_STRIDE + head_local / 4][head_local % 4]) * p.scale + mask; + weight = exp(score - new_max); + block_sum += weight; + } + p_sh[head_local * P_STRIDE + key_local / 4][key_local % 4] = float16_t(weight); + } + [[unroll]] for (uint delta = 1; delta < SUBGROUP_SIZE / 4; delta *= 2) { + block_sum += subgroupShuffleXor(block_sum, delta); + } + + if (softmax_lane == 0) { + row_sum_sh[head_local] = old_row_sum * old_scale + block_sum; + row_max_sh[head_local] = new_max; + old_scale_sh[head_local] = old_scale; + } + } + barrier(); + + [[unroll]] for (uint i = 0; i < accum.length(); ++i) { + const uint out_idx = tid + i * WORKGROUP_SIZE; + const uint head_local = out_idx / HEAD_SIZE; + accum[i] *= old_scale_sh[head_local]; + } + + [[unroll]] for (uint dim_base = 0; dim_base < HEAD_SIZE; dim_base += DIMS_PER_BLOCK) { + [[unroll]] for (uint idx = tid; idx < KEYS_PER_BLOCK * (DIMS_PER_BLOCK / 4); idx += WORKGROUP_SIZE) { + const uint key_local = idx / (DIMS_PER_BLOCK / 4); + const uint d4 = idx % (DIMS_PER_BLOCK / 4); + const uint key = key_idx[key_local]; + f16vec4 value = f16vec4(0.0); + if (key < p.n_kv) { + const uint offset = stream * p.nbk3 + key * p.nbk1 + dim_base + d4 * 4; + value = f16vec4(data_k[offset], data_k[offset + 1], data_k[offset + 2], data_k[offset + 3]); + } + v_sh[key_local * V_STRIDE + d4] = value; + } + barrier(); + + coopmat pv = + coopmat(0.0); + coopmat pmat; + coopmat vmat; + + const uint pv_head_tile = gl_SubgroupID / (DIMS_PER_BLOCK / TILE); + const uint pv_dim_tile = gl_SubgroupID % (DIMS_PER_BLOCK / TILE); + [[unroll]] for (uint key_chunk = 0; key_chunk < KEYS_PER_BLOCK / TILE; ++key_chunk) { + coopMatLoad(pmat, p_sh, + pv_head_tile * TILE * P_STRIDE + key_chunk * (TILE / 4), + P_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + coopMatLoad(vmat, v_sh, + key_chunk * TILE * V_STRIDE + pv_dim_tile * (TILE / 4), + V_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + pv = coopMatMulAdd(pmat, vmat, pv); + } + + coopMatStore(pv, pv_sh, + pv_head_tile * TILE * PV_STRIDE + pv_dim_tile * (TILE / 4), PV_STRIDE, + gl_CooperativeMatrixLayoutRowMajor); + barrier(); + + [[unroll]] for (uint i = 0; i < accum.length(); ++i) { + const uint out_idx = tid + i * WORKGROUP_SIZE; + const uint head_local = out_idx / HEAD_SIZE; + const uint dim = out_idx % HEAD_SIZE; + if (dim >= dim_base && dim < dim_base + DIMS_PER_BLOCK) { + accum[i] += pv_sh[head_local * PV_STRIDE + (dim - dim_base) / 4][dim % 4]; + } + } + barrier(); + } + } + + if (p.has_sinks != 0 && tid < HEADS_PER_GROUP) { + const float sink = data_s[head_base + tid]; + const float new_max = max(row_max_sh[tid], sink); + const float old_scale = row_sum_sh[tid] == 0.0 ? 0.0 : exp(row_max_sh[tid] - new_max); + row_sum_sh[tid] = row_sum_sh[tid] * old_scale + exp(sink - new_max); + row_max_sh[tid] = new_max; + old_scale_sh[tid] = old_scale; + } + barrier(); + + if (p.has_sinks != 0) { + [[unroll]] for (uint i = 0; i < accum.length(); ++i) { + const uint out_idx = tid + i * WORKGROUP_SIZE; + accum[i] *= old_scale_sh[out_idx / HEAD_SIZE]; + } + } + [[unroll]] for (uint i = 0; i < accum.length(); ++i) { + const uint out_idx = tid + i * WORKGROUP_SIZE; + const uint head_local = out_idx / HEAD_SIZE; + const uint dim = out_idx % HEAD_SIZE; + const uint dst_base = stream * p.nb3 + token * p.nb2 + (head_base + head_local) * p.nb1; + const float inv_sum = row_sum_sh[head_local] == 0.0 ? 0.0 : 1.0 / row_sum_sh[head_local]; + data_dst[dst_base + dim] = accum[i] * inv_sum; + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 3232278e5881..db65e1d49ae5 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -817,6 +817,9 @@ void process_shaders() { string_to_spv("lightning_indexer_decode_cm_f16", "lightning_indexer_decode_cm.comp", {}); #endif string_to_spv("flash_attn_top_k_f16", "flash_attn_top_k.comp", {}); +#if defined(GGML_VULKAN_COOPMAT_GLSLC_SUPPORT) + string_to_spv("flash_attn_top_k_cm_f16", "flash_attn_top_k_cm.comp", {}); +#endif string_to_spv("flash_attn_gather_f16", "flash_attn_gather.comp", {}); string_to_spv("dsv4_hc_pre_f32", "dsv4_hc_pre.comp", {}); string_to_spv("dsv4_hc_comb_f32", "dsv4_hc_comb.comp", {}); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 248fbd0555f7..5534f798edf6 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11395,6 +11395,7 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext_top_k( 512, 4, 64, 128, false)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, true)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(1024, 64, 65, 128, false)); test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 128, 256, 512, false)); return test_cases; From d4cf90797fc52952393afe07c8a730619a0d1cfe Mon Sep 17 00:00:00 2001 From: Jaap Buurman Date: Thu, 13 Aug 2026 11:12:21 +0200 Subject: [PATCH 106/212] vulkan: split sparse prefill attention Assisted-by: Codex --- .../DSV4-vulkan-sparse-prefill-progress.md | 92 +++++++++++++++++ ggml/src/ggml-vulkan/ggml-vulkan.cpp | 99 ++++++++++++++++++- .../vulkan-shaders/flash_attn_base.glsl | 39 ++++---- .../vulkan-shaders/flash_attn_cm1.comp | 16 +-- .../vulkan-shaders/flash_attn_top_k.comp | 2 + .../vulkan-shaders/flash_attn_top_k_cm.comp | 38 +++++-- tests/test-backend-ops.cpp | 1 + 7 files changed, 251 insertions(+), 36 deletions(-) diff --git a/docs/development/DSV4-vulkan-sparse-prefill-progress.md b/docs/development/DSV4-vulkan-sparse-prefill-progress.md index 80bf562a0623..93c42476cfc1 100644 --- a/docs/development/DSV4-vulkan-sparse-prefill-progress.md +++ b/docs/development/DSV4-vulkan-sparse-prefill-progress.md @@ -257,3 +257,95 @@ GGML_VK_PERF_LOGGER=1 ./build/bin/test-backend-ops perf \ ``` Always run these sequentially. Do not run a compiler concurrently on this APU. + +## Experimental raw-prefix split prototype + +An uncommitted follow-up prototype was tested after commit `24c7ead76cc1a9631fa1b42b0bfa53a15169e1ba`. Check `git status` before continuing. It was initially tested with `GGML_VK_FA_TOPK_SPLIT=1`. After the successful 32k run, the split path was changed to default-on for a llama-server coherence test. Set `GGML_VK_FA_TOPK_SPLIT=0` to restore the single cooperative sparse kernel. + +Stage profiling on the exact production sparse shape (`kv=19200,nb=2048,n_kv_raw=2304,n_top_k=512,sinks=0`) showed: + +```text +cooperative QK and selected-K gather: 88.885 ms +softmax increment: 4.909 ms +cooperative PV and output increment: 128.399 ms +full cooperative sparse kernel: 222.193 ms +``` + +PV and output were 57.8% of the kernel. More importantly, most active keys are not sparse: all 2,304 raw-prefix rows are contiguous, while only 512 compressed rows use top-K indices. The prototype therefore makes two attention partitions: + +1. Ordinary optimized Vulkan cooperative FA processes the contiguous raw prefix. +2. The cooperative top-K shader processes only the 512 selected compressed rows. +3. The existing split-K reduction combines both online-softmax partitions and applies sinks. + +This preserves the exact raw-prefix, sparse top-K, causal mask, and softmax semantics. It reuses the existing ordinary FA and split-K reduction rather than adding a new subsystem. + +The exact-shape microbenchmark improved from 222.19 ms to 111.33 ms. Its steady split stages were about 62-64 ms raw-prefix FA, 43-46 ms selected sparse FA, and 3.6-3.9 ms reduction. + +Correctness validation: + +```bash +GGML_VK_FA_TOPK_SPLIT=1 ./build/bin/test-backend-ops test \ + -b Vulkan0 -o FLASH_ATTN_EXT -p 'n_top_k=' + +./build/bin/test-backend-ops test -b Vulkan0 -o FLASH_ATTN_EXT +``` + +Results were 8/8 sparse top-K cases and 13,296/13,296 complete Vulkan FA cases against the CPU reference. No NaN or Inf failure occurred. + +Canonical 32k prototype command: + +```bash +GGML_VK_FA_TOPK_SPLIT=1 GGML_VK_PERF_LOGGER=1 ./build/bin/llama-bench \ + -m ~/Projects/docker/localLLaMA/models/models--unsloth--DeepSeek-V4-Flash-0731-GGUF/snapshots/109848da2469efe1f1aab9e11acea08a065ccd4f/UD-IQ3_XXS/DeepSeek-V4-Flash-0731-UD-IQ3_XXS-00001-of-00004.gguf \ + -r 1 -d 32768 -p 2048 -ub 2048 -fa 1 -n 0 \ + > /tmp/dsv4-vulkan-split-32k.log 2>&1 +``` + +Final 32k result: + +```text +32k context, PP 2048, ub 2048 + +Old scalar: 112.29 tok/s, 18.1957 s total, 8.84496 s sparse FA +Committed coopmat: 152.32 tok/s, 13.4032 s total, 4.44701 s sparse FA +Experimental split: 208.70 tok/s, 9.7704 s total, 1.20170 s split sparse FA + +Experimental split stages: +raw-prefix FA: 0.210775 s total, 10.037 ms/layer +selected sparse FA: 0.908190 s total, 43.247 ms/layer +split reduction: 0.082732 s total, 3.940 ms/layer +Lightning Indexer: 1.110410 s total, 52.877 ms/layer +TOP_K: 0.077394 s total, 3.685 ms/layer +``` + +Relative to the committed cooperative path, throughput improved 37.0%, total Vulkan time fell 27.1%, and sparse FA time fell 73.0%. Relative to the old scalar path, throughput improved 85.9%, total Vulkan time fell 46.3%, and sparse FA time fell 86.4%. + +The main unresolved tradeoff is scratch memory. At PP2048, the two output partitions use about 539 MiB because each stores an f32 partial output for 512 dimensions x 64 heads x 2048 queries, plus L/M data. The device maximum storage-buffer range is checked and the code falls back when the allocation is unavailable. The path is temporarily default-on for a llama-server coherence test but should not be finalized without discussing this footprint. A likely next step is to avoid materializing both full output partitions, for example by directly merging the selected partition into the raw result or processing query tiles, while retaining the measured split-path speed. + +The old sparse selection heuristic required `total_k >= 3 * active_k`. For the coherence test it now selects sparse attention whenever `total_k > active_k`; equality still uses dense FA because no keys are pruned. The shape, capability, and allocation gates remain unchanged. + +Prototype logs: + +- `/tmp/dsv4-fa-exact-qk.log` +- `/tmp/dsv4-fa-exact-softmax.log` +- `/tmp/dsv4-fa-exact-full.log` +- `/tmp/dsv4-fa-exact-split.log` +- `/tmp/dsv4-fa-split-correctness.log` +- `/tmp/dsv4-fa-all-correctness.log` +- `/tmp/dsv4-vulkan-split-32k.log` + +## Llama-server coherence check + +After making the split path default-on and changing the sparse crossover to `total_k > active_k`, `llama-server` produced a coherent response from a 2,044-token prompt at significant context depth. The final profiler block confirmed that all 21 sparse-attention layers used the split path: + +```text +FA_TOP_K_RAW: 0.179223 s total, 8.534 ms/layer +FA_TOP_K_SELECTED: 0.886629 s total, 42.220 ms/layer +FA_TOP_K_REDUCE: 0.083240 s total, 3.964 ms/layer +Split sparse FA: 1.149092 s total, 54.719 ms/layer +Lightning Indexer: 1.054610 s total, 50.220 ms/layer +TOP_K: 0.044540 s total, 2.121 ms/layer +Total Vulkan: 9.797560 s +``` + +This closely matches the canonical PP2048 llama-bench result of 9.770 s total and 1.202 s split sparse FA. The focused sparse CPU-reference test was rerun after the crossover change and passed 8/8 cases. diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 6c2dcef824b8..bf5f889faa8f 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1997,6 +1997,8 @@ struct vk_op_flash_attn_top_k_push_constants { uint32_t nb1, nb2, nb3; float scale; uint32_t has_sinks; + uint32_t profile_stage; + uint32_t split_mode; }; static_assert(sizeof(vk_op_flash_attn_top_k_push_constants) <= 128); @@ -11458,11 +11460,11 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & return false; } const int64_t n_kv_active = n_kv_raw + top_k->ne[0]; - if (k->ne[1] < 3 * n_kv_active) { + if (k->ne[1] <= n_kv_active) { return false; } - const vk_op_flash_attn_top_k_push_constants pc = { + vk_op_flash_attn_top_k_push_constants pc = { (uint32_t) q->ne[1], (uint32_t) k->ne[1], (uint32_t) n_kv_raw, (uint32_t) top_k->ne[0], (uint32_t) q->ne[2], (uint32_t) (q->nb[1] / sizeof(float)), @@ -11477,14 +11479,105 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & (uint32_t) (dst->nb[1] / sizeof(float)), (uint32_t) (dst->nb[2] / sizeof(float)), (uint32_t) (dst->nb[3] / sizeof(float)), - scale, sinks != nullptr, + scale, sinks != nullptr, 0, 0, }; const vk_subbuffer q_buf = ggml_vk_tensor_subbuffer(ctx, q); const vk_subbuffer sinks_buf = sinks ? ggml_vk_tensor_subbuffer(ctx, sinks) : q_buf; static const char * top_k_cm_env = getenv("GGML_VK_FA_TOPK_CM"); const bool use_cm = (!top_k_cm_env || top_k_cm_env[0] != '0') && ctx->device->pipeline_flash_attn_top_k_cm_f16; + static const char * top_k_profile_env = getenv("GGML_VK_FA_TOPK_PROFILE"); + pc.profile_stage = use_cm && top_k_profile_env ? atoi(top_k_profile_env) : 0; vk_pipeline pipeline = use_cm ? ctx->device->pipeline_flash_attn_top_k_cm_f16 : ctx->device->pipeline_flash_attn_top_k_f16; + + static const char * top_k_split_env = getenv("GGML_VK_FA_TOPK_SPLIT"); + const uint32_t mask_stride = (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)); + const bool try_split = use_cm && (!top_k_split_env || top_k_split_env[0] != '0') && n_kv_raw > 0 && top_k->ne[0] > 0 && mask_stride <= 0xffff; + if (try_split) { + const uint32_t N = (uint32_t) q->ne[1]; + const uint32_t D = 512; + const uint32_t NH = 64; + const uint32_t NS = (uint32_t) q->ne[3]; + const uint32_t raw_kv = (uint32_t) n_kv_raw; + const uint32_t partitions = 2; + const bool f32acc = true; + vk_fa_tuning_params tuning = get_fa_tuning_params(ctx->device, D, D, N, raw_kv, GGML_TYPE_F16, GGML_TYPE_F16, f32acc); + + const uint32_t q_stride = (uint32_t) (q->nb[1] / sizeof(float)); + const uint32_t k_stride = (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)); + const bool aligned = raw_kv % tuning.block_cols == 0 && (q_stride & 7) == 0 && (k_stride & 7) == 0; + const vk_fa_pipeline_state raw_state = get_fa_pipeline_state(ctx->device, tuning, D, D, aligned, f32acc, + true, false, false, GGML_TYPE_F16, GGML_TYPE_F16); + if (raw_state.path == FA_COOPMAT1 && ctx->device->pipeline_flash_attn_split_k_reduce) { + vk_pipeline raw_pipeline; + { + std::lock_guard guard(ctx->device->compile_mutex); + auto & pipelines = ctx->device->pipeline_flash_attn_f32_f16; + auto it = pipelines.find(raw_state); + if (it != pipelines.end()) { + raw_pipeline = it->second; + } else { + pipelines[raw_state] = raw_pipeline = std::make_shared(); + } + } + ggml_pipeline_request_descriptor_sets(ctx, raw_pipeline, 1); + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_split_k_reduce, 1); + + const uint64_t split_size = ((uint64_t) D * NH * sizeof(float) + NH * 2 * sizeof(float)) * partitions * N * NS; + if (split_size <= ctx->device->properties.limits.maxStorageBufferRange) { + if (ctx->prealloc_size_split_k < split_size) { + ctx->prealloc_size_split_k = split_size; + ggml_vk_preallocate_buffers(ctx, subctx); + } + if (ctx->prealloc_split_k_need_sync) { + ggml_vk_sync_buffers(ctx, subctx); + } + + const vk_subbuffer split_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_split_k, 0); + const uint32_t n_head_log2 = 64; + const uint32_t packed_gqa = (mask_stride << 16) | 1; + const uint32_t packed_partitions = (partitions << 16) | 1; + const vk_flash_attn_push_constants raw_pc = { + N, raw_kv, + NH, N, NS, + NH, NS, + 1, NS, + 1, NS, + (uint32_t) mask->ne[1], (uint32_t) mask->ne[2], (uint32_t) mask->ne[3], + q_stride, (uint32_t) q->nb[2], (uint32_t) q->nb[3], + k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], + k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], + scale, 0.0f, 0.0f, + n_head_log2, 1.0f, 1.0f, + packed_gqa, raw_kv, packed_partitions, + }; + + ggml_vk_dispatch_pipeline(ctx, subctx, raw_pipeline, + {q_buf, ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, k), + ggml_vk_tensor_subbuffer(ctx, mask), q_buf, split_buf, q_buf}, + raw_pc, {N, NH, NS}); + ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_RAW (sub-op)"); + + pc.split_mode = 1; + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, + {q_buf, ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, mask), sinks_buf, + ggml_vk_tensor_subbuffer(ctx, top_k), split_buf}, + pc, {N, (uint32_t) CEIL_DIV(q->ne[2], 32), NS}); + ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_SELECTED (sub-op)"); + + ggml_vk_sync_buffers(ctx, subctx); + const vk_op_flash_attn_split_k_reduce_push_constants reduce_pc = {D, NH, N, NS, partitions, sinks != nullptr}; + ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_split_k_reduce, + {split_buf, sinks_buf, ggml_vk_tensor_subbuffer(ctx, dst)}, + reduce_pc, {NH, D, N * NS}); + ctx->prealloc_split_k_need_sync = true; + ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_REDUCE (sub-op)"); + return true; + } + } + } + ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, {q_buf, ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, mask), sinks_buf, diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl index 0ce4503a8847..800a79a97d17 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl @@ -124,7 +124,7 @@ ACC_TYPE perElemOpStoreCol0(const in uint32_t r, const in uint32_t c, const in A // Load the slope matrix, indexed by Q's dimension 2. ACC_TYPE perElemOpComputeSlope(const in uint32_t r, const in uint32_t c, const in ACC_TYPE elem, const in uint32_t iq2) { - const uint32_t h = iq2 + (r % p.gqa_ratio); + const uint32_t h = iq2 + (r % (p.gqa_ratio & 0xffff)); uint32_t n_head_log2 = p.mask_n_head_log2 & N_LOG2_MASK; @@ -137,32 +137,40 @@ ACC_TYPE perElemOpComputeSlope(const in uint32_t r, const in uint32_t c, const i // Load the sink value, indexed by Q's dimension 2. ACC_TYPE perElemOpGetSink(const in uint32_t r, const in uint32_t c, const in ACC_TYPE elem, const in uint32_t iq2) { - const uint32_t h = iq2 + (r % p.gqa_ratio); + const uint32_t h = iq2 + (r % (p.gqa_ratio & 0xffff)); return ACC_TYPE(data_s[h]); } uint32_t i, N, KV, split_k_index, Tr, start_j, end_j, gqa_iq1, iq2, iq3, rk2, rk3, rv2, rv3, ik2, ik3, iv2, iv3, - q_stride, k_stride, v_stride, m_stride; + q_stride, k_stride, v_stride, m_stride, gqa_ratio, split_k_num, output_k_num; +bool partial_output; void init_indices() { N = p.N; KV = p.KV; + gqa_ratio = p.gqa_ratio & 0xffff; + split_k_num = p.k_num & 0xffff; + output_k_num = p.k_num >> 16; + partial_output = output_k_num != 0; + if (!partial_output) { + output_k_num = split_k_num; + } - if (p.k_num > 1) { - if (p.gqa_ratio > 1) { + if (split_k_num > 1) { + if (gqa_ratio > 1) { i = 0; // batch and split_k share gl_WorkGroupID.x - gqa_iq1 = gl_WorkGroupID.x / p.k_num; - split_k_index = gl_WorkGroupID.x % p.k_num; + gqa_iq1 = gl_WorkGroupID.x / split_k_num; + split_k_index = gl_WorkGroupID.x % split_k_num; } else { gqa_iq1 = 0; - split_k_index = gl_WorkGroupID.x % p.k_num; - i = gl_WorkGroupID.x / p.k_num; + split_k_index = gl_WorkGroupID.x % split_k_num; + i = gl_WorkGroupID.x / split_k_num; } - } else if (p.gqa_ratio > 1) { + } else if (gqa_ratio > 1) { i = 0; gqa_iq1 = gl_WorkGroupID.x; split_k_index = 0; @@ -179,7 +187,7 @@ void init_indices() // When not using grouped query attention, all rows share the same iq2, equal to gl_WorkGroupID.y. // When using grouped query attention, each workgroup does gqa_ratio consecutive values of iq2. - iq2 = gl_WorkGroupID.y * p.gqa_ratio; + iq2 = gl_WorkGroupID.y * gqa_ratio; iq3 = gl_WorkGroupID.z; // broadcast factors @@ -200,14 +208,11 @@ void init_indices() // nb?1 are already divided by the type size and are in units of elements. // When using grouped query attention, Q is indexed by iq2, so the stride // should be nb02 (which is in bytes). - q_stride = p.gqa_ratio > 1 ? (p.nb02 / 4) : p.nb01; + q_stride = gqa_ratio > 1 ? (p.nb02 / 4) : p.nb01; k_stride = p.nb11; v_stride = p.nb21; - // When using grouped query attention, all rows use the same mask (stride 0). - // "p.gqa_ratio >> 16" is just a roundabout way of writing zero - // that prevents the compiler from folding the "&" through the select - // and breaking the alignment detection. - m_stride = (p.gqa_ratio > 1) ? (p.gqa_ratio >> 16) : KV; + const uint32_t mask_stride_override = p.gqa_ratio >> 16; + m_stride = mask_stride_override != 0 ? mask_stride_override : (gqa_ratio > 1 ? 0 : KV); } // Bias applied to softmax to stay in fp16 range. diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp index 057ed739aa8d..e3ea909a59ce 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp @@ -169,7 +169,7 @@ void main() { } // Only load if the block is not all zeros if (mask_opt_bits != MASK_OPT_ALL_ZERO) { - bool nem1_bounds_check = !(p.gqa_ratio > 1) && (p.nem1 % Br) != 0; + bool nem1_bounds_check = !(gqa_ratio > 1) && (p.nem1 % Br) != 0; float max_mask = NEG_FLT_MAX_OVER_2; [[unroll]] for (uint32_t idx = 0; idx < Bc * Br / 4; idx += gl_WorkGroupSize.x) { @@ -533,10 +533,10 @@ void main() { // If there is split_k, then the split_k resolve shader does the final // division by L. Store the intermediate O value and per-row m and L values. - if (p.k_num > 1) { - if (p.gqa_ratio > 1) { + if (partial_output || split_k_num > 1) { + if (gqa_ratio > 1) { // note: O and Q have swapped coord 1,2. - uint32_t o_offset = HSV * p.ne1 * (split_k_index + p.k_num * (gqa_iq1 + p.ne2 * iq3)) / 4; + uint32_t o_offset = HSV * p.ne1 * (split_k_index + output_k_num * (gqa_iq1 + p.ne2 * iq3)) / 4; [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { if (tile_row(r) < N) { @@ -549,7 +549,7 @@ void main() { } } - o_offset = HSV * p.ne1 * p.k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + p.k_num * (gqa_iq1 + p.ne2 * iq3)); + o_offset = HSV * p.ne1 * output_k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + output_k_num * (gqa_iq1 + p.ne2 * iq3)); [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { if (tile_row(r) < N) { perElemOpStoreCol0(tile_row(r), 0u, ACC_TYPE(Lf[r]), o_offset, iq2, N); @@ -562,7 +562,7 @@ void main() { const uint global_row = i * Br + row; if (global_row < N) { - uint32_t o_offset = HSV * p.ne1 * (split_k_index + p.k_num * (global_row + p.ne2 * iq3)) / 4; + uint32_t o_offset = HSV * p.ne1 * (split_k_index + output_k_num * (global_row + p.ne2 * iq3)) / 4; [[unroll]] for (uint32_t d0 = 0; d0 < HSV / 4; d0 += threads_per_rowgroup) { const uint d = d0 + col_tid; @@ -572,7 +572,7 @@ void main() { } if (global_row < N && col_tid == 0) { - uint32_t lm_offset = HSV * p.ne1 * p.k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + p.k_num * (global_row + p.ne2 * iq3)); + uint32_t lm_offset = HSV * p.ne1 * output_k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + output_k_num * (global_row + p.ne2 * iq3)); data_o[lm_offset + iq2] = D_TYPE(Lf[r]); data_o[lm_offset + p.ne1 + iq2] = D_TYPE(Mf[r]); } @@ -621,7 +621,7 @@ void main() { uint32_t o_offset = (gqa_iq1*p.ne1*HSV + iq3*p.ne2*p.ne1*HSV) / 4; - if (p.gqa_ratio > 1) { + if (gqa_ratio > 1) { [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { if (tile_row(r) < N) { [[unroll]] for (uint32_t d0 = 0; d0 < HSV / 4; d0 += threads_per_rowgroup) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp index b8b49c677bd5..62807471bdd1 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp @@ -37,6 +37,8 @@ layout(push_constant) uniform Parameters { uint nb3; float scale; uint has_sinks; + uint profile_stage; + uint split_mode; } p; // Shape constants pinned by the dispatch gate in ggml_vk_flash_attn_top_k: DeepSeek V4 diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp index 420a05375a7b..f3dfbe59ac00 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp @@ -39,6 +39,8 @@ layout(push_constant) uniform Parameters { uint nb3; float scale; uint has_sinks; + uint profile_stage; + uint split_mode; } p; const uint TILE = 16; @@ -71,7 +73,7 @@ void main() { const uint stream = gl_WorkGroupID.z; const uint mask_base = stream * p.nbm3 + token * p.nbm1; const uint top_base = stream * p.nbt3 + token * p.nbt1; - const uint total_keys = p.n_kv_raw + p.n_top_k; + const uint total_keys = p.split_mode != 0 ? p.n_top_k : p.n_kv_raw + p.n_top_k; float accum[HEADS_PER_GROUP * HEAD_SIZE / WORKGROUP_SIZE]; [[unroll]] for (uint i = 0; i < accum.length(); ++i) { @@ -88,10 +90,11 @@ void main() { if (tid < KEYS_PER_BLOCK) { const uint selected = kb + tid; uint key = p.n_kv; - if (selected < p.n_kv_raw) { + if (p.split_mode == 0 && selected < p.n_kv_raw) { key = selected; } else if (selected < total_keys) { - const int compressed = data_top[top_base + selected - p.n_kv_raw]; + const uint top_pos = p.split_mode != 0 ? selected : selected - p.n_kv_raw; + const int compressed = data_top[top_base + top_pos]; if (compressed >= 0 && uint(compressed) < p.n_kv - p.n_kv_raw) { key = p.n_kv_raw + uint(compressed); } @@ -144,6 +147,10 @@ void main() { SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); barrier(); + if (p.profile_stage == 1) { + continue; + } + { const uint head_local = tid / (SUBGROUP_SIZE / 4); const uint softmax_lane = tid % (SUBGROUP_SIZE / 4); @@ -194,6 +201,10 @@ void main() { accum[i] *= old_scale_sh[head_local]; } + if (p.profile_stage == 2) { + continue; + } + [[unroll]] for (uint dim_base = 0; dim_base < HEAD_SIZE; dim_base += DIMS_PER_BLOCK) { [[unroll]] for (uint idx = tid; idx < KEYS_PER_BLOCK * (DIMS_PER_BLOCK / 4); idx += WORKGROUP_SIZE) { const uint key_local = idx / (DIMS_PER_BLOCK / 4); @@ -242,7 +253,7 @@ void main() { } } - if (p.has_sinks != 0 && tid < HEADS_PER_GROUP) { + if (p.split_mode == 0 && p.has_sinks != 0 && tid < HEADS_PER_GROUP) { const float sink = data_s[head_base + tid]; const float new_max = max(row_max_sh[tid], sink); const float old_scale = row_sum_sh[tid] == 0.0 ? 0.0 : exp(row_max_sh[tid] - new_max); @@ -252,7 +263,7 @@ void main() { } barrier(); - if (p.has_sinks != 0) { + if (p.split_mode == 0 && p.has_sinks != 0) { [[unroll]] for (uint i = 0; i < accum.length(); ++i) { const uint out_idx = tid + i * WORKGROUP_SIZE; accum[i] *= old_scale_sh[out_idx / HEAD_SIZE]; @@ -262,8 +273,19 @@ void main() { const uint out_idx = tid + i * WORKGROUP_SIZE; const uint head_local = out_idx / HEAD_SIZE; const uint dim = out_idx % HEAD_SIZE; - const uint dst_base = stream * p.nb3 + token * p.nb2 + (head_base + head_local) * p.nb1; - const float inv_sum = row_sum_sh[head_local] == 0.0 ? 0.0 : 1.0 / row_sum_sh[head_local]; - data_dst[dst_base + dim] = accum[i] * inv_sum; + if (p.split_mode != 0) { + const uint part_idx = 1; + const uint matrix_base = HEAD_SIZE * p.n_head * (part_idx + 2 * (token + p.n_batch * stream)); + const uint lm_base = HEAD_SIZE * p.n_head * p.n_batch * 2 + p.n_head * 2 * (part_idx + 2 * (token + p.n_batch * stream)); + data_dst[matrix_base + (head_base + head_local) * HEAD_SIZE + dim] = accum[i]; + if (dim == 0) { + data_dst[lm_base + head_base + head_local] = row_sum_sh[head_local]; + data_dst[lm_base + p.n_head + head_base + head_local] = row_max_sh[head_local]; + } + } else { + const uint dst_base = stream * p.nb3 + token * p.nb2 + (head_base + head_local) * p.nb1; + const float inv_sum = row_sum_sh[head_local] == 0.0 ? 0.0 : 1.0 / row_sum_sh[head_local]; + data_dst[dst_base + dim] = accum[i] * inv_sum; + } } } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 5534f798edf6..c1629865ce48 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11898,6 +11898,7 @@ static std::vector> make_test_cases_perf() { test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, nb, 1024, 512, false)); } } + test_cases.emplace_back(new test_flash_attn_ext_top_k(19200, 2048, 2304, 512, false)); return test_cases; } From 57a64cb26af0928106cd239bf98668198bb40a8c Mon Sep 17 00:00:00 2001 From: Jaap Buurman Date: Thu, 13 Aug 2026 11:50:12 +0200 Subject: [PATCH 107/212] vulkan: tile sparse prefill scratch Assisted-by: Codex --- .../DSV4-vulkan-sparse-prefill-progress.md | 58 ++++++++++- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 97 +++++++++++-------- tests/test-backend-ops.cpp | 1 + 3 files changed, 117 insertions(+), 39 deletions(-) diff --git a/docs/development/DSV4-vulkan-sparse-prefill-progress.md b/docs/development/DSV4-vulkan-sparse-prefill-progress.md index 93c42476cfc1..5ace979a08df 100644 --- a/docs/development/DSV4-vulkan-sparse-prefill-progress.md +++ b/docs/development/DSV4-vulkan-sparse-prefill-progress.md @@ -320,7 +320,7 @@ TOP_K: 0.077394 s total, 3.685 ms/layer Relative to the committed cooperative path, throughput improved 37.0%, total Vulkan time fell 27.1%, and sparse FA time fell 73.0%. Relative to the old scalar path, throughput improved 85.9%, total Vulkan time fell 46.3%, and sparse FA time fell 86.4%. -The main unresolved tradeoff is scratch memory. At PP2048, the two output partitions use about 539 MiB because each stores an f32 partial output for 512 dimensions x 64 heads x 2048 queries, plus L/M data. The device maximum storage-buffer range is checked and the code falls back when the allocation is unavailable. The path is temporarily default-on for a llama-server coherence test but should not be finalized without discussing this footprint. A likely next step is to avoid materializing both full output partitions, for example by directly merging the selected partition into the raw result or processing query tiles, while retaining the measured split-path speed. +The initial split implementation used 538,968,064 bytes (514 MiB) of scratch at PP2048 because each of two partitions stored an f32 partial output for 512 dimensions x 64 heads x 2048 queries, plus L/M data. This was subsequently reduced by query tiling as described below. The old sparse selection heuristic required `total_k >= 3 * active_k`. For the coherence test it now selects sparse attention whenever `total_k > active_k`; equality still uses dense FA because no keys are pruned. The shape, capability, and allocation gates remain unchanged. @@ -349,3 +349,59 @@ Total Vulkan: 9.797560 s ``` This closely matches the canonical PP2048 llama-bench result of 9.770 s total and 1.202 s split sparse FA. The focused sparse CPU-reference test was rerun after the crossover change and passed 8/8 cases. + +## Tiled split scratch optimization + +Commit `4bbe53e4775f0707de8158e4977a16ab770829da` used two full PP2048 output partitions. The same two-partition algorithm now processes at most 256 query tokens per tile and reuses the split scratch between tiles. Q, mask, top-K, and destination descriptors are offset to the tile while K/V remain shared. A Vulkan pipeline barrier separates reuse of each scratch tile. + +Scratch at PP2048 changed from: + +```text +Before: 538,968,064 bytes (514 MiB) +After: 67,371,008 bytes (64.25 MiB) +Change: 8x reduction +``` + +The exact production-shape microbenchmark (`kv=19200,nb=2048,n_kv_raw=2304,n_top_k=512,sinks=0`) measured: + +```text +Full-batch two partitions: 114.65 ms +256-query tiled path: 113.70 ms +``` + +Two one-partition alternatives were tested and discarded. Merging the raw partial directly inside the cooperative selected shader measured 119.17 ms. Writing selected output separately and using a lightweight merge kernel measured 120.45 ms. Both cut scratch in half but regressed because writing selected output outside the contiguous split layout increased the selected stage from about 44 ms to about 51 ms. Query tiling preserves the faster memory layout. + +Canonical 32k result after tiling: + +```text +32k context, PP 2048, ub 2048 + +Full-batch split: 208.70 tok/s, 9.77039 s total, 1.20170 s sparse FA +Tiled split: 209.62 tok/s, 9.72897 s total, 1.18721 s sparse FA + +Tiled split stages: +raw-prefix FA: 0.192879 s total, 168 tile dispatches +selected sparse FA: 0.910153 s total, 168 tile dispatches +split reduction: 0.084179 s total, 168 tile dispatches +Lightning Indexer: 1.100230 s total +TOP_K: 0.075158 s total +``` + +The 168 dispatch count is eight tiles x 21 sparse-attention layers. Relative to the full-batch split path, throughput improved 0.44%, total Vulkan time fell 0.42%, and sparse FA time fell 1.21%. The main result is the 8x scratch reduction without a performance regression. + +Final correctness after removing the discarded merge prototypes: + +- focused sparse top-K suite: 9/9 passed against the CPU reference, including a 257-query tile-boundary case +- complete Vulkan Flash Attention suite: 13,296/13,296 passed +- no NaN or Inf failure + +Logs: + +- `/tmp/dsv4-fa-exact-fused.log` +- `/tmp/dsv4-fa-exact-merge.log` +- `/tmp/dsv4-fa-exact-two-part-current.log` +- `/tmp/dsv4-fa-exact-tiled.log` +- `/tmp/dsv4-fa-tiled-final-correctness.log` +- `/tmp/dsv4-fa-tiled-boundary-correctness.log` +- `/tmp/dsv4-fa-tiled-all-correctness.log` +- `/tmp/dsv4-vulkan-tiled-32k.log` diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index bf5f889faa8f..0cd804993d7c 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11500,6 +11500,8 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & const uint32_t NS = (uint32_t) q->ne[3]; const uint32_t raw_kv = (uint32_t) n_kv_raw; const uint32_t partitions = 2; + const uint32_t tile_size = std::min(N, 256u); + const uint32_t n_tiles = CEIL_DIV(N, tile_size); const bool f32acc = true; vk_fa_tuning_params tuning = get_fa_tuning_params(ctx->device, D, D, N, raw_kv, GGML_TYPE_F16, GGML_TYPE_F16, f32acc); @@ -11520,11 +11522,12 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & pipelines[raw_state] = raw_pipeline = std::make_shared(); } } - ggml_pipeline_request_descriptor_sets(ctx, raw_pipeline, 1); - ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); - ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_split_k_reduce, 1); + ggml_pipeline_request_descriptor_sets(ctx, raw_pipeline, n_tiles); + ggml_pipeline_request_descriptor_sets(ctx, pipeline, n_tiles); + ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_split_k_reduce, n_tiles); - const uint64_t split_size = ((uint64_t) D * NH * sizeof(float) + NH * 2 * sizeof(float)) * partitions * N * NS; + const uint64_t partition_size = ((uint64_t) D * NH + NH * 2) * sizeof(float) * tile_size * NS; + const uint64_t split_size = partition_size * partitions; if (split_size <= ctx->device->properties.limits.maxStorageBufferRange) { if (ctx->prealloc_size_split_k < split_size) { ctx->prealloc_size_split_k = split_size; @@ -11534,45 +11537,63 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & ggml_vk_sync_buffers(ctx, subctx); } - const vk_subbuffer split_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_split_k, 0); const uint32_t n_head_log2 = 64; const uint32_t packed_gqa = (mask_stride << 16) | 1; const uint32_t packed_partitions = (partitions << 16) | 1; - const vk_flash_attn_push_constants raw_pc = { - N, raw_kv, - NH, N, NS, - NH, NS, - 1, NS, - 1, NS, - (uint32_t) mask->ne[1], (uint32_t) mask->ne[2], (uint32_t) mask->ne[3], - q_stride, (uint32_t) q->nb[2], (uint32_t) q->nb[3], - k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], - k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], - scale, 0.0f, 0.0f, - n_head_log2, 1.0f, 1.0f, - packed_gqa, raw_kv, packed_partitions, + const vk_subbuffer split_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_split_k, 0); + const vk_subbuffer k_buf = ggml_vk_tensor_subbuffer(ctx, k); + const vk_subbuffer mask_buf = ggml_vk_tensor_subbuffer(ctx, mask); + const vk_subbuffer top_buf = ggml_vk_tensor_subbuffer(ctx, top_k); + const vk_subbuffer dst_buf = ggml_vk_tensor_subbuffer(ctx, dst); + const auto sliced = [](const vk_subbuffer & buf, uint64_t offset) { + return vk_subbuffer{buf.buffer, buf.offset + offset, buf.size - offset}; }; - ggml_vk_dispatch_pipeline(ctx, subctx, raw_pipeline, - {q_buf, ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, k), - ggml_vk_tensor_subbuffer(ctx, mask), q_buf, split_buf, q_buf}, - raw_pc, {N, NH, NS}); - ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_RAW (sub-op)"); - - pc.split_mode = 1; - ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, - {q_buf, ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, mask), sinks_buf, - ggml_vk_tensor_subbuffer(ctx, top_k), split_buf}, - pc, {N, (uint32_t) CEIL_DIV(q->ne[2], 32), NS}); - ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_SELECTED (sub-op)"); - - ggml_vk_sync_buffers(ctx, subctx); - const vk_op_flash_attn_split_k_reduce_push_constants reduce_pc = {D, NH, N, NS, partitions, sinks != nullptr}; - ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_split_k_reduce, - {split_buf, sinks_buf, ggml_vk_tensor_subbuffer(ctx, dst)}, - reduce_pc, {NH, D, N * NS}); - ctx->prealloc_split_k_need_sync = true; - ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_REDUCE (sub-op)"); + for (uint32_t tile = 0; tile < n_tiles; ++tile) { + if (tile != 0) { + ggml_vk_sync_buffers(ctx, subctx); + } + const uint32_t token_offset = tile * tile_size; + const uint32_t tile_n = std::min(tile_size, N - token_offset); + const vk_subbuffer tile_q = sliced(q_buf, (uint64_t) token_offset * q->nb[1]); + const vk_subbuffer tile_mask = sliced(mask_buf, (uint64_t) token_offset * mask->nb[1]); + const vk_subbuffer tile_top = sliced(top_buf, (uint64_t) token_offset * top_k->nb[1]); + const vk_subbuffer tile_dst = sliced(dst_buf, (uint64_t) token_offset * dst->nb[2]); + const vk_flash_attn_push_constants raw_pc = { + tile_n, raw_kv, + NH, tile_n, NS, + NH, NS, + 1, NS, + 1, NS, + (uint32_t) mask->ne[1], (uint32_t) mask->ne[2], (uint32_t) mask->ne[3], + q_stride, (uint32_t) q->nb[2], (uint32_t) q->nb[3], + k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], + k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], + scale, 0.0f, 0.0f, + n_head_log2, 1.0f, 1.0f, + packed_gqa, raw_kv, packed_partitions, + }; + + ggml_vk_dispatch_pipeline(ctx, subctx, raw_pipeline, + {tile_q, k_buf, k_buf, tile_mask, tile_q, split_buf, tile_q}, + raw_pc, {tile_n, NH, NS}); + ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_RAW (sub-op)"); + + pc.n_batch = tile_n; + pc.split_mode = 1; + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, + {tile_q, k_buf, tile_mask, sinks_buf, tile_top, split_buf}, + pc, {tile_n, (uint32_t) CEIL_DIV(q->ne[2], 32), NS}); + ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_SELECTED (sub-op)"); + + ctx->prealloc_split_k_need_sync = true; + ggml_vk_sync_buffers(ctx, subctx); + const vk_op_flash_attn_split_k_reduce_push_constants reduce_pc = {D, NH, tile_n, NS, partitions, sinks != nullptr}; + ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_split_k_reduce, + {split_buf, sinks_buf, tile_dst}, reduce_pc, {NH, D, tile_n * NS}); + ctx->prealloc_split_k_need_sync = true; + ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_REDUCE (sub-op)"); + } return true; } } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index c1629865ce48..5d4551550851 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11397,6 +11397,7 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, true)); test_cases.emplace_back(new test_flash_attn_ext_top_k(1024, 64, 65, 128, false)); test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 128, 256, 512, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 257, 256, 512, false)); return test_cases; } From 989c21b1c5cb1426ab3fdbf4314408ff48e4c8c9 Mon Sep 17 00:00:00 2001 From: Jaap Buurman Date: Thu, 13 Aug 2026 12:19:10 +0200 Subject: [PATCH 108/212] vulkan: reuse sparse FA probability fragments Assisted-by: Codex --- .../DSV4-vulkan-sparse-prefill-progress.md | 85 +++++++++++++++++++ .../vulkan-shaders/flash_attn_top_k_cm.comp | 27 +++--- 2 files changed, 100 insertions(+), 12 deletions(-) diff --git a/docs/development/DSV4-vulkan-sparse-prefill-progress.md b/docs/development/DSV4-vulkan-sparse-prefill-progress.md index 5ace979a08df..82843486bc0f 100644 --- a/docs/development/DSV4-vulkan-sparse-prefill-progress.md +++ b/docs/development/DSV4-vulkan-sparse-prefill-progress.md @@ -405,3 +405,88 @@ Logs: - `/tmp/dsv4-fa-tiled-boundary-correctness.log` - `/tmp/dsv4-fa-tiled-all-correctness.log` - `/tmp/dsv4-vulkan-tiled-32k.log` + +## Selected PV probability reuse + +The selected cooperative-matrix stage remained the largest split sparse-FA component. Profiling the exact tiled production shape showed: + +```text +selected K gather and cooperative QK: about 20.0 ms +QK plus softmax: about 22.3 ms +full selected stage: about 45.5 ms +cooperative PV and output increment: about 23.2 ms +``` + +PV and output were about 51% of the selected stage. The shader previously loaded each of four 16x16 probability cooperative-matrix fragments again for every one of the eight 64-dimension PV passes. The optimized shader loads these four fragments once per 64-key block and retains them across all PV dimension passes. This follows the probability-fragment lifetime used by ordinary cooperative Vulkan FA. + +The shader also aliases the shared score and PV-output matrices because their lifetimes do not overlap. Shader resource statistics changed as follows: + +```text + Before After +VGPRs 192 192 +VGPR spills 0 0 +LDS 36,864 28,672 bytes +static instructions 11,524 11,358 +``` + +The exact production-shape microbenchmark changed from 113.70 ms for the committed tiled path to 110.11 ms. The selected stage fell from about 45.5 ms to about 43.7 ms. The raw-prefix and reduction implementations are unchanged. + +Two canonical 32k runs after this change measured: + +```text +32k context, PP 2048, ub 2048 + +Committed tiled reference: +209.62 tok/s +Total Vulkan: 9.72897 s +Split sparse FA: 1.18721 s + raw-prefix FA: 0.192879 s + selected sparse FA: 0.910153 s + split reduction: 0.084179 s +Lightning Indexer: 1.100230 s +TOP_K: 0.075158 s + +Probability reuse, run 1: +211.03 tok/s +Total Vulkan: 9.66363 s +Split sparse FA: 1.12776 s + raw-prefix FA: 0.192078 s + selected sparse FA: 0.849862 s + split reduction: 0.085821 s +Lightning Indexer: 1.098980 s +TOP_K: 0.071036 s + +Probability reuse, run 2: +209.57 tok/s +Total Vulkan: 9.72968 s +Split sparse FA: 1.13553 s + raw-prefix FA: 0.192987 s + selected sparse FA: 0.859890 s + split reduction: 0.082653 s +Lightning Indexer: 1.102550 s +TOP_K: 0.071474 s +``` + +The two-run selected-stage improvement is 5.5-6.6%, and the complete split sparse-FA improvement is 4.4-5.0%. The two-run throughput mean is 210.30 tok/s, 0.32% above the 209.62 tok/s reference. End-to-end noise in unrelated model kernels is larger than this small total-throughput change, but both profiler runs isolate a consistent gain in the modified selected stage. + +Correctness after the change: + +- focused sparse top-K suite: 9/9 passed against the CPU reference +- complete Vulkan Flash Attention suite: 13,297/13,297 passed +- pipeline statistics probe: 1/1 passed, with no SGPR or VGPR spills +- no NaN or Inf failure + +Logs: + +- `/tmp/dsv4-fa-selected-qk-tiled.log` +- `/tmp/dsv4-fa-selected-softmax-tiled.log` +- `/tmp/dsv4-fa-lds-alias-stats.log` +- `/tmp/dsv4-fa-pmat-stats.log` +- `/tmp/dsv4-fa-exact-lds-alias.log` +- `/tmp/dsv4-fa-exact-pmat.log` +- `/tmp/dsv4-fa-pmat-correctness.log` +- `/tmp/dsv4-fa-pmat-all-correctness.log` +- `/tmp/dsv4-vulkan-32k-pmat.log` +- `/tmp/dsv4-vulkan-32k-pmat-repeat.log` + +The next sparse-FA optimization should continue to target the selected PV/output half. The retained probability fragments remove redundant cooperative loads without increasing reported VGPR allocation. More invasive changes such as doubling the PV dimension tile can reduce barriers but must be designed around the eight available wave64 subgroups, 64 KiB LDS limit, and already high 192-VGPR allocation. Do not build a crossover matrix until the next kernel layout is settled because an optimization can shift the crossover. diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp index f3dfbe59ac00..d4bc4011d076 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp @@ -58,10 +58,9 @@ const float MASK_NEG_INF = -65500.0; shared uint key_idx[KEYS_PER_BLOCK]; shared f16vec4 q_sh[HEADS_PER_GROUP * QK_STRIDE]; shared f16vec4 k_sh[KEYS_PER_BLOCK * QK_STRIDE]; -shared vec4 score_sh[KEYS_PER_BLOCK * SCORE_STRIDE]; +shared vec4 matrix_sh[KEYS_PER_BLOCK * SCORE_STRIDE]; shared f16vec4 p_sh[HEADS_PER_GROUP * P_STRIDE]; shared f16vec4 v_sh[KEYS_PER_BLOCK * V_STRIDE]; -shared vec4 pv_sh[HEADS_PER_GROUP * PV_STRIDE]; shared float old_scale_sh[HEADS_PER_GROUP]; shared float row_max_sh[HEADS_PER_GROUP]; shared float row_sum_sh[HEADS_PER_GROUP]; @@ -142,7 +141,7 @@ void main() { const uint score_key_chunk = gl_SubgroupID % (KEYS_PER_BLOCK / TILE); const uint score_head_tile = gl_SubgroupID / (KEYS_PER_BLOCK / TILE); - coopMatStore(scores, score_sh, + coopMatStore(scores, matrix_sh, score_key_chunk * TILE * SCORE_STRIDE + score_head_tile * (TILE / 4), SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); barrier(); @@ -159,7 +158,7 @@ void main() { const uint key_local = softmax_lane + i * (SUBGROUP_SIZE / 4); const uint key = key_idx[key_local]; const float mask = key < p.n_kv ? float(data_m[mask_base + key]) : uintBitsToFloat(0xff800000); - const float score = float(score_sh[key_local * SCORE_STRIDE + head_local / 4][head_local % 4]) * p.scale + mask; + const float score = float(matrix_sh[key_local * SCORE_STRIDE + head_local / 4][head_local % 4]) * p.scale + mask; block_max = mask < MASK_NEG_INF ? block_max : max(block_max, score); } [[unroll]] for (uint delta = 1; delta < SUBGROUP_SIZE / 4; delta *= 2) { @@ -177,7 +176,7 @@ void main() { const float mask = key < p.n_kv ? float(data_m[mask_base + key]) : uintBitsToFloat(0xff800000); float weight = 0.0; if (mask >= MASK_NEG_INF) { - const float score = float(score_sh[key_local * SCORE_STRIDE + head_local / 4][head_local % 4]) * p.scale + mask; + const float score = float(matrix_sh[key_local * SCORE_STRIDE + head_local / 4][head_local % 4]) * p.scale + mask; weight = exp(score - new_max); block_sum += weight; } @@ -205,6 +204,14 @@ void main() { continue; } + coopmat pmats[KEYS_PER_BLOCK / TILE]; + [[unroll]] for (uint key_chunk = 0; key_chunk < KEYS_PER_BLOCK / TILE; ++key_chunk) { + const uint pv_head_tile = gl_SubgroupID / (DIMS_PER_BLOCK / TILE); + coopMatLoad(pmats[key_chunk], p_sh, + pv_head_tile * TILE * P_STRIDE + key_chunk * (TILE / 4), + P_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + } + [[unroll]] for (uint dim_base = 0; dim_base < HEAD_SIZE; dim_base += DIMS_PER_BLOCK) { [[unroll]] for (uint idx = tid; idx < KEYS_PER_BLOCK * (DIMS_PER_BLOCK / 4); idx += WORKGROUP_SIZE) { const uint key_local = idx / (DIMS_PER_BLOCK / 4); @@ -221,22 +228,18 @@ void main() { coopmat pv = coopmat(0.0); - coopmat pmat; coopmat vmat; const uint pv_head_tile = gl_SubgroupID / (DIMS_PER_BLOCK / TILE); const uint pv_dim_tile = gl_SubgroupID % (DIMS_PER_BLOCK / TILE); [[unroll]] for (uint key_chunk = 0; key_chunk < KEYS_PER_BLOCK / TILE; ++key_chunk) { - coopMatLoad(pmat, p_sh, - pv_head_tile * TILE * P_STRIDE + key_chunk * (TILE / 4), - P_STRIDE, gl_CooperativeMatrixLayoutRowMajor); coopMatLoad(vmat, v_sh, key_chunk * TILE * V_STRIDE + pv_dim_tile * (TILE / 4), V_STRIDE, gl_CooperativeMatrixLayoutRowMajor); - pv = coopMatMulAdd(pmat, vmat, pv); + pv = coopMatMulAdd(pmats[key_chunk], vmat, pv); } - coopMatStore(pv, pv_sh, + coopMatStore(pv, matrix_sh, pv_head_tile * TILE * PV_STRIDE + pv_dim_tile * (TILE / 4), PV_STRIDE, gl_CooperativeMatrixLayoutRowMajor); barrier(); @@ -246,7 +249,7 @@ void main() { const uint head_local = out_idx / HEAD_SIZE; const uint dim = out_idx % HEAD_SIZE; if (dim >= dim_base && dim < dim_base + DIMS_PER_BLOCK) { - accum[i] += pv_sh[head_local * PV_STRIDE + (dim - dim_base) / 4][dim % 4]; + accum[i] += matrix_sh[head_local * PV_STRIDE + (dim - dim_base) / 4][dim % 4]; } } barrier(); From 94ecd38a20042244c10f668c4474b16449987580 Mon Sep 17 00:00:00 2001 From: Jaap Buurman Date: Thu, 13 Aug 2026 12:59:50 +0200 Subject: [PATCH 109/212] vulkan: cache sparse FA masks per key block Assisted-by: Codex --- .../DSV4-vulkan-sparse-prefill-progress.md | 103 +++++++++++++++++- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 7 +- .../vulkan-shaders/flash_attn_base.glsl | 3 +- .../vulkan-shaders/flash_attn_top_k_cm.comp | 30 +++-- tests/test-backend-ops.cpp | 5 +- 5 files changed, 123 insertions(+), 25 deletions(-) diff --git a/docs/development/DSV4-vulkan-sparse-prefill-progress.md b/docs/development/DSV4-vulkan-sparse-prefill-progress.md index 82843486bc0f..e880ae63e99e 100644 --- a/docs/development/DSV4-vulkan-sparse-prefill-progress.md +++ b/docs/development/DSV4-vulkan-sparse-prefill-progress.md @@ -262,7 +262,7 @@ Always run these sequentially. Do not run a compiler concurrently on this APU. An uncommitted follow-up prototype was tested after commit `24c7ead76cc1a9631fa1b42b0bfa53a15169e1ba`. Check `git status` before continuing. It was initially tested with `GGML_VK_FA_TOPK_SPLIT=1`. After the successful 32k run, the split path was changed to default-on for a llama-server coherence test. Set `GGML_VK_FA_TOPK_SPLIT=0` to restore the single cooperative sparse kernel. -Stage profiling on the exact production sparse shape (`kv=19200,nb=2048,n_kv_raw=2304,n_top_k=512,sinks=0`) showed: +Stage profiling on the previously used 19,200-row sparse shape (`kv=19200,nb=2048,n_kv_raw=2304,n_top_k=512,sinks=0`) showed: ```text cooperative QK and selected-K gather: 88.885 ms @@ -362,7 +362,7 @@ After: 67,371,008 bytes (64.25 MiB) Change: 8x reduction ``` -The exact production-shape microbenchmark (`kv=19200,nb=2048,n_kv_raw=2304,n_top_k=512,sinks=0`) measured: +The previously used 19,200-row microbenchmark (`kv=19200,nb=2048,n_kv_raw=2304,n_top_k=512,sinks=0`) measured: ```text Full-batch two partitions: 114.65 ms @@ -408,7 +408,7 @@ Logs: ## Selected PV probability reuse -The selected cooperative-matrix stage remained the largest split sparse-FA component. Profiling the exact tiled production shape showed: +The selected cooperative-matrix stage remained the largest split sparse-FA component. Profiling the tiled 19,200-row shape showed: ```text selected K gather and cooperative QK: about 20.0 ms @@ -429,7 +429,7 @@ LDS 36,864 28,672 bytes static instructions 11,524 11,358 ``` -The exact production-shape microbenchmark changed from 113.70 ms for the committed tiled path to 110.11 ms. The selected stage fell from about 45.5 ms to about 43.7 ms. The raw-prefix and reduction implementations are unchanged. +The 19,200-row microbenchmark changed from 113.70 ms for the committed tiled path to 110.11 ms. The selected stage fell from about 45.5 ms to about 43.7 ms. The raw-prefix and reduction implementations are unchanged. Two canonical 32k runs after this change measured: @@ -490,3 +490,98 @@ Logs: - `/tmp/dsv4-vulkan-32k-pmat-repeat.log` The next sparse-FA optimization should continue to target the selected PV/output half. The retained probability fragments remove redundant cooperative loads without increasing reported VGPR allocation. More invasive changes such as doubling the PV dimension tile can reduce barriers but must be designed around the eight available wave64 subgroups, 64 KiB LDS limit, and already high 192-VGPR allocation. Do not build a crossover matrix until the next kernel layout is settled because an optimization can shift the crossover. + +## Selected mask caching and deep-context micro matrix + +The sparse FA `kv` dimension is compressed K/V rows, not source-token context depth. For the tested DeepSeek V4 graph, a PP2048 batch uses 2,304 raw rows and approximately one compressed row per four source tokens. The useful synthetic mapping is: + +```text +Source context Sparse FA kv rows +32k 11,008 +64k 19,200 +128k 35,584 +256k 68,352 +512k 133,888 +``` + +The performance test registry now contains PP2048 cases at all five K extents with `n_kv_raw=2304` and `n_top_k=512`. Use this command template and replace `KV` with a value from the table: + +```bash +GGML_VK_PERF_LOGGER=1 ./build/bin/test-backend-ops perf \ + -b Vulkan0 -o FLASH_ATTN_EXT \ + -p 'kv=KV,nb=2048,n_kv_raw=2304,n_top_k=512,sinks=0' +``` + +These are synthetic sparse-FA depth tests. They do not include the Lightning Indexer or the rest of the model graph and do not replace the canonical 32k llama-bench. + +The selected shader previously loaded the same selected-key mask value independently for all 32 heads during both softmax passes. The shader now loads each mask value once per 64-key block and reuses it from LDS. Q and probability storage also share one LDS allocation, while K and V staging share another because both pairs have disjoint lifetimes. + +Shader resources changed from the probability-reuse commit: + +```text + Before After +VGPRs 192 192 +VGPR spills 0 0 +LDS 28,672 24,576 bytes +static instructions 11,358 11,233 +``` + +A 128-dimension V staging experiment was correct but rejected. With operand aliasing it used exactly 32 KiB LDS. It was 2.3% slower at simulated 32k, approximately equal at 64k, and 1.6% faster at 128k. The retained 64-dimension layout is better for the canonical 32k target and is simpler. + +Selected-mask caching improved every synthetic depth relative to the same 64-dimension operand-alias layout: + +```text +Depth Selected before Selected after Split total before Split total after +32k 43.16 ms 41.23 ms 109.45 ms 108.07 ms +64k 43.22 ms 41.42 ms 110.45 ms 109.11 ms +128k 52.26 ms 48.74 ms 119.15 ms 116.92 ms +256k 51.64 ms 49.02 ms 118.06 ms 116.93 ms +512k 51.33 ms 49.20 ms 117.87 ms 116.27 ms +``` + +The 256k case exposed a separate path-selection cutoff. The raw-prefix ordinary FA dispatch encoded its mask row stride in 16 bits and disabled split sparse FA above 65,535 K rows. It now uses a flagged convention that carries the full 32-bit mask stride in the existing `split_kv` push constant for this one-partition partial-output dispatch. No push-constant structure was enlarged. At simulated 256k, this changes the selected path from unsplit cooperative sparse FA at 206.22 ms to split sparse FA at 118.06 ms before mask caching, a 42.8% reduction. The 512k case also uses the split path successfully. + +Canonical 32k PP2048 after operand aliasing and selected-mask caching: + +```text +Previous commit, two runs: +209.57-211.03 tok/s +Total Vulkan: 9.664-9.730 s +Split sparse FA: 1.128-1.136 s + raw-prefix FA: 0.192-0.193 s + selected sparse FA: 0.850-0.860 s + split reduction: 0.083-0.086 s + +Current result: +210.47 tok/s +Total Vulkan: 9.68921 s +Split sparse FA: 1.10160 s + raw-prefix FA: 0.192860 s + selected sparse FA: 0.821381 s + split reduction: 0.087354 s +Lightning Indexer: 1.102230 s +TOP_K: 0.072488 s +``` + +The selected stage improves 3.4-4.5% and complete split sparse FA improves 2.3-3.0% relative to the previous commit's two canonical runs. End-to-end throughput remains within model-wide benchmark noise. + +Correctness: + +- focused sparse top-K suite: 9/9 passed +- complete Vulkan Flash Attention suite: 13,297/13,297 passed +- no NaN or Inf failure +- simulated 256k and 512k performance cases both selected the split path and completed successfully + +Benchmark policy for this laptop APU: do not repeat llama-bench when the exact-shape microbenchmarks and the first canonical profiler block agree. Sustained load can lower GPU clocks and bias a repeat. Repeat only when the first result is anomalous or contradicts the microbenchmarks. Never build and benchmark concurrently. + +Logs: + +- `/tmp/dsv4-fa-mask-cache-32k.log` +- `/tmp/dsv4-fa-mask-cache-64k.log` +- `/tmp/dsv4-fa-mask-cache-128k.log` +- `/tmp/dsv4-fa-mask-cache-256k.log` +- `/tmp/dsv4-fa-mask-cache-512k.log` +- `/tmp/dsv4-fa-final-micro-32k.log` +- `/tmp/dsv4-fa-final-focused-correctness.log` +- `/tmp/dsv4-fa-mask-cache-all-correctness.log` +- `/tmp/dsv4-vulkan-32k-mask-cache.log` diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 0cd804993d7c..5ba53b06a5db 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11492,7 +11492,7 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & static const char * top_k_split_env = getenv("GGML_VK_FA_TOPK_SPLIT"); const uint32_t mask_stride = (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)); - const bool try_split = use_cm && (!top_k_split_env || top_k_split_env[0] != '0') && n_kv_raw > 0 && top_k->ne[0] > 0 && mask_stride <= 0xffff; + const bool try_split = use_cm && (!top_k_split_env || top_k_split_env[0] != '0') && n_kv_raw > 0 && top_k->ne[0] > 0; if (try_split) { const uint32_t N = (uint32_t) q->ne[1]; const uint32_t D = 512; @@ -11538,7 +11538,8 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & } const uint32_t n_head_log2 = 64; - const uint32_t packed_gqa = (mask_stride << 16) | 1; + const uint32_t mask_stride_in_split_kv = 1u << 31; + const uint32_t packed_gqa = mask_stride_in_split_kv | 1u; const uint32_t packed_partitions = (partitions << 16) | 1; const vk_subbuffer split_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_split_k, 0); const vk_subbuffer k_buf = ggml_vk_tensor_subbuffer(ctx, k); @@ -11571,7 +11572,7 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], scale, 0.0f, 0.0f, n_head_log2, 1.0f, 1.0f, - packed_gqa, raw_kv, packed_partitions, + packed_gqa, mask_stride, packed_partitions, }; ggml_vk_dispatch_pipeline(ctx, subctx, raw_pipeline, diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl index 800a79a97d17..8fba7a450f7e 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl @@ -211,8 +211,9 @@ void init_indices() q_stride = gqa_ratio > 1 ? (p.nb02 / 4) : p.nb01; k_stride = p.nb11; v_stride = p.nb21; + const bool mask_stride_in_split_kv = (p.gqa_ratio & 0x80000000u) != 0; const uint32_t mask_stride_override = p.gqa_ratio >> 16; - m_stride = mask_stride_override != 0 ? mask_stride_override : (gqa_ratio > 1 ? 0 : KV); + m_stride = mask_stride_in_split_kv ? p.split_kv : (mask_stride_override != 0 ? mask_stride_override : (gqa_ratio > 1 ? 0 : KV)); } // Bias applied to softmax to stay in fp16 range. diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp index d4bc4011d076..3d0024553e7a 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp @@ -56,11 +56,10 @@ const uint PV_STRIDE = DIMS_PER_BLOCK / 4; const float MASK_NEG_INF = -65500.0; shared uint key_idx[KEYS_PER_BLOCK]; -shared f16vec4 q_sh[HEADS_PER_GROUP * QK_STRIDE]; -shared f16vec4 k_sh[KEYS_PER_BLOCK * QK_STRIDE]; +shared float key_mask[KEYS_PER_BLOCK]; +shared f16vec4 q_p_sh[HEADS_PER_GROUP * P_STRIDE]; +shared f16vec4 k_v_sh[KEYS_PER_BLOCK * V_STRIDE]; shared vec4 matrix_sh[KEYS_PER_BLOCK * SCORE_STRIDE]; -shared f16vec4 p_sh[HEADS_PER_GROUP * P_STRIDE]; -shared f16vec4 v_sh[KEYS_PER_BLOCK * V_STRIDE]; shared float old_scale_sh[HEADS_PER_GROUP]; shared float row_max_sh[HEADS_PER_GROUP]; shared float row_sum_sh[HEADS_PER_GROUP]; @@ -99,6 +98,7 @@ void main() { } } key_idx[tid] = key; + key_mask[tid] = key < p.n_kv ? float(data_m[mask_base + key]) : uintBitsToFloat(0xff800000); } barrier(); @@ -117,23 +117,23 @@ void main() { const uint offset = stream * p.nbk3 + key * p.nbk1 + d + d4 * 4; value = f16vec4(data_k[offset], data_k[offset + 1], data_k[offset + 2], data_k[offset + 3]); } - k_sh[key_local * QK_STRIDE + d4] = value; + k_v_sh[key_local * QK_STRIDE + d4] = value; } if (tid < HEADS_PER_GROUP * (TILE / 4)) { const uint head_local = tid / (TILE / 4); const uint d4 = tid % (TILE / 4); const uint head = head_base + head_local; const uint offset = stream * p.nbq3 + head * p.nbq2 + token * p.nbq1 + d + d4 * 4; - q_sh[head_local * QK_STRIDE + d4] = f16vec4( + q_p_sh[head_local * QK_STRIDE + d4] = f16vec4( data_q[offset], data_q[offset + 1], data_q[offset + 2], data_q[offset + 3]); } barrier(); const uint key_chunk = gl_SubgroupID % (KEYS_PER_BLOCK / TILE); const uint head_tile = gl_SubgroupID / (KEYS_PER_BLOCK / TILE); - coopMatLoad(kmat, k_sh, key_chunk * TILE * QK_STRIDE, + coopMatLoad(kmat, k_v_sh, key_chunk * TILE * QK_STRIDE, QK_STRIDE, gl_CooperativeMatrixLayoutRowMajor); - coopMatLoad(qmat, q_sh, head_tile * TILE * QK_STRIDE, + coopMatLoad(qmat, q_p_sh, head_tile * TILE * QK_STRIDE, QK_STRIDE, gl_CooperativeMatrixLayoutColumnMajor); scores = coopMatMulAdd(kmat, qmat, scores); barrier(); @@ -156,8 +156,7 @@ void main() { float block_max = uintBitsToFloat(0xff800000); [[unroll]] for (uint i = 0; i < KEYS_PER_BLOCK / (SUBGROUP_SIZE / 4); ++i) { const uint key_local = softmax_lane + i * (SUBGROUP_SIZE / 4); - const uint key = key_idx[key_local]; - const float mask = key < p.n_kv ? float(data_m[mask_base + key]) : uintBitsToFloat(0xff800000); + const float mask = key_mask[key_local]; const float score = float(matrix_sh[key_local * SCORE_STRIDE + head_local / 4][head_local % 4]) * p.scale + mask; block_max = mask < MASK_NEG_INF ? block_max : max(block_max, score); } @@ -172,15 +171,14 @@ void main() { float block_sum = 0.0; [[unroll]] for (uint i = 0; i < KEYS_PER_BLOCK / (SUBGROUP_SIZE / 4); ++i) { const uint key_local = softmax_lane + i * (SUBGROUP_SIZE / 4); - const uint key = key_idx[key_local]; - const float mask = key < p.n_kv ? float(data_m[mask_base + key]) : uintBitsToFloat(0xff800000); + const float mask = key_mask[key_local]; float weight = 0.0; if (mask >= MASK_NEG_INF) { const float score = float(matrix_sh[key_local * SCORE_STRIDE + head_local / 4][head_local % 4]) * p.scale + mask; weight = exp(score - new_max); block_sum += weight; } - p_sh[head_local * P_STRIDE + key_local / 4][key_local % 4] = float16_t(weight); + q_p_sh[head_local * P_STRIDE + key_local / 4][key_local % 4] = float16_t(weight); } [[unroll]] for (uint delta = 1; delta < SUBGROUP_SIZE / 4; delta *= 2) { block_sum += subgroupShuffleXor(block_sum, delta); @@ -207,7 +205,7 @@ void main() { coopmat pmats[KEYS_PER_BLOCK / TILE]; [[unroll]] for (uint key_chunk = 0; key_chunk < KEYS_PER_BLOCK / TILE; ++key_chunk) { const uint pv_head_tile = gl_SubgroupID / (DIMS_PER_BLOCK / TILE); - coopMatLoad(pmats[key_chunk], p_sh, + coopMatLoad(pmats[key_chunk], q_p_sh, pv_head_tile * TILE * P_STRIDE + key_chunk * (TILE / 4), P_STRIDE, gl_CooperativeMatrixLayoutRowMajor); } @@ -222,7 +220,7 @@ void main() { const uint offset = stream * p.nbk3 + key * p.nbk1 + dim_base + d4 * 4; value = f16vec4(data_k[offset], data_k[offset + 1], data_k[offset + 2], data_k[offset + 3]); } - v_sh[key_local * V_STRIDE + d4] = value; + k_v_sh[key_local * V_STRIDE + d4] = value; } barrier(); @@ -233,7 +231,7 @@ void main() { const uint pv_head_tile = gl_SubgroupID / (DIMS_PER_BLOCK / TILE); const uint pv_dim_tile = gl_SubgroupID % (DIMS_PER_BLOCK / TILE); [[unroll]] for (uint key_chunk = 0; key_chunk < KEYS_PER_BLOCK / TILE; ++key_chunk) { - coopMatLoad(vmat, v_sh, + coopMatLoad(vmat, k_v_sh, key_chunk * TILE * V_STRIDE + pv_dim_tile * (TILE / 4), V_STRIDE, gl_CooperativeMatrixLayoutRowMajor); pv = coopMatMulAdd(pmats[key_chunk], vmat, pv); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 5d4551550851..23913f627007 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11899,7 +11899,10 @@ static std::vector> make_test_cases_perf() { test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, nb, 1024, 512, false)); } } - test_cases.emplace_back(new test_flash_attn_ext_top_k(19200, 2048, 2304, 512, false)); + // PP2048 compressed-K rows for source context depths 32k through 512k. + for (int kv : { 11008, 19200, 35584, 68352, 133888 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, 2048, 2304, 512, false)); + } return test_cases; } From 5f60911fe26e6ff2a537a5697c020e37951ba079 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 13 Aug 2026 14:32:06 +0000 Subject: [PATCH 110/212] vulkan: fix DeepSeek V4 sparse split attention with multiple sequences The raw/selected split path mis-indexed three things once q->ne[3] > 1. All three are inert at a single sequence, so the existing tests never reached them. - flash_attn_top_k_cm.comp wrote the L/M rows at a base that omitted the stream count. The split buffer is [O matrices][L/M rows] with both regions spanning ne3, so with more than one sequence the selected partition's L/M landed inside the O region and corrupted partition 0. - The mask's per-stream offset stepped by nem1 * KV. That assumes the mask row length equals KV, which the split path breaks: it overrides the mask stride to the full K range while KV covers only the raw prefix. Separate the two quantities - m_stride is the row-to-row step inside the tile (0 under GQA), m_row_len is the mask's real row length used to step between tokens and streams. They coincide everywhere except GQA and this path. - Query tiling conflicts with multiple streams: flash_attn_split_k_reduce takes the tile height as ne2 and derives the destination row from it, so with several tiles it writes stream s at s*tile_size instead of s*N. Tile only when there is one stream, which is the prefill case the tiling was added for. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 7 ++++++- .../vulkan-shaders/flash_attn_base.glsl | 18 +++++++++++++++--- .../vulkan-shaders/flash_attn_cm1.comp | 4 ++-- .../vulkan-shaders/flash_attn_top_k_cm.comp | 9 ++++++++- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 5ba53b06a5db..4342e78f2a66 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11500,7 +11500,12 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & const uint32_t NS = (uint32_t) q->ne[3]; const uint32_t raw_kv = (uint32_t) n_kv_raw; const uint32_t partitions = 2; - const uint32_t tile_size = std::min(N, 256u); + // Query tiling keeps the split scratch small, but flash_attn_split_k_reduce derives the + // destination row from ne2, which it is handed as the TILE height. With one tile that + // equals N and the stream stride is right; with several tiles and more than one stream + // it would write stream s at s*tile_size instead of s*N. Only tile when there is a + // single stream, which is the prefill case the tiling exists for. + const uint32_t tile_size = (NS == 1) ? std::min(N, 256u) : N; const uint32_t n_tiles = CEIL_DIV(N, tile_size); const bool f32acc = true; vk_fa_tuning_params tuning = get_fa_tuning_params(ctx->device, D, D, N, raw_kv, GGML_TYPE_F16, GGML_TYPE_F16, f32acc); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl index 8fba7a450f7e..e308c7214dd3 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl @@ -144,7 +144,7 @@ ACC_TYPE perElemOpGetSink(const in uint32_t r, const in uint32_t c, const in ACC uint32_t i, N, KV, split_k_index, Tr, start_j, end_j, gqa_iq1, iq2, iq3, rk2, rk3, rv2, rv3, ik2, ik3, iv2, iv3, - q_stride, k_stride, v_stride, m_stride, gqa_ratio, split_k_num, output_k_num; + q_stride, k_stride, v_stride, m_stride, m_row_len, gqa_ratio, split_k_num, output_k_num; bool partial_output; void init_indices() @@ -211,9 +211,21 @@ void init_indices() q_stride = gqa_ratio > 1 ? (p.nb02 / 4) : p.nb01; k_stride = p.nb11; v_stride = p.nb21; + // Bit 31 of gqa_ratio means "the mask row stride is in split_kv", used by the DeepSeek V4 + // sparse split path where the mask spans the full K range but this dispatch only covers the + // raw prefix, so m_stride != KV. That path always sets split_k_num == 1, which is what keeps + // split_kv free to carry the stride; ggml_vk_flash_attn_top_k asserts the invariant. + // Otherwise: when using grouped query attention all rows share the same mask (stride 0). + // "p.gqa_ratio >> 16" is just a roundabout way of writing zero that prevents the compiler + // from folding the "&" through the select and breaking the alignment detection. const bool mask_stride_in_split_kv = (p.gqa_ratio & 0x80000000u) != 0; - const uint32_t mask_stride_override = p.gqa_ratio >> 16; - m_stride = mask_stride_in_split_kv ? p.split_kv : (mask_stride_override != 0 ? mask_stride_override : (gqa_ratio > 1 ? 0 : KV)); + m_stride = mask_stride_in_split_kv ? p.split_kv : ((gqa_ratio > 1) ? (p.gqa_ratio >> 16) : KV); + // Distinct from m_stride: m_stride is the row-to-row step INSIDE this tile (0 under GQA, + // where every row shares one mask row), while m_row_len is the mask tensor's actual row + // length, used to step between tokens and between streams. They differ under GQA and + // under the sparse split path, where this dispatch only covers the raw prefix (KV) but + // the mask rows span the whole K range. + m_row_len = mask_stride_in_split_kv ? p.split_kv : KV; } // Bias applied to softmax to stay in fp16 range. diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp index e3ea909a59ce..38ae42c81f0c 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp @@ -139,9 +139,9 @@ void main() { // FaBlockBytesK/V == 2 for f16 (sizeof f16) and == 16 for f32 (vec4) and == ggml block size for quants. uint32_t k_offset = (ik2*p.nb12 + ik3*p.nb13) / FaBlockBytesK; uint32_t v_offset = (iv2*p.nb22 + iv3*p.nb23) / FaBlockBytesV; - uint32_t m_offset = gqa_iq1*KV; + uint32_t m_offset = gqa_iq1*m_row_len; if (p.nem2 != 1 || p.nem3 != 1) { - m_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * p.nem1 * KV; + m_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * p.nem1 * m_row_len; mo_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * CEIL_DIV(p.nem1, Br) * mo_stride; } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp index 3d0024553e7a..e0628c65705b 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp @@ -276,8 +276,15 @@ void main() { const uint dim = out_idx % HEAD_SIZE; if (p.split_mode != 0) { const uint part_idx = 1; + // Split-K buffer layout, shared with flash_attn_cm1.comp and + // flash_attn_split_k_reduce.comp: all O matrices [HSV, ne1, k_num, ne2, ne3] + // first, then the L/M rows [ne1, k_num, ne2, ne3]. Here ne1 = n_head, + // ne2 = n_batch and ne3 = the stream count, so the L/M base must span every + // stream -- omitting gl_NumWorkGroups.z lands L/M inside the O region as soon + // as there is more than one sequence. + const uint n_streams = gl_NumWorkGroups.z; const uint matrix_base = HEAD_SIZE * p.n_head * (part_idx + 2 * (token + p.n_batch * stream)); - const uint lm_base = HEAD_SIZE * p.n_head * p.n_batch * 2 + p.n_head * 2 * (part_idx + 2 * (token + p.n_batch * stream)); + const uint lm_base = HEAD_SIZE * p.n_head * p.n_batch * n_streams * 2 + p.n_head * 2 * (part_idx + 2 * (token + p.n_batch * stream)); data_dst[matrix_base + (head_base + head_local) * HEAD_SIZE + dim] = accum[i]; if (dim == 0) { data_dst[lm_base + head_base + head_local] = row_sum_sh[head_local]; From 76b18c3a9bf461ad98671f860a6574546b32b358 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 13 Aug 2026 14:32:23 +0000 Subject: [PATCH 111/212] vulkan: harden the sparse FA split path and drop its debug scaffolding No behaviour change on any path that runs today; this removes footguns the split path left in shared code and clears the leftovers. - flash_attn.comp and flash_attn_cm2.comp still read p.k_num and p.gqa_ratio raw, while the split path packs a partition count into the high half of k_num and a flag into bit 31 of gqa_ratio. Only the coopmat1 gate keeps them from ever seeing those values, and nothing said so. Use the decoded globals init_indices() already computes, so routing the split path at a sibling shader fails loudly instead of writing at wild offsets. - Drop profile_stage and GGML_VK_FA_TOPK_PROFILE. This also removes two continues from the sparse kernel's key loop. - Remove the unreachable mask-stride override branch (nothing sets bits 16..30 without bit 31) and restore the comment explaining why the GQA case writes zero the roundabout way: the compiler must not fold it, or stride alignment detection breaks. - Assert that the mask stride covers the raw KV range. The raw dispatch smuggles that stride through split_kv, which the shader also uses to derive its KV range, so a stride below KV would silently clip it. - Request descriptor sets only after the split scratch is known to fit, so the fallback path does not inherit n_tiles of unused requests. - n_head_log2 is unread with max_bias == 0; pass 0 rather than a number that looks computed. - Make the crossover depend on the path that will run. The coopmat sparse path wins as soon as any key is pruned, but the scalar fallback does not and regresses against dense FA until the pruned fraction is large, so it keeps its original 3x margin. - Record that the coopmat sparse shader requires 64-wide subgroups and eight of them, at the constants that encode it. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 30 ++++++++++++------- .../vulkan-shaders/flash_attn.comp | 20 ++++++------- .../vulkan-shaders/flash_attn_cm2.comp | 24 +++++++-------- .../vulkan-shaders/flash_attn_top_k.comp | 1 - .../vulkan-shaders/flash_attn_top_k_cm.comp | 15 ++++------ 5 files changed, 48 insertions(+), 42 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 4342e78f2a66..6e4d10f69f22 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1997,7 +1997,6 @@ struct vk_op_flash_attn_top_k_push_constants { uint32_t nb1, nb2, nb3; float scale; uint32_t has_sinks; - uint32_t profile_stage; uint32_t split_mode; }; static_assert(sizeof(vk_op_flash_attn_top_k_push_constants) <= 128); @@ -11460,7 +11459,14 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & return false; } const int64_t n_kv_active = n_kv_raw + top_k->ne[0]; - if (k->ne[1] <= n_kv_active) { + // Crossover against ordinary dense FA. The coopmat sparse path (and especially the + // raw/selected split below) is cheap enough to win as soon as any key is pruned; the + // scalar fallback shader is not, and regresses against dense FA until the pruned + // fraction is large, so it keeps its original 3x margin. Equality always uses dense + // FA because nothing is pruned. + const bool have_cm_sparse = ctx->device->pipeline_flash_attn_top_k_cm_f16 != nullptr; + const int64_t min_total_k = have_cm_sparse ? n_kv_active + 1 : 3 * n_kv_active; + if (k->ne[1] < min_total_k) { return false; } @@ -11479,15 +11485,13 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & (uint32_t) (dst->nb[1] / sizeof(float)), (uint32_t) (dst->nb[2] / sizeof(float)), (uint32_t) (dst->nb[3] / sizeof(float)), - scale, sinks != nullptr, 0, 0, + scale, sinks != nullptr, 0, }; const vk_subbuffer q_buf = ggml_vk_tensor_subbuffer(ctx, q); const vk_subbuffer sinks_buf = sinks ? ggml_vk_tensor_subbuffer(ctx, sinks) : q_buf; static const char * top_k_cm_env = getenv("GGML_VK_FA_TOPK_CM"); const bool use_cm = (!top_k_cm_env || top_k_cm_env[0] != '0') && ctx->device->pipeline_flash_attn_top_k_cm_f16; - static const char * top_k_profile_env = getenv("GGML_VK_FA_TOPK_PROFILE"); - pc.profile_stage = use_cm && top_k_profile_env ? atoi(top_k_profile_env) : 0; vk_pipeline pipeline = use_cm ? ctx->device->pipeline_flash_attn_top_k_cm_f16 : ctx->device->pipeline_flash_attn_top_k_f16; static const char * top_k_split_env = getenv("GGML_VK_FA_TOPK_SPLIT"); @@ -11527,13 +11531,12 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & pipelines[raw_state] = raw_pipeline = std::make_shared(); } } - ggml_pipeline_request_descriptor_sets(ctx, raw_pipeline, n_tiles); - ggml_pipeline_request_descriptor_sets(ctx, pipeline, n_tiles); - ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_split_k_reduce, n_tiles); - const uint64_t partition_size = ((uint64_t) D * NH + NH * 2) * sizeof(float) * tile_size * NS; const uint64_t split_size = partition_size * partitions; if (split_size <= ctx->device->properties.limits.maxStorageBufferRange) { + ggml_pipeline_request_descriptor_sets(ctx, raw_pipeline, n_tiles); + ggml_pipeline_request_descriptor_sets(ctx, pipeline, n_tiles); + ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_split_k_reduce, n_tiles); if (ctx->prealloc_size_split_k < split_size) { ctx->prealloc_size_split_k = split_size; ggml_vk_preallocate_buffers(ctx, subctx); @@ -11542,7 +11545,14 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & ggml_vk_sync_buffers(ctx, subctx); } - const uint32_t n_head_log2 = 64; + // ALiBi is disabled (max_bias == 0), so n_head_log2 is never read; keep it 0 + // rather than a value that looks computed. + const uint32_t n_head_log2 = 0; + // The raw dispatch smuggles the mask row stride through split_kv, which the FA + // shader also uses to derive its KV range as min(KV, (split_k_index+1)*split_kv). + // That is only safe while split_k_index == 0 and the stride covers the whole raw + // prefix -- both hold here, but assert rather than rely on it silently. + GGML_ASSERT(mask_stride >= raw_kv && "split_kv carries the mask stride; it must not clip the raw KV range"); const uint32_t mask_stride_in_split_kv = 1u << 31; const uint32_t packed_gqa = mask_stride_in_split_kv | 1u; const uint32_t packed_partitions = (partitions << 16) | 1; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp index 18a37add9bf8..5b19ff61093f 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp @@ -186,9 +186,9 @@ void main() { // FaBlockBytesK/V == 2 for f16, 16 for f32, ggml block byte size for quants. uint32_t k_offset = (ik2*p.nb12 + ik3*p.nb13) / FaBlockBytesK; uint32_t v_offset = (iv2*p.nb22 + iv3*p.nb23) / FaBlockBytesV; - uint32_t m_offset = gqa_iq1*KV; + uint32_t m_offset = gqa_iq1*m_row_len; if (p.nem2 != 1 || p.nem3 != 1) { - m_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * p.nem1 * KV; + m_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * p.nem1 * m_row_len; mo_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * CEIL_DIV(p.nem1, Br) * mo_stride; } @@ -210,7 +210,7 @@ void main() { } // Only load if the block is not all zeros if (mask_opt_bits != MASK_OPT_ALL_ZERO) { - bool nem1_bounds_check = !(p.gqa_ratio > 1) && (p.nem1 % Br) != 0; + bool nem1_bounds_check = !(gqa_ratio > 1) && (p.nem1 % Br) != 0; float max_mask = NEG_FLT_MAX_OVER_2; barrier(); @@ -660,10 +660,10 @@ void main() { // If there is split_k, then the split_k resolve shader does the final // division by L. Store the intermediate O value and per-row m and L values. - if (p.k_num > 1) { - if (p.gqa_ratio > 1) { + if (partial_output || split_k_num > 1) { + if (gqa_ratio > 1) { // note: O and Q have swapped coord 1,2. - uint32_t o_offset = HSV * p.ne1 * (split_k_index + p.k_num * (gqa_iq1 + p.ne2 * iq3)) / 4; + uint32_t o_offset = HSV * p.ne1 * (split_k_index + output_k_num * (gqa_iq1 + p.ne2 * iq3)) / 4; [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { const uint row = tile_row(r); @@ -674,7 +674,7 @@ void main() { } } - o_offset = HSV * p.ne1 * p.k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + p.k_num * (gqa_iq1 + p.ne2 * iq3)); + o_offset = HSV * p.ne1 * output_k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + output_k_num * (gqa_iq1 + p.ne2 * iq3)); [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { const uint row = tile_row(r); if (row < N) { @@ -688,7 +688,7 @@ void main() { const uint global_row = i * Br + row; if (global_row < N) { - uint32_t o_offset = HSV * p.ne1 * (split_k_index + p.k_num * (global_row + p.ne2 * iq3)) / 4; + uint32_t o_offset = HSV * p.ne1 * (split_k_index + output_k_num * (global_row + p.ne2 * iq3)) / 4; [[unroll]] for (uint32_t d = 0; d < HSV_per_thread / 4; ++d) { data_ov4[o_offset + iq2 * HSV/4 + d * D_split + d_tid] = D_TYPEV4(Of[r][d]); @@ -696,7 +696,7 @@ void main() { } if (global_row < N && d_tid == 0 && col_tid == 0) { - uint32_t lm_offset = HSV * p.ne1 * p.k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + p.k_num * (global_row + p.ne2 * iq3)); + uint32_t lm_offset = HSV * p.ne1 * output_k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + output_k_num * (global_row + p.ne2 * iq3)); data_o[lm_offset + iq2] = D_TYPE(Lf[r]); data_o[lm_offset + p.ne1 + iq2] = D_TYPE(Mf[r]); } @@ -742,7 +742,7 @@ void main() { uint32_t o_offset = (gqa_iq1*p.ne1*HSV + iq3*p.ne2*p.ne1*HSV) / 4; - if (p.gqa_ratio > 1) { + if (gqa_ratio > 1) { [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { const uint row = tile_row(r); if (row < N) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm2.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm2.comp index 317411153087..54be1e6daa99 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm2.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm2.comp @@ -151,7 +151,7 @@ D_TYPE perElemOpNonGqaSplitKStore(const in uint32_t r, const in uint32_t c, cons uint32_t global_row = i * Br + r; if (global_row < N && c < HSV) { uint32_t o_off = HSV * p.ne1 - * (split_k_index + p.k_num * (global_row + p.ne2 * iq3)); + * (split_k_index + output_k_num * (global_row + p.ne2 * iq3)); data_o[o_off + iq2 * HSV + c] = D_TYPE(elem); } return elem; @@ -161,8 +161,8 @@ D_TYPE perElemOpNonGqaSplitKStore(const in uint32_t r, const in uint32_t c, cons ACC_TYPE perElemOpNonGqaSplitKStoreCol0(const in uint32_t r, const in uint32_t c, const in ACC_TYPE elem, const in uint32_t lm_base, const in uint32_t iq2, const in uint32_t N) { uint32_t global_row = i * Br + r; if (global_row < N && c == 0) { - uint32_t lm_off = HSV * p.ne1 * p.k_num * p.ne2 * p.ne3 - + p.ne1 * 2 * (split_k_index + p.k_num * (global_row + p.ne2 * iq3)); + uint32_t lm_off = HSV * p.ne1 * output_k_num * p.ne2 * p.ne3 + + p.ne1 * 2 * (split_k_index + output_k_num * (global_row + p.ne2 * iq3)); data_o[lm_off + lm_base + iq2] = D_TYPE(elem); } return elem; @@ -242,9 +242,9 @@ void main() { // mo_offset will point to the tile starting at row i*Br and col 0 uint32_t mo_offset = mo_stride * i; - uint32_t m_offset = gqa_iq1*KV * 2 /*sizeof(float16_t)*/; + uint32_t m_offset = gqa_iq1*m_row_len * 2 /*sizeof(float16_t)*/; if (p.nem2 != 1 || p.nem3 != 1) { - m_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * p.nem1 * KV * 2 /*sizeof(float16_t)*/; + m_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * p.nem1 * m_row_len * 2 /*sizeof(float16_t)*/; mo_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * CEIL_DIV(p.nem1, Br) * mo_stride; } @@ -268,7 +268,7 @@ void main() { } // Only load if the block is not all zeros if (mask_opt_bits != MASK_OPT_ALL_ZERO) { - bool nem1_bounds_check = !(p.gqa_ratio > 1) && (p.nem1 % Br) != 0; + bool nem1_bounds_check = !(gqa_ratio > 1) && (p.nem1 % Br) != 0; if (nem1_bounds_check) { tensorLayoutNV<2, gl_CooperativeMatrixClampModeConstantNV> tensorLayoutM = createTensorLayoutNV(2, gl_CooperativeMatrixClampModeConstantNV); @@ -287,7 +287,7 @@ void main() { } else { tensorLayoutNV<2, Clamp> tensorLayoutM = createTensorLayoutNV(2, Clamp); // Don't clamp against nem1 when GQA is enabled - uint32_t m_height = p.gqa_ratio > 1 ? ~0 : p.nem1; + uint32_t m_height = gqa_ratio > 1 ? ~0 : p.nem1; tensorLayoutM = setTensorLayoutDimensionNV(tensorLayoutM, m_height, KV); tensorLayoutM = setTensorLayoutStrideNV(tensorLayoutM, m_stride, 1); @@ -406,15 +406,15 @@ void main() { // If there is split_k, then the split_k resolve shader does the final // division by L. Store the intermediate O value and per-row m and L values. - if (p.k_num > 1) { + if (partial_output || split_k_num > 1) { coopmat O_D = coopmat(O); - if (p.gqa_ratio > 1) { + if (gqa_ratio > 1) { // note: O and Q have swapped coord 1,2. - uint32_t o_offset = HSV * p.ne1 * (split_k_index + p.k_num * (gqa_iq1 + p.ne2 * iq3)); + uint32_t o_offset = HSV * p.ne1 * (split_k_index + output_k_num * (gqa_iq1 + p.ne2 * iq3)); coopMatPerElementNV(O_D, O_D, perElemOpGqaStore, o_offset, iq2, N); - o_offset = HSV * p.ne1 * p.k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + p.k_num * (gqa_iq1 + p.ne2 * iq3)); + o_offset = HSV * p.ne1 * output_k_num * p.ne2 * p.ne3 + p.ne1 * 2 * (split_k_index + output_k_num * (gqa_iq1 + p.ne2 * iq3)); coopMatPerElementNV(L, L, perElemOpStoreCol0, o_offset, iq2, N); coopMatPerElementNV(M, M, perElemOpStoreCol0, o_offset + p.ne1, iq2, N); } else { @@ -473,7 +473,7 @@ void main() { uint32_t o_offset = gqa_iq1*p.ne1*HSV + iq3*p.ne2*p.ne1*HSV; - if (p.gqa_ratio > 1) { + if (gqa_ratio > 1) { coopMatPerElementNV(O_D, O_D, perElemOpGqaStore, o_offset, iq2, N); } else { tensorLayoutNV<3, gl_CooperativeMatrixClampModeConstantNV> tensorLayoutD = createTensorLayoutNV(3, gl_CooperativeMatrixClampModeConstantNV); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp index 62807471bdd1..930777b5f0ea 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k.comp @@ -37,7 +37,6 @@ layout(push_constant) uniform Parameters { uint nb3; float scale; uint has_sinks; - uint profile_stage; uint split_mode; } p; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp index e0628c65705b..7a328938777a 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_top_k_cm.comp @@ -39,10 +39,15 @@ layout(push_constant) uniform Parameters { uint nb3; float scale; uint has_sinks; - uint profile_stage; uint split_mode; } p; +// This shader is hard-wired to a 512-thread workgroup of eight 64-wide subgroups: +// the softmax segments SUBGROUP_SIZE/4 = 16 lanes per head (so head_local stays < 32), +// and the QK/PV tiling assumes gl_NumSubgroups == 8 via gl_SubgroupID % 4 / gl_SubgroupID / 4. +// A different subgroup size indexes past row_max_sh/q_p_sh and corrupts the score tiles, so +// the pipeline is only created under `device->subgroup_size == 64` in ggml_vk_load_shaders. +// Keep that gate in sync with these constants. const uint TILE = 16; const uint HEAD_SIZE = 512; const uint HEADS_PER_GROUP = 32; @@ -146,10 +151,6 @@ void main() { SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); barrier(); - if (p.profile_stage == 1) { - continue; - } - { const uint head_local = tid / (SUBGROUP_SIZE / 4); const uint softmax_lane = tid % (SUBGROUP_SIZE / 4); @@ -198,10 +199,6 @@ void main() { accum[i] *= old_scale_sh[head_local]; } - if (p.profile_stage == 2) { - continue; - } - coopmat pmats[KEYS_PER_BLOCK / TILE]; [[unroll]] for (uint key_chunk = 0; key_chunk < KEYS_PER_BLOCK / TILE; ++key_chunk) { const uint pv_head_tile = gl_SubgroupID / (DIMS_PER_BLOCK / TILE); From 75e195fcdd37357ba0dc96b616d5fa00d5c48b78 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 13 Aug 2026 14:32:59 +0000 Subject: [PATCH 112/212] test-backend-ops: cover sparse top-k FA with more than one sequence The sparse top-k fixture pinned every tensor's fourth dimension to 1, so no case reached the split path's stream indexing and three separate mis-indexings passed the suite. Add an ns parameter and three cases: two at ns=2 (with and without sinks, single tile) and one at ns=3 with 300 query tokens, which also crosses the 256-token tile boundary. The per-token selection is offset by the stream so a dropped stream stride reads another sequence's keys rather than the same ones. All three fail on the unfixed shaders and pass with them fixed. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- tests/test-backend-ops.cpp | 56 +++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 23913f627007..23b30958db3e 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -8038,12 +8038,13 @@ struct test_flash_attn_ext_top_k : public test_case { const int64_t n_kv_raw; // dense prefix always attended const int64_t n_top_k; // selected keys per query token const bool sinks; + const int64_t ns; // sequences (ne3); >1 exercises the split-K stream stride static constexpr int64_t hs = 512; // V4 CSA head size, K == V latent static constexpr int64_t nh = 64; // V4 CSA query heads (MQA) std::string vars() override { - return VARS_TO_STR5(kv, nb, n_kv_raw, n_top_k, sinks); + return VARS_TO_STR6(kv, nb, n_kv_raw, n_top_k, sinks, ns); } double max_nmse_err() override { @@ -8054,27 +8055,27 @@ struct test_flash_attn_ext_top_k : public test_case { GGML_UNUSED(t); // only the active keys contribute compute on a sparse backend; count those so // perf mode reports the useful-work rate - return 2 * nh * nb * (hs + hs) * (n_kv_raw + n_top_k); + return 2 * nh * nb * ns * (hs + hs) * (n_kv_raw + n_top_k); } - test_flash_attn_ext_top_k(int64_t kv = 768, int64_t nb = 8, int64_t n_kv_raw = 64, int64_t n_top_k = 128, bool sinks = false) - : kv(kv), nb(nb), n_kv_raw(n_kv_raw), n_top_k(n_top_k), sinks(sinks) {} + test_flash_attn_ext_top_k(int64_t kv = 768, int64_t nb = 8, int64_t n_kv_raw = 64, int64_t n_top_k = 128, bool sinks = false, int64_t ns = 1) + : kv(kv), nb(nb), n_kv_raw(n_kv_raw), n_top_k(n_top_k), sinks(sinks), ns(ns) {} ggml_tensor * build_graph(ggml_context * ctx) override { - ggml_tensor * q = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, hs, nb, nh, 1); + ggml_tensor * q = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, hs, nb, nh, ns); ggml_set_name(q, "q"); - ggml_tensor * k = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, hs, kv, 1, 1); + ggml_tensor * k = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, hs, kv, 1, ns); ggml_set_name(k, "k"); // V4 CSA attends over the K latent itself: V is the same cache tensor - ggml_tensor * v = ggml_view_4d(ctx, k, hs, kv, 1, 1, k->nb[1], k->nb[2], k->nb[3], 0); + ggml_tensor * v = ggml_view_4d(ctx, k, hs, kv, 1, ns, k->nb[1], k->nb[2], k->nb[3], 0); ggml_set_name(v, "v"); - ggml_tensor * m = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, kv, nb, 1, 1); + ggml_tensor * m = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, kv, nb, 1, ns); ggml_set_name(m, "m"); - ggml_tensor * t = ggml_new_tensor_4d(ctx, GGML_TYPE_I32, n_top_k, nb, 1, 1); + ggml_tensor * t = ggml_new_tensor_4d(ctx, GGML_TYPE_I32, n_top_k, nb, 1, ns); ggml_set_name(t, "top_k"); ggml_tensor * s = nullptr; @@ -8109,23 +8110,29 @@ struct test_flash_attn_ext_top_k : public test_case { // build a consistent (top_k, mask) pair: a deterministic per-token selection, // strided so adjacent tokens select overlapping-but-different keys, with one // deliberately invalid index (-1) whose mask slot stays -inf - std::vector top(n_top_k * nb); - std::vector mask(kv * nb); + std::vector top(n_top_k * nb * ns); + std::vector mask(kv * nb * ns); const ggml_fp16_t minus_inf = ggml_fp32_to_fp16(-INFINITY); const ggml_fp16_t zero = ggml_fp32_to_fp16(0.0f); - for (int64_t b = 0; b < nb; ++b) { - for (int64_t i = 0; i < kv; ++i) { - mask[b * kv + i] = i < n_kv_raw ? zero : minus_inf; - } - for (int64_t j = 0; j < n_top_k; ++j) { - int32_t idx = (int32_t) ((j * range) / n_top_k + b) % (int32_t) range; - if (j == n_top_k - 1 && b == 0) { - idx = -1; // exercise the ignore-invalid-index path - } else { - mask[b * kv + n_kv_raw + idx] = zero; + for (int64_t s = 0; s < ns; ++s) { + for (int64_t b = 0; b < nb; ++b) { + const int64_t mrow = (s * nb + b) * kv; + const int64_t trow = (s * nb + b) * n_top_k; + for (int64_t i = 0; i < kv; ++i) { + mask[mrow + i] = i < n_kv_raw ? zero : minus_inf; + } + for (int64_t j = 0; j < n_top_k; ++j) { + // offset the selection by the stream too, so a dropped stream stride + // reads another sequence's keys and shows up as a mismatch + int32_t idx = (int32_t) ((j * range) / n_top_k + b + s * 7) % (int32_t) range; + if (j == n_top_k - 1 && b == 0 && s == 0) { + idx = -1; // exercise the ignore-invalid-index path + } else { + mask[mrow + n_kv_raw + idx] = zero; + } + top[trow + j] = idx; } - top[b * n_top_k + j] = idx; } } @@ -11398,6 +11405,11 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext_top_k(1024, 64, 65, 128, false)); test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 128, 256, 512, false)); test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 257, 256, 512, false)); + // ns > 1: the split-K partial-output path indexes O and L/M by stream, so these cover + // the stream stride in both regions (single tile and multi-tile). + test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false, 2)); + test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, true, 2)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 300, 256, 512, false, 3)); return test_cases; } From 04311a08e5acdc0152ba9c3bcc624fdd1a764899 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 13 Aug 2026 15:34:07 +0000 Subject: [PATCH 113/212] vulkan: let sparse FA query tiling and multiple sequences coexist flash_attn_split_k_reduce was using one value for two different things: ne2 is the split buffer's query count, which the sparse path hands the TILE height, but the destination's stream stride needs the FULL query count. With several tiles and more than one sequence it wrote stream s at s*tile_size instead of s*N, so the previous fix simply refused to tile whenever there was more than one stream. Give the reduce a separate dst_ne2 and use it for the destination index only. Ordinary FA split-K passes ne2 for it and is bit-identical. The sparse path passes the full batch, so tiling is unconditional again. This matters for memory, which is the binding constraint on this model. The scratch is capped at 256 queries per tile rather than scaling with the batch, so at ub2048 a 2-sequence context drops from 1028 MB to 128.5 MB and a 4-sequence one from 2056 MB to 257 MB. It also removes a cliff: 8 sequences at ub2048 would have exceeded maxStorageBufferRange and silently fallen back to the slower non-split kernel. Sparse FA timings are unchanged (every shape within 0.45%, inside run-to-run spread). 13,308 FLASH_ATTN_EXT cases pass, which covers the ordinary split-K path that shares this shader. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 18 ++++++++++-------- .../flash_attn_split_k_reduce.comp | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 6e4d10f69f22..a4abb75d56bc 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -2152,7 +2152,11 @@ struct vk_quantize_q8_1_push_constants { struct vk_op_flash_attn_split_k_reduce_push_constants { uint32_t D; uint32_t ne1; + // ne2 describes the SPLIT BUFFER (which may cover only a tile of queries); dst_ne2 is the + // destination's query count. They differ only when the caller tiles the split buffer, and + // the destination stride between streams must always use the full count. uint32_t ne2; + uint32_t dst_ne2; uint32_t ne3; uint32_t k_num; uint32_t sinks; @@ -11504,12 +11508,10 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & const uint32_t NS = (uint32_t) q->ne[3]; const uint32_t raw_kv = (uint32_t) n_kv_raw; const uint32_t partitions = 2; - // Query tiling keeps the split scratch small, but flash_attn_split_k_reduce derives the - // destination row from ne2, which it is handed as the TILE height. With one tile that - // equals N and the stream stride is right; with several tiles and more than one stream - // it would write stream s at s*tile_size instead of s*N. Only tile when there is a - // single stream, which is the prefill case the tiling exists for. - const uint32_t tile_size = (NS == 1) ? std::min(N, 256u) : N; + // Query tiling caps the split scratch at 256 queries regardless of batch or stream + // count. The reduce takes the tile height as ne2 and the full query count as dst_ne2, + // so the destination stream stride stays correct across tiles. + const uint32_t tile_size = std::min(N, 256u); const uint32_t n_tiles = CEIL_DIV(N, tile_size); const bool f32acc = true; vk_fa_tuning_params tuning = get_fa_tuning_params(ctx->device, D, D, N, raw_kv, GGML_TYPE_F16, GGML_TYPE_F16, f32acc); @@ -11604,7 +11606,7 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & ctx->prealloc_split_k_need_sync = true; ggml_vk_sync_buffers(ctx, subctx); - const vk_op_flash_attn_split_k_reduce_push_constants reduce_pc = {D, NH, tile_n, NS, partitions, sinks != nullptr}; + const vk_op_flash_attn_split_k_reduce_push_constants reduce_pc = {D, NH, tile_n, N, NS, partitions, sinks != nullptr}; ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_split_k_reduce, {split_buf, sinks_buf, tile_dst}, reduce_pc, {NH, D, tile_n * NS}); ctx->prealloc_split_k_need_sync = true; @@ -12121,7 +12123,7 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx pc, { dispatch_x, workgroups_y, workgroups_z }); ggml_vk_sync_buffers(ctx, subctx); - const vk_op_flash_attn_split_k_reduce_push_constants pc2 = { HSV, (uint32_t)ne1, (uint32_t)ne2, (uint32_t)ne3, split_k, (sinks != nullptr) }; + const vk_op_flash_attn_split_k_reduce_push_constants pc2 = { HSV, (uint32_t)ne1, (uint32_t)ne2, (uint32_t)ne2, (uint32_t)ne3, split_k, (sinks != nullptr) }; ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_split_k_reduce, {split_k_buf, sinks_buf, dst_buf}, pc2, { (uint32_t)ne1, HSV, (uint32_t)(ne2 * ne3) }); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_split_k_reduce.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_split_k_reduce.comp index 68917fc0bb02..69342ba3ceea 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_split_k_reduce.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_split_k_reduce.comp @@ -14,6 +14,7 @@ layout (push_constant) uniform parameter { uint D; uint ne1; uint ne2; + uint dst_ne2; uint ne3; uint k_num; uint sinks; @@ -116,6 +117,6 @@ void main() { const float FLT_MAX = uintBitsToFloat(0x7F7FFFFF); O = clamp(O, -FLT_MAX, FLT_MAX); - data_d[(i3 * p.ne2 + i2) * p.ne1 * D + D * n + d] = O; + data_d[(i3 * p.dst_ne2 + i2) * p.ne1 * D + D * n + d] = O; } } From feff28ebf178472cb7ab044e3ff869c1197de4a2 Mon Sep 17 00:00:00 2001 From: Jaap Buurman Date: Thu, 13 Aug 2026 20:00:25 +0200 Subject: [PATCH 114/212] vulkan: parallelize DSV4 Lightning Indexer prefill Assisted-by: Codex --- .../DSV4-vulkan-lightning-indexer-progress.md | 158 ++++++++++++++++++ ggml/src/ggml-vulkan/ggml-vulkan.cpp | 18 +- .../vulkan-shaders/lightning_indexer_cm.comp | 94 +++++++---- .../vulkan-shaders/vulkan-shaders-gen.cpp | 3 +- tests/test-backend-ops.cpp | 9 + 5 files changed, 241 insertions(+), 41 deletions(-) create mode 100644 docs/development/DSV4-vulkan-lightning-indexer-progress.md diff --git a/docs/development/DSV4-vulkan-lightning-indexer-progress.md b/docs/development/DSV4-vulkan-lightning-indexer-progress.md new file mode 100644 index 000000000000..d19fbd6903aa --- /dev/null +++ b/docs/development/DSV4-vulkan-lightning-indexer-progress.md @@ -0,0 +1,158 @@ +# DeepSeek V4 Vulkan Lightning Indexer progress + +This is a restart note for the Strix Halo Lightning Indexer optimization. It is a development scratch pad and can be removed before the final PR. + +## Repository state + +- Main repository: `/home/jaap/Projects/git/llama.cpp` +- Optimization worktree: `/tmp/llama-strix-beta-bench` +- Branch: `strix-halo-vulkan-lightning-indexer` +- Base commit: `316c72ee9eab590f5891089d3b6bfc0d01d00d19` +- Base branch: Nathan's `strix-halo-vulkan-beta` +- Decode microbench work is stored in the main worktree as `stash@{0}: On strix-halo-vulkan: wip: DSV4 decode microbench depth matrix`. +- The Indexer changes are uncommitted. Do not commit without explicit user approval. An assisted commit needs an `Assisted-by:` trailer. +- Do not run builds and GPU benchmarks together. The APU shares its power and memory-bandwidth budget. +- GPU commands need sandbox escalation. + +## Objective and result + +After sparse prefill attention was flattened, the context-dependent Lightning Indexer became the next prefill bottleneck. The old cooperative-matrix shader used one wave64 subgroup per workgroup, processed one 16-key tile, and loaded one query head at a time. + +The new wide pipeline uses eight wave64 subgroups per workgroup. Each subgroup processes a separate 16-key tile, so one workgroup covers 128 keys. It stages four query heads and their weights together, reuses them across all eight subgroups, and uses subgroup-scoped synchronization between cooperative-matrix result stores. A workgroup barrier remains between four-head groups because all subgroups reuse the shared query storage. + +The optimized shader requires 512 workgroup invocations and 64 KiB shared memory. Pipeline creation is capability-based. Devices without those limits use a one-wave, one-head cooperative-matrix specialization. The scalar implementation remains the fallback when cooperative matrices are unavailable. The decode-specific cooperative-matrix pipeline is unchanged. + +At the 32k-equivalent prefill microbench shape: + +| Version | Time per layer | Throughput | +| --- | ---: | ---: | +| Baseline | 50.51 ms | 5.83 TFLOPS | +| Optimized | 31.81 ms | 9.25 TFLOPS | + +This is a 37.0% reduction in Lightning Indexer kernel time. + +The canonical 32k llama-bench improved from 209.45 to 216.32 tokens/s. Total Vulkan time fell from 9.73729 to 9.42448 seconds. Total Lightning Indexer time fell from 1.11860 to 0.697276 seconds. Sparse attention and top-K were effectively unchanged. + +## Changed files + +- `ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp`: parameterizes the shader, adds the eight-wave four-head implementation, and remains usable for the small fallback. +- `ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp`: generates wide `N_WAVES=8`, `HEADS_PER_TILE=4` and small `N_WAVES=1`, `HEADS_PER_TILE=1` variants. +- `ggml/src/ggml-vulkan/ggml-vulkan.cpp`: creates and selects the capability-gated wide pipeline and the small cooperative-matrix fallback. +- `tests/test-backend-ops.cpp`: adds 127, 128, and 129-key correctness boundaries and PP2048 performance shapes through 512k simulated source context. + +## Performance data + +The performance rows model the actual PP2048 Indexer shapes after the source context is filled in 2048-token batches. `kv=8704` is the measured shape near 32k source context. It differs from 32768 because the Indexer compresses source tokens into rows. + +| Source depth | `kv` | Baseline | Optimized | Reduction | +| ---: | ---: | ---: | ---: | ---: | +| 0 | 512 | 4.23 ms | 2.14 ms | 49.5% | +| 8k | 2560 | 17.10 ms | 10.09 ms | 41.0% | +| 16k | 4608 | 29.97 ms | 17.62 ms | 41.2% | +| 32k | 8704 | 50.51 ms | 32.94 ms | 34.8% | +| 64k | 16896 | 94.36 ms | 64.87 ms | 31.3% | +| 128k | 33280 | 174.96 ms | 128.76 ms | 26.4% | +| 256k | 66048 | 352.73 ms | 250.88 ms | 28.9% | +| 512k | 131584 | 696.79 ms | 495.90 ms | 28.8% | + +The final isolated 32k run after cleanup measured 31.81159 ms. Small matrix differences are normal laptop GPU clock variation. + +| Canonical 32k metric | Baseline | Optimized | +| --- | ---: | ---: | +| PP2048 | 209.45 tokens/s | 216.32 tokens/s | +| Total Vulkan | 9.73729 s | 9.42448 s | +| Lightning Indexer | 1.11860 s | 0.697276 s | +| Sparse FA raw | 0.198438 s | 0.195367 s | +| Sparse FA selected | 0.835110 s | 0.843385 s | +| Sparse FA reduce | 0.088463 s | 0.086678 s | +| TOP_K | 0.075473 s | 0.075350 s | + +Logs: + +- Baseline matrix: `/tmp/dsv4-lightning-prefill-baseline.log` +- Optimized matrix: `/tmp/dsv4-lightning-four-head-matrix.log` +- Final selected-pipeline 32k microbench: `/tmp/dsv4-lightning-final-selected-32k.log` +- Baseline canonical 32k llama-bench: `/tmp/dsv4-nathan-beta-32k-rerun-new-first.log` +- Optimized canonical 32k llama-bench: `/tmp/dsv4-lightning-final-32k-llama-bench.log` + +## Correctness and resources + +- Wide pipeline: all 20 focused F16 cases passed, including 127, 128, and 129-key boundaries. +- Small cooperative-matrix fallback: temporarily forced and all the same 20 cases passed. +- Wide shader on gfx1151: 168 VGPRs, 63,488 bytes LDS, no spills, eight subgroups per SIMD. +- The final pipeline-statistics run confirmed `lightning_indexer_cm_f16` was selected. +- No NaN, Inf, or comparison failures were reported. + +Correctness logs: + +- Wide: `/tmp/dsv4-lightning-consolidated-wide-correctness.log` +- Small fallback: `/tmp/dsv4-lightning-consolidated-small-correctness.log` + +## Experiments and decisions + +- Four waves improved the 32k shape about 3% and became worse at deep simulated contexts. +- Eight waves improved it about 8% before the other changes. +- Subgroup-scoped synchronization after cooperative-matrix stores improved the eight-wave version. +- Staging four query heads and weights produced the large gain by reducing redundant loads and barriers. +- Using only a subgroup barrier between head groups failed four boundary tests. One subgroup could overwrite shared query data while another still read it. A workgroup barrier is required there. +- A separate fallback shader source was avoided. Generator definitions create both variants from one file. + +## Commands + +Build only, with no GPU benchmark running: + +```sh +cd /tmp/llama-strix-beta-bench +git diff --check +cmake --build build --config Release --target test-backend-ops llama-bench -j "$(nproc)" +``` + +Focused correctness: + +```sh +cd /tmp/llama-strix-beta-bench +./build/bin/test-backend-ops test -b Vulkan0 -o LIGHTNING_INDEXER -p 'type_K=f16' > /tmp/dsv4-lightning-correctness.log 2>&1 +tail -n 30 /tmp/dsv4-lightning-correctness.log +``` + +Final 32k-equivalent microbench and pipeline selection: + +```sh +cd /tmp/llama-strix-beta-bench +GGML_VK_PIPELINE_STATS=lightning_indexer_cm_f16 ./build/bin/test-backend-ops perf -b Vulkan0 -o LIGHTNING_INDEXER -p 'kv=8704' > /tmp/dsv4-lightning-final-selected-32k.log 2>&1 +tail -n 16 /tmp/dsv4-lightning-final-selected-32k.log +``` + +Full Indexer depth matrix: + +```sh +cd /tmp/llama-strix-beta-bench +./build/bin/test-backend-ops perf -b Vulkan0 -o LIGHTNING_INDEXER -p 'nb=2048,nh=64,ns=1,nm=1,type_K=f16' > /tmp/dsv4-lightning-matrix.log 2>&1 +rg 'kv=(512|2560|4608|8704|16896|33280|66048|131584),nb=2048' /tmp/dsv4-lightning-matrix.log +``` + +Canonical 32k llama-bench. Run it only when needed, never while compiling, and inspect only the final block: + +```sh +cd /tmp/llama-strix-beta-bench +GGML_VK_PERF_LOGGER=1 ./build/bin/llama-bench -m /home/jaap/Projects/docker/localLLaMA/models/models--unsloth--DeepSeek-V4-Flash-0731-GGUF/snapshots/109848da2469efe1f1aab9e11acea08a065ccd4f/UD-IQ3_XXS/DeepSeek-V4-Flash-0731-UD-IQ3_XXS-00001-of-00004.gguf -r 1 -d 32768 -p 2048 -ub 2048 -fa 1 -n 0 > /tmp/dsv4-lightning-32k-llama-bench.log 2>&1 +last=$(grep -n 'Vulkan Timings:' /tmp/dsv4-lightning-32k-llama-bench.log | tail -n 1 | cut -d: -f1) +sed -n "${last},\$p" /tmp/dsv4-lightning-32k-llama-bench.log | tail -n 180 +``` + +Patch inspection: + +```sh +cd /tmp/llama-strix-beta-bench +git diff --check +git diff --stat +git diff +git status --short +``` + +## Next actions + +1. The user reviews and understands the four-file implementation and result summary. +2. Commit only after explicit user approval for that commit action. +3. Remove this scratch pad before a PR if it is not useful as permanent documentation. +4. Restore the separate decode microbench stash from the main worktree only if that work resumes. diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index a4abb75d56bc..81021b6204ce 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1106,6 +1106,7 @@ struct vk_device_struct { vk_pipeline pipeline_gated_delta_net[4][2]; vk_pipeline pipeline_lightning_indexer_f16; vk_pipeline pipeline_lightning_indexer_cm_f16; + vk_pipeline pipeline_lightning_indexer_cm_small_f16; vk_pipeline pipeline_lightning_indexer_decode_cm_f16; vk_pipeline pipeline_flash_attn_top_k_f16; vk_pipeline pipeline_flash_attn_top_k_cm_f16; @@ -6284,10 +6285,18 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { device->subgroup_size); #if defined(VK_KHR_cooperative_matrix) && defined(GGML_VULKAN_COOPMAT_GLSLC_SUPPORT) if (device->coopmat_support && device->coopmat_support_16x16x16_f32acc && device->subgroup_size_control) { - ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_cm_f16, - "lightning_indexer_cm_f16", lightning_indexer_cm_f16_len, lightning_indexer_cm_f16_data, "main", 5, + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_cm_small_f16, + "lightning_indexer_cm_small_f16", lightning_indexer_cm_small_f16_len, lightning_indexer_cm_small_f16_data, "main", 5, sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 16, 1}, {device->subgroup_size}, 1, true, true, device->subgroup_size); + if (device->properties.limits.maxComputeWorkGroupInvocations >= 512 && + device->properties.limits.maxComputeWorkGroupSize[0] >= 512 && + device->properties.limits.maxComputeSharedMemorySize >= 64 * 1024) { + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_cm_f16, + "lightning_indexer_cm_f16", lightning_indexer_cm_f16_len, lightning_indexer_cm_f16_data, "main", 5, + sizeof(vk_op_lightning_indexer_cm_push_constants), {128, 16, 1}, {device->subgroup_size}, 1, true, true, + device->subgroup_size); + } ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_decode_cm_f16, "lightning_indexer_decode_cm_f16", lightning_indexer_decode_cm_f16_len, lightning_indexer_decode_cm_f16_data, "main", 5, sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 1, 1}, {device->subgroup_size}, 1, true, true, @@ -12713,8 +12722,9 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const if (ctx->device->pipeline_lightning_indexer_decode_cm_f16 && src0->ne[2] == 1) { return ctx->device->pipeline_lightning_indexer_decode_cm_f16; } - return ctx->device->pipeline_lightning_indexer_cm_f16 && src0->ne[2] >= 16 ? - ctx->device->pipeline_lightning_indexer_cm_f16 : ctx->device->pipeline_lightning_indexer_f16; + vk_pipeline cm = ctx->device->pipeline_lightning_indexer_cm_f16 ? + ctx->device->pipeline_lightning_indexer_cm_f16 : ctx->device->pipeline_lightning_indexer_cm_small_f16; + return cm && src0->ne[2] >= 16 ? cm : ctx->device->pipeline_lightning_indexer_f16; } // 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)) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp index a0a3639d2546..c53eb74f8d4c 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp @@ -5,9 +5,14 @@ #extension GL_EXT_shader_explicit_arithmetic_types_float16 : require #extension GL_KHR_cooperative_matrix : require #extension GL_KHR_memory_scope_semantics : require +#extension GL_KHR_shader_subgroup_basic : require layout(constant_id = 0) const uint SUBGROUP_SIZE = 64; +#if N_WAVES == 1 layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; +#else +layout(local_size_x = 512, local_size_y = 1, local_size_z = 1) in; +#endif layout(binding = 0) readonly buffer QBuf { float data_q[]; }; layout(binding = 1) readonly buffer KBuf { float16_t data_k[]; }; @@ -40,13 +45,16 @@ const uint VEC_PER_HEAD = HEAD_SIZE / 4; const uint TILE_STRIDE = VEC_PER_HEAD + 2; const uint SCORE_STRIDE = TILE / 4 + 1; -shared f16vec4 q_sh[TILE * TILE_STRIDE]; -shared f16vec4 k_sh[TILE * TILE_STRIDE]; -shared vec4 score_sh[TILE * SCORE_STRIDE]; +shared f16vec4 q_sh[HEADS_PER_TILE][TILE * TILE_STRIDE]; +shared f16vec4 k_sh[N_WAVES][TILE * TILE_STRIDE]; +shared vec4 score_sh[N_WAVES][TILE * SCORE_STRIDE]; +shared float weight_sh[HEADS_PER_TILE][TILE]; void main() { const uint tid = gl_LocalInvocationIndex; - const uint kv_base = gl_WorkGroupID.x * TILE; + const uint lane = gl_SubgroupInvocationID; + const uint wave = gl_SubgroupID; + const uint kv_base = (gl_WorkGroupID.x * N_WAVES + wave) * TILE; const uint token_base = gl_WorkGroupID.y * TILE; const uint stream = gl_WorkGroupID.z; @@ -55,63 +63,77 @@ void main() { totals[i] = 0.0; } - for (uint idx = tid; idx < TILE * VEC_PER_HEAD; idx += SUBGROUP_SIZE) { - const uint key = idx / VEC_PER_HEAD; - const uint d4 = idx % VEC_PER_HEAD; - const uint kv = kv_base + key; + for (uint idx = tid; idx < N_WAVES * TILE * VEC_PER_HEAD; idx += gl_WorkGroupSize.x) { + const uint load_wave = idx / (TILE * VEC_PER_HEAD); + const uint wave_idx = idx % (TILE * VEC_PER_HEAD); + const uint key = wave_idx / VEC_PER_HEAD; + const uint d4 = wave_idx % VEC_PER_HEAD; + const uint kv = (gl_WorkGroupID.x * N_WAVES + load_wave) * TILE + key; f16vec4 value = f16vec4(0.0); if (kv < p.n_kv) { const uint offset = stream * p.nbk3 + kv * p.nbk2 + d4 * 4; value = f16vec4(data_k[offset], data_k[offset + 1], data_k[offset + 2], data_k[offset + 3]); } - k_sh[key * TILE_STRIDE + d4] = value; + k_sh[load_wave][key * TILE_STRIDE + d4] = value; } barrier(); - for (uint head = 0; head < N_HEAD; ++head) { - for (uint idx = tid; idx < TILE * VEC_PER_HEAD; idx += SUBGROUP_SIZE) { - const uint token_local = idx / VEC_PER_HEAD; - const uint d4 = idx % VEC_PER_HEAD; + for (uint head_base = 0; head_base < N_HEAD; head_base += HEADS_PER_TILE) { + for (uint idx = tid; idx < HEADS_PER_TILE * TILE * VEC_PER_HEAD; idx += gl_WorkGroupSize.x) { + const uint head_local = idx / (TILE * VEC_PER_HEAD); + const uint head_idx = idx % (TILE * VEC_PER_HEAD); + const uint token_local = head_idx / VEC_PER_HEAD; + const uint d4 = head_idx % VEC_PER_HEAD; const uint token = token_base + token_local; f16vec4 value = f16vec4(0.0); if (token < p.n_batch) { - const uint offset = stream * p.nbq3 + token * p.nbq2 + head * p.nbq1 + d4 * 4; + const uint offset = stream * p.nbq3 + token * p.nbq2 + (head_base + head_local) * p.nbq1 + d4 * 4; value = f16vec4(data_q[offset], data_q[offset + 1], data_q[offset + 2], data_q[offset + 3]); } - q_sh[token_local * TILE_STRIDE + d4] = value; + q_sh[head_local][token_local * TILE_STRIDE + d4] = value; + } + if (tid < HEADS_PER_TILE * TILE) { + const uint head_local = tid / TILE; + const uint token_local = tid % TILE; + const uint token = token_base + token_local; + weight_sh[head_local][token_local] = token < p.n_batch ? data_w[stream * p.nbw3 + token * p.nbw1 + head_base + head_local] : 0.0; } barrier(); - coopmat scores = - coopmat(0.0); - coopmat kmat; - coopmat qmat; + [[unroll]] for (uint head_local = 0; head_local < HEADS_PER_TILE; ++head_local) { + coopmat scores = + coopmat(0.0); + coopmat kmat; + coopmat qmat; - [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { - coopMatLoad(kmat, k_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); - coopMatLoad(qmat, q_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutColumnMajor); - scores = coopMatMulAdd(kmat, qmat, scores); - } + [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { + coopMatLoad(kmat, k_sh[wave], d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + coopMatLoad(qmat, q_sh[head_local], d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutColumnMajor); + scores = coopMatMulAdd(kmat, qmat, scores); + } - coopMatStore(scores, score_sh, 0, SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); - barrier(); + coopMatStore(scores, score_sh[wave], 0, SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + controlBarrier(gl_ScopeSubgroup, gl_ScopeSubgroup, gl_StorageSemanticsShared, gl_SemanticsAcquireRelease); - [[unroll]] for (uint i = 0; i < 4; ++i) { - const uint idx = tid + i * SUBGROUP_SIZE; - const uint key = idx / TILE; - const uint token_local = idx % TILE; - const uint token = token_base + token_local; - if (token < p.n_batch && kv_base + key < p.n_kv) { - const float score = score_sh[key * SCORE_STRIDE + token_local / 4][token_local % 4]; - const float weight = data_w[stream * p.nbw3 + token * p.nbw1 + head]; - totals[i] += max(score, 0.0) * weight; + [[unroll]] for (uint i = 0; i < 4; ++i) { + const uint idx = lane + i * SUBGROUP_SIZE; + const uint key = idx / TILE; + const uint token_local = idx % TILE; + const uint token = token_base + token_local; + if (token < p.n_batch && kv_base + key < p.n_kv) { + const float score = score_sh[wave][key * SCORE_STRIDE + token_local / 4][token_local % 4]; + totals[i] += max(score, 0.0) * weight_sh[head_local][token_local]; + } + } + if (head_local + 1 < HEADS_PER_TILE) { + controlBarrier(gl_ScopeSubgroup, gl_ScopeSubgroup, gl_StorageSemanticsShared, gl_SemanticsAcquireRelease); } } barrier(); } [[unroll]] for (uint i = 0; i < 4; ++i) { - const uint idx = tid + i * SUBGROUP_SIZE; + const uint idx = lane + i * SUBGROUP_SIZE; const uint key = idx / TILE; const uint token_local = idx % TILE; const uint kv = kv_base + key; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index db65e1d49ae5..2b8642e45f8b 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -813,7 +813,8 @@ void process_shaders() { string_to_spv("lightning_indexer_f16", "lightning_indexer_scalar64.comp", {}); #if defined(GGML_VULKAN_COOPMAT_GLSLC_SUPPORT) - string_to_spv("lightning_indexer_cm_f16", "lightning_indexer_cm.comp", {}); + string_to_spv("lightning_indexer_cm_f16", "lightning_indexer_cm.comp", {{"N_WAVES", "8"}, {"HEADS_PER_TILE", "4"}}); + string_to_spv("lightning_indexer_cm_small_f16", "lightning_indexer_cm.comp", {{"N_WAVES", "1"}, {"HEADS_PER_TILE", "1"}}); string_to_spv("lightning_indexer_decode_cm_f16", "lightning_indexer_decode_cm.comp", {}); #endif string_to_spv("flash_attn_top_k_f16", "flash_attn_top_k.comp", {}); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 23b30958db3e..86e4713303c9 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11411,6 +11411,10 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, true, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 300, 256, 512, false, 3)); + for (int kv : { 127, 128, 129 }) { + test_cases.emplace_back(new test_lightning_indexer(128, 64, kv, 32, 4, 1, GGML_TYPE_F16)); + } + return test_cases; } #ifdef _MSC_VER @@ -11900,6 +11904,11 @@ static std::vector> make_test_cases_perf() { } } } + // DSV4 PP2048 indexer rows after filling source contexts from 8k through 512k. + // The zero-depth kv=512 shape is covered above. + for (int kv : { 2560, 4608, 8704, 16896, 33280, 66048, 131584 }) { + test_cases.emplace_back(new test_lightning_indexer(128, 64, kv, 2048, 1, 1, GGML_TYPE_F16)); + } // sparse top-k FA at V4 decode/prefill shapes — the A/B instrument for the // gather-to-compact work (n_active = n_kv_raw + n_top_k stays fixed as kv grows). From 2a6d1de4376204749f8cfd4a80eb50c57c2fdb83 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 13 Aug 2026 16:00:20 +0000 Subject: [PATCH 115/212] vulkan: extend DeepSeek V4 gather-to-compact to small batches The gap and its consequence were diagnosed by Jaap Buurman: the sparse prefill path gates on batch >= 64 and gather-to-compact gated on batch == 1, so batch 2..63 fell through to dense attention over the whole compressed KV, at a cost that grows with context. That is where a speculative draft lands (n_max 3-5), which is why token generation dropped off sharply with DSpark enabled. This implements the fix for that diagnosis. Give each query token its own gathered top-k block rather than deduplicating into a union. No dedup pass, no atomics, and the size is bounded by n_kv_raw + n_batch*n_top_k regardless of depth. Cross-token rows are neutralised through the mask, which already encodes each token's selection: token t reads -inf on any block that is not its own, so the softmax cannot double count. The compact mask is token-major [n_batch][kv_c], which is what the GQA mask path already expects (m_stride 0, rows stepped by gqa_iq1 * m_row_len). Measured on gfx1151, test-backend-ops perf, medians of 2 launches, n_kv_raw=2304 n_top_k=512. Gather cost is flat in depth (894 us at batch 4 at every depth); dense is not: kv rows batch dense gathered speedup 11008 2 2194 us 680 us 3.23x 11008 4 2204 us 895 us 2.46x 11008 8 2217 us 2217 us 1.00x (gate declines: kv < 2*kv_c) 35584 2 7065 us 680 us 10.38x 35584 4 7099 us 895 us 7.93x 35584 8 7114 us 1316 us 5.41x 133888 2 7552 us 680 us 11.10x 133888 4 7971 us 894 us 8.92x 133888 8 8348 us 1320 us 6.32x Batch 1 is unchanged in behaviour and stays on the same code path. Not validated end to end. A deduplicated union would shrink the gathered set further wherever adjacent draft tokens select overlapping keys, and would lift the batch ceiling documented in the following commit. Suggested-by: Jaap Buurman (@Mushoz) Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 19 +++++-- .../vulkan-shaders/flash_attn_gather.comp | 52 ++++++++++++++----- tests/test-backend-ops.cpp | 17 ++++++ 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 81021b6204ce..840d9e376be7 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1985,7 +1985,7 @@ static_assert(sizeof(vk_op_dsv4_hc_post_push_constants) <= 128); struct vk_op_flash_attn_gather_push_constants { uint32_t n_kv, n_kv_raw, n_top_k, kv_c; - uint32_t nbk1, nbk3, nbt3, nbm3, nem3; + uint32_t nbk1, nbk3, nbt1, nbt3, nbm1, nbm3, nem3, n_batch; }; static_assert(sizeof(vk_op_flash_attn_gather_push_constants) <= 128); @@ -11638,6 +11638,7 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & struct vk_fa_compact_state { bool active = false; uint32_t kv_c = 0; + uint32_t n_batch = 1; vk_subbuffer kc_buf, mc_buf; }; @@ -11655,7 +11656,7 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ static const char * gather_env = getenv("GGML_VK_FA_TOPK_GATHER"); if ((gather_env && gather_env[0] == '0') || !top_k || !ctx->device->pipeline_flash_attn_gather_f16 || - q->ne[1] != 1 || // single-token decode only; batched queries need a union gather + q->ne[1] < 1 || q->ne[1] >= 64 || // 1..63: >=64 goes to the sparse prefill path q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || v->type != GGML_TYPE_F16 || !mask || mask->type != GGML_TYPE_F16 || top_k->type != GGML_TYPE_I32 || q->ne[0] != 512 || k->ne[0] != 512 || v->ne[0] != 512 || q->ne[2] != 64 || @@ -11679,7 +11680,10 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ return false; } - const uint32_t kv_c = GGML_PAD((uint32_t)(n_kv_raw + top_k->ne[0]), 256u); + const uint32_t n_batch = (uint32_t) q->ne[1]; + // every token gets its own top-k block; bounded by n_kv_raw + n_batch*n_top_k regardless + // of context depth, which is the whole point at decode + const uint32_t kv_c = GGML_PAD((uint32_t)(n_kv_raw + (int64_t) n_batch * top_k->ne[0]), 256u); // the gather writes then re-reads ~the active bytes; dense reads the source KV once, // so compaction only pays when the source is comfortably larger than the active set if ((uint64_t) k->ne[1] < 2ull * kv_c) { @@ -11688,7 +11692,7 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ const uint32_t ns = (uint32_t) q->ne[3]; const size_t kc_sz = (size_t) ns * kv_c * 512 * sizeof(ggml_fp16_t); - const size_t mc_sz = (size_t) ns * kv_c * sizeof(ggml_fp16_t); + const size_t mc_sz = (size_t) ns * n_batch * kv_c * sizeof(ggml_fp16_t); if (ctx->prealloc_size_y < kc_sz + mc_sz) { ctx->prealloc_size_y = kc_sz + mc_sz; @@ -11705,9 +11709,12 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ (uint32_t) k->ne[1], (uint32_t) n_kv_raw, (uint32_t) top_k->ne[0], kv_c, (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)), (uint32_t) (k->nb[3] / sizeof(ggml_fp16_t)), + (uint32_t) (top_k->nb[1] / sizeof(int32_t)), (uint32_t) (top_k->nb[3] / sizeof(int32_t)), + (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)), (uint32_t) (mask->nb[3] / sizeof(ggml_fp16_t)), (uint32_t) mask->ne[3], + n_batch, }; st.kc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0); @@ -11721,6 +11728,7 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ st.active = true; st.kv_c = kv_c; + st.n_batch = n_batch; return true; } @@ -11785,6 +11793,9 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx if (ggml_vk_flash_attn_gather_compact(ctx, subctx, q, k, v, mask, dst, fa_compact)) { KV = fa_compact.kv_c; nem0 = fa_compact.kv_c; + nem1 = fa_compact.n_batch; + nem2 = 1; + nem3 = (uint32_t) q->ne[3]; nem1 = N; nem2 = 1; nem3 = (uint32_t) q->ne[3]; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp index 4e3dc1c42623..3c9ac68e2c69 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp @@ -4,9 +4,15 @@ #extension GL_EXT_shader_16bit_storage : require #extension GL_EXT_shader_explicit_arithmetic_types_float16 : require -// Gathers the active KV rows of a top-k sparse attention (DeepSeek V4 CSA decode) into a -// compact contiguous scratch: rows [0, n_kv_raw) of the source (the dense prefix), then the -// n_top_k selected rows, then zero padding up to kv_c. The gathered mask row keeps the +// Gathers the active KV rows of a top-k sparse attention (DeepSeek V4 CSA) into a compact +// contiguous scratch: rows [0, n_kv_raw) of the source (the dense prefix), then, for each of +// the n_batch query tokens, that token's n_top_k selected rows, then zero padding up to kv_c. +// +// n_batch > 1 (small-batch decode, e.g. speculative drafts) gives every token its own block +// rather than deduplicating into a union. That costs n_batch*n_top_k gathered rows instead of +// |union|, but needs no dedup pass and no atomics, and the size is bounded by the worst case +// the caller already allocates for. Cross-token rows are neutralised through the mask: token t +// sees -inf on any block that is not its own, so nothing is double counted in the softmax. The gathered mask row keeps the // per-key mask values so causality/validity survive compaction; invalid top-k indices and // padding get -inf mask and zeroed K (softmax-neutral either way, zeroed so no NaN*0). // One workgroup per compact row; V is the K latent (V==K), so a single gather serves both. @@ -26,9 +32,12 @@ layout(push_constant) uniform Parameters { uint kv_c; // padded compact row count == dispatch row range uint nbk1; // K source row stride, elements uint nbk3; // K source stream stride, elements + uint nbt1; // top_k row (per query token) stride, elements uint nbt3; // top_k stream stride, elements + uint nbm1; // mask source row (per query token) stride, elements uint nbm3; // mask source stream stride, elements uint nem3; // mask ne[3], for stream broadcast + uint n_batch; // query tokens sharing this gather; <= LANES } p; const uint HEAD_SIZE = 512; @@ -39,14 +48,23 @@ void main() { const uint stream = gl_WorkGroupID.z; const uint tid = gl_LocalInvocationIndex; - // map compact row -> source row; p.n_kv is the invalid sentinel - uint src = p.n_kv; + // map compact row -> source row; p.n_kv is the invalid sentinel. + // owner is the token whose block this row belongs to, or ALL_TOKENS for the shared prefix. + const uint ALL_TOKENS = 0xffffffffu; + uint src = p.n_kv; + uint owner = ALL_TOKENS; if (row < p.n_kv_raw) { src = row; - } else if (row < p.n_kv_raw + p.n_top_k) { - const int idx = data_top[stream * p.nbt3 + (row - p.n_kv_raw)]; - if (idx >= 0 && uint(idx) < p.n_kv - p.n_kv_raw) { - src = p.n_kv_raw + uint(idx); + } else { + const uint off = row - p.n_kv_raw; + const uint tok = off / p.n_top_k; + const uint slot = off - tok * p.n_top_k; + if (tok < p.n_batch) { + owner = tok; + const int idx = data_top[stream * p.nbt3 + tok * p.nbt1 + slot]; + if (idx >= 0 && uint(idx) < p.n_kv - p.n_kv_raw) { + src = p.n_kv_raw + uint(idx); + } } } @@ -56,15 +74,21 @@ void main() { [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { data_kc[dst_base + tid + i * LANES] = data_k[src_base + tid + i * LANES]; } - if (tid == 0) { - data_mc[stream * p.kv_c + row] = data_m[(stream % p.nem3) * p.nbm3 + src]; - } } else { [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { data_kc[dst_base + tid + i * LANES] = float16_t(0.0); } - if (tid == 0) { - data_mc[stream * p.kv_c + row] = float16_t(uintBitsToFloat(0xff800000)); + } + + // Compact mask is token-major [n_batch][kv_c], which is what the GQA mask path expects + // (m_stride 0, rows stepped by gqa_iq1 * m_row_len). One lane per token; n_batch <= LANES. + const float NEG_INF = uintBitsToFloat(0xff800000); + if (tid < p.n_batch) { + const uint mc_idx = (stream * p.n_batch + tid) * p.kv_c + row; + float mv = NEG_INF; + if (src < p.n_kv && (owner == ALL_TOKENS || owner == tid)) { + mv = float(data_m[(stream % p.nem3) * p.nbm3 + tid * p.nbm1 + src]); } + data_mc[mc_idx] = float16_t(mv); } } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 86e4713303c9..a3fd3562dbc3 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11407,6 +11407,16 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 257, 256, 512, false)); // ns > 1: the split-K partial-output path indexes O and L/M by stream, so these cover // the stream stride in both regions (single tile and multi-tile). + // small-batch decode (speculative drafts): each token gets its own gathered top-k block, + // so cross-token rows must be masked out or the softmax double counts. kv must be large + // enough that compaction is worth it (the gather gates on kv >= 2*kv_c). + test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 2, 1024, 512, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 3, 1024, 512, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 4, 1024, 512, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 8, 1024, 512, true)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(32768, 16, 2304, 512, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(65536, 63, 2304, 512, false)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 4, 1024, 512, false, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, true, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 300, 256, 512, false, 3)); @@ -11924,6 +11934,13 @@ static std::vector> make_test_cases_perf() { for (int kv : { 11008, 19200, 35584, 68352, 133888 }) { test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, 2048, 2304, 512, false)); } + // small-batch decode at depth: the speculative-draft regime (batch 2-8), where the old + // path fell through to dense attention over the whole compressed KV. + for (int kv : { 11008, 35584, 133888 }) { + for (int nb : { 1, 2, 4, 8 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, nb, 2304, 512, false)); + } + } return test_cases; } From 6a31bf0adbc787510f9c4c0a465019dc135e99f0 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 13 Aug 2026 22:20:56 +0000 Subject: [PATCH 116/212] vulkan: record the resource limits behind the two DSV4 prefill kernels Both additions sit close to a limit that is invisible at the call site. The parallel Lightning Indexer uses 62720 of 65536 bytes of shared memory at N_WAVES=8 / HEADS_PER_TILE=4, so exactly one workgroup fits per CU. That is the intended trade, but raising either constant overruns the budget and the pipeline then fails to create and silently falls back to the small variant. Write the arithmetic down next to the arrays. The small-batch gather's compact set is independent of context depth but grows with batch, so its attention work is quadratic in batch against dense's linear. The kv >= 2*kv_c gate already caps this at about batch 6 at 32k depth and ~30 at 128k, and beyond the cap dense runs instead, so it is never slower. But the measured speedups do not show that ceiling and it is the main argument for building the deduplicated union later. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 10 ++++++++-- .../vulkan-shaders/lightning_indexer_cm.comp | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 840d9e376be7..b84c1512541b 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11681,8 +11681,14 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ } const uint32_t n_batch = (uint32_t) q->ne[1]; - // every token gets its own top-k block; bounded by n_kv_raw + n_batch*n_top_k regardless - // of context depth, which is the whole point at decode + // Every token gets its own top-k block, so the compact set is n_kv_raw + n_batch*n_top_k: + // independent of context depth, which is the point, but GROWING WITH BATCH. Attention work + // is then n_batch * (n_kv_raw + n_batch*n_top_k), i.e. quadratic in batch, against dense's + // n_batch * n_kv. Break-even is n_batch = (n_kv - n_kv_raw) / n_top_k, and the + // kv >= 2*kv_c gate below caps the useful batch at (n_kv/2 - n_kv_raw) / n_top_k -- + // about 6 at 32k depth, ~30 at 128k, batch-capped at 512k. Beyond that the gate declines + // and dense runs, so this can never be slower; it just stops helping. A deduplicated union + // would lift that ceiling wherever draft tokens select overlapping keys. const uint32_t kv_c = GGML_PAD((uint32_t)(n_kv_raw + (int64_t) n_batch * top_k->ne[0]), 256u); // the gather writes then re-reads ~the active bytes; dense reads the source KV once, // so compaction only pays when the source is comfortably larger than the active set diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp index c53eb74f8d4c..d4379e8899d0 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp @@ -45,6 +45,16 @@ const uint VEC_PER_HEAD = HEAD_SIZE / 4; const uint TILE_STRIDE = VEC_PER_HEAD + 2; const uint SCORE_STRIDE = TILE / 4 + 1; +// Shared-memory budget, N_WAVES=8 / HEADS_PER_TILE=4 (TILE 16, HEAD_SIZE 128): +// k_sh 8 * 16 * 34 * 8 B = 34816 +// q_sh 4 * 16 * 34 * 8 B = 17408 +// score_sh 8 * 16 * 5 * 16 B = 10240 +// weight_sh 4 * 16 * 4 B = 256 +// total = 62720 of 65536 (95.7%) +// Only one workgroup fits per CU at that size, which is the intended trade. Raising either +// constant overruns: HEADS_PER_TILE=8 needs 80128 B and the pipeline then fails to create, +// silently falling back to the small variant. The host gates on +// maxComputeSharedMemorySize >= 64 KiB; keep that gate and this arithmetic in sync. shared f16vec4 q_sh[HEADS_PER_TILE][TILE * TILE_STRIDE]; shared f16vec4 k_sh[N_WAVES][TILE * TILE_STRIDE]; shared vec4 score_sh[N_WAVES][TILE * SCORE_STRIDE]; From 964a218fc92ede4bb18a15dae5fea89f1fbbfd9c Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Fri, 14 Aug 2026 00:01:20 +0000 Subject: [PATCH 117/212] vulkan: deduplicated union for DeepSeek V4 small-batch decode Replaces the per-token top-k blocks with one row per DISTINCT selected key, so the compact set stops growing linearly with batch. Measured on the real model, adjacent tokens share 60% of their selections over 4 tokens and 76% over 8, so the union is materially smaller than n_batch*n_top_k. The size is only known on the GPU, so flash-attention now reads its KV bound from a buffer instead of the push constant, behind a new DYNAMIC_KV pipeline flag. Every other pipeline folds the flag away at compile time and is unchanged (13308 FLASH_ATTN_EXT cases pass identically with the union off). No indirect dispatch is needed: FA workgroup counts come from neq1/neq2/neq3 and never from KV, so only the loop bound moves. Padding the count to 256 keeps KV % Bc == 0, which lets the aligned pipeline variant still apply. Dedup marks a bitmap from the top-k lists and compacts by scanning bitmap WORDS. An earlier version scanned the mask row by row: simpler, but it made dedup cost scale with depth and measured 0.71x at kv=133888, ie a loss. The bitmap form is O(n_batch*n_top_k) to mark and R/32 to compact, and is depth-independent. test-backend-ops perf, medians of 2, n_kv_raw=2304 n_top_k=512, fixture overlap 60% (the default generator produces near-zero overlap and would make the union look worthless by construction): kv batch per-token union speedup 35584 2 680 us 632 us 1.08x 35584 4 892 us 720 us 1.24x 35584 8 1315 us 900 us 1.46x 133888 2 679 us 638 us 1.06x 133888 4 892 us 726 us 1.23x 133888 8 1314 us 909 us 1.45x Union cost is flat in depth (720 vs 726 us at batch 4 across a 3.8x depth range), which the per-token form was not. Opt-in via GGML_VK_FA_TOPK_UNION=1, single stream only, and it falls back to the per-token blocks when the compressed region exceeds the shared bitmap. The kv >= 2*kv_c gate still uses the worst case, so the batch ceiling is NOT yet lifted; doing that needs a GPU-side fallback for an oversized union. Suggested-by: Jaap Buurman (@Mushoz) Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 209 +++++++++++++++++- .../vulkan-shaders/flash_attn_base.glsl | 8 +- .../flash_attn_gather_union.comp | 82 +++++++ .../vulkan-shaders/flash_attn_union.comp | 122 ++++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 2 + tests/test-backend-ops.cpp | 20 +- 6 files changed, 428 insertions(+), 15 deletions(-) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_union.comp diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index b84c1512541b..1e22e8be4598 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -64,6 +64,7 @@ typedef struct VkPhysicalDeviceCooperativeMatrixDecodeVectorFeaturesNV { #include #include #include +#include #include #include #include @@ -1111,6 +1112,8 @@ struct vk_device_struct { vk_pipeline pipeline_flash_attn_top_k_f16; vk_pipeline pipeline_flash_attn_top_k_cm_f16; vk_pipeline pipeline_flash_attn_gather_f16; + vk_pipeline pipeline_flash_attn_union_f16; + vk_pipeline pipeline_flash_attn_gather_union_f16; vk_pipeline pipeline_dsv4_hc_pre_f32; vk_pipeline pipeline_dsv4_hc_comb_f32; vk_pipeline pipeline_dsv4_hc_post_f32; @@ -1983,6 +1986,12 @@ struct vk_op_dsv4_hc_post_push_constants { }; static_assert(sizeof(vk_op_dsv4_hc_post_push_constants) <= 128); +struct vk_op_flash_attn_union_push_constants { + uint32_t n_kv, n_kv_raw, n_batch, n_top_k, max_union, nbt1, max_words, pad_to; +}; +struct vk_op_flash_attn_gather_union_push_constants { + uint32_t n_kv, n_kv_raw, kv_c_max, nbk1, nbm1, n_batch; +}; struct vk_op_flash_attn_gather_push_constants { uint32_t n_kv, n_kv_raw, n_top_k, kv_c; uint32_t nbk1, nbk3, nbt1, nbt3, nbm1, nbm3, nem3, n_batch; @@ -4050,14 +4059,16 @@ static vk_fa_tuning_params get_fa_tuning_params(const vk_device& device, uint32_ } static vk_fa_pipeline_state get_fa_pipeline_state(const vk_device& device, const vk_fa_tuning_params& params, uint32_t hsk, uint32_t hsv, bool aligned, bool f32acc, - bool use_mask, bool use_mask_opt, bool use_logit_softcap, ggml_type k_type, ggml_type v_type) { + bool use_mask, bool use_mask_opt, bool use_logit_softcap, ggml_type k_type, ggml_type v_type, + bool use_dynamic_kv = false) { const bool old_amd_windows = device->vendor_id == VK_VENDOR_ID_AMD && device->driver_id == vk::DriverId::eAmdProprietary && (device->architecture == AMD_GCN || device->architecture == AMD_RDNA1 || device->architecture == AMD_RDNA2); uint32_t flags = (use_mask_opt ? 1 : 0) | (use_mask ? 2 : 0) | (use_logit_softcap ? 4 : 0) | - (old_amd_windows ? 8 : 0); + (old_amd_windows ? 8 : 0) | + (use_dynamic_kv ? 16 : 0); const uint32_t subgroup_size = params.disable_subgroups ? 0 : params.subgroup_size; @@ -4732,7 +4743,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { } name = aligned ? "flash_attn_f32_f16_aligned" : "flash_attn_f32_f16"; } - ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 7, + ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 8, sizeof(vk_flash_attn_push_constants), {Br, 1, 1}, get_fa_spec_constants(fa.first), aligned ? Bc : 1, true, !fa_ds, !fa_ds ? fa_sgs : 0); @@ -4768,7 +4779,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { else { spv_data = flash_attn_f32_f16_f16acc_cm1_data; spv_size = flash_attn_f32_f16_f16acc_cm1_len; } name = aligned ? "flash_attn_f32_f16_aligned_cm1" : "flash_attn_f32_f16_cm1"; } - ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 7, + ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 8, sizeof(vk_flash_attn_push_constants), {Br, 1, 1}, get_fa_spec_constants(fa.first), aligned ? Bc : 1, true, !fa_ds, !fa_ds ? fa_sgs : 0); @@ -4805,7 +4816,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { if (f32acc) { spv_data = flash_attn_f32_f16_cm2_data; spv_size = flash_attn_f32_f16_cm2_len; name = "flash_attn_f32_f16_f32acc_cm2"; } else { spv_data = flash_attn_f32_f16_f16acc_cm2_data; spv_size = flash_attn_f32_f16_f16acc_cm2_len; name = "flash_attn_f32_f16_f16acc_cm2"; } } - ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 7, + ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 8, sizeof(vk_flash_attn_push_constants), {Br, 1, 1}, get_fa_spec_constants(fa.first), aligned ? Bc : 1, true, false, 0); } @@ -6315,6 +6326,16 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { "flash_attn_gather_f16", flash_attn_gather_f16_len, flash_attn_gather_f16_data, "main", 5, sizeof(vk_op_flash_attn_gather_push_constants), {1, 1, 1}, {}, 1, true, true, device->subgroup_size); + if (device->subgroup_arithmetic) { + ggml_vk_create_pipeline(device, device->pipeline_flash_attn_union_f16, + "flash_attn_union_f16", flash_attn_union_f16_len, flash_attn_union_f16_data, "main", 3, + sizeof(vk_op_flash_attn_union_push_constants), {1, 1, 1}, {}, 1, true, true, + device->subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_flash_attn_gather_union_f16, + "flash_attn_gather_union_f16", flash_attn_gather_union_f16_len, flash_attn_gather_union_f16_data, "main", 6, + sizeof(vk_op_flash_attn_gather_union_push_constants), {1, 1, 1}, {}, 1, true, true, + device->subgroup_size); + } } // DSv4 fused hyper-connection ops: plain f32 compute, no subgroup/coopmat requirements @@ -11483,6 +11504,56 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & return false; } + // ---- diagnostic: adjacent-token top-k overlap (GGML_VK_TOPK_OVERLAP=1) --------------- + // Decides whether a deduplicated union is worth building for the small-batch path. A + // speculative draft attends at adjacent POSITIONS, so overlap between adjacent prefill + // tokens is the same quantity and a plain deep prefill samples it thousands of times + // without needing a draft model. Reports |union| / (W * n_top_k) for window sizes W. + // Sampled, host-side, off by default; the readback would be far too costly otherwise. + { + static const char * ov_env = getenv("GGML_VK_TOPK_OVERLAP"); + if (ov_env && ov_env[0] == '1') { + static std::mutex ov_mu; + static uint64_t ov_seen = 0; + static std::map> ov_acc; // W -> {sum ratio, n} + std::lock_guard lock(ov_mu); + if ((ov_seen++ % 32) == 0) { // sample: this is a multi-MB readback + const uint32_t nb = (uint32_t) q->ne[1]; + const uint32_t tk = (uint32_t) top_k->ne[0]; + const int32_t rng = (int32_t) (k->ne[1] - n_kv_raw); + std::vector idx((size_t) nb * tk); + vk_subbuffer sb = ggml_vk_tensor_subbuffer(ctx, top_k); + ggml_vk_buffer_read(sb.buffer, sb.offset, idx.data(), idx.size() * sizeof(int32_t)); + for (uint32_t W : {2u, 4u, 8u}) { + if (nb < W) continue; + double sum = 0.0; uint64_t n = 0; + for (uint32_t t0 = 0; t0 + W <= nb; t0 += W) { // disjoint windows + std::unordered_set u; + uint64_t valid = 0; + for (uint32_t t = t0; t < t0 + W; ++t) { + for (uint32_t j = 0; j < tk; ++j) { + const int32_t v = idx[(size_t) t * tk + j]; + if (v >= 0 && v < rng) { u.insert(v); ++valid; } + } + } + if (valid) { sum += (double) u.size() / (double) valid; ++n; } + } + if (n) { auto & a = ov_acc[W]; a.first += sum; a.second += n; } + } + fprintf(stderr, "[topk-overlap] sample %llu (n_batch=%u, kv=%lld)\n", + (unsigned long long) ov_seen, nb, (long long) k->ne[1]); + for (const auto & e : ov_acc) { + const double r = e.second.first / (double) e.second.second; + const double now = (double) n_kv_raw + (double) e.first * tk; + const double dedup = (double) n_kv_raw + r * (double) e.first * tk; + fprintf(stderr, "[topk-overlap] W=%u union/selected=%.3f (overlap %.1f%%) " + "kv_c %.0f -> %.0f projected small-batch gain %.1f%%\n", + e.first, r, 100.0 * (1.0 - r), now, dedup, 100.0 * (1.0 - dedup / now)); + } + } + } + } + vk_op_flash_attn_top_k_push_constants pc = { (uint32_t) q->ne[1], (uint32_t) k->ne[1], (uint32_t) n_kv_raw, (uint32_t) top_k->ne[0], (uint32_t) q->ne[2], @@ -11602,7 +11673,7 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & }; ggml_vk_dispatch_pipeline(ctx, subctx, raw_pipeline, - {tile_q, k_buf, k_buf, tile_mask, tile_q, split_buf, tile_q}, + {tile_q, k_buf, k_buf, tile_mask, tile_q, split_buf, tile_q, tile_q /* dyn-KV: unused */}, raw_pc, {tile_n, NH, NS}); ggml_vk_perf_mark_subop(ctx, subctx, "FA_TOP_K_RAW (sub-op)"); @@ -11637,8 +11708,10 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & struct vk_fa_compact_state { bool active = false; - uint32_t kv_c = 0; + bool dynamic_kv = false; // KV row count lives in kv_buf, not the push constant + uint32_t kv_c = 0; // upper bound; the real count is runtime when dynamic_kv uint32_t n_batch = 1; + vk_subbuffer kv_buf; vk_subbuffer kc_buf, mc_buf; }; @@ -11696,6 +11769,74 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ return false; } + // ---- deduplicated union (GGML_VK_FA_TOPK_UNION=1) ---------------------------------- + // Same compact layout, but one row per DISTINCT selected key instead of one block per + // token. Measured adjacent-token overlap on the real model is 60% at 4 tokens and 76% at + // 8, so the union is materially smaller. Its size is only known on the GPU, so the FA + // reads its KV bound from a buffer (the DYNAMIC_KV pipeline flag) rather than a push + // constant; padding the count to 256 keeps KV % Bc == 0 so the aligned variant still + // applies. Single stream only: the FA takes one KV for all streams. + static const char * union_env = getenv("GGML_VK_FA_TOPK_UNION"); + if (union_env && union_env[0] == '1' && q->ne[3] == 1 && n_batch > 1 && + ctx->device->pipeline_flash_attn_union_f16 && ctx->device->pipeline_flash_attn_gather_union_f16) { + const uint32_t max_union = (uint32_t) ((int64_t) n_batch * top_k->ne[0]); + // shared bitmap capacity in flash_attn_union.comp + const uint32_t max_words = 12288; + const uint32_t need_words = (uint32_t) (((k->ne[1] - n_kv_raw) + 31) / 32); + if (need_words > max_words) { + goto union_unavailable; // fall through to the per-token block form + } + const size_t ukc_sz = (size_t) kv_c * 512 * sizeof(ggml_fp16_t); + const size_t umc_sz = (size_t) n_batch * kv_c * sizeof(ggml_fp16_t); + const size_t ul_sz = (size_t) max_union * sizeof(uint32_t); + const size_t uc_sz = 2 * sizeof(uint32_t); + const size_t need = ukc_sz + umc_sz + ul_sz + uc_sz; + if (ctx->prealloc_size_y < need) { + ctx->prealloc_size_y = need; + ggml_vk_preallocate_buffers(ctx, subctx); + } + if (ctx->prealloc_y_need_sync) { + ggml_vk_sync_buffers(ctx, subctx); + } + const vk_subbuffer kc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0); + const vk_subbuffer mc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, ukc_sz); + const vk_subbuffer ul_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, ukc_sz + umc_sz); + const vk_subbuffer uc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, ukc_sz + umc_sz + ul_sz); + + ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_union_f16, 1); + ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_gather_union_f16, 1); + + const vk_op_flash_attn_union_push_constants upc = { + (uint32_t) k->ne[1], (uint32_t) n_kv_raw, n_batch, (uint32_t) top_k->ne[0], max_union, + (uint32_t) (top_k->nb[1] / sizeof(int32_t)), max_words, 256u, + }; + ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_union_f16, + { ggml_vk_tensor_subbuffer(ctx, top_k), ul_buf, uc_buf }, upc, { 1, 1, 1 }); + ggml_vk_sync_buffers(ctx, subctx); + + const vk_op_flash_attn_gather_union_push_constants gpc = { + (uint32_t) k->ne[1], (uint32_t) n_kv_raw, kv_c, + (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)), + (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)), + n_batch, + }; + ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_gather_union_f16, + { ggml_vk_tensor_subbuffer(ctx, k), ul_buf, ggml_vk_tensor_subbuffer(ctx, mask), + kc_buf, mc_buf, uc_buf }, gpc, { kv_c, 1, 1 }); + ggml_vk_sync_buffers(ctx, subctx); + ctx->prealloc_y_need_sync = true; + + st.active = true; + st.dynamic_kv = true; + st.kv_c = kv_c; + st.n_batch = n_batch; + st.kc_buf = kc_buf; + st.mc_buf = mc_buf; + st.kv_buf = uc_buf; + return true; + } +union_unavailable:; + const uint32_t ns = (uint32_t) q->ne[3]; const size_t kc_sz = (size_t) ns * kv_c * 512 * sizeof(ggml_fp16_t); const size_t mc_sz = (size_t) ns * n_batch * kv_c * sizeof(ggml_fp16_t); @@ -11732,6 +11873,53 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ ggml_vk_sync_buffers(ctx, subctx); ctx->prealloc_y_need_sync = true; + // ---- diagnostic: measure real top-k overlap between the query tokens ---------------- + // GGML_VK_TOPK_OVERLAP=1. Off by default and never touched on the hot path. Answers the + // one question that decides whether a deduplicated union is worth building: this path + // gathers n_batch*n_top_k rows, a union would gather |union| rows, and the op's cost is + // linear in that count (measured: 204.4 / 205.7 / 205.5 us per row at batch 2 / 4 / 8). + // Needs a real model - synthetic top-k selections say nothing about real overlap, which + // is exactly how the mul_mat_id large-tile probe misled once before. + static const char * overlap_env = getenv("GGML_VK_TOPK_OVERLAP"); + if (overlap_env && overlap_env[0] == '1' && n_batch > 1) { + static std::mutex ov_mutex; + static uint64_t ov_calls = 0, ov_selected = 0, ov_union = 0; + static std::map> ov_by_batch; // n_batch -> {selected, union} + const size_t n_idx = (size_t) n_batch * top_k->ne[0]; + std::vector idx(n_idx); + vk_subbuffer top_sb = ggml_vk_tensor_subbuffer(ctx, top_k); + ggml_vk_buffer_read(top_sb.buffer, top_sb.offset, idx.data(), n_idx * sizeof(int32_t)); + + const int32_t range = (int32_t) (k->ne[1] - n_kv_raw); + std::unordered_set uni; + uint64_t valid = 0; + for (size_t i = 0; i < n_idx; ++i) { + const int32_t v = idx[i]; + if (v >= 0 && v < range) { uni.insert(v); ++valid; } + } + std::lock_guard lock(ov_mutex); + ov_calls++; ov_selected += valid; ov_union += uni.size(); + auto & e = ov_by_batch[n_batch]; + e.first += valid; e.second += uni.size(); + if ((ov_calls % 256) == 0) { + fprintf(stderr, "[topk-overlap] calls=%llu selected=%llu union=%llu " + "union/selected=%.3f => a dedup union would gather %.1f%% fewer compressed rows\n", + (unsigned long long) ov_calls, (unsigned long long) ov_selected, + (unsigned long long) ov_union, + ov_selected ? (double) ov_union / (double) ov_selected : 0.0, + ov_selected ? 100.0 * (1.0 - (double) ov_union / (double) ov_selected) : 0.0); + for (const auto & kv : ov_by_batch) { + const double ratio = kv.second.first ? (double) kv.second.second / (double) kv.second.first : 0.0; + // projected op speedup uses the measured linear cost model on kv_c + const double now = (double) n_kv_raw + (double) kv.first * (double) top_k->ne[0]; + const double dedup = (double) n_kv_raw + ratio * (double) kv.first * (double) top_k->ne[0]; + fprintf(stderr, "[topk-overlap] n_batch=%u union/selected=%.3f " + "kv_c %.0f -> %.0f projected op gain %.1f%%\n", + kv.first, ratio, now, dedup, 100.0 * (1.0 - dedup / now)); + } + } + } + st.active = true; st.kv_c = kv_c; st.n_batch = n_batch; @@ -11943,7 +12131,8 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx bool use_mask_opt = mask && nem1 >= 32 && nem0 * nem1 > 32768 && nem0 >= tuning_params.block_cols * 16 && (ctx->device->architecture != vk_device_architecture::AMD_GCN || HSK > 256 || HSV > 256); vk_fa_pipeline_state fa_pipeline_state = get_fa_pipeline_state(ctx->device, tuning_params, HSK, HSV, aligned, f32acc, - mask != nullptr, use_mask_opt, logit_softcap != 0, k_type_eff, v_type_eff); + mask != nullptr, use_mask_opt, logit_softcap != 0, k_type_eff, v_type_eff, + fa_compact.dynamic_kv); vk_pipeline pipeline = nullptr; @@ -12145,7 +12334,7 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx vk_subbuffer split_k_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_split_k, 0); ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, - {q_buf, k_buf, v_buf, mask_buf, sinks_buf, split_k_buf, mask_opt_buf}, + {q_buf, k_buf, v_buf, mask_buf, sinks_buf, split_k_buf, mask_opt_buf, fa_compact.dynamic_kv ? fa_compact.kv_buf : q_buf}, pc, { dispatch_x, workgroups_y, workgroups_z }); ggml_vk_sync_buffers(ctx, subctx); @@ -12160,7 +12349,7 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx workgroups_x *= pipeline->wg_denoms[0]; } ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, - {q_buf, k_buf, v_buf, mask_buf, sinks_buf, dst_buf, mask_opt_buf}, + {q_buf, k_buf, v_buf, mask_buf, sinks_buf, dst_buf, mask_opt_buf, fa_compact.dynamic_kv ? fa_compact.kv_buf : q_buf}, pc, { workgroups_x, workgroups_y, workgroups_z }); } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl index e308c7214dd3..b562c5d78749 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl @@ -24,6 +24,11 @@ const bool USE_MASK_OPT = (Flags & 1) != 0; const bool MASK_ENABLE = (Flags & 2) != 0; const bool LOGIT_SOFTCAP = (Flags & 4) != 0; const bool OLD_AMD_WINDOWS = (Flags & 8) != 0; +// KV comes from a buffer instead of the push constant. Used by paths that compact K/V on the +// GPU, where the row count is only known after a dedup pass and so cannot be pushed. The +// workgroup counts derive from neq1/neq2/neq3 and never from KV, so no indirect dispatch is +// needed: only this loop bound changes. Folds away for every other pipeline. +const bool DYNAMIC_KV = (Flags & 16) != 0; // Round up head sizes to a multiple of 16, for coopmat1/coopmat2 paths const uint32_t HSK_pad = (HSK + 15) & ~15; @@ -81,6 +86,7 @@ layout (binding = 5) writeonly buffer O {D_TYPE data_o[];}; layout (binding = 5) writeonly buffer OV4 {D_TYPEV4 data_ov4[];}; layout (binding = 6) readonly buffer MO {uint32_t data_mask_opt[];}; +layout (binding = 7) readonly buffer KVB {uint32_t data_kv_dyn[];}; #define MASK_OPT_ALL_NEG_INF 1 #define MASK_OPT_ALL_ZERO 2 @@ -150,7 +156,7 @@ bool partial_output; void init_indices() { N = p.N; - KV = p.KV; + KV = DYNAMIC_KV ? data_kv_dyn[0] : p.KV; gqa_ratio = p.gqa_ratio & 0xffff; split_k_num = p.k_num & 0xffff; output_k_num = p.k_num >> 16; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp new file mode 100644 index 000000000000..775b1bc96c62 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp @@ -0,0 +1,82 @@ +#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 + +// Gathers K/V and the mask for the DeepSeek V4 small-batch decode path, using the deduplicated +// union produced by flash_attn_union.comp: rows [0, n_kv_raw) of the source, then one row per +// distinct selected compressed row, then padding. +// +// Simpler than the block-per-token form it replaces, because a union row appears exactly once: +// there is no owner to track and no cross-token masking to apply. Each token just reads its own +// mask value for the gathered source row, which is already -inf where that token did not select +// it, so the softmax still cannot double count. +// +// Row count is a runtime value in data_c[0]; rows past the union are zeroed K and -inf mask, +// which is softmax-neutral and keeps the padded tail harmless. + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer KBuf { float16_t data_k[]; }; +layout(binding = 1) readonly buffer UBuf { uint data_u[]; }; +layout(binding = 2) readonly buffer MBuf { float16_t data_m[]; }; +layout(binding = 3) writeonly buffer KcBuf { float16_t data_kc[]; }; +layout(binding = 4) writeonly buffer McBuf { float16_t data_mc[]; }; +layout(binding = 5) readonly buffer CBuf { uint data_c[]; }; + +layout(push_constant) uniform Parameters { + uint n_kv; + uint n_kv_raw; + uint kv_c_max; + uint nbk1; + uint nbm1; + uint n_batch; +} p; + +const uint HEAD_SIZE = 512; +const uint LANES = 64; + +void main() { + const uint row = gl_WorkGroupID.x; + const uint tid = gl_LocalInvocationIndex; + + const uint kv_c = data_c[0]; // padded compact rows, the FA's runtime KV + const uint n_uni = data_c[1]; // unpadded union size + + if (row >= kv_c) { + return; + } + + uint src = p.n_kv; // sentinel: invalid + if (row < p.n_kv_raw) { + src = row; + } else if (row - p.n_kv_raw < n_uni) { + src = p.n_kv_raw + data_u[row - p.n_kv_raw]; + } + + const uint dst_base = row * HEAD_SIZE; + if (src < p.n_kv) { + const uint src_base = src * p.nbk1; + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { + data_kc[dst_base + tid + i * LANES] = data_k[src_base + tid + i * LANES]; + } + } else { + [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { + data_kc[dst_base + tid + i * LANES] = float16_t(0.0); + } + } + + // Compact mask is token-major [n_batch][kv_c]. The stride must be the RUNTIME kv_c, not + // kv_c_max: the FA derives m_row_len from KV, which is now that same runtime value, and a + // mismatch would step the mask by the wrong amount for every token past the first. The + // buffer is allocated for kv_c_max, so a smaller stride simply leaves a tail unused. + const float NEG_INF = uintBitsToFloat(0xff800000); + if (tid < p.n_batch) { + float mv = NEG_INF; + if (src < p.n_kv) { + mv = float(data_m[tid * p.nbm1 + src]); + } + data_mc[tid * kv_c + row] = float16_t(mv); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_union.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_union.comp new file mode 100644 index 000000000000..cd779e0a0f99 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_union.comp @@ -0,0 +1,122 @@ +#version 450 + +#extension GL_EXT_control_flow_attributes : require +#extension GL_KHR_shader_subgroup_arithmetic : require +#extension GL_KHR_shader_subgroup_basic : require + +// Builds the deduplicated union of the compressed rows selected by any of the n_batch query +// tokens, for the DeepSeek V4 small-batch decode path. +// +// Marks a bitmap from the top-k index lists, then compacts by scanning bitmap WORDS. The +// marking is O(n_batch * n_top_k), independent of context depth, and the compaction touches +// R/32 words instead of R rows. An earlier version scanned the mask row by row instead: that +// is simpler, but it made dedup cost scale with depth and measured 0.71x (ie a loss) at +// kv=133888, which defeats the purpose. Do not go back to it. +// +// Ascending source order falls out of the bitmap scan, so the result is deterministic. +// +// Emits the index list plus the PADDED compact row count. Padding to a multiple of pad_to +// (a multiple of every FA block width) is what lets flash-attention keep its "aligned" +// pipeline variant even though the row count is now a runtime value. +// +// One workgroup: the bitmap and the running offset both live in shared memory. + +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer TBuf { int data_top[]; }; +layout(binding = 1) writeonly buffer UBuf { uint data_u[]; }; +layout(binding = 2) writeonly buffer CBuf { uint data_c[]; }; + +layout(push_constant) uniform Parameters { + uint n_kv; + uint n_kv_raw; + uint n_batch; + uint n_top_k; + uint max_union; + uint nbt1; + uint max_words; // capacity of the shared bitmap, host-checked + uint pad_to; +} p; + +// 12288 words = 393216 compressed rows, ~48 KiB of shared memory. +const uint MAX_WORDS = 12288; + +shared uint bitmap[MAX_WORDS]; +shared uint wave_totals[16]; +shared uint base_sh; + +void main() { + const uint tid = gl_LocalInvocationIndex; + const uint lane = gl_SubgroupInvocationID; + const uint wave = gl_SubgroupID; + const uint nwave = gl_NumSubgroups; + const uint R = p.n_kv - p.n_kv_raw; + const uint words = min((R + 31) / 32, p.max_words); + + // phase 1: clear + for (uint w = tid; w < words; w += gl_WorkGroupSize.x) { + bitmap[w] = 0; + } + if (tid == 0) { + base_sh = 0; + } + barrier(); + + // phase 2: mark. Depth-independent: one pass over the top-k lists. + const uint n_cand = p.n_batch * p.n_top_k; + for (uint c = tid; c < n_cand; c += gl_WorkGroupSize.x) { + const uint t = c / p.n_top_k; + const uint j = c - t * p.n_top_k; + const int idx = data_top[t * p.nbt1 + j]; + if (idx >= 0 && uint(idx) < R) { + atomicOr(bitmap[uint(idx) >> 5], 1u << (uint(idx) & 31u)); + } + } + barrier(); + + // phase 3: compact. R/32 iterations, ascending, exact running offset in shared memory. + for (uint chunk = 0; chunk < words; chunk += gl_WorkGroupSize.x) { + const uint w = chunk + tid; + const uint bits = w < words ? bitmap[w] : 0u; + const uint cnt = bitCount(bits); + + const uint wave_off = subgroupExclusiveAdd(cnt); + const uint wave_tot = subgroupAdd(cnt); + if (lane == 0) { + wave_totals[wave] = wave_tot; + } + barrier(); + + uint prefix = 0; + for (uint i = 0; i < wave; ++i) { + prefix += wave_totals[i]; + } + uint total = 0; + for (uint i = 0; i < nwave; ++i) { + total += wave_totals[i]; + } + + uint slot = base_sh + prefix + wave_off; + uint rem = bits; + while (rem != 0) { + const uint b = findLSB(rem); + rem &= rem - 1; + if (slot < p.max_union) { + data_u[slot] = w * 32 + b; + } + ++slot; + } + barrier(); + if (tid == 0) { + base_sh += total; + } + barrier(); + } + + if (tid == 0) { + const uint u = min(base_sh, p.max_union); + const uint rows = p.n_kv_raw + u; + data_c[0] = ((rows + p.pad_to - 1) / p.pad_to) * p.pad_to; + data_c[1] = u; + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 2b8642e45f8b..38e9e4e0af5b 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -822,6 +822,8 @@ void process_shaders() { string_to_spv("flash_attn_top_k_cm_f16", "flash_attn_top_k_cm.comp", {}); #endif string_to_spv("flash_attn_gather_f16", "flash_attn_gather.comp", {}); + string_to_spv("flash_attn_union_f16", "flash_attn_union.comp", {}); + string_to_spv("flash_attn_gather_union_f16", "flash_attn_gather_union.comp", {}); string_to_spv("dsv4_hc_pre_f32", "dsv4_hc_pre.comp", {}); string_to_spv("dsv4_hc_comb_f32", "dsv4_hc_comb.comp", {}); string_to_spv("dsv4_hc_post_f32", "dsv4_hc_post.comp", {}); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index a3fd3562dbc3..c6c4796c26cd 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -8039,12 +8039,13 @@ struct test_flash_attn_ext_top_k : public test_case { const int64_t n_top_k; // selected keys per query token const bool sinks; const int64_t ns; // sequences (ne3); >1 exercises the split-K stream stride + const int64_t ov; // % of each token's picks shared with its neighbours (dedup-union realism) static constexpr int64_t hs = 512; // V4 CSA head size, K == V latent static constexpr int64_t nh = 64; // V4 CSA query heads (MQA) std::string vars() override { - return VARS_TO_STR6(kv, nb, n_kv_raw, n_top_k, sinks, ns); + return VARS_TO_STR7(kv, nb, n_kv_raw, n_top_k, sinks, ns, ov); } double max_nmse_err() override { @@ -8058,8 +8059,8 @@ struct test_flash_attn_ext_top_k : public test_case { return 2 * nh * nb * ns * (hs + hs) * (n_kv_raw + n_top_k); } - test_flash_attn_ext_top_k(int64_t kv = 768, int64_t nb = 8, int64_t n_kv_raw = 64, int64_t n_top_k = 128, bool sinks = false, int64_t ns = 1) - : kv(kv), nb(nb), n_kv_raw(n_kv_raw), n_top_k(n_top_k), sinks(sinks), ns(ns) {} + test_flash_attn_ext_top_k(int64_t kv = 768, int64_t nb = 8, int64_t n_kv_raw = 64, int64_t n_top_k = 128, bool sinks = false, int64_t ns = 1, int64_t ov = 0) + : kv(kv), nb(nb), n_kv_raw(n_kv_raw), n_top_k(n_top_k), sinks(sinks), ns(ns), ov(ov) {} ggml_tensor * build_graph(ggml_context * ctx) override { ggml_tensor * q = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, hs, nb, nh, ns); @@ -8125,7 +8126,10 @@ struct test_flash_attn_ext_top_k : public test_case { for (int64_t j = 0; j < n_top_k; ++j) { // offset the selection by the stream too, so a dropped stream stride // reads another sequence's keys and shows up as a mismatch - int32_t idx = (int32_t) ((j * range) / n_top_k + b + s * 7) % (int32_t) range; + const bool shared = (int64_t) j * 100 < n_top_k * ov; + int32_t idx = shared + ? (int32_t) ((j * range) / n_top_k + s * 7) % (int32_t) range + : (int32_t) ((j * range) / n_top_k + b + s * 7) % (int32_t) range; if (j == n_top_k - 1 && b == 0 && s == 0) { idx = -1; // exercise the ignore-invalid-index path } else { @@ -11941,6 +11945,14 @@ static std::vector> make_test_cases_perf() { test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, nb, 2304, 512, false)); } } + // Same shapes with realistic adjacent-token overlap. Measured on DeepSeek-V4-Flash the + // real overlap is 60% over 4 adjacent tokens and 76% over 8; the default generator is + // near 0%, which would make a deduplicated union look worthless by construction. + for (int kv : { 35584, 133888 }) { + for (int nb : { 2, 4, 8 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, nb, 2304, 512, false, 1, 60)); + } + } return test_cases; } From 628788d27a8de55f53b5b95d2272319e3645899b Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Fri, 14 Aug 2026 01:21:31 +0000 Subject: [PATCH 118/212] vulkan: gate the DeepSeek V4 small-batch union on the measured union size The union shrinks the compact set but the gate could not see it. kv_c is the worst case n_kv_raw + n_batch*n_top_k, so at batch 8 and 32k depth the gate priced 6400 rows against 11008 source rows, declined, and dense attention ran over the whole compressed KV -- even though those selections deduplicate to about 3300 rows. That is the batch ceiling the previous commit documented and did not lift. The union size is only known on the device, and the note on the previous commit assumed lifting the ceiling therefore needed a GPU-side fallback for an oversized union. It does not. The union is bounded by the source, so there is no dispatch to recover from; the missing piece was only a measurement. The union shader now writes its count into a small host-visible buffer and the host prices the next step from it. The read is deliberately unsynchronised and one graph stale -- overlap is a property of the model and the draft, not of one op -- and a wrong estimate costs part of one step, never correctness, because every allocation and dispatch bound is still the worst case. Estimates are held per batch size. A speculative decode varies the batch with the accept count, and the overlap itself varies with the batch (0.64 at 2 tokens, 0.40 at 4, 0.24 at 8), so a single slot would be invalidated on nearly every step. Each estimate tracks the latest measurement, lightly smoothed, and a declined step dispatches the scan in a new count_only mode that writes no index list. Both of those are deliberate and were measured the other way round first. Holding a decaying peak to stay conservative, and sampling the probe every 256 declines to stay cheap, together produced 1992 us on a shape the union runs in 898: the asymmetry actually runs the other way, because an estimate that is too high declines compaction and forgoes 2-3x for as long as it stays high, while one that is too low costs a single step and is corrected by the count that step produces. Sampling makes it worse still, since a decline is exactly the state in which the compact path stops refreshing the estimate, so the stale value stays latched for the whole sampling period. The probe is one workgroup against the ~2.2 ms dense op it rides along with, and it costs 0.2% of a declined step. The same measurement settles the reverse case. Where selections do not overlap, the union is the same size as the per-token blocks and its scan is pure cost, so those now stay on the per-token path rather than being admitted whenever the worst-case gate happened to allow them. test-backend-ops perf, medians of 3 launches counterbalanced A B B A A B, spreads at or under 0.4%, n_kv_raw=2304 n_top_k=512, at kv=11008 (~32k source) which is the shape where the gate used to decline. The fixture's ov is a per-token share, not the union/selected ratio the model was measured by: at nb tokens it yields (ov + (1-ov)*nb)/nb of the selections, so ov=86 is the setting that reproduces the 0.243 measured on DeepSeek-V4-Flash over 8 adjacent tokens, and ov=60 is deliberately more pessimistic than the model. batch overlap before after 8 ov=86 2215.2 us 704.8 us 3.14x 16 ov=86 2227.3 us 841.3 us 2.65x 8 ov=60 2214.5 us 897.9 us 2.47x 16 ov=60 2226.2 us 2231.2 us 1.00x union does not fit, declined 8 ov=0 2213.5 us 2219.0 us 1.00x nothing to deduplicate, declined The union cost is depth-independent as before, so the lifted cells now sit alongside the deeper ones: 897.9 / 900.7 / 906.6 us at batch 8 ov=60 across kv 11008 / 35584 / 133888. Every cell at kv 35584 and 133888 is 1.00x: this changes which shapes are admitted, not how the union performs once admitted. Both declines above are the estimate working rather than failing. At ov=60 and batch 16 the union really is 5888 rows against 11008 source rows and compaction would not pay; at ov=0 there is no overlap to exploit at all. Verified from the executed graph with GGML_VK_FA_UNION_STATS=1, added here, which reports the measured union/candidate ratio and the resulting decision rather than leaving engagement to be inferred from a timing. It reads 0.246 at ov=86 and batch 8, against the 0.243 measured on the model. 13310 FLASH_ATTN_EXT cases pass with the union off and on, including three new overlap cases at the shapes the gate now admits. End to end on a model that has no top-k tensor at all, so the compaction path is never reached and the change should be structurally inert: Qwen3-Coder-30B-A3B UD-Q4_K_XL, same counterbalanced order, medians of 3. pp512 1578.20 -> 1591.67 t/s and tg32 96.89 -> 97.28 at d0; pp512 700.64 -> 699.32 and tg32 53.92 -> 54.45 at d16384. All within 1%. The union path itself cannot be checked this way: it needs batch 2..63 decode, which only a speculative draft produces. Still measured against a synthetic fixture rather than real draft tokens: that needs a runnable target and draft pair, which this box cannot host. Suggested-by: Jaap Buurman (@Mushoz) Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 214 +++++++++++++++--- .../vulkan-shaders/flash_attn_union.comp | 14 +- tests/test-backend-ops.cpp | 21 +- 3 files changed, 216 insertions(+), 33 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 1e22e8be4598..182f51741cdb 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1987,7 +1987,7 @@ struct vk_op_dsv4_hc_post_push_constants { static_assert(sizeof(vk_op_dsv4_hc_post_push_constants) <= 128); struct vk_op_flash_attn_union_push_constants { - uint32_t n_kv, n_kv_raw, n_batch, n_top_k, max_union, nbt1, max_words, pad_to; + uint32_t n_kv, n_kv_raw, n_batch, n_top_k, max_union, nbt1, max_words, pad_to, count_only; }; struct vk_op_flash_attn_gather_union_push_constants { uint32_t n_kv, n_kv_raw, kv_c_max, nbk1, nbm1, n_batch; @@ -2509,6 +2509,14 @@ struct ggml_backend_vk_context { uint64_t fa_dequant_gate_sz; bool fa_dequant_gate_fits; bool fa_dequant_gate_logged; + // DeepSeek V4 small-batch union: the compact row count is produced on the device, so the + // host prices compaction from the last count it wrote. See ggml_vk_fa_union_estimate. + // Per batch size, because the overlap depends on it (measured 0.64 at 2 tokens, 0.40 at 4, + // 0.24 at 8) and because a speculative decode varies the batch with the accept count, so a + // single slot would be invalidated on nearly every step. This path caps the batch at 64. + vk_buffer fa_union_stat; + float fa_union_est_ratio[64]; // union / candidates, decaying peak; 0 = unseeded + uint64_t fa_union_declines; vk::Fence fence, almost_ready_fence; bool submit_pending {}; bool almost_ready_fence_pending {}; @@ -8059,6 +8067,8 @@ static void ggml_vk_init(ggml_backend_vk_context * ctx, size_t idx) { ctx->fa_dequant_gate_sz = 0; ctx->fa_dequant_gate_fits = false; ctx->fa_dequant_gate_logged = false; + memset(ctx->fa_union_est_ratio, 0, sizeof(ctx->fa_union_est_ratio)); + ctx->fa_union_declines = 0; // Fixed size of 1KB, for deterministic behavior ctx->prealloc_size_add_rms_partials = 1024; @@ -11715,6 +11725,80 @@ struct vk_fa_compact_state { vk_subbuffer kc_buf, mc_buf; }; +// Small host-visible buffer holding the last union count the device produced: +// [0] padded compact rows (also read by the gather and by the FA), [1] raw union size, +// [2] the candidate count it came from, [3] the batch it came from. Host-visible is a +// requirement rather than a preference here - the whole point is that the host can read it +// without submitting. +static bool ggml_vk_fa_union_stat_init(ggml_backend_vk_context * ctx) { + if (ctx->fa_union_stat) { + return ctx->fa_union_stat->ptr != nullptr; + } + try { + ctx->fa_union_stat = ggml_vk_create_buffer(ctx->device, 64, + {vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent}); + } catch (const vk::SystemError &) { + return false; + } + if (ctx->fa_union_stat->ptr == nullptr) { + return false; + } + memset(ctx->fa_union_stat->ptr, 0, 64); + return true; +} + +// Host-coherent memory still needs the write made available to the host domain. This is the +// only barrier in the path that names eHost, and it is cheap: nothing waits on it, it just +// lets the next graph's host-side read see the count this one produced. +static void ggml_vk_fa_union_stat_host_barrier(vk_context & subctx) { + subctx->s->buffer->buf.pipelineBarrier( + vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eHost, + {}, + { { vk::AccessFlagBits::eShaderWrite, vk::AccessFlagBits::eHostRead } }, + {}, {}); +} + +// Price the union without synchronising for it. The count read here belongs to whichever call +// last wrote the slot, which under a decode is the final flash-attention op of the previous +// graph - one token stale, and that is fine: selection overlap is a property of the model and +// the draft, not of an individual op, and a wrong estimate costs part of one step rather than +// correctness (the compact buffers are still sized for the worst case). +// +// Tracks the latest measurement, lightly smoothed. The asymmetry runs the other way from what +// a conservative estimator would assume: an estimate that is too HIGH declines compaction and +// forgoes 2-3x for as long as it stays high, while one that is too low costs a single step at +// roughly dense cost and is corrected by the count that step produces. An earlier version held +// a decaying peak instead and measured 1992 us where the union delivers 900, because a spell of +// genuinely low overlap pinned the estimate and 0.999 per read took hundreds of steps to relax. +// +// The words are read without ordering against the device write, so they can come from +// different calls. The sample is filed under the batch the device reported rather than the +// batch being priced, so a torn read costs one mispriced step for that batch and then +// corrects, which is the same failure the estimate already tolerates. +// +// Returns the padded compact row count to gate on, or 0 when this batch is unseeded. +static uint32_t ggml_vk_fa_union_estimate(ggml_backend_vk_context * ctx, uint32_t n_kv_raw, + uint32_t n_batch, uint32_t n_cand) { + const volatile uint32_t * stat = (const volatile uint32_t *) ctx->fa_union_stat->ptr; + const uint32_t u = stat[1]; + const uint32_t cand = stat[2]; + const uint32_t nb_obs = stat[3]; + + if (u > 0 && cand > 0 && nb_obs > 0 && nb_obs < 64) { + const float r = std::min(1.0f, (float) u / (float) cand); + float & e = ctx->fa_union_est_ratio[nb_obs]; + e = e > 0.0f ? 0.5f * r + 0.5f * e : r; + } + + const float ratio = ctx->fa_union_est_ratio[n_batch]; + if (ratio <= 0.0f) { + return 0; + } + return GGML_PAD(n_kv_raw + (uint32_t) ceilf(ratio * (float) n_cand), 256u); +} + // V4 sparse decode (gather-to-compact): the sparse prefill shader above gates on // q->ne[1] >= 64, so single-token decode otherwise attends densely over the whole // compressed KV, at a cost that grows with context. Instead, gather the active rows @@ -11754,20 +11838,17 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ } const uint32_t n_batch = (uint32_t) q->ne[1]; - // Every token gets its own top-k block, so the compact set is n_kv_raw + n_batch*n_top_k: - // independent of context depth, which is the point, but GROWING WITH BATCH. Attention work - // is then n_batch * (n_kv_raw + n_batch*n_top_k), i.e. quadratic in batch, against dense's - // n_batch * n_kv. Break-even is n_batch = (n_kv - n_kv_raw) / n_top_k, and the - // kv >= 2*kv_c gate below caps the useful batch at (n_kv/2 - n_kv_raw) / n_top_k -- - // about 6 at 32k depth, ~30 at 128k, batch-capped at 512k. Beyond that the gate declines - // and dense runs, so this can never be slower; it just stops helping. A deduplicated union - // would lift that ceiling wherever draft tokens select overlapping keys. - const uint32_t kv_c = GGML_PAD((uint32_t)(n_kv_raw + (int64_t) n_batch * top_k->ne[0]), 256u); - // the gather writes then re-reads ~the active bytes; dense reads the source KV once, - // so compaction only pays when the source is comfortably larger than the active set - if ((uint64_t) k->ne[1] < 2ull * kv_c) { - return false; - } + const uint32_t n_cand = (uint32_t) ((int64_t) n_batch * top_k->ne[0]); + // Worst case: every token gets its own top-k block, so the compact set is + // n_kv_raw + n_batch*n_top_k -- independent of context depth, which is the point, but + // GROWING WITH BATCH. Attention work is then n_batch * (n_kv_raw + n_batch*n_top_k), i.e. + // quadratic in batch, against dense's n_batch * n_kv. Break-even is + // n_batch = (n_kv - n_kv_raw) / n_top_k, and a kv >= 2*kv_c gate on this worst case caps + // the useful batch at (n_kv/2 - n_kv_raw) / n_top_k -- about 6 at 32k depth, ~30 at 128k. + // Beyond that the gate declines and dense runs, so this can never be slower; it just stops + // helping. The union below lifts that ceiling by gating on what the selections actually + // deduplicate to; kv_c remains the bound for every allocation and dispatch count. + const uint32_t kv_c = GGML_PAD((uint32_t) (n_kv_raw + (int64_t) n_cand), 256u); // ---- deduplicated union (GGML_VK_FA_TOPK_UNION=1) ---------------------------------- // Same compact layout, but one row per DISTINCT selected key instead of one block per @@ -11776,39 +11857,101 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ // reads its KV bound from a buffer (the DYNAMIC_KV pipeline flag) rather than a push // constant; padding the count to 256 keeps KV % Bc == 0 so the aligned variant still // applies. Single stream only: the FA takes one KV for all streams. + // + // Gated on the ESTIMATED union rather than on kv_c, which is why this sits above the + // worst-case gate: at batch 8 and 32k depth the worst case is 6400 rows against 11008 + // source rows and would decline, while the union measures around 3300 and is well worth + // compacting. kv_c stays the worst case for every allocation and dispatch bound, so a + // wrong estimate is a slow step, never a wrong answer. + const uint32_t max_words = 12288; // shared bitmap capacity in flash_attn_union.comp + const bool bitmap_fits = (uint64_t) ((k->ne[1] - n_kv_raw) + 31) / 32 <= max_words; + static const char * union_env = getenv("GGML_VK_FA_TOPK_UNION"); - if (union_env && union_env[0] == '1' && q->ne[3] == 1 && n_batch > 1 && - ctx->device->pipeline_flash_attn_union_f16 && ctx->device->pipeline_flash_attn_gather_union_f16) { - const uint32_t max_union = (uint32_t) ((int64_t) n_batch * top_k->ne[0]); - // shared bitmap capacity in flash_attn_union.comp - const uint32_t max_words = 12288; - const uint32_t need_words = (uint32_t) (((k->ne[1] - n_kv_raw) + 31) / 32); - if (need_words > max_words) { - goto union_unavailable; // fall through to the per-token block form + if (union_env && union_env[0] == '1' && q->ne[3] == 1 && n_batch > 1 && bitmap_fits && + ctx->device->pipeline_flash_attn_union_f16 && ctx->device->pipeline_flash_attn_gather_union_f16 && + ggml_vk_fa_union_stat_init(ctx)) { + const uint32_t max_union = n_cand; + const uint32_t kv_c_est = ggml_vk_fa_union_estimate(ctx, (uint32_t) n_kv_raw, n_batch, n_cand); + // Two separate questions. Does the compact set fit under the gate at all, and does + // deduplicating actually shrink it: with no overlap to exploit the union is the same + // size as the per-token blocks and the scan is pure cost, measured at 1.2% of the op + // at 512k depth. The worst-case bound on the source keeps a collapse in overlap to + // roughly dense cost for the one step it takes the estimate to catch up. + const bool worth_it = kv_c_est != 0 && kv_c_est < kv_c && + (uint64_t) k->ne[1] >= 2ull * kv_c_est && + (uint64_t) k->ne[1] >= (uint64_t) kv_c; + + // GGML_VK_FA_UNION_STATS=1: what the gate actually decided and on what measurement. + // The alternative is inferring engagement from a timing, which is how a sparse path + // gets credited for a run it never took. + static const char * stats_env = getenv("GGML_VK_FA_UNION_STATS"); + if (stats_env && stats_env[0] == '1') { + static uint64_t calls = 0; + if ((calls++ % 256) == 0) { + fprintf(stderr, "[fa-union] n_kv=%lld n_kv_raw=%d n_batch=%u cand=%u " + "union/cand=%.3f kv_c %u -> est %u %s\n", + (long long) k->ne[1], n_kv_raw, n_batch, n_cand, (double) ctx->fa_union_est_ratio[n_batch], + kv_c, kv_c_est, worth_it ? "UNION" : "declined"); + } + } + + if (!worth_it) { + // Nothing is known about this batch shape, or the last count says compaction does + // not pay. Either way the count is what settles it, so produce one: the scan is a + // single workgroup and depth-independent, and count_only needs no index list. Dense + // runs this step; the next one decides on a measurement instead of a bound. + // + // Every declined step, not a sample: a decline is exactly the state in which the + // estimate stops being refreshed by the compact path, so sampling it leaves a stale + // estimate latched for as many steps as the sampling period. The dispatch is one + // workgroup against the ~2.2 ms dense op it is riding along with. + ctx->fa_union_declines++; + { + const vk_op_flash_attn_union_push_constants ppc = { + (uint32_t) k->ne[1], (uint32_t) n_kv_raw, n_batch, (uint32_t) top_k->ne[0], max_union, + (uint32_t) (top_k->nb[1] / sizeof(int32_t)), max_words, 256u, 1u, + }; + const vk_subbuffer stat_buf = ggml_vk_subbuffer(ctx, ctx->fa_union_stat); + ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_union_f16, 1); + // The probe writes the same slot a taken union path reads back as its row + // count, and a graph can contain both: the estimate is refreshed from the + // device as the graph is recorded, so an op late in the graph can be admitted + // after an earlier one was declined. Order it explicitly rather than rely on + // the compact path's own sync, which this path does not go through. + ggml_vk_sync_buffers(ctx, subctx); + ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_union_f16, + { ggml_vk_tensor_subbuffer(ctx, top_k), stat_buf, stat_buf }, ppc, { 1, 1, 1 }); + ggml_vk_fa_union_stat_host_barrier(subctx); + } + goto union_unavailable; } + const size_t ukc_sz = (size_t) kv_c * 512 * sizeof(ggml_fp16_t); const size_t umc_sz = (size_t) n_batch * kv_c * sizeof(ggml_fp16_t); const size_t ul_sz = (size_t) max_union * sizeof(uint32_t); - const size_t uc_sz = 2 * sizeof(uint32_t); - const size_t need = ukc_sz + umc_sz + ul_sz + uc_sz; + const size_t need = ukc_sz + umc_sz + ul_sz; if (ctx->prealloc_size_y < need) { ctx->prealloc_size_y = need; ggml_vk_preallocate_buffers(ctx, subctx); } - if (ctx->prealloc_y_need_sync) { - ggml_vk_sync_buffers(ctx, subctx); - } + // Unconditional, not gated on prealloc_y_need_sync: this also orders the count slot + // against a probe dispatched earlier in the same graph, which leaves that flag clear. + ggml_vk_sync_buffers(ctx, subctx); const vk_subbuffer kc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0); const vk_subbuffer mc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, ukc_sz); const vk_subbuffer ul_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, ukc_sz + umc_sz); - const vk_subbuffer uc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, ukc_sz + umc_sz + ul_sz); + // The count lives in the stat buffer rather than in prealloc_y so that the same write + // that the gather and the FA consume is also the one the host prices the next step from. + // Both are single-slot and rewritten by every layer, so the WAR hazard is unchanged: the + // prealloc_y sync above is a global barrier and orders the previous layer's read. + const vk_subbuffer uc_buf = ggml_vk_subbuffer(ctx, ctx->fa_union_stat); ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_union_f16, 1); ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_gather_union_f16, 1); const vk_op_flash_attn_union_push_constants upc = { (uint32_t) k->ne[1], (uint32_t) n_kv_raw, n_batch, (uint32_t) top_k->ne[0], max_union, - (uint32_t) (top_k->nb[1] / sizeof(int32_t)), max_words, 256u, + (uint32_t) (top_k->nb[1] / sizeof(int32_t)), max_words, 256u, 0u, }; ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_union_f16, { ggml_vk_tensor_subbuffer(ctx, top_k), ul_buf, uc_buf }, upc, { 1, 1, 1 }); @@ -11824,6 +11967,7 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ { ggml_vk_tensor_subbuffer(ctx, k), ul_buf, ggml_vk_tensor_subbuffer(ctx, mask), kc_buf, mc_buf, uc_buf }, gpc, { kv_c, 1, 1 }); ggml_vk_sync_buffers(ctx, subctx); + ggml_vk_fa_union_stat_host_barrier(subctx); ctx->prealloc_y_need_sync = true; st.active = true; @@ -11837,6 +11981,13 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ } union_unavailable:; + // Per-token blocks have no dedup, so this form really does cost kv_c: the gather writes + // then re-reads ~the active bytes while dense reads the source KV once, so compaction only + // pays when the source is comfortably larger than the active set. + if ((uint64_t) k->ne[1] < 2ull * kv_c) { + return false; + } + const uint32_t ns = (uint32_t) q->ne[3]; const size_t kc_sz = (size_t) ns * kv_c * 512 * sizeof(ggml_fp16_t); const size_t mc_sz = (size_t) ns * n_batch * kv_c * sizeof(ggml_fp16_t); @@ -17574,8 +17725,11 @@ static void ggml_vk_cleanup(ggml_backend_vk_context * ctx) { ggml_vk_destroy_buffer(ctx->prealloc_y); ggml_vk_destroy_buffer(ctx->prealloc_split_k); ggml_vk_destroy_buffer(ctx->prealloc_add_rms_partials); + ggml_vk_destroy_buffer(ctx->fa_union_stat); ggml_vk_destroy_buffer(ctx->sync_staging); + memset(ctx->fa_union_est_ratio, 0, sizeof(ctx->fa_union_est_ratio)); + ctx->prealloc_y_last_pipeline_used = nullptr; ctx->prealloc_y_last_tensor_used = nullptr; ctx->prealloc_y_last_k_padded = false; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_union.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_union.comp index cd779e0a0f99..513375709300 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_union.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_union.comp @@ -19,6 +19,15 @@ // (a multiple of every FA block width) is what lets flash-attention keep its "aligned" // pipeline variant even though the row count is now a runtime value. // +// Also emits the raw union size, the candidate count and the batch it came from. The host +// reads those back to decide whether compaction is worth it at all, and files the sample under +// the batch reported here rather than the one it is pricing: it cannot tell which call last +// wrote the slot, and the overlap it is measuring depends on the batch. +// +// count_only skips the list writes. The scan still has to run to produce the count, but with +// nothing to write the index buffer need not exist, which is what lets the host price the +// union on a call it is not going to compact. +// // One workgroup: the bitmap and the running offset both live in shared memory. layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; @@ -36,6 +45,7 @@ layout(push_constant) uniform Parameters { uint nbt1; uint max_words; // capacity of the shared bitmap, host-checked uint pad_to; + uint count_only; // 1: produce the count, write no index list } p; // 12288 words = 393216 compressed rows, ~48 KiB of shared memory. @@ -97,7 +107,7 @@ void main() { } uint slot = base_sh + prefix + wave_off; - uint rem = bits; + uint rem = p.count_only != 0 ? 0u : bits; while (rem != 0) { const uint b = findLSB(rem); rem &= rem - 1; @@ -118,5 +128,7 @@ void main() { const uint rows = p.n_kv_raw + u; data_c[0] = ((rows + p.pad_to - 1) / p.pad_to) * p.pad_to; data_c[1] = u; + data_c[2] = n_cand; + data_c[3] = p.n_batch; } } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index c6c4796c26cd..6ab027c7ea18 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11420,6 +11420,12 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 8, 1024, 512, true)); test_cases.emplace_back(new test_flash_attn_ext_top_k(32768, 16, 2304, 512, false)); test_cases.emplace_back(new test_flash_attn_ext_top_k(65536, 63, 2304, 512, false)); + // overlapping selections at the shapes where the compaction gate is tightest: with a + // deduplicated union these are admitted on the estimated union size rather than the + // worst case, so they cover the estimator's gate as well as the union itself. + test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 8, 2304, 512, false, 1, 60)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 16, 2304, 512, false, 1, 60)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 16, 2304, 512, false, 1, 86)); test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 4, 1024, 512, false, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, true, 2)); @@ -11948,11 +11954,22 @@ static std::vector> make_test_cases_perf() { // Same shapes with realistic adjacent-token overlap. Measured on DeepSeek-V4-Flash the // real overlap is 60% over 4 adjacent tokens and 76% over 8; the default generator is // near 0%, which would make a deduplicated union look worthless by construction. - for (int kv : { 35584, 133888 }) { - for (int nb : { 2, 4, 8 }) { + // kv=11008 (~32k source) and nb=16 are where the compaction gate is tightest: the + // worst-case compact set 2304 + nb*512 crosses kv/2 at nb=6, so those cells measure + // whether the gate can be opened by the union rather than by the worst case. + for (int kv : { 11008, 35584, 133888 }) { + for (int nb : { 2, 4, 8, 16 }) { test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, nb, 2304, 512, false, 1, 60)); } } + // ov is a per-token share, not the union/selected ratio the model was measured by: at nb + // tokens it gives a union of (ov + (1-ov)*nb)/nb of the selections, so ov=60 is 0.475 at + // nb=8 where the model measured 0.243. ov=86 is the setting that reproduces the model, and + // at kv=11008 it is the difference between a union that fits under the gate and one that + // does not. + for (int nb : { 8, 16 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, nb, 2304, 512, false, 1, 86)); + } return test_cases; } From 857f81cf66bb0c8240a3ec52ae8ba3d263a38726 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Fri, 14 Aug 2026 02:27:30 +0000 Subject: [PATCH 119/212] vulkan: measure the V4 union on real draft tokens, and correct the fixture calibration The previous commit calibrated its fixture against a PROXY, because a proxy was all that was reachable at the time: adjacent PREFILL tokens standing in for draft tokens. With the draft now drivable, the proxy turns out to have been optimistic, and the correction runs against that commit's own message. DeepSeek-V4-Flash UD-IQ3_XXS with the DSpark draft, Vulkan attention and CPU experts, 25538 tokens of context. The gate reports union/candidate 0.560 and 0.609 on the target layers and 0.484 to 0.651 on a second shape (n_kv_raw 256, most consistent with the draft's own sparse attention, also GPU-resident). Mean about 0.56 at batch 4, against 0.397 from the proxy at a window of 4. Draft tokens diverge from each other more than adjacent prefill tokens do, which is what a draft exploring rather than committed text should look like. The fixture's ov maps to (ov + (1-ov)*nb)/nb, so ov=60 predicts 0.55 at batch 4 against 0.56 measured: ov=60 reproduces real drafting almost exactly. The previous commit says ov=86 is the setting that reproduces the model and ov=60 is deliberately pessimistic. That is backwards for real draft tokens, and the realistic headline at batch 8 is 2.47x rather than 3.14x. The kernel numbers in that table are unchanged and still correct for the overlap each column states; only which column describes reality has moved. Batch 8 overlap itself is still not measured on real drafts, for the reason below, so 2.47x is an extrapolation from batch 4. The path does engage: 56 of 57 sampled gate decisions took the union, and the single decline is the unseeded first call, which then probes. That is the designed bootstrap, observed on the real model rather than a fixture. Scope, stated plainly because the previous commit implies more: DSpark drafts 3 tokens, so the batch is 4 and --spec-draft-n-max does not raise it, an MTP head having a fixed width. At batch 4 and this depth kv_c is 4352 against n_kv 8704, so the OLD worst-case gate admits too and the gate change makes no difference there; the smaller compact set comes from the union commit. For this target and draft the gate change buys a lower depth threshold instead: roughly 21.0k tokens rather than 25.5k on the target layers, and 11.8k rather than 17.7k on the second shape. The 2.47x and 2.65x cells need batch 8 or 16, which this pair does not generate. GGML_VK_FA_UNION_STATS takes a period now rather than being fixed at 256, and reports running totals. The fixed period is why this took two runs: a 64-token speculative decode over 25k of context emitted exactly ONE line, which reported only that the first call was unseeded. A diagnostic whose sampling rate can hide the thing it exists to measure is not a diagnostic. Measurements and provenance: ~/strix-results/derived-dsv4-union-gate-20260814.md section 9. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 182f51741cdb..07912c4cf7ab 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11881,17 +11881,25 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ (uint64_t) k->ne[1] >= 2ull * kv_c_est && (uint64_t) k->ne[1] >= (uint64_t) kv_c; - // GGML_VK_FA_UNION_STATS=1: what the gate actually decided and on what measurement. - // The alternative is inferring engagement from a timing, which is how a sparse path - // gets credited for a run it never took. + // GGML_VK_FA_UNION_STATS=N: report every Nth call (N=1 means every call) what the gate + // decided and on what measurement. The alternative is inferring engagement from a + // timing, which is how a sparse path gets credited for a run it never took. + // + // The period is a parameter because a fixed one is a way to miss the answer: a 64-token + // speculative decode over 25k of context produced ONE line at a period of 256, which + // said only that the first call was unseeded. Running totals rather than instants, so a + // single late line still reports whether the path engaged. static const char * stats_env = getenv("GGML_VK_FA_UNION_STATS"); - if (stats_env && stats_env[0] == '1') { - static uint64_t calls = 0; - if ((calls++ % 256) == 0) { + if (stats_env && stats_env[0] != '\0' && stats_env[0] != '0') { + static uint64_t calls = 0, taken = 0; + const uint64_t period = std::max(1ull, (unsigned long long) atoll(stats_env)); + taken += worth_it ? 1 : 0; + if ((calls++ % period) == 0) { fprintf(stderr, "[fa-union] n_kv=%lld n_kv_raw=%d n_batch=%u cand=%u " - "union/cand=%.3f kv_c %u -> est %u %s\n", + "union/cand=%.3f kv_c %u -> est %u %s (%llu/%llu taken)\n", (long long) k->ne[1], n_kv_raw, n_batch, n_cand, (double) ctx->fa_union_est_ratio[n_batch], - kv_c, kv_c_est, worth_it ? "UNION" : "declined"); + kv_c, kv_c_est, worth_it ? "UNION" : "declined", + (unsigned long long) taken, (unsigned long long) calls); } } From fa825766d42c596f8f622d8a256dbcf463846c92 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Fri, 14 Aug 2026 03:11:13 +0000 Subject: [PATCH 120/212] vulkan: default the DeepSeek V4 small-batch union on Every other gate in this family is on by default with =0 to disable: GGML_VK_FA_TOPK, GGML_VK_FA_TOPK_CM, GGML_VK_FA_TOPK_SPLIT. GGML_VK_FA_TOPK_UNION=1 was the odd one out because it began as a prototype, and it is no longer one. What makes it safe to default is that the path declines itself rather than needing a user to know when to avoid it. Where the selections do not deduplicate, the estimate says so and the per-token form runs instead; where the compact set would not fit under the gate, dense runs. Both were measured, at 1.00x, rather than assumed. Kept as one commit of its own rather than folded into the gate change, because it is a policy change and not a mechanism one: reverting these four lines restores opt-in behaviour without touching the gate logic, which is what a bisect would want if a regression turns up. 13310 FLASH_ATTN_EXT cases pass with the variable UNSET, which is the configuration this changes and the one that ships. That is the same code path as the previously validated GGML_VK_FA_TOPK_UNION=1, but it was run again rather than argued from equivalence. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 07912c4cf7ab..f72612c91f91 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11850,10 +11850,10 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ // deduplicate to; kv_c remains the bound for every allocation and dispatch count. const uint32_t kv_c = GGML_PAD((uint32_t) (n_kv_raw + (int64_t) n_cand), 256u); - // ---- deduplicated union (GGML_VK_FA_TOPK_UNION=1) ---------------------------------- + // ---- deduplicated union (default on, GGML_VK_FA_TOPK_UNION=0 disables) -------------- // Same compact layout, but one row per DISTINCT selected key instead of one block per - // token. Measured adjacent-token overlap on the real model is 60% at 4 tokens and 76% at - // 8, so the union is materially smaller. Its size is only known on the GPU, so the FA + // token. Measured on real draft tokens the union is 0.56 of the selections at batch 4, + // so it is materially smaller. Its size is only known on the GPU, so the FA // reads its KV bound from a buffer (the DYNAMIC_KV pipeline flag) rather than a push // constant; padding the count to 256 keeps KV % Bc == 0 so the aligned variant still // applies. Single stream only: the FA takes one KV for all streams. @@ -11867,7 +11867,7 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ const bool bitmap_fits = (uint64_t) ((k->ne[1] - n_kv_raw) + 31) / 32 <= max_words; static const char * union_env = getenv("GGML_VK_FA_TOPK_UNION"); - if (union_env && union_env[0] == '1' && q->ne[3] == 1 && n_batch > 1 && bitmap_fits && + if ((!union_env || union_env[0] != '0') && q->ne[3] == 1 && n_batch > 1 && bitmap_fits && ctx->device->pipeline_flash_attn_union_f16 && ctx->device->pipeline_flash_attn_gather_union_f16 && ggml_vk_fa_union_stat_init(ctx)) { const uint32_t max_union = n_cand; From 1481bbeff75160788bb05b4629dfd3c10979638e Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Fri, 14 Aug 2026 06:27:04 +0000 Subject: [PATCH 121/212] vulkan: let the DeepSeek V4 small-batch gather serve quantised K/V The gather rejected anything but f16 K/V, so a DSv4 run with -ctk q8_0 -ctv q8_0 took the dense fallback for every batch 2..63 decode - the whole small-batch path, silently off. Two community test sets were run that way before anyone noticed, which is what prompted this. The restriction was incidental rather than essential. The gather RELOCATES rows; it never reads a value out of one. Addressing K as raw 4-byte words instead of f16 elements makes the same shader serve any type whose row is a whole number of words, which every block-quantised type here satisfies at head size 512. The union shader needed no change at all - it only ever touched top_k indices. Zeroing an unused row now writes zero BYTES. That decodes to zero for the block-quantised types as well as for f16, because a zero scale zeroes the block; those rows are -inf in the mask regardless, so zeroing only keeps a garbage dot product out of the softmax as a NaN. The type gate asks ggml_vk_fa_kv_native rather than just "is it quantised". While the compact scratch is active the dequant/contiguize pass is disabled by construction, and the assert guarding that combination aborts rather than falling back, so admitting a type flash-attention has no native shader for would crash instead of degrade. test-backend-ops perf, kv=11008 n_kv_raw=2304 n_top_k=512 ov=60, gather off vs on: batch f16 dense -> gathered q8_0 dense -> gathered 2 2192 -> 645 us 3.40x 3917 -> 1110 us 3.53x 4 2203 -> 726 us 3.03x 3924 -> 1257 us 3.12x 8 2213 -> 910 us 2.43x 3936 -> 1567 us 2.51x 16 2219 -> 2232 us declined 3930 -> 3952 us declined Two things worth stating because they contradict what I expected going in. Compaction does NOT pay more on quantised K/V. The reasoning was that a gathered set is contiguous and would dodge the strided-read and channel-aliasing taxes that hurt quantised reads more; the measured ratios differ by 3 to 4%, inside noise. It pays the same proportion from a worse starting point. That worse starting point is the real result: q8_0 attention is about 1.78x SLOWER than f16 here, dense (3917 vs 2192) and gathered (1110 vs 645) alike. Halving the bytes does not pay for the dequant work in the inner loop at this head size. So this change makes q8_0 K/V survivable for DSv4 rather than advisable - f16 is still the faster cache by a wide margin, and that inverts the usual q8_0-KV-buys-speed rule of thumb for this model. 13318 FLASH_ATTN_EXT cases pass, including 8 new q8_0/q4_0 top-k cases at the shapes a DSv4 decode actually hits. test_flash_attn_ext_top_k takes a type_K parameter for them. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 55 ++++++++++++++----- .../vulkan-shaders/flash_attn_gather.comp | 25 +++++---- .../flash_attn_gather_union.comp | 25 ++++++--- tests/test-backend-ops.cpp | 24 ++++++-- 4 files changed, 91 insertions(+), 38 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index f72612c91f91..de2cb0745425 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1989,12 +1989,14 @@ static_assert(sizeof(vk_op_dsv4_hc_post_push_constants) <= 128); struct vk_op_flash_attn_union_push_constants { uint32_t n_kv, n_kv_raw, n_batch, n_top_k, max_union, nbt1, max_words, pad_to, count_only; }; +// nbk1/nbk3 are in 4-byte WORDS, not elements: the gather relocates K rows verbatim and never +// interprets what is in them, so it works for any type whose row is a whole number of words. struct vk_op_flash_attn_gather_union_push_constants { - uint32_t n_kv, n_kv_raw, kv_c_max, nbk1, nbm1, n_batch; + uint32_t n_kv, n_kv_raw, kv_c_max, nbk1, nbm1, n_batch, row_words; }; struct vk_op_flash_attn_gather_push_constants { uint32_t n_kv, n_kv_raw, n_top_k, kv_c; - uint32_t nbk1, nbk3, nbt1, nbt3, nbm1, nbm3, nem3, n_batch; + uint32_t nbk1, nbk3, nbt1, nbt3, nbm1, nbm3, nem3, n_batch, row_words; }; static_assert(sizeof(vk_op_flash_attn_gather_push_constants) <= 128); @@ -11721,6 +11723,8 @@ struct vk_fa_compact_state { bool dynamic_kv = false; // KV row count lives in kv_buf, not the push constant uint32_t kv_c = 0; // upper bound; the real count is runtime when dynamic_kv uint32_t n_batch = 1; + uint32_t row_bytes = 0; // bytes per compact K row; K may be quantised + uint32_t row_elems = 0; // K row stride in ELEMENTS/blocks, for the FA push constant vk_subbuffer kv_buf; vk_subbuffer kc_buf, mc_buf; }; @@ -11811,10 +11815,22 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ const ggml_tensor * mask, ggml_tensor * dst, vk_fa_compact_state & st) { const ggml_tensor * top_k = dst->src[5]; static const char * gather_env = getenv("GGML_VK_FA_TOPK_GATHER"); + // K/V type: the gather relocates rows verbatim and never reads a value out of them, so it + // does not have to be f16 - it has to be a type whose row is a whole number of 4-byte words, + // and one flash-attention has a NATIVE shader for. The dequant/contiguize pass is disabled + // while this scratch is active and asserts if it turns out to be needed, so admitting a + // non-native type here would abort rather than fall back. + const bool kv_word_addressable = + k->type == v->type && + ggml_vk_fa_kv_native(k->type, ctx->device->coopmat2) && + k->ne[0] % ggml_blck_size(k->type) == 0 && + ggml_row_size(k->type, k->ne[0]) % 4 == 0 && + k->nb[1] % 4 == 0 && k->nb[3] % 4 == 0; + if ((gather_env && gather_env[0] == '0') || !top_k || !ctx->device->pipeline_flash_attn_gather_f16 || q->ne[1] < 1 || q->ne[1] >= 64 || // 1..63: >=64 goes to the sparse prefill path - q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || v->type != GGML_TYPE_F16 || + q->type != GGML_TYPE_F32 || !kv_word_addressable || !mask || mask->type != GGML_TYPE_F16 || top_k->type != GGML_TYPE_I32 || q->ne[0] != 512 || k->ne[0] != 512 || v->ne[0] != 512 || q->ne[2] != 64 || k->ne[2] != 1 || v->ne[2] != 1 || @@ -11824,6 +11840,9 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ return false; } + const uint32_t k_row_bytes = (uint32_t) ggml_row_size(k->type, k->ne[0]); + const uint32_t k_row_words = k_row_bytes / 4; + float max_bias = 0.0f; float logit_softcap = 0.0f; memcpy(&max_bias, (const float *) dst->op_params + 1, sizeof(float)); @@ -11934,7 +11953,7 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ goto union_unavailable; } - const size_t ukc_sz = (size_t) kv_c * 512 * sizeof(ggml_fp16_t); + const size_t ukc_sz = (size_t) kv_c * k_row_bytes; const size_t umc_sz = (size_t) n_batch * kv_c * sizeof(ggml_fp16_t); const size_t ul_sz = (size_t) max_union * sizeof(uint32_t); const size_t need = ukc_sz + umc_sz + ul_sz; @@ -11967,9 +11986,9 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ const vk_op_flash_attn_gather_union_push_constants gpc = { (uint32_t) k->ne[1], (uint32_t) n_kv_raw, kv_c, - (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)), + (uint32_t) (k->nb[1] / 4), (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)), - n_batch, + n_batch, k_row_words, }; ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_gather_union_f16, { ggml_vk_tensor_subbuffer(ctx, k), ul_buf, ggml_vk_tensor_subbuffer(ctx, mask), @@ -11982,6 +12001,8 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ st.dynamic_kv = true; st.kv_c = kv_c; st.n_batch = n_batch; + st.row_bytes = k_row_bytes; + st.row_elems = (uint32_t) (k->ne[0] / ggml_blck_size(k->type)); st.kc_buf = kc_buf; st.mc_buf = mc_buf; st.kv_buf = uc_buf; @@ -11997,7 +12018,7 @@ union_unavailable:; } const uint32_t ns = (uint32_t) q->ne[3]; - const size_t kc_sz = (size_t) ns * kv_c * 512 * sizeof(ggml_fp16_t); + const size_t kc_sz = (size_t) ns * kv_c * k_row_bytes; const size_t mc_sz = (size_t) ns * n_batch * kv_c * sizeof(ggml_fp16_t); if (ctx->prealloc_size_y < kc_sz + mc_sz) { @@ -12013,14 +12034,14 @@ union_unavailable:; const vk_op_flash_attn_gather_push_constants pc = { (uint32_t) k->ne[1], (uint32_t) n_kv_raw, (uint32_t) top_k->ne[0], kv_c, - (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)), - (uint32_t) (k->nb[3] / sizeof(ggml_fp16_t)), + (uint32_t) (k->nb[1] / 4), + (uint32_t) (k->nb[3] / 4), (uint32_t) (top_k->nb[1] / sizeof(int32_t)), (uint32_t) (top_k->nb[3] / sizeof(int32_t)), (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)), (uint32_t) (mask->nb[3] / sizeof(ggml_fp16_t)), (uint32_t) mask->ne[3], - n_batch, + n_batch, k_row_words, }; st.kc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0); @@ -12082,6 +12103,8 @@ union_unavailable:; st.active = true; st.kv_c = kv_c; st.n_batch = n_batch; + st.row_bytes = k_row_bytes; + st.row_elems = (uint32_t) (k->ne[0] / ggml_blck_size(k->type)); return true; } @@ -12241,8 +12264,10 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx uint32_t k_stride = (uint32_t)(nbk1 / ggml_type_size(k->type)); uint32_t v_stride = (uint32_t)(nbv1 / ggml_type_size(v->type)); if (fa_compact.active) { - k_stride = 512; - v_stride = 512; + // rows are tightly packed in the compact scratch; for a quantised K this is the block + // count per row, which is what nbk1 / ggml_type_size would have given for the source + k_stride = fa_compact.row_elems; + v_stride = fa_compact.row_elems; } // For F32, the shader treats it as a block of size 4 (for vec4 loads) @@ -12455,9 +12480,9 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx ggml_vk_sync_buffers(ctx, subctx); } - // compact scratch layout: [512, kv_c, 1, ns] f16, tightly packed - const uint32_t eff_nbk2 = fa_compact.active ? fa_compact.kv_c * 512 * (uint32_t)sizeof(ggml_fp16_t) : nbk2_eff; - const uint32_t eff_nbk3 = fa_compact.active ? fa_compact.kv_c * 512 * (uint32_t)sizeof(ggml_fp16_t) : nbk3_eff; + // compact scratch layout: [512, kv_c, 1, ns] tightly packed, in K's own type + const uint32_t eff_nbk2 = fa_compact.active ? fa_compact.kv_c * fa_compact.row_bytes : nbk2_eff; + const uint32_t eff_nbk3 = fa_compact.active ? fa_compact.kv_c * fa_compact.row_bytes : nbk3_eff; const uint32_t eff_nbv2 = fa_compact.active ? eff_nbk2 : nbv2_eff; const uint32_t eff_nbv3 = fa_compact.active ? eff_nbk3 : nbv3_eff; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp index 3c9ac68e2c69..6dd24d31b517 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp @@ -19,10 +19,12 @@ layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; -layout(binding = 0) readonly buffer KBuf { float16_t data_k[]; }; +// K is addressed as raw 4-byte WORDS, never as values: a gather relocates rows and does not +// need to know whether they hold f16 elements or quantised blocks. Only the MASK is typed. +layout(binding = 0) readonly buffer KBuf { uint data_k[]; }; layout(binding = 1) readonly buffer TopBuf { int data_top[]; }; layout(binding = 2) readonly buffer MBuf { float16_t data_m[]; }; -layout(binding = 3) writeonly buffer KcBuf { float16_t data_kc[]; }; +layout(binding = 3) writeonly buffer KcBuf { uint data_kc[]; }; layout(binding = 4) writeonly buffer McBuf { float16_t data_mc[]; }; layout(push_constant) uniform Parameters { @@ -30,17 +32,17 @@ layout(push_constant) uniform Parameters { uint n_kv_raw; // dense prefix length uint n_top_k; // selected rows for the (single) query token uint kv_c; // padded compact row count == dispatch row range - uint nbk1; // K source row stride, elements - uint nbk3; // K source stream stride, elements + uint nbk1; // K source row stride, WORDS + uint nbk3; // K source stream stride, WORDS uint nbt1; // top_k row (per query token) stride, elements uint nbt3; // top_k stream stride, elements uint nbm1; // mask source row (per query token) stride, elements uint nbm3; // mask source stream stride, elements uint nem3; // mask ne[3], for stream broadcast uint n_batch; // query tokens sharing this gather; <= LANES + uint row_words; // bytes per K row / 4 } p; -const uint HEAD_SIZE = 512; const uint LANES = 64; void main() { @@ -68,15 +70,18 @@ void main() { } } - const uint dst_base = (stream * p.kv_c + row) * HEAD_SIZE; + // Zeroing writes zero BYTES, which decode to zero for every block-quantised type here (a + // zero scale zeroes the block) as well as for f16. The row is -inf in the mask either way; + // zeroing only keeps a garbage dot product from reaching the softmax as a NaN. + const uint dst_base = (stream * p.kv_c + row) * p.row_words; if (src < p.n_kv) { const uint src_base = stream * p.nbk3 + src * p.nbk1; - [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { - data_kc[dst_base + tid + i * LANES] = data_k[src_base + tid + i * LANES]; + for (uint i = tid; i < p.row_words; i += LANES) { + data_kc[dst_base + i] = data_k[src_base + i]; } } else { - [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { - data_kc[dst_base + tid + i * LANES] = float16_t(0.0); + for (uint i = tid; i < p.row_words; i += LANES) { + data_kc[dst_base + i] = 0u; } } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp index 775b1bc96c62..4669e0d1c75e 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp @@ -18,10 +18,14 @@ layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; -layout(binding = 0) readonly buffer KBuf { float16_t data_k[]; }; +// K is addressed as raw 4-byte WORDS, never as values. A gather relocates rows; it does not +// need to know whether they hold f16 elements or quantised blocks, so the same shader serves +// every K type whose row is a whole number of words. Only the MASK is typed, and a mask is +// always f16. +layout(binding = 0) readonly buffer KBuf { uint data_k[]; }; layout(binding = 1) readonly buffer UBuf { uint data_u[]; }; layout(binding = 2) readonly buffer MBuf { float16_t data_m[]; }; -layout(binding = 3) writeonly buffer KcBuf { float16_t data_kc[]; }; +layout(binding = 3) writeonly buffer KcBuf { uint data_kc[]; }; layout(binding = 4) writeonly buffer McBuf { float16_t data_mc[]; }; layout(binding = 5) readonly buffer CBuf { uint data_c[]; }; @@ -29,12 +33,12 @@ layout(push_constant) uniform Parameters { uint n_kv; uint n_kv_raw; uint kv_c_max; - uint nbk1; + uint nbk1; // K source row stride, WORDS uint nbm1; uint n_batch; + uint row_words; // bytes per K row / 4 } p; -const uint HEAD_SIZE = 512; const uint LANES = 64; void main() { @@ -55,15 +59,18 @@ void main() { src = p.n_kv_raw + data_u[row - p.n_kv_raw]; } - const uint dst_base = row * HEAD_SIZE; + // Zeroing an unused row writes zero BYTES, which decode to zero for every block-quantised + // type here (a zero scale zeroes the block) as well as for f16. The row is -inf in the mask + // either way; zeroing only keeps a garbage dot product from reaching the softmax as a NaN. + const uint dst_base = row * p.row_words; if (src < p.n_kv) { const uint src_base = src * p.nbk1; - [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { - data_kc[dst_base + tid + i * LANES] = data_k[src_base + tid + i * LANES]; + for (uint i = tid; i < p.row_words; i += LANES) { + data_kc[dst_base + i] = data_k[src_base + i]; } } else { - [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) { - data_kc[dst_base + tid + i * LANES] = float16_t(0.0); + for (uint i = tid; i < p.row_words; i += LANES) { + data_kc[dst_base + i] = 0u; } } diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 6ab027c7ea18..749369be2abd 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -8040,12 +8040,13 @@ struct test_flash_attn_ext_top_k : public test_case { const bool sinks; const int64_t ns; // sequences (ne3); >1 exercises the split-K stream stride const int64_t ov; // % of each token's picks shared with its neighbours (dedup-union realism) + const ggml_type type_K; // K/V cache type; V is the same tensor, so one type covers both static constexpr int64_t hs = 512; // V4 CSA head size, K == V latent static constexpr int64_t nh = 64; // V4 CSA query heads (MQA) std::string vars() override { - return VARS_TO_STR7(kv, nb, n_kv_raw, n_top_k, sinks, ns, ov); + return VARS_TO_STR8(kv, nb, n_kv_raw, n_top_k, sinks, ns, ov, type_K); } double max_nmse_err() override { @@ -8059,14 +8060,15 @@ struct test_flash_attn_ext_top_k : public test_case { return 2 * nh * nb * ns * (hs + hs) * (n_kv_raw + n_top_k); } - test_flash_attn_ext_top_k(int64_t kv = 768, int64_t nb = 8, int64_t n_kv_raw = 64, int64_t n_top_k = 128, bool sinks = false, int64_t ns = 1, int64_t ov = 0) - : kv(kv), nb(nb), n_kv_raw(n_kv_raw), n_top_k(n_top_k), sinks(sinks), ns(ns), ov(ov) {} + test_flash_attn_ext_top_k(int64_t kv = 768, int64_t nb = 8, int64_t n_kv_raw = 64, int64_t n_top_k = 128, bool sinks = false, int64_t ns = 1, int64_t ov = 0, + ggml_type type_K = GGML_TYPE_F16) + : kv(kv), nb(nb), n_kv_raw(n_kv_raw), n_top_k(n_top_k), sinks(sinks), ns(ns), ov(ov), type_K(type_K) {} ggml_tensor * build_graph(ggml_context * ctx) override { ggml_tensor * q = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, hs, nb, nh, ns); ggml_set_name(q, "q"); - ggml_tensor * k = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, hs, kv, 1, ns); + ggml_tensor * k = ggml_new_tensor_4d(ctx, type_K, hs, kv, 1, ns); ggml_set_name(k, "k"); // V4 CSA attends over the K latent itself: V is the same cache tensor @@ -11426,6 +11428,15 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 8, 2304, 512, false, 1, 60)); test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 16, 2304, 512, false, 1, 60)); test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 16, 2304, 512, false, 1, 86)); + // quantised K/V: the gather relocates rows verbatim, so it should serve any type whose row + // is a whole number of 4-byte words. These are the shapes a DSv4 decode with -ctk q8_0 hits, + // which took the dense fallback entirely before the gather learned to address rows as bytes. + for (ggml_type tk : { GGML_TYPE_Q8_0, GGML_TYPE_Q4_0 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 4, 1024, 512, false, 1, 0, tk)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 8, 2304, 512, false, 1, 60, tk)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 16, 2304, 512, false, 1, 86, tk)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 1, 1024, 512, false, 1, 0, tk)); + } test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 4, 1024, 512, false, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, true, 2)); @@ -11970,6 +11981,11 @@ static std::vector> make_test_cases_perf() { for (int nb : { 8, 16 }) { test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, nb, 2304, 512, false, 1, 86)); } + // q8_0 K/V at the same shapes: DSv4 with -ctk q8_0 took the dense fallback before the + // gather became type-agnostic, so this is the cell that says whether it now pays there. + for (int nb : { 2, 4, 8, 16 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, nb, 2304, 512, false, 1, 60, GGML_TYPE_Q8_0)); + } return test_cases; } From 35b75782ffe19c8c01eea1755299c664da5b37e2 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Fri, 14 Aug 2026 06:52:40 +0000 Subject: [PATCH 122/212] vulkan: dequantise q8_0 K/V inside the DeepSeek V4 small-batch gather Relocating quantised rows verbatim leaves flash attention to decode them in its inner loop, which it does once per query block that reads a row rather than once per row. Measured, that costs 0.15 us per KV row attended - 0.1481 / 0.1510 / 0.1512 us/row at batch 8 / 4 / 2 and 0.1565 dense, so a constant across a 3.6x span of row counts and across both regimes. It is what made q8_0 attention 1.78x slower than f16 while reading half the bytes. The gather is the natural place to pay it instead: it already touches every selected row exactly once. Decoding there makes the compact scratch f16, flash attention takes its f16 path, and the redundancy disappears. No extra pass - the pass already existed. test-backend-ops perf, kv=11008 n_kv_raw=2304 n_top_k=512 ov=60, q8_0 K/V: batch dense verbatim gather decoded gather f16 gather 2 3917 us 1110 us 630 us 640 us 4 3924 us 1257 us 715 us 727 us 8 3936 us 1567 us 891 us 908 us q8_0 now edges f16 by 1.5 to 1.8% rather than trailing it by 78%: the FA does identical f16 work either way, and the gather's scattered read side moves half the bytes. Against the dense fallback a q8_0 cache took before any of this work, batch 8 is 4.41x. Flash attention picks its pipeline from what the SCRATCH holds, not from the source tensor, so k_type_eff/v_type_eff now also key off the compact state - the same mechanism use_dequant_kv already used, which is why that variable existed in this form. q8_0 only, deliberately. It is the type both community test sets used and the one worth having; every other quantised type keeps the verbatim path, which is correct for them and validated. Generalising means one shader variant per type through dequant_funcs.glsl, which is only worth the shader-permutation cost if this shape of win reproduces elsewhere. Not covered: the per-token gather still relocates verbatim, so a batch where the union declines but the per-token form fits keeps paying the inline decode. Same fix applies, and the same measurement will say whether it is worth a second shader. 13318 FLASH_ATTN_EXT cases pass. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 33 +++++-- .../flash_attn_gather_union_dq.comp | 99 +++++++++++++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 1 + 3 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union_dq.comp diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index de2cb0745425..b599012284ef 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1114,6 +1114,7 @@ struct vk_device_struct { vk_pipeline pipeline_flash_attn_gather_f16; vk_pipeline pipeline_flash_attn_union_f16; vk_pipeline pipeline_flash_attn_gather_union_f16; + vk_pipeline pipeline_flash_attn_gather_union_dq_q8_0; vk_pipeline pipeline_dsv4_hc_pre_f32; vk_pipeline pipeline_dsv4_hc_comb_f32; vk_pipeline pipeline_dsv4_hc_post_f32; @@ -6345,6 +6346,10 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { "flash_attn_gather_union_f16", flash_attn_gather_union_f16_len, flash_attn_gather_union_f16_data, "main", 6, sizeof(vk_op_flash_attn_gather_union_push_constants), {1, 1, 1}, {}, 1, true, true, device->subgroup_size); + ggml_vk_create_pipeline(device, device->pipeline_flash_attn_gather_union_dq_q8_0, + "flash_attn_gather_union_dq_q8_0", flash_attn_gather_union_dq_q8_0_len, flash_attn_gather_union_dq_q8_0_data, "main", 6, + sizeof(vk_op_flash_attn_gather_union_push_constants), {1, 1, 1}, {}, 1, true, true, + device->subgroup_size); } } @@ -11725,6 +11730,7 @@ struct vk_fa_compact_state { uint32_t n_batch = 1; uint32_t row_bytes = 0; // bytes per compact K row; K may be quantised uint32_t row_elems = 0; // K row stride in ELEMENTS/blocks, for the FA push constant + bool dequantized = false; // scratch holds f16 because the gather decoded on the way in vk_subbuffer kv_buf; vk_subbuffer kc_buf, mc_buf; }; @@ -11953,7 +11959,12 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ goto union_unavailable; } - const size_t ukc_sz = (size_t) kv_c * k_row_bytes; + // Decoding on the way in makes the scratch f16 and hands flash attention its f16 path, + // which is worth far more than the extra scratch bytes: the inline decode it replaces + // costs a measured 0.15 us per KV row attended, every step. + const bool dq = k->type == GGML_TYPE_Q8_0 && ctx->device->pipeline_flash_attn_gather_union_dq_q8_0; + const uint32_t u_row_by = dq ? (uint32_t) (k->ne[0] * sizeof(ggml_fp16_t)) : k_row_bytes; + const size_t ukc_sz = (size_t) kv_c * u_row_by; const size_t umc_sz = (size_t) n_batch * kv_c * sizeof(ggml_fp16_t); const size_t ul_sz = (size_t) max_union * sizeof(uint32_t); const size_t need = ukc_sz + umc_sz + ul_sz; @@ -11973,8 +11984,10 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ // prealloc_y sync above is a global barrier and orders the previous layer's read. const vk_subbuffer uc_buf = ggml_vk_subbuffer(ctx, ctx->fa_union_stat); + vk_pipeline gather_pipe = dq ? ctx->device->pipeline_flash_attn_gather_union_dq_q8_0 + : ctx->device->pipeline_flash_attn_gather_union_f16; ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_union_f16, 1); - ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_gather_union_f16, 1); + ggml_pipeline_request_descriptor_sets(ctx, gather_pipe, 1); const vk_op_flash_attn_union_push_constants upc = { (uint32_t) k->ne[1], (uint32_t) n_kv_raw, n_batch, (uint32_t) top_k->ne[0], max_union, @@ -11984,13 +11997,14 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ { ggml_vk_tensor_subbuffer(ctx, top_k), ul_buf, uc_buf }, upc, { 1, 1, 1 }); ggml_vk_sync_buffers(ctx, subctx); + // the fused decoder steps K in BLOCKS and writes elements; the verbatim one does words const vk_op_flash_attn_gather_union_push_constants gpc = { (uint32_t) k->ne[1], (uint32_t) n_kv_raw, kv_c, - (uint32_t) (k->nb[1] / 4), + dq ? (uint32_t) (k->nb[1] / ggml_type_size(k->type)) : (uint32_t) (k->nb[1] / 4), (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)), - n_batch, k_row_words, + n_batch, dq ? (uint32_t) k->ne[0] : k_row_words, }; - ggml_vk_dispatch_pipeline(ctx, subctx, ctx->device->pipeline_flash_attn_gather_union_f16, + ggml_vk_dispatch_pipeline(ctx, subctx, gather_pipe, { ggml_vk_tensor_subbuffer(ctx, k), ul_buf, ggml_vk_tensor_subbuffer(ctx, mask), kc_buf, mc_buf, uc_buf }, gpc, { kv_c, 1, 1 }); ggml_vk_sync_buffers(ctx, subctx); @@ -12001,8 +12015,9 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ st.dynamic_kv = true; st.kv_c = kv_c; st.n_batch = n_batch; - st.row_bytes = k_row_bytes; - st.row_elems = (uint32_t) (k->ne[0] / ggml_blck_size(k->type)); + st.row_bytes = u_row_by; + st.row_elems = dq ? (uint32_t) k->ne[0] : (uint32_t) (k->ne[0] / ggml_blck_size(k->type)); + st.dequantized = dq; st.kc_buf = kc_buf; st.mc_buf = mc_buf; st.kv_buf = uc_buf; @@ -12240,8 +12255,8 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx // If this fires, supports_op admitted a non-native K/V type the gate then rejected; the // native shader would return garbage rather than fail, so abort instead. GGML_ASSERT(use_dequant_kv || !kv_needs_dequant); - const ggml_type k_type_eff = use_dequant_kv ? GGML_TYPE_F16 : k->type; - const ggml_type v_type_eff = use_dequant_kv ? GGML_TYPE_F16 : v->type; + const ggml_type k_type_eff = (use_dequant_kv || fa_compact.dequantized) ? GGML_TYPE_F16 : k->type; + const ggml_type v_type_eff = (use_dequant_kv || fa_compact.dequantized) ? GGML_TYPE_F16 : v->type; // For scalar/coopmat1 FA, we can use the "large" size to accommodate qga. // For coopmat2 FA, we always use the small size (which is still pretty large for gqa). diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union_dq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union_dq.comp new file mode 100644 index 000000000000..fc2a0f4541a5 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union_dq.comp @@ -0,0 +1,99 @@ +#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_EXT_shader_explicit_arithmetic_types_int8 : require +#extension GL_EXT_shader_explicit_arithmetic_types_int16 : require +#extension GL_EXT_shader_explicit_arithmetic_types_int32 : require + +#include "types.glsl" + +// Gather + DEQUANTISE in one pass, for the DeepSeek V4 small-batch decode path with a +// quantised KV cache. +// +// The sibling flash_attn_gather_union.comp relocates rows verbatim, which leaves flash +// attention to dequantise them in its inner loop. That costs a MEASURED 0.15 us per KV row +// attended - constant across batch 2/4/8 and across the dense and gathered regimes alike, and +// large enough to make q8_0 attention 1.78x slower than f16 despite reading half the bytes. +// It is redundant work: the FA re-decodes the same row for every query block that reads it, +// whereas the gather already touches each selected row exactly once. +// +// So dequantise here instead. The compact scratch becomes f16, flash attention takes its f16 +// path, and the decode is paid once per row rather than once per use. The extra cost is +// writing f16 instead of q8_0 into the scratch; the pass itself already existed. +// +// q8_0 only. Other quantised types keep the verbatim path, which is correct for them and was +// validated; generalising means one variant per type through dequant_funcs.glsl, which is only +// worth doing if this shape of win holds up. + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer KBuf { block_q8_0_packed16 data_k[]; }; +layout(binding = 1) readonly buffer UBuf { uint data_u[]; }; +layout(binding = 2) readonly buffer MBuf { float16_t data_m[]; }; +layout(binding = 3) writeonly buffer KcBuf { float16_t data_kc[]; }; +layout(binding = 4) writeonly buffer McBuf { float16_t data_mc[]; }; +layout(binding = 5) readonly buffer CBuf { uint data_c[]; }; + +layout(push_constant) uniform Parameters { + uint n_kv; + uint n_kv_raw; + uint kv_c_max; + uint nbk1; // K source row stride, BLOCKS + uint nbm1; + uint n_batch; + uint row_elems; // elements per K row (== head size) +} p; + +const uint LANES = 64; +const uint QK8_0 = 32; + +void main() { + const uint row = gl_WorkGroupID.x; + const uint tid = gl_LocalInvocationIndex; + + const uint kv_c = data_c[0]; // padded compact rows, the FA's runtime KV + const uint n_uni = data_c[1]; // unpadded union size + + if (row >= kv_c) { + return; + } + + uint src = p.n_kv; // sentinel: invalid + if (row < p.n_kv_raw) { + src = row; + } else if (row - p.n_kv_raw < n_uni) { + src = p.n_kv_raw + data_u[row - p.n_kv_raw]; + } + + // 64 lanes x 8 elements covers a 512-element row exactly; a q8_0 block is 32 elements, so + // each lane stays inside one block and reads four int16 pairs from it. + const uint dst_base = row * p.row_elems; + const uint e0 = tid * 8; + if (src < p.n_kv) { + const uint ib = src * p.nbk1 + e0 / QK8_0; + const uint iqs = e0 % QK8_0; + const float d = float(data_k[ib].d); + [[unroll]] for (uint j = 0; j < 4; ++j) { + const i8vec2 v = unpack8(int32_t(data_k[ib].qs[iqs / 2 + j])).xy; + data_kc[dst_base + e0 + 2 * j] = float16_t(d * float(v.x)); + data_kc[dst_base + e0 + 2 * j + 1] = float16_t(d * float(v.y)); + } + } else { + [[unroll]] for (uint j = 0; j < 8; ++j) { + data_kc[dst_base + e0 + j] = float16_t(0.0); + } + } + + // Compact mask is token-major [n_batch][kv_c], stride the RUNTIME kv_c: the FA derives + // m_row_len from KV, which is that same runtime value. + const float NEG_INF = uintBitsToFloat(0xff800000); + if (tid < p.n_batch) { + float mv = NEG_INF; + if (src < p.n_kv) { + mv = float(data_m[tid * p.nbm1 + src]); + } + data_mc[tid * kv_c + row] = float16_t(mv); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 38e9e4e0af5b..44c00f4acacc 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -824,6 +824,7 @@ void process_shaders() { string_to_spv("flash_attn_gather_f16", "flash_attn_gather.comp", {}); string_to_spv("flash_attn_union_f16", "flash_attn_union.comp", {}); string_to_spv("flash_attn_gather_union_f16", "flash_attn_gather_union.comp", {}); + string_to_spv("flash_attn_gather_union_dq_q8_0", "flash_attn_gather_union_dq.comp", {}); string_to_spv("dsv4_hc_pre_f32", "dsv4_hc_pre.comp", {}); string_to_spv("dsv4_hc_comb_f32", "dsv4_hc_comb.comp", {}); string_to_spv("dsv4_hc_post_f32", "dsv4_hc_post.comp", {}); From 987f66222055ae2945597d416f3d38da2a03df5d Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Fri, 14 Aug 2026 07:29:56 +0000 Subject: [PATCH 123/212] vulkan: decode q4_0 in the V4 gather too, with the real element mapping q4_0 turns out to want this more than q8_0 did. Its inline-decode penalty is the same shape - a constant per KV row attended, flat across batch and across dense vs gathered - but larger: 0.172 to 0.179 us/row against q8_0's 0.148 to 0.153. Unpacking nibbles costs more ALU than the halved bytes save, which is also why q4_0 was the SLOWEST of the three types before this. kv=11008 n_kv_raw=2304 n_top_k=512 ov=60, batch 2 / 4 / 8 type dense verbatim gather decoded gather f16 2192/2203/2213 - 640/726/902 q8_0 3917/3924/3936 1110/1257/1567 628/712/888 q4_0 4134/4141/4145 1189/1342/1671 626/711/886 All three converge, because after the gather they run the same f16 attention; what is left is the scattered-read side, where fewer bytes now wins. Against the dense fallback each type took before any of this work, batch 8 is 2.45x for f16, 4.43x for q8_0 and 4.68x for q4_0. The interesting part is what the first attempt got wrong. Generalising via dequantize4() from dequant_funcs.glsl looked obvious and passed on q8_0, then failed q4_0 with NMSE 1.17. That helper is permutation AGNOSTIC by design: mul_mat_vec only ever feeds it into a dot product, where any consistent element order gives the same sum, so for q4_0 it returns nibbles in packed order rather than element order. q8_0 passed only because its layout happens to be contiguous. Materialising a row into memory is precisely the use where the order matters, so each type's true mapping is now written out - for q4_0, byte j carries element j in its low nibble and element j+16 in its high one, exactly as dequant_q4_0.comp materialises it. That is also why this stops at two types rather than looping over the whole native list: every type needs its mapping stated and tested, an #error guards anything else reaching the shader, and q8_0 and q4_0 are the two actually used as a KV cache. q4_1/q5_0/q5_1/iq4_nl keep the verbatim gather, which is correct for them. 13318 FLASH_ATTN_EXT cases pass, including q4_0 and q8_0 top-k cases at the shapes a DSv4 decode hits, plus q4_0 perf cells alongside the q8_0 ones. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 17 +++-- .../flash_attn_gather_union_dq.comp | 70 +++++++++++++------ .../vulkan-shaders/vulkan-shaders-gen.cpp | 9 ++- tests/test-backend-ops.cpp | 6 +- 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index b599012284ef..7de378a8f481 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1114,7 +1114,7 @@ struct vk_device_struct { vk_pipeline pipeline_flash_attn_gather_f16; vk_pipeline pipeline_flash_attn_union_f16; vk_pipeline pipeline_flash_attn_gather_union_f16; - vk_pipeline pipeline_flash_attn_gather_union_dq_q8_0; + vk_pipeline pipeline_flash_attn_gather_union_dq[GGML_TYPE_COUNT]; vk_pipeline pipeline_dsv4_hc_pre_f32; vk_pipeline pipeline_dsv4_hc_comb_f32; vk_pipeline pipeline_dsv4_hc_post_f32; @@ -6346,10 +6346,15 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { "flash_attn_gather_union_f16", flash_attn_gather_union_f16_len, flash_attn_gather_union_f16_data, "main", 6, sizeof(vk_op_flash_attn_gather_union_push_constants), {1, 1, 1}, {}, 1, true, true, device->subgroup_size); - ggml_vk_create_pipeline(device, device->pipeline_flash_attn_gather_union_dq_q8_0, - "flash_attn_gather_union_dq_q8_0", flash_attn_gather_union_dq_q8_0_len, flash_attn_gather_union_dq_q8_0_data, "main", 6, - sizeof(vk_op_flash_attn_gather_union_push_constants), {1, 1, 1}, {}, 1, true, true, +#define CREATE_FA_GATHER_DQ(TYPE, NAMED) \ + ggml_vk_create_pipeline(device, device->pipeline_flash_attn_gather_union_dq[TYPE], \ + "flash_attn_gather_union_dq_" #NAMED, flash_attn_gather_union_dq_ ## NAMED ## _len, \ + flash_attn_gather_union_dq_ ## NAMED ## _data, "main", 6, \ + sizeof(vk_op_flash_attn_gather_union_push_constants), {1, 1, 1}, {}, 1, true, true, \ device->subgroup_size); + CREATE_FA_GATHER_DQ(GGML_TYPE_Q4_0, q4_0) + CREATE_FA_GATHER_DQ(GGML_TYPE_Q8_0, q8_0) +#undef CREATE_FA_GATHER_DQ } } @@ -11962,7 +11967,7 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ // Decoding on the way in makes the scratch f16 and hands flash attention its f16 path, // which is worth far more than the extra scratch bytes: the inline decode it replaces // costs a measured 0.15 us per KV row attended, every step. - const bool dq = k->type == GGML_TYPE_Q8_0 && ctx->device->pipeline_flash_attn_gather_union_dq_q8_0; + const bool dq = ggml_is_quantized(k->type) && ctx->device->pipeline_flash_attn_gather_union_dq[k->type]; const uint32_t u_row_by = dq ? (uint32_t) (k->ne[0] * sizeof(ggml_fp16_t)) : k_row_bytes; const size_t ukc_sz = (size_t) kv_c * u_row_by; const size_t umc_sz = (size_t) n_batch * kv_c * sizeof(ggml_fp16_t); @@ -11984,7 +11989,7 @@ static bool ggml_vk_flash_attn_gather_compact(ggml_backend_vk_context * ctx, vk_ // prealloc_y sync above is a global barrier and orders the previous layer's read. const vk_subbuffer uc_buf = ggml_vk_subbuffer(ctx, ctx->fa_union_stat); - vk_pipeline gather_pipe = dq ? ctx->device->pipeline_flash_attn_gather_union_dq_q8_0 + vk_pipeline gather_pipe = dq ? ctx->device->pipeline_flash_attn_gather_union_dq[k->type] : ctx->device->pipeline_flash_attn_gather_union_f16; ggml_pipeline_request_descriptor_sets(ctx, ctx->device->pipeline_flash_attn_union_f16, 1); ggml_pipeline_request_descriptor_sets(ctx, gather_pipe, 1); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union_dq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union_dq.comp index fc2a0f4541a5..fd3d0708b2f0 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union_dq.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union_dq.comp @@ -7,29 +7,32 @@ #extension GL_EXT_shader_explicit_arithmetic_types_int16 : require #extension GL_EXT_shader_explicit_arithmetic_types_int32 : require +// iq4_nl's value table lives in shared memory and is typed FLOAT_TYPE +#define FLOAT_TYPE float #include "types.glsl" // Gather + DEQUANTISE in one pass, for the DeepSeek V4 small-batch decode path with a -// quantised KV cache. +// quantised KV cache. One variant per K type, via DATA_A_*. // // The sibling flash_attn_gather_union.comp relocates rows verbatim, which leaves flash -// attention to dequantise them in its inner loop. That costs a MEASURED 0.15 us per KV row -// attended - constant across batch 2/4/8 and across the dense and gathered regimes alike, and -// large enough to make q8_0 attention 1.78x slower than f16 despite reading half the bytes. -// It is redundant work: the FA re-decodes the same row for every query block that reads it, -// whereas the gather already touches each selected row exactly once. +// attention to decode them in its inner loop - once per query block that reads a row, rather +// than once per row. Measured, that is a constant per KV row attended: // -// So dequantise here instead. The compact scratch becomes f16, flash attention takes its f16 -// path, and the decode is paid once per row rather than once per use. The extra cost is -// writing f16 instead of q8_0 into the scratch; the pass itself already existed. +// q8_0 0.148 - 0.153 us/row q4_0 0.172 - 0.179 us/row // -// q8_0 only. Other quantised types keep the verbatim path, which is correct for them and was -// validated; generalising means one variant per type through dequant_funcs.glsl, which is only -// worth doing if this shape of win holds up. +// flat across batch 2/4/8 and across the dense and gathered regimes alike. It is what made +// quantised attention slower than f16 despite reading fewer bytes, and q4_0 worse than q8_0 +// despite reading fewer bytes still: unpacking nibbles costs more ALU than the bytes save. +// +// The gather already touches each selected row exactly once, so decoding here converts per-use +// work into per-row work. The compact scratch becomes f16 and flash attention takes its f16 +// path. The pass itself already existed, so the only added cost is writing f16 rather than +// blocks into the scratch. layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; -layout(binding = 0) readonly buffer KBuf { block_q8_0_packed16 data_k[]; }; +layout (binding = 0) readonly buffer A {A_TYPE data_a[];}; + layout(binding = 1) readonly buffer UBuf { uint data_u[]; }; layout(binding = 2) readonly buffer MBuf { float16_t data_m[]; }; layout(binding = 3) writeonly buffer KcBuf { float16_t data_kc[]; }; @@ -47,9 +50,12 @@ layout(push_constant) uniform Parameters { } p; const uint LANES = 64; -const uint QK8_0 = 32; void main() { +#ifdef NEEDS_INIT_IQ_SHMEM + // barrier inside; must run before any divergent return below + init_iq_shmem(gl_WorkGroupSize); +#endif const uint row = gl_WorkGroupID.x; const uint tid = gl_LocalInvocationIndex; @@ -67,19 +73,37 @@ void main() { src = p.n_kv_raw + data_u[row - p.n_kv_raw]; } - // 64 lanes x 8 elements covers a 512-element row exactly; a q8_0 block is 32 elements, so - // each lane stays inside one block and reads four int16 pairs from it. + // 64 lanes x 8 elements covers a 512-element row exactly, and QUANT_K is 32, so a lane's 8 + // elements sit wholly inside one block AND wholly inside one nibble half. + // + // Deliberately NOT dequantize4() from dequant_funcs.glsl. That helper is permutation + // AGNOSTIC: mul_mat_vec only ever feeds it into a dot product, where any consistent element + // order gives the same answer, so for q4_0 it returns nibbles in packed order rather than + // element order. Materialising a row to memory is the one use where the order matters, and + // using it here produced NMSE 1.17 on q4_0 while q8_0 passed - q8_0's layout just happens to + // be contiguous. Each type's true element mapping is written out below instead. const uint dst_base = row * p.row_elems; const uint e0 = tid * 8; if (src < p.n_kv) { - const uint ib = src * p.nbk1 + e0 / QK8_0; - const uint iqs = e0 % QK8_0; - const float d = float(data_k[ib].d); - [[unroll]] for (uint j = 0; j < 4; ++j) { - const i8vec2 v = unpack8(int32_t(data_k[ib].qs[iqs / 2 + j])).xy; - data_kc[dst_base + e0 + 2 * j] = float16_t(d * float(v.x)); - data_kc[dst_base + e0 + 2 * j + 1] = float16_t(d * float(v.y)); + const uint ib = src * p.nbk1 + e0 / QUANT_K; + const uint iqs = e0 % QUANT_K; + const float d = float(data_a[ib].d); +#if defined(DATA_A_Q8_0) + // element e is qs[e] + [[unroll]] for (uint l = 0; l < 8; ++l) { + data_kc[dst_base + e0 + l] = float16_t(d * float(data_a[ib].qs[iqs + l])); + } +#elif defined(DATA_A_Q4_0) + // byte j carries element j in its low nibble and element j+16 in its high one + const uint shift = (iqs >> 4) * 4; // 0 for elements 0..15, 4 for 16..31 + const uint byte0 = iqs & 0xF; + [[unroll]] for (uint l = 0; l < 8; ++l) { + const float q = float((data_a[ib].qs[byte0 + l] >> shift) & 0xF) - 8.0f; + data_kc[dst_base + e0 + l] = float16_t(d * q); } +#else +#error "no element mapping written for this K type" +#endif } else { [[unroll]] for (uint j = 0; j < 8; ++j) { data_kc[dst_base + e0 + j] = float16_t(0.0); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 44c00f4acacc..efef6542a77b 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -824,7 +824,14 @@ void process_shaders() { string_to_spv("flash_attn_gather_f16", "flash_attn_gather.comp", {}); string_to_spv("flash_attn_union_f16", "flash_attn_union.comp", {}); string_to_spv("flash_attn_gather_union_f16", "flash_attn_gather_union.comp", {}); - string_to_spv("flash_attn_gather_union_dq_q8_0", "flash_attn_gather_union_dq.comp", {}); + // one decoder per quantised K type flash-attention supports natively; f16/bf16/f32 need no + // decode and take the verbatim gather + // q8_0 and q4_0 only: each needs its true element mapping written out (see the shader), and + // these are the two types actually used as a KV cache. The rest take the verbatim gather. + for (const auto& tname : {"q4_0", "q8_0"}) { + string_to_spv("flash_attn_gather_union_dq_" + std::string(tname), "flash_attn_gather_union_dq.comp", + {{"DATA_A_" + to_uppercase(tname), "1"}}); + } string_to_spv("dsv4_hc_pre_f32", "dsv4_hc_pre.comp", {}); string_to_spv("dsv4_hc_comb_f32", "dsv4_hc_comb.comp", {}); string_to_spv("dsv4_hc_post_f32", "dsv4_hc_post.comp", {}); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 749369be2abd..c3cb4ef8e99e 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11983,8 +11983,10 @@ static std::vector> make_test_cases_perf() { } // q8_0 K/V at the same shapes: DSv4 with -ctk q8_0 took the dense fallback before the // gather became type-agnostic, so this is the cell that says whether it now pays there. - for (int nb : { 2, 4, 8, 16 }) { - test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, nb, 2304, 512, false, 1, 60, GGML_TYPE_Q8_0)); + for (ggml_type tk : { GGML_TYPE_Q8_0, GGML_TYPE_Q4_0 }) { + for (int nb : { 2, 4, 8, 16 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, nb, 2304, 512, false, 1, 60, tk)); + } } return test_cases; From fa5f2517f19297d5ae4c32d03b52faa26db81891 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Mon, 17 Aug 2026 09:31:21 +0000 Subject: [PATCH 124/212] vulkan : restore the unrolled row copy in the DSV4 gather shaders Generalising the gather from f16 elements to raw words (6b2cade31) replaced [[unroll]] for (uint i = 0; i < HEAD_SIZE / LANES; ++i) with a loop bounded by the row_words push constant, which cannot be unrolled and pays a bounds check per iteration. Half as many, twice as wide accesses were still 6% slower on the smallest op the gather serves. Bisected over the 19 beta3 commits with the tip's test file overlaid at every point, so the instrument stayed fixed while beta3 grew its own perf cases. nb=1 at kv=8192: 54.58 us at the pre-beta3 base, flat through commit 16 (54.84), 58.24 at 6b2cade31, 58.23 at the tip. Step 4*LANES instead. Each access stays contiguous across the lanes, so coalescing is unchanged, and an f16 row at head size 512 is 256 words == 4*LANES: one iteration, no loop overhead. The tail loop carries the quantised row sizes, which are not multiples of 4*LANES (q8_0 is 136 words, q4_0 is 72). nb=1 against the pre-beta3 base, f16 K/V: -0.07% at kv=8192, -2.83% at kv=35584, mean -1.29% across the six shapes, so the regression is gone rather than reduced. Against beta3 as merged: -4.8% to -6.2%. The nb>=2 gains are untouched, worst cell -0.51%. 13318 FLASH_ATTN_EXT cases pass. Worth noting the gather itself was never the cost at nb=1: with GGML_VK_FA_TOPK_GATHER=0 that cell is 249.84 us against 58.14 us with it on. Assisted-by: Claude Opus 5 --- .../vulkan-shaders/flash_attn_gather.comp | 22 +++++++++++++++++-- .../flash_attn_gather_union.comp | 20 +++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp index 6dd24d31b517..4e16b2a9cece 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather.comp @@ -74,13 +74,31 @@ void main() { // zero scale zeroes the block) as well as for f16. The row is -inf in the mask either way; // zeroing only keeps a garbage dot product from reaching the softmax as a NaN. const uint dst_base = (stream * p.kv_c + row) * p.row_words; + // row_words is a push constant, so this cannot be [[unroll]]ed the way it was when the row + // was a fixed 512 f16 elements. Stepping 4*LANES recovers that: each access stays contiguous + // across the lanes, and an f16 row (256 words == 4*LANES) is one iteration with no loop + // overhead. The tail carries the quantised row sizes, which are not multiples of 4*LANES. if (src < p.n_kv) { const uint src_base = stream * p.nbk3 + src * p.nbk1; - for (uint i = tid; i < p.row_words; i += LANES) { + uint i = tid; + for (; i + 3 * LANES < p.row_words; i += 4 * LANES) { + data_kc[dst_base + i] = data_k[src_base + i]; + data_kc[dst_base + i + LANES] = data_k[src_base + i + LANES]; + data_kc[dst_base + i + 2 * LANES] = data_k[src_base + i + 2 * LANES]; + data_kc[dst_base + i + 3 * LANES] = data_k[src_base + i + 3 * LANES]; + } + for (; i < p.row_words; i += LANES) { data_kc[dst_base + i] = data_k[src_base + i]; } } else { - for (uint i = tid; i < p.row_words; i += LANES) { + uint i = tid; + for (; i + 3 * LANES < p.row_words; i += 4 * LANES) { + data_kc[dst_base + i] = 0u; + data_kc[dst_base + i + LANES] = 0u; + data_kc[dst_base + i + 2 * LANES] = 0u; + data_kc[dst_base + i + 3 * LANES] = 0u; + } + for (; i < p.row_words; i += LANES) { data_kc[dst_base + i] = 0u; } } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp index 4669e0d1c75e..306372ba8af6 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_union.comp @@ -63,13 +63,29 @@ void main() { // type here (a zero scale zeroes the block) as well as for f16. The row is -inf in the mask // either way; zeroing only keeps a garbage dot product from reaching the softmax as a NaN. const uint dst_base = row * p.row_words; + // Stepped 4*LANES for the same reason as flash_attn_gather.comp: row_words is a push + // constant, so the copy cannot be [[unroll]]ed, and an f16 row is one iteration this way. if (src < p.n_kv) { const uint src_base = src * p.nbk1; - for (uint i = tid; i < p.row_words; i += LANES) { + uint i = tid; + for (; i + 3 * LANES < p.row_words; i += 4 * LANES) { + data_kc[dst_base + i] = data_k[src_base + i]; + data_kc[dst_base + i + LANES] = data_k[src_base + i + LANES]; + data_kc[dst_base + i + 2 * LANES] = data_k[src_base + i + 2 * LANES]; + data_kc[dst_base + i + 3 * LANES] = data_k[src_base + i + 3 * LANES]; + } + for (; i < p.row_words; i += LANES) { data_kc[dst_base + i] = data_k[src_base + i]; } } else { - for (uint i = tid; i < p.row_words; i += LANES) { + uint i = tid; + for (; i + 3 * LANES < p.row_words; i += 4 * LANES) { + data_kc[dst_base + i] = 0u; + data_kc[dst_base + i + LANES] = 0u; + data_kc[dst_base + i + 2 * LANES] = 0u; + data_kc[dst_base + i + 3 * LANES] = 0u; + } + for (; i < p.row_words; i += LANES) { data_kc[dst_base + i] = 0u; } } From 7ceebb6b5261ce565d1bc192a59ab619a7c9c521 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 20 Aug 2026 00:38:13 +0000 Subject: [PATCH 125/212] vulkan: decode quantised K/V inside the DeepSeek V4 per-token gather The union gather decodes q8_0/q4_0 on the way in, but it exists only for n_batch > 1: a single token has nothing to deduplicate, so plain autoregressive decode takes the per-token gather, which relocated rows verbatim and left flash attention paying the same inline decode the union work measured at ~0.15 us per KV row attended. flash_attn_gather_dq.comp closes that: same row mapping as flash_attn_gather.comp, same per-type element mapping as flash_attn_gather_union_dq.comp (q4_0 materialised in element order, not packed order). The compact scratch becomes f16 and flash attention takes its f16 path, keyed off the compact state exactly as the union path already is. test-backend-ops perf, kv=11008 n_kv_raw=2304 n_top_k=512 ov=60, nb=1: type verbatim gather decoded gather q8_0 226.9 us 102.9 us q4_0 258.4 us 101.8 us f16 114.5 us 105.5 us (f16 never decodes; its two runs bound the harness spread) Both quantised types land on the f16 op time instead of trailing it 2.0-2.3x: after the gather the attention work is identical f16, and the gather's scattered read side moves fewer bytes. q8_0 and q4_0 only, as in the union: each type's element mapping must be stated and tested, an #error guards anything else reaching the shader, and every other type keeps the verbatim gather. nb=1 rows are added to the top-k perf grids so this regime stays measured. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 33 ++++-- .../vulkan-shaders/flash_attn_gather_dq.comp | 106 ++++++++++++++++++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 2 + tests/test-backend-ops.cpp | 6 +- 4 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_dq.comp diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 7de378a8f481..92e2c58282b8 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -1112,6 +1112,7 @@ struct vk_device_struct { vk_pipeline pipeline_flash_attn_top_k_f16; vk_pipeline pipeline_flash_attn_top_k_cm_f16; vk_pipeline pipeline_flash_attn_gather_f16; + vk_pipeline pipeline_flash_attn_gather_dq[GGML_TYPE_COUNT]; vk_pipeline pipeline_flash_attn_union_f16; vk_pipeline pipeline_flash_attn_gather_union_f16; vk_pipeline pipeline_flash_attn_gather_union_dq[GGML_TYPE_COUNT]; @@ -6337,6 +6338,15 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { "flash_attn_gather_f16", flash_attn_gather_f16_len, flash_attn_gather_f16_data, "main", 5, sizeof(vk_op_flash_attn_gather_push_constants), {1, 1, 1}, {}, 1, true, true, device->subgroup_size); +#define CREATE_FA_GATHER_TOK_DQ(TYPE, NAMED) \ + ggml_vk_create_pipeline(device, device->pipeline_flash_attn_gather_dq[TYPE], \ + "flash_attn_gather_dq_" #NAMED, flash_attn_gather_dq_ ## NAMED ## _len, \ + flash_attn_gather_dq_ ## NAMED ## _data, "main", 5, \ + sizeof(vk_op_flash_attn_gather_push_constants), {1, 1, 1}, {}, 1, true, true, \ + device->subgroup_size); + CREATE_FA_GATHER_TOK_DQ(GGML_TYPE_Q4_0, q4_0) + CREATE_FA_GATHER_TOK_DQ(GGML_TYPE_Q8_0, q8_0) +#undef CREATE_FA_GATHER_TOK_DQ if (device->subgroup_arithmetic) { ggml_vk_create_pipeline(device, device->pipeline_flash_attn_union_f16, "flash_attn_union_f16", flash_attn_union_f16_len, flash_attn_union_f16_data, "main", 3, @@ -12037,8 +12047,14 @@ union_unavailable:; return false; } + // Decode on the way in, as the union path does: the gather touches each row once, while + // flash attention decodes once per query block that reads it. The union only covers + // n_batch > 1, so single-token decode lands here. + const bool tok_dq = ggml_is_quantized(k->type) && ctx->device->pipeline_flash_attn_gather_dq[k->type]; + const uint32_t row_by = tok_dq ? (uint32_t) (k->ne[0] * sizeof(ggml_fp16_t)) : k_row_bytes; + const uint32_t ns = (uint32_t) q->ne[3]; - const size_t kc_sz = (size_t) ns * kv_c * k_row_bytes; + const size_t kc_sz = (size_t) ns * kv_c * row_by; const size_t mc_sz = (size_t) ns * n_batch * kv_c * sizeof(ggml_fp16_t); if (ctx->prealloc_size_y < kc_sz + mc_sz) { @@ -12049,19 +12065,21 @@ union_unavailable:; ggml_vk_sync_buffers(ctx, subctx); } - vk_pipeline pipeline = ctx->device->pipeline_flash_attn_gather_f16; + vk_pipeline pipeline = tok_dq ? ctx->device->pipeline_flash_attn_gather_dq[k->type] + : ctx->device->pipeline_flash_attn_gather_f16; ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); + // the fused decoder steps K in BLOCKS and writes elements; the verbatim one does words const vk_op_flash_attn_gather_push_constants pc = { (uint32_t) k->ne[1], (uint32_t) n_kv_raw, (uint32_t) top_k->ne[0], kv_c, - (uint32_t) (k->nb[1] / 4), - (uint32_t) (k->nb[3] / 4), + tok_dq ? (uint32_t) (k->nb[1] / ggml_type_size(k->type)) : (uint32_t) (k->nb[1] / 4), + tok_dq ? (uint32_t) (k->nb[3] / ggml_type_size(k->type)) : (uint32_t) (k->nb[3] / 4), (uint32_t) (top_k->nb[1] / sizeof(int32_t)), (uint32_t) (top_k->nb[3] / sizeof(int32_t)), (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)), (uint32_t) (mask->nb[3] / sizeof(ggml_fp16_t)), (uint32_t) mask->ne[3], - n_batch, k_row_words, + n_batch, tok_dq ? (uint32_t) k->ne[0] : k_row_words, }; st.kc_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0); @@ -12123,8 +12141,9 @@ union_unavailable:; st.active = true; st.kv_c = kv_c; st.n_batch = n_batch; - st.row_bytes = k_row_bytes; - st.row_elems = (uint32_t) (k->ne[0] / ggml_blck_size(k->type)); + st.row_bytes = row_by; + st.row_elems = tok_dq ? (uint32_t) k->ne[0] : (uint32_t) (k->ne[0] / ggml_blck_size(k->type)); + st.dequantized = tok_dq; return true; } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_dq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_dq.comp new file mode 100644 index 000000000000..f2711539641a --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_gather_dq.comp @@ -0,0 +1,106 @@ +#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_EXT_shader_explicit_arithmetic_types_int8 : require +#extension GL_EXT_shader_explicit_arithmetic_types_int16 : require +#extension GL_EXT_shader_explicit_arithmetic_types_int32 : require + +#define FLOAT_TYPE float +#include "types.glsl" + +// Gather + DEQUANTISE for the per-token compact form: same row mapping as the verbatim +// flash_attn_gather.comp, same decode as flash_attn_gather_union_dq.comp. One variant per K +// type, via DATA_A_*. +// +// The union carries the decode only for n_batch > 1, because it needs more than one token to +// deduplicate. Single-token decode is the common case and it lands here; a verbatim gather +// leaves flash attention to decode inline, measured 1.98x the f16 op at q8_0 and 2.26x at +// q4_0, kv 11008. + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0) readonly buffer A { A_TYPE data_a[]; }; +layout(binding = 1) readonly buffer TopBuf { int data_top[]; }; +layout(binding = 2) readonly buffer MBuf { float16_t data_m[]; }; +layout(binding = 3) writeonly buffer KcBuf { float16_t data_kc[]; }; +layout(binding = 4) writeonly buffer McBuf { float16_t data_mc[]; }; + +layout(push_constant) uniform Parameters { + uint n_kv; + uint n_kv_raw; + uint n_top_k; + uint kv_c; + uint nbk1; // K source row stride, BLOCKS + uint nbk3; // K source stream stride, BLOCKS + uint nbt1; + uint nbt3; + uint nbm1; + uint nbm3; + uint nem3; + uint n_batch; + uint row_elems; // elements per K row; 64 lanes x 8 covers the 512 the gate pins +} p; + +void main() { + const uint row = gl_WorkGroupID.x; + const uint stream = gl_WorkGroupID.z; + const uint tid = gl_LocalInvocationIndex; + + const uint ALL_TOKENS = 0xffffffffu; + uint src = p.n_kv; + uint owner = ALL_TOKENS; + if (row < p.n_kv_raw) { + src = row; + } else { + const uint off = row - p.n_kv_raw; + const uint tok = off / p.n_top_k; + const uint slot = off - tok * p.n_top_k; + if (tok < p.n_batch) { + owner = tok; + const int idx = data_top[stream * p.nbt3 + tok * p.nbt1 + slot]; + if (idx >= 0 && uint(idx) < p.n_kv - p.n_kv_raw) { + src = p.n_kv_raw + uint(idx); + } + } + } + + const uint dst_base = (stream * p.kv_c + row) * p.row_elems; + const uint e0 = tid * 8; + if (src < p.n_kv) { + const uint ib = stream * p.nbk3 + src * p.nbk1 + e0 / QUANT_K; + const uint iqs = e0 % QUANT_K; + const float d = float(data_a[ib].d); +#if defined(DATA_A_Q8_0) + // element e is qs[e] + [[unroll]] for (uint l = 0; l < 8; ++l) { + data_kc[dst_base + e0 + l] = float16_t(d * float(data_a[ib].qs[iqs + l])); + } +#elif defined(DATA_A_Q4_0) + // byte j carries element j in its low nibble and element j+16 in its high one + const uint shift = (iqs >> 4) * 4; + const uint byte0 = iqs & 0xF; + [[unroll]] for (uint l = 0; l < 8; ++l) { + const float q = float((data_a[ib].qs[byte0 + l] >> shift) & 0xF) - 8.0f; + data_kc[dst_base + e0 + l] = float16_t(d * q); + } +#else +#error "no element mapping written for this K type" +#endif + } else { + [[unroll]] for (uint l = 0; l < 8; ++l) { + data_kc[dst_base + e0 + l] = float16_t(0.0); + } + } + + const float NEG_INF = uintBitsToFloat(0xff800000); + if (tid < p.n_batch) { + const uint mc_idx = (stream * p.n_batch + tid) * p.kv_c + row; + float mv = NEG_INF; + if (src < p.n_kv && (owner == ALL_TOKENS || owner == tid)) { + mv = float(data_m[(stream % p.nem3) * p.nbm3 + tid * p.nbm1 + src]); + } + data_mc[mc_idx] = float16_t(mv); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index efef6542a77b..fe82b8fb6cb1 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -831,6 +831,8 @@ void process_shaders() { for (const auto& tname : {"q4_0", "q8_0"}) { string_to_spv("flash_attn_gather_union_dq_" + std::string(tname), "flash_attn_gather_union_dq.comp", {{"DATA_A_" + to_uppercase(tname), "1"}}); + string_to_spv("flash_attn_gather_dq_" + std::string(tname), "flash_attn_gather_dq.comp", + {{"DATA_A_" + to_uppercase(tname), "1"}}); } string_to_spv("dsv4_hc_pre_f32", "dsv4_hc_pre.comp", {}); string_to_spv("dsv4_hc_comb_f32", "dsv4_hc_comb.comp", {}); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index c3cb4ef8e99e..7c943a503ae6 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11969,7 +11969,7 @@ static std::vector> make_test_cases_perf() { // worst-case compact set 2304 + nb*512 crosses kv/2 at nb=6, so those cells measure // whether the gate can be opened by the union rather than by the worst case. for (int kv : { 11008, 35584, 133888 }) { - for (int nb : { 2, 4, 8, 16 }) { + for (int nb : { 1, 2, 4, 8, 16 }) { test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, nb, 2304, 512, false, 1, 60)); } } @@ -11983,8 +11983,10 @@ static std::vector> make_test_cases_perf() { } // q8_0 K/V at the same shapes: DSv4 with -ctk q8_0 took the dense fallback before the // gather became type-agnostic, so this is the cell that says whether it now pays there. + // nb=1 is plain autoregressive decode: the union needs nb > 1 to dedup, so this width + // takes the per-token gather. Its f16 row is in the grid above. for (ggml_type tk : { GGML_TYPE_Q8_0, GGML_TYPE_Q4_0 }) { - for (int nb : { 2, 4, 8, 16 }) { + for (int nb : { 1, 2, 4, 8, 16 }) { test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, nb, 2304, 512, false, 1, 60, tk)); } } From f9471c66bbc54b92b5207d56ddd119c7cc3b5a8b Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 20 Aug 2026 00:25:10 +0000 Subject: [PATCH 126/212] vulkan: dequantise the cache for the DeepSeek V4 sparse prefill ggml_vk_flash_attn_top_k declined any K/V but f16, so a DSv4 run with a quantised cache took dense FA for every prefill batch - O(kv) against the sparse path's O(n_kv_raw + n_top_k), a penalty that grows with depth. A 128GB community run measured it end to end: prefill -5.4/-22.2/-39.4/-52.9% against f16 at source depths 17k/33k/67k/134k with -ctk q8_0. The sparse shaders stay f16-only. A quantised cache is dequantised once per op into the prealloc_x f16 scratch by the dense path's own fused dequant+transpose pipeline, and the shaders read the scratch. K and V are the same tensor in this path, so one pass covers both at half the dense path's scratch footprint. Types without a dequant-transpose pipeline, oversized caches, and GGML_VK_FA_DEQUANT=0 keep the dense fallback. test-backend-ops perf at nb=1024, kv 5504/11008/19200/35584 (the community depths in compressed-K rows): q8_0 was 60.9 -> 427.6 ms dense (1.30x -> 8.27x of f16, growing with kv) and lands within +0.8% of f16's flat 46.9-51.7 ms with the scratch; the dequant pass itself costs under 1%. 36/36 FLASH_ATTN_EXT top-k cases pass, including 6 new quantised prefill cases (nb=64 ns=1/2, nb=128 split form). Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 56 ++++++++++++++++++++++++---- tests/test-backend-ops.cpp | 17 +++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 92e2c58282b8..7fbc651d97f8 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11498,9 +11498,22 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & const ggml_tensor * mask, const ggml_tensor * sinks, ggml_tensor * dst) { const ggml_tensor * top_k = dst->src[5]; static const char * top_k_env = getenv("GGML_VK_FA_TOPK"); + // The sparse shaders read f16 only. Serve a quantised cache by dequantising it once into + // the f16 scratch, the same pass the dense path uses. K and V are the same tensor here, so + // one pass covers both. Without this a quantised cache declines to dense FA, which costs + // O(kv) where the sparse path costs O(n_kv_raw + n_top_k). + static const char * fa_dequant_env = getenv("GGML_VK_FA_DEQUANT"); + const uint64_t kv_f16_sz = (uint64_t) ggml_nelements(k) * sizeof(ggml_fp16_t); + const bool dequant_kv = top_k && k->type != GGML_TYPE_F16 && + !(fa_dequant_env && fa_dequant_env[0] == '0') && + ctx->device->pipeline_dequant_transpose[k->type] != nullptr && + k->nb[0] == ggml_type_size(k->type) && + ggml_is_contiguously_allocated(k) && + kv_f16_sz <= ctx->device->properties.limits.maxStorageBufferRange && + ggml_vk_fa_dequant_scratch_fits(ctx, kv_f16_sz); if ((top_k_env && top_k_env[0] == '0') || !top_k || (!ctx->device->pipeline_flash_attn_top_k_f16 && !ctx->device->pipeline_flash_attn_top_k_cm_f16) || - q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || v->type != GGML_TYPE_F16 || + q->type != GGML_TYPE_F32 || (k->type != GGML_TYPE_F16 && !dequant_kv) || v->type != k->type || !mask || mask->type != GGML_TYPE_F16 || top_k->type != GGML_TYPE_I32 || q->ne[0] != 512 || q->ne[1] < 64 || k->ne[0] != 512 || v->ne[0] != 512 || q->ne[2] != 64 || k->ne[2] != 1 || v->ne[2] != 1 || @@ -11586,14 +11599,43 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & } } + vk_subbuffer k_buf = ggml_vk_tensor_subbuffer(ctx, k); + if (dequant_kv) { + if (ctx->prealloc_size_x < kv_f16_sz) { + ctx->prealloc_size_x = kv_f16_sz; + ggml_vk_preallocate_buffers(ctx, subctx); + } + vk_pipeline tr_k = ctx->device->pipeline_dequant_transpose[k->type]; + ggml_pipeline_request_descriptor_sets(ctx, tr_k, 1); + if (ctx->prealloc_x_need_sync) { + ggml_vk_sync_buffers(ctx, subctx); + } + const vk_subbuffer k_dst = vk_subbuffer{ ctx->prealloc_x, 0, kv_f16_sz }; + const uint32_t k_nel = (uint32_t) ggml_nelements(k); + const std::vector tr_pc = { (uint32_t) k->ne[0], (uint32_t) k->ne[2], (uint32_t) k->ne[1], 0, k_nel }; + ggml_vk_dispatch_pipeline(ctx, subctx, tr_k, { k_buf, k_dst }, tr_pc, { k_nel, 1, 1 }); + ggml_vk_sync_buffers(ctx, subctx); + ctx->prealloc_x_need_sync = true; + k_buf = k_dst; + ggml_vk_perf_mark_subop(ctx, subctx, "FA_KV_DEQUANT (sub-op)"); + } + // strides of what the shaders actually read: the source cache, or the contiguous + // [HS, KV, n_head_kv, ns] f16 scratch the dequant just wrote + const uint32_t k_stride = dequant_kv ? (uint32_t) k->ne[0] : (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)); + const uint32_t k_nb3_el = dequant_kv ? (uint32_t) ((uint64_t) k->ne[0] * k->ne[1] * k->ne[2]) + : (uint32_t) (k->nb[3] / sizeof(ggml_fp16_t)); + const uint32_t k_nb2_byte = dequant_kv ? (uint32_t) ((uint64_t) k->ne[0] * k->ne[1] * sizeof(ggml_fp16_t)) + : (uint32_t) k->nb[2]; + const uint32_t k_nb3_byte = dequant_kv ? (uint32_t) (k_nb3_el * sizeof(ggml_fp16_t)) : (uint32_t) k->nb[3]; + vk_op_flash_attn_top_k_push_constants pc = { (uint32_t) q->ne[1], (uint32_t) k->ne[1], (uint32_t) n_kv_raw, (uint32_t) top_k->ne[0], (uint32_t) q->ne[2], (uint32_t) (q->nb[1] / sizeof(float)), (uint32_t) (q->nb[2] / sizeof(float)), (uint32_t) (q->nb[3] / sizeof(float)), - (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)), - (uint32_t) (k->nb[3] / sizeof(ggml_fp16_t)), + k_stride, + k_nb3_el, (uint32_t) (mask->nb[1] / sizeof(ggml_fp16_t)), (uint32_t) (mask->nb[3] / sizeof(ggml_fp16_t)), (uint32_t) (top_k->nb[1] / sizeof(int32_t)), @@ -11629,7 +11671,6 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & vk_fa_tuning_params tuning = get_fa_tuning_params(ctx->device, D, D, N, raw_kv, GGML_TYPE_F16, GGML_TYPE_F16, f32acc); const uint32_t q_stride = (uint32_t) (q->nb[1] / sizeof(float)); - const uint32_t k_stride = (uint32_t) (k->nb[1] / sizeof(ggml_fp16_t)); const bool aligned = raw_kv % tuning.block_cols == 0 && (q_stride & 7) == 0 && (k_stride & 7) == 0; const vk_fa_pipeline_state raw_state = get_fa_pipeline_state(ctx->device, tuning, D, D, aligned, f32acc, true, false, false, GGML_TYPE_F16, GGML_TYPE_F16); @@ -11671,7 +11712,6 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & const uint32_t packed_gqa = mask_stride_in_split_kv | 1u; const uint32_t packed_partitions = (partitions << 16) | 1; const vk_subbuffer split_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_split_k, 0); - const vk_subbuffer k_buf = ggml_vk_tensor_subbuffer(ctx, k); const vk_subbuffer mask_buf = ggml_vk_tensor_subbuffer(ctx, mask); const vk_subbuffer top_buf = ggml_vk_tensor_subbuffer(ctx, top_k); const vk_subbuffer dst_buf = ggml_vk_tensor_subbuffer(ctx, dst); @@ -11697,8 +11737,8 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & 1, NS, (uint32_t) mask->ne[1], (uint32_t) mask->ne[2], (uint32_t) mask->ne[3], q_stride, (uint32_t) q->nb[2], (uint32_t) q->nb[3], - k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], - k_stride, (uint32_t) k->nb[2], (uint32_t) k->nb[3], + k_stride, k_nb2_byte, k_nb3_byte, + k_stride, k_nb2_byte, k_nb3_byte, scale, 0.0f, 0.0f, n_head_log2, 1.0f, 1.0f, packed_gqa, mask_stride, packed_partitions, @@ -11731,7 +11771,7 @@ static bool ggml_vk_flash_attn_top_k(ggml_backend_vk_context * ctx, vk_context & ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1); ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, - {q_buf, ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, mask), sinks_buf, + {q_buf, k_buf, ggml_vk_tensor_subbuffer(ctx, mask), sinks_buf, ggml_vk_tensor_subbuffer(ctx, top_k), ggml_vk_tensor_subbuffer(ctx, dst)}, pc, {(uint32_t) q->ne[1], (uint32_t) CEIL_DIV(q->ne[2], use_cm ? 32 : 8), (uint32_t) q->ne[3]}); ggml_vk_perf_mark_subop(ctx, subctx, use_cm ? "FA_TOP_K_CM (sub-op)" : "FA_TOP_K_SPARSE (sub-op)"); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 7c943a503ae6..f00b69e24963 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11436,6 +11436,12 @@ static std::vector> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 8, 2304, 512, false, 1, 60, tk)); test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, 16, 2304, 512, false, 1, 86, tk)); test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 1, 1024, 512, false, 1, 0, tk)); + // prefill widths (nb >= 64), where the sparse shaders run on a dequantised f16 scratch + // instead of the cache. ns=2 covers the scratch's stream stride, and the 4096 case is + // wide enough for the raw/selected split form. + test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false, 1, 0, tk)); + test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false, 2, 0, tk)); + test_cases.emplace_back(new test_flash_attn_ext_top_k(4096, 128, 256, 512, false, 1, 0, tk)); } test_cases.emplace_back(new test_flash_attn_ext_top_k(8192, 4, 1024, 512, false, 2)); test_cases.emplace_back(new test_flash_attn_ext_top_k( 768, 64, 64, 128, false, 2)); @@ -11990,6 +11996,17 @@ static std::vector> make_test_cases_perf() { test_cases.emplace_back(new test_flash_attn_ext_top_k(11008, nb, 2304, 512, false, 1, 60, tk)); } } + // PREFILL widths with quantised K/V, which the sparse path now serves through a one-shot + // dequant into the f16 scratch; quantised should sit within ~1% of f16 at every kv here. + // nb=1024 is the reporting user's --ubatch-size; the kv list is their four source depths + // (17k/33k/67k/134k) in compressed-K rows. GGML_VK_FA_DEQUANT=0 reproduces the old dense + // fallback, whose gap GROWS with kv (dense is O(kv), sparse O(n_kv_raw + n_top_k)); f16 + // under GGML_VK_FA_TOPK=0 is the falsification arm for attributing that gap to the gate. + for (ggml_type tk : { GGML_TYPE_F16, GGML_TYPE_Q8_0, GGML_TYPE_Q4_0 }) { + for (int kv : { 5504, 11008, 19200, 35584 }) { + test_cases.emplace_back(new test_flash_attn_ext_top_k(kv, 1024, 2304, 512, false, 1, 0, tk)); + } + } return test_cases; } From f7ffc13272cb4fb6b29e3cbcd3f34d6f17a409e7 Mon Sep 17 00:00:00 2001 From: pepuscz Date: Mon, 24 Aug 2026 22:17:58 +0200 Subject: [PATCH 127/212] vulkan: use small Lightning Indexer CM for batches 4-15 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 7fbc651d97f8..8969f3328d40 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -13191,6 +13191,11 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const if (ctx->device->pipeline_lightning_indexer_decode_cm_f16 && src0->ne[2] == 1) { return ctx->device->pipeline_lightning_indexer_decode_cm_f16; } + static const char * small_cm_env = getenv("GGML_VK_LIGHTNING_INDEXER_SMALL_CM"); + if (ctx->device->pipeline_lightning_indexer_cm_small_f16 && + src0->ne[2] >= 4 && src0->ne[2] < 16 && small_cm_env && small_cm_env[0] == '1') { + return ctx->device->pipeline_lightning_indexer_cm_small_f16; + } vk_pipeline cm = ctx->device->pipeline_lightning_indexer_cm_f16 ? ctx->device->pipeline_lightning_indexer_cm_f16 : ctx->device->pipeline_lightning_indexer_cm_small_f16; return cm && src0->ne[2] >= 16 ? cm : ctx->device->pipeline_lightning_indexer_f16; From ef56966fa024f002d94d9416f79ffec204935df9 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Wed, 26 Aug 2026 05:01:45 +0000 Subject: [PATCH 128/212] vulkan: hoist the Lightning Indexer K fragments out of the head loop The K tile is invariant across the head loop but coopMatLoad sits inside it, so the shader re-read the same 8 MatrixA fragments from shared memory once per head tile - 4x at N_HEAD=64. Each wave64 fragment load moves 1 KiB of LDS traffic because the 16x16 f16 fragment is replicated 4x across the subgroup. Same loads of the same data feeding the same coopMatMulAdd sequence, so the output is bit-identical; the fragments just live in registers (8 of them, 16 f16 per lane) instead of being re-fetched. gfx1151, test-backend-ops perf, kv 8704/33280/131584 x batch 1-15: 16-20% faster at every shape, no spills, no regression. Batch 1 is ordinary DSv4 decode and gets 12-18% of that. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- .../lightning_indexer_decode_cm.comp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp index fd555c76806d..24d1b98d571c 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp @@ -63,6 +63,17 @@ void main() { } barrier(); + // The K tile is invariant across the head loop, but coopMatLoad is inside it, so the shipped + // shader re-reads the same 8 A fragments from shared memory once per head tile (4x at + // N_HEAD=64). Each wave64 fragment load moves 1 KiB of LDS traffic because the 16x16 f16 + // fragment is replicated 4x across the subgroup, so those re-reads are the largest single + // item of per-tile cost after the multiplies themselves. Hoisting costs 8 A fragments of + // register state (16 f16 per lane each). + coopmat kmat[HEAD_SIZE / TILE]; + [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { + coopMatLoad(kmat[d / TILE], k_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + } + float total = 0.0; for (uint head_base = 0; head_base < N_HEAD; head_base += TILE) { for (uint idx = tid; idx < TILE * VEC_PER_HEAD; idx += SUBGROUP_SIZE) { @@ -77,13 +88,11 @@ void main() { coopmat scores = coopmat(0.0); - coopmat kmat; coopmat qmat; [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { - coopMatLoad(kmat, k_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); coopMatLoad(qmat, q_sh, d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutColumnMajor); - scores = coopMatMulAdd(kmat, qmat, scores); + scores = coopMatMulAdd(kmat[d / TILE], qmat, scores); } coopMatStore(scores, score_sh, 0, SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); From 24d18f710e83e12f42233d4727165843bead22e7 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Wed, 26 Aug 2026 05:01:45 +0000 Subject: [PATCH 129/212] vulkan: route the whole small-batch Lightning Indexer window to the decode CM shader Builds directly on pepuscz's PR #6 and issue #10. Their per-kernel table - 43.8 us scalar against 27.9 us small CM per 1k scanned rows at batch 5, and the same ratio at every depth - is what showed the indexer's cost is a per-tile constant rather than anything to do with tokens or bytes. Once the cost is per tile, the thing to minimise is the tile count, and that is what this changes. The finding is downstream of their work; only the choice of shader differs. The decode CM shader puts 16 HEADS in the coopmat N dimension and dispatches one workgroup per token, so it issues 4*n_batch tiles per 16 KV rows. That is the arithmetic minimum - (64 heads x n_batch tokens) / 16 columns - because 64 heads fill its 16 columns exactly, with no remainder at any batch. The small CM shader puts 16 TOKENS in N and pays a flat 64 tiles however small the batch is, so at batch 5 eleven of its sixteen columns are padding. Measured cost tracks tile count: at kv=131584 batch 1 costs 1.95 us per 1k scanned rows and batch 5 costs 26.28, i.e. 13.5x for 5x the work, and both shaders sit at 6.6-7.8 ns per tile. The decode CM shader body has no n_batch == 1 assumption - token is gl_WorkGroupID.y and it indexes q/w/mask/dst by it - so the old gate was an artefact of where it was written. gfx1151, kv=131584 (526k source tokens), us/run, shipped vs this: batch 2 2335.6 -> 419.0 (5.6x, was on the scalar path) batch 3 3492.3 -> 631.8 (5.5x, was on the scalar path) batch 4 3267.9 -> 840.8 (3.9x) batch 5 3465.6 -> 1119.4 (3.1x) <- DSpark n-max 4 verify shape batch 8 3704.9 -> 1764.1 (2.1x) batch 15 4467.9 -> 3231.6 (1.4x) Batch 16 and 32 are unchanged, which confirms the arms are isolated. PR #6's route is kept as the opt-out arm rather than deleted, and is promoted from opt-in to default-on so that one variable is enough to reach it: default decode CM for the whole 2-15 window ..._DECODE_CM_BATCH=0 small CM for 4-15, scalar for 2-3 (PR #6) ..._DECODE_CM_BATCH=0 SMALL_CM=0 scalar for 2-15 (pre-PR #6 baseline) Verified all three route as documented and pass 29/29. Keeping it is not just courtesy: the tile-count argument is hardware independent, but decode CM re-reads the K tile once per token and that part is bandwidth dependent, so the crossover need not sit in the same place on other devices, and these numbers are from one gfx1151 box. A (head, token) packing shader was also tried and is strictly worse - it reaches 4b tiles only when the batch divides 16, and it divides the workgroup count by the batch. Kept out of tree. Numerics: decode CM sums the 64 heads in tiles of 16 rather than one at a time, so f32 accumulation order differs from the small CM path. Cleared by KLD A/B on trunc10 (-c 8192, -b/-ub small so every indexer dispatch routes through the window, 8 chunks wikitext-2, same binary, only the env var differing): -ub 8 mean KLD 0.000000, max 6.0e-5, RMS dp 0.000%, same-top 99.994%, PPL 10507558.3156 identical in both arms -ub 5 mean KLD 0.000000, max 8.8e-4 but 99.9% 4.9e-5 (one tail event, no argmax flip), RMS dp 0.000%, same-top 100.000% Same class as PR #6's own measured numbers (max 5.5e-5, same-top 100.000%) and 45x tighter than the FA_WAVE32 change already shipped. Adds the eval coverage the 2-15 window never had (batches 4/8/15 at kv=256) and the perf grid these numbers came from. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 33 +++++++++++++++++++++++++--- tests/test-backend-ops.cpp | 10 ++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 8969f3328d40..a47792968ecf 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -13188,12 +13188,39 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const if (src0->type == GGML_TYPE_F32 && src1->type == GGML_TYPE_F16 && dst->type == GGML_TYPE_F32 && src0->ne[0] == 128 && src0->ne[1] == 64 && src1->ne[1] == 1 && ctx->device->pipeline_lightning_indexer_f16) { - if (ctx->device->pipeline_lightning_indexer_decode_cm_f16 && src0->ne[2] == 1) { + // Small-batch routing. Three arms, one env var each, most specific first: + // + // default decode CM for the whole 2-15 window + // ..._DECODE_CM_BATCH=0 small CM for 4-15, scalar for 2-3 (PR #6) + // ..._DECODE_CM_BATCH=0 SMALL_CM=0 scalar for 2-15 (pre-PR #6 baseline) + // + // The decode CM shader puts 16 HEADS in the coopmat N dimension and dispatches one + // workgroup per token, so it issues 4*n_batch tiles per 16 KV rows - the arithmetic + // minimum, since 64 heads fill its 16 columns exactly. The small CM shader puts 16 + // TOKENS in N and pays a flat 64 tiles no matter how small the batch. Routing the + // whole window to decode CM is 2.7x at batch 5 and 4.8x at batch 2-3 (526k source + // tokens, gfx1151); the shader body has no n_batch == 1 assumption, token is + // gl_WorkGroupID.y, so the old ne[2] == 1 gate was an artefact. + // + // That measurement only exists because of pepuscz's PR #6 and issue #10: their + // per-kernel table (43.8 us scalar vs 27.9 us small CM per 1k scanned rows at batch + // 5, the same ratio at every depth) is what showed the cost is a per-tile constant, + // which is what makes the tile count the thing to minimise. Their small CM route is + // kept as the opt-out arm rather than deleted: the tile-count argument is hardware + // independent, but decode CM re-reads the K tile once per token, and that part is + // bandwidth dependent, so the crossover need not sit here on other devices. + static const char * decode_cm_batch_env = getenv("GGML_VK_LIGHTNING_INDEXER_DECODE_CM_BATCH"); + static const bool decode_cm_batch_on = !decode_cm_batch_env || decode_cm_batch_env[0] != '0'; + const int64_t decode_cm_max = decode_cm_batch_on ? 15 : 1; + if (ctx->device->pipeline_lightning_indexer_decode_cm_f16 && src0->ne[2] <= decode_cm_max) { return ctx->device->pipeline_lightning_indexer_decode_cm_f16; } + // PR #6 as its author proposed it, now default-on so that the kill switch above is + // enough on its own to reach it. static const char * small_cm_env = getenv("GGML_VK_LIGHTNING_INDEXER_SMALL_CM"); - if (ctx->device->pipeline_lightning_indexer_cm_small_f16 && - src0->ne[2] >= 4 && src0->ne[2] < 16 && small_cm_env && small_cm_env[0] == '1') { + static const bool small_cm_on = !small_cm_env || small_cm_env[0] != '0'; + if (ctx->device->pipeline_lightning_indexer_cm_small_f16 && small_cm_on && + src0->ne[2] >= 4 && src0->ne[2] < 16) { return ctx->device->pipeline_lightning_indexer_cm_small_f16; } vk_pipeline cm = ctx->device->pipeline_lightning_indexer_cm_f16 ? diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index f00b69e24963..55d25ccf9c3c 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -11374,7 +11374,7 @@ static std::vector> make_test_cases_eval() { // lightning_indexer for (int kv : { 256 }) { - for (int bs : { 1, 512 }) { + for (int bs : { 1, 4, 8, 15, 512 }) { for (int nh : { 32, 64 }) { for (auto [ns, nm] : { std::pair{1, 1}, std::pair{4, 4}, std::pair{4, 1} }) { for (ggml_type type_K : {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}) { @@ -11947,6 +11947,14 @@ static std::vector> make_test_cases_perf() { test_cases.emplace_back(new test_lightning_indexer(128, 64, kv, 2048, 1, 1, GGML_TYPE_F16)); } + // DSpark verify-step indexer rows: batch 1-32 across the 4-15 small-CM routing window, + // at the compressed key counts a 128k/491k source context produces (source/4). + for (int kv : { 8704, 33280, 131584 }) { + for (int bs : { 1, 2, 3, 4, 5, 6, 8, 12, 15, 16, 32 }) { + test_cases.emplace_back(new test_lightning_indexer(128, 64, kv, bs, 1, 1, GGML_TYPE_F16)); + } + } + // sparse top-k FA at V4 decode/prefill shapes — the A/B instrument for the // gather-to-compact work (n_active = n_kv_raw + n_top_k stays fixed as kv grows). // nb 1/8 currently takes the DENSE path (the sparse shader gates on nb >= 64): From 4d83452a433a5da46b1faa0bda25d273b5cf1bc0 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Wed, 26 Aug 2026 07:15:56 +0000 Subject: [PATCH 130/212] vulkan: hoist the Lightning Indexer K fragments out of the CM head loop Same defect as the decode CM shader, larger blast radius. `k_sh[wave]` is written once above the head loop and never touched again, but `coopMatLoad` sits inside both the head_base and head_local loops, so each wave re-reads the same 8 MatrixA fragments N_HEAD times: 512 loads where 8 would do. Each wave64 fragment load moves 1 KiB of shared-memory traffic because the 16x16 f16 fragment is replicated 4x across the subgroup. Same loads of the same data feeding the same coopMatMulAdd order, so the output is bit-identical; the fragments live in registers (8 of them, 16 f16 per lane) instead of being re-fetched. This source compiles to both the prefill pipeline and the small-batch one, so both gain. gfx1151, test-backend-ops perf, us/run: prefill (lightning_indexer_cm_f16) kv 8704 nb 2048 27492.5 -> 18207.0 1.51x kv 33280 nb 2048 104101.9 -> 68947.8 1.51x kv 131584 nb 2048 421239.8 -> 281540.5 1.50x kv 131584 nb 16 3456.2 -> 2329.2 1.48x kv 131584 nb 32 6858.6 -> 4623.6 1.48x small batch (lightning_indexer_cm_small_f16) kv 131584 nb 5 3433.1 -> 2783.3 1.23x kv 131584 nb 8 3682.3 -> 3105.1 1.19x kv 131584 nb 15 4474.4 -> 4209.5 1.06x 1.48-1.54x on prefill at every depth and batch measured, flat. The small-batch variant gains less, which says the 1-wave configuration is bound by its Q staging and epilogue rather than by fragment traffic. No regression at any shape, so the 8 held fragments (64 VGPRs per lane) neither spill nor cost occupancy even in the prefill pipeline, which runs 512 threads at 62.7 KiB of shared memory and was already pinned to one workgroup per CU. LIGHTNING_INDEXER 29/29. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- .../vulkan-shaders/lightning_indexer_cm.comp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp index d4379e8899d0..6747feba505e 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp @@ -88,6 +88,15 @@ void main() { } barrier(); + // k_sh[wave] is written once above and never touched again, but coopMatLoad sits inside both + // the head_base and head_local loops, so the same 8 A fragments are re-read from shared + // memory N_HEAD times per wave (512 loads where 8 would do). Each wave64 fragment load moves + // 1 KiB of LDS traffic because the 16x16 f16 fragment is replicated 4x across the subgroup. + coopmat kmat_h[HEAD_SIZE / TILE]; + [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { + coopMatLoad(kmat_h[d / TILE], k_sh[wave], d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); + } + for (uint head_base = 0; head_base < N_HEAD; head_base += HEADS_PER_TILE) { for (uint idx = tid; idx < HEADS_PER_TILE * TILE * VEC_PER_HEAD; idx += gl_WorkGroupSize.x) { const uint head_local = idx / (TILE * VEC_PER_HEAD); @@ -113,13 +122,11 @@ void main() { [[unroll]] for (uint head_local = 0; head_local < HEADS_PER_TILE; ++head_local) { coopmat scores = coopmat(0.0); - coopmat kmat; coopmat qmat; [[unroll]] for (uint d = 0; d < HEAD_SIZE; d += TILE) { - coopMatLoad(kmat, k_sh[wave], d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); coopMatLoad(qmat, q_sh[head_local], d / 4, TILE_STRIDE, gl_CooperativeMatrixLayoutColumnMajor); - scores = coopMatMulAdd(kmat, qmat, scores); + scores = coopMatMulAdd(kmat_h[d / TILE], qmat, scores); } coopMatStore(scores, score_sh[wave], 0, SCORE_STRIDE, gl_CooperativeMatrixLayoutRowMajor); From 883b4d8fafa92676e2567e221e42c266f302ff53 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Wed, 26 Aug 2026 13:29:50 +0000 Subject: [PATCH 131/212] vulkan: support arbitrary Lightning Indexer head counts via specialization constant The indexer pipelines hardcoded nh=64 (DeepSeek-V4's geometry) in supports_op and as a shader constant, so any other head count fell to the CPU backend. Qwen3.8-Flash-Next (qwen4_exp, released today) carries the same indexer at nh=4. N_HEAD becomes specialization constant 1, one pipeline per supported head count (LI_NH_VALUES = {4, 32, 64}), selected by q->ne[1] at dispatch. It must be a specialization constant, not a push constant: the head loop's trip count has to stay visible to the compiler - as a push constant the loop cannot unroll and decode costs ~55% (measured, gfx1151, kv=131584, and why this commit is not the simpler design). nh=4 does not fill a 16-wide head tile, so the tail tile stages q as zeros (relu(0)*w = 0, padded heads drop out of the sum) and the weight read clamps its index. Both guards are gated on N_HEAD % TILE != 0, a specialization-constant expression, so at nh=64 they fold away at pipeline compile and the codegen is equivalent to the previous shader - measured, because relying on the compiler to range-prove head < N_HEAD instead still cost ~45%: kv=131584, us/run before after decode b1 226.8 219.6 decode b5 1058.8 1007.4 decode b15 3174.0 2966.8 prefill b16 2329.2 2230.2 prefill b2048 281540.5 281169.1 No regression at nh=64; the small decode improvement is within a cautious reading of run-to-run variance and is not claimed. test-backend-ops LIGHTNING_INDEXER: 44/44, up from 29/29 - the suite's existing nh=32 cases had been silently reporting "not supported" under the old gate and now run against the CPU reference. Head size stays pinned at 128: the coopmat tiling and f16vec4 staging both assume it, and every known indexer model (DSv4, GLM-DSA, Qwen4-exp) uses 128. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 96 ++++++++++++------- .../vulkan-shaders/lightning_indexer_cm.comp | 10 +- .../lightning_indexer_decode_cm.comp | 28 +++++- 3 files changed, 94 insertions(+), 40 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index a47792968ecf..acd8c03bf62b 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -799,6 +799,22 @@ 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(); } +// Indexer head counts we build kernels for: 4 = Qwen4-exp (qwen4_exp), 64 = DeepSeek-V4 and +// GLM-DSA, 32 kept because test-backend-ops exercises it. N_HEAD is a specialization constant, +// so each entry is a separately compiled kernel with the head loop fully unrolled - as a push +// constant the loop cannot unroll and decode costs ~55% more (measured, gfx1151, kv=131584). +static constexpr uint32_t LI_NH_VALUES[] = { 4, 32, 64 }; +#define LI_NH_COUNT (sizeof(LI_NH_VALUES) / sizeof(LI_NH_VALUES[0])) + +static int ggml_vk_li_nh_index(int64_t nh) { + for (size_t i = 0; i < LI_NH_COUNT; ++i) { + if ((int64_t) LI_NH_VALUES[i] == nh) { + return (int) i; + } + } + return -1; +} + struct vk_device_struct { std::recursive_mutex mutex; mutable std::shared_mutex pinned_memory_mutex; @@ -1105,10 +1121,11 @@ struct vk_device_struct { 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_lightning_indexer_f16; - vk_pipeline pipeline_lightning_indexer_cm_f16; - vk_pipeline pipeline_lightning_indexer_cm_small_f16; - vk_pipeline pipeline_lightning_indexer_decode_cm_f16; + // One pipeline per supported indexer head count; see LI_NH_VALUES. + vk_pipeline pipeline_lightning_indexer_f16[LI_NH_COUNT]; + vk_pipeline pipeline_lightning_indexer_cm_f16[LI_NH_COUNT]; + vk_pipeline pipeline_lightning_indexer_cm_small_f16[LI_NH_COUNT]; + vk_pipeline pipeline_lightning_indexer_decode_cm_f16[LI_NH_COUNT]; vk_pipeline pipeline_flash_attn_top_k_f16; vk_pipeline pipeline_flash_attn_top_k_cm_f16; vk_pipeline pipeline_flash_attn_gather_f16; @@ -6302,28 +6319,36 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { } if (device->subgroup_arithmetic && device->subgroup_size == 64) { - ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_f16, - "lightning_indexer_f16", lightning_indexer_f16_len, lightning_indexer_f16_data, "main", 5, - sizeof(vk_op_lightning_indexer_cm_push_constants), {8, 1, 1}, {device->subgroup_size}, 1, true, true, - device->subgroup_size); + for (size_t nhi = 0; nhi < LI_NH_COUNT; ++nhi) { + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_f16[nhi], + "lightning_indexer_f16", lightning_indexer_f16_len, lightning_indexer_f16_data, "main", 5, + sizeof(vk_op_lightning_indexer_cm_push_constants), {8, 1, 1}, {device->subgroup_size, LI_NH_VALUES[nhi]}, 1, true, true, + device->subgroup_size); + } #if defined(VK_KHR_cooperative_matrix) && defined(GGML_VULKAN_COOPMAT_GLSLC_SUPPORT) if (device->coopmat_support && device->coopmat_support_16x16x16_f32acc && device->subgroup_size_control) { - ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_cm_small_f16, - "lightning_indexer_cm_small_f16", lightning_indexer_cm_small_f16_len, lightning_indexer_cm_small_f16_data, "main", 5, - sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 16, 1}, {device->subgroup_size}, 1, true, true, - device->subgroup_size); + for (size_t nhi = 0; nhi < LI_NH_COUNT; ++nhi) { + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_cm_small_f16[nhi], + "lightning_indexer_cm_small_f16", lightning_indexer_cm_small_f16_len, lightning_indexer_cm_small_f16_data, "main", 5, + sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 16, 1}, {device->subgroup_size, LI_NH_VALUES[nhi]}, 1, true, true, + device->subgroup_size); + } if (device->properties.limits.maxComputeWorkGroupInvocations >= 512 && device->properties.limits.maxComputeWorkGroupSize[0] >= 512 && device->properties.limits.maxComputeSharedMemorySize >= 64 * 1024) { - ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_cm_f16, - "lightning_indexer_cm_f16", lightning_indexer_cm_f16_len, lightning_indexer_cm_f16_data, "main", 5, - sizeof(vk_op_lightning_indexer_cm_push_constants), {128, 16, 1}, {device->subgroup_size}, 1, true, true, + for (size_t nhi = 0; nhi < LI_NH_COUNT; ++nhi) { + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_cm_f16[nhi], + "lightning_indexer_cm_f16", lightning_indexer_cm_f16_len, lightning_indexer_cm_f16_data, "main", 5, + sizeof(vk_op_lightning_indexer_cm_push_constants), {128, 16, 1}, {device->subgroup_size, LI_NH_VALUES[nhi]}, 1, true, true, + device->subgroup_size); + } + } + for (size_t nhi = 0; nhi < LI_NH_COUNT; ++nhi) { + ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_decode_cm_f16[nhi], + "lightning_indexer_decode_cm_f16", lightning_indexer_decode_cm_f16_len, lightning_indexer_decode_cm_f16_data, "main", 5, + sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 1, 1}, {device->subgroup_size, LI_NH_VALUES[nhi]}, 1, true, true, device->subgroup_size); } - ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_decode_cm_f16, - "lightning_indexer_decode_cm_f16", lightning_indexer_decode_cm_f16_len, lightning_indexer_decode_cm_f16_data, "main", 5, - sizeof(vk_op_lightning_indexer_cm_push_constants), {16, 1, 1}, {device->subgroup_size}, 1, true, true, - device->subgroup_size); ggml_vk_create_pipeline(device, device->pipeline_flash_attn_top_k_cm_f16, "flash_attn_top_k_cm_f16", flash_attn_top_k_cm_f16_len, flash_attn_top_k_cm_f16_data, "main", 6, sizeof(vk_op_flash_attn_top_k_push_constants), {1, 1, 1}, {512, device->subgroup_size}, 1, true, true, @@ -13184,10 +13209,12 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const return nullptr; case GGML_OP_LIGHTNING_INDEXER: // fork fast path: f16 K on wave64 subgroup-arithmetic devices routes to the tuned - // scalar-64/CM kernels; anything else falls through to the generic pipeline table + // scalar-64/CM kernels (head counts in LI_NH_VALUES); anything else falls through to + // the generic pipeline table if (src0->type == GGML_TYPE_F32 && src1->type == GGML_TYPE_F16 && dst->type == GGML_TYPE_F32 && - src0->ne[0] == 128 && src0->ne[1] == 64 && src1->ne[1] == 1 && - ctx->device->pipeline_lightning_indexer_f16) { + src0->ne[0] == 128 && src1->ne[1] == 1 && + ggml_vk_li_nh_index(src0->ne[1]) >= 0 && ctx->device->pipeline_lightning_indexer_f16[0]) { + const int nhi = ggml_vk_li_nh_index(src0->ne[1]); // Small-batch routing. Three arms, one env var each, most specific first: // // default decode CM for the whole 2-15 window @@ -13212,20 +13239,20 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const static const char * decode_cm_batch_env = getenv("GGML_VK_LIGHTNING_INDEXER_DECODE_CM_BATCH"); static const bool decode_cm_batch_on = !decode_cm_batch_env || decode_cm_batch_env[0] != '0'; const int64_t decode_cm_max = decode_cm_batch_on ? 15 : 1; - if (ctx->device->pipeline_lightning_indexer_decode_cm_f16 && src0->ne[2] <= decode_cm_max) { - return ctx->device->pipeline_lightning_indexer_decode_cm_f16; + if (ctx->device->pipeline_lightning_indexer_decode_cm_f16[nhi] && src0->ne[2] <= decode_cm_max) { + return ctx->device->pipeline_lightning_indexer_decode_cm_f16[nhi]; } // PR #6 as its author proposed it, now default-on so that the kill switch above is // enough on its own to reach it. static const char * small_cm_env = getenv("GGML_VK_LIGHTNING_INDEXER_SMALL_CM"); static const bool small_cm_on = !small_cm_env || small_cm_env[0] != '0'; - if (ctx->device->pipeline_lightning_indexer_cm_small_f16 && small_cm_on && + if (ctx->device->pipeline_lightning_indexer_cm_small_f16[nhi] && small_cm_on && src0->ne[2] >= 4 && src0->ne[2] < 16) { - return ctx->device->pipeline_lightning_indexer_cm_small_f16; + return ctx->device->pipeline_lightning_indexer_cm_small_f16[nhi]; } - vk_pipeline cm = ctx->device->pipeline_lightning_indexer_cm_f16 ? - ctx->device->pipeline_lightning_indexer_cm_f16 : ctx->device->pipeline_lightning_indexer_cm_small_f16; - return cm && src0->ne[2] >= 16 ? cm : ctx->device->pipeline_lightning_indexer_f16; + vk_pipeline cm = ctx->device->pipeline_lightning_indexer_cm_f16[nhi] ? + ctx->device->pipeline_lightning_indexer_cm_f16[nhi] : ctx->device->pipeline_lightning_indexer_cm_small_f16[nhi]; + return cm && src0->ne[2] >= 16 ? cm : ctx->device->pipeline_lightning_indexer_f16[nhi]; } // 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)) { @@ -14319,9 +14346,14 @@ static void ggml_vk_lightning_indexer(ggml_backend_vk_context * ctx, vk_context& GGML_ASSERT(pipeline != nullptr); // the fork's wave64 f16 kernels take their own push-constant layout - if (pipeline == ctx->device->pipeline_lightning_indexer_f16 || - pipeline == ctx->device->pipeline_lightning_indexer_cm_f16 || - pipeline == ctx->device->pipeline_lightning_indexer_decode_cm_f16) { + bool fork_li = false; + for (size_t nhi = 0; nhi < LI_NH_COUNT && !fork_li; ++nhi) { + fork_li = pipeline == ctx->device->pipeline_lightning_indexer_f16[nhi] || + pipeline == ctx->device->pipeline_lightning_indexer_cm_f16[nhi] || + pipeline == ctx->device->pipeline_lightning_indexer_cm_small_f16[nhi] || + pipeline == ctx->device->pipeline_lightning_indexer_decode_cm_f16[nhi]; + } + if (fork_li) { ggml_vk_lightning_indexer_cm(ctx, subctx, dst, pipeline); return; } diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp index 6747feba505e..a2004bd93d7f 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_cm.comp @@ -8,6 +8,10 @@ #extension GL_KHR_shader_subgroup_basic : require layout(constant_id = 0) const uint SUBGROUP_SIZE = 64; +// Head count is a SPECIALIZATION constant, not a push constant: the head loop's trip +// count must stay visible to the compiler. As a push constant it cannot unroll and +// decode costs ~55% more (measured, gfx1151, kv=131584). +layout(constant_id = 1) const uint N_HEAD = 64; #if N_WAVES == 1 layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; #else @@ -40,7 +44,6 @@ layout(push_constant) uniform Parameters { const uint TILE = 16; const uint HEAD_SIZE = 128; -const uint N_HEAD = 64; const uint VEC_PER_HEAD = HEAD_SIZE / 4; const uint TILE_STRIDE = VEC_PER_HEAD + 2; const uint SCORE_STRIDE = TILE / 4 + 1; @@ -105,7 +108,7 @@ void main() { const uint d4 = head_idx % VEC_PER_HEAD; const uint token = token_base + token_local; f16vec4 value = f16vec4(0.0); - if (token < p.n_batch) { + if (token < p.n_batch && (N_HEAD % HEADS_PER_TILE == 0 || head_base + head_local < N_HEAD)) { const uint offset = stream * p.nbq3 + token * p.nbq2 + (head_base + head_local) * p.nbq1 + d4 * 4; value = f16vec4(data_q[offset], data_q[offset + 1], data_q[offset + 2], data_q[offset + 3]); } @@ -115,7 +118,8 @@ void main() { const uint head_local = tid / TILE; const uint token_local = tid % TILE; const uint token = token_base + token_local; - weight_sh[head_local][token_local] = token < p.n_batch ? data_w[stream * p.nbw3 + token * p.nbw1 + head_base + head_local] : 0.0; + weight_sh[head_local][token_local] = (token < p.n_batch && (N_HEAD % HEADS_PER_TILE == 0 || head_base + head_local < N_HEAD)) + ? data_w[stream * p.nbw3 + token * p.nbw1 + head_base + head_local] : 0.0; } barrier(); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp index 24d1b98d571c..5d32e51361b3 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_decode_cm.comp @@ -7,6 +7,10 @@ #extension GL_KHR_memory_scope_semantics : require layout(constant_id = 0) const uint SUBGROUP_SIZE = 64; +// Head count is a SPECIALIZATION constant, not a push constant: the head loop's trip +// count must stay visible to the compiler. As a push constant it cannot unroll and +// decode costs ~55% more (measured, gfx1151, kv=131584). +layout(constant_id = 1) const uint N_HEAD = 64; layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; layout(binding = 0) readonly buffer QBuf { float data_q[]; }; @@ -35,7 +39,6 @@ layout(push_constant) uniform Parameters { const uint TILE = 16; const uint HEAD_SIZE = 128; -const uint N_HEAD = 64; const uint VEC_PER_HEAD = HEAD_SIZE / 4; const uint TILE_STRIDE = VEC_PER_HEAD + 2; const uint SCORE_STRIDE = TILE / 4 + 1; @@ -80,9 +83,19 @@ void main() { const uint head_local = idx / VEC_PER_HEAD; const uint d4 = idx % VEC_PER_HEAD; const uint head = head_base + head_local; - const uint offset = stream * p.nbq3 + token * p.nbq2 + head * p.nbq1 + d4 * 4; - q_sh[head_local * TILE_STRIDE + d4] = - f16vec4(data_q[offset], data_q[offset + 1], data_q[offset + 2], data_q[offset + 3]); + // n_head need not be a multiple of TILE (Qwen4-exp indexers are nh=4), so the tail + // tile stages zeros rather than reading past the end of Q. Zero q gives score 0, + // and relu(0) * w = 0, so the padded heads drop out of the sum on their own. + // The guard exists only when N_HEAD is not tile-aligned (nh=4). The condition is a + // specialization-constant expression, so at nh=64 it folds to true and this compiles + // to the original unguarded load - measured: relying on the compiler to range-prove + // head < N_HEAD instead costs ~45% at nh=64. + f16vec4 value = f16vec4(0.0); + if (N_HEAD % TILE == 0 || head < N_HEAD) { + const uint offset = stream * p.nbq3 + token * p.nbq2 + head * p.nbq1 + d4 * 4; + value = f16vec4(data_q[offset], data_q[offset + 1], data_q[offset + 2], data_q[offset + 3]); + } + q_sh[head_local * TILE_STRIDE + d4] = value; } barrier(); @@ -100,8 +113,13 @@ void main() { if (tid < TILE && kv_base + tid < p.n_kv) { [[unroll]] for (uint head_local = 0; head_local < TILE; ++head_local) { + // Padded heads staged q as zero, so their score is 0 and the term dies in the + // multiply. Only the weight READ needs bounding, and clamping the index does that + // without a branch - a dynamic break here forces the loop rolled and costs ~60%. + const uint head = N_HEAD % TILE == 0 ? head_base + head_local + : min(head_base + head_local, N_HEAD - 1); const float score = score_sh[tid * SCORE_STRIDE + head_local / 4][head_local % 4]; - const float weight = data_w[stream * p.nbw3 + token * p.nbw1 + head_base + head_local]; + const float weight = data_w[stream * p.nbw3 + token * p.nbw1 + head]; total += max(score, 0.0) * weight; } } From 808ce1c87b3fa1ec942233cbc3d204a48e36a3f4 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 30 Aug 2026 09:02:36 +0000 Subject: [PATCH 132/212] vulkan: N_HEAD spec constant for the scalar-64 lightning indexer The scalar64 shader kept its hardcoded 64-head loop when the pipelines went per-head-count; nh 4 and 32 would have run with the wrong trip count. Assisted-by: Claude Fable 5 --- .../vulkan-shaders/lightning_indexer_scalar64.comp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_scalar64.comp b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_scalar64.comp index 693bb3ece8e8..83fd923fe738 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_scalar64.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/lightning_indexer_scalar64.comp @@ -7,6 +7,10 @@ #extension GL_KHR_shader_subgroup_basic : require layout(constant_id = 0) const uint SUBGROUP_SIZE = 64; +// Head count is a SPECIALIZATION constant, not a push constant: the head loop's trip +// count must stay visible to the compiler. As a push constant it cannot unroll and +// decode costs ~55% more (measured, gfx1151, kv=131584). +layout(constant_id = 1) const uint N_HEAD = 64; layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; layout(binding = 0) readonly buffer QBuf { float data_q[]; }; @@ -34,7 +38,6 @@ layout(push_constant) uniform Parameters { } p; const uint K_PER_GROUP = 8; -const uint N_HEAD = 64; void main() { const uint lane = gl_SubgroupInvocationID; From 1007fdc4ada16480c06fc747df3aa28f714d35f3 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 30 Jul 2026 01:21:49 +0000 Subject: [PATCH 133/212] vulkan: hoist the coopmat1 FA P-fragment load out of the hsv_tile loop The GEMM2 P load depends only on bc_chunk, but sat inside the hsv_tile loop, so all four fragments were re-read from shared memory once per tile (2 tiles at HSV=128, 4 at HSV=256). Load them once into a coopmat array before the loop. Psh is not written again until the next KV block, so the fragments stay valid across the barriers inside it. Measured on gfx1151 (RADV), Qwen3-Coder-30B-A3B, pp2048 f16 KV: d8192 +6.9%, d16384 +8.1%, d32768 +9.2%. VGPRs and LDS unchanged, 0 spilled. FLASH_ATTN_EXT suite 5105/5105. Assisted-by: Claude Fable 5 --- .../ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp index 38ae42c81f0c..695d459d0a4b 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp @@ -423,6 +423,13 @@ void main() { const uint num_hsv_tiles = (HSV + MatBc * row_split - 1) / (MatBc * row_split); // round up + // Psh is not written again until the next KV block, so the P fragments are the same + // for every hsv_tile. Load them once instead of re-reading LDS per tile. + coopmat PMat[Bc / MatBc]; + [[unroll]] for (uint32_t bc_chunk = 0; bc_chunk < Bc / MatBc; ++bc_chunk) { + coopMatLoad(PMat[bc_chunk], Psh, bc_chunk * MatBc * psh_stride, psh_stride, gl_CooperativeMatrixLayoutColumnMajor); + } + // Each subgroup handles HSV/4 columns [[unroll]] for (uint32_t hsv_tile = 0; hsv_tile < num_hsv_tiles; ++hsv_tile) { const uint hsv_offset = (hsv_tile * row_split + gl_SubgroupID) * 16; @@ -476,8 +483,6 @@ void main() { if (hsv_offset < HSV_pad) { [[unroll]] for (uint32_t bc_chunk = 0; bc_chunk < Bc / MatBc; ++bc_chunk) { - coopMatLoad(KMat, Psh, bc_chunk * MatBc * psh_stride, psh_stride, gl_CooperativeMatrixLayoutColumnMajor); - if (SHMEM_STAGING == 0) { if (!USE_DECODE_V && !KV_bounds_check) { // F16/BF16 values can be loaded directly from global memory @@ -493,7 +498,7 @@ void main() { coopMatLoad(QMat, kvsh, v_tile_offset, kvsh_stride, gl_CooperativeMatrixLayoutRowMajor); } - PVMat = coopMatMulAdd(KMat, QMat, PVMat); + PVMat = coopMatMulAdd(PMat[bc_chunk], QMat, PVMat); } // Store PVMat to pvsh and load into Of From 11eaefa454042843b86d739fa8c090c74ad83ee3 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 30 Jul 2026 01:23:34 +0000 Subject: [PATCH 134/212] vulkan: store coopmat1 FA Psh query-major so the GEMM2 A load vectorizes Psh held P as [kv][query]. The GEMM2 UseA load therefore had to request ColumnMajor, and RADV only attaches an alignment hint to the internal column-major case, which for UseA means the RowMajor request. The load was emitted as 16 separate 16-bit shared reads per fragment. Store P as [query][kv] instead and request RowMajor. The producer now writes four scalar components rather than one vec4; those go to disjoint bytes, so there is no read-modify-write and no partially written vec4. Also updates the host shared-memory estimator, which mirrored the old stride and would otherwise disagree with the shader. Perf-neutral on gfx1151 (within 1% at hd128 and hd256), but it shrinks Psh: LDS 16384 -> 15360 B and code size 13896 -> 13596 at hd128, VGPRs unchanged at 96 with 0 spilled. Kept because it is free and removes a scalar shared-memory access pattern. FLASH_ATTN_EXT suite 5105/5105. Assisted-by: Claude Fable 5 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 4 ++-- .../vulkan-shaders/flash_attn_cm1.comp | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index acd8c03bf62b..869b9f655b79 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -11393,8 +11393,8 @@ static bool ggml_vk_flash_attn_coopmat_shmem_support(const vk_device& device, co const uint32_t qstride = hsk_pad / 4 + 2; const uint32_t Qf = Br * qstride * f16vec4; - const uint32_t psh_stride = Br / 4 + 2; - const uint32_t Psh = Bc * psh_stride * f16vec4; + const uint32_t psh_stride = Bc / 4 + 2; + const uint32_t Psh = Br * psh_stride * f16vec4; const uint32_t sfshstride = (hsk <= 128) ? (Br + 8) : Br; const uint32_t sfsh = Bc * sfshstride * acctype; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp index 695d459d0a4b..195901a7d90d 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp @@ -44,8 +44,11 @@ shared float tmpsh[row_split]; const uint32_t qstride = HSK_pad / 4 + 2; shared FLOAT_TYPEV4 Qf[Br * qstride]; -const uint psh_stride = Br / 4 + 2; -shared FLOAT_TYPEV4 Psh[Bc * psh_stride]; +// P is stored query-major with KV contiguous so the GEMM2 UseA load can be RowMajor: +// RADV flips the requested layout for UseA, and only the resulting internal column-major +// case gets an alignment hint, which is what lets the 16 element loads vectorize. +const uint psh_stride = Bc / 4 + 2; +shared FLOAT_TYPEV4 Psh[Br * psh_stride]; // Avoid padding for hsk==256 to make it fit in 48KB shmem. const uint32_t sfshstride = (HSK <= 128) ? (Br / 4 + 2) : Br / 4; @@ -382,15 +385,19 @@ void main() { [[unroll]] for (uint32_t r = 0; r < rows_per_thread; r += 4) { const uint row = tile_row(r); + const uint pcol_vec = col / 4; + const uint pcol_comp = col % 4; if (KV_bounds_check && j * Bc + col >= KV) { - Psh[col * psh_stride + row / 4] = FLOAT_TYPEV4(0.0f); + [[unroll]] for (uint32_t vec_idx = 0; vec_idx < 4; ++vec_idx) { + Psh[(row + vec_idx) * psh_stride + pcol_vec][pcol_comp] = FLOAT_TYPE(0.0f); + } } else { const vec4 mfvec = vec4(Mf[r], Mf[r + 1], Mf[r + 2], Mf[r + 3]); const FLOAT_TYPEV4 Pf = FLOAT_TYPEV4(exp(vec4(sfsh[row / 4 + col * sfshstride]) - mfvec)); [[unroll]] for (uint32_t vec_idx = 0; vec_idx < 4; ++vec_idx) { Lf[r + vec_idx] += Pf[vec_idx]; + Psh[(row + vec_idx) * psh_stride + pcol_vec][pcol_comp] = Pf[vec_idx]; } - Psh[col * psh_stride + row / 4] = Pf; } } } @@ -427,7 +434,7 @@ void main() { // for every hsv_tile. Load them once instead of re-reading LDS per tile. coopmat PMat[Bc / MatBc]; [[unroll]] for (uint32_t bc_chunk = 0; bc_chunk < Bc / MatBc; ++bc_chunk) { - coopMatLoad(PMat[bc_chunk], Psh, bc_chunk * MatBc * psh_stride, psh_stride, gl_CooperativeMatrixLayoutColumnMajor); + coopMatLoad(PMat[bc_chunk], Psh, bc_chunk * (MatBc / 4), psh_stride, gl_CooperativeMatrixLayoutRowMajor); } // Each subgroup handles HSV/4 columns From bc2b9ad55236fde185f5e9a2f3e25cecdf37c50f Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 30 Jul 2026 01:25:28 +0000 Subject: [PATCH 135/212] vulkan: pin a 32-wide subgroup for coopmat1 FA where narrowing is free get_fa_tuning_params_coopmat1 took device->subgroup_size unconditionally, so on a 64-wide device the coopmat1 FA path always ran wave64. The sibling scalar path already does AMD-specific wave selection; the coopmat1 path never got the equivalent. Narrowing is free exactly when it does not add an iteration to the O-accumulation loop, which runs ceil((HSV/4) / threads_per_rowgroup) per row. On a 64-wide device the test reduces to hsv <= 128. Above it the narrow subgroup issues 1.5x to 1.8x the instructions for the same SIMD passes and hd256 measures 6 to 18 percent slower, so the rule declines. Gated behind GGML_VK_FA_WAVE32 (=1 rule, =2 forces the pin regardless of head size, diagnostic only). Off by default. Measured on gfx1151 (RADV), model-level pp2048, Qwen3-Coder-30B-A3B: d0 +2.5%, d8192 +8.4%, d16384 +10.1%, d32768 +11.3%. Op-level across head sizes, largest where wave64 wastes the most lanes: hsv=64 +12.3%, hsv=96 +7.4%, hsv=128 +6.5%, hsv=256 correctly declined. Full test-backend-ops suite 15884/15884 at =0, =1 and =2; the pin was confirmed to engage in-band via pipeline VGPR statistics rather than inferred from the env var being set. Assisted-by: Claude Fable 5 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 869b9f655b79..e4796f12a78f 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4005,8 +4005,41 @@ static vk_fa_tuning_params get_fa_tuning_params_coopmat1(const vk_device& device result.block_cols = coopmat_block_cols * num_subgroups; result.row_split = num_subgroups; result.subgroup_size = device->subgroup_size; + + // Pin a 32-wide subgroup only where narrowing is free. The shader derives cols_per_iter, + // threads_per_rowgroup and every strided load loop from gl_WorkGroupSize.x, and + // workgroup_size is num_subgroups * subgroup_size, so threads_per_rowgroup always equals the + // real subgroup size. Halving the subgroup halves the workgroup, and the per-lane O state + // grows as d_per_thread = ceil((HSV/4) / threads_per_rowgroup). Pin only when that count is + // unchanged. Above that point the narrow subgroup issues roughly 1.5x to 1.8x the + // instructions for the same number of SIMD passes, which loses on an issue-bound kernel: + // hd256 measures 6 to 18 percent slower. The test depends on HSV only; HSK does not enter + // d_per_thread. On a 64-wide device it reduces exactly to hsv <= 128. + // =1 applies the rule; =2 forces the pin regardless of head size, for measuring the + // configurations the rule rejects. Diagnostic only. + static const int fa_wave32 = [] { + const char * e = getenv("GGML_VK_FA_WAVE32"); + return e ? atoi(e) : 0; + }(); + if (fa_wave32 != 0 && + device->subgroup_size_control && + 32 < device->subgroup_size && // narrow only, never widen + device->subgroup_min_size <= 32 && 32 <= device->subgroup_max_size && + (result.block_cols % 32) == 0 && // cols_per_thread stays >= 1 + (result.block_cols * result.block_rows / 4) >= num_subgroups * 32 && // mask_cache != 0 + (fa_wave32 == 2 || + CEIL_DIV(hsv / 4, 32u) == CEIL_DIV(hsv / 4, device->subgroup_size))) { + result.subgroup_size = 32; + } + result.workgroup_size = num_subgroups * result.subgroup_size; + // threads_per_rowgroup == the real subgroup size is load-bearing in three places: + // the subgroupMax row reduction, the subgroupAdd of Lf, and tmpsh[gl_SubgroupID], which is + // sized by row_split and would be written out of bounds if gl_NumSubgroups exceeded it. + GGML_ASSERT(result.workgroup_size == result.row_split * result.subgroup_size); + GGML_ASSERT(result.block_cols % result.subgroup_size == 0); + const uint32_t D_lsb = D ^ (D & (D-1)); // extract lowest set bit result.d_split = std::min(std::min(result.subgroup_size, 8u), D_lsb / 4); From 1556cd12dc1d2e148700e01d9f494b949608f1da Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 30 Aug 2026 10:17:20 +0000 Subject: [PATCH 136/212] vulkan: enable the coopmat1 FA wave32 narrowing rule by default Split from the fork's flag-flip commit (f7d804ee7): GGML_VK_FA_WAVE32 now defaults to 1 (apply the HSV-based narrowing rule); =0 opts out and =2 keeps its diagnostic force meaning. The subgroup_size_control device guard is unchanged, so devices without a 32-wide subgroup mode are unaffected. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index e4796f12a78f..ee35ba7c5a99 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4015,11 +4015,11 @@ static vk_fa_tuning_params get_fa_tuning_params_coopmat1(const vk_device& device // instructions for the same number of SIMD passes, which loses on an issue-bound kernel: // hd256 measures 6 to 18 percent slower. The test depends on HSV only; HSK does not enter // d_per_thread. On a 64-wide device it reduces exactly to hsv <= 128. - // =1 applies the rule; =2 forces the pin regardless of head size, for measuring the - // configurations the rule rejects. Diagnostic only. + // On by default (=1, applies the rule); =0 disables. =2 forces the pin regardless of + // head size, for measuring the configurations the rule rejects. Diagnostic only. static const int fa_wave32 = [] { const char * e = getenv("GGML_VK_FA_WAVE32"); - return e ? atoi(e) : 0; + return e ? atoi(e) : 1; }(); if (fa_wave32 != 0 && device->subgroup_size_control && From e75a22bf1bca22f64b458e0b05dd8eb9a0cbf25e Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 14 Jul 2026 05:53:12 +0000 Subject: [PATCH 137/212] vulkan: mul_mat_id per-expert-n tile selection (Stage 2a, env-gated) Select the matmul_id tile by expected per-expert token count (nei1*nei0/n_expert) instead of aggregate nei1, which always picked the widest tile and left most N-lanes empty at MoE prefill (~16 rows/expert vs BN=64). Gated on GGML_VK_MMID_SMALLN=1, default off pending cross-model validation. Was null (+0.7%) standalone pre-row-lists: smaller tiles meant more workgroups each re-paying the per-WG id scan. With the Stage 1 row-list prepass the scan is gone and the occupancy win materializes. Strix Halo (RADV gfx1151), Qwen3.6-35B-A3B UD-Q5_K_XL, clean window, pp512 4-way (rowlists x smalln): 914.7 / 937.9 / 1005.3 / 1063.6 t/s (combined +16.3% vs baseline). MUL_MAT_ID q5_K 2.33->4.43 TFLOPS (1.91x), q6_K 1.99->3.57 (1.79x) cumulative. Hot pipelines verified via probe: matmul_id_*_f32 (y_f32 path, quantize_y does not engage for these quants); smalln flips tile _m -> _s. 790/790 MUL_MAT_ID both rowlists configs. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index ee35ba7c5a99..74372580bc3b 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -10844,7 +10844,18 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& const ggml_type effective_src1_type = quantize_y ? GGML_TYPE_Q8_1 : (y_f32_kernel ? GGML_TYPE_F32 : src1->type); - const uint32_t kpad = quantize_y ? 0 : ggml_vk_align_size(ne10, ggml_vk_guess_matmul_id_pipeline_align(ctx, mmp, ne01, nei1, qx_needs_dequant ? f16_type : src0->type, effective_src1_type)); + // EXPERIMENT (GGML_VK_MMID_SMALLN=1): select the matmul tile by the EXPECTED PER-EXPERT + // token count rather than the whole batch. With E experts and nei0 active per token, each + // expert sees ~nei1*nei0/E rows; selecting by aggregate nei1 always picks the widest tile + // and leaves most N-lanes empty at MoE prefill (measured: MUL_MAT_ID at ~20% of dense + // matmul efficiency, ~78% of MoE prefill time on Qwen3.6-35B-A3B). + uint32_t n_for_tile = (uint32_t)nei1; + static const char * mmid_smalln_env = getenv("GGML_VK_MMID_SMALLN"); + if (mmid_smalln_env && atoi(mmid_smalln_env) != 0 && ne02 > 1) { + n_for_tile = std::max(1u, (uint32_t)((nei1 * nei0 + ne02 - 1) / ne02)); + } + + const uint32_t kpad = quantize_y ? 0 : ggml_vk_align_size(ne10, ggml_vk_guess_matmul_id_pipeline_align(ctx, mmp, ne01, n_for_tile, qx_needs_dequant ? f16_type : src0->type, effective_src1_type)); // Coopmat2 MUL_MAT_ID BK specialization constants in ggml_vk_load_shaders are at most 64. const uint32_t y_staged_row_stride = ctx->device->coopmat2 && !quantize_y ? ggml_vk_align_size(ne10, 64) : ne10; const bool y_needs_k_padding = ne10 != y_staged_row_stride; @@ -10853,10 +10864,9 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& // Not implemented GGML_ASSERT(y_needs_reformat || !qy_needs_dequant); // NOLINT - const bool aligned = !quantize_y && ne10 == kpad && ne01 > 8 && nei1 > 8; - vk_pipeline pipeline = ggml_vk_guess_matmul_id_pipeline(ctx, mmp, ne01, nei1, aligned, qx_needs_dequant ? f16_type : src0->type, effective_src1_type); + vk_pipeline pipeline = ggml_vk_guess_matmul_id_pipeline(ctx, mmp, ne01, n_for_tile, aligned, qx_needs_dequant ? f16_type : src0->type, effective_src1_type); if (ggml_nbytes(src0) > ctx->device->properties.limits.maxStorageBufferRange) { pipeline = ggml_vk_get_64b_indexing_pipeline(ctx, pipeline); From 33a08e893d996ffd1c75883a37edb4ecda13a9e9 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 14 Jul 2026 06:04:18 +0000 Subject: [PATCH 138/212] vulkan: mul_mat_id small-tile shape probes (env-gated) Two env-gated overrides of the mmid small-tile config (KHR coopmat branch only, scoped to the mul_mat_id quant pipelines; dense untouched): - GGML_VK_MMID_TILE16: BN/WN 32->16. NEGATIVE (-3.8% e2e): experts with n_e>16 split into two column tiles and re-stream their full weight matrix; A-traffic scales with sum(ceil(n_e/BN)), so BN must not drop below the mean per-expert n. Kept as a documented dead end. - GGML_VK_MMID_BM64: BM 32->64, BLOCK_SIZE 64->128 (two warps). +1.3% e2e on top of rowlists+smalln: same A-traffic, half the ir-tiles so half the per-expert B re-reads, larger WGs hide latency. Strix Halo, Qwen3.6-35B-A3B UD-Q5_K_XL pp512, clean window, all on top of RL+SN control 1065.4 +/- 3.3: BM64 1078.9 +/- 3.5, TILE16 1024.6, BM64+TILE16 1007.5. Cumulative vs pre-Stage-1 baseline: 914.7 -> 1078.9 (+17.9%). 790/790 MUL_MAT_ID for all configs. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 74372580bc3b..f1211d67a257 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -5107,6 +5107,34 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { } #endif + // EXPERIMENT (GGML_VK_MMID_TILE16=1): narrow the matmul_id small tile to BN=16. + // MoE prefill leaves ~nei1*nei0/n_expert rows per expert (~16 at ub512/256E/8a), + // so even the 32-wide small tile runs half empty. Only meaningful stacked on + // GGML_VK_MMID_SMALLN=1 (routes mmid to the small tile) + the row-list prepass. + // Shadows the s-tile config for the mmid quant pipelines only; dense unaffected. + auto s_warptile_mmq_id16 = s_warptile_mmq; + auto s_mmq_wg_denoms_id16 = s_mmq_wg_denoms; + { + const char * tile16_env = getenv("GGML_VK_MMID_TILE16"); + if (tile16_env && atoi(tile16_env) != 0) { + s_warptile_mmq_id16[2] = 16; // BN + s_warptile_mmq_id16[5] = 16; // WN + s_mmq_wg_denoms_id16[1] = 16; + } + // GGML_VK_MMID_BM64=1: taller small tile (BM 32->64, two warps). Same + // A-traffic (ic-tile count unchanged), halves per-expert B re-reads + // (ir-tile count), larger WGs to hide latency. + const char * bm64_env = getenv("GGML_VK_MMID_BM64"); + if (bm64_env && atoi(bm64_env) != 0) { + s_warptile_mmq_id16[0] = 2 * mul_mat_subgroup_size; // BLOCK_SIZE + s_warptile_mmq_id16[1] = 64; // BM + s_mmq_wg_denoms_id16[0] = 64; + } + } + { + const auto &s_warptile_mmq = s_warptile_mmq_id16; + const auto &s_mmq_wg_denoms = s_mmq_wg_denoms_id16; + CREATE_MM2(GGML_TYPE_Q1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q1_0], matmul_id_subgroup_q1_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); CREATE_MM2(GGML_TYPE_Q2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_0], matmul_id_subgroup_q2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); CREATE_MM2(GGML_TYPE_Q4_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0], matmul_id_subgroup_q4_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); From fe10c7f2ae9f6a7fff98ae38e456b226fedcf28a Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 14 Jul 2026 08:42:07 +0000 Subject: [PATCH 139/212] vulkan: mul_mat_id taller medium tile probe (GGML_VK_MMID_M128, env-gated) Extends the BM64 idea to the medium tile: GGML_VK_MMID_M128=1 raises the mmid m-tile to BM=128 / BLOCK_SIZE=4*subgroup (four warps), same A-traffic, half the ir-tiles and per-expert B re-reads. The medium tile is what the per-expert-n heuristic selects at n~64 (e.g. ub2048 on 256-expert/top-8 models, or ub1024 at 128 experts). Strix Halo, Qwen3.6-35B-A3B UD-Q5_K_XL pp2048, drain-verified window: ub2048 stack 1039.9 +/- 1.1 -> +M128 1091.6 +/- 2.6 (+5.0%); inert in the s-tile regime (ub1024 1137.2 vs 1147.9 ref). ub1024 remains the throughput sweet spot for this model. 790/790 MUL_MAT_ID. Also measured this session, NOT kept: caching counts+row lists across the three expert matmuls of a layer (they share one ids tensor) was correctness-clean but perf-null (1066.3 vs 1070.9 baseline) - the prepass dispatches and barriers are already free on this queue; reverted rather than carry the invalidation surface. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index f1211d67a257..79797c69525b 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -5114,6 +5114,8 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { // Shadows the s-tile config for the mmid quant pipelines only; dense unaffected. auto s_warptile_mmq_id16 = s_warptile_mmq; auto s_mmq_wg_denoms_id16 = s_mmq_wg_denoms; + auto m_warptile_mmq_id128 = m_warptile_mmq; + auto m_mmq_wg_denoms_id128 = m_mmq_wg_denoms; { const char * tile16_env = getenv("GGML_VK_MMID_TILE16"); if (tile16_env && atoi(tile16_env) != 0) { @@ -5130,10 +5132,20 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { s_warptile_mmq_id16[1] = 64; // BM s_mmq_wg_denoms_id16[0] = 64; } + // GGML_VK_MMID_M128=1: same idea for the medium tile (BM 64->128, four + // warps) — the tile the per-expert-n heuristic picks at n~64 (e.g. ub2048). + const char * m128_env = getenv("GGML_VK_MMID_M128"); + if (m128_env && atoi(m128_env) != 0) { + m_warptile_mmq_id128[0] = 4 * mul_mat_subgroup_size; // BLOCK_SIZE + m_warptile_mmq_id128[1] = 128; // BM + m_mmq_wg_denoms_id128[0] = 128; + } } { const auto &s_warptile_mmq = s_warptile_mmq_id16; const auto &s_mmq_wg_denoms = s_mmq_wg_denoms_id16; + const auto &m_warptile_mmq = m_warptile_mmq_id128; + const auto &m_mmq_wg_denoms = m_mmq_wg_denoms_id128; CREATE_MM2(GGML_TYPE_Q1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q1_0], matmul_id_subgroup_q1_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); CREATE_MM2(GGML_TYPE_Q2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_0], matmul_id_subgroup_q2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); From 07e35febaf3182611c849e980e96348d5297b1d8 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 14 Jul 2026 11:20:31 +0000 Subject: [PATCH 140/212] vulkan: mmid wave32 probe (GGML_VK_MMID_WAVE32, env-gated) Force required subgroup size 32 on the KHR-coopmat mmid quant pipelines (pipeline_dequant_mul_mat_mat_id[*] only; dense untouched). Hypothesis: RDNA3.5 WMMA is wave32-native, so RADV may lower KHR_coopmat better at wave32 than at the reported default of 64. Mechanics: the cm1 path in mul_mm.comp derives its warp grid from the real subgroup (warp_i = gl_SubgroupID, tiw = gl_SubgroupInvocationID) while NUM_WARPS = BLOCK_SIZE/WARP (spec constants) sizes coopmat_stage[] and ballots_sh[] and warp_r/warp_c assume NUM_WARPS == (BM/WM)*(BN/WN). Forcing sg32 with WARP=64 would over-run those shared arrays and leave warp_c outside the tile, so the gate (a) sets WARP=32 in shadowed copies of the s/m/l mmid warptiles (composes after the BM64/M128 shadows) and (b) halves WM (or WN, keeping WM>=TM, WN>=TN) until the doubled subgroup count exactly tiles BM x BN again, asserting both invariants. BLOCK_SIZE is kept, so workgroup shape, load loops and shmem match the wave64 stack; per-lane accumulator footprint is also unchanged (half the lanes per subgroup, half the (WM/TM)*(WN/TN) fragments). The required size is passed via a scoped CREATE_MM redefinition adding a trailing required_subgroup_size arg (fp16-branch pattern), gated on subgroup_size_control covering 32 since ggml_vk_create_pipeline_func silently drops the required size otherwise. Correctness (test-backend-ops -o MUL_MAT_ID -b Vulkan0): 790/790 plain, 790/790 WAVE32=1, 790/790 WAVE32+SMALLN+BM64. Perf, Qwen3.6-35B-A3B-UD-Q5_K_XL, fa=1 b/ub=512 ctk/ctv=q8_0 pp512 r=3, atomic window, canary clean: stack (SMALLN+BM64), wave64 default: 1076.61 +/- 3.71 t/s stack + WAVE32: 1106.73 +/- 2.52 t/s (+2.8%) Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 79797c69525b..e8bb9555ed00 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -5116,6 +5116,8 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { auto s_mmq_wg_denoms_id16 = s_mmq_wg_denoms; auto m_warptile_mmq_id128 = m_warptile_mmq; auto m_mmq_wg_denoms_id128 = m_mmq_wg_denoms; + auto l_warptile_mmq_idw = l_warptile_mmq; + uint32_t mmid_req_sgs = 0; { const char * tile16_env = getenv("GGML_VK_MMID_TILE16"); if (tile16_env && atoi(tile16_env) != 0) { @@ -5140,12 +5142,68 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { m_warptile_mmq_id128[1] = 128; // BM m_mmq_wg_denoms_id128[0] = 128; } + // GGML_VK_MMID_WAVE32=1: force required subgroup size 32 on the mmid + // quant coopmat pipelines (RDNA3.x WMMA is wave32-native; probe whether + // RADV lowers KHR_coopmat better at wave32). The cm1 path derives its + // warp grid from the real subgroup (warp_i = gl_SubgroupID, tiw = + // gl_SubgroupInvocationID) and sizes shared arrays (coopmat_stage, + // ballots_sh) with NUM_WARPS = BLOCK_SIZE / WARP, so the WARP spec + // constant must equal the forced size, and the coverage invariant + // NUM_WARPS == (BM/WM)*(BN/WN) must be restored. BLOCK_SIZE is kept + // (same workgroup shape, load loops and shmem as the wave64 stack), so + // the subgroup count doubles and WM (or WN) is halved until the warp + // grid exactly tiles BM x BN again. Per-lane coopmat accumulator + // footprint is unchanged: half the lanes per subgroup, half the + // (WM/TM)*(WN/TN) fragments per subgroup. Runs after the BM64/M128 + // gates so it composes with the probe stack. Applies only when the + // driver honors a required size (subgroup_size_control covering 32); + // otherwise WARP=32 with a real subgroup of 64 would corrupt tiling. + const char * wave32_env = getenv("GGML_VK_MMID_WAVE32"); + if (wave32_env && atoi(wave32_env) != 0 && device->subgroup_size_control && + device->subgroup_min_size <= 32 && 32 <= device->subgroup_max_size) { + mmid_req_sgs = 32; + auto wave32_tile = [](std::vector &w) { + // {BLOCK_SIZE, BM, BN, BK, WM, WN, WMITER, TM, TN, TK, WARP} + w[10] = 32; // WARP: must match the forced subgroup size + for (int guard = 0; guard < 4 && w[0] / w[10] != (w[1] / w[4]) * (w[2] / w[5]); ++guard) { + if (w[4] >= w[5] && w[4] > w[7]) { + w[4] /= 2; // halve WM, keeping WM >= TM + } else { + w[5] /= 2; // halve WN + } + } + GGML_ASSERT(w[0] / w[10] == (w[1] / w[4]) * (w[2] / w[5])); // NUM_WARPS == (BM/WM)*(BN/WN) + GGML_ASSERT(w[4] >= w[7] && w[5] >= w[8]); // WM >= TM, WN >= TN + }; + wave32_tile(s_warptile_mmq_id16); + wave32_tile(m_warptile_mmq_id128); + wave32_tile(l_warptile_mmq_idw); + } } { const auto &s_warptile_mmq = s_warptile_mmq_id16; const auto &s_mmq_wg_denoms = s_mmq_wg_denoms_id16; const auto &m_warptile_mmq = m_warptile_mmq_id128; const auto &m_mmq_wg_denoms = m_mmq_wg_denoms_id128; + const auto &l_warptile_mmq = l_warptile_mmq_idw; + + // Same expansion as CREATE_MM above, plus a trailing required subgroup + // size (0 = driver default) for the GGML_VK_MMID_WAVE32 probe. Scoped to + // the mmid quant pipelines below; dense pipelines are untouched. +#undef CREATE_MM +#define CREATE_MM(TYPE, PIPELINE_NAME, NAMELC, F16ACC, WG_DENOMS, WARPTILE, PUSHCONST, PARAMCOUNT, ID) \ + if (device->mul_mat ## ID ## _l[TYPE]) \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->l, #NAMELC #F16ACC "_l", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), l_ ## WG_DENOMS, ggml_vk_mul_mm_spec(l_ ## WARPTILE, false), 1, false, true, mmid_req_sgs); \ + if (device->mul_mat ## ID ## _m[TYPE]) \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->m, #NAMELC #F16ACC "_m", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), m_ ## WG_DENOMS, ggml_vk_mul_mm_spec(m_ ## WARPTILE, false), 1, false, true, mmid_req_sgs); \ + if (device->mul_mat ## ID ## _s[TYPE]) \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->s, #NAMELC #F16ACC "_s", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), s_ ## WG_DENOMS, ggml_vk_mul_mm_spec(s_ ## WARPTILE, false), 1, false, true, mmid_req_sgs); \ + if (device->mul_mat ## ID ## _l[TYPE]) \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_l, #NAMELC #F16ACC "_aligned_l", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), l_ ## WG_DENOMS, ggml_vk_mul_mm_spec(l_ ## WARPTILE, true), l_align, false, true, mmid_req_sgs); \ + if (device->mul_mat ## ID ## _m[TYPE]) \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_m, #NAMELC #F16ACC "_aligned_m", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), m_ ## WG_DENOMS, ggml_vk_mul_mm_spec(m_ ## WARPTILE, true), m_align, false, true, mmid_req_sgs); \ + if (device->mul_mat ## ID ## _s[TYPE]) \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_s, #NAMELC #F16ACC "_aligned_s", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), s_ ## WG_DENOMS, ggml_vk_mul_mm_spec(s_ ## WARPTILE, true), s_align, false, true, mmid_req_sgs); \ CREATE_MM2(GGML_TYPE_Q1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q1_0], matmul_id_subgroup_q1_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); CREATE_MM2(GGML_TYPE_Q2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_0], matmul_id_subgroup_q2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); From 18569a90308efad8e597ae15721eb9de704f692c Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 14 Jul 2026 11:29:58 +0000 Subject: [PATCH 141/212] vulkan: mul_mat_id f16-B probe (GGML_VK_MMID_F16B, env-gated) Convert contiguous f32 activations (B) to f16 for quantized MUL_MAT_ID on KHR_coopmat devices and run the matmul_id_subgroup__f16 kernels instead of the f32-B ones. Halves B bytes and buf_b shared memory. The _f16 SPIR-V already exists for every quant type; this adds a parallel env-gated pipeline array (pipeline_dequant_mul_mat_mat_id_f16b), extends the getter to return it for src1=F16 on non-coopmat2 devices, relaxes the src1-type assert, and forces the existing y_non_contig convert-to- prealloc_y plumbing (same as coopmat2). Default OFF, zero behavior change when unset. Measured on Radeon 8060S (RADV gfx1151), Qwen3.6-35B-A3B-UD-Q5_K_XL, -fa 1 -b 512 -ub 512 -ctk q8_0 -ctv q8_0 -p 512 -r 3, stacked on GGML_VK_MMID_SMALLN=1 GGML_VK_MMID_BM64=1: stack (f32 B, canary): pp512 1075.04 +/- 8.66 t/s stack + F16B: pp512 1100.66 +/- 2.37 t/s (+2.4%) test-backend-ops test -o MUL_MAT_ID -b Vulkan0: 790/790 with and without GGML_VK_MMID_F16B=1. Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 72 +++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index e8bb9555ed00..f7bf2ac664af 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -955,6 +955,10 @@ struct vk_device_struct { vk_matmul_pipeline2 pipeline_dequant_mul_mat_mat_id[GGML_TYPE_COUNT]; vk_matmul_pipeline2 pipeline_dequant_mul_mat_mat_id_q8_1[GGML_TYPE_COUNT]; + // EXPERIMENT (GGML_VK_MMID_F16B=1): f16-B mul_mat_id pipelines on KHR_coopmat + // devices (upstream only builds f32-B there). Populated only when the env flag + // is set; empty otherwise. + vk_matmul_pipeline2 pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_COUNT]; vk_pipeline pipeline_matmul_split_k_reduce; vk_pipeline pipeline_quantize_q8_1_x4; @@ -4454,6 +4458,18 @@ struct CompileTask { uint32_t required_subgroup_size; }; +// EXPERIMENT (GGML_VK_MMID_F16B=1): convert the contiguous f32 activations (B) of +// quantized MUL_MAT_ID to f16 and run the f16-B matmul_id kernels instead of the +// f32-B ones. Halves B bytes and buf_b shared memory (better occupancy) at the +// f32->f16 rounding cost upstream already accepts on the coopmat2 path. +static bool ggml_vk_mmid_f16b_enabled() { + static const bool enabled = [] { + const char * env = getenv("GGML_VK_MMID_F16B"); + return env != nullptr && atoi(env) != 0; + }(); + return enabled; +} + static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { VK_LOG_DEBUG("ggml_vk_load_shaders(" << device->name << ")"); @@ -5242,6 +5258,37 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); CREATE_MM2(GGML_TYPE_NVFP4, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4], matmul_id_subgroup_nvfp4_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); } + + // EXPERIMENT (GGML_VK_MMID_F16B=1): f16-B variants of the quant matmul_id + // pipelines. The _f16 SPIR-V exists for every quant type; upstream just never + // instantiates it in the KHR_coopmat branch. Same warptiles as the f32-B lines + // (including the SMALLN/BM64/M128 tile experiments shadowed above). + if (ggml_vk_mmid_f16b_enabled()) { + CREATE_MM2(GGML_TYPE_Q1_0, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q1_0], matmul_id_subgroup_q1_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q4_0, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q4_0], matmul_id_subgroup_q4_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q4_1, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q4_1], matmul_id_subgroup_q4_1_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q5_0], matmul_id_subgroup_q5_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q5_1], matmul_id_subgroup_q5_1_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q4_K], matmul_id_subgroup_q4_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q5_K, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q5_K], matmul_id_subgroup_q5_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_Q6_K, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_Q6_K], matmul_id_subgroup_q6_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ1_S, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ1_S], matmul_id_subgroup_iq1_s_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ1_M, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ1_M], matmul_id_subgroup_iq1_m_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ2_XXS, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ2_XXS], matmul_id_subgroup_iq2_xxs_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ2_XS, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ2_XS], matmul_id_subgroup_iq2_xs_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ2_S, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ2_S], matmul_id_subgroup_iq2_s_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ3_XXS, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ3_XXS], matmul_id_subgroup_iq3_xxs_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ3_S, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ3_S], matmul_id_subgroup_iq3_s_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ4_XS, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ4_XS], matmul_id_subgroup_iq4_xs_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_IQ4_NL, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_IQ4_NL], matmul_id_subgroup_iq4_nl_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_MXFP4, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + CREATE_MM2(GGML_TYPE_NVFP4, pipeline_dequant_mul_mat_mat_id_f16b[GGML_TYPE_NVFP4], matmul_id_subgroup_nvfp4_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id); + } + } + #undef CREATE_MM2 #undef CREATE_MM } else @@ -8534,7 +8581,8 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_id_pipeline(ggml_backend_vk_co return pipelines; } - GGML_ASSERT(src1_type == GGML_TYPE_F32 || (ctx->device->coopmat2 && src1_type == GGML_TYPE_F16)); + GGML_ASSERT(src1_type == GGML_TYPE_F32 || + ((ctx->device->coopmat2 || ggml_vk_mmid_f16b_enabled()) && src1_type == GGML_TYPE_F16)); switch (src0_type) { case GGML_TYPE_Q1_0: @@ -8571,7 +8619,11 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_id_pipeline(ggml_backend_vk_co return nullptr; } - vk_matmul_pipeline2& mmp = ctx->device->pipeline_dequant_mul_mat_mat_id[src0_type]; + // GGML_VK_MMID_F16B: on KHR_coopmat devices the f16-B mmid pipelines live in a + // parallel array (coopmat2's main array already holds f16-B pipelines). + vk_matmul_pipeline2& mmp = (src1_type == GGML_TYPE_F16 && !ctx->device->coopmat2) + ? ctx->device->pipeline_dequant_mul_mat_mat_id_f16b[src0_type] + : ctx->device->pipeline_dequant_mul_mat_mat_id[src0_type]; // XXX TODO 'prec' is not actually allowed in mul_mat_id. bool prefer_fp16acc = ctx->device->fp16 /*&& prec == GGML_PREC_DEFAULT*/; bool support_fp16acc = !mmp.f16acc->is_empty(); @@ -10914,7 +10966,23 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& #else const bool y_decode_vector_staging = false; #endif + // EXPERIMENT (GGML_VK_MMID_F16B=1): route quantized MUL_MAT_ID through the f16-B + // kernels. Treating contiguous f32 B as y_non_contig reuses the existing + // convert-to-prealloc_y plumbing (to_fp16_vk_1), exactly like coopmat2 does. + // Gated on coopmat_support because the f16b pipelines are only created there. + const bool mmid_f16b = ggml_vk_mmid_f16b_enabled() && + ctx->device->coopmat_support && !ctx->device->coopmat2 && + ggml_is_quantized(src0->type) && src1->type == GGML_TYPE_F32; + if (mmid_f16b) { + static bool mmid_f16b_logged = false; + if (!mmid_f16b_logged) { + mmid_f16b_logged = true; + fprintf(stderr, "ggml_vulkan: MUL_MAT_ID f16-B path engaged (GGML_VK_MMID_F16B)\n"); + } + } + const bool y_non_contig = y_decode_vector_staging || + mmid_f16b || (ctx->device->coopmat2 && src1->type == GGML_TYPE_F32) || (src0->type == GGML_TYPE_BF16 && src1->type != GGML_TYPE_BF16) || !ggml_vk_dim01_contiguous(src1); From 8ad46e197db665b0204bf6dc36ddab43a354e5e5 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 26 Jul 2026 10:53:57 +0000 Subject: [PATCH 142/212] vulkan: guard mmid f16-B path on pipeline existence (Q2_0 fallback) Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index f7bf2ac664af..a7c7f15b4ea6 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -10972,7 +10972,11 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& // Gated on coopmat_support because the f16b pipelines are only created there. const bool mmid_f16b = ggml_vk_mmid_f16b_enabled() && ctx->device->coopmat_support && !ctx->device->coopmat2 && - ggml_is_quantized(src0->type) && src1->type == GGML_TYPE_F32; + ggml_is_quantized(src0->type) && src1->type == GGML_TYPE_F32 && + // only take the f16-B path if a pipeline exists for this src0 type + // (e.g. Q2_0 has none); otherwise fall through to the normal f32-B path. + !(ctx->device->pipeline_dequant_mul_mat_mat_id_f16b[src0->type].f16acc->is_empty() && + ctx->device->pipeline_dequant_mul_mat_mat_id_f16b[src0->type].f32acc->is_empty()); if (mmid_f16b) { static bool mmid_f16b_logged = false; if (!mmid_f16b_logged) { From 68df0714b84f0ea041c1c00a2bc6a7ff2896ae57 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 6 Aug 2026 22:30:38 +0000 Subject: [PATCH 143/212] tests: cover MMQ tile boundaries in MUL_MAT and MUL_MAT_ID The MMQ J config is chosen from n, so a wave-partitioning error in one J tile only shows up when n sits on that tile's boundary. The neighbouring n picks a different J and passes, which hides it. The existing quantized cases stop at n=129 and the general MUL_MAT set jumps 64 -> 4096, so nothing lands on 256 or 512 and the whole class went untested. Sweep n over 255/256/257/511/512/513 for q8_0, q4_0, q4_K, q5_K and q6_K, in both MUL_MAT and MUL_MAT_ID. On an RDNA3.5 build that runs the J128 kernel with 16 wave32 waves over a 128-row tile these fail for every n that selects J128 and pass at n=513, which selects J112. A build predating that config passes the whole sweep. Co-Authored-By: Claude Opus 5 (cherry picked from commit b54cd8a8add4ddb2710eaf256b9fb2ba2be4b383) Assisted-by: Claude (Opus 5) --- tests/test-backend-ops.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 55d25ccf9c3c..964c51728667 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -10515,6 +10515,19 @@ static std::vector> make_test_cases_eval() { int k = 256; test_cases.emplace_back(new test_mul_mat_id(type_a, type_b, n_mats, n_used, b, m, n, k)); } + // MMQ tile-boundary cases. The MMQ J config is picked from n, and a wave-partitioning error in + // one J tile only shows up when n sits on that tile's boundary: the neighbouring n selects a + // different J and passes, which hides it. The general cases above stop at 129 and the MUL_MAT + // set jumps 64 -> 4096, so no existing case lands on 256 or 512. + // MMQ tile-boundary sweep: n on and either side of the 256 / 512 J boundaries. + for (ggml_type type_a : {GGML_TYPE_Q8_0, GGML_TYPE_Q4_0, GGML_TYPE_Q4_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K}) { + for (int64_t n : {255, 256, 257, 511, 512, 513}) { + test_cases.emplace_back(new test_mul_mat(type_a, GGML_TYPE_F32, 1024, n, 256, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat_id(type_a, GGML_TYPE_F32, 8, 2, false, 1024, n, 256)); + } + } + + } } } From 77214b50c05fe5a21db8144992051b910a9bf2d9 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sat, 8 Aug 2026 04:24:38 +0000 Subject: [PATCH 144/212] vulkan: create coopmat2 mul_mat_id pipelines with the real param count 30d8bb02b raised mul_mat_id_param_count to 6 for the fused MUL epilogue and gave every mul_mat_id shader a binding 5 (FusedScale), including the coopmat2 one, which binds it purely for descriptor-layout parity. The coopmat2 pipeline creation block was left passing a literal 5, so two things go wrong there: - the pipelines are created with 5 descriptors while the shader declares 6 - PARAMCOUNT == mul_mat_id_param_count doubles as the "this is mul_mat_id" argument to ggml_vk_mul_mm_cm2_spec, so it went false and every coopmat2 mul_mat_id pipeline was specialized as a plain matmul, dropping the trailing spec constant Only reachable where device->coopmat2 is true. gfx1151 does not take that path and the v0.5 release predates the constant bump, so neither is affected. Not validated on hardware - no coopmat2 device here. The block does compile: built with the pinned shaderc (GL_NV_cooperative_matrix2 supported). The two OCP FP4 sites stay behind GL_EXT_float_e2m1, which that glslc does not support. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 66 ++++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index a7c7f15b4ea6..fc7c41b15b27 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4989,48 +4989,48 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { GGML_ASSERT(device->subgroup_ballot); - CREATE_MM2(pipeline_matmul_id_f16, matmul_id_subgroup_f16, wg_denoms, warptile, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_matmul_id_f16, matmul_id_subgroup_f16, wg_denoms, warptile, vk_mat_mat_id_push_constants, mul_mat_id_param_count) #if defined(GGML_VULKAN_BFLOAT16_GLSLC_SUPPORT) if (device->coopmat_bf16_support) { - CREATE_MM(pipeline_matmul_id_bf16, matmul_id_subgroup_bf16, , wg_denoms, warptile, vk_mat_mat_id_push_constants, 5) + CREATE_MM(pipeline_matmul_id_bf16, matmul_id_subgroup_bf16, , wg_denoms, warptile, vk_mat_mat_id_push_constants, mul_mat_id_param_count) } #endif - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q1_0], matmul_id_subgroup_q1_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_0], matmul_id_subgroup_q2_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0], matmul_id_subgroup_q4_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_1], matmul_id_subgroup_q4_1_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_0], matmul_id_subgroup_q5_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_1], matmul_id_subgroup_q5_1_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_subgroup_tq2_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K], matmul_id_subgroup_q4_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_K], matmul_id_subgroup_q5_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_K], matmul_id_subgroup_q6_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ1_S], matmul_id_subgroup_iq1_s_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ1_M], matmul_id_subgroup_iq1_m_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ2_XXS], matmul_id_subgroup_iq2_xxs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ2_XS], matmul_id_subgroup_iq2_xs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ2_S], matmul_id_subgroup_iq2_s_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_XXS], matmul_id_subgroup_iq3_xxs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_S], matmul_id_subgroup_iq3_s_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_XS], matmul_id_subgroup_iq4_xs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_NL], matmul_id_subgroup_iq4_nl_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4], matmul_id_subgroup_rocmfp4_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_id_subgroup_rocmfp4_fast_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp3_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp6_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp8_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q1_0], matmul_id_subgroup_q1_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_0], matmul_id_subgroup_q2_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0], matmul_id_subgroup_q4_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_1], matmul_id_subgroup_q4_1_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_0], matmul_id_subgroup_q5_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_1], matmul_id_subgroup_q5_1_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_subgroup_tq2_0_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K], matmul_id_subgroup_q4_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_K], matmul_id_subgroup_q5_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_K], matmul_id_subgroup_q6_k_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ1_S], matmul_id_subgroup_iq1_s_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ1_M], matmul_id_subgroup_iq1_m_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ2_XXS], matmul_id_subgroup_iq2_xxs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ2_XS], matmul_id_subgroup_iq2_xs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ2_S], matmul_id_subgroup_iq2_s_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_XXS], matmul_id_subgroup_iq3_xxs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ3_S], matmul_id_subgroup_iq3_s_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_XS], matmul_id_subgroup_iq4_xs_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_IQ4_NL], matmul_id_subgroup_iq4_nl_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4], matmul_id_subgroup_rocmfp4_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_0_ROCMFP4_FAST], matmul_id_subgroup_rocmfp4_fast_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp3_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q6_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp6_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0_ROCMFPX], matmul_id_subgroup_rocmfpx_fp8_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) #if defined(GGML_VULKAN_FLOAT_E2M1_GLSLC_SUPPORT) && defined(GGML_VULKAN_FLOAT_E4M3_GLSLC_SUPPORT) if (device->ocp_fp4) { - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f16_ocp, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4], matmul_id_subgroup_nvfp4_f16_ocp, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f16_ocp, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4], matmul_id_subgroup_nvfp4_f16_ocp, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) } else #endif { - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) - CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4], matmul_id_subgroup_nvfp4_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, 5) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_MXFP4], matmul_id_subgroup_mxfp4_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) + CREATE_MM2(pipeline_dequant_mul_mat_mat_id[GGML_TYPE_NVFP4], matmul_id_subgroup_nvfp4_f16, mmqid_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count) } #undef CREATE_MM #undef CREATE_MM2 From 6192a050dc3f751a74134f524828f621e8bdc5d1 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 30 Aug 2026 10:20:41 +0000 Subject: [PATCH 145/212] vulkan: enable the Strix mmid tile gates by default Split from the fork's flag-flip commit (f7d804ee7): GGML_VK_MMID_F16B, BM64, M128, WAVE32 and SMALLN now default on with =0 opt-out. The wave32 gate keeps its subgroup_size_control device guard, so devices without a 32-wide subgroup mode are unaffected. GGML_VK_MMID_TILE16 stays opt-in (documented negative on gfx1151). Co-Authored-By: Claude Fable 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index fc7c41b15b27..04bcdec56063 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4465,7 +4465,7 @@ struct CompileTask { static bool ggml_vk_mmid_f16b_enabled() { static const bool enabled = [] { const char * env = getenv("GGML_VK_MMID_F16B"); - return env != nullptr && atoi(env) != 0; + return env == nullptr || atoi(env) != 0; // on by default; =0 disables }(); return enabled; } @@ -5145,7 +5145,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { // A-traffic (ic-tile count unchanged), halves per-expert B re-reads // (ir-tile count), larger WGs to hide latency. const char * bm64_env = getenv("GGML_VK_MMID_BM64"); - if (bm64_env && atoi(bm64_env) != 0) { + if (!bm64_env || atoi(bm64_env) != 0) { // on by default; =0 disables s_warptile_mmq_id16[0] = 2 * mul_mat_subgroup_size; // BLOCK_SIZE s_warptile_mmq_id16[1] = 64; // BM s_mmq_wg_denoms_id16[0] = 64; @@ -5153,7 +5153,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { // GGML_VK_MMID_M128=1: same idea for the medium tile (BM 64->128, four // warps) — the tile the per-expert-n heuristic picks at n~64 (e.g. ub2048). const char * m128_env = getenv("GGML_VK_MMID_M128"); - if (m128_env && atoi(m128_env) != 0) { + if (!m128_env || atoi(m128_env) != 0) { // on by default; =0 disables m_warptile_mmq_id128[0] = 4 * mul_mat_subgroup_size; // BLOCK_SIZE m_warptile_mmq_id128[1] = 128; // BM m_mmq_wg_denoms_id128[0] = 128; @@ -5175,7 +5175,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { // driver honors a required size (subgroup_size_control covering 32); // otherwise WARP=32 with a real subgroup of 64 would corrupt tiling. const char * wave32_env = getenv("GGML_VK_MMID_WAVE32"); - if (wave32_env && atoi(wave32_env) != 0 && device->subgroup_size_control && + if ((!wave32_env || atoi(wave32_env) != 0) && device->subgroup_size_control && // on by default; =0 disables device->subgroup_min_size <= 32 && 32 <= device->subgroup_max_size) { mmid_req_sgs = 32; auto wave32_tile = [](std::vector &w) { @@ -11021,7 +11021,7 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& // matmul efficiency, ~78% of MoE prefill time on Qwen3.6-35B-A3B). uint32_t n_for_tile = (uint32_t)nei1; static const char * mmid_smalln_env = getenv("GGML_VK_MMID_SMALLN"); - if (mmid_smalln_env && atoi(mmid_smalln_env) != 0 && ne02 > 1) { + if (!(mmid_smalln_env && atoi(mmid_smalln_env) == 0) && ne02 > 1) { // on by default; =0 disables n_for_tile = std::max(1u, (uint32_t)((nei1 * nei0 + ne02 - 1) / ne02)); } From 891cfff9ae640eda8956957452a396947fac3212 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 18 Aug 2026 01:12:00 +0000 Subject: [PATCH 146/212] vulkan: run the quantised dense coopmat pipelines at wave32 RDNA3.x WMMA is wave32-native, so a wave64 subgroup issues each coopmat op as two halves. GGML_VK_MMID_WAVE32 already exploits this for mul_mat_id and leaves the dense pipelines at the driver default; this gives dense the same treatment, gated on measurement rather than a flag. BLOCK_SIZE is kept, so the subgroup count doubles and WM (or WN) halves until the warp grid tiles BM x BN again. Only the quantised tiles are retiled: on gfx1151 a standalone MUL_MAT microbench at both dense FFN shapes reads q6_K +5.2..+10.8%, q8_0 +5.4..+8.4%, q4_K +0.7..+9.1%, q4_0 -1.5..+1.8%, while f16 reads -6.7..+6.4% and bf16 ~0 - the float paths are bandwidth-bound on the weight stream, not issue-bound. The win tracks inline dequant instruction count (q6_K 3907 -> 3433 instructions, identical 192 VGPRs and 8 subgroups/SIMD). The required subgroup size is now the tile's own WARP for every dense coopmat pipeline. The cm1 shaders derive their warp grid from gl_SubgroupID and size shared arrays as NUM_WARPS = BLOCK_SIZE / WARP, so WARP and the real subgroup must agree; leaving that to the driver made the agreement incidental. Scoped to AMD coopmat1 on a wave64 default; other vendors keep the driver default. GGML_VK_DENSE_WAVE32=0 disables, =2 also retiles the float tiles. Qwen3-32B Q6_K_XL pp2048 +7.2% at ub256 / +3.9% at ub2048, Qwen3.8-27B +5.3% / +4.8%. PPL unchanged: 6.9496 +/- 0.24246 in both arms, all 20 per-chunk values identical, since the retile changes which warp owns an output sub-tile and not the K-reduction order within an element. Assisted-by: Claude Opus 5 (cherry picked from commit 448994e9637405610ced3e0ead02c7b6fa688314) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 83 ++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 04bcdec56063..59acd2dfb8ff 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -5038,20 +5038,91 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { #endif // defined(VK_NV_cooperative_matrix2) && defined(GGML_VULKAN_COOPMAT2_GLSLC_SUPPORT) #if defined(VK_KHR_cooperative_matrix) && defined(GGML_VULKAN_COOPMAT_GLSLC_SUPPORT) if (device->coopmat_support) { + // Deterministic subgroup sizing for the dense coopmat pipelines. Two parts: + // + // (1) The required subgroup size is the tile's own WARP, not the driver's choice. The cm1 + // shaders derive their warp grid from the real subgroup (warp_i = gl_SubgroupID) and + // size shared arrays with NUM_WARPS = BLOCK_SIZE / WARP, so WARP and the actual + // subgroup must agree - leaving that to the driver makes the agreement incidental. + // + // (2) The QUANTISED dense tiles run at wave32. RDNA3.x WMMA is wave32-native, so a wave64 + // subgroup issues each coopmat op as two halves. BLOCK_SIZE is kept, so the subgroup + // count doubles and WM (or WN) halves until the warp grid tiles BM x BN again: + // NUM_WARPS == (BM/WM)*(BN/WN). A tile that cannot be retiled is left at wave64. + // + // The FLOAT tiles are deliberately excluded. Measured on gfx1151 with a standalone + // MUL_MAT microbench at both dense FFN shapes (m=25600 k=5120, m=5120 k=25600): + // q6_K +5.2..+10.8%, q8_0 +5.4..+8.4%, q4_K +0.7..+9.1%, q4_0 -1.5..+1.8%, but + // f16 -6.7..+6.4% and bf16 ~0. The win tracks inline dequant instruction count + // (q6_K 3907 -> 3433 instructions at wave32, identical 192 VGPRs and 8 subgroups/SIMD), + // so it lands on the issue-bound quantised kernels and not on the float ones, which are + // bandwidth-bound on the weight stream. + // + // Scoped to AMD coopmat1 on a wave64 default; other vendors keep the driver default, + // since none of the above is validated there. + // GGML_VK_DENSE_WAVE32=0 disables, =2 additionally retiles the float tiles (probe). + const bool dense_sgs_scope = + device->vendor_id == VK_VENDOR_ID_AMD && + device->driver_id != vk::DriverId::eAmdProprietary && + device->subgroup_size_control; + const bool dense_wave32_possible = + dense_sgs_scope && + device->subgroup_min_size <= 32 && 32 <= device->subgroup_max_size && + device->subgroup_size > 32; + const char * dense_wave32_env = getenv("GGML_VK_DENSE_WAVE32"); + const int dense_wave32 = dense_wave32_env ? atoi(dense_wave32_env) : 1; + + if (dense_wave32_possible && dense_wave32 != 0) { + auto wave32_tile = [](std::vector & w) -> bool { + // {BLOCK_SIZE, BM, BN, BK, WM, WN, WMITER, TM, TN, TK, WARP} + std::vector t = w; + t[10] = 32; + for (int guard = 0; guard < 4 && t[0] / t[10] != (t[1] / t[4]) * (t[2] / t[5]); ++guard) { + if (t[4] >= t[5] && t[4] > t[7]) { + t[4] /= 2; // halve WM, keeping WM >= TM + } else { + t[5] /= 2; // halve WN + } + } + if (t[0] / t[10] != (t[1] / t[4]) * (t[2] / t[5]) || t[4] < t[7] || t[5] < t[8]) { + return false; + } + w = t; + return true; + }; + wave32_tile(l_warptile_mmq); + wave32_tile(m_warptile_mmq); + wave32_tile(s_warptile_mmq); + if (dense_wave32 >= 2) { + wave32_tile(l_warptile); + wave32_tile(m_warptile); + wave32_tile(s_warptile); + } + } + + // WARP -> required subgroup size, or 0 where the device cannot honor one. + auto dense_req_sgs = [dense_sgs_scope, &device](const std::vector & w) -> uint32_t { + const uint32_t warp = w[10]; + if (!dense_sgs_scope || warp < device->subgroup_min_size || warp > device->subgroup_max_size) { + return 0; + } + return warp; + }; + // Create 6 variants, {s,m,l}x{unaligned,aligned} #define CREATE_MM(TYPE, PIPELINE_NAME, NAMELC, F16ACC, WG_DENOMS, WARPTILE, PUSHCONST, PARAMCOUNT, ID) \ if (device->mul_mat ## ID ## _l[TYPE]) \ - ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->l, #NAMELC #F16ACC "_l", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), l_ ## WG_DENOMS, ggml_vk_mul_mm_spec(l_ ## WARPTILE, false), 1, false, true); \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->l, #NAMELC #F16ACC "_l", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), l_ ## WG_DENOMS, ggml_vk_mul_mm_spec(l_ ## WARPTILE, false), 1, false, true, dense_req_sgs(l_ ## WARPTILE)); \ if (device->mul_mat ## ID ## _m[TYPE]) \ - ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->m, #NAMELC #F16ACC "_m", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), m_ ## WG_DENOMS, ggml_vk_mul_mm_spec(m_ ## WARPTILE, false), 1, false, true); \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->m, #NAMELC #F16ACC "_m", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), m_ ## WG_DENOMS, ggml_vk_mul_mm_spec(m_ ## WARPTILE, false), 1, false, true, dense_req_sgs(m_ ## WARPTILE)); \ if (device->mul_mat ## ID ## _s[TYPE]) \ - ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->s, #NAMELC #F16ACC "_s", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), s_ ## WG_DENOMS, ggml_vk_mul_mm_spec(s_ ## WARPTILE, false), 1, false, true); \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->s, #NAMELC #F16ACC "_s", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), s_ ## WG_DENOMS, ggml_vk_mul_mm_spec(s_ ## WARPTILE, false), 1, false, true, dense_req_sgs(s_ ## WARPTILE)); \ if (device->mul_mat ## ID ## _l[TYPE]) \ - ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_l, #NAMELC #F16ACC "_aligned_l", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), l_ ## WG_DENOMS, ggml_vk_mul_mm_spec(l_ ## WARPTILE, true), l_align, false, true); \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_l, #NAMELC #F16ACC "_aligned_l", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), l_ ## WG_DENOMS, ggml_vk_mul_mm_spec(l_ ## WARPTILE, true), l_align, false, true, dense_req_sgs(l_ ## WARPTILE)); \ if (device->mul_mat ## ID ## _m[TYPE]) \ - ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_m, #NAMELC #F16ACC "_aligned_m", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), m_ ## WG_DENOMS, ggml_vk_mul_mm_spec(m_ ## WARPTILE, true), m_align, false, true); \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_m, #NAMELC #F16ACC "_aligned_m", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), m_ ## WG_DENOMS, ggml_vk_mul_mm_spec(m_ ## WARPTILE, true), m_align, false, true, dense_req_sgs(m_ ## WARPTILE)); \ if (device->mul_mat ## ID ## _s[TYPE]) \ - ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_s, #NAMELC #F16ACC "_aligned_s", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), s_ ## WG_DENOMS, ggml_vk_mul_mm_spec(s_ ## WARPTILE, true), s_align, false, true); \ + ggml_vk_create_pipeline(device, device-> PIPELINE_NAME ->a_s, #NAMELC #F16ACC "_aligned_s", NAMELC ## F16ACC ## _cm1_len, NAMELC ## F16ACC ## _cm1_data, "main", PARAMCOUNT, sizeof(PUSHCONST), s_ ## WG_DENOMS, ggml_vk_mul_mm_spec(s_ ## WARPTILE, true), s_align, false, true, dense_req_sgs(s_ ## WARPTILE)); \ // Create 2 variants, {f16,f32} accumulator #define CREATE_MM2(TYPE, PIPELINE_NAME, NAMELC, WG_DENOMS, WARPTILE, PUSHCONST, PARAMCOUNT, ID) \ From f4d6f9e8da7886598083167416865cdcfee5da18 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 6 Aug 2026 01:34:10 +0000 Subject: [PATCH 147/212] vulkan: four env-gated Strix Halo prefill fixes for delta-net MoE All default OFF, so the same binary A/Bs each change. GGML_VK_CONCAT_TRANSPOSE: delta-net does ggml_transpose() into a dim-0 ggml_concat(), which the generic concat reads fully de-coalesced. Route that shape through a 32x32 shared-memory tile transpose. CONCAT 11877 -> 957 us/op. GGML_VK_MMID_SCALE_EPILOGUE: apply the following MUL's per-(expert,token) broadcast scale as mul_mat_id writes out, removing a 134 MB write plus read back. Prefill only; the existing fusion is gated to mat-vec. Not implemented in the coopmat2 shader, so it is refused there. GGML_VK_FUSE_UNARY_MUL: silu(x)*y is two nodes in the delta-net path; run it as the existing swiglu split. 750 -> 443 us/op. GGML_VK_MMID_WG256: the RADV tuning gives the dense large tile 256 threads on a 128x128 tile but left the mul_mat_id variants at 128. Qwen3.6-35B-A3B UD-Q4_K_XL, gfx1151, pp2048 at ub2048: 1223.75 -> 1733.64 t/s. test-backend-ops CONCAT/MUL_MAT_ID/UNARY/MUL pass; generated text is unchanged with each flag on, and the disabled paths are bit-identical to before. Co-Authored-By: Claude Opus 5 (cherry picked from commit 6f7a49e661d93fa42df528bdc88bc788eaf0ad2c) Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 182 ++++++++++++++++-- .../vulkan-shaders/concat_transpose.comp | 43 +++++ .../ggml-vulkan/vulkan-shaders/mul_mm.comp | 20 +- .../vulkan-shaders/mul_mm_cm2.comp | 3 + .../ggml-vulkan/vulkan-shaders/mul_mmq.comp | 8 +- .../vulkan-shaders/vulkan-shaders-gen.cpp | 1 + 6 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/concat_transpose.comp diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 59acd2dfb8ff..8f297cb66964 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -999,6 +999,7 @@ struct vk_device_struct { vk_pipeline pipeline_add_id_f32; vk_pipeline pipeline_concat_i8, pipeline_concat_i16, pipeline_concat_i32, pipeline_concat_i64; + vk_pipeline pipeline_concat_transpose_i32; vk_pipeline pipeline_upscale_nearest_f32, pipeline_upscale_bilinear_f32, pipeline_upscale_bicubic_f32, pipeline_upscale_bilinear_antialias_f32; vk_pipeline pipeline_scale_f32; vk_pipeline pipeline_log[2]; @@ -1403,6 +1404,7 @@ struct vk_mat_mat_id_push_constants { uint32_t nei0; uint32_t nei1; uint32_t nbi1; uint32_t ne11; uint32_t n_experts; uint32_t hoist_row_ids; + uint32_t fusion_flags; }; struct vk_mat_vec_id_push_constants { uint32_t ncols; @@ -2418,6 +2420,20 @@ class vk_perf_logger { if (node->op == GGML_OP_UNARY) { return fusion_str + ggml_unary_op_name(ggml_get_unary_op(node)); } + if (node->op == GGML_OP_MUL && getenv("GGML_VK_PERF_SHAPES")) { + std::string name = "MUL "; + name += "dst(" + std::to_string(node->ne[0]) + "," + std::to_string(node->ne[1]) + "," + + std::to_string(node->ne[2]) + ") b(" + std::to_string(node->src[1]->ne[0]) + "," + + std::to_string(node->src[1]->ne[1]) + "," + std::to_string(node->src[1]->ne[2]) + ")"; + name += std::string(" a=") + ggml_op_name(node->src[0]->op); + if (node->src[0]->op == GGML_OP_UNARY) { name += std::string(":") + ggml_unary_op_name(ggml_get_unary_op(node->src[0])); } + name += std::string(" b=") + ggml_op_name(node->src[1]->op); + if (node->src[1]->op == GGML_OP_UNARY) { name += std::string(":") + ggml_unary_op_name(ggml_get_unary_op(node->src[1])); } + if (node->src[1]->op == GGML_OP_RESHAPE && node->src[1]->src[0]) { + name += std::string("(") + ggml_op_name(node->src[1]->src[0]->op) + ")"; + } + return fusion_str + name; + } if (node->op == GGML_OP_MUL_MAT || node->op == GGML_OP_MUL_MAT_ID) { const uint64_t m = node->ne[0]; const uint64_t n = node->ne[1]; @@ -4611,6 +4627,23 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { l_warptile = { 256, 128, 128, 16, mm_warp_8, 64, 2, tm_m, tn_m, tk_m, mm_warp_8 }; l_warptile_mmq = l_warptile_mmq_int = { 256, 128, 128, 32, mm_warp_8, 64, 2, tm_m, tn_m, tk_m, mm_warp_8 }; l_warptile_mmq_int_k = { 256, 128, 128, 32, mm_warp_16, 64, 1, 4, 2, 1, mm_warp_16 }; + + // EXPERIMENT (GGML_VK_MMID_WG256=1): the dense large tile above runs 256 threads on a + // 128x128 tile, but the mul_mat_id variants still run 128. Give MoE the same thread + // count per tile: same BM/BN/BK (so same shared memory), twice the threads sharing each + // A/B tile load, half the accumulators per thread. Warp split stays legal: + // (BM/WM)*(BN/WN) == wg/subgroup == 4, WNITER == (WM*WN)/(WARP*TM*TN*WMITER) == 2. + // A 256-expert MoE at ub=2048 sees only ~64 rows per expert, so the tile that actually + // runs is the medium one, not the large one. Override both. + static const char * mmid_wg256_env = getenv("GGML_VK_MMID_WG256"); + if (mmid_wg256_env && atoi(mmid_wg256_env) != 0) { + l_warptile_mmqid = { 256, 128, 128, 32, mul_mat_subgroup_size_8, 64, 2, tm_m, tn_m, tk_m, mul_mat_subgroup_size_8 }; + l_warptile_mmqid_int = { 256, 128, 128, 32, mul_mat_subgroup_size_8, 64, 2, 4, 4, 1, mul_mat_subgroup_size_8 }; + // BM=BN=64 at 4 warps needs WM=WN=32: (BM/WM)*(BN/WN) == 4, cms_per_row/col == 2. + m_warptile_mmqid = { 256, 64, 64, 32, 32, 32, 2, tm_m, tn_m, tk_m, mul_mat_subgroup_size_8 }; + m_warptile_mmqid_int = { 256, 64, 64, 32, 32, 32, 2, 2, 2, 1, mul_mat_subgroup_size_8 }; + fprintf(stderr, "ggml_vulkan: MUL_MAT_ID medium+large tiles at 256 threads (GGML_VK_MMID_WG256)\n"); + } } else if (device->vendor_id == VK_VENDOR_ID_INTEL && device->coopmat_support) { // Xe2/Xe3 with coopmat enabled - warptile performance tuning l_warptile = { 512, 128, 128, 16, mm_warp_8, 32, 2, tm_m, tn_m, tk_m, mm_warp_8 }; @@ -4913,7 +4946,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { return spec; }; - const int mul_mat_id_param_count = 5; + const int mul_mat_id_param_count = 6; // a, b, d, ids, expert_counts, fused scale #if defined(VK_NV_cooperative_matrix2) && defined(GGML_VULKAN_COOPMAT2_GLSLC_SUPPORT) if (device->coopmat2) { @@ -6250,6 +6283,8 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, device->pipeline_concat_i8, "concat_i8", concat_i8_len, concat_i8_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_concat_i16, "concat_i16", concat_i16_len, concat_i16_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_concat_i32, "concat_i32", concat_i32_len, concat_i32_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); + // One workgroup per 32x32 tile: elements are passed as (rows, cols, 1). + ggml_vk_create_pipeline(device, device->pipeline_concat_transpose_i32, "concat_transpose_i32", concat_transpose_i32_len, concat_transpose_i32_data, "main", 3, sizeof(vk_op_binary_push_constants), {32, 32, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_concat_i64, "concat_i64", concat_i64_len, concat_i64_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {}, 1); ggml_vk_create_pipeline(device, device->pipeline_upscale_nearest_f32, "upscale_f32", upscale_f32_len, upscale_f32_data, "main", 2, sizeof(vk_op_upscale_push_constants), {512, 1, 1}, {GGML_SCALE_MODE_NEAREST}, 1); @@ -9699,14 +9734,14 @@ static void ggml_vk_matmul_id( uint32_t m, uint32_t n, uint32_t k, uint32_t stride_a, uint32_t stride_b, uint32_t stride_d, uint32_t batch_stride_a, uint32_t batch_stride_b, uint32_t batch_stride_d, uint32_t n_as, uint32_t nei0, uint32_t nei1, uint32_t nbi1, uint32_t ne11, - bool hoist_row_ids) { + bool hoist_row_ids, const vk_subbuffer & fused_scale, uint32_t fusion_flags) { VK_LOG_DEBUG("ggml_vk_matmul_id(a: (" << a.buffer->buffer << ", " << a.offset << ", " << a.size << "), b: (" << b.buffer->buffer << ", " << b.offset << ", " << b.size << "), d: (" << d.buffer->buffer << ", " << d.offset << ", " << d.size << "), ids: (" << ids.buffer->buffer << ", " << ids.offset << ", " << ids.size << "), expert_count: (" << expert_count_buf.buffer->buffer << ", " << expert_count_buf.offset << ", " << expert_count_buf.size << "), " << "m: " << m << ", n: " << n << ", k: " << k << ", stride_a: " << stride_a << ", stride_b: " << stride_b << ", stride_d: " << stride_d << ", " << "batch_stride_a: " << batch_stride_a << ", batch_stride_b: " << batch_stride_b << ", batch_stride_d: " << batch_stride_d << ", " << "n_as: " << n_as << ", nei0: " << nei0 << ", nei1: " << nei1 << ", nbi1: " << nbi1 << ", ne11: " << ne11 << ")"); const vk_mat_mat_id_push_constants pc = { m, n, k, stride_a, stride_b, stride_d, batch_stride_a, batch_stride_b, batch_stride_d, - nei0, nei1, nbi1, ne11, n_as, uint32_t(hoist_row_ids) }; - ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { a, b, d, ids, expert_count_buf }, pc, { m, nei1, n_as }); + nei0, nei1, nbi1, ne11, n_as, uint32_t(hoist_row_ids), fusion_flags }; + ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { a, b, d, ids, expert_count_buf, fused_scale }, pc, { m, nei1, n_as }); } static bool ggml_vk_dim01_contiguous(const ggml_tensor * tensor) { @@ -10953,7 +10988,7 @@ static void ggml_vk_mul_mat(ggml_backend_vk_context * ctx, vk_context& subctx, c } } -static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst) { +static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst, const ggml_tensor * fused_scale = nullptr, ggml_tensor * fused_dst = nullptr) { VK_LOG_DEBUG("ggml_vk_mul_mat_id_q_f16((" << src0 << ", name=" << src0->name << ", type=" << src0->type << ", ne0=" << src0->ne[0] << ", ne1=" << src0->ne[1] << ", ne2=" << src0->ne[2] << ", ne3=" << src0->ne[3] << ", nb0=" << src0->nb[0] << ", nb1=" << src0->nb[1] << ", nb2=" << src0->nb[2] << ", nb3=" << src0->nb[3]; std::cerr << "), (" << src1 << ", name=" << src1->name << ", type=" << src1->type << ", ne0=" << src1->ne[0] << ", ne1=" << src1->ne[1] << ", ne2=" << src1->ne[2] << ", ne3=" << src1->ne[3] << ", nb0=" << src1->nb[0] << ", nb1=" << src1->nb[1] << ", nb2=" << src1->nb[2] << ", nb3=" << src1->nb[3]; std::cerr << "), (" << ids << ", name=" << ids->name << ", type=" << ids->type << ", ne0=" << ids->ne[0] << ", ne1=" << ids->ne[1] << ", ne2=" << ids->ne[2] << ", ne3=" << ids->ne[3] << ", nb0=" << ids->nb[0] << ", nb1=" << ids->nb[1] << ", nb2=" << ids->nb[2] << ", nb3=" << ids->nb[3]; @@ -10991,7 +11026,9 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& hoisted_row_id_words * sizeof(uint32_t) <= ctx->device->properties.limits.maxStorageBufferRange; - ggml_backend_vk_buffer_context * dst_buf_ctx = (ggml_backend_vk_buffer_context *)dst->buffer->context; + // When the following MUL is fused in, write the scaled result straight to its destination. + const ggml_tensor * out_dst = fused_dst ? fused_dst : dst; + ggml_backend_vk_buffer_context * dst_buf_ctx = (ggml_backend_vk_buffer_context *)out_dst->buffer->context; ggml_backend_vk_buffer_context * src0_buf_ctx = (ggml_backend_vk_buffer_context *)src0->buffer->context; ggml_backend_vk_buffer_context * src1_buf_ctx = (ggml_backend_vk_buffer_context *)src1->buffer->context; ggml_backend_vk_buffer_context * ids_buf_ctx = (ggml_backend_vk_buffer_context *)ids->buffer->context; @@ -11109,6 +11146,18 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& vk_pipeline pipeline = ggml_vk_guess_matmul_id_pipeline(ctx, mmp, ne01, n_for_tile, aligned, qx_needs_dequant ? f16_type : src0->type, effective_src1_type); + // PROBE (GGML_VK_MMID_PROBE=1): which mmid tile actually runs, and with how many threads. + static const char * mmid_probe_env = getenv("GGML_VK_MMID_PROBE"); + if (mmid_probe_env && atoi(mmid_probe_env) != 0) { + static std::set seen; + std::string key = pipeline->name + ":" + std::to_string(n_for_tile); + if (seen.insert(key).second) { + fprintf(stderr, "ggml_vulkan: mmid pipeline=%s n_for_tile=%u m=%u wg=(%u,%u,%u)\n", + pipeline->name.c_str(), n_for_tile, (uint32_t)ne01, + pipeline->wg_denoms[0], pipeline->wg_denoms[1], pipeline->wg_denoms[2]); + } + } + if (ggml_nbytes(src0) > ctx->device->properties.limits.maxStorageBufferRange) { pipeline = ggml_vk_get_64b_indexing_pipeline(ctx, pipeline); } @@ -11199,7 +11248,7 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& } vk_buffer d_D = dst_buf_ctx->dev_buffer; - const uint64_t d_buf_offset = vk_tensor_offset(dst) + dst->view_offs; + const uint64_t d_buf_offset = vk_tensor_offset(out_dst) + out_dst->view_offs; GGML_ASSERT(d_D != nullptr); vk_buffer d_X; uint64_t x_buf_offset = 0; @@ -11335,7 +11384,9 @@ static void ggml_vk_mul_mat_id_q_f16(ggml_backend_vk_context * ctx, vk_context& { d_D, d_buf_offset, d_sz }, { d_ids, ids_buf_offset, ids_sz }, expert_count_buf, ne01, ne21, ne10, ne10, stride_b_y, ne01, stride_batch_x, stride_batch_y, ne20*ne21, - n_as, nei0, nei1, nbi1 / ggml_type_size(ids->type), ne11, hoist_row_ids + n_as, nei0, nei1, nbi1 / ggml_type_size(ids->type), ne11, hoist_row_ids, + fused_scale ? ggml_vk_tensor_subbuffer(ctx, fused_scale) : vk_subbuffer{ d_D, d_buf_offset, d_sz }, + fused_scale ? 1u : 0u ); // NOLINT if (x_non_contig || qx_needs_dequant) { @@ -11599,7 +11650,16 @@ static void ggml_vk_mul_mat_id(ggml_backend_vk_context * ctx, vk_context& subctx if (ggml_vk_use_mul_mat_vec_id(cgraph, node_idx)) { ggml_vk_mul_mat_vec_id_q_f16(ctx, subctx, cgraph, node_idx); } else { - ggml_vk_mul_mat_id_q_f16(ctx, subctx, src0, src1, src2, dst); + // Fused scale epilogue: the MUL's other operand is applied as the matmul writes out, + // and the result goes straight to the MUL's destination. + const ggml_tensor * fused_scale = nullptr; + ggml_tensor * fused_dst = nullptr; + if (ctx->num_additional_fused_ops == 1) { + ggml_tensor * mul = cgraph->nodes[node_idx + 1]; + fused_scale = (mul->src[0] == dst) ? mul->src[1] : mul->src[0]; + fused_dst = mul; + } + ggml_vk_mul_mat_id_q_f16(ctx, subctx, src0, src1, src2, dst, fused_scale, fused_dst); } } @@ -12962,6 +13022,31 @@ static vk_conv_shapes ggml_vk_conv_select_shape(ggml_backend_vk_context * ctx, u } } +// EXPERIMENT (GGML_VK_CONCAT_TRANSPOSE=1): the delta-net conv-state path does +// ggml_transpose() straight into a dim-0 ggml_concat(), so the generic concat kernel reads +// src1 fully de-coalesced. Measured on Qwen3.6-35B-A3B: CONCAT is ~22% of pp2048 at ub=2048 +// and grows 3.1x for a 2x ubatch. Route that exact shape to a tiled-transpose kernel. +static bool ggml_vk_concat_is_transposed(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst) { + static const char * env = getenv("GGML_VK_CONCAT_TRANSPOSE"); + if (!(env && atoi(env) != 0)) { + return false; + } + if (ggml_get_op_params_i32(dst, 0) != 0) { // dim 0 only + return false; + } + if (src0->ne[2] != 1 || src0->ne[3] != 1 || src1->ne[2] != 1 || src1->ne[3] != 1) { + return false; + } + const size_t ts = ggml_type_size(src0->type); + if (src0->nb[0] != ts || dst->nb[0] != ts) { // src0 and dst rows must be contiguous + return false; + } + if (src1->nb[0] <= src1->nb[1]) { // src1 must actually be transposed + return false; + } + return src0->ne[1] == src1->ne[1] && dst->ne[1] == src1->ne[1]; +} + static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * dst, ggml_op op) { switch (op) { case GGML_OP_GET_ROWS: @@ -13054,6 +13139,11 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const if (!ggml_vk_concat_supported(src0, src1, dst)) { return nullptr; } + // Tiled-transpose path handles unquantized 4-byte elements only. + if (!ggml_is_quantized(src0->type) && ggml_vk_concat_unit_size(src0->type) == 4 && + ggml_vk_concat_is_transposed(src0, src1, dst)) { + return ctx->device->pipeline_concat_transpose_i32; + } switch (ggml_vk_concat_unit_size(src0->type)) { case 1: return ctx->device->pipeline_concat_i8; @@ -14126,6 +14216,11 @@ static void ggml_vk_op_f32(ggml_backend_vk_context * ctx, vk_context& subctx, co case GGML_OP_GLU: case GGML_OP_CONV_2D_DW: { + // The tiled concat kernel is dispatched per 32x32 tile, not per element. + if (op == GGML_OP_CONCAT && pipeline == ctx->device->pipeline_concat_transpose_i32) { + elements = { (uint32_t)src1->ne[1], (uint32_t)src1->ne[0], 1 }; + break; + } uint32_t ne = ggml_nelements(dst); if (op == GGML_OP_CPY && ggml_is_quantized(src0->type) && ggml_is_quantized(dst->type)) { // Convert from number of logical elements to 2- or 4-byte units. @@ -17816,6 +17911,22 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr break; } + // Fused silu(x)*y: run it as a swiglu split, writing straight to the MUL's destination. + if (ctx->num_additional_fused_ops == 1) { + ggml_tensor * mul = cgraph->nodes[node_idx + 1]; + ggml_tensor * other = (mul->src[0] == node) ? mul->src[1] : mul->src[0]; + + ggml_tensor fused = *mul; + fused.op = GGML_OP_GLU; + memset(fused.op_params, 0, sizeof(fused.op_params)); + ggml_set_op_params_i32(&fused, 0, (int32_t) GGML_GLU_OP_SWIGLU); + fused.src[0] = node->src[0]; + fused.src[1] = other; + + ggml_vk_glu(ctx, compute_ctx, fused.src[0], fused.src[1], &fused); + break; + } + switch (ggml_get_unary_op(node)) { case GGML_UNARY_OP_ELU: case GGML_UNARY_OP_EXP: @@ -18851,15 +18962,61 @@ static bool ggml_vk_can_fuse(const ggml_backend_vk_context * ctx, const struct g } } + // EXPERIMENT (GGML_VK_FUSE_UNARY_MUL=1): silu(x)*y is emitted as two nodes by the delta-net + // path, so the silu result makes a full round trip through memory. That is the same shape + // swiglu-split already computes in one pass, so route the pair to the existing GLU pipeline. + if (ops.size() == 2 && ops.begin()[0] == GGML_OP_UNARY && ops.begin()[1] == GGML_OP_MUL) { + static const char * env = getenv("GGML_VK_FUSE_UNARY_MUL"); + if (!(env && atoi(env) != 0)) { + return false; + } + const ggml_tensor * unary = cgraph->nodes[node_idx]; + const ggml_tensor * mul = cgraph->nodes[node_idx + 1]; + + if (ggml_get_unary_op(unary) != GGML_UNARY_OP_SILU) { + return false; + } + if (mul->src[0] != unary && mul->src[1] != unary) { + return false; + } + const ggml_tensor * other = (mul->src[0] == unary) ? mul->src[1] : mul->src[0]; + // The GLU split shader walks both inputs and the output with the same element count. + if (unary->type != GGML_TYPE_F32 || other->type != GGML_TYPE_F32 || mul->type != GGML_TYPE_F32) { + return false; + } + if (!ggml_are_same_shape(unary, other) || !ggml_are_same_shape(unary, mul)) { + return false; + } + if (!ggml_is_contiguous(unary->src[0]) || !ggml_is_contiguous(other) || !ggml_is_contiguous(mul)) { + return false; + } + return true; + } + auto const &mmid_mul_ok = [&](const ggml_tensor *mmid, const ggml_tensor *mul) { const ggml_tensor *scale = mul->src[1]; if (mmid != mul->src[0]) { return false; } - // mat-vec only + // EXPERIMENT (GGML_VK_MMID_SCALE_EPILOGUE=1): the tile shader can apply the scale as it + // writes out, which removes a full write+read of the matmul result at prefill. The + // coopmat2 shader has the binding but not the epilogue, so it stays on the old path. if (!ggml_vk_use_mul_mat_vec_id(cgraph, node_idx)) { - return false; + static const char * env = getenv("GGML_VK_MMID_SCALE_EPILOGUE"); + if (!(env && atoi(env) != 0) || ctx->device->coopmat2) { + return false; + } + // Shader indexes the scale as [token * nei0 + expert_slot]. + if (scale->type != GGML_TYPE_F32 || mul->type != GGML_TYPE_F32 || !ggml_is_contiguous(scale)) { + return false; + } + if (get_misalign_bytes(ctx, scale) != 0) { + return false; + } + return scale->ne[0] == 1 && + scale->ne[1] == mmid->ne[1] && scale->ne[2] == mmid->ne[2] && scale->ne[3] == mmid->ne[3] && + ggml_are_same_shape(mul, mmid); } // shaders assume the types match if (mmid->type != scale->type) { @@ -19604,6 +19761,9 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg fusion_string = "MUL_MAT_ID_MUL"; op_srcs_fused_elementwise[0] = false; op_srcs_fused_elementwise[1] = true; + } else if (ggml_vk_can_fuse(ctx, cgraph, i, { GGML_OP_UNARY, GGML_OP_MUL })) { + ctx->num_additional_fused_ops = 1; + fusion_string = "SILU_MUL"; } else if (ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ROPE, GGML_OP_VIEW, GGML_OP_SET_ROWS }, { i + 4 }) && ggml_check_edges(cgraph, i, rms_norm_mul_rope_view_set_rows_edges) && ggml_vk_can_fuse_rms_norm_mul_rope(ctx, cgraph, i) && diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/concat_transpose.comp b/ggml/src/ggml-vulkan/vulkan-shaders/concat_transpose.comp new file mode 100644 index 000000000000..653aeaa011b2 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/concat_transpose.comp @@ -0,0 +1,43 @@ +#version 450 + +#include "types.glsl" +#include "generic_binary_head.glsl" + +layout(local_size_x = 32, local_size_y = 8, local_size_z = 1) in; + +// dim-0 concat whose src1 is transposed. The generic kernel reads src1 with the transposed +// stride, so neighbouring lanes touch different cache lines. Stage a 32x32 tile in shared +// memory instead, which keeps both the load and the store coalesced. 33 columns pads away +// the shared-memory bank conflicts. +shared A_TYPE tmp[32][33]; + +void main() { + const uint tx = gl_LocalInvocationID.x; + const uint ty = gl_LocalInvocationID.y; + + const uint row = gl_WorkGroupID.x * 32 + tx; + + // src0 is already contiguous, copy it straight through. + if (gl_WorkGroupID.y == 0 && row < p.ne01) { + for (uint i0 = ty; i0 < p.ne00; i0 += 8) { + data_d[get_doffset() + row*p.nb21 + i0*p.nb20] = D_TYPE(data_a[get_aoffset() + row*p.nb01 + i0*p.nb00]); + } + } + + [[unroll]] for (uint j = 0; j < 32; j += 8) { + const uint c = gl_WorkGroupID.y * 32 + ty + j; + if (c < p.ne10 && row < p.ne11) { + tmp[ty + j][tx] = A_TYPE(data_b[get_boffset() + c*p.nb10 + row*p.nb11]); + } + } + + barrier(); + + const uint col = gl_WorkGroupID.y * 32 + tx; + [[unroll]] for (uint j = 0; j < 32; j += 8) { + const uint r = gl_WorkGroupID.x * 32 + ty + j; + if (col < p.ne10 && r < p.ne11) { + data_d[get_doffset() + r*p.nb21 + (p.ne00 + col)*p.nb20] = D_TYPE(tmp[tx][ty + j]); + } + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm.comp index 63c4aaebcb1a..7d32124b969b 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm.comp @@ -68,6 +68,9 @@ layout (binding = 2) writeonly buffer D {D_TYPE data_d[];}; #ifdef MUL_MAT_ID layout (binding = 3) readonly buffer IDS {int data_ids[];}; layout (binding = 4) readonly buffer Counts {int data_expert_count[];}; +// Fused MUL epilogue: one scale per (expert slot, token), broadcast down the M dimension. +// Always bound; p.fusion_flags == 0 means ignore it. +layout (binding = 5) readonly buffer FusedScale {float data_fscale[];}; #endif layout (push_constant) uniform parameter @@ -90,6 +93,7 @@ layout (push_constant) uniform parameter uint ne11; uint n_experts; uint hoist_row_ids; + uint fusion_flags; #else uint base_work_group_z; uint num_batches; @@ -394,9 +398,13 @@ void main() { if (row_i >= _ne1) break; const u16vec2 row_idx = row_ids[row_i - ic * BN]; - if (dr + cm_row * TM + store_r < p.M) { - data_d[row_idx.y * p.batch_stride_d + row_idx.x * p.stride_d + dr + cm_row * TM + store_r] = D_TYPE(coopmat_stage[warp_i * TM * TN + (col + store_c) * TM + store_r]); + const uint didx = row_idx.y * p.batch_stride_d + row_idx.x * p.stride_d + dr + cm_row * TM + store_r; + if (p.fusion_flags != 0) { + data_d[didx] = D_TYPE(float(coopmat_stage[warp_i * TM * TN + (col + store_c) * TM + store_r]) * data_fscale[row_idx.y * p.nei0 + row_idx.x]); + } else { + data_d[didx] = D_TYPE(coopmat_stage[warp_i * TM * TN + (col + store_c) * TM + store_r]); + } } } barrier(); @@ -449,15 +457,19 @@ void main() { if (row_i >= _ne1) break; const u16vec2 row_idx = row_ids[row_i - ic * BN]; + const bool do_scale = p.fusion_flags != 0; + const float fscale = do_scale ? data_fscale[row_idx.y * p.nei0 + row_idx.x] : 1.0f; #endif // MUL_MAT_ID [[unroll]] for (uint cr = 0; cr < TM / 2; cr++) { const uint sums_idx = (wsic * TN + cc) * WMITER * (TM / 2) + wsir * (TM / 2) + cr; #ifdef MUL_MAT_ID if (dr_warp + 2 * cr < p.M) { - data_d[row_idx.y * p.batch_stride_d + row_idx.x * p.stride_d + dr_warp + 2 * cr] = D_TYPE(sums[sums_idx].x); + const uint didx = row_idx.y * p.batch_stride_d + row_idx.x * p.stride_d + dr_warp + 2 * cr; + data_d[didx] = do_scale ? D_TYPE(float(sums[sums_idx].x) * fscale) : D_TYPE(sums[sums_idx].x); } if (dr_warp + 2 * cr + 1 < p.M) { - data_d[row_idx.y * p.batch_stride_d + row_idx.x * p.stride_d + dr_warp + 2 * cr + 1] = D_TYPE(sums[sums_idx].y); + const uint didx = row_idx.y * p.batch_stride_d + row_idx.x * p.stride_d + dr_warp + 2 * cr + 1; + data_d[didx] = do_scale ? D_TYPE(float(sums[sums_idx].y) * fscale) : D_TYPE(sums[sums_idx].y); } #else if (dr_warp + 2 * cr < p.M && dc_warp + cc < p.N) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_cm2.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_cm2.comp index 27f3178e7f26..9bfad031d1b6 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_cm2.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_cm2.comp @@ -106,6 +106,9 @@ layout (binding = 1) readonly buffer B4 {B_TYPEV4 data_b_v4[];}; #ifdef MUL_MAT_ID layout (binding = 3) readonly buffer IDS {int data_ids[];}; layout (binding = 4) readonly buffer Counts {int data_expert_count[];}; +// Bound for descriptor-layout parity with the other mul_mat_id shaders. The fused MUL +// epilogue is not implemented here, so the host never enables it on coopmat2 devices. +layout (binding = 5) readonly buffer FusedScale {float data_fscale[];}; shared u16vec4 row_ids[BN]; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp index 1fbcbf6c9332..b36d056c1c9c 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mmq.comp @@ -36,6 +36,8 @@ layout (binding = 2) writeonly buffer D {D_TYPE data_d[];}; #ifdef MUL_MAT_ID layout (binding = 3) readonly buffer IDS {int data_ids[];}; layout (binding = 4) readonly buffer Counts {int data_expert_count[];}; +// Fused MUL epilogue, see mul_mm.comp. +layout (binding = 5) readonly buffer FusedScale {float data_fscale[];}; #endif layout (push_constant) uniform parameter @@ -58,6 +60,7 @@ layout (push_constant) uniform parameter uint ne11; uint n_experts; uint hoist_row_ids; + uint fusion_flags; #else uint base_work_group_z; uint num_batches; @@ -303,7 +306,10 @@ void main() { const uint sums_idx = (wsic * TN + cc) * WMITER * TM + wsir * TM + cr; #ifdef MUL_MAT_ID if (dr_warp + cr < p.M) { - data_d[row_idx.y * p.batch_stride_d + row_idx.x * p.stride_d + dr_warp + cr] = D_TYPE(sums[sums_idx].x); + const uint didx = row_idx.y * p.batch_stride_d + row_idx.x * p.stride_d + dr_warp + cr; + data_d[didx] = p.fusion_flags != 0 + ? D_TYPE(float(sums[sums_idx].x) * data_fscale[row_idx.y * p.nei0 + row_idx.x]) + : D_TYPE(sums[sums_idx].x); } #else if (dr_warp + cr < p.M && dc_warp + cc < p.N) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index fe82b8fb6cb1..295e17b7b088 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -951,6 +951,7 @@ void process_shaders() { string_to_spv("concat_i16", "concat.comp", {{"A_TYPE", "uint16_t"}, {"B_TYPE", "uint16_t"}, {"D_TYPE", "uint16_t"}}); string_to_spv("concat_i32", "concat.comp", {{"A_TYPE", "uint"}, {"B_TYPE", "uint"}, {"D_TYPE", "uint"}}); string_to_spv("concat_i64", "concat.comp", {{"A_TYPE", "uvec2"}, {"B_TYPE", "uvec2"}, {"D_TYPE", "uvec2"}}); + string_to_spv("concat_transpose_i32", "concat_transpose.comp", {{"A_TYPE", "uint"}, {"B_TYPE", "uint"}, {"D_TYPE", "uint"}}); string_to_spv("upscale_f32", "upscale.comp", {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}}); From 874b9043d1b4e1761219bfcd2ec7f0c1e37703f1 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Thu, 6 Aug 2026 08:48:36 +0000 Subject: [PATCH 148/212] vulkan: reject ne[3] > 1 in the mul_mat_id scale epilogue The fused epilogue derives the scale index from row_ids as [token * nei0 + expert_slot], which carries no 4th dimension, but the gate admitted any ne[3] as long as the scale and the matmul agreed. A tensor with ne[3] > 1 would read the wrong scale for every batch past the first and return quietly wrong results. test-backend-ops never generates such a case, so the suite passed throughout; found by reading the gate against the shader. Co-Authored-By: Claude Opus 5 (cherry picked from commit 016e906788057b9734ab7727de645d58f8080716) Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 8f297cb66964..c51440149d74 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -19014,8 +19014,10 @@ static bool ggml_vk_can_fuse(const ggml_backend_vk_context * ctx, const struct g if (get_misalign_bytes(ctx, scale) != 0) { return false; } - return scale->ne[0] == 1 && - scale->ne[1] == mmid->ne[1] && scale->ne[2] == mmid->ne[2] && scale->ne[3] == mmid->ne[3] && + // The shader indexes the scale as [token * nei0 + expert_slot] from row_ids, which + // carries no 4th dimension, so ne[3] must be 1 or later batches read the wrong scale. + return scale->ne[0] == 1 && mmid->ne[3] == 1 && scale->ne[3] == 1 && + scale->ne[1] == mmid->ne[1] && scale->ne[2] == mmid->ne[2] && ggml_are_same_shape(mul, mmid); } // shaders assume the types match From e249248565e8fbd736f9ce9e2db5352734ad5844 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Mon, 17 Aug 2026 06:13:27 +0000 Subject: [PATCH 149/212] vulkan : default the transposed-concat path on The delta-net conv-state path transposes straight into a dim-0 concat, so the generic concat kernel walks src1 with a conv_channels * 4 byte stride. On qwen35 that is 40960 B, which is 160 * 256 B with 160 % 16 == 0, so every read lands on the same one of the 16 memory channels: 13.7 GB/s against 138.9 GB/s for the tiled path. The tiled-transpose route has been behind GGML_VK_CONCAT_TRANSPOSE=1 since it landed. Turn it on by default and keep GGML_VK_CONCAT_TRANSPOSE=0 as the opt-out. Qwen3.8-27B pp2048: +0.4% at ub 256, +4.7% at ub 1024, +7.2% at ub 2048. Assisted-by: Claude Opus 5 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index c51440149d74..52f986fb92e3 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -13022,13 +13022,15 @@ static vk_conv_shapes ggml_vk_conv_select_shape(ggml_backend_vk_context * ctx, u } } -// EXPERIMENT (GGML_VK_CONCAT_TRANSPOSE=1): the delta-net conv-state path does -// ggml_transpose() straight into a dim-0 ggml_concat(), so the generic concat kernel reads -// src1 fully de-coalesced. Measured on Qwen3.6-35B-A3B: CONCAT is ~22% of pp2048 at ub=2048 -// and grows 3.1x for a 2x ubatch. Route that exact shape to a tiled-transpose kernel. +// The delta-net conv-state path does ggml_transpose() straight into a dim-0 ggml_concat(), so +// the generic concat kernel walks src1 with a conv_channels * 4 byte stride. On qwen35 that is +// 40960 B = 160 * 256 B and 160 % 16 == 0, so every read lands on one of the 16 memory channels: +// 13.7 GB/s against 138.9 GB/s for the tiled path. Route that exact shape to a tiled-transpose +// kernel. On by default; GGML_VK_CONCAT_TRANSPOSE=0 opts out. +// Qwen3.8-27B pp2048: +0.4% at ub 256, +4.7% at ub 1024, +7.2% at ub 2048. static bool ggml_vk_concat_is_transposed(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst) { static const char * env = getenv("GGML_VK_CONCAT_TRANSPOSE"); - if (!(env && atoi(env) != 0)) { + if (env && env[0] == '0') { return false; } if (ggml_get_op_params_i32(dst, 0) != 0) { // dim 0 only From 7db0111d53cbcc6750f61c4d2fdcb10bc01ce7d0 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 2 Aug 2026 00:13:47 +0000 Subject: [PATCH 150/212] vulkan: flush pending compute ctx before perf logger timestamps The scheduler's async input copies between graph splits land in the compute ctx on devices without a separate transfer queue, so the perf logger's fresh-ctx assert fired under partial offload (--n-cpu-moe). Assisted-by: Claude Fable 5 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 52f986fb92e3..a4f619b20615 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -19630,6 +19630,12 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg std::fill(ctx->query_nodes.begin(), ctx->query_nodes.end(), nullptr); std::fill(ctx->query_node_idx.begin(), ctx->query_node_idx.end(), 0); + // Under partial offload the scheduler's async input copies between graph + // splits can leave commands in a pending compute ctx. Flush it so the + // timestamp stream starts on a fresh command buffer. + if (!ctx->compute_ctx.expired()) { + ggml_vk_synchronize(ctx); + } GGML_ASSERT(ctx->compute_ctx.expired()); compute_ctx = ggml_vk_get_compute_ctx(ctx); ctx->query_idx = 0; From 6ab7cb6b49316488600461dab94748b73290a6bb Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 2 Aug 2026 11:56:37 +0000 Subject: [PATCH 151/212] vulkan: bound command buffers by memory traffic, not just flops Nodes with no flops estimate (large copies, set_rows, mask fills) can pack a command buffer whose execution time grows with context length until it exceeds the amdgpu ring timeout (10s on the compute ring), causing the ring resets and DeviceLost reported at long context. Add a bytes-per-submit cap (default 8 GiB, GGML_VK_MAX_MB_PER_SUBMIT to override, 0 disables) alongside the existing flops and node-count gates. Assisted-by: Claude Fable 5 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 34 ++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index a4f619b20615..3e954312372c 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -869,6 +869,7 @@ struct vk_device_struct { bool add_rms_fusion; uint32_t partials_binding_alignment; uint32_t max_nodes_per_submit; + uint64_t max_bytes_per_submit; bool shader_64b_indexing; @@ -2302,6 +2303,20 @@ static bool vk_enable_sync_logger = false; static uint32_t vk_perf_logger_frequency = 1; static std::string vk_pipeline_stats_filter; +// Total memory traffic of a node (dst + srcs). Used to bound command buffer +// execution time for bandwidth-bound ops with no flops estimate (large copies, +// set_rows, mask fills at long context) - packing too many of them into one +// submission can exceed the driver timeout. +static uint64_t ggml_vk_get_node_bytes(const ggml_tensor * node) { + uint64_t bytes = ggml_nbytes(node); + for (int i = 0; i < GGML_MAX_SRC; i++) { + if (node->src[i]) { + bytes += ggml_nbytes(node->src[i]); + } + } + return bytes; +} + static uint64_t ggml_vk_get_node_flops(const ggml_tensor * node) { if (node->op == GGML_OP_MUL_MAT || node->op == GGML_OP_MUL_MAT_ID) { const uint64_t m = node->ne[0]; @@ -7225,6 +7240,15 @@ static vk_device ggml_vk_get_device(size_t idx) { device->max_nodes_per_submit = std::max(max_nodes_per_submit, 1u); } + // Also submit once a batch has accumulated enough memory traffic, so that + // bandwidth-bound nodes with no flops estimate cannot grow a command buffer + // past the driver timeout. 0 disables the limit. + device->max_bytes_per_submit = 8ull * 1024 * 1024 * 1024; + const char* GGML_VK_MAX_MB_PER_SUBMIT = getenv("GGML_VK_MAX_MB_PER_SUBMIT"); + if (GGML_VK_MAX_MB_PER_SUBMIT != nullptr) { + device->max_bytes_per_submit = std::stoull(GGML_VK_MAX_MB_PER_SUBMIT) * 1024 * 1024; + } + const bool force_disable_f16 = getenv("GGML_VK_DISABLE_F16") != nullptr; device->fp16 = !force_disable_f16 && fp16_storage && fp16_compute; @@ -19662,6 +19686,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg uint32_t submitted_nodes = 0; uint32_t submit_count = 0; uint64_t batch_flops = 0; + uint64_t batch_bytes = 0; uint64_t total_flops = 0; uint64_t flops_cap = 200'000'000'000ULL; @@ -19699,6 +19724,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg first_node_in_batch = true; submitted_nodes = 0; batch_flops = 0; + batch_bytes = 0; if (submit_count < 3) { flops_per_submit *= 2; } @@ -19713,9 +19739,11 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg { auto node_flops = ggml_vk_get_node_flops(cgraph->nodes[i]); total_flops += node_flops; + auto node_bytes = ggml_vk_get_node_bytes(cgraph->nodes[i]); - // Flush the current batch before recording a node that would push it over the flop threshold - if (flops_per_submit != 0 && submitted_nodes > 0 && batch_flops + node_flops >= flops_per_submit) { + // Flush the current batch before recording a node that would push it over the flop or byte threshold + if ((flops_per_submit != 0 && submitted_nodes > 0 && batch_flops + node_flops >= flops_per_submit) || + (ctx->device->max_bytes_per_submit != 0 && submitted_nodes > 0 && batch_bytes + node_bytes >= ctx->device->max_bytes_per_submit)) { vk_context flush_ctx = ggml_vk_get_compute_ctx(ctx); ggml_vk_ctx_end(flush_ctx); flush_ctx->exit_tensor_idx = -1; @@ -19726,6 +19754,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg } batch_flops += node_flops; + batch_bytes += node_bytes; } // op_srcs_fused_elementwise indicates whether an op's srcs all contribute to @@ -19958,6 +19987,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg bool almost_ready = (cgraph->n_nodes - i) < cgraph->n_nodes / 5; bool submit = (submitted_nodes >= ctx->device->max_nodes_per_submit) || (flops_per_submit != 0 && batch_flops >= flops_per_submit) || + (ctx->device->max_bytes_per_submit != 0 && batch_bytes >= ctx->device->max_bytes_per_submit) || (i + ctx->num_additional_fused_ops >= last_node) || (almost_ready && !ctx->almost_ready_fence_pending); From e38725201c560ab333e561c830ce960060a1fcce Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 9 Aug 2026 02:48:56 +0000 Subject: [PATCH 152/212] ggml: cut backend splits on the input constant, not the grown capacity #22789 replaced the fixed 30-entry split input array with a growable one and, in the same edit, changed the split-cutting heuristic from the constant to split->inputs_capacity: - if (split->n_inputs == GGML_SCHED_MAX_SPLIT_INPUTS) { + if (split->n_inputs >= split->inputs_capacity) { inputs_capacity starts at GGML_SCHED_MAX_SPLIT_INPUTS but doubles on demand and is never reset for the life of the sched, so once a split slot grows, the scheduler stops cutting there and the cut point ratchets up for every later graph build. Longer splits mean every cross-backend input copy is materialised at the split's start and stays live to its last use inside it, which raises the peak the compute-buffer allocator has to cover - n_copies times over under pipeline parallelism. Only multi-backend configurations can reach this. Keep the growable array, which is what fixes the original >30-input assert, and cut on the constant again as before #22789. >= rather than == so the check keeps firing for splits that did have to grow. DeepSeek-V4-Flash UD-IQ3_XXS, gfx1151, -c 400000 -ub 2048 -fa 1 --fit off: Vulkan0 compute buffer 4714.00 MiB and 9157 graph nodes, byte-identical to the unpatched tree, and neither run grows a split past 30 inputs. Expected - one Vulkan device plus the CPU backend cannot exercise the path on this box. The reported case is 3 devices with pipeline parallelism. test-backend-ops -o FLASH_ATTN_EXT, run alone on gfx1151: 13257/13295 on both this and the unpatched tree, with the same 38 failing cases (identical case list, all type_K=q8_0 prec=def kv_view=1). Pre-existing on the branch, not touched by this change. Co-Authored-By: Claude Opus 5 Assisted-by: Claude (Opus 5) --- ggml/src/ggml-backend.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-backend.cpp b/ggml/src/ggml-backend.cpp index dc6b98946e20..0955a431b171 100644 --- a/ggml/src/ggml-backend.cpp +++ b/ggml/src/ggml-backend.cpp @@ -1446,7 +1446,10 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra } // check if the split has too many inputs // FIXME: count the number of inputs instead of only checking when full - if (split->n_inputs >= split->inputs_capacity) { + // cut on the constant, not on inputs_capacity: capacity doubles on demand and + // is never reset, so using it lets the cut point drift up and keeps every input + // copy of an ever-longer split live at once + if (split->n_inputs >= GGML_SCHED_MAX_SPLIT_INPUTS) { const size_t id = hash_id(src); int src_backend_id = sched->hv_tensor_backend_ids[id]; bool supported = ggml_backend_sched_buffer_supported(sched, src, cur_backend_id); From 492e443eac327a170104ac37147b8bd20e364c5f Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 16 Aug 2026 11:49:48 +0000 Subject: [PATCH 153/212] vulkan : optional f16 B operand for quantized MUL_MAT on coopmat1 The _f16 SPIR-V and pipeline_dequant_mul_mat_mat_f16 already exist, but are only populated in the coopmat2 branch. GGML_VK_DENSE_F16B populates them for coopmat1 too and routes B through the existing convert-to-prealloc_y path. Off by default: it helps large dense models and costs a little elsewhere. gfx1151, pp2048: Qwen3.8-27B and Qwen3-32B (hidden 5120) +5 to +7% for both q6_K and q8_0 weights, Qwen2.5-7B -1.2%, Qwen3-Coder-30B MoE -0.5%. Decode is untouched, ne1==1 does not reach this path. Numerically identical: mul_mm stages B into shared FLOAT_TYPE either way, so the f32-B kernel already rounds B to f16. Wikitext PPL matches to 4 dp. Assisted-by: Claude Opus 5 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 45 ++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 3e954312372c..8eee6436d7f7 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4498,6 +4498,11 @@ static bool ggml_vk_mmid_f16b_enabled() { const char * env = getenv("GGML_VK_MMID_F16B"); return env == nullptr || atoi(env) != 0; // on by default; =0 disables }(); +// GGML_VK_DENSE_F16B=1: same idea as GGML_VK_MMID_F16B but for plain MUL_MAT. Halves the B +// bytes, which keeps the activations inside the LLC at large ubatch and moves their row stride +// off the 1-of-16 channel pattern. Off by default, it is a loss when B already fits. +static bool ggml_vk_dense_f16b_enabled() { + static const bool enabled = getenv("GGML_VK_DENSE_F16B") != nullptr; return enabled; } @@ -5205,6 +5210,21 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { CREATE_MM2(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q4_K], matmul_q4_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_Q5_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_K], matmul_q5_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_Q6_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q6_K], matmul_q6_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + + // f16-B variants of the same quant pipelines. The _f16 SPIR-V is already built for + // every type; upstream only instantiates it in the coopmat2 branch. + if (ggml_vk_dense_f16b_enabled()) { + CREATE_MM2(GGML_TYPE_Q4_0, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q4_0], matmul_q4_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q4_1, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q4_1], matmul_q4_1_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q5_0, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q5_0], matmul_q5_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q5_1, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q5_1], matmul_q5_1_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q8_0], matmul_q8_0_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q2_K], matmul_q2_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q3_K], matmul_q3_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q4_K], matmul_q4_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q5_K, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q5_K], matmul_q5_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + CREATE_MM2(GGML_TYPE_Q6_K, pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q6_K], matmul_q6_k_f16, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); + } CREATE_MM2(GGML_TYPE_IQ1_S, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ1_S], matmul_iq1_s_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_IQ1_M, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ1_M], matmul_iq1_m_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); CREATE_MM2(GGML_TYPE_IQ2_XXS, pipeline_dequant_mul_mat_mat[GGML_TYPE_IQ2_XXS], matmul_iq2_xxs_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, ); @@ -8532,7 +8552,8 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_pipeline(ggml_backend_vk_conte return pipelines; } - if (src1_type != GGML_TYPE_F32 && !ctx->device->coopmat2) { + if (src1_type != GGML_TYPE_F32 && !ctx->device->coopmat2 && + !(src1_type == GGML_TYPE_F16 && ggml_vk_dense_f16b_enabled())) { return nullptr; } @@ -8576,7 +8597,9 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_pipeline(ggml_backend_vk_conte return prec == GGML_PREC_DEFAULT ? ctx->device->pipeline_dequant_mul_mat_mat_f16[src0_type].f16acc : ctx->device->pipeline_dequant_mul_mat_mat_f16[src0_type].f32acc; } if (ctx->device->coopmat_support) { - return (ctx->device->fp16 && ctx->device->coopmat_acc_f16_support && prec == GGML_PREC_DEFAULT) ? ctx->device->pipeline_dequant_mul_mat_mat[src0_type].f16acc : ctx->device->pipeline_dequant_mul_mat_mat[src0_type].f32acc; + vk_matmul_pipeline2 & p = (src1_type == GGML_TYPE_F16) ? ctx->device->pipeline_dequant_mul_mat_mat_f16[src0_type] + : ctx->device->pipeline_dequant_mul_mat_mat[src0_type]; + return (ctx->device->fp16 && ctx->device->coopmat_acc_f16_support && prec == GGML_PREC_DEFAULT) ? p.f16acc : p.f32acc; } return (ctx->device->fp16 && prec == GGML_PREC_DEFAULT) ? ctx->device->pipeline_dequant_mul_mat_mat[src0_type].f16acc : ctx->device->pipeline_dequant_mul_mat_mat[src0_type].f32acc; } @@ -10071,7 +10094,23 @@ static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& sub // Reformat and convert to fp16 if non-contiguous, or for coopmat2 for better perf const bool x_non_contig = (ctx->device->coopmat2 && src0->type == GGML_TYPE_F32) || !ggml_vk_dim01_contiguous(src0); - const bool y_non_contig = (ctx->device->coopmat2 && src1->type == GGML_TYPE_F32) || + // Route quantized MUL_MAT through the f16-B kernels. Treating contiguous f32 B as + // y_non_contig reuses the convert-to-prealloc_y plumbing, like coopmat2 does. + const bool dense_f16b = ggml_vk_dense_f16b_enabled() && + ctx->device->coopmat_support && !ctx->device->coopmat2 && + ggml_is_quantized(src0->type) && src1->type == GGML_TYPE_F32 && + !(ctx->device->pipeline_dequant_mul_mat_mat_f16[src0->type].f16acc->is_empty() && + ctx->device->pipeline_dequant_mul_mat_mat_f16[src0->type].f32acc->is_empty()); + if (dense_f16b) { + static bool dense_f16b_logged = false; + if (!dense_f16b_logged) { + dense_f16b_logged = true; + fprintf(stderr, "ggml_vulkan: MUL_MAT f16-B path engaged (GGML_VK_DENSE_F16B)\n"); + } + } + + const bool y_non_contig = dense_f16b || + (ctx->device->coopmat2 && src1->type == GGML_TYPE_F32) || (src0->type == GGML_TYPE_BF16 && src1->type != GGML_TYPE_BF16) || !ggml_vk_dim01_contiguous(src1); From d737bd5f72b30c920ee398136971161c4b4be49b Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Sun, 16 Aug 2026 14:34:15 +0000 Subject: [PATCH 154/212] vulkan : add auto mode to GGML_VK_DENSE_F16B =auto restricts the f16-B path to ne10 == 5120, the only reduction width measured to gain, so it cannot fire on the widths that lose. =1 keeps the old all-shapes behaviour as a manual override. gfx1151, Qwen3.8-27B UD-Q6_K_XL pp2048, auto vs off: +5.8 / +6.0 / +5.9 / +5.3% at ub 256/512/1024/2048, which is 97% of the all-shapes win at ub256 and 82-86% above it. Qwen3-Coder-30B MoE is untouched, the gate never fires. The width equality is a stopgap until a per-shape predicate is derived; it will silently do nothing for a dense model of another width. Assisted-by: Claude Opus 5 --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 8eee6436d7f7..6c97e1b4d46e 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4498,14 +4498,25 @@ static bool ggml_vk_mmid_f16b_enabled() { const char * env = getenv("GGML_VK_MMID_F16B"); return env == nullptr || atoi(env) != 0; // on by default; =0 disables }(); -// GGML_VK_DENSE_F16B=1: same idea as GGML_VK_MMID_F16B but for plain MUL_MAT. Halves the B -// bytes, which keeps the activations inside the LLC at large ubatch and moves their row stride -// off the 1-of-16 channel pattern. Off by default, it is a loss when B already fits. -static bool ggml_vk_dense_f16b_enabled() { - static const bool enabled = getenv("GGML_VK_DENSE_F16B") != nullptr; return enabled; } +// GGML_VK_DENSE_F16B: same idea as GGML_VK_MMID_F16B but for plain MUL_MAT. Halves the B bytes +// moved. Numerically identical: mul_mm stages B into shared FLOAT_TYPE either way, so the f32-B +// kernel already rounds B to f16. Helps wide dense models, costs ~1% on narrow ones. +// 0 = off, 1 = all quantized dense matmuls, 2 = auto (only the K we have positive data for) +static int ggml_vk_dense_f16b_mode() { + static const int mode = [] { + const char * e = getenv("GGML_VK_DENSE_F16B"); + if (e == nullptr) return 0; + if (e[0] == 'a') return 2; + return atoi(e) != 0 ? 1 : 0; + }(); + return mode; +} + +static bool ggml_vk_dense_f16b_enabled() { return ggml_vk_dense_f16b_mode() != 0; } + static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { VK_LOG_DEBUG("ggml_vk_load_shaders(" << device->name << ")"); @@ -10096,7 +10107,10 @@ static void ggml_vk_mul_mat_q_f16(ggml_backend_vk_context * ctx, vk_context& sub !ggml_vk_dim01_contiguous(src0); // Route quantized MUL_MAT through the f16-B kernels. Treating contiguous f32 B as // y_non_contig reuses the convert-to-prealloc_y plumbing, like coopmat2 does. + // auto mode restricts to ne10 == 5120: the only width measured to gain. Narrow on purpose, + // so widths measured as losses (3584 dense, 2048 MoE) cannot trigger it. const bool dense_f16b = ggml_vk_dense_f16b_enabled() && + (ggml_vk_dense_f16b_mode() == 1 || ne10 == 5120) && ctx->device->coopmat_support && !ctx->device->coopmat2 && ggml_is_quantized(src0->type) && src1->type == GGML_TYPE_F32 && !(ctx->device->pipeline_dequant_mul_mat_mat_f16[src0->type].f16acc->is_empty() && From 9f5ece3a5ae1a3417be0d45cc18ac4797a6b7a68 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 8 Sep 2026 09:46:19 +0000 Subject: [PATCH 155/212] vulkan: derive the mul_mat_id tiles from the wave64 dense tiles The dense wave32 change rewrites s/m/l_warptile_mmq in place (WARP=32, WM/WN halved). The mul_mat_id tile block copied those rewritten vectors, so with GGML_VK_MMID_WAVE32=0 the mmid pipelines carried a WARP=32 spec constant but no required subgroup size and ran at wave64 with the wrong warp grid: test-backend-ops MUL_MAT_ID 700/7432 for every quantised type at n >= 16 (garbage output, not an error). Default settings were not affected; the opt-out was. Snapshot the dense tiles before the wave32 rewrite and build the mmid tiles from the snapshot. When the mmid wave32 pin is not applied, assert the coverage invariant and require the tile's own WARP as the subgroup size where the driver honours it, so the two can never disagree. Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 6c97e1b4d46e..d22e355aac1e 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -5136,6 +5136,12 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { const char * dense_wave32_env = getenv("GGML_VK_DENSE_WAVE32"); const int dense_wave32 = dense_wave32_env ? atoi(dense_wave32_env) : 1; + // The mul_mat_id tiles below start from these. Snapshot them before the dense + // wave32 rewrite so an mmid pipeline created without a required subgroup size + // (GGML_VK_MMID_WAVE32=0) keeps a WARP that matches the real wave64 subgroup. + const auto l_warptile_mmq_w64 = l_warptile_mmq; + const auto m_warptile_mmq_w64 = m_warptile_mmq; + const auto s_warptile_mmq_w64 = s_warptile_mmq; if (dense_wave32_possible && dense_wave32 != 0) { auto wave32_tile = [](std::vector & w) -> bool { // {BLOCK_SIZE, BM, BN, BK, WM, WN, WMITER, TM, TN, TK, WARP} @@ -5278,11 +5284,11 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { // so even the 32-wide small tile runs half empty. Only meaningful stacked on // GGML_VK_MMID_SMALLN=1 (routes mmid to the small tile) + the row-list prepass. // Shadows the s-tile config for the mmid quant pipelines only; dense unaffected. - auto s_warptile_mmq_id16 = s_warptile_mmq; + auto s_warptile_mmq_id16 = s_warptile_mmq_w64; auto s_mmq_wg_denoms_id16 = s_mmq_wg_denoms; - auto m_warptile_mmq_id128 = m_warptile_mmq; + auto m_warptile_mmq_id128 = m_warptile_mmq_w64; auto m_mmq_wg_denoms_id128 = m_mmq_wg_denoms; - auto l_warptile_mmq_idw = l_warptile_mmq; + auto l_warptile_mmq_idw = l_warptile_mmq_w64; uint32_t mmid_req_sgs = 0; { const char * tile16_env = getenv("GGML_VK_MMID_TILE16"); @@ -5344,6 +5350,19 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { wave32_tile(s_warptile_mmq_id16); wave32_tile(m_warptile_mmq_id128); wave32_tile(l_warptile_mmq_idw); + } else { + // Wave64 stack: the tiles are the dense wave64 tiles plus the BM64/M128 reshapes, + // which keep NUM_WARPS == (BM/WM)*(BN/WN). Pin the subgroup to the tile's WARP + // where the driver honours it, so the spec constant can never disagree with + // the real subgroup size. + for (const auto * w : { &s_warptile_mmq_id16, &m_warptile_mmq_id128, &l_warptile_mmq_idw }) { + GGML_ASSERT((*w)[0] / (*w)[10] == ((*w)[1] / (*w)[4]) * ((*w)[2] / (*w)[5])); + } + const uint32_t warp = s_warptile_mmq_id16[10]; + if (device->subgroup_size_control && warp == m_warptile_mmq_id128[10] && warp == l_warptile_mmq_idw[10] && + device->subgroup_min_size <= warp && warp <= device->subgroup_max_size) { + mmid_req_sgs = warp; + } } } { From 1debd524b05de9502a318386e737c76366986793 Mon Sep 17 00:00:00 2001 From: Nathan Wilson Date: Tue, 8 Sep 2026 10:56:08 +0000 Subject: [PATCH 156/212] vulkan: keep the coopmat1 FA wave32 pin to multi-row dispatches Decode dispatches carry N = gqa_ratio query rows and the narrow subgroup loses there: Qwen3-Coder-30B q8_0 KV tg64 at d8192/d32768 measured 3.5 to 4 percent slower with the pin than without, while the prefill gain it was introduced for (up to 10 percent at d8192) needs the multi-row shapes. Apply the rule only when n_rows >= 32. Assisted-by: Claude (Opus 5) --- ggml/src/ggml-vulkan/ggml-vulkan.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index d22e355aac1e..353edb87c466 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -4056,7 +4056,12 @@ static vk_fa_tuning_params get_fa_tuning_params_coopmat1(const vk_device& device const char * e = getenv("GGML_VK_FA_WAVE32"); return e ? atoi(e) : 1; }(); + // Multi-row dispatches only. Decode dispatches carry N = gqa_ratio rows (8 on + // Qwen3-Coder-30B) and there the narrow subgroup loses: tg64 at d8192/d32768 measured + // 3.5 to 4 percent slower with the pin (q8_0 KV), while prefill gains up to 10 percent + // at d8192. Keep the pin to the prefill shapes it was measured on. if (fa_wave32 != 0 && + n_rows >= 32 && device->subgroup_size_control && 32 < device->subgroup_size && // narrow only, never widen device->subgroup_min_size <= 32 && 32 <= device->subgroup_max_size && From f2431a23a2cc059c1d1ed599396aa3b5d6a4342a Mon Sep 17 00:00:00 2001 From: Local Dev Date: Mon, 14 Sep 2026 16:29:14 +0800 Subject: [PATCH 157/212] server : rename --cache-disk to --cache-dir The flag takes a filesystem directory, not a block device, so "disk" is a misnomer; it also collides with a plausible future raw-device cache. Rename --cache-disk/--cache-disk-max to --cache-dir/--cache-dir-max (env LLAMA_ARG_CACHE_DIR/_MAX) to match the existing --cache-ram/--cache-reuse family, and clarify in the help text that this is not the model download cache directory. Suggested by @lee-b. No behavior change. Assisted-by: Codex Assisted-by: Claude Opus 5 --- common/arg.cpp | 18 +++++----- common/common.h | 4 +-- tools/server/README.md | 4 +-- tools/server/server-context.cpp | 24 ++++++------- .../tests/unit/test_prompt_cache_disk.py | 36 +++++++++---------- tools/server/tests/utils.py | 12 +++---- 6 files changed, 49 insertions(+), 49 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index 6fcc067357ad..938818893194 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -1718,22 +1718,22 @@ common_params_context common_params_parser_init(common_params & params, llama_ex } ).set_env("LLAMA_ARG_CACHE_RAM").set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI})); add_opt(common_arg( - {"--cache-disk"}, "PATH", - "persist prompt cache state under this directory and restore it after server restart", + {"--cache-dir"}, "PATH", + "persist prompt cache state in this directory (not the model download cache) and restore it after server restart", [](common_params & params, const std::string & value) { - params.cache_disk_path = value; + params.cache_dir_path = value; } - ).set_env("LLAMA_ARG_CACHE_DISK").set_examples({LLAMA_EXAMPLE_SERVER})); + ).set_env("LLAMA_ARG_CACHE_DIR").set_examples({LLAMA_EXAMPLE_SERVER})); add_opt(common_arg( - {"--cache-disk-max"}, "N", - string_format("set the maximum disk cache size in MiB (default: %d, -1 - no limit, 0 - disable)", params.cache_disk_max_mib), + {"--cache-dir-max"}, "N", + string_format("set the maximum persistent prompt cache size in MiB (default: %d, -1 - no limit, 0 - disable)", params.cache_dir_max_mib), [](common_params & params, int value) { if (value < -1) { - throw std::invalid_argument("cache-disk-max must be -1 or non-negative"); + throw std::invalid_argument("cache-dir-max must be -1 or non-negative"); } - params.cache_disk_max_mib = value; + params.cache_dir_max_mib = value; } - ).set_env("LLAMA_ARG_CACHE_DISK_MAX").set_examples({LLAMA_EXAMPLE_SERVER})); + ).set_env("LLAMA_ARG_CACHE_DIR_MAX").set_examples({LLAMA_EXAMPLE_SERVER})); add_opt(common_arg( {"-kvu", "--kv-unified"}, {"-no-kvu", "--no-kv-unified"}, diff --git a/common/common.h b/common/common.h index e2afda5be8cb..10d0f127c364 100644 --- a/common/common.h +++ b/common/common.h @@ -630,9 +630,9 @@ struct common_params { 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. - int32_t cache_disk_max_mib = -1; // -1 = no limit, 0 - disable, 1 = 1 MiB, etc. + int32_t cache_dir_max_mib = -1; // -1 = no limit, 0 - disable, 1 = 1 MiB, etc. - std::string cache_disk_path; + std::string cache_dir_path; std::string hostname = "127.0.0.1"; std::string public_path = ""; // NOLINT diff --git a/tools/server/README.md b/tools/server/README.md index 634dcdaf0b64..65b2b3f277b3 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -167,8 +167,8 @@ For the full list of features, please refer to [server's changelog](https://gith | `-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)
(env: LLAMA_ARG_CTX_CHECKPOINTS) | | `-cms, --checkpoint-min-step N` | minimum spacing between context checkpoints in tokens (default: 8192, 0 = no minimum)
(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)
(env: LLAMA_ARG_CACHE_RAM) | -| `--cache-disk PATH` | persist prompt cache state under this directory and restore it after server restart
(env: LLAMA_ARG_CACHE_DISK) | -| `--cache-disk-max N` | set the maximum disk cache size in MiB (default: -1, -1 - no limit, 0 - disable)
(env: LLAMA_ARG_CACHE_DISK_MAX) | +| `--cache-dir PATH` | persist prompt cache state in this directory (not the model download cache) and restore it after server restart
(env: LLAMA_ARG_CACHE_DIR) | +| `--cache-dir-max N` | set the maximum persistent prompt cache size in MiB (default: -1, -1 - no limit, 0 - disable)
(env: LLAMA_ARG_CACHE_DIR_MAX) | | `-kvu, --kv-unified, -no-kvu, --no-kv-unified` | use single unified KV buffer shared across all sequences (default: enabled if number of slots is auto)
(env: LLAMA_ARG_KV_UNIFIED) | | `--cache-idle-slots, --no-cache-idle-slots` | save idle slots to the prompt cache on new task, and clear them when using unified KV (default: enabled, requires a prompt cache)
(env: LLAMA_ARG_CACHE_IDLE_SLOTS) | | `--context-shift, --no-context-shift` | whether to use context shift on infinite text generation (default: disabled)
(env: LLAMA_ARG_CONTEXT_SHIFT) | diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index c4bcc47a2d0a..7b84a25cb83e 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -1454,23 +1454,23 @@ struct server_context_impl { batch.init(std::max(n_batch, params_base.n_parallel), n_embd); } - const bool cache_disk_enabled = !params_base.cache_disk_path.empty() && params_base.cache_disk_max_mib != 0; - const bool prompt_cache_enabled = cache_disk_enabled || params_base.cache_ram_mib != 0; + const bool cache_dir_enabled = !params_base.cache_dir_path.empty() && params_base.cache_dir_max_mib != 0; + const bool prompt_cache_enabled = cache_dir_enabled || params_base.cache_ram_mib != 0; if (prompt_cache_enabled) { - const int32_t cache_limit_mib = cache_disk_enabled ? params_base.cache_disk_max_mib : params_base.cache_ram_mib; - const size_t cache_limit_tokens = cache_disk_enabled ? 0 : n_ctx; + const int32_t cache_limit_mib = cache_dir_enabled ? params_base.cache_dir_max_mib : params_base.cache_ram_mib; + const size_t cache_limit_tokens = cache_dir_enabled ? 0 : n_ctx; - if (cache_disk_enabled) { + if (cache_dir_enabled) { std::error_code ec; - std::filesystem::create_directories(params_base.cache_disk_path, ec); - const bool cache_dir_valid = !ec && std::filesystem::is_directory(params_base.cache_disk_path, ec); + std::filesystem::create_directories(params_base.cache_dir_path, ec); + const bool cache_dir_valid = !ec && std::filesystem::is_directory(params_base.cache_dir_path, ec); if (ec || !cache_dir_valid) { SRV_ERR("failed to create prompt cache directory %s: %s\n", - params_base.cache_disk_path.c_str(), ec ? ec.message().c_str() : "path is not a directory"); + params_base.cache_dir_path.c_str(), ec ? ec.message().c_str() : "path is not a directory"); return false; } - SRV_TRC("prompt cache is enabled on disk: %s\n", params_base.cache_disk_path.c_str()); + SRV_TRC("prompt cache is enabled on disk: %s\n", params_base.cache_dir_path.c_str()); } else { SRV_TRC("%s", "prompt cache is enabled in RAM\n"); } @@ -1484,11 +1484,11 @@ struct server_context_impl { prompt_cache = std::make_unique( cache_limit_mib, cache_limit_tokens, - cache_disk_enabled ? params_base.cache_disk_path : "", - cache_disk_enabled ? server_prompt_cache_key(params_base, ctx_tgt, ctx_dft) : "", + cache_dir_enabled ? params_base.cache_dir_path : "", + cache_dir_enabled ? server_prompt_cache_key(params_base, ctx_tgt, ctx_dft) : "", mctx != nullptr); } else { - SRV_TRC("%s", "prompt cache is disabled - use `--cache-ram N` or `--cache-disk PATH` to enable it\n"); + SRV_TRC("%s", "prompt cache is disabled - use `--cache-ram N` or `--cache-dir PATH` to enable it\n"); } SRV_TRC("%s", "for more info see https://github.com/ggml-org/llama.cpp/pull/16391\n"); diff --git a/tools/server/tests/unit/test_prompt_cache_disk.py b/tools/server/tests/unit/test_prompt_cache_disk.py index 2623705dca37..956c55b7de74 100644 --- a/tools/server/tests/unit/test_prompt_cache_disk.py +++ b/tools/server/tests/unit/test_prompt_cache_disk.py @@ -89,8 +89,8 @@ def meta_checkpoint_count(path) -> int: def test_disk_cache_restores_across_server_restart(tmp_path): server = make_server() server.cache_ram = 0 - server.cache_disk = str(tmp_path) - server.cache_disk_max = 256 + server.cache_dir = str(tmp_path) + server.cache_dir_max = 256 server.start() timings_full = completion(server, LONG_PROMPT, 0) @@ -125,8 +125,8 @@ def test_disk_cache_removes_incomplete_and_invalid_entries(tmp_path): server = make_server() server.cache_ram = 0 - server.cache_disk = str(tmp_path) - server.cache_disk_max = 256 + server.cache_dir = str(tmp_path) + server.cache_dir_max = 256 server.start() assert not incomplete.exists() @@ -138,8 +138,8 @@ def test_disk_cache_removes_incomplete_and_invalid_entries(tmp_path): def test_disk_cache_size_limit_keeps_recent_entries(tmp_path): server = make_server() server.cache_ram = 0 - server.cache_disk = str(tmp_path) - server.cache_disk_max = -1 + server.cache_dir = str(tmp_path) + server.cache_dir_max = -1 server.start() completion(server, LONG_PROMPT, 0) @@ -152,7 +152,7 @@ def test_disk_cache_size_limit_keeps_recent_entries(tmp_path): for path in cache_files(tmp_path): path.unlink() - server.cache_disk_max = max(1, math.ceil(2.25 * entry_size / MIB)) + server.cache_dir_max = max(1, math.ceil(2.25 * entry_size / MIB)) server.start() prompts = [f"Cache entry {index}. {LONG_PROMPT}" for index in range(4)] @@ -160,7 +160,7 @@ def test_disk_cache_size_limit_keeps_recent_entries(tmp_path): completion(server, prompt, index % 2) completion(server, "Move the last prompt into the disk cache.", 0) - cache_limit = server.cache_disk_max * MIB + cache_limit = server.cache_dir_max * MIB assert sum(path.stat().st_size for path in cache_files(tmp_path)) <= cache_limit assert len(cache_data_files(tmp_path)) <= 2 @@ -175,8 +175,8 @@ def test_disk_cache_strict_extension_matches_predicted_reuse(tmp_path, monkeypat server = make_server() server.n_predict = 1 server.cache_ram = 0 - server.cache_disk = str(tmp_path) - server.cache_disk_max = 256 + server.cache_dir = str(tmp_path) + server.cache_dir_max = 256 server.log_path = str(tmp_path / "server.log") server.start() @@ -209,8 +209,8 @@ def test_disk_cache_no_usable_checkpoint_skips_candidate(tmp_path, monkeypatch): server = make_server() server.n_predict = 1 server.cache_ram = 0 - server.cache_disk = str(tmp_path) - server.cache_disk_max = 256 + server.cache_dir = str(tmp_path) + server.cache_dir_max = 256 server.start() cached_prompt = LONG_PROMPT + " This cached branch ends at the red gate." @@ -240,8 +240,8 @@ def test_disk_cache_no_usable_checkpoint_skips_candidate(tmp_path, monkeypatch): def test_disk_cache_selector_prefers_smaller_equal_prefix(tmp_path): server = make_pure_attention_server() server.cache_ram = 0 - server.cache_disk = str(tmp_path) - server.cache_disk_max = 256 + server.cache_dir = str(tmp_path) + server.cache_dir_max = 256 server.start() shared = LONG_PROMPT[:len(LONG_PROMPT) // 4] @@ -286,8 +286,8 @@ def test_disk_cache_selector_prefers_smaller_equal_prefix(tmp_path): def test_disk_cache_reuses_long_entry_below_keep_threshold(tmp_path): server = make_pure_attention_server() server.cache_ram = 0 - server.cache_disk = str(tmp_path) - server.cache_disk_max = 256 + server.cache_dir = str(tmp_path) + server.cache_dir_max = 256 server.start() shared = LONG_PROMPT[:len(LONG_PROMPT) // 8] @@ -333,8 +333,8 @@ def test_disk_cache_does_not_load_when_prompt_cache_disabled(tmp_path): server = make_server() server.n_predict = 1 server.cache_ram = 0 - server.cache_disk = str(tmp_path) - server.cache_disk_max = 256 + server.cache_dir = str(tmp_path) + server.cache_dir_max = 256 server.start() completion(server, LONG_PROMPT, 0) diff --git a/tools/server/tests/utils.py b/tools/server/tests/utils.py index d748aafd32ed..d9472a8ea0bb 100644 --- a/tools/server/tests/utils.py +++ b/tools/server/tests/utils.py @@ -113,8 +113,8 @@ class ServerProcess: media_path: str | None = None sleep_idle_seconds: int | None = None cache_ram: int | None = None - cache_disk: str | None = None - cache_disk_max: int | None = None + cache_dir: str | None = None + cache_dir_max: int | None = None no_cache_idle_slots: bool = False log_path: str | None = None ui_mcp_proxy: bool = False @@ -280,10 +280,10 @@ def start(self, timeout_seconds: int = DEFAULT_HTTP_TIMEOUT) -> None: server_args.extend(["--sleep-idle-seconds", self.sleep_idle_seconds]) if self.cache_ram is not None: server_args.extend(["--cache-ram", self.cache_ram]) - if self.cache_disk is not None: - server_args.extend(["--cache-disk", self.cache_disk]) - if self.cache_disk_max is not None: - server_args.extend(["--cache-disk-max", self.cache_disk_max]) + if self.cache_dir is not None: + server_args.extend(["--cache-dir", self.cache_dir]) + if self.cache_dir_max is not None: + server_args.extend(["--cache-dir-max", self.cache_dir_max]) if self.no_cache_idle_slots: server_args.append("--no-cache-idle-slots") if self.ui_mcp_proxy: From b009b797278b2a89cfe3eea024c17d16e8f81da1 Mon Sep 17 00:00:00 2001 From: Simon Teixidor Date: Wed, 9 Sep 2026 22:24:48 +0200 Subject: [PATCH 158/212] llama: flatten the GDN output projection input to 2-D build_layer_attn_linear reshapes the gated-norm result to [d, n_seq_tokens, n_seqs] before the ssm_out projection and then flattens the projection result to [n_embd, n_seq_tokens*n_seqs] on the very next line. The 3-D shape therefore carries no information past the mul_mat, but it does decide how the mul_mat is dispatched: src1 with ne12 = n_seqs is a batch of n_seqs matmuls of ne11 = n_seq_tokens columns each, and every one of them re-reads the whole ssm_out weight. Reshaping to [d, n_seq_tokens*n_seqs] instead gives a single matmul of ne11 = n_seq_tokens*n_seqs columns over one weight read. Mathematically identical: mul_mat is per-column, attn_out_norm is contiguous (the 3-D reshape already required that), and the columns are in the same order in both layouts, so the flattened call computes the same columns in the same order. The reshape_2d after the projection becomes shape-preserving. At n_seqs = 6 this is the difference between a batched call and six unbatched ones on every backend, over 48 of these projections per decode step on the 27B models. Note this only helps n_seqs > 1: one sequence with n_seq_tokens speculative tokens already arrives as a single ne11 = n_seq_tokens matmul. Only qwen35 and qwen35moe are changed, the architectures this was tested on. Assisted-by: Claude Opus 5 --- src/models/qwen35.cpp | 4 ++-- src/models/qwen35moe.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp index 746719da2edf..73eba2cf6c8f 100644 --- a/src/models/qwen35.cpp +++ b/src/models/qwen35.cpp @@ -466,8 +466,8 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn_linear( // Apply gated normalization: self.norm(core_attn_out, z) ggml_tensor * attn_out_norm = build_norm_gated(output, model.layers[il].ssm_norm, z_2d, il); - // Final reshape: [head_dim, n_heads, n_tokens, n_seqs] -> [n_tokens, n_seqs, n_heads * head_dim] - ggml_tensor * final_output = ggml_reshape_3d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens, n_seqs); + // 2-D: with a [d, n_seq_tokens, n_seqs] src1 the projection is n_seqs matmuls that each read the whole ssm_out weight + ggml_tensor * final_output = ggml_reshape_2d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens * n_seqs); cb(final_output, "final_output", il); // Output projection diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp index b6b5283303c6..cc7f44e56980 100644 --- a/src/models/qwen35moe.cpp +++ b/src/models/qwen35moe.cpp @@ -483,8 +483,8 @@ ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn_linear( // Apply gated normalization: self.norm(core_attn_out, z) ggml_tensor * attn_out_norm = build_norm_gated(output, model.layers[il].ssm_norm, z_2d, il); - // Final reshape: [head_dim, n_heads, n_tokens, n_seqs] -> [n_tokens, n_seqs, n_heads * head_dim] - ggml_tensor * final_output = ggml_reshape_3d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens, n_seqs); + // 2-D: with a [d, n_seq_tokens, n_seqs] src1 the projection is n_seqs matmuls that each read the whole ssm_out weight + ggml_tensor * final_output = ggml_reshape_2d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens * n_seqs); cb(final_output, "final_output", il); // Output projection From e3f31cf6d1e7613d5c10caee3477866b6ecc21eb Mon Sep 17 00:00:00 2001 From: Simon Teixidor Date: Wed, 2 Sep 2026 20:27:19 +0200 Subject: [PATCH 159/212] qwen35, qwen35moe: run gated_delta_net in place on the recurrent state cache build_rs gains an allow_inplace option: when the sequence-copy mapping is the identity, the state input is a view of the cache slot rather than a get_rows copy. qwen35 and qwen35moe both opt in, since they build the same Gated DeltaNet graph through llm_build_delta_net_base. s_copy_main_identity joins the graph-reuse key: a seq_cp between steps changes whether the mapping is the identity, so a graph built for one must not be reused for the other. test-recurrent-state-rollback gains a check that decodes two sequences in shared ubatches while seq_cp swaps them, forks one and rolls it back, with graph reuse on. The logits are compared with a context without graph reuse (bit-exact), one with n_rs_seq = 1 and one that decodes one sequence per ubatch. It uses fresh random weights at scale 1: at the 0.01 scale of the generated models the recurrent branch does not change the logits, so a wrong state would not be seen. The check runs for qwen35 and qwen35moe, and the test now also runs on the generated qwen35moe model. Assisted-by: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC --- src/llama-graph.cpp | 29 ++- src/llama-graph.h | 11 +- src/llama-memory-recurrent.cpp | 14 ++ src/llama-memory-recurrent.h | 3 + src/models/qwen35.cpp | 2 +- src/models/qwen35moe.cpp | 2 +- tests/CMakeLists.txt | 9 + tests/test-recurrent-state-rollback.cpp | 260 ++++++++++++++++++++++++ 8 files changed, 321 insertions(+), 9 deletions(-) diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index 010c40bcc500..e5607ae0501f 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -358,6 +358,8 @@ bool llm_graph_input_rs::can_reuse(const llm_graph_params & params) { res &= head == mctx->get_head(); res &= rs_z == mctx->get_rs_z(); + res &= s_copy_main_identity == mctx->is_s_copy_main_identity(params.ubatch.n_seqs); + return res; } @@ -1134,6 +1136,8 @@ bool llm_graph_input_mem_hybrid::can_reuse(const llm_graph_params & params) { res &= inp_rs->head == mctx->get_recr()->get_head(); res &= inp_rs->rs_z == mctx->get_recr()->get_rs_z(); + res &= inp_rs->s_copy_main_identity == mctx->get_recr()->is_s_copy_main_identity(params.ubatch.n_seqs); + return res; } @@ -1177,6 +1181,8 @@ bool llm_graph_input_mem_hybrid_k::can_reuse(const llm_graph_params & params) { res &= inp_rs->head == mctx->get_recr()->get_head(); res &= inp_rs->rs_z == mctx->get_recr()->get_rs_z(); + res &= inp_rs->s_copy_main_identity == mctx->get_recr()->is_s_copy_main_identity(params.ubatch.n_seqs); + return res; } @@ -1265,6 +1271,8 @@ bool llm_graph_input_mem_hybrid_iswa::can_reuse(const llm_graph_params & params) res &= inp_rs->head == mctx->get_recr()->get_head(); res &= inp_rs->rs_z == mctx->get_recr()->get_rs_z(); + res &= inp_rs->s_copy_main_identity == mctx->get_recr()->is_s_copy_main_identity(params.ubatch.n_seqs); + return res; } @@ -3489,7 +3497,8 @@ ggml_tensor * llm_graph_context::build_rs( uint32_t rs_head, uint32_t rs_size, int32_t rs_zero, - const llm_graph_get_rows_fn & get_state_rows) const { + const llm_graph_get_rows_fn & get_state_rows, + bool main_inplace) const { GGML_UNUSED(rs_size); ggml_tensor * states = ggml_reshape_2d(ctx0, s, state_size, s->ne[1]); @@ -3502,8 +3511,14 @@ ggml_tensor * llm_graph_context::build_rs( // copy states // NOTE: assuming the copy destinations are ALL contained between rs_head and rs_head + n_rs // {state_size, rs_size} -> {state_size, n_seqs} - ggml_tensor * output_states = get_state_rows(ctx0, states, state_copy_main); - ggml_build_forward_expand(gf, output_states); + ggml_tensor * output_states; + if (main_inplace) { + // rows are already in place; not expanded here, so the view stays next to its consumer + output_states = ggml_view_2d(ctx0, states, state_size, n_seqs, states->nb[1], rs_head*states->nb[1]); + } else { + output_states = get_state_rows(ctx0, states, state_copy_main); + ggml_build_forward_expand(gf, output_states); + } // copy extra states which won't be changed further (between n_seqs and n_rs) ggml_tensor * states_extra = ggml_get_rows(ctx0, states, state_copy_extra); @@ -3534,6 +3549,7 @@ static std::unique_ptr build_rs_inp_impl( inp->head = mctx_cur->get_head(); inp->rs_z = mctx_cur->get_rs_z(); + inp->s_copy_main_identity = mctx_cur->is_s_copy_main_identity(n_seqs); return inp; } @@ -3551,12 +3567,15 @@ ggml_tensor * llm_graph_context::build_rs( ggml_tensor * s, int32_t state_size, int32_t n_seqs, - const llm_graph_get_rows_fn & get_state_rows) const { + const llm_graph_get_rows_fn & get_state_rows, + bool allow_inplace) const { const auto * kv_state = inp->mctx; + const bool main_inplace = allow_inplace && inp->s_copy_main_identity; + return build_rs(s, inp->s_copy_main, inp->s_copy_extra, state_size, n_seqs, kv_state->get_n_rs(), kv_state->get_head(), kv_state->get_size(), kv_state->get_rs_z(), - get_state_rows); + get_state_rows, main_inplace); } ggml_tensor * llm_graph_context::build_rwkv_token_shift_load( diff --git a/src/llama-graph.h b/src/llama-graph.h index ce68832ce47a..35928daab07c 100644 --- a/src/llama-graph.h +++ b/src/llama-graph.h @@ -280,6 +280,9 @@ class llm_graph_input_rs : public llm_graph_input_i { // used in view offsets, need to match for valid graph reuse uint32_t head; int32_t rs_z; + + // part of the reuse key: a seq_cp between two ubatches can change it + bool s_copy_main_identity = false; }; class llm_graph_input_cross_embd : public llm_graph_input_i { @@ -1332,16 +1335,20 @@ struct llm_graph_context { uint32_t rs_head, uint32_t rs_size, int32_t rs_zero, - const llm_graph_get_rows_fn & get_state_rows = ggml_get_rows) const; + const llm_graph_get_rows_fn & get_state_rows = ggml_get_rows, + bool main_inplace = false) const; llm_graph_input_rs * build_rs_inp() const; + // allow_inplace: when no state is copied, return a view of the cache rows instead of a get_rows copy + // the consumer must read the whole state before the write-back cpy ggml_tensor * build_rs( llm_graph_input_rs * inp, ggml_tensor * s, int32_t state_size, int32_t n_seqs, - const llm_graph_get_rows_fn & get_state_rows = ggml_get_rows) const; + const llm_graph_get_rows_fn & get_state_rows = ggml_get_rows, + bool allow_inplace = false) const; ggml_tensor * build_rwkv_token_shift_load( llm_graph_input_rs * inp, diff --git a/src/llama-memory-recurrent.cpp b/src/llama-memory-recurrent.cpp index 57919accf095..93efed585990 100644 --- a/src/llama-memory-recurrent.cpp +++ b/src/llama-memory-recurrent.cpp @@ -1303,6 +1303,20 @@ ggml_tensor * llama_memory_recurrent_context::get_p_l(int32_t il) const { return mem->p_l[il]; } +bool llama_memory_recurrent_context::is_s_copy_main_identity(uint32_t n_seqs) const { + // with n_rs_seq == 0, s_copy(i) is cells[head + i].src0 + if (is_full || mem->n_rs_seq > 0 || n_seqs > get_n_rs()) { + return false; + } + for (uint32_t i = 0; i < n_seqs; ++i) { + const uint32_t cell = mem->head + i; + if (mem->cells[cell].src0 != (int32_t) cell) { + return false; + } + } + return true; +} + 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; diff --git a/src/llama-memory-recurrent.h b/src/llama-memory-recurrent.h index 4abb3f5cf5c0..3391403188f9 100644 --- a/src/llama-memory-recurrent.h +++ b/src/llama-memory-recurrent.h @@ -177,6 +177,9 @@ class llama_memory_recurrent_context : public llama_memory_context_i { int32_t s_copy(int i) const; + // s_copy(i) == head + i for all i < n_seqs, without side effects; always false with n_rs_seq > 0 + bool is_s_copy_main_identity(uint32_t n_seqs) const; + private: const llama_memory_status status; diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp index 73eba2cf6c8f..1b0b47f07c00 100644 --- a/src/models/qwen35.cpp +++ b/src/models/qwen35.cpp @@ -397,7 +397,7 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn_linear( ggml_tensor * conv_input = build_conv_state(inp, conv_states_all, qkv_mixed, conv_kernel_size, conv_channels, il); - ggml_tensor * state = build_rs(inp, ssm_states_all, hparams.n_embd_s(), n_seqs); + ggml_tensor * state = build_rs(inp, ssm_states_all, hparams.n_embd_s(), n_seqs, ggml_get_rows, /*allow_inplace=*/true); state = ggml_reshape_4d(ctx0, state, head_v_dim, head_v_dim, num_v_heads, n_seqs); cb(state, "state_predelta", il); diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp index cc7f44e56980..74ee8c0d4ac6 100644 --- a/src/models/qwen35moe.cpp +++ b/src/models/qwen35moe.cpp @@ -414,7 +414,7 @@ ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn_linear( ggml_tensor * conv_input = build_conv_state(inp, conv_states_all, qkv_mixed, conv_kernel_size, conv_channels, il); - ggml_tensor * state = build_rs(inp, ssm_states_all, hparams.n_embd_s(), n_seqs); + ggml_tensor * state = build_rs(inp, ssm_states_all, hparams.n_embd_s(), n_seqs, ggml_get_rows, /*allow_inplace=*/true); state = ggml_reshape_4d(ctx0, state, head_v_dim, head_v_dim, num_v_heads, n_seqs); cb(state, "state_predelta", il); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2e1f67df181c..ee05598a6aad 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -247,6 +247,15 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS) set_tests_properties(test-recurrent-state-rollback-kimi-k3 PROPERTIES FIXTURES_REQUIRED generate-models ) + llama_test( + test-recurrent-state-rollback + NAME test-recurrent-state-rollback-qwen35moe + LABEL main + ARGS -m "${MODEL_DIR}/qwen35moe-moe.gguf" + ) + set_tests_properties(test-recurrent-state-rollback-qwen35moe PROPERTIES + FIXTURES_REQUIRED generate-models + ) # Test state save/load functionality across all architectures, using the generated dummy models llama_test( diff --git a/tests/test-recurrent-state-rollback.cpp b/tests/test-recurrent-state-rollback.cpp index ef05de67d004..397edc3b7b08 100644 --- a/tests/test-recurrent-state-rollback.cpp +++ b/tests/test-recurrent-state-rollback.cpp @@ -1,6 +1,7 @@ #include "arg.h" #include "common.h" #include "ggml-backend.h" +#include "gguf.h" #include "llama.h" #include "../src/llama-io.h" @@ -10,8 +11,13 @@ #include #include #include +#include +#include +#include #include +#include #include +#include #include static bool decode_tokens(llama_context * ctx, const std::vector & tokens, uint32_t count) { @@ -262,6 +268,241 @@ static bool test_multi_seq_split_replay(const common_params & params, llama_mode return true; } +static void set_tensor_data_scaled(ggml_tensor * tensor, void * userdata) { + const size_t seed = *(const size_t *) userdata ^ std::hash{}(tensor->name); + std::mt19937 gen(seed); + std::normal_distribution dis(0.0f, 1.0f); + + // norm weights and matrices at scale 1: at the 0.01 scale of the generated models the recurrent + // branch is too small to change the logits, so a wrong recurrent state would go unnoticed + const bool is_norm = strstr(tensor->name, "norm") != nullptr; + const float scale = ggml_n_dims(tensor) > 1 ? 1.0f : 1.0e-2f; + + GGML_ASSERT(tensor->type == GGML_TYPE_F32); + std::vector tmp(ggml_nelements(tensor)); + for (auto & x : tmp) { + x = is_norm ? 1.0f : scale*dis(gen); + } + ggml_backend_tensor_set(tensor, tmp.data(), 0, ggml_nbytes(tensor)); +} + +// same hparams as the model file, new random weights +static llama_model * load_model_scaled(common_params & params) { + gguf_init_params gparams = { + /*.no_alloc =*/ true, + /*.ctx =*/ nullptr, + }; + gguf_context * meta_file = gguf_init_from_file(params.model.path.c_str(), gparams); + if (meta_file == nullptr) { + return nullptr; + } + gguf_context * meta = gguf_init_empty(); + gguf_set_kv(meta, meta_file); + gguf_free(meta_file); + + size_t seed = 1234; + llama_model * model = llama_model_init_from_user(meta, set_tensor_data_scaled, &seed, common_model_params_to_llama(params)); + gguf_free(meta); + return model; +} + +static void set_graph_reuse_disable(bool disable) { +#ifdef _WIN32 + _putenv_s("LLAMA_GRAPH_REUSE_DISABLE", disable ? "1" : ""); +#else + if (disable) { + setenv("LLAMA_GRAPH_REUSE_DISABLE", "1", 1); + } else { + unsetenv("LLAMA_GRAPH_REUSE_DISABLE"); + } +#endif +} + +// Two sequences share ubatches while seq_cp swaps them, forks one and rolls it back, with graph reuse on. +// With n_rs_seq = 0 the recurrent state can be read in place when no state is copied, so a seq_cp between +// two same-shape ubatches must stop the graph from being reused. Checked against a context without graph +// reuse (same ubatches, so bit-exact), a context with n_rs_seq = 1, which never reads the state in place, +// and a context that decodes one sequence per ubatch. +static bool test_seq_cp_graph_reuse(const common_params & params, llama_model * model) { + const char * func = __func__; + const int n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model)); + + const auto make = [&](uint32_t n_rs_seq, bool reuse) { + auto cparams = common_context_params_to_llama(params); + cparams.n_seq_max = 3; + cparams.n_rs_seq = n_rs_seq; + cparams.n_ctx = 256; + cparams.n_batch = 64; + cparams.n_ubatch = 64; + cparams.kv_unified = true; + set_graph_reuse_disable(!reuse); + llama_context * ctx = llama_init_from_model(model, cparams); + set_graph_reuse_disable(false); + return ctx; + }; + + struct arm { + const char * name; + llama_context * ctx; + bool split; // one llama_decode per sequence + float eps; // max |logit diff| against the first arm, relative to its max |logit| + float diff_max; + }; + std::vector arms = { + { "reuse", make(0, true), false, 0.0f, 0.0f }, + { "no-reuse", make(0, false), false, 1e-6f, 0.0f }, + { "no-in-place", make(1, false), false, 1e-5f, 0.0f }, + { "seq-split", make(0, false), true, 1e-4f, 0.0f }, + }; + + const auto cleanup = [&]() { + for (auto & a : arms) { + llama_free(a.ctx); + } + }; + + for (const auto & a : arms) { + if (a.ctx == nullptr) { + fprintf(stderr, "%s : failed to init %s context\n", __func__, a.name); + cleanup(); + return false; + } + } + + llama_pos pos[3] = { 0, 0, 0 }; + uint32_t n_step = 0; + bool ok = true; + + const auto decode = [&](const std::vector & seqs, uint32_t n_tokens) { + if (!ok) { + return; + } + n_step++; + const auto token = [&](llama_seq_id s, uint32_t i) { + return (llama_token) ((13*n_step + 7*(uint32_t) s + 3*i + 1) % (uint32_t) n_vocab); + }; + + const size_t n_rows = seqs.size()*n_tokens; + std::vector> logits(arms.size()); + llama_batch batch = llama_batch_init(n_rows, 0, 1); + for (size_t k = 0; ok && k < arms.size(); ++k) { + logits[k].reserve(n_rows*n_vocab); + const size_t n_batches = arms[k].split ? seqs.size() : 1; + for (size_t b = 0; ok && b < n_batches; ++b) { + common_batch_clear(batch); + for (size_t j = 0; j < seqs.size(); ++j) { + if (arms[k].split && j != b) { + continue; + } + for (uint32_t i = 0; i < n_tokens; ++i) { + common_batch_add(batch, token(seqs[j], i), pos[seqs[j]] + (llama_pos) i, { seqs[j] }, true); + } + } + ok = llama_decode(arms[k].ctx, batch) == 0; + for (int32_t i = 0; ok && i < batch.n_tokens; ++i) { + const float * l = llama_get_logits_ith(arms[k].ctx, i); + logits[k].insert(logits[k].end(), l, l + n_vocab); + } + } + if (!ok) { + fprintf(stderr, "%s : %s decode failed at step %u\n", func, arms[k].name, n_step); + } + } + llama_batch_free(batch); + + for (size_t r = 0; ok && r < n_rows; ++r) { + const float * ref = logits[0].data() + r*n_vocab; + float ref_max = 0.0f; + for (int t = 0; t < n_vocab; ++t) { + ref_max = std::max(ref_max, std::fabs(ref[t])); + } + for (size_t k = 1; k < arms.size(); ++k) { + const float * cur = logits[k].data() + r*n_vocab; + float diff = 0.0f; + for (int t = 0; t < n_vocab; ++t) { + diff = std::max(diff, logit_diff(ref[t], cur[t])); + } + diff /= std::max(ref_max, std::numeric_limits::min()); + arms[k].diff_max = std::max(arms[k].diff_max, diff); + if (!(diff <= arms[k].eps)) { + fprintf(stderr, "%s : step %u, seq %d row %zu: %s differs from %s by %g (rel)\n", + func, n_step, seqs[r/n_tokens], r % n_tokens, arms[k].name, arms[0].name, (double) diff); + ok = false; + } + } + } + for (llama_seq_id s : seqs) { + pos[s] += (llama_pos) n_tokens; + } + }; + + const auto seq_cp = [&](llama_seq_id src, llama_seq_id dst) { + for (auto & a : arms) { + llama_memory_t mem = llama_get_memory(a.ctx); + ok = ok && llama_memory_seq_rm(mem, dst, -1, -1); + llama_memory_seq_cp(mem, src, dst, -1, -1); + } + pos[dst] = pos[src]; + }; + + const auto seq_rm = [&](llama_seq_id s) { + for (auto & a : arms) { + ok = ok && llama_memory_seq_rm(llama_get_memory(a.ctx), s, -1, -1); + } + pos[s] = 0; + }; + + const auto swap_01 = [&]() { + seq_cp(0, 2); + seq_cp(1, 0); + seq_cp(2, 1); + seq_rm(2); + }; + + // seq 1 is decoded first, so the first shared ubatch reorders the cells + decode({ 1 }, 5); + decode({ 0 }, 5); + for (uint32_t n_tokens : { 2u, 3u, 1u }) { + decode({ 0, 1 }, n_tokens); + decode({ 0, 1 }, n_tokens); + decode({ 0, 1 }, n_tokens); + + // same ubatch shape, head and rs_z as the step before: only the state copy map changes + swap_01(); + decode({ 0, 1 }, n_tokens); + decode({ 0, 1 }, n_tokens); + + decode({ 0 }, n_tokens); + decode({ 1 }, n_tokens); + decode({ 0 }, n_tokens); + decode({ 1 }, n_tokens); + + // fork seq 0, advance it, then roll it back to the fork + seq_cp(0, 2); + decode({ 0, 1 }, n_tokens); + decode({ 0, 1 }, n_tokens); + seq_cp(2, 0); + decode({ 0, 1 }, n_tokens); + seq_rm(2); + decode({ 0, 1 }, n_tokens); + decode({ 0, 1 }, n_tokens); + } + + const int32_t n_reused = llama_perf_context(arms[0].ctx).n_reused; + if (ok && n_reused == 0) { + fprintf(stderr, "%s : graph reuse was not exercised\n", __func__); + ok = false; + } + + if (ok) { + fprintf(stderr, "%s : %u steps matched, %d graphs reused (max rel diff: %s %g, %s %g, %s %g)\n", __func__, n_step, n_reused, + arms[1].name, (double) arms[1].diff_max, arms[2].name, (double) arms[2].diff_max, arms[3].name, (double) arms[3].diff_max); + } + + cleanup(); + return ok; +} + static int test_rollback(const common_params & params, llama_model * model, uint8_t fill) { const llama_vocab * vocab = llama_model_get_vocab(model); const int n_vocab = llama_vocab_n_tokens(vocab); @@ -463,5 +704,24 @@ int main(int argc, char ** argv) { } } + // the in-place recurrent state is only enabled for qwen35 and qwen35moe + char arch[64] = {}; + llama_model_meta_val_str(model, "general.architecture", arch, sizeof(arch)); + if (strcmp(arch, "qwen35") != 0 && strcmp(arch, "qwen35moe") != 0) { + fprintf(stderr, "%s : skipping test_seq_cp_graph_reuse for %s\n", __func__, arch); + return 0; + } + + llama_model * model_scaled = load_model_scaled(params); + if (model_scaled == nullptr) { + fprintf(stderr, "%s : failed to create scaled model\n", __func__); + return 1; + } + const bool ok = test_seq_cp_graph_reuse(params, model_scaled); + llama_model_free(model_scaled); + if (!ok) { + return 1; + } + return 0; } From e20d6f509b8b2e2c8a3e4dde3f78085e4e3dcae6 Mon Sep 17 00:00:00 2001 From: ARC Date: Tue, 15 Sep 2026 13:29:38 +0200 Subject: [PATCH 160/212] chat : enforce string enums in Qwen XML tool arguments (#26) --- common/parsers/qwen3-coder.cpp | 10 ++++++++ tests/test-chat.cpp | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/common/parsers/qwen3-coder.cpp b/common/parsers/qwen3-coder.cpp index 8a1e5213700f..2c89d2986b3a 100644 --- a/common/parsers/qwen3-coder.cpp +++ b/common/parsers/qwen3-coder.cpp @@ -111,6 +111,16 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat auto arg_value = schema_info.resolves_to_string(param_schema) ? arg_string : p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", param_schema)) + arg_close; + if (param_schema.contains("enum") && param_schema.at("enum").is_array() && + !param_schema.at("enum").empty() && + std::all_of(param_schema.at("enum").begin(), param_schema.at("enum").end(), + [](const json & value) { return value.is_string(); })) { + auto values = p.choice(); + for (const auto & value : param_schema.at("enum")) { + values |= p.literal(value.get()) + p.peek(p.literal("\n\n")); + } + arg_value = p.tool_arg_string_value(values) + arg_close; + } auto arg_rule = p.rule(rule_name, p.tool_arg(arg_open + arg_value)); diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index f27c91e4d4cc..c3106e480acb 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -7248,6 +7248,49 @@ static void test_msg_diffs_compute() { } } +static void test_qwen_string_enums() { + const auto xml = [](const std::string & value) { + return "\n\n\n" + value + + "\n\n\n"; + }; + for (const std::string path : {"models/templates/Qwen3-Coder.jinja", "models/templates/Qwen3.5-4B.jinja"}) { + auto tmpls = read_templates(path); + common_chat_templates_inputs in; + in.messages = {message_user}; + in.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK; + in.parallel_tool_calls = true; + auto schema = json::parse(R"({"type":"object","properties":{"action":{"type":"string","enum":["load","reload","re","","quoted\"value","line\nbreak"]}},"required":["action"],"additionalProperties":false})"); + in.tools = {{"records", "Access records.", schema.dump()}}; + auto params = common_chat_templates_apply(tmpls.get(), in); + for (const std::string value : {"erase", "loading", "reloaded", " load", "load "}) { + auto grammar = build_grammar(params.grammar); + assert_equals(true, grammar != nullptr); + assert_equals(false, match_string(xml(value), grammar.get())); + } + for (const auto & value : schema["properties"]["action"]["enum"]) { + const auto text = value.get(); + auto grammar = build_grammar(params.grammar); + assert_equals(true, match_string(xml(text), grammar.get())); + test_peg_parser(tmpls.get(), [&](peg_test_case & tc) { + tc.params = in; + tc.input = (path.find("Qwen3-Coder") == std::string::npos ? "\n\n" : "") + xml(text); + tc.expect = simple_assist_msg("", "", "records", json({{"action", text}}).dump()); + }, false); + } + test_peg_parser(tmpls.get(), [&](peg_test_case & tc) { + tc.params = in; + tc.input = (path.find("Qwen3-Coder") == std::string::npos ? "\n\n" : "") + xml("re") + "\n" + xml("reload"); + tc.expect.role = "assistant"; + tc.expect.tool_calls = {{"records", R"({"action":"re"})", ""}, {"records", R"({"action":"reload"})", ""}}; + }, false); + schema["properties"]["action"].erase("enum"); + in.tools = {{"records", "Access records.", schema.dump()}}; + params = common_chat_templates_apply(tmpls.get(), in); + auto grammar = build_grammar(params.grammar); + assert_equals(true, match_string(xml("erase"), grammar.get())); + } +} + int main(int argc, char ** argv) { bool detailed_debug = false; bool only_run_filtered = false; @@ -7320,6 +7363,7 @@ int main(int argc, char ** argv) { } else #endif { + test_qwen_string_enums(); test_msg_diffs_compute(); test_msgs_oaicompat_json_conversion(); test_msg_token_delimiters_split(); From cfe6bb14efb3708f388e502e6fcd6fac16fda4e8 Mon Sep 17 00:00:00 2001 From: Simon Teixidor Date: Tue, 15 Sep 2026 17:18:26 +0200 Subject: [PATCH 161/212] HIP : keep FA head size 192 off the WMMA kernel (#55) --- ggml/src/ggml-cuda/fattn.cu | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-cuda/fattn.cu b/ggml/src/ggml-cuda/fattn.cu index 0499fb27db08..a38566111f48 100644 --- a/ggml/src/ggml-cuda/fattn.cu +++ b/ggml/src/ggml-cuda/fattn.cu @@ -683,7 +683,9 @@ static best_fattn_kernel ggml_cuda_get_best_fattn_kernel(const int device, const } // AMD WMMA is faster than the tile kernel if the wide tiles with high arithmetic intensity can be utilized. - if ((amd_wmma_available(cc) && gqa_opt_applies && Q->ne[0] <= 256) && Q->ne[0] != 40 && Q->ne[0] != 72 && + // Only head sizes that have WMMA device code in flash_attn_ext_f16 (fattn-mma-f16.cuh, AMD_WMMA_AVAILABLE guard: + // DKQ <= 128 or DKQ == 256) may be routed here; any other head size (e.g. 192) hits NO_DEVICE_CODE -> __trap(). + if ((amd_wmma_available(cc) && gqa_opt_applies && (Q->ne[0] <= 128 || Q->ne[0] == 256)) && Q->ne[0] != 40 && Q->ne[0] != 72 && Q->ne[1] * gqa_ratio_eff > (Q->ne[0] <= 128 ? 8 : 16)) { return BEST_FATTN_KERNEL_MMA_F16; } From e3120a1df3948e744b673bcc1a0796a0024e2084 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti Date: Tue, 15 Sep 2026 21:34:34 +0100 Subject: [PATCH 162/212] sync upstream llama.cpp commits 2026 09 15 (#28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adrien Gallouët Signed-off-by: Aaron Teo Co-authored-by: Xuan-Son Nguyen Co-authored-by: Pascal Co-authored-by: Mendy Berger <12537668+MendyBerger@users.noreply.github.com> Co-authored-by: Masashi Yoshimura Co-authored-by: Max Krasnyansky Co-authored-by: Alexander Lu Co-authored-by: shaofeiqi Co-authored-by: Li He Co-authored-by: Hongqiang Wang Co-authored-by: Ed Addario <29247825+EAddario@users.noreply.github.com> Co-authored-by: Michael Taylor <162068037+mctylr-gh@users.noreply.github.com> Co-authored-by: Alessandro de Oliveira Faria (A.K.A.CABELO) Co-authored-by: Adrien Gallouët Co-authored-by: thelittlefireman <5165783+thelittlefireman@users.noreply.github.com> Co-authored-by: MiaoMing Chen Co-authored-by: Sigbjørn Skjæret Co-authored-by: Aldehir Rojas Co-authored-by: Jeff Bolz Co-authored-by: Georgi Gerganov Co-authored-by: Neo Zhang Co-authored-by: Neo Zhang Jianyu Co-authored-by: Titaniumtown Co-authored-by: Bernard Ladenthin Co-authored-by: Yaniss Amazouz Co-authored-by: fairydreaming <166155368+fairydreaming@users.noreply.github.com> Co-authored-by: Stanisław Szymczyk Co-authored-by: Clint Herron Co-authored-by: Johannes Gäßler Co-authored-by: Daniel Bevenius Co-authored-by: Łukasz Ślusarczyk Co-authored-by: cwriter Co-authored-by: cwriter Co-authored-by: Alex <59368173+AlexGabbia@users.noreply.github.com> Co-authored-by: Aaron Teo Co-authored-by: Christian Kastner Co-authored-by: Apoorv Parle <19315187+apparle@users.noreply.github.com> Co-authored-by: Aman Gupta Co-authored-by: Oliver Simons Co-authored-by: uvos Co-authored-by: Abhiram <78226909+geckguy@users.noreply.github.com> Co-authored-by: Aman Karki Co-authored-by: shivamkumard-ctrl Co-authored-by: Yanzhao Wang Co-authored-by: Zijun Yu Co-authored-by: Mostafa Faheem Co-authored-by: Mustafa Cavus Co-authored-by: zhaixuejun1993 Co-authored-by: ravi9 Co-authored-by: Ruben Ortlam Co-authored-by: Chris Peterson Co-authored-by: Mohamed Elashri Co-authored-by: Patrick Hoffmann Co-authored-by: Patrick Hoffmann Co-authored-by: Claude Opus 5 --- .ecrc | 2 +- .github/actions/get-tag-name/action.yml | 2 +- .github/actions/windows-setup-cuda/action.yml | 54 +- .github/workflows/build-cpu.yml | 1 - .github/workflows/build-cuda-ubuntu.yml | 3 +- .github/workflows/build-cuda-windows.yml | 2 +- .github/workflows/build-ibm.yml | 10 +- .github/workflows/build-self-hosted.yml | 14 +- .github/workflows/build-wasm.yml | 2 +- .github/workflows/build-webgpu.yml | 8 +- .github/workflows/code-style.yml | 2 +- .github/workflows/editorconfig.yml | 2 +- .github/workflows/fusion.yml | 8 +- .github/workflows/release.yml | 150 ++- .github/workflows/server-sanitize.yml | 2 - .github/workflows/server-self-hosted.yml | 4 +- .pi/gg/SYSTEM.md | 4 +- CMakeLists.txt | 12 +- CONTRIBUTING.md | 5 +- app/CMakeLists.txt | 2 +- ci/run.sh | 201 +-- common/CMakeLists.txt | 4 +- common/arg.cpp | 6 +- common/chat-auto-parser-generator.cpp | 52 +- common/chat-peg-parser.cpp | 10 +- common/chat.cpp | 10 + common/chat.h | 3 + common/common.cpp | 11 +- common/fit.cpp | 41 + common/jinja/runtime.cpp | 6 + common/json-schema-to-grammar.cpp | 597 +++------ common/json-schema-to-grammar.h | 30 +- common/json-schema.cpp | 514 ++++++++ common/json-schema.h | 198 +++ common/log.cpp | 76 +- common/log.h | 19 +- common/parsers/cohere2moe.cpp | 9 - common/parsers/deepseek.cpp | 36 +- common/parsers/functionary-v3-2.cpp | 7 +- common/parsers/gemma4.cpp | 9 - common/parsers/gigachat-v3.cpp | 7 +- common/parsers/gpt-oss.cpp | 11 +- common/parsers/kimi-k2.cpp | 7 +- common/parsers/kimi-k3.cpp | 9 +- common/parsers/lfm2.cpp | 9 - common/parsers/minicpm5.cpp | 52 +- common/parsers/minimax-m3.cpp | 82 +- common/parsers/ministral3.cpp | 11 +- common/parsers/muse-glimmer.cpp | 46 +- common/parsers/parsers.cpp | 23 +- common/parsers/parsers.h | 4 +- common/parsers/qwen3-coder.cpp | 74 +- common/peg-parser.cpp | 42 +- common/peg-parser.h | 10 +- conversion/__init__.py | 1 + conversion/maple.py | 87 ++ docs/backend/OPENVINO.md | 3 + docs/backend/SYCL.md | 5 +- docs/backend/snapdragon/README.md | 141 +- docs/backend/snapdragon/developer.md | 360 ++++- docs/build.md | 2 +- docs/development/parsing.md | 9 +- docs/ops.md | 2 +- docs/ops/CUDA.csv | 8 +- examples/eval-callback/CMakeLists.txt | 2 +- examples/json_schema_to_grammar.py | 842 ------------ examples/regex_to_grammar.py | 20 - examples/test-cmake/.gitignore | 1 + examples/test-cmake/CMakeLists.txt | 19 +- examples/test-cmake/README.md | 19 +- examples/test-cmake/build.sh | 17 +- examples/test-cmake/test-cmake.cpp | 4 + examples/ts-type-to-grammar.sh | 28 - ggml/CMakeLists.txt | 2 +- ggml/include/ggml-rpc.h | 2 +- ggml/src/ggml-cpu/CMakeLists.txt | 6 - ggml/src/ggml-cpu/arch/s390/quants.c | 1 + ggml/src/ggml-cpu/arch/s390/repack.cpp | 2 + ggml/src/ggml-cpu/ops.h | 17 +- ggml/src/ggml-cuda/common.cuh | 6 + ggml/src/ggml-cuda/cpy.cu | 8 + ggml/src/ggml-cuda/fattn-mma-f16.cuh | 8 +- ggml/src/ggml-cuda/ggml-cuda.cu | 19 +- ggml/src/ggml-cuda/mean.cu | 19 +- ggml/src/ggml-cuda/mmq-config-gcn.cuh | 281 ++++ ggml/src/ggml-cuda/mmq.cuh | 8 +- ggml/src/ggml-cuda/reduce_rows.cuh | 43 +- ggml/src/ggml-cuda/sumrows.cu | 20 +- ggml/src/ggml-hexagon/ggml-hexagon.cpp | 1163 ++++++++++++----- ggml/src/ggml-hexagon/htp-opnode.h | 6 + ggml/src/ggml-hexagon/htp/act-ops.c | 233 ++-- ggml/src/ggml-hexagon/htp/allreduce-ops.c | 111 +- ggml/src/ggml-hexagon/htp/allreduce-ops.h | 11 + ggml/src/ggml-hexagon/htp/argsort-ops.c | 50 +- ggml/src/ggml-hexagon/htp/binary-ops.c | 228 ++-- ggml/src/ggml-hexagon/htp/concat-ops.c | 81 +- ggml/src/ggml-hexagon/htp/cpy-ops.c | 464 ++++--- ggml/src/ggml-hexagon/htp/cumsum-ops.c | 105 +- ggml/src/ggml-hexagon/htp/diag-ops.c | 101 +- ggml/src/ggml-hexagon/htp/fill-ops.c | 69 +- ggml/src/ggml-hexagon/htp/flash-attn-ops.c | 84 +- ggml/src/ggml-hexagon/htp/flash-attn-ops.h | 1 + .../ggml-hexagon/htp/gated-delta-net-ops.c | 74 +- ggml/src/ggml-hexagon/htp/get-rows-ops.c | 62 +- ggml/src/ggml-hexagon/htp/hex-common.h | 9 + ggml/src/ggml-hexagon/htp/hex-utils.h | 1 - ggml/src/ggml-hexagon/htp/hmx-utils.h | 14 +- ggml/src/ggml-hexagon/htp/htp-ctx.h | 51 +- ggml/src/ggml-hexagon/htp/htp-fence.h | 89 ++ ggml/src/ggml-hexagon/htp/htp-ops.h | 23 +- ggml/src/ggml-hexagon/htp/htp-tensor.c | 135 +- ggml/src/ggml-hexagon/htp/htp-tensor.h | 109 ++ ggml/src/ggml-hexagon/htp/hvx-arith.h | 274 ++-- ggml/src/ggml-hexagon/htp/hvx-div.h | 94 +- ggml/src/ggml-hexagon/htp/hvx-inverse.h | 46 +- ggml/src/ggml-hexagon/htp/hvx-scale.h | 44 +- ggml/src/ggml-hexagon/htp/hvx-sigmoid.h | 80 +- ggml/src/ggml-hexagon/htp/im2col-ops.c | 103 +- ggml/src/ggml-hexagon/htp/main.c | 103 +- ggml/src/ggml-hexagon/htp/matmul-ops.c | 371 ++++-- ggml/src/ggml-hexagon/htp/pad-ops.c | 163 +-- ggml/src/ggml-hexagon/htp/repeat-ops.c | 49 +- ggml/src/ggml-hexagon/htp/rope-ops.c | 41 +- ggml/src/ggml-hexagon/htp/set-rows-ops.c | 54 +- ggml/src/ggml-hexagon/htp/softmax-ops.c | 53 +- ggml/src/ggml-hexagon/htp/solve-tri-ops.c | 92 +- ggml/src/ggml-hexagon/htp/ssm-conv.c | 169 +-- ggml/src/ggml-hexagon/htp/sum-rows-ops.c | 80 +- ggml/src/ggml-hexagon/htp/unary-ops.c | 278 ++-- ggml/src/ggml-metal/ggml-metal-ops.cpp | 1 + ggml/src/ggml-metal/ggml-metal-tuning.cpp | 3 + ggml/src/ggml-metal/kernels/fa.metal | 31 + ggml/src/ggml-opencl/CMakeLists.txt | 1 + ggml/src/ggml-opencl/ggml-opencl.cpp | 866 ++++++++---- .../gemv_noshuffle_q4_k_f32_32b_trans.cl | 134 ++ ggml/src/ggml-opencl/kernels/ssm_scan.cl | 130 ++ ggml/src/ggml-openvino/ggml-decoder.cpp | 266 +++- ggml/src/ggml-openvino/ggml-decoder.h | 76 +- .../src/ggml-openvino/ggml-openvino-extra.cpp | 88 ++ ggml/src/ggml-openvino/ggml-openvino-extra.h | 5 +- ggml/src/ggml-openvino/ggml-openvino.cpp | 187 ++- ggml/src/ggml-openvino/ggml-quants.cpp | 72 +- ggml/src/ggml-openvino/ggml-quants.h | 10 + ggml/src/ggml-openvino/openvino/frontend.cpp | 3 +- .../src/ggml-openvino/openvino/node_context.h | 4 + ggml/src/ggml-openvino/openvino/op/add.cpp | 16 +- ggml/src/ggml-openvino/openvino/op/diag.cpp | 33 +- ggml/src/ggml-openvino/openvino/op/div.cpp | 18 +- .../openvino/op/glu_geglu_quick.cpp | 6 +- .../ggml-openvino/openvino/op/glu_swiglu.cpp | 27 +- .../openvino/op/moe_compressed.hpp | 90 ++ .../ggml-openvino/openvino/op/mul_mat_id.cpp | 60 +- ggml/src/ggml-openvino/openvino/op/mulmat.cpp | 16 +- ggml/src/ggml-openvino/openvino/op/norm.cpp | 35 +- ggml/src/ggml-openvino/openvino/op/pad.cpp | 4 +- .../src/ggml-openvino/openvino/op/permute.cpp | 16 +- ggml/src/ggml-openvino/openvino/op/rope.cpp | 267 ++-- .../ggml-openvino/openvino/op/set_rows.cpp | 3 +- .../ggml-openvino/openvino/op/transpose.cpp | 2 - .../ggml-openvino/openvino/op/unary_silu.cpp | 27 - .../openvino/op/unary_softplus.cpp | 6 + ggml/src/ggml-openvino/openvino/op_table.cpp | 4 +- ggml/src/ggml-openvino/openvino/op_table.h | 1 - .../openvino/pass/fuse_moe_compressed.cpp | 273 ++++ .../openvino/pass/fuse_moe_compressed.h | 19 + .../openvino/pass/kv_state_seq_axis.cpp | 114 ++ .../openvino/pass/kv_state_seq_axis.h | 24 + .../openvino/pass/squeeze_matmul.cpp | 3 +- .../openvino/translate_session.cpp | 85 +- ggml/src/ggml-openvino/utils.cpp | 527 ++++++-- ggml/src/ggml-openvino/utils.h | 44 +- ggml/src/ggml-rpc/CMakeLists.txt | 6 +- ggml/src/ggml-rpc/ggml-rpc.cpp | 69 +- ggml/src/ggml-sycl/CMakeLists.txt | 14 +- ggml/src/ggml-sycl/backend.hpp | 1 + ggml/src/ggml-sycl/base.hpp | 7 + ggml/src/ggml-sycl/common.hpp | 19 - ggml/src/ggml-sycl/gemm.hpp | 6 +- ggml/src/ggml-sycl/ggml-sycl.cpp | 102 +- ggml/src/ggml-sycl/mem.cpp | 95 +- ggml/src/ggml-sycl/topk-radix.cpp | 531 ++++++++ ggml/src/ggml-sycl/topk-radix.hpp | 24 + ggml/src/ggml-vulkan/ggml-vulkan.cpp | 160 ++- .../vulkan-shaders/flash_attn.comp | 46 +- .../vulkan-shaders/flash_attn_base.glsl | 33 +- .../vulkan-shaders/flash_attn_cm1.comp | 48 +- .../vulkan-shaders/flash_attn_cm2.comp | 95 +- .../flash_attn_sparse_compact.comp | 102 ++ .../vulkan-shaders/vulkan-shaders-gen.cpp | 2 + ggml/src/ggml-webgpu/CMakeLists.txt | 6 + ggml/src/ggml-webgpu/ggml-webgpu.cpp | 43 +- gguf-py/gguf/constants.py | 19 + grammars/README.md | 8 +- scripts/check-apiabi-compat.sh | 272 ++++ scripts/hip/gcn-cdna-vgpr-check.py | 1 + .../snapdragon/ggml-hexagon-align-macros.py | 296 +++++ scripts/snapdragon/run.py | 128 +- scripts/sync-ggml.last | 2 +- scripts/sync_vendor.py | 2 +- scripts/ui-assets.cmake | 157 ++- src/CMakeLists.txt | 1 - src/llama-arch.cpp | 1 + src/llama-arch.h | 1 + src/llama-grammar.cpp | 11 +- src/llama-graph.cpp | 2 +- src/llama-model-saver.cpp | 1 + src/llama-model.cpp | 3 + src/models/dflash.cpp | 30 +- src/models/eagle3.cpp | 24 +- src/models/gemma4-assistant.cpp | 2 +- src/models/gemma4.cpp | 2 +- src/models/maple.cpp | 150 +++ src/models/mimo2.cpp | 2 +- src/models/models.h | 13 + src/models/nemotron-h.cpp | 10 +- src/models/qwen4exp.cpp | 29 +- src/models/step35.cpp | 2 +- src/models/t5.cpp | 24 +- tests/CMakeLists.txt | 11 +- tests/fusion/README.md | 26 + ... => nvidia-nemotron-3-nano-30b-a3b.schema} | 0 tests/test-backend-ops.cpp | 65 +- tests/test-chat-peg-parser.cpp | 15 - tests/test-chat.cpp | 72 + tests/test-grammar-integration.cpp | 12 +- tests/test-jinja.cpp | 12 + tests/test-json-schema-to-grammar.cpp | 347 ++--- tests/test-json-schema.cpp | 513 ++++++++ tests/test-llama-archs.cpp | 12 +- tests/test-quant-type-selection.cpp | 2 +- tools/cli/README.md | 4 +- tools/completion/README.md | 6 +- tools/mtmd/CMakeLists.txt | 7 - tools/server/CMakeLists.txt | 20 +- tools/server/README.md | 4 +- tools/server/server-common.cpp | 152 +++ tools/server/server-common.h | 38 + tools/server/server-models.cpp | 477 ++++--- tools/server/server-models.h | 26 +- tools/server/server-schema.cpp | 4 + tools/server/tests/unit/test_router.py | 7 +- tools/tuning/CMakeLists.txt | 2 +- .../src/lib/hooks/use-tools-panel.svelte.ts | 2 +- .../ui/src/lib/stores/agentic/index.svelte.ts | 10 +- tools/ui/src/lib/stores/tools.svelte.ts | 9 +- vendor/cpp-httplib/httplib.cpp | 296 +++-- vendor/cpp-httplib/httplib.h | 92 +- 247 files changed, 12629 insertions(+), 5708 deletions(-) create mode 100644 common/json-schema.cpp create mode 100644 common/json-schema.h create mode 100644 conversion/maple.py delete mode 100755 examples/json_schema_to_grammar.py delete mode 100644 examples/regex_to_grammar.py delete mode 100755 examples/ts-type-to-grammar.sh create mode 100644 ggml/src/ggml-cuda/mmq-config-gcn.cuh create mode 100644 ggml/src/ggml-hexagon/htp/htp-fence.h create mode 100644 ggml/src/ggml-opencl/kernels/gemv_noshuffle_q4_k_f32_32b_trans.cl create mode 100644 ggml/src/ggml-openvino/openvino/op/moe_compressed.hpp delete mode 100644 ggml/src/ggml-openvino/openvino/op/unary_silu.cpp create mode 100644 ggml/src/ggml-openvino/openvino/pass/fuse_moe_compressed.cpp create mode 100644 ggml/src/ggml-openvino/openvino/pass/fuse_moe_compressed.h create mode 100644 ggml/src/ggml-openvino/openvino/pass/kv_state_seq_axis.cpp create mode 100644 ggml/src/ggml-openvino/openvino/pass/kv_state_seq_axis.h create mode 100644 ggml/src/ggml-sycl/topk-radix.cpp create mode 100644 ggml/src/ggml-sycl/topk-radix.hpp create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_sparse_compact.comp create mode 100755 scripts/check-apiabi-compat.sh create mode 100755 scripts/snapdragon/ggml-hexagon-align-macros.py create mode 100644 src/models/maple.cpp create mode 100644 tests/fusion/README.md rename tests/snapshots/{nemotron-nano-3-30b-a3b.schema => nvidia-nemotron-3-nano-30b-a3b.schema} (100%) create mode 100644 tests/test-json-schema.cpp diff --git a/.ecrc b/.ecrc index c68877ec211f..0338e4faa4a2 100644 --- a/.ecrc +++ b/.ecrc @@ -1,5 +1,5 @@ { - "Exclude": ["^\\.gitmodules$", "stb_image\\.h"], + "Exclude": ["^\\.gitmodules$", "stb_image\\.h", "examples/test-cmake/build/", "examples/test-cmake/build-subdir/"], "Disable": { "IndentSize": true } diff --git a/.github/actions/get-tag-name/action.yml b/.github/actions/get-tag-name/action.yml index 7ace23b2a3e7..46acce5828e9 100644 --- a/.github/actions/get-tag-name/action.yml +++ b/.github/actions/get-tag-name/action.yml @@ -14,7 +14,7 @@ runs: run: | BUILD_NUMBER="$(git rev-list --count HEAD)" SHORT_HASH="$(git rev-parse --short=7 HEAD)" - if [[ "${{ env.BRANCH_NAME }}" == "master" ]]; then + if [[ "${{ env.BRANCH_NAME }}" == "master" || "${{ env.BRANCH_NAME }}" == "b${BUILD_NUMBER}" ]]; then echo "name=b${BUILD_NUMBER}" >> $GITHUB_OUTPUT else SAFE_NAME=$(echo "${{ env.BRANCH_NAME }}" | tr '/' '-') diff --git a/.github/actions/windows-setup-cuda/action.yml b/.github/actions/windows-setup-cuda/action.yml index 2048740a2549..e67b6321e95a 100644 --- a/.github/actions/windows-setup-cuda/action.yml +++ b/.github/actions/windows-setup-cuda/action.yml @@ -100,36 +100,36 @@ runs: echo "CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.1" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 echo "CUDA_PATH_V13_1=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.1" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - - name: Install Cuda Toolkit 13.3 - if: ${{ inputs.cuda_version == '13.3' }} + - name: Install Cuda Toolkit 13.4 for x64 + if: ${{ inputs.cuda_version == '13.4' && inputs.cuda_arch == 'x64' }} shell: pwsh run: | - mkdir -p "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" + mkdir -p "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" choco install unzip -y - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_crt/windows-x86_64/cuda_crt-windows-x86_64-13.3.33-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/windows-x86_64/cuda_cudart-windows-x86_64-13.3.29-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvcc/windows-x86_64/cuda_nvcc-windows-x86_64-13.3.33-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvrtc/windows-x86_64/cuda_nvrtc-windows-x86_64-13.3.33-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/libcublas/windows-x86_64/libcublas-windows-x86_64-13.5.1.27-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/libnvvm/windows-x86_64/libnvvm-windows-x86_64-13.3.33-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvtx/windows-x86_64/cuda_nvtx-windows-x86_64-13.3.29-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_profiler_api/windows-x86_64/cuda_profiler_api-windows-x86_64-13.3.27-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/visual_studio_integration/windows-x86_64/visual_studio_integration-windows-x86_64-13.3.27-archive.zip" - curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cccl/windows-x86_64/cccl-windows-x86_64-13.3.3.3.1-archive.zip" - unzip '*.zip' -d "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\cuda_crt-windows-x86_64-13.3.33-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\cuda_cudart-windows-x86_64-13.3.29-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\cuda_nvcc-windows-x86_64-13.3.33-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\cuda_nvrtc-windows-x86_64-13.3.33-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\libcublas-windows-x86_64-13.5.1.27-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\libnvvm-windows-x86_64-13.3.33-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\cuda_nvtx-windows-x86_64-13.3.29-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\cuda_profiler_api-windows-x86_64-13.3.27-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\visual_studio_integration-windows-x86_64-13.3.27-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\cccl-windows-x86_64-13.3.3.3.1-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" /E /I /H /Y - echo "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - echo "CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - echo "CUDA_PATH_V13_3=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_crt/windows-x86_64/cuda_crt-windows-x86_64-13.4.59-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/windows-x86_64/cuda_cudart-windows-x86_64-13.4.49-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvcc/windows-x86_64/cuda_nvcc-windows-x86_64-13.4.59-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvrtc/windows-x86_64/cuda_nvrtc-windows-x86_64-13.4.59-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/libcublas/windows-x86_64/libcublas-windows-x86_64-13.7.0.27-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/libnvvm/windows-x86_64/libnvvm-windows-x86_64-13.4.59-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvtx/windows-x86_64/cuda_nvtx-windows-x86_64-13.4.49-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cuda_profiler_api/windows-x86_64/cuda_profiler_api-windows-x86_64-13.4.49-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/visual_studio_integration/windows-x86_64/visual_studio_integration-windows-x86_64-13.4.49-archive.zip" + curl -O "https://developer.download.nvidia.com/compute/cuda/redist/cccl/windows-x86_64/cccl-windows-x86_64-13.3.4.2.1-archive.zip" + unzip '*.zip' -d "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\cuda_crt-windows-x86_64-13.4.59-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\cuda_cudart-windows-x86_64-13.4.49-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\cuda_nvcc-windows-x86_64-13.4.59-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\cuda_nvrtc-windows-x86_64-13.4.59-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\libcublas-windows-x86_64-13.7.0.27-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\libnvvm-windows-x86_64-13.4.59-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\cuda_nvtx-windows-x86_64-13.4.49-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\cuda_profiler_api-windows-x86_64-13.4.49-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\visual_studio_integration-windows-x86_64-13.4.49-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + xcopy "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\cccl-windows-x86_64-13.3.4.2.1-archive\*" "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" /E /I /H /Y + echo "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + echo "CUDA_PATH_V13_4=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.4" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Install Cuda Toolkit 13.4 for ARM64 if: ${{ inputs.cuda_version == '13.4' && inputs.cuda_arch == 'arm64' }} diff --git a/.github/workflows/build-cpu.yml b/.github/workflows/build-cpu.yml index 9e92314bc2f2..ddda55f1c6b0 100644 --- a/.github/workflows/build-cpu.yml +++ b/.github/workflows/build-cpu.yml @@ -221,7 +221,6 @@ jobs: # 7z x "-o${env:RUNNER_TEMP}" $env:RUNNER_TEMP/sde.tar # $sde = $(join-path $env:RUNNER_TEMP sde-external-${env:SDE_VERSION}-win/sde.exe) # cd build - # $env:LLAMA_SKIP_TESTS_SLOW_ON_EMULATOR = 1 # & $sde -future -- ctest -L main -C Release --verbose --timeout 900 - name: ccache-clear diff --git a/.github/workflows/build-cuda-ubuntu.yml b/.github/workflows/build-cuda-ubuntu.yml index da61b3353e97..30029887c3d0 100644 --- a/.github/workflows/build-cuda-ubuntu.yml +++ b/.github/workflows/build-cuda-ubuntu.yml @@ -177,7 +177,8 @@ jobs: id: cmake_build run: | cmake -B build -S . \ - -DGGML_MUSA=ON + -DGGML_MUSA=ON \ + -DMUSA_ARCHITECTURES=21 time cmake --build build --config Release -j $(nproc) - name: ccache-buckets-save diff --git a/.github/workflows/build-cuda-windows.yml b/.github/workflows/build-cuda-windows.yml index 416724ec7081..e08553e6cdfc 100644 --- a/.github/workflows/build-cuda-windows.yml +++ b/.github/workflows/build-cuda-windows.yml @@ -34,7 +34,7 @@ jobs: - cuda: '12.4' arch: x64 defines: '-DGGML_CUDA_CUB_3DOT2=ON' - - cuda: '13.3' + - cuda: '13.4' arch: x64 defines: '' - cuda: '13.4' diff --git a/.github/workflows/build-ibm.yml b/.github/workflows/build-ibm.yml index d2e4f3cdaeb7..355487e97b8d 100644 --- a/.github/workflows/build-ibm.yml +++ b/.github/workflows/build-ibm.yml @@ -34,10 +34,15 @@ env: LLAMA_ARG_LOG_TIMESTAMPS: 1 jobs: - ubuntu-24-s390x: + name: ubuntu-24-s390x (VXE ${{ matrix.vxe }}) runs-on: ubuntu-24.04-s390x + strategy: + fail-fast: false + matrix: + vxe: ["ON", "OFF"] # `-DGGML_VXE=ON/OFF` + steps: - name: Clone id: checkout @@ -77,7 +82,8 @@ jobs: run: | cmake -B build \ -DLLAMA_FATAL_WARNINGS=ON \ - -DGGML_RPC=ON + -DGGML_RPC=ON \ + -DGGML_VXE=${{ matrix.vxe }} time cmake --build build --config Release -j $(nproc) - name: Test diff --git a/.github/workflows/build-self-hosted.yml b/.github/workflows/build-self-hosted.yml index 02a38466fa76..1337a0ed5bd1 100644 --- a/.github/workflows/build-self-hosted.yml +++ b/.github/workflows/build-self-hosted.yml @@ -7,6 +7,7 @@ on: - master paths: [ '.github/workflows/build-self-hosted.yml', + 'ci/run.sh', '**/CMakeLists.txt', '**/.cmake', '**/*.h', @@ -27,6 +28,7 @@ on: types: [opened, synchronize, reopened] paths: [ '.github/workflows/build-self-hosted.yml', + 'ci/run.sh', '**/CMakeLists.txt', '**/.cmake', '**/*.h', @@ -160,10 +162,10 @@ jobs: - name: Dawn Dependency id: dawn-depends run: | - DAWN_VERSION="v20260317.182325" + DAWN_VERSION="v20260908.214631" DAWN_OWNER="google" DAWN_REPO="dawn" - DAWN_ASSET_NAME="Dawn-18eb229ef5f707c1464cc581252e7603c73a3ef0-ubuntu-latest-Release" + DAWN_ASSET_NAME="Dawn-94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655-ubuntu-latest-Release" echo "Fetching release asset from https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz" curl -L -o artifact.tar.gz \ "https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz" @@ -246,10 +248,10 @@ jobs: - name: Dawn Dependency id: dawn-depends run: | - DAWN_VERSION="v20260317.182325" + DAWN_VERSION="v20260908.214631" DAWN_OWNER="google" DAWN_REPO="dawn" - DAWN_ASSET_NAME="Dawn-18eb229ef5f707c1464cc581252e7603c73a3ef0-macos-latest-Release" + DAWN_ASSET_NAME="Dawn-94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655-macos-latest-Release" echo "Fetching release asset from https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz" curl -L -o artifact.tar.gz \ "https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz" @@ -359,7 +361,7 @@ jobs: LLAMA_ARG_THREADS=$(nproc) GG_BUILD_HIGH_PERF=1 GG_BUILD_EXTRA_TESTS_0=1 bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp cpu-arm64-high-perf-graviton4: - runs-on: ah-ubuntu_22_04-c8g_8x + runs-on: ah-ubuntu_24_04-c8g_8x steps: - name: Clone @@ -402,7 +404,7 @@ jobs: bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp cpu-arm64-graviton4-kleidiai: - runs-on: ah-ubuntu_22_04-c8g_8x + runs-on: ah-ubuntu_24_04-c8g_8x steps: - name: Clone diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index 81b943df7b65..5a3166ce6885 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -68,7 +68,7 @@ jobs: - name: Fetch emdawnwebgpu run: | - DAWN_TAG="v20260317.182325" + DAWN_TAG="v20260908.214631" EMDAWN_PKG="emdawnwebgpu_pkg-${DAWN_TAG}.zip" echo "Downloading ${EMDAWN_PKG}" curl -L -o emdawn.zip \ diff --git a/.github/workflows/build-webgpu.yml b/.github/workflows/build-webgpu.yml index 8277abcc47c3..ec582ff274b3 100644 --- a/.github/workflows/build-webgpu.yml +++ b/.github/workflows/build-webgpu.yml @@ -77,10 +77,10 @@ jobs: - name: Dawn Dependency id: dawn-depends run: | - DAWN_VERSION="v20260317.182325" + DAWN_VERSION="v20260908.214631" DAWN_OWNER="google" DAWN_REPO="dawn" - DAWN_ASSET_NAME="Dawn-18eb229ef5f707c1464cc581252e7603c73a3ef0-macos-latest-Release" + DAWN_ASSET_NAME="Dawn-94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655-macos-latest-Release" echo "Fetching release asset from https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz" curl -L -o artifact.tar.gz \ "https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz" @@ -147,10 +147,10 @@ jobs: id: dawn-depends run: | sudo apt-get install -y libxrandr-dev libxinerama-dev libxcursor-dev mesa-common-dev libx11-xcb-dev libxi-dev - DAWN_VERSION="v20260317.182325" + DAWN_VERSION="v20260908.214631" DAWN_OWNER="google" DAWN_REPO="dawn" - DAWN_ASSET_NAME="Dawn-18eb229ef5f707c1464cc581252e7603c73a3ef0-ubuntu-latest-Release" + DAWN_ASSET_NAME="Dawn-94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655-ubuntu-latest-Release" echo "Fetching release asset from https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz" curl -L -o artifact.tar.gz \ "https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz" diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index 50b598b84ddd..c88396c0a7d1 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -15,7 +15,7 @@ concurrency: jobs: model-naming: - runs-on: [self-hosted, fast] + runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 - name: Check model naming conventions diff --git a/.github/workflows/editorconfig.yml b/.github/workflows/editorconfig.yml index 59159cd41444..53f6a0ccfda4 100644 --- a/.github/workflows/editorconfig.yml +++ b/.github/workflows/editorconfig.yml @@ -15,7 +15,7 @@ concurrency: jobs: editorconfig: - runs-on: [self-hosted, fast] + runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 - uses: editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c # v2.2.0 diff --git a/.github/workflows/fusion.yml b/.github/workflows/fusion.yml index ad7d5ab60e01..7c8596467aab 100644 --- a/.github/workflows/fusion.yml +++ b/.github/workflows/fusion.yml @@ -9,7 +9,9 @@ on: '.github/workflows/fusion.yml', 'ggml/**', 'tests/fusion/**', - 'tests/test-fusion.cpp' + 'tests/test-fusion.cpp', + 'tests/test-llama-archs.cpp', + 'src/models/**' ] pull_request: @@ -18,7 +20,9 @@ on: '.github/workflows/fusion.yml', 'ggml/**', 'tests/fusion/**', - 'tests/test-fusion.cpp' + 'tests/test-fusion.cpp', + 'tests/test-llama-archs.cpp', + 'src/models/**' ] concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b77c2d97d80..8389f017b90d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -310,6 +310,145 @@ jobs: with: key: release-${{ matrix.os }}-vulkan + ubuntu-cuda: + name: ubuntu-cuda (${{ matrix.label }}, ${{ matrix.build }}) + needs: [check-release, ui-build] + if: ${{ needs.check-release.outputs.should_release == 'true' }} + + strategy: + matrix: + include: + # label = short version used in artifact names / release body + # cuda = full container image tag + - build: 'x64' + os: ubuntu-24.04 + cuda: '12.8.2' + label: '12.8' + defines: '-DGGML_CUDA_CUB_3DOT2=ON' + - build: 'x64' + os: ubuntu-24.04 + cuda: '13.3.1' + label: '13.3' + defines: '' + - build: 'arm64' + os: ubuntu-24.04-arm + cuda: '13.3.1' + label: '13.3' + defines: '' + + runs-on: ${{ matrix.os }} + container: nvidia/cuda:${{ matrix.cuda }}-devel-ubuntu24.04 + + permissions: + actions: write + + steps: + # the container has no git; install it before checkout so that a real git + # repository is created (the get-tag-name action and the build both need it) + - name: Install git + run: | + apt-get update + apt-get install -y --no-install-recommends git + + - name: Clone + id: checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # checkout runs as the host user; in-container steps run as root, so git + # refuses to touch a repo it does not own. Mark the workspace as safe. + # use the env var: the github.workspace context holds the HOST path, + # GITHUB_WORKSPACE the container path + - name: Git safe directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Download UI build + uses: actions/download-artifact@v7 + with: + name: llama-ui.zip + path: tools/ui/dist + + - name: Dependencies + id: depends + # container jobs default to sh (dash); need bash for the [[ ]] below + shell: bash + run: | + apt-get update + apt-get install -y --no-install-recommends build-essential cmake ninja-build libssl-dev jq python3-venv + # the container ships GCC 13, which does not know the 'sme' march + # feature used by the armv9.2 CPU variant of GGML_CPU_ALL_VARIANTS + if [[ "${{ matrix.build }}" == "arm64" ]]; then + apt-get install -y --no-install-recommends gcc-14 g++-14 + echo "CC=gcc-14" >> "$GITHUB_ENV" + echo "CXX=g++-14" >> "$GITHUB_ENV" + fi + + - name: ccache + uses: ggml-org/ccache-action@v1.2.24 + with: + key: release-ubuntu-${{ matrix.os }}-cuda-${{ matrix.label }}-${{ matrix.build }} + evict-old-files: 1d + max-size: "1G" + + - name: Build + id: cmake_build + # no CMAKE_CUDA_ARCHITECTURES: use the broad default arch set from + # ggml/src/ggml-cuda/CMakeLists.txt so the release binary covers many GPUs + run: | + cmake -B build \ + -DCMAKE_INSTALL_RPATH='$ORIGIN' \ + -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \ + -DGGML_BACKEND_DL=ON \ + -DGGML_NATIVE=OFF \ + -DGGML_CPU_ALL_VARIANTS=ON \ + -DGGML_CUDA=ON \ + -DGGML_CUDA_NCCL=OFF \ + ${{ env.CMAKE_ARGS }} ${{ matrix.defines }} + cmake --build build --config Release -j $(nproc) + + - name: Determine tag name + id: tag + uses: ./.github/actions/get-tag-name + + - name: Pack artifacts + id: pack_artifacts + run: | + cp LICENSE ./build/bin/ + tar -czvf llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-${{ matrix.label }}-${{ matrix.build }}.tar.gz --transform "s,^\.,llama-${{ steps.tag.outputs.name }}," -C ./build/bin . + + - name: Upload artifacts + uses: actions/upload-artifact@v6 + with: + path: llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-${{ matrix.label }}-${{ matrix.build }}.tar.gz + name: llama-bin-ubuntu-cuda-${{ matrix.label }}-${{ matrix.build }}.tar.gz + + # ship the CUDA runtime libraries the backend links against, mirroring + # the windows-cuda cudart zip - extract next to the binaries ($ORIGIN rpath) + - name: Pack CUDA runtime + id: pack_cuda_runtime + run: | + major="${{ matrix.label }}" + major="${major%%.*}" + mkdir -p ./cudart + # cp -L dereferences the SONAME symlinks into plain files, so the + # tarball holds exactly 3 files with no versioned duplicates + cp -L /usr/local/cuda/lib64/libcudart.so.${major} ./cudart/ + cp -L /usr/local/cuda/lib64/libcublas.so.${major} ./cudart/ + cp -L /usr/local/cuda/lib64/libcublasLt.so.${major} ./cudart/ + tar -czvf cudart-llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-${{ matrix.label }}-${{ matrix.build }}.tar.gz --transform "s,^\.,cudart-llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-${{ matrix.label }}-${{ matrix.build }}," -C ./cudart . + + - name: Upload CUDA runtime + uses: actions/upload-artifact@v6 + with: + path: cudart-llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-${{ matrix.label }}-${{ matrix.build }}.tar.gz + name: cudart-llama-bin-ubuntu-cuda-${{ matrix.label }}-${{ matrix.build }}.tar.gz + + - name: ccache-clear + uses: ./.github/actions/ccache-clear + with: + key: release-ubuntu-${{ matrix.os }}-cuda-${{ matrix.label }}-${{ matrix.build }} + android-arm64: needs: [check-release, ui-build] if: ${{ needs.check-release.outputs.should_release == 'true' }} @@ -345,6 +484,7 @@ jobs: uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 with: log-accepted-android-sdk-licenses: false + packages: 'platform-tools' - name: Install NDK run: | @@ -987,7 +1127,7 @@ jobs: - cuda: '12.4' arch: x64 defines: '-DGGML_CUDA_CUB_3DOT2=ON' - - cuda: '13.3' + - cuda: '13.4' arch: x64 defines: '' - cuda: '13.4' @@ -1280,7 +1420,7 @@ jobs: matrix: include: - ROCM_VERSION: "10.0.0" - gpu_targets: "gfx908;gfx90a;gfx942;gfx950;gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1033;gfx1034;gfx1035;gfx1036;gfx1100;gfx1101;gfx1102;gfx1150;gfx1151;gfx1152;gfx1200;gfx1201" + gpu_targets: "gfx908;gfx90a;gfx942;gfx950;gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1033;gfx1034;gfx1035;gfx1036;gfx1100;gfx1101;gfx1102;gfx1103;gfx1150;gfx1151;gfx1152;gfx1200;gfx1201" build: 'x64' steps: @@ -1572,6 +1712,7 @@ jobs: - ubuntu-24-rocm - ubuntu-cpu - ubuntu-vulkan + - ubuntu-cuda - ubuntu-24-openvino - ubuntu-24-sycl - android-arm64 @@ -1703,6 +1844,9 @@ jobs: - [Ubuntu s390x (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-s390x.tar.gz) - [Ubuntu x64 (Vulkan)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-vulkan-x64.tar.gz) - [Ubuntu arm64 (Vulkan)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-vulkan-arm64.tar.gz) + - [Ubuntu x64 (CUDA 12)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-12.8-x64.tar.gz) - [CUDA 12.8 libraries](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/cudart-llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-12.8-x64.tar.gz) + - [Ubuntu x64 (CUDA 13)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-13.3-x64.tar.gz) - [CUDA 13.3 libraries](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/cudart-llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-13.3-x64.tar.gz) + - [Ubuntu arm64 (CUDA 13)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-13.3-arm64.tar.gz) - [CUDA 13.3 libraries](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/cudart-llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-13.3-arm64.tar.gz) - [Ubuntu x64 (ROCm 10.0)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-rocm-10.0-x64.tar.gz) - [Ubuntu x64 (OpenVINO)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-openvino-${{ needs.ubuntu-24-openvino.outputs.openvino_version }}-x64.tar.gz) - [Ubuntu x64 (SYCL FP32)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-sycl-fp32-x64.tar.gz) @@ -1716,7 +1860,7 @@ jobs: - [Windows arm64 (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-cpu-arm64.zip) - [Windows arm64 (OpenCL Adreno)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-opencl-adreno-arm64.zip) - [Windows x64 (CUDA 12)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-cuda-12.4-x64.zip) - [CUDA 12.4 DLLs](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/cudart-llama-bin-win-cuda-12.4-x64.zip) - - [Windows x64 (CUDA 13)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-cuda-13.3-x64.zip) - [CUDA 13.3 DLLs](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/cudart-llama-bin-win-cuda-13.3-x64.zip) + - [Windows x64 (CUDA 13)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-cuda-13.4-x64.zip) - [CUDA 13.4 DLLs](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/cudart-llama-bin-win-cuda-13.4-x64.zip) - [Windows arm64 (CUDA 13)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-cuda-13.4-arm64.zip) - [CUDA 13.4 DLLs](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/cudart-llama-bin-win-cuda-13.4-arm64.zip) - [Windows x64 (Vulkan)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-vulkan-x64.zip) - [Windows x64 (OpenVINO)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-win-openvino-${{ needs.windows-openvino.outputs.openvino_version }}-x64.zip) diff --git a/.github/workflows/server-sanitize.yml b/.github/workflows/server-sanitize.yml index 11237cf51b18..43746e91eb35 100644 --- a/.github/workflows/server-sanitize.yml +++ b/.github/workflows/server-sanitize.yml @@ -116,7 +116,6 @@ jobs: run: | source .venv/bin/activate cd tools/server/tests - export ${{ matrix.extra_args }} PYTEST_WORKERS=1 ./tests.sh - name: Slow tests @@ -125,5 +124,4 @@ jobs: run: | source .venv/bin/activate cd tools/server/tests - export ${{ matrix.extra_args }} PYTEST_WORKERS=1 SLOW_TESTS=1 ./tests.sh diff --git a/.github/workflows/server-self-hosted.yml b/.github/workflows/server-self-hosted.yml index de30d1a749b0..8dc4637c4570 100644 --- a/.github/workflows/server-self-hosted.yml +++ b/.github/workflows/server-self-hosted.yml @@ -192,7 +192,7 @@ jobs: PYTEST_WORKERS=1 ./tests.sh server-kleidiai: - runs-on: ah-ubuntu_22_04-c8g_8x + runs-on: ah-ubuntu_24_04-c8g_8x steps: - name: Clone @@ -232,7 +232,7 @@ jobs: - name: Build id: cmake_build run: | - cmake -B build -DGGML_SCHED_NO_REALLOC=ON -DGGML_CPU_KLEIDIAI=ON + cmake -B build -DGGML_SCHED_NO_REALLOC=ON -DGGML_CPU_KLEIDIAI=ON -DLLAMA_FATAL_WARNINGS=ON cmake --build build --config Release -j $(nproc) --target llama-server - name: Python setup diff --git a/.pi/gg/SYSTEM.md b/.pi/gg/SYSTEM.md index 47883081cfad..bd308ea963bc 100644 --- a/.pi/gg/SYSTEM.md +++ b/.pi/gg/SYSTEM.md @@ -6,6 +6,7 @@ General: - PR and commit titles format: ` : `. Lookup recents for examples - Don't try to build or run the code unless you are explicitly asked to do so - Use the `gh` CLI tool when querying PRs, issues, or other GitHub resources +- When [MODEL] is needed, first try to get it from the `PI_MODEL_NAME` env var before asking the user Coding: - When in doubt, always refer to the CONTRIBUTING.md file of the project @@ -20,8 +21,9 @@ Pull requests (PRs): - Don't explicitly wrap lines in the PR description (each paragraph and bullet is a single line) - When creating a pull request, look for the repository's PR template and follow it - For the AI usage disclosure section, write "YES. pi:llama.cpp/[MODEL]" -- Ask the user to tell you what model was used and write it in place of [MODEL] +- If `PI_MODEL_NAME` env var is not set, ask the user to tell you what model was used and write it in place of [MODEL] - Always create the pull requests in draft mode +- Never reply to review comments or post comments on issues/PRs without explicit permission from the user Commits: - On every commit that you make, include a "Assisted-by: pi:llama.cpp/[MODEL]" tag diff --git a/CMakeLists.txt b/CMakeLists.txt index 86b09dfd4640..4feaf083e04e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ include(CheckIncludeFileCXX) ### llama.cpp version set(LLAMA_VERSION_MAJOR 0) set(LLAMA_VERSION_MINOR 4) -set(LLAMA_VERSION_PATCH 0) +set(LLAMA_VERSION_PATCH 1) set(LLAMA_VERSION_BASE "${LLAMA_VERSION_MAJOR}.${LLAMA_VERSION_MINOR}.${LLAMA_VERSION_PATCH}") # whether this is a development/nightly build @@ -197,6 +197,16 @@ llama_option_depr(WARNING LLAMA_CURL) include("cmake/license.cmake") license_add_file("llama.cpp" "LICENSE") +# +# compile options +# + +# clang stores the modification time of the precompiled header sources inside the +# header and rejects it when they differ, so the timestamp is left out of it +add_compile_options( + "$<$<COMPILE_LANG_AND_ID:C,Clang,IntelLLVM>:SHELL:-Xclang -fno-pch-timestamp>" + "$<$<COMPILE_LANG_AND_ID:CXX,Clang,IntelLLVM>:SHELL:-Xclang -fno-pch-timestamp>") + # # 3rd-party # diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 505f5c98f80d..59ec3f311b5d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,8 +20,9 @@ If AI is used to generate any portion of the code, contributors must adhere to t 1. Explicitly disclose the manner in which AI was employed. 2. Check for an existing PR addressing the same change; if one exists, comment there to work with its author instead of opening a duplicate. -3. Perform a comprehensive manual review prior to submitting the pull request. -4. Be prepared to explain every line of code they submitted when asked about it by a maintainer. +3. Perform a comprehensive manual review prior to submitting the pull request. A proper code review usually takes something like one hour per 200-400 LOC and you should be spending **at least that much time on code review alone**. +4. Be prepared to explain every line of code you submit when asked about it by a maintainer. +5. It is strictly prohibited to use AI to write your posts for you (bug reports, feature requests, pull request descriptions, Github discussions, responding to humans, ...). For more info, please refer to the [AGENTS.md](AGENTS.md) file. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 3450ff49000f..0b044228aff8 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -16,7 +16,7 @@ target_link_libraries(${TARGET} PRIVATE target_compile_features(${TARGET} PRIVATE cxx_std_17) # Automatically add all files from the 'licenses' directory -file(GLOB EXTRA_LICENSES "${CMAKE_SOURCE_DIR}/licenses/LICENSE-*") +file(GLOB EXTRA_LICENSES "${PROJECT_SOURCE_DIR}/licenses/LICENSE-*") foreach(FILE_PATH ${EXTRA_LICENSES}) get_filename_component(FILE_NAME "${FILE_PATH}" NAME) diff --git a/ci/run.sh b/ci/run.sh index 1ceb19fd50aa..0595fac5a1b8 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -58,8 +58,6 @@ if [ -n "${GG_BUILD_ROCM}" ] && [ -n "${GITHUB_RUN_ID}" ]; then fi rm -f $OUT/*.log -rm -f $OUT/*.exit -rm -f $OUT/*.md sd=`dirname $0` cd $sd/../ @@ -211,10 +209,6 @@ function gg_wget { cd $cwd } -function gg_printf { - printf -- "$@" >> $OUT/README.md -} - function gg_run { ci=$1 @@ -223,13 +217,10 @@ function gg_run { gg_run_$ci | tee $OUT/$ci.log cur=$? - echo "$cur" > $OUT/$ci.exit set +x set +o pipefail - gg_sum_$ci - ret=$((ret | cur)) } @@ -255,17 +246,6 @@ function gg_run_ctest_debug { set +e } -function gg_sum_ctest_debug { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Runs ctest in debug mode\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '```\n' - gg_printf '%s\n' "$(cat $OUT/${ci}-ctest.log)" - gg_printf '```\n' - gg_printf '\n' -} - # ctest_release function gg_run_ctest_release { @@ -290,16 +270,6 @@ function gg_run_ctest_release { set +e } -function gg_sum_ctest_release { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Runs ctest in release mode\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '```\n' - gg_printf '%s\n' "$(cat $OUT/${ci}-ctest.log)" - gg_printf '```\n' -} - # test_llama_archs_tensor_split function gg_run_test_llama_archs_tensor_split { @@ -324,16 +294,6 @@ function gg_run_test_llama_archs_tensor_split { set +e } -function gg_sum_test_llama_archs_tensor_split { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Runs test-llama-archs with 1 to 4 devices\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '```\n' - gg_printf '%s\n' "$(cat $OUT/${ci}.log)" - gg_printf '```\n' -} - # test_llama_archs_models function gg_run_test_llama_archs_models { @@ -353,16 +313,6 @@ function gg_run_test_llama_archs_models { set +e } -function gg_sum_test_llama_archs_models { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Generates the dummy models used by the model-dependent tests\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '```\n' - gg_printf '%s\n' "$(cat $OUT/${ci}.log)" - gg_printf '```\n' -} - # test_scripts function gg_run_test_scripts { @@ -376,17 +326,6 @@ function gg_run_test_scripts { set +e } -function gg_sum_test_scripts { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Runs test scripts\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '```\n' - gg_printf '%s\n' "$(cat $OUT/${ci}-scripts.log)" - gg_printf '```\n' - gg_printf '\n' -} - function gg_get_model { #local gguf_0="$MNT/models/qwen3/0.6B/ggml-model-f16.gguf" local gguf_0="$MNT/models/qwen3/0.6B/ggml-model-q4_0.gguf" @@ -430,26 +369,6 @@ function gg_run_ctest_with_model_release { cd .. } -function gg_sum_ctest_with_model_debug { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Runs ctest with model files in debug mode\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '```\n' - gg_printf '%s\n' "$(cat $OUT/${ci}-ctest.log)" - gg_printf '```\n' -} - -function gg_sum_ctest_with_model_release { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Runs ctest with model files in release mode\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '```\n' - gg_printf '%s\n' "$(cat $OUT/${ci}-ctest.log)" - gg_printf '```\n' -} - # qwen3_0_6b function gg_run_qwen3_0_6b { @@ -554,50 +473,24 @@ function gg_run_qwen3_0_6b { return 0 } - check_ppl "f16" "$(cat $OUT/${ci}-tg-f16.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log + check_ppl "f16" "$(cat $OUT/${ci}-tg-f16.log | grep "^\[1\]")" if [ -z ${GG_BUILD_NO_BF16} ]; then - check_ppl "bf16" "$(cat $OUT/${ci}-tg-bf16.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - fi - check_ppl "q8_0" "$(cat $OUT/${ci}-tg-q8_0.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - check_ppl "q4_0" "$(cat $OUT/${ci}-tg-q4_0.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - check_ppl "q4_1" "$(cat $OUT/${ci}-tg-q4_1.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - check_ppl "q5_0" "$(cat $OUT/${ci}-tg-q5_0.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - check_ppl "q5_1" "$(cat $OUT/${ci}-tg-q5_1.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - #check_ppl "q2_k" "$(cat $OUT/${ci}-tg-q2_k.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log # note: ppl > 20.0 for this quant and model - check_ppl "q3_k" "$(cat $OUT/${ci}-tg-q3_k.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - check_ppl "q4_k" "$(cat $OUT/${ci}-tg-q4_k.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - check_ppl "q5_k" "$(cat $OUT/${ci}-tg-q5_k.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - check_ppl "q6_k" "$(cat $OUT/${ci}-tg-q6_k.log | grep "^\[1\]")" | tee -a $OUT/${ci}-ppl.log - - cat $OUT/${ci}-imatrix.log | grep "Final" >> $OUT/${ci}-imatrix-sum.log + check_ppl "bf16" "$(cat $OUT/${ci}-tg-bf16.log | grep "^\[1\]")" + fi + check_ppl "q8_0" "$(cat $OUT/${ci}-tg-q8_0.log | grep "^\[1\]")" + check_ppl "q4_0" "$(cat $OUT/${ci}-tg-q4_0.log | grep "^\[1\]")" + check_ppl "q4_1" "$(cat $OUT/${ci}-tg-q4_1.log | grep "^\[1\]")" + check_ppl "q5_0" "$(cat $OUT/${ci}-tg-q5_0.log | grep "^\[1\]")" + check_ppl "q5_1" "$(cat $OUT/${ci}-tg-q5_1.log | grep "^\[1\]")" + #check_ppl "q2_k" "$(cat $OUT/${ci}-tg-q2_k.log | grep "^\[1\]")" # note: ppl > 20.0 for this quant and model + check_ppl "q3_k" "$(cat $OUT/${ci}-tg-q3_k.log | grep "^\[1\]")" + check_ppl "q4_k" "$(cat $OUT/${ci}-tg-q4_k.log | grep "^\[1\]")" + check_ppl "q5_k" "$(cat $OUT/${ci}-tg-q5_k.log | grep "^\[1\]")" + check_ppl "q6_k" "$(cat $OUT/${ci}-tg-q6_k.log | grep "^\[1\]")" set +e } -function gg_sum_qwen3_0_6b { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Qwen3 0.6B:\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '- perplexity:\n%s\n' "$(cat $OUT/${ci}-ppl.log)" - gg_printf '- imatrix:\n```\n%s\n```\n' "$(cat $OUT/${ci}-imatrix-sum.log)" - gg_printf '- f16:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-f16.log)" - if [ -z ${GG_BUILD_NO_BF16} ]; then - gg_printf '- bf16:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-bf16.log)" - fi - gg_printf '- q8_0:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q8_0.log)" - gg_printf '- q4_0:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q4_0.log)" - gg_printf '- q4_1:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q4_1.log)" - gg_printf '- q5_0:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q5_0.log)" - gg_printf '- q5_1:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q5_1.log)" - gg_printf '- q2_k:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q2_k.log)" - gg_printf '- q3_k:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q3_k.log)" - gg_printf '- q4_k:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q4_k.log)" - gg_printf '- q5_k:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q5_k.log)" - gg_printf '- q6_k:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q6_k.log)" - gg_printf '- save-load-state: \n```\n%s\n```\n' "$(cat $OUT/${ci}-save-load-state.log)" -} - # bge-small function gg_run_embd_bge_small { @@ -639,15 +532,6 @@ function gg_run_embd_bge_small { set +e } -function gg_sum_embd_bge_small { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'BGE Small (BERT):\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '- f16: \n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-f16.log)" - gg_printf '- q8_0:\n```\n%s\n```\n' "$(cat $OUT/${ci}-tg-q8_0.log)" -} - # rerank_tiny function gg_run_rerank_tiny { @@ -704,66 +588,58 @@ function gg_run_rerank_tiny { set +e } -function gg_sum_rerank_tiny { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Rerank Tiny (Jina):\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '- f16: \n```\n%s\n```\n' "$(cat $OUT/${ci}-rk-f16.log)" -} - function gg_check_build_requirements { if ! command -v git &> /dev/null; then - gg_printf 'git not found, please install\n' + echo 'git not found, please install' exit 1 fi if ! command -v git-lfs &> /dev/null; then - gg_printf 'git-lfs not found, please install\n' + echo 'git-lfs not found, please install' exit 1 fi if ! git config --get filter.lfs.clean &> /dev/null; then - gg_printf 'git-lfs not initialized, please run `git lfs install`\n' + echo 'git-lfs not initialized, please run `git lfs install`' exit 1 fi if ! command -v wget &> /dev/null; then - gg_printf 'wget not found, please install\n' + echo 'wget not found, please install' exit 1 fi if ! command -v python3 &> /dev/null; then - gg_printf 'python3 not found, please install\n' + echo 'python3 not found, please install' exit 1 fi if ! command -v pip3 &> /dev/null; then - gg_printf 'pip3 not found, please install\n' + echo 'pip3 not found, please install' exit 1 fi if ! python3 -m ensurepip --help &> /dev/null; then - gg_printf 'ensurepip not found, please install python3-venv package\n' + echo 'ensurepip not found, please install python3-venv package' exit 1 fi if ! command -v cmake &> /dev/null; then - gg_printf 'cmake not found, please install\n' + echo 'cmake not found, please install' exit 1 fi if ! command -v ccache &> /dev/null; then - gg_printf 'ccache not found, please consider installing for faster builds\n' + echo 'ccache not found, please consider installing for faster builds' fi if ! command -v ctest &> /dev/null; then - gg_printf 'ctest not found, please install\n' + echo 'ctest not found, please install' exit 1 fi if ! command -v unzip &> /dev/null; then - gg_printf 'unzip not found, please install\n' + echo 'unzip not found, please install' exit 1 fi } @@ -775,7 +651,11 @@ function gg_run_test_backend_ops { set -e - local args_extra="-j $(nproc)" + local n_jobs=$(nproc) + if [ "${n_jobs}" -gt 2 ]; then + n_jobs=2 + fi + local args_extra="-j ${n_jobs}" # TODO: fix multi-threaded for ROCm # https://github.com/ggml-org/llama.cpp/actions/runs/34576278519/job/103297889044?pr=28740#step:3:4865 @@ -789,26 +669,21 @@ function gg_run_test_backend_ops { args_extra="" fi + # TODO: OpenVINO GPU plugin crashes (CL_OUT_OF_RESOURCES) with 2 concurrent workers on GPU. + if [ ! -z "${GG_BUILD_OPENVINO}" ] && [ "${GGML_OPENVINO_DEVICE:-}" = "GPU" ]; then + args_extra="" + fi + + # TODO: reduce the test-backend-ops timeout to 1800s if [ ! -z ${GG_BUILD_HIGH_PERF} ]; then - (time ./bin/test-backend-ops ${args_extra} -b CPU) 2>&1 | tee -a $OUT/${ci}-test-backend-ops.log + (time timeout 3600 ./bin/test-backend-ops ${args_extra} -b CPU) 2>&1 | tee -a $OUT/${ci}-test-backend-ops.log else - (time ./bin/test-backend-ops ${args_extra} ) 2>&1 | tee -a $OUT/${ci}-test-backend-ops.log + (time timeout 3600 ./bin/test-backend-ops ${args_extra} ) 2>&1 | tee -a $OUT/${ci}-test-backend-ops.log fi set +e } -function gg_sum_test_backend_ops { - gg_printf '### %s\n\n' "${ci}" - - gg_printf 'Runs test-backend-ops\n' - gg_printf '- status: %s\n' "$(cat $OUT/${ci}.exit)" - gg_printf '```\n' - gg_printf '%s\n' "$(cat $OUT/${ci}-test-backend-ops.log)" - gg_printf '```\n' - gg_printf '\n' -} - ## main export LLAMA_ARG_LOG_PREFIX=1 @@ -856,6 +731,4 @@ if [ -z ${GG_BUILD_LOW_PERF} ]; then test $ret -eq 0 && gg_run ctest_with_model_release fi -cat $OUT/README.md - exit $ret diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index a1ef09d1d640..b061bf688e67 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -84,6 +84,8 @@ add_library(${TARGET} imatrix-loader.cpp imatrix-loader.h json-schema-to-grammar.cpp + json-schema.cpp + json-schema.h json.cpp json.h llguidance.cpp @@ -136,8 +138,6 @@ set_target_properties(${TARGET} PROPERTIES target_include_directories(${TARGET} PUBLIC .) target_link_libraries (${TARGET} PUBLIC vendor::nlohmann vendor::sheredom) target_compile_features (${TARGET} PUBLIC cxx_std_17) -target_precompile_headers (${TARGET} PRIVATE common.h) -target_precompile_headers (${TARGET} PRIVATE chat.h) if (LLAMA_SUBPROCESS) target_compile_definitions(${TARGET} PUBLIC LLAMA_SUBPROCESS) diff --git a/common/arg.cpp b/common/arg.cpp index 86aed32ce413..f5bce417e7a4 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -2296,14 +2296,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex ).set_sampling()); add_opt(common_arg( {"-j", "--json-schema"}, "SCHEMA", - "JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object\nFor schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead", + "JSON schema to constrain generations (https://json-schema.org/), e.g. `{\"type\": \"object\"}` for any JSON object", [](common_params & params, const std::string & value) { params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(json::parse(value))}; } ).set_sampling()); add_opt(common_arg( {"-jf", "--json-schema-file"}, "FILE", - "File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object\nFor schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead", + "File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{\"type\": \"object\"}` for any JSON object", [](common_params & params, const std::string & value) { std::ifstream file(value); if (!file) { @@ -3925,7 +3925,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex {"--no-log-jsonl"}, "Log as JSONL (one JSON object per line) to stdout, this also disables colored logging (default: disabled)", [](common_params &, bool value) { - common_log_set_jsonl(common_log_main(), value); + common_log_set_jsonl(value); } ).set_env("LLAMA_ARG_LOG_JSONL")); add_opt(common_arg( diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp index d7e117e4d98b..b78789d8c0df 100644 --- a/common/chat-auto-parser-generator.cpp +++ b/common/chat-auto-parser-generator.cpp @@ -5,6 +5,7 @@ #include "common.h" #include "json-schema-to-grammar.h" #include "log.h" +#include "parsers/parsers.h" #include "peg-parser.h" #include <stdexcept> @@ -12,16 +13,6 @@ using json = common_json; -// Helper to iterate over tools/functions -static void foreach_function(const json & tools, const std::function<void(const json &)> & fn) { - for (const auto & tool : tools) { - if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) { - continue; - } - fn(tool); - } -} - namespace autoparser { parser_build_context::parser_build_context(common_chat_peg_builder & p, const generation_params & inputs) : @@ -87,15 +78,6 @@ common_chat_params peg_generator::generate_parser(const common_chat_template & if (include_grammar) { data.grammar_lazy = !has_response_format && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.contains("parameters") ? function.at("parameters") : json::object(); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); @@ -312,7 +294,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context foreach_function(inputs.tools, [&](const json & tool) { const auto & func = tool.at("function"); std::string name = func.at("name"); - const auto & schema = func.contains("parameters") ? func.at("parameters") : json::object(); + const auto schema = common_chat_tool_parameters(func); // Build call_id parser based on position (if supported) bool have_call_id = false; @@ -383,43 +365,31 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte common_peg_parser tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { - const auto & func = tool.at("function"); - std::string name = func.at("name"); - auto params = func.contains("parameters") ? func.at("parameters") : json::object(); - const auto & properties = params.contains("properties") ? params.at("properties") : json::object(); - - std::set<std::string> required; - if (params.contains("required")) { - required = params.at("required").get<std::set<std::string>>(); - } - - auto schema_info = common_schema_info(); - schema_info.resolve_refs(params); + const auto & func = tool.at("function"); + std::string name = func.at("name"); // Build parser for each argument, separating required and optional std::vector<common_peg_parser> required_parsers; std::vector<common_peg_parser> optional_parsers; - for (const auto & [param_name, param_schema] : properties.items()) { - bool is_required = required.find(param_name) != required.end(); - + foreach_parameter(func, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { auto arg = - p.tool_arg(p.tool_arg_open(arguments.name_prefix + p.tool_arg_name(p.literal(param_name)) + + p.tool_arg(p.tool_arg_open(arguments.name_prefix + p.tool_arg_name(p.literal(param.name)) + arguments.name_suffix) + arguments.value_prefix + - (schema_info.resolves_to_string(param_schema) ? + (param.schema->may_be_string() ? p.ac(p.tool_arg_string_value(until_suffix) + p.tool_arg_close(p.literal(arguments.value_suffix)), arguments.value_suffix) : (p.tool_arg_json_value(p.schema( - p.json(), "tool-" + name + "-arg-" + param_name + "-schema", param_schema, false)) + + p.json(), "tool-" + name + "-arg-" + param.name + "-schema", doc, *param.schema)) + p.tool_arg_close(p.literal(arguments.value_suffix))))); - auto named_arg = p.rule("tool-" + name + "-arg-" + param_name, arg); - if (is_required) { + auto named_arg = p.rule("tool-" + name + "-arg-" + param.name, arg); + if (param.required) { required_parsers.push_back(named_arg); } else { optional_parsers.push_back(named_arg); } - } + }); // Build required arg sequence in definition order common_peg_parser args_seq = p.eps(); diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index 79b97a80f1b2..ffa43a318888 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -488,7 +488,7 @@ common_peg_parser common_chat_peg_builder::standard_constructed_tools( } const auto & function = tool_def.at("function"); std::string name = function.at("name"); - ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); + ordered_json params = common_chat_tool_parameters(function); // Build argument parsers auto args = eps(); @@ -565,7 +565,7 @@ common_peg_parser common_chat_peg_builder::python_style_tool_calls( } const auto & function = tool_def.at("function"); std::string name = function.at("name"); - ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); + ordered_json params = common_chat_tool_parameters(function); auto args = eps(); if (params.contains("properties") && !params["properties"].empty()) { @@ -640,7 +640,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key( } const auto & function = tool_def.at("function"); std::string name = function.at("name"); - ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); + ordered_json params = common_chat_tool_parameters(function); // Build inner object fields std::vector<common_peg_parser> inner_fields; @@ -726,7 +726,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys( } const auto & function = tool_def.at("function"); std::string name = function.at("name"); - ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); + ordered_json params = common_chat_tool_parameters(function); auto nested_name = literal("\"" + nested_name_field + "\"") + space() + literal(":") + space() + atomic(literal("\"") + tool_name(literal(name)) + literal("\"")); @@ -795,7 +795,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys( } const auto & function = tool_def.at("function"); std::string name = function.at("name"); - ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); + ordered_json params = common_chat_tool_parameters(function); auto tool_name_ = name_key_parser + space() + literal(":") + space() + atomic(literal("\"") + tool_name(literal(name)) + literal("\"")); diff --git a/common/chat.cpp b/common/chat.cpp index faf27f78672d..3a204e12d758 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -574,6 +574,16 @@ json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & t return result; } +json common_chat_tool_parameters(const json & function) { + if (function.contains("parameters")) { + const auto & params = function.at("parameters"); + if (!params.is_null() && !(params.is_object() && params.empty())) { + return params; + } + } + return json{{"type", "object"}, {"properties", json::object()}}; +} + std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const json & tools) { std::vector<common_chat_tool> result; diff --git a/common/chat.h b/common/chat.h index cb39e3458f44..0e1423a5a3b1 100644 --- a/common/chat.h +++ b/common/chat.h @@ -360,6 +360,9 @@ common_json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg common_json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & tools); +// The parameters schema of a function tool. A tool without parameters, or with an empty {}, takes zero arguments. +common_json common_chat_tool_parameters(const common_json & function); + // get template caps, useful for reporting to server /props endpoint std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_templates * chat_templates); diff --git a/common/common.cpp b/common/common.cpp index a6a5364163bc..ef1ccc4d6a7e 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -1586,6 +1586,11 @@ common_context_seq_rm_type common_context_can_seq_rm(llama_context * ctx) { return COMMON_CONTEXT_SEQ_RM_TYPE_NO; } + if (llama_n_rs_seq(ctx) > 0) { + COM_TRC("%s", "the context supports bounded partial sequence removal\n"); + return COMMON_CONTEXT_SEQ_RM_TYPE_RS; + } + common_context_seq_rm_type res = COMMON_CONTEXT_SEQ_RM_TYPE_PART; llama_memory_clear(mem, true); @@ -1602,12 +1607,6 @@ common_context_seq_rm_type common_context_can_seq_rm(llama_context * ctx) { goto done; } - if (llama_n_rs_seq(ctx) > 0) { - COM_TRC("%s", "the context supports bounded partial sequence removal\n"); - res = COMMON_CONTEXT_SEQ_RM_TYPE_RS; - goto done; - } - // try to remove the last tokens if (!llama_memory_seq_rm(mem, 0, 1, -1)) { COM_TRC("%s", "the context does not support partial sequence removal\n"); diff --git a/common/fit.cpp b/common/fit.cpp index c601fe405ea5..7a0300829508 100644 --- a/common/fit.cpp +++ b/common/fit.cpp @@ -1,5 +1,6 @@ #include "fit.h" +#include "json.h" #include "log.h" #include "../src/llama-ext.h" @@ -915,6 +916,9 @@ void common_memory_breakdown_print(const struct llama_context * ctx) { std::vector<std::array<std::string, 9>> table_data; table_data.reserve(devices.size()); + + // same data as the table below, for --log-jsonl consumers + common_json rows = common_json::array(); const std::string template_header = "%s: | %s | %s %s %s %s %s %s %s |\n"; const std::string template_gpu = "%s: | %s | %s = %s + (%s = %s + %s + %s) + %s |\n"; const std::string template_other = "%s: | %s | %s %s %s = %s + %s + %s %s |\n"; @@ -989,6 +993,19 @@ void common_memory_breakdown_print(const struct llama_context * ctx) { std::to_string(mb.context / MiB), std::to_string(mb.compute / MiB), std::to_string(unaccounted / static_cast<int64_t>(MiB))}); + + rows.push_back({ + {"kind", "device"}, + {"name", name}, + {"description", desc}, + {"total", total / MiB}, + {"free", free / MiB}, + {"self", self / MiB}, + {"model", mb.model / MiB}, + {"context", mb.context / MiB}, + {"compute", mb.compute / MiB}, + {"unaccounted", unaccounted / static_cast<int64_t>(MiB)}, + }); } // print memory breakdown for host: @@ -1004,6 +1021,15 @@ void common_memory_breakdown_print(const struct llama_context * ctx) { std::to_string(mb_host.context / MiB), std::to_string(mb_host.compute / MiB), ""}); // unaccounted + + rows.push_back({ + {"kind", "host"}, + {"name", "Host"}, + {"self", self / MiB}, + {"model", mb_host.model / MiB}, + {"context", mb_host.context / MiB}, + {"compute", mb_host.compute / MiB}, + }); } // print memory breakdown for all remaining buffer types: @@ -1025,6 +1051,16 @@ void common_memory_breakdown_print(const struct llama_context * ctx) { std::to_string(mb.context / MiB), std::to_string(mb.compute / MiB), ""}); // unaccounted + + rows.push_back({ + {"kind", "buffer_type"}, + {"name", name}, + {"self", self / MiB}, + {"model", mb.model / MiB}, + {"context", mb.context / MiB}, + {"compute", mb.compute / MiB}, + }); + seen_buffer_types.insert(buft); } @@ -1042,6 +1078,11 @@ void common_memory_breakdown_print(const struct llama_context * ctx) { __func__, td[1].c_str(), td[2].c_str(), td[3].c_str(), td[4].c_str(), td[5].c_str(), td[6].c_str(), td[7].c_str(), td[8].c_str()); } + + LOG_JSON("fit_memory_breakdown", common_json({ + {"unit", "MiB"}, + {"rows", rows}, + })); } void common_fit_print( diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp index 49354c7c9cd2..252ab55de20e 100644 --- a/common/jinja/runtime.cpp +++ b/common/jinja/runtime.cpp @@ -842,6 +842,12 @@ value member_expression::execute_impl(context & ctx) { } else { property = this->property->execute(ctx); } + } else if (is_stmt<integer_literal>(this->property)) { + // syntax: obj.index + property = mk_val<value_int>(cast_stmt<integer_literal>(this->property)->val); + if (property->as_int() < 0) { + throw std::runtime_error("Static member property cannot be negative"); + } } else { // syntax: obj.prop if (!is_stmt<identifier>(this->property)) { diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index a7a18857d713..e0426098c08a 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -1,5 +1,7 @@ #include "json-schema-to-grammar.h" #include "common.h" +#include "trie.h" +#include "unicode.h" #include <algorithm> #include <limits> @@ -336,18 +338,20 @@ static size_t gbnf_escape_length(const std::string & pattern, size_t pos) { return 2 + n_hex; } -class common_schema_converter { +class common_chat_schema_converter { private: - friend class common_schema_info; friend std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options); - std::function<json(const std::string &)> _fetch_json; bool _dotall; std::map<std::string, std::string> _rules; - std::unordered_map<std::string, json> _refs; std::unordered_set<std::string> _refs_being_resolved; std::vector<std::string> _errors; std::vector<std::string> _warnings; + template <typename T> + static const T & as(const common_chat_schema & node) { + return static_cast<const T &>(node); + } + std::string _add_rule(const std::string & name, const std::string & rule) { std::string esc_name = regex_replace(name, INVALID_RULE_CHARS_RE, "-"); if (_rules.find(esc_name) == _rules.end() || _rules[esc_name] == rule) { @@ -363,11 +367,11 @@ class common_schema_converter { return key; } - std::string _generate_union_rule(const std::string & name, const std::vector<json> & alt_schemas) { + std::string _generate_union_rule(const std::string & name, const std::vector<common_chat_schema_ptr> & alt_schemas) { std::vector<std::string> rules; rules.reserve(alt_schemas.size()); for (size_t i = 0; i < alt_schemas.size(); i++) { - rules.push_back(visit(alt_schemas[i], name + (name.empty() ? "alternative-" : "-") + std::to_string(i))); + rules.push_back(visit(*alt_schemas[i], name + (name.empty() ? "alternative-" : "-") + std::to_string(i))); } return string_join(rules, " | "); } @@ -634,85 +638,68 @@ class common_schema_converter { -> ["] ( [a] ([l] ([s] ([o] char+ | [^"o] char*) | [^"s] char*) | [n] ([d] char+ | [^"d] char*) | [^"ln] char*) | [^"a] char* )? ["] */ std::string _not_strings(const std::vector<std::string> & strings) { - - struct TrieNode { - std::map<char, TrieNode> children; - bool is_end_of_string; - - TrieNode() : is_end_of_string(false) {} - - void insert(const std::string & string) { - auto *node = this; - for (char c : string) { - node = &node->children[c]; - } - node->is_end_of_string = true; - } - }; - - TrieNode trie; - for (const auto & s : strings) { - trie.insert(s); - } + common_trie trie(strings); std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char")); std::ostringstream out; out << "[\"] ( "; - std::function<void(const TrieNode &)> visit = [&](const TrieNode & node) { - std::ostringstream rejects; + std::function<void(size_t)> visit = [&](size_t idx) { + const auto & node = trie.nodes[idx]; + std::string rejects; auto first = true; - for (const auto & kv : node.children) { - rejects << kv.first; + for (const auto & [cpt, child] : node.children) { + std::string c = common_unicode_cpt_to_utf8(cpt); + rejects += c; if (first) { first = false; } else { out << " | "; } - out << "[" << kv.first << "]"; - if (!kv.second.children.empty()) { + out << "[" << c << "]"; + if (!trie.nodes[child].children.empty()) { out << " ("; - visit(kv.second); + visit(child); out << ")"; - } else if (kv.second.is_end_of_string) { + } else { out << " " << char_rule << "+"; } } if (!node.children.empty()) { - if (!first) { - out << " | "; - } - out << "[^\"" << rejects.str() << "] " << char_rule << "*"; + out << " | [^\"" << rejects << "] " << char_rule << "*"; } }; - visit(trie); + visit(0); out << " )"; - if (!trie.is_end_of_string) { + if (trie.nodes[0].pattern < 0) { out << "?"; } out << " [\"]"; return out.str(); } - std::string _resolve_ref(const std::string & ref) { - auto it = ref.find('#'); - std::string ref_fragment = it != std::string::npos ? ref.substr(it + 1) : ref; + std::string _resolve_ref(const common_chat_schema_ref & schema) { + auto it = schema.ref.find('#'); + std::string ref_fragment = it != std::string::npos ? schema.ref.substr(it + 1) : schema.ref; static const std::regex nonalphanumeric_regex(R"([^a-zA-Z0-9-]+)"); std::string ref_name = "ref" + std::regex_replace(ref_fragment, nonalphanumeric_regex, "-"); - if (_rules.find(ref_name) == _rules.end() && _refs_being_resolved.find(ref) == _refs_being_resolved.end()) { - _refs_being_resolved.insert(ref); - json resolved = _refs[ref]; - ref_name = visit(resolved, ref_name); - _refs_being_resolved.erase(ref); + if (_rules.find(ref_name) == _rules.end() && _refs_being_resolved.find(schema.ref) == _refs_being_resolved.end()) { + if (!schema.target) { + _errors.push_back("Unresolved $ref " + schema.ref); + return ""; + } + _refs_being_resolved.insert(schema.ref); + ref_name = visit(*schema.target, ref_name); + _refs_being_resolved.erase(schema.ref); } return ref_name; } std::string _build_object_rule( - const std::vector<std::pair<std::string, json>> & properties, + const std::vector<std::pair<std::string, const common_chat_schema *>> & properties, const std::unordered_set<std::string> & required, const std::string & name, - const json & additional_properties) + const common_chat_schema * additional_properties) { std::vector<std::string> required_props; std::vector<std::string> optional_props; @@ -722,7 +709,7 @@ class common_schema_converter { const auto &prop_name = kv.first; const auto &prop_schema = kv.second; - std::string prop_rule_name = visit(prop_schema, name + (name.empty() ? "" : "-") + prop_name); + std::string prop_rule_name = visit(*prop_schema, name + (name.empty() ? "" : "-") + prop_name); prop_kv_rule_names[prop_name] = _add_rule( name + (name.empty() ? "" : "-") + prop_name + "-kv", format_literal(json(prop_name).dump()) + " space \":\" space " + prop_rule_name @@ -734,10 +721,10 @@ class common_schema_converter { } prop_names.push_back(prop_name); } - if ((additional_properties.is_boolean() && additional_properties.get<bool>()) || additional_properties.is_object()) { + if (additional_properties) { std::string sub_name = name + (name.empty() ? "" : "-") + "additional"; std::string value_rule = - additional_properties.is_object() ? visit(additional_properties, sub_name + "-value") + additional_properties->kind() != common_chat_schema::KIND_ANY ? visit(*additional_properties, sub_name + "-value") : _add_primitive("value", PRIMITIVE_RULES.at("value")); auto key_rule = @@ -825,267 +812,163 @@ class common_schema_converter { } public: - common_schema_converter( - const std::function<json(const std::string &)> & fetch_json, - bool dotall) - : _fetch_json(fetch_json), _dotall(dotall) - { + explicit common_chat_schema_converter(bool dotall) : _dotall(dotall) { _rules["space"] = SPACE_RULE; } - void resolve_refs(json & schema, const std::string & url) { - /* - * Resolves all $ref fields in the given schema, fetching any remote schemas, - * replacing each $ref with absolute reference URL and populates _refs with the - * respective referenced (sub)schema dictionaries. - */ - std::function<void(json &)> visit_refs = [&](json & n) { - if (n.is_array()) { - for (auto & x : n) { - visit_refs(x); - } - } else if (n.is_object()) { - if (n.contains("$ref")) { - std::string ref = n["$ref"]; - if (_refs.find(ref) == _refs.end()) { - json target; - if (ref.find("https://") == 0) { - std::string base_url = ref.substr(0, ref.find('#')); - auto it = _refs.find(base_url); - if (it != _refs.end()) { - target = it->second; - } else { - // Fetch the referenced schema and resolve its refs - auto referenced = _fetch_json(ref); - resolve_refs(referenced, base_url); - _refs[base_url] = referenced; - } - if (ref.find('#') == std::string::npos || ref.substr(ref.find('#') + 1).empty()) { - return; - } - } else if (ref.find("#/") == 0) { - target = schema; - n["$ref"] = url + ref; - ref = url + ref; - } else { - _errors.push_back("Unsupported ref: " + ref); - return; - } - std::string pointer = ref.substr(ref.find('#') + 1); - std::vector<std::string> tokens = string_split(pointer, "/"); - for (size_t i = 1; i < tokens.size(); ++i) { - const std::string& sel = tokens[i]; - if (target.is_object() && target.contains(sel)) { - target = target[sel]; - } else if (target.is_array()) { - size_t sel_index; - try { - sel_index = std::stoull(sel); - } catch (const std::invalid_argument & e) { - sel_index = target.size(); - } - if (sel_index >= target.size()) { - _errors.push_back("Error resolving ref " + ref + ": " + sel + " not in " + target.dump()); - return; - } - target = target[sel_index]; - } else { - _errors.push_back("Error resolving ref " + ref + ": " + sel + " not in " + target.dump()); - return; - } - } - _refs[ref] = target; - } - } else { - for (const auto & kv : n.items()) { - visit_refs(kv.value()); - } - } - } - }; - - visit_refs(schema); + std::string add_schema(const std::string & name, const common_chat_schema & schema) { + return visit(schema, name); } static std::string _generate_constant_rule(const json & value) { return format_literal(value.dump()); } - std::string visit(const json & schema, const std::string & name) { - json schema_type = schema.contains("type") ? schema["type"] : json(); - std::string schema_format = schema.contains("format") ? schema["format"].get<std::string>() : ""; - std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name; + std::string _visit_primitive(const std::string & rule_name, const std::string & type) { + return _add_primitive(rule_name == "root" ? "root" : type, PRIMITIVE_RULES.at(type)); + } - if (schema.contains("$ref")) { - return _add_rule(rule_name, _resolve_ref(schema["$ref"])); - } - if (schema.contains("oneOf") || schema.contains("anyOf")) { - const json & alts = schema.contains("oneOf") ? schema.at("oneOf") : schema.at("anyOf"); - std::vector<json> alt_schemas; - for (const auto & alt : alts) { - alt_schemas.push_back(alt); - } - return _add_rule(rule_name, _generate_union_rule(name, alt_schemas)); - } - if (schema_type.is_array()) { - std::vector<json> schema_types; - for (const auto & t : schema_type) { - json schema_copy(schema); - schema_copy["type"] = t; - schema_types.push_back(schema_copy); - } - return _add_rule(rule_name, _generate_union_rule(name, schema_types)); - } - if (schema.contains("const")) { - return _add_rule(rule_name, _generate_constant_rule(schema["const"])); - } - if (schema.contains("enum")) { - std::vector<std::string> enum_values; - for (const auto & v : schema["enum"]) { - enum_values.push_back(_generate_constant_rule(v)); - } - return _add_rule(rule_name, "(" + string_join(enum_values, " | ") + ")"); - } - if ((schema_type.is_null() || schema_type == "object") - && (schema.contains("properties") || - (schema.contains("additionalProperties") && schema["additionalProperties"] != true))) { - std::unordered_set<std::string> required; - if (schema.contains("required") && schema["required"].is_array()) { - for (const auto & item : schema["required"]) { - if (item.is_string()) { - required.insert(item.get<std::string>()); + std::string _visit_all_of(const common_chat_schema_all_of & schema, const std::string & name, const std::string & rule_name) { + std::unordered_set<std::string> required; + std::vector<std::pair<std::string, const common_chat_schema *>> properties; + std::map<std::string, size_t> enum_values; + std::function<void(const common_chat_schema &, bool)> add_component = [&](const common_chat_schema & comp, bool is_required) { + if (comp.kind() == common_chat_schema::KIND_REF) { + if (const auto * target = as<common_chat_schema_ref>(comp).target) { + add_component(*target, is_required); + } + } else if (comp.kind() == common_chat_schema::KIND_OBJECT) { + for (const auto & prop : as<common_chat_schema_object>(comp).properties) { + properties.emplace_back(prop.name, prop.schema.get()); + if (is_required) { + required.insert(prop.name); } } + } else if (comp.kind() == common_chat_schema::KIND_ENUM) { + for (const auto & v : as<common_chat_schema_enum>(comp).values) { + enum_values[_generate_constant_rule(v)] += 1; + } } - std::vector<std::pair<std::string, json>> properties; - if (schema.contains("properties")) { - for (const auto & prop : schema["properties"].items()) { - properties.emplace_back(prop.key(), prop.value()); + }; + for (const auto & child : schema.children) { + if (child->kind() == common_chat_schema::KIND_ANY_OF) { + for (const auto & alt : as<common_chat_schema_any_of>(*child).children) { + add_component(*alt, false); } + } else { + add_component(*child, true); } - return _add_rule(rule_name, - _build_object_rule( - properties, required, name, - schema.contains("additionalProperties") ? schema["additionalProperties"] : json())); } - if ((schema_type.is_null() || schema_type == "object" || schema_type == "string") && schema.contains("allOf")) { - std::unordered_set<std::string> required; - std::vector<std::pair<std::string, json>> properties; - std::map<std::string, size_t> enum_values; - const std::string& hybrid_name = name; - std::function<void(const json &, bool)> add_component = [&](const json & comp_schema, bool is_required) { - if (comp_schema.contains("$ref")) { - add_component(_refs[comp_schema["$ref"]], is_required); - } else if (comp_schema.contains("properties")) { - for (const auto & prop : comp_schema["properties"].items()) { - properties.emplace_back(prop.key(), prop.value()); - if (is_required) { - required.insert(prop.key()); - } - } - } else if (comp_schema.contains("enum")) { - for (const auto & v : comp_schema["enum"]) { - const auto rule = _generate_constant_rule(v); - if (enum_values.find(rule) == enum_values.end()) { - enum_values[rule] = 0; - } - enum_values[rule] += 1; - } - } else { - // todo warning + if (!enum_values.empty()) { + std::vector<std::string> enum_intersection; + for (const auto & p : enum_values) { + if (p.second == schema.children.size()) { + enum_intersection.push_back(p.first); } - }; - for (const auto & t : schema["allOf"]) { - if (t.contains("anyOf")) { - for (const auto & tt : t["anyOf"]) { - add_component(tt, false); - } - } else { - add_component(t, true); + } + if (!enum_intersection.empty()) { + return _add_rule(rule_name, "(" + string_join(enum_intersection, " | ") + ")"); + } + } + return _add_rule(rule_name, _build_object_rule(properties, required, name, nullptr)); + } + + std::string visit(const common_chat_schema & schema, const std::string & name) { + std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name; + std::string sub_name = name + (name.empty() ? "" : "-"); + + switch (schema.kind()) { + case common_chat_schema::KIND_REF: + return _add_rule(rule_name, _resolve_ref(as<common_chat_schema_ref>(schema))); + case common_chat_schema::KIND_ANY_OF: + return _add_rule(rule_name, _generate_union_rule(name, as<common_chat_schema_any_of>(schema).children)); + case common_chat_schema::KIND_ALL_OF: + return _visit_all_of(as<common_chat_schema_all_of>(schema), name, rule_name); + case common_chat_schema::KIND_CONST: + return _add_rule(rule_name, _generate_constant_rule(as<common_chat_schema_const>(schema).value)); + case common_chat_schema::KIND_ENUM: { + std::vector<std::string> enum_values; + for (const auto & v : as<common_chat_schema_enum>(schema).values) { + enum_values.push_back(_generate_constant_rule(v)); } + return _add_rule(rule_name, "(" + string_join(enum_values, " | ") + ")"); } - if (!enum_values.empty()) { - std::vector<std::string> enum_intersection; - for (const auto & p : enum_values) { - if (p.second == schema["allOf"].size()) { - enum_intersection.push_back(p.first); - } + case common_chat_schema::KIND_OBJECT: { + const auto & obj = as<common_chat_schema_object>(schema); + if (obj.properties.empty() && obj.additional_properties && obj.additional_properties->kind() == common_chat_schema::KIND_ANY) { + return _add_rule(rule_name, _add_primitive("object", PRIMITIVE_RULES.at("object"))); } - if (!enum_intersection.empty()) { - return _add_rule(rule_name, "(" + string_join(enum_intersection, " | ") + ")"); + std::vector<std::pair<std::string, const common_chat_schema *>> properties; + std::unordered_set<std::string> required; + for (const auto & prop : obj.properties) { + properties.emplace_back(prop.name, prop.schema.get()); + if (prop.required) { + required.insert(prop.name); + } } + return _add_rule(rule_name, _build_object_rule(properties, required, name, obj.additional_properties.get())); } - return _add_rule(rule_name, _build_object_rule(properties, required, hybrid_name, json())); - } - if ((schema_type.is_null() || schema_type == "array") && (schema.contains("items") || schema.contains("prefixItems"))) { - json items = schema.contains("items") ? schema["items"] : schema["prefixItems"]; - if (items.is_array()) { + case common_chat_schema::KIND_TUPLE: { + const auto & items = as<common_chat_schema_tuple>(schema).items; std::string rule = "\"[\" space "; for (size_t i = 0; i < items.size(); i++) { if (i > 0) { rule += " \",\" space "; } - rule += visit(items[i], name + (name.empty() ? "" : "-") + "tuple-" + std::to_string(i)); + rule += visit(*items[i], sub_name + "tuple-" + std::to_string(i)); } rule += " space \"]\""; return _add_rule(rule_name, rule); } - std::string item_rule_name = visit(items, name + (name.empty() ? "" : "-") + "item"); - int min_items = schema.contains("minItems") ? schema["minItems"].get<int>() : 0; - json max_items_json = schema.contains("maxItems") ? schema["maxItems"] : json(); - int max_items = max_items_json.is_number_integer() ? max_items_json.get<int>() : std::numeric_limits<int>::max(); - - return _add_rule(rule_name, "\"[\" space " + build_repetition(item_rule_name, min_items, max_items, "\",\" space") + " space \"]\""); - } - if ((schema_type.is_null() || schema_type == "string") && schema.contains("pattern")) { - return _visit_pattern(schema["pattern"], rule_name); - } - if ((schema_type.is_null() || schema_type == "string") && std::regex_match(schema_format, std::regex("^uuid[1-5]?$"))) { - return _add_primitive(rule_name == "root" ? "root" : schema_format, PRIMITIVE_RULES.at("uuid")); - } - if ((schema_type.is_null() || schema_type == "string") && STRING_FORMAT_RULES.find(schema_format + "-string") != STRING_FORMAT_RULES.end()) { - auto prim_name = schema_format + "-string"; - return _add_rule(rule_name, _add_primitive(prim_name, STRING_FORMAT_RULES.at(prim_name))); - } - if (schema_type == "string" && (schema.contains("minLength") || schema.contains("maxLength"))) { - std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char")); - int min_len = schema.contains("minLength") ? schema["minLength"].get<int>() : 0; - int max_len = schema.contains("maxLength") ? schema["maxLength"].get<int>() : std::numeric_limits<int>::max(); - return _add_rule(rule_name, "\"\\\"\" " + build_repetition(char_rule, min_len, max_len) + " \"\\\"\""); - } - if (schema_type == "integer" && (schema.contains("minimum") || schema.contains("exclusiveMinimum") || schema.contains("maximum") || schema.contains("exclusiveMaximum"))) { - int64_t min_value = std::numeric_limits<int64_t>::min(); - int64_t max_value = std::numeric_limits<int64_t>::max(); - if (schema.contains("minimum")) { - min_value = schema["minimum"].get<int64_t>(); - } else if (schema.contains("exclusiveMinimum")) { - min_value = schema["exclusiveMinimum"].get<int64_t>() + 1; + case common_chat_schema::KIND_ARRAY: { + const auto & arr = as<common_chat_schema_array>(schema); + if (arr.items->kind() == common_chat_schema::KIND_ANY && arr.min_items == 0 && arr.max_items < 0) { + return _visit_primitive(rule_name, "array"); + } + std::string item_rule_name = visit(*arr.items, sub_name + "item"); + int max_items = arr.max_items < 0 ? std::numeric_limits<int>::max() : arr.max_items; + return _add_rule(rule_name, "\"[\" space " + build_repetition(item_rule_name, arr.min_items, max_items, "\",\" space") + " space \"]\""); } - if (schema.contains("maximum")) { - max_value = schema["maximum"].get<int64_t>(); - } else if (schema.contains("exclusiveMaximum")) { - max_value = schema["exclusiveMaximum"].get<int64_t>() - 1; + case common_chat_schema::KIND_STRING: { + const auto & str = as<common_chat_schema_string>(schema); + if (!str.pattern.empty()) { + return _visit_pattern(str.pattern, rule_name); + } + if (str.format == common_chat_schema::FORMAT_UUID) { + return _visit_primitive(rule_name, "uuid"); + } + if (str.format != common_chat_schema::FORMAT_NONE) { + std::string prim_name = std::string(str.format == common_chat_schema::FORMAT_DATE ? "date" : str.format == common_chat_schema::FORMAT_TIME ? "time" : "date-time") + "-string"; + return _add_rule(rule_name, _add_primitive(prim_name, STRING_FORMAT_RULES.at(prim_name))); + } + if (str.min_length > 0 || str.max_length >= 0) { + std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char")); + int max_len = str.max_length < 0 ? std::numeric_limits<int>::max() : str.max_length; + return _add_rule(rule_name, "\"\\\"\" " + build_repetition(char_rule, str.min_length, max_len) + " \"\\\"\""); + } + return _visit_primitive(rule_name, "string"); } - std::stringstream out; - out << "("; - build_min_max_int(min_value, max_value, out); - out << ")"; - return _add_rule(rule_name, out.str()); - } - if (schema.empty() || schema_type == "object") { - return _add_rule(rule_name, _add_primitive("object", PRIMITIVE_RULES.at("object"))); - } - if (schema_type.is_null() && schema.is_object()) { - // No type constraint and no recognized structural keywords (e.g. {"description": "..."}). - // Per JSON Schema semantics this is equivalent to {} and accepts any value. - return _add_rule(rule_name, _add_primitive("value", PRIMITIVE_RULES.at("value"))); - } - if (!schema_type.is_string() || PRIMITIVE_RULES.find(schema_type.get<std::string>()) == PRIMITIVE_RULES.end()) { - _errors.push_back("Unrecognized schema: " + schema.dump()); - return ""; + case common_chat_schema::KIND_INTEGER: { + const auto & i = as<common_chat_schema_integer>(schema); + if (i.minimum == std::numeric_limits<int64_t>::min() && i.maximum == std::numeric_limits<int64_t>::max()) { + return _visit_primitive(rule_name, "integer"); + } + std::stringstream out; + out << "("; + build_min_max_int(i.minimum, i.maximum, out); + out << ")"; + return _add_rule(rule_name, out.str()); + } + case common_chat_schema::KIND_NUMBER: + return _visit_primitive(rule_name, "number"); + case common_chat_schema::KIND_BOOLEAN: + return _visit_primitive(rule_name, "boolean"); + case common_chat_schema::KIND_NULL: + return _visit_primitive(rule_name, "null"); + case common_chat_schema::KIND_ANY: + return _add_rule(rule_name, _add_primitive("value", PRIMITIVE_RULES.at("value"))); } - // TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero - return _add_primitive(rule_name == "root" ? "root" : schema_type.get<std::string>(), PRIMITIVE_RULES.at(schema_type.get<std::string>())); + return ""; } void check_errors() { @@ -1106,134 +989,6 @@ class common_schema_converter { } }; -// common_schema_info implementation (pimpl) - -common_schema_info::common_schema_info() - : impl_(std::make_unique<common_schema_converter>( - [](const std::string &) { return json(); }, - false)) {} - -common_schema_info::~common_schema_info() = default; - -common_schema_info::common_schema_info(common_schema_info &&) noexcept = default; -common_schema_info & common_schema_info::operator=(common_schema_info &&) noexcept = default; - -void common_schema_info::resolve_refs(common_json & schema) { - impl_->resolve_refs(schema, ""); -} - -// Determines if a JSON schema can resolve to a string type through any path. -// Some models emit raw string values rather than JSON-encoded strings for string parameters. -// If any branch of the schema (via oneOf, anyOf, $ref, etc.) permits a string, this returns -// true, allowing callers to handle the value as a raw string for simplicity. -bool common_schema_info::resolves_to_string(const common_json & schema) { - std::unordered_set<std::string> visited_refs; - - std::function<bool(const json &)> check = [&](const json & s) -> bool { - if (!s.is_object()) { - return false; - } - - // Handle $ref - if (s.contains("$ref")) { - const std::string & ref = s["$ref"]; - if (visited_refs.find(ref) != visited_refs.end()) { - // Circular reference, assume not a string to be safe - return false; - } - visited_refs.insert(ref); - auto it = impl_->_refs.find(ref); - if (it != impl_->_refs.end()) { - return check(it->second); - } - return false; - } - - // Check type field - if (s.contains("type")) { - const json & schema_type = s["type"]; - if (schema_type.is_string()) { - if (schema_type == "string") { - return true; - } - } else if (schema_type.is_array()) { - // Type can be an array like ["string", "null"] - for (const auto & t : schema_type) { - if (t == "string") { - return true; - } - } - } - } - - // Check oneOf/anyOf - if any alternative can be a string - if (s.contains("oneOf")) { - for (const auto & alt : s["oneOf"]) { - if (check(alt)) { - return true; - } - } - } - if (s.contains("anyOf")) { - for (const auto & alt : s["anyOf"]) { - if (check(alt)) { - return true; - } - } - } - - // Check allOf - all components must be compatible with string type - if (s.contains("allOf")) { - bool all_string = true; - for (const auto & component : s["allOf"]) { - if (!check(component)) { - all_string = false; - break; - } - } - if (all_string) { - return true; - } - } - - // Check const - if the constant value is a string - if (s.contains("const")) { - if (s["const"].is_string()) { - return true; - } - } - - // Check enum - if any enum value is a string - if (s.contains("enum")) { - for (const auto & val : s["enum"]) { - if (val.is_string()) { - return true; - } - } - } - - // String-specific keywords imply string type - if (s.contains("pattern") || s.contains("minLength") || s.contains("maxLength")) { - return true; - } - - // Check format - many formats imply string - if (s.contains("format")) { - const std::string & fmt = s["format"]; - if (fmt == "date" || fmt == "time" || fmt == "date-time" || - fmt == "uri" || fmt == "email" || fmt == "hostname" || - fmt == "ipv4" || fmt == "ipv6" || fmt == "uuid" || - fmt.find("uuid") == 0) { - return true; - } - } - - return false; - }; - - return check(schema); -} - std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf) { #ifdef LLAMA_USE_LLGUIDANCE if (!force_gbnf) { @@ -1242,25 +997,29 @@ std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf) #else (void)force_gbnf; #endif // LLAMA_USE_LLGUIDANCE - return build_grammar([&](const common_grammar_builder & callbacks) { - auto copy = schema; - callbacks.resolve_refs(copy); - callbacks.add_schema("", copy); - }); + try { + return json_schema_to_grammar(common_chat_schema_from_json(schema)); + } catch (const std::runtime_error & e) { + throw std::invalid_argument(std::string("JSON schema conversion failed:\n") + e.what()); + } +} + +std::string json_schema_to_grammar(const common_chat_schema_document & schema) { + common_chat_schema_converter converter(false); + converter.visit(*schema.root, ""); + converter.check_errors(); + return converter.format_grammar(); } std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options) { - common_schema_converter converter([&](const std::string &) { return json(); }, options.dotall); + common_chat_schema_converter converter(options.dotall); common_grammar_builder builder { /* .add_rule = */ [&](const std::string & name, const std::string & rule) { return converter._add_rule(name, rule); }, - /* .add_schema = */ [&](const std::string & name, const common_json & schema) { - return converter.visit(schema, name == "root" ? "" : name); + /* .add_schema = */ [&](const std::string & name, const common_chat_schema & schema) { + return converter.add_schema(name == "root" ? "" : name, schema); }, - /* .resolve_refs = */ [&](common_json & schema) { - converter.resolve_refs(schema, ""); - } }; cb(builder); converter.check_errors(); diff --git a/common/json-schema-to-grammar.h b/common/json-schema-to-grammar.h index 84ed71c76a13..b928c250bbef 100644 --- a/common/json-schema-to-grammar.h +++ b/common/json-schema-to-grammar.h @@ -1,37 +1,17 @@ #pragma once +#include "json-schema.h" #include "json.h" #include <functional> -#include <memory> #include <string> -std::string json_schema_to_grammar(const common_json & schema, - bool force_gbnf = false); - -class common_schema_converter; - -// Probes a JSON schema to extract information about its structure and type constraints. -class common_schema_info { - std::unique_ptr<common_schema_converter> impl_; - - public: - common_schema_info(); - ~common_schema_info(); - - common_schema_info(const common_schema_info &) = delete; - common_schema_info & operator=(const common_schema_info &) = delete; - common_schema_info(common_schema_info &&) noexcept; - common_schema_info & operator=(common_schema_info &&) noexcept; - - void resolve_refs(common_json & schema); - bool resolves_to_string(const common_json & schema); -}; +std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf = false); +std::string json_schema_to_grammar(const common_chat_schema_document & schema); struct common_grammar_builder { - std::function<std::string(const std::string &, const std::string &)> add_rule; - std::function<std::string(const std::string &, const common_json &)> add_schema; - std::function<void(common_json &)> resolve_refs; + std::function<std::string(const std::string &, const std::string &)> add_rule; + std::function<std::string(const std::string &, const common_chat_schema &)> add_schema; }; struct common_grammar_options { diff --git a/common/json-schema.cpp b/common/json-schema.cpp new file mode 100644 index 000000000000..6898840e7d1f --- /dev/null +++ b/common/json-schema.cpp @@ -0,0 +1,514 @@ +#include "json-schema.h" +#include "common.h" + +#include <cmath> +#include <map> +#include <stdexcept> +#include <string> +#include <unordered_set> +#include <utility> +#include <vector> + +class common_chat_schema_builder { + const common_json & root_; + common_chat_schema_document & doc_; + + // the targets built here, moved into doc_ once the whole schema is built + std::map<std::string, common_chat_schema_ptr> refs_; + + // ref nodes get their target once every $ref is built, a cycle would otherwise need it too early + std::vector<common_chat_schema_ref *> pending_; + + [[noreturn]] static void fail(const std::string & path, const std::string & msg) { + throw std::runtime_error("JSON schema error at " + path + ": " + msg); + } + + static int get_count(const common_json & schema, const std::string & key, const std::string & path, int def) { + if (!schema.contains(key)) { + return def; + } + const common_json & value = schema.at(key); + if (!value.is_number_integer() || value.get<int>() < 0) { + fail(path, key + " must be a non-negative integer"); + } + return value.get<int>(); + } + + // a fractional bound is rounded inwards, towards the integers it still admits + static int64_t get_bound(const common_json & schema, const std::string & key, const std::string & path, bool round_up) { + const common_json & value = schema.at(key); + if (value.is_number_integer()) { + return value.get<int64_t>(); + } + if (!value.is_number()) { + fail(path, key + " must be a number"); + } + double d = value.get<double>(); + return (int64_t) (round_up ? std::ceil(d) : std::floor(d)); + } + + static common_chat_schema::string_format get_format(const common_json & schema, const std::string & path) { + if (!schema.contains("format")) { + return common_chat_schema::FORMAT_NONE; + } + const common_json & value = schema.at("format"); + if (!value.is_string()) { + fail(path, "format must be a string"); + } + std::string format = value.get<std::string>(); + if (format == "date") { + return common_chat_schema::FORMAT_DATE; + } + if (format == "time") { + return common_chat_schema::FORMAT_TIME; + } + if (format == "date-time") { + return common_chat_schema::FORMAT_DATE_TIME; + } + if (format == "uuid" || (format.size() == 5 && format.compare(0, 4, "uuid") == 0 && format[4] >= '1' && format[4] <= '5')) { + return common_chat_schema::FORMAT_UUID; + } + return common_chat_schema::FORMAT_NONE; + } + + const common_json & resolve_ref(const std::string & ref, const std::string & path) { + const common_json * target = &root_; + auto tokens = string_split(ref.substr(1), "/"); + for (size_t i = 1; i < tokens.size(); i++) { + const std::string & sel = tokens[i]; + if (target->is_object() && target->contains(sel)) { + target = &target->at(sel); + } else if (target->is_array()) { + size_t idx; + try { + idx = std::stoull(sel); + } catch (const std::logic_error &) { + idx = target->size(); + } + if (idx >= target->size()) { + fail(path, "cannot resolve $ref " + ref + ", " + sel + " is out of range"); + } + target = &target->at(idx); + } else { + fail(path, "cannot resolve $ref " + ref + ", " + sel + " not found"); + } + } + return *target; + } + + common_chat_schema_ptr build_ref(const common_json & value, const std::string & path) { + if (!value.is_string()) { + fail(path, "$ref must be a string"); + } + std::string ref = value.get<std::string>(); + if (ref.compare(0, 2, "#/") != 0) { + fail(path, "unsupported $ref " + ref + ", only references into the same document are supported"); + } + if (refs_.find(ref) == refs_.end()) { + // reserve the key first, so that a cycle back to this $ref stops here + refs_[ref] = nullptr; + refs_[ref] = build_node(resolve_ref(ref, path), ref); + } + auto node = std::make_unique<common_chat_schema_ref>(ref); + pending_.push_back(node.get()); + return node; + } + + template <typename T> + common_chat_schema_ptr build_alternatives(const common_json & alts, const std::string & path) { + if (!alts.is_array()) { + fail(path, "must be an array of schemas"); + } + if (alts.empty()) { + fail(path, "must not be empty"); + } + auto node = std::make_unique<T>(); + size_t i = 0; + for (const auto & alt : alts) { + node->children.push_back(build_node(alt, path + "/" + std::to_string(i++))); + } + return node; + } + + common_chat_schema_ptr build_object(const common_json & schema, const std::string & path) { + auto node = std::make_unique<common_chat_schema_object>(); + + std::unordered_set<std::string> required; + if (schema.contains("required") && schema.at("required").is_array()) { + for (const auto & name : schema.at("required")) { + if (name.is_string()) { + required.insert(name.get<std::string>()); + } + } + } + + if (schema.contains("properties")) { + const common_json & properties = schema.at("properties"); + if (!properties.is_object()) { + fail(path, "properties must be an object"); + } + for (const auto & [name, prop] : properties.items()) { + node->properties.push_back({name, build_node(prop, path + "/properties/" + name), required.count(name) > 0}); + } + } + + if (schema.contains("additionalProperties")) { + const common_json & additional = schema.at("additionalProperties"); + if (additional.is_boolean()) { + if (additional.get<bool>()) { + node->additional_properties = std::make_unique<common_chat_schema_any>(); + } + } else if (additional.is_object()) { + node->additional_properties = build_node(additional, path + "/additionalProperties"); + } else { + fail(path, "additionalProperties must be a boolean or a schema"); + } + } else if (!schema.contains("properties")) { + // {"type": "object"} on its own accepts any object + node->additional_properties = std::make_unique<common_chat_schema_any>(); + } + + return node; + } + + common_chat_schema_ptr build_array(const common_json & schema, const std::string & path) { + auto node = std::make_unique<common_chat_schema_array>(); + if (schema.contains("items") || schema.contains("prefixItems")) { + // "items" wins when both are present; as in the converter, a schema instead of an array is the item schema + const std::string key = schema.contains("items") ? "items" : "prefixItems"; + const common_json & items = schema.at(key); + if (items.is_array()) { + auto tuple = std::make_unique<common_chat_schema_tuple>(); + size_t i = 0; + for (const auto & item : items) { + tuple->items.push_back(build_node(item, path + "/" + key + "/" + std::to_string(i++))); + } + return tuple; + } + node->items = build_node(items, path + "/" + key); + } else { + node->items = std::make_unique<common_chat_schema_any>(); + } + node->min_items = get_count(schema, "minItems", path, 0); + node->max_items = get_count(schema, "maxItems", path, -1); + return node; + } + + common_chat_schema_ptr build_string(const common_json & schema, const std::string & path) { + auto node = std::make_unique<common_chat_schema_string>(); + if (schema.contains("pattern")) { + const common_json & pattern = schema.at("pattern"); + if (!pattern.is_string()) { + fail(path, "pattern must be a string"); + } + node->pattern = pattern.get<std::string>(); + } + node->format = get_format(schema, path); + node->min_length = get_count(schema, "minLength", path, 0); + node->max_length = get_count(schema, "maxLength", path, -1); + return node; + } + + common_chat_schema_ptr build_integer(const common_json & schema, const std::string & path) { + auto node = std::make_unique<common_chat_schema_integer>(); + if (schema.contains("minimum")) { + node->minimum = get_bound(schema, "minimum", path, /* round_up */ true); + } else if (schema.contains("exclusiveMinimum")) { + node->minimum = get_bound(schema, "exclusiveMinimum", path, /* round_up */ false) + 1; + } + if (schema.contains("maximum")) { + node->maximum = get_bound(schema, "maximum", path, /* round_up */ false); + } else if (schema.contains("exclusiveMaximum")) { + node->maximum = get_bound(schema, "exclusiveMaximum", path, /* round_up */ true) - 1; + } + return node; + } + + common_chat_schema_ptr build_node(const common_json & schema, const std::string & path) { + if (!schema.is_object()) { + fail(path, "schema must be an object"); + } + if (schema.contains("$ref")) { + return build_ref(schema.at("$ref"), path); + } + if (schema.contains("oneOf") || schema.contains("anyOf")) { + const std::string key = schema.contains("oneOf") ? "oneOf" : "anyOf"; + return build_alternatives<common_chat_schema_any_of>(schema.at(key), path + "/" + key); + } + + common_json type; + if (schema.contains("type")) { + type = schema.at("type"); + } + if (type.is_array()) { + // {"type": ["a", "b"], ...} is {"anyOf": [{"type": "a", ...}, {"type": "b", ...}]} + if (type.empty()) { + fail(path, "type must not be empty"); + } + auto node = std::make_unique<common_chat_schema_any_of>(); + size_t i = 0; + for (const auto & t : type) { + common_json alt = schema; + alt["type"] = t; + node->children.push_back(build_node(alt, path + "/type/" + std::to_string(i++))); + } + return node; + } + if (schema.contains("const")) { + return std::make_unique<common_chat_schema_const>(schema.at("const")); + } + if (schema.contains("enum")) { + const common_json & values = schema.at("enum"); + if (!values.is_array() || values.empty()) { + fail(path, "enum must be a non-empty array"); + } + auto node = std::make_unique<common_chat_schema_enum>(); + for (const auto & value : values) { + node->values.push_back(value); + } + return node; + } + if (!type.is_null() && !type.is_string()) { + fail(path, "type must be a string or an array of strings"); + } + + const std::string type_name = type.is_string() ? type.get<std::string>() : ""; + const bool has_properties = schema.contains("properties") || + (schema.contains("additionalProperties") && schema.at("additionalProperties") != true); + + if (type_name.empty()) { + // without a type the structural keywords decide, in the same order as the converter + if (has_properties) { + return build_object(schema, path); + } + if (schema.contains("allOf")) { + return build_alternatives<common_chat_schema_all_of>(schema.at("allOf"), path + "/allOf"); + } + if (schema.contains("items") || schema.contains("prefixItems")) { + return build_array(schema, path); + } + if (schema.contains("pattern") || schema.contains("minLength") || schema.contains("maxLength") || get_format(schema, path) != common_chat_schema::FORMAT_NONE) { + return build_string(schema, path); + } + return std::make_unique<common_chat_schema_any>(); + } + if (type_name == "object") { + if (!has_properties && schema.contains("allOf")) { + return build_alternatives<common_chat_schema_all_of>(schema.at("allOf"), path + "/allOf"); + } + return build_object(schema, path); + } + if (type_name == "string") { + if (schema.contains("allOf")) { + return build_alternatives<common_chat_schema_all_of>(schema.at("allOf"), path + "/allOf"); + } + return build_string(schema, path); + } + if (type_name == "array") { + return build_array(schema, path); + } + if (type_name == "integer") { + return build_integer(schema, path); + } + if (type_name == "number") { + return std::make_unique<common_chat_schema_number>(); + } + if (type_name == "boolean") { + return std::make_unique<common_chat_schema_boolean>(); + } + if (type_name == "null") { + return std::make_unique<common_chat_schema_null>(); + } + fail(path, "unrecognized type " + type_name); + } + + public: + common_chat_schema_builder(const common_json & root, common_chat_schema_document & doc) : root_(root), doc_(doc) {} + + common_chat_schema_ptr build() { + auto node = build_node(root_, "#"); + for (auto & entry : refs_) { + doc_.refs[entry.first] = std::move(entry.second); + } + for (auto * ref : pending_) { + ref->target = doc_.refs.at(ref->ref).get(); + } + return node; + } +}; + +common_chat_schema_document common_chat_schema_from_json(const common_json & schema) { + common_chat_schema_document doc; + doc.root = common_chat_schema_builder(schema, doc).build(); + return doc; +} + +static common_chat_schema::value_type json_type(const common_json & value) { + if (value.is_null()) { + return common_chat_schema::TYPE_NULL; + } + if (value.is_boolean()) { + return common_chat_schema::TYPE_BOOLEAN; + } + if (value.is_number_integer()) { + return common_chat_schema::TYPE_INTEGER; + } + if (value.is_number()) { + return common_chat_schema::TYPE_NUMBER; + } + if (value.is_string()) { + return common_chat_schema::TYPE_STRING; + } + if (value.is_array()) { + return common_chat_schema::TYPE_ARRAY; + } + return common_chat_schema::TYPE_OBJECT; +} + +static common_chat_schema::type_set value_types_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited) { + switch (s.kind()) { + case common_chat_schema::KIND_ANY: + return common_chat_schema::type_set::all(); + case common_chat_schema::KIND_NULL: + return { common_chat_schema::TYPE_NULL }; + case common_chat_schema::KIND_BOOLEAN: + return { common_chat_schema::TYPE_BOOLEAN }; + case common_chat_schema::KIND_NUMBER: + return { common_chat_schema::TYPE_NUMBER, common_chat_schema::TYPE_INTEGER }; + case common_chat_schema::KIND_INTEGER: + return { common_chat_schema::TYPE_INTEGER }; + case common_chat_schema::KIND_STRING: + return { common_chat_schema::TYPE_STRING }; + case common_chat_schema::KIND_ARRAY: + case common_chat_schema::KIND_TUPLE: + return { common_chat_schema::TYPE_ARRAY }; + case common_chat_schema::KIND_OBJECT: + return { common_chat_schema::TYPE_OBJECT }; + case common_chat_schema::KIND_CONST: + return { json_type(static_cast<const common_chat_schema_const &>(s).value) }; + case common_chat_schema::KIND_ENUM: { + common_chat_schema::type_set types; + for (const auto & value : static_cast<const common_chat_schema_enum &>(s).values) { + types.add(json_type(value)); + } + return types; + } + case common_chat_schema::KIND_REF: { + const auto * target = static_cast<const common_chat_schema_ref &>(s).target; + if (!target || !visited.insert(target).second) { + // a cycle contributes no type, to be safe + return {}; + } + auto types = value_types_impl(*target, visited); + visited.erase(target); + return types; + } + case common_chat_schema::KIND_ANY_OF: { + common_chat_schema::type_set types; + for (const auto & child : static_cast<const common_chat_schema_any_of &>(s).children) { + types |= value_types_impl(*child, visited); + } + return types; + } + case common_chat_schema::KIND_ALL_OF: { + auto types = common_chat_schema::type_set::all(); + for (const auto & child : static_cast<const common_chat_schema_all_of &>(s).children) { + types &= value_types_impl(*child, visited); + } + return types; + } + } + return {}; +} + +common_chat_schema::type_set common_chat_schema::value_types() const { + std::unordered_set<const common_chat_schema *> visited; + return value_types_impl(*this, visited); +} + +static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited) { + switch (s.kind()) { + case common_chat_schema::KIND_STRING: + return true; + case common_chat_schema::KIND_CONST: + return static_cast<const common_chat_schema_const &>(s).value.is_string(); + case common_chat_schema::KIND_ENUM: + for (const auto & v : static_cast<const common_chat_schema_enum &>(s).values) { + if (v.is_string()) { + return true; + } + } + return false; + case common_chat_schema::KIND_REF: { + // a cycle is taken as not a string, to be safe + const auto * target = static_cast<const common_chat_schema_ref &>(s).target; + if (!target || !visited.insert(target).second) { + return false; + } + bool result = may_be_string_impl(*target, visited); + visited.erase(target); + return result; + } + case common_chat_schema::KIND_ANY_OF: + for (const auto & child : static_cast<const common_chat_schema_any_of &>(s).children) { + if (may_be_string_impl(*child, visited)) { + return true; + } + } + return false; + case common_chat_schema::KIND_ALL_OF: { + // every child must allow a string, an any child constrains nothing + bool any_string = false; + for (const auto & child : static_cast<const common_chat_schema_all_of &>(s).children) { + if (child->kind() == common_chat_schema::KIND_ANY) { + continue; + } + if (!may_be_string_impl(*child, visited)) { + return false; + } + any_string = true; + } + return any_string; + } + default: + return false; + } +} + +bool common_chat_schema::may_be_string() const { + std::unordered_set<const common_chat_schema *> visited; + return may_be_string_impl(*this, visited); +} + +const char * common_chat_schema::kind_name(node_kind kind) { + switch (kind) { + case KIND_ANY: return "any"; + case KIND_REF: return "ref"; + case KIND_ANY_OF: return "anyOf"; + case KIND_ALL_OF: return "allOf"; + case KIND_CONST: return "const"; + case KIND_ENUM: return "enum"; + case KIND_NULL: return "null"; + case KIND_BOOLEAN: return "boolean"; + case KIND_NUMBER: return "number"; + case KIND_INTEGER: return "integer"; + case KIND_STRING: return "string"; + case KIND_ARRAY: return "array"; + case KIND_TUPLE: return "tuple"; + case KIND_OBJECT: return "object"; + } + return "?"; +} + +const char * common_chat_schema::type_name(value_type type) { + switch (type) { + case TYPE_NULL: return "null"; + case TYPE_BOOLEAN: return "boolean"; + case TYPE_NUMBER: return "number"; + case TYPE_INTEGER: return "integer"; + case TYPE_STRING: return "string"; + case TYPE_ARRAY: return "array"; + case TYPE_OBJECT: return "object"; + } + return "?"; +} diff --git a/common/json-schema.h b/common/json-schema.h new file mode 100644 index 000000000000..084208c96201 --- /dev/null +++ b/common/json-schema.h @@ -0,0 +1,198 @@ +#pragma once + +#include "json.h" + +#include <cstdint> +#include <initializer_list> +#include <map> +#include <memory> +#include <string> +#include <vector> + +// JSON schema, covering the subset that json_schema_to_grammar() can convert. + +struct common_chat_schema { + enum node_kind { + KIND_ANY, + KIND_REF, + KIND_ANY_OF, + KIND_ALL_OF, + KIND_CONST, + KIND_ENUM, + KIND_NULL, + KIND_BOOLEAN, + KIND_NUMBER, + KIND_INTEGER, + KIND_STRING, + KIND_ARRAY, + KIND_TUPLE, + KIND_OBJECT, + }; + + enum value_type { + TYPE_NULL, + TYPE_BOOLEAN, + TYPE_NUMBER, + TYPE_INTEGER, + TYPE_STRING, + TYPE_ARRAY, + TYPE_OBJECT, + }; + + enum string_format { + FORMAT_NONE, + FORMAT_UUID, // uuid, uuid1 .. uuid5 + FORMAT_DATE, + FORMAT_TIME, + FORMAT_DATE_TIME, + }; + + class type_set { + uint32_t mask_ = 0; + + public: + type_set() = default; + type_set(std::initializer_list<value_type> types) { + for (auto type : types) { + add(type); + } + } + + static type_set all() { + return { TYPE_NULL, TYPE_BOOLEAN, TYPE_NUMBER, TYPE_INTEGER, TYPE_STRING, TYPE_ARRAY, TYPE_OBJECT }; + } + + void add(value_type type) { mask_ |= 1u << type; } + + bool has(value_type type) const { return (mask_ & (1u << type)) != 0; } + bool is_only(value_type type) const { return mask_ == (1u << type); } + bool empty() const { return mask_ == 0; } + + type_set & operator|=(const type_set & other) { mask_ |= other.mask_; return *this; } + type_set & operator&=(const type_set & other) { mask_ &= other.mask_; return *this; } + + bool operator==(const type_set & other) const { return mask_ == other.mask_; } + bool operator!=(const type_set & other) const { return mask_ != other.mask_; } + }; + + virtual ~common_chat_schema() = default; + virtual node_kind kind() const = 0; + + type_set value_types() const; + + // Whether a value matching the schema may be a string, through any branch of it. + bool may_be_string() const; + + static const char * kind_name(node_kind kind); + static const char * type_name(value_type type); +}; + +using common_chat_schema_ptr = std::unique_ptr<common_chat_schema>; + +struct common_chat_schema_any : common_chat_schema { + node_kind kind() const override { return KIND_ANY; } +}; + +// {"$ref": "#/..."}, only references into the same document are supported +struct common_chat_schema_ref : common_chat_schema { + std::string ref; + const common_chat_schema * target = nullptr; // owned by common_chat_schema_document::refs + + explicit common_chat_schema_ref(std::string ref) : ref(std::move(ref)) {} + + node_kind kind() const override { return KIND_REF; } +}; + +// oneOf / anyOf, or a "type" array expanded to one alternative per type +struct common_chat_schema_any_of : common_chat_schema { + std::vector<common_chat_schema_ptr> children; + + node_kind kind() const override { return KIND_ANY_OF; } +}; + +struct common_chat_schema_all_of : common_chat_schema { + std::vector<common_chat_schema_ptr> children; + + node_kind kind() const override { return KIND_ALL_OF; } +}; + +struct common_chat_schema_const : common_chat_schema { + common_json value; + + explicit common_chat_schema_const(common_json value) : value(std::move(value)) {} + + node_kind kind() const override { return KIND_CONST; } +}; + +struct common_chat_schema_enum : common_chat_schema { + std::vector<common_json> values; + + node_kind kind() const override { return KIND_ENUM; } +}; + +struct common_chat_schema_null : common_chat_schema { + node_kind kind() const override { return KIND_NULL; } +}; + +struct common_chat_schema_boolean : common_chat_schema { + node_kind kind() const override { return KIND_BOOLEAN; } +}; + +struct common_chat_schema_number : common_chat_schema { + node_kind kind() const override { return KIND_NUMBER; } +}; + +// bounds are inclusive, exclusiveMinimum / exclusiveMaximum are folded in +struct common_chat_schema_integer : common_chat_schema { + int64_t minimum = INT64_MIN; // INT64_MIN for unbounded + int64_t maximum = INT64_MAX; // INT64_MAX for unbounded + + node_kind kind() const override { return KIND_INTEGER; } +}; + +struct common_chat_schema_string : common_chat_schema { + std::string pattern; // empty when absent + string_format format = FORMAT_NONE; + int min_length = 0; + int max_length = -1; // -1 for unbounded + + node_kind kind() const override { return KIND_STRING; } +}; + +struct common_chat_schema_array : common_chat_schema { + common_chat_schema_ptr items; // a common_chat_schema_any when "items" is absent + int min_items = 0; + int max_items = -1; // -1 for unbounded + + node_kind kind() const override { return KIND_ARRAY; } +}; + +struct common_chat_schema_tuple : common_chat_schema { + std::vector<common_chat_schema_ptr> items; + + node_kind kind() const override { return KIND_TUPLE; } +}; + +struct common_chat_schema_property { + std::string name; + common_chat_schema_ptr schema; + bool required = false; +}; + +struct common_chat_schema_object : common_chat_schema { + std::vector<common_chat_schema_property> properties; // in schema order + common_chat_schema_ptr additional_properties; // null when not allowed + + node_kind kind() const override { return KIND_OBJECT; } +}; + +struct common_chat_schema_document { + common_chat_schema_ptr root; + std::map<std::string, common_chat_schema_ptr> refs; +}; + +// A document shared by the PEG parsers built from its nodes, which it keeps alive +using common_chat_schema_document_ptr = std::shared_ptr<const common_chat_schema_document>; + +// Throws std::runtime_error when the schema falls outside the supported subset. +common_chat_schema_document common_chat_schema_from_json(const common_json & schema); diff --git a/common/log.cpp b/common/log.cpp index 42951190c082..0a9a4eb9ea49 100644 --- a/common/log.cpp +++ b/common/log.cpp @@ -37,6 +37,16 @@ void common_log_set_verbosity_thold(int verbosity) { common_log_verbosity_thold = verbosity; } +static bool common_log_jsonl = false; + +bool common_log_get_jsonl(void) { + return common_log_jsonl; +} + +void common_log_set_jsonl(bool jsonl) { + common_log_jsonl = jsonl; +} + static int64_t t_us() { return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count(); } @@ -87,6 +97,7 @@ struct common_log_entry { bool is_end { false }; // signals the worker thread to stop bool prefix { false }; bool jsonl { false }; + bool is_json { false }; // msg already holds a serialized JSON object common_log_entry(size_t size = 256) : msg(size) { } @@ -107,6 +118,12 @@ struct common_log_entry { } if (jsonl) { + if (is_json) { + fprintf(fcur, "%s\n", msg.data()); + fflush(fcur); + return; + } + common_json obj = { {"type", "log"}, {"time", timestamp}, @@ -156,7 +173,6 @@ struct common_log { file = nullptr; prefix = false; timestamps = false; - jsonl = false; running = false; t_start = t_us(); @@ -184,7 +200,6 @@ struct common_log { bool prefix; bool timestamps; - bool jsonl; bool running; int64_t t_start; @@ -273,7 +288,8 @@ struct common_log { entry.is_end = false; entry.level = level; entry.prefix = prefix; - entry.jsonl = jsonl; + entry.jsonl = common_log_jsonl; + entry.is_json = false; entry.timestamp = 0; if (timestamps) { entry.timestamp = t_us() - t_start; @@ -283,6 +299,42 @@ struct common_log { cv_new.notify_one(); } + void add_json(const char * type, const common_json & obj) { + const common_json full = { + {"type", type}, + {"data", obj}, + }; + + const std::string text = full.dump_safe(); + + std::unique_lock<std::mutex> lock(mtx); + + // block if the queue is full + cv_full.wait(lock, [this]() { return !running || !is_full(); }); + + if (!running) { + // discard messages while the worker thread is paused + return; + } + + auto & entry = queue[tail]; + + if (entry.msg.size() < text.size() + 1) { + entry.msg.resize(text.size() + 1); + } + memcpy(entry.msg.data(), text.c_str(), text.size() + 1); + + entry.is_end = false; + entry.level = GGML_LOG_LEVEL_NONE; + entry.prefix = false; + entry.jsonl = true; + entry.is_json = true; + entry.timestamp = 0; + + tail = (tail + 1) % queue.size(); + cv_new.notify_one(); + } + void resume() { std::lock_guard<std::mutex> lock(mtx); @@ -388,12 +440,6 @@ struct common_log { this->timestamps = timestamps; } - - void set_jsonl(bool jsonl) { - std::lock_guard<std::mutex> lock(mtx); - - this->jsonl = jsonl; - } }; // @@ -440,6 +486,14 @@ void common_log_add(struct common_log * log, enum ggml_log_level level, const ch va_end(args); } +void common_log_add_json(struct common_log * log, const char * type, const common_json & obj) { + if (!common_log_jsonl) { + return; + } + + log->add_json(type, obj); +} + void common_log_set_file(struct common_log * log, const char * file) { log->set_file(file); } @@ -467,10 +521,6 @@ void common_log_set_timestamps(struct common_log * log, bool timestamps) { log->set_timestamps(timestamps); } -void common_log_set_jsonl(struct common_log * log, bool jsonl) { - log->set_jsonl(jsonl); -} - void common_log_flush(struct common_log * log) { log->pause(); log->resume(); diff --git a/common/log.h b/common/log.h index 37f4de92b212..e36b09463e60 100644 --- a/common/log.h +++ b/common/log.h @@ -43,6 +43,10 @@ int common_log_get_verbosity_thold(void); void common_log_set_verbosity_thold(int verbosity); // not thread-safe +bool common_log_get_jsonl(void); + +void common_log_set_jsonl(bool jsonl); // not thread-safe + int common_log_get_verbosity(enum ggml_log_level level); void common_log_default_callback(enum ggml_log_level level, const char * text, void * user_data); @@ -91,7 +95,6 @@ void common_log_set_file (struct common_log * log, const char * file); // n void common_log_set_colors (struct common_log * log, log_colors colors); // not thread-safe void common_log_set_prefix (struct common_log * log, bool prefix); // whether to output prefix to each log void common_log_set_timestamps(struct common_log * log, bool timestamps); // whether to output timestamps in the prefix -void common_log_set_jsonl (struct common_log * log, bool jsonl); // print each log as a JSON object on one line, not thread-safe void common_log_flush (struct common_log * log); // flush all pending log messages // helper macros for logging @@ -127,3 +130,17 @@ void common_log_flush (struct common_log * log); // f #define LOG_WRNV(verbosity, ...) LOG_TMPL(GGML_LOG_LEVEL_WARN, verbosity, __VA_ARGS__) #define LOG_ERRV(verbosity, ...) LOG_TMPL(GGML_LOG_LEVEL_ERROR, verbosity, __VA_ARGS__) #define LOG_CNTV(verbosity, ...) LOG_TMPL(GGML_LOG_LEVEL_CONT, verbosity, __VA_ARGS__) + +class common_json; // defined in common/json.h + +// helper allows different types of json output +// no-op if --log-jsonl is not set +void common_log_add_json(struct common_log * log, const char * type, const common_json & data); + +// will only print if --log-jsonl is set +#define LOG_JSON(type, data) \ + do { \ + if (common_log_get_jsonl()) { \ + common_log_add_json(common_log_main(), type, data); \ + } \ + } while (0) diff --git a/common/parsers/cohere2moe.cpp b/common/parsers/cohere2moe.cpp index 46a2a01baae1..59595368dc3e 100644 --- a/common/parsers/cohere2moe.cpp +++ b/common/parsers/cohere2moe.cpp @@ -129,15 +129,6 @@ common_chat_params common_chat_params_init_cohere2moe(const common_chat_template if (include_grammar) { data.grammar_lazy = !has_response_format && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/deepseek.cpp b/common/parsers/deepseek.cpp index 5e2581727204..640fa9e1560d 100644 --- a/common/parsers/deepseek.cpp +++ b/common/parsers/deepseek.cpp @@ -149,39 +149,28 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); - auto params = function.contains("parameters") ? function.at("parameters") : json::object(); - const auto & props = params.contains("properties") ? params.at("properties") : json::object(); - - std::set<std::string> required; - if (params.contains("required")) { - required = params.at("required").get<std::set<std::string>>(); - } - - auto schema_info = common_schema_info(); - schema_info.resolve_refs(params); std::vector<common_peg_parser> required_parsers; std::vector<common_peg_parser> optional_parsers; - for (const auto & [param_name, param_schema] : props.items()) { - bool is_required = required.find(param_name) != required.end(); - bool is_string = schema_info.resolves_to_string(param_schema); + foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { + bool is_string = param.schema->may_be_string(); auto arg = p.tool_arg( - p.tool_arg_open(p.literal(PARAM_START + " name=\"") + p.tool_arg_name(p.literal(param_name)) + + p.tool_arg_open(p.literal(PARAM_START + " name=\"") + p.tool_arg_name(p.literal(param.name)) + p.literal("\" string=\"" + std::string(is_string ? "true" : "false") + "\">")) + (is_string ? p.tool_arg_string_value(p.until(PARAM_END)) : - p.tool_arg_json_value(p.schema(p.json(), "tool-" + name + "-arg-" + param_name + "-schema", - param_schema, false))) + + p.tool_arg_json_value(p.schema(p.json(), "tool-" + name + "-arg-" + param.name + "-schema", + doc, *param.schema))) + p.tool_arg_close(p.literal(PARAM_END))); - auto named_arg = p.rule("tool-" + name + "-arg-" + param_name, arg); - if (is_required) { + auto named_arg = p.rule("tool-" + name + "-arg-" + param.name, arg); + if (param.required) { required_parsers.push_back(named_arg); } else { optional_parsers.push_back(named_arg); } - } + }); common_peg_parser args_seq = p.eps(); for (size_t i = 0; i < required_parsers.size(); i++) { @@ -266,15 +255,6 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ if (include_grammar) { data.grammar_lazy = has_tools && !require_tools; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.contains("parameters") ? function.at("parameters") : json::object(); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/functionary-v3-2.cpp b/common/parsers/functionary-v3-2.cpp index 349b8065ac1a..9d47f0a3328f 100644 --- a/common/parsers/functionary-v3-2.cpp +++ b/common/parsers/functionary-v3-2.cpp @@ -45,7 +45,7 @@ common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_te foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); - const auto & schema = function.at("parameters"); + const auto schema = common_chat_tool_parameters(function); // Tool format: >>>function_name\n{json_args} auto tool_parser = p.tool( @@ -82,11 +82,6 @@ common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_te data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - }); parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/gemma4.cpp b/common/parsers/gemma4.cpp index 041523acb8d2..ad48226e611b 100644 --- a/common/parsers/gemma4.cpp +++ b/common/parsers/gemma4.cpp @@ -291,15 +291,6 @@ common_chat_params common_chat_params_init_gemma4(const common_chat_template & if (include_grammar) { data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED)); data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/gigachat-v3.cpp b/common/parsers/gigachat-v3.cpp index 41da5554acbc..48abcb3f33c1 100644 --- a/common/parsers/gigachat-v3.cpp +++ b/common/parsers/gigachat-v3.cpp @@ -33,7 +33,7 @@ common_chat_params common_chat_params_init_gigachat_v3( for (const auto & tool : inputs.tools) { const auto & function = tool.at("function"); std::string name = function.at("name"); - const auto & schema = function.at("parameters"); + const auto schema = common_chat_tool_parameters(function); auto tool_name = p.json_member("name", "\"" + p.tool_name(p.literal(name)) + "\""); auto tool_args = p.json_member("arguments", p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema))); @@ -65,11 +65,6 @@ common_chat_params common_chat_params_init_gigachat_v3( data.grammar_lazy = has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - }); parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/gpt-oss.cpp b/common/parsers/gpt-oss.cpp index d7dbfbfb57b0..00beb41a47ee 100644 --- a/common/parsers/gpt-oss.cpp +++ b/common/parsers/gpt-oss.cpp @@ -109,7 +109,7 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template & foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); - const auto & params = function.at("parameters"); + const auto params = common_chat_tool_parameters(function); auto func_name = p.literal(" to=functions.") + p.tool_name(p.literal(name)); auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type); @@ -143,15 +143,6 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template & if (include_grammar) { data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED)); data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/kimi-k2.cpp b/common/parsers/kimi-k2.cpp index 57f6bfdcb60d..5ee9121abab2 100644 --- a/common/parsers/kimi-k2.cpp +++ b/common/parsers/kimi-k2.cpp @@ -82,7 +82,7 @@ common_chat_params common_chat_params_init_kimi_k2(const common_chat_template & foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); - const auto & schema = function.at("parameters"); + const auto schema = common_chat_tool_parameters(function); // Match: functions.<name>:<digits> // Capture the full call id (functions.<name>:<digits>) using tool_id tag @@ -116,11 +116,6 @@ common_chat_params common_chat_params_init_kimi_k2(const common_chat_template & if (include_grammar) { data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - }); parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/kimi-k3.cpp b/common/parsers/kimi-k3.cpp index 56a49903f701..989e39f956f0 100644 --- a/common/parsers/kimi-k3.cpp +++ b/common/parsers/kimi-k3.cpp @@ -98,7 +98,7 @@ common_chat_params common_chat_params_init_kimi_k3(const common_chat_template & foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); - const json schema = function.contains("parameters") ? function.at("parameters") : json::object(); + const json schema = common_chat_tool_parameters(function); // arguments come one tag per key, with the JSON type in a type="..." // attribute. the type is taken from the tool schema instead, as it tells @@ -155,13 +155,6 @@ common_chat_params common_chat_params_init_kimi_k3(const common_chat_template & if (include_grammar) { data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - if (function.contains("parameters")) { - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - } - }); parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/lfm2.cpp b/common/parsers/lfm2.cpp index 4514f908b956..280788509eca 100644 --- a/common/parsers/lfm2.cpp +++ b/common/parsers/lfm2.cpp @@ -98,15 +98,6 @@ common_chat_params common_chat_params_init_lfm2(const common_chat_template & if (include_grammar) { data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED)); data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/minicpm5.cpp b/common/parsers/minicpm5.cpp index e6e0abf066c1..4d18d3d9600e 100644 --- a/common/parsers/minicpm5.cpp +++ b/common/parsers/minicpm5.cpp @@ -71,32 +71,27 @@ common_chat_params common_chat_params_init_minicpm5(const common_chat_template & foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); const std::string name = function.at("name"); - auto params = function.contains("parameters") ? function.at("parameters") : json::object(); - auto args = p.eps(); - if (params.contains("properties") && params.at("properties").is_object() && !params.at("properties").empty()) { - auto schema_info = common_schema_info(); - schema_info.resolve_refs(params); - - auto arg_choice = p.choice(); - for (const auto & [prop_name, prop_schema] : params.at("properties").items()) { - auto value_parser = p.eps(); - if (schema_info.resolves_to_string(prop_schema)) { - value_parser = string_value; - } else { - value_parser = p.tool_arg_json_value( - p.schema(p.json(), "tool-" + name + "-arg-" + prop_name + "-schema", prop_schema, false) - ) + p.tool_arg_close(p.literal("</param>")); - } - - auto arg_rule = p.tool_arg( - p.tool_arg_open(p.literal("<param name=\"") + p.tool_arg_name(p.literal(prop_name)) + p.literal("\">")) + - value_parser - ); - - arg_choice |= arg_rule; + std::vector<common_peg_parser> arg_rules; + foreach_parameter(function, [&](const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) { + auto value_parser = p.eps(); + if (prop.schema->may_be_string()) { + value_parser = string_value; + } else { + value_parser = p.tool_arg_json_value( + p.schema(p.json(), "tool-" + name + "-arg-" + prop.name + "-schema", doc, *prop.schema) + ) + p.tool_arg_close(p.literal("</param>")); } - args = p.zero_or_more(arg_choice + p.space()); + + arg_rules.push_back(p.tool_arg( + p.tool_arg_open(p.literal("<param name=\"") + p.tool_arg_name(p.literal(prop.name)) + p.literal("\">")) + + value_parser + )); + }); + + auto args = p.eps(); + if (!arg_rules.empty()) { + args = p.zero_or_more(p.choice(arg_rules) + p.space()); } auto tool_parser = p.tool( @@ -123,15 +118,6 @@ common_chat_params common_chat_params_init_minicpm5(const common_chat_template & if (include_grammar) { data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED)); data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.contains("parameters") ? function.at("parameters") : json::object(); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/minimax-m3.cpp b/common/parsers/minimax-m3.cpp index ff23ea153c40..7ea9bfe5a083 100644 --- a/common/parsers/minimax-m3.cpp +++ b/common/parsers/minimax-m3.cpp @@ -84,29 +84,18 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template return generation_prompt + reasoning + p.content(p.rest()) + end; } - auto alternatives_of = [](const json & schema) -> std::optional<json> { - for (const auto * keyword : { "oneOf", "anyOf" }) { - if (schema.contains(keyword) && schema.at(keyword).is_array() && !schema.at(keyword).empty()) { - return schema.at(keyword); - } - } - return std::nullopt; - }; - auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); - auto params = function.contains("parameters") ? function.at("parameters") : json::object(); - - auto schema_info = common_schema_info(); - schema_info.resolve_refs(params); + auto params = common_chat_tool_parameters(function); + auto doc = std::make_shared<const common_chat_schema_document>(common_chat_schema_from_json(params)); // The template expands argument values recursively in XML (see the to_xml() macro) - std::function<common_peg_parser(const json &, const std::string &, const std::string &)> value_of; - std::function<common_peg_parser(const json &, const std::string &)> members_of; + std::function<common_peg_parser(const common_chat_schema &, const std::string &, const std::string &)> value_of; + std::function<common_peg_parser(const common_chat_schema_object &, const std::string &)> members_of; - auto element_of = [&](const std::string & tag, const json & schema, const std::string & rule_name) { + auto element_of = [&](const std::string & tag, const common_chat_schema & schema, const std::string & rule_name) { const std::string close = NS + "</" + tag + ">"; return p.rule(rule_name, p.tool_arg( @@ -117,69 +106,57 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template value_of(schema, rule_name, close))); }; - value_of = [&](const json & schema, + value_of = [&](const common_chat_schema & schema, const std::string & rule_name, const std::string & close) -> common_peg_parser { auto close_tag = p.tool_arg_close(p.literal(close)); // A string accepts anything, so a union with a string alternative is a string - if (schema_info.resolves_to_string(schema)) { + if (schema.may_be_string()) { return p.ac(p.tool_arg_string_value(p.until(close)) + close_tag, close); } - if (auto alternatives = alternatives_of(schema)) { + if (schema.kind() == common_chat_schema::KIND_ANY_OF) { std::vector<common_peg_parser> choices; size_t index = 0; - for (const auto & alternative : *alternatives) { + for (const auto & alternative : static_cast<const common_chat_schema_any_of &>(schema).children) { const std::string alt_name = rule_name + "-" + std::to_string(index++); // There is a risk that this breaks streaming deltas, but that's a risk we // assume to provide tool arg streaming. - choices.push_back(value_of(alternative, alt_name, close)); + choices.push_back(value_of(*alternative, alt_name, close)); } return p.choice(choices); } - const std::string type = schema.contains("type") && schema.at("type").is_string() - ? schema.at("type").get<std::string>() - : ""; - - if (type == "object" && schema.contains("properties")) { - return p.tag(mm3::TOOL_ARG_OBJECT, members_of(schema, rule_name)) + p.space() + close_tag; + if (schema.kind() == common_chat_schema::KIND_OBJECT) { + const auto & object = static_cast<const common_chat_schema_object &>(schema); + if (!object.properties.empty()) { + return p.tag(mm3::TOOL_ARG_OBJECT, members_of(object, rule_name)) + p.space() + close_tag; + } } - if (type == "array" && schema.contains("items")) { + if (schema.kind() == common_chat_schema::KIND_ARRAY) { const std::string item_close = NS + "</item>"; auto item = p.rule(rule_name + "-item", p.tag(mm3::TOOL_ARG_ITEM, p.literal(NS + "<item>") + - value_of(schema.at("items"), rule_name + "-item", item_close))); + value_of(*static_cast<const common_chat_schema_array &>(schema).items, rule_name + "-item", item_close))); return p.tag(mm3::TOOL_ARG_ARRAY, p.repeat(p.space() + item, 0, -1)) + p.space() + close_tag; } - return p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", schema, false)) + close_tag; + return p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", doc, schema)) + close_tag; }; // Required properties in schema order, then any number of optional ones in any order. - members_of = [&](const json & schema, const std::string & rule_prefix) -> common_peg_parser { - const auto & props = schema.at("properties"); - - std::set<std::string> required; - if (schema.contains("required")) { - required = schema.at("required").get<std::set<std::string>>(); - } - + members_of = [&](const common_chat_schema_object & object, const std::string & rule_prefix) -> common_peg_parser { std::vector<common_peg_parser> required_elements; std::vector<common_peg_parser> optional_elements; - for (const auto & [key, key_schema] : props.items()) { - auto element = element_of(key, key_schema, rule_prefix + "-" + key); - if (required.find(key) != required.end()) { - required_elements.push_back(element); - } else { - optional_elements.push_back(element); - } + for (const auto & prop : object.properties) { + auto element = element_of(prop.name, *prop.schema, rule_prefix + "-" + prop.name); + (prop.required ? required_elements : optional_elements).push_back(element); } common_peg_parser members = p.eps(); @@ -201,8 +178,10 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template return members; }; - common_peg_parser invoke_body = - params.contains("properties") ? members_of(params, "tool-" + name + "-arg") : p.eps(); + common_peg_parser invoke_body = p.eps(); + if (doc->root->kind() == common_chat_schema::KIND_OBJECT) { + invoke_body = members_of(static_cast<const common_chat_schema_object &>(*doc->root), "tool-" + name + "-arg"); + } auto func_parser = p.tool( p.tool_open(p.literal(NS + "<invoke name=\"") + @@ -238,15 +217,6 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template if (include_grammar) { data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED)); data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.contains("parameters") ? function.at("parameters") : json::object(); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/ministral3.cpp b/common/parsers/ministral3.cpp index 075f14dbc13e..8bcd204c62e8 100644 --- a/common/parsers/ministral3.cpp +++ b/common/parsers/ministral3.cpp @@ -89,7 +89,7 @@ common_chat_params common_chat_params_init_ministral_3(const common_chat_templat foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); std::string name = function.at("name"); - const auto & schema = function.at("parameters"); + const auto schema = common_chat_tool_parameters(function); tool_choice |= p.rule("tool-" + name, p.tool_open(p.tool_name(p.literal(name)) + "[ARGS]") + @@ -114,15 +114,6 @@ common_chat_params common_chat_params_init_ministral_3(const common_chat_templat data.grammar_lazy = has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.at("parameters"); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/parsers/muse-glimmer.cpp b/common/parsers/muse-glimmer.cpp index 7f4dfcd5112d..add95697eb6d 100644 --- a/common/parsers/muse-glimmer.cpp +++ b/common/parsers/muse-glimmer.cpp @@ -74,31 +74,26 @@ common_chat_params common_chat_params_init_muse_glimmer(const common_chat_templa foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); const std::string name = function.at("name"); - auto params = function.contains("parameters") ? function.at("parameters") : json::object(); - auto args = p.eps(); - if (params.contains("properties") && params.at("properties").is_object() && !params.at("properties").empty()) { - auto schema_info = common_schema_info(); - schema_info.resolve_refs(params); - - auto arg_choice = p.choice(); - for (const auto & [prop_name, prop_schema] : params.at("properties").items()) { - auto value_parser = p.eps(); - if (schema_info.resolves_to_string(prop_schema)) { - value_parser = string_value; - } else { - value_parser = p.tool_arg_json_value( - p.schema(p.json(), "tool-" + name + "-arg-" + prop_name + "-schema", prop_schema, false)) - + p.tool_arg_close(p.literal("</atem:parameter>")); - } - - auto arg_rule = p.tool_arg( - p.tool_arg_open(p.literal("<atem:parameter name=\"") + p.tool_arg_name(p.literal(prop_name)) + p.literal("\">")) + - value_parser); - - arg_choice |= arg_rule; + std::vector<common_peg_parser> arg_rules; + foreach_parameter(function, [&](const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) { + auto value_parser = p.eps(); + if (prop.schema->may_be_string()) { + value_parser = string_value; + } else { + value_parser = p.tool_arg_json_value( + p.schema(p.json(), "tool-" + name + "-arg-" + prop.name + "-schema", doc, *prop.schema)) + + p.tool_arg_close(p.literal("</atem:parameter>")); } - args = p.zero_or_more(arg_choice + p.space()); + + arg_rules.push_back(p.tool_arg( + p.tool_arg_open(p.literal("<atem:parameter name=\"") + p.tool_arg_name(p.literal(prop.name)) + p.literal("\">")) + + value_parser)); + }); + + auto args = p.eps(); + if (!arg_rules.empty()) { + args = p.zero_or_more(p.choice(arg_rules) + p.space()); } auto tool_parser = p.tool( @@ -131,11 +126,6 @@ common_chat_params common_chat_params_init_muse_glimmer(const common_chat_templa if (include_grammar) { data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.contains("parameters") ? function.at("parameters") : json::object(); - builder.resolve_refs(schema); - }); parser.build_grammar(builder, data.grammar_lazy); }); data.grammar_triggers = { diff --git a/common/parsers/parsers.cpp b/common/parsers/parsers.cpp index 0a4d5cfbb522..643186c34cf2 100644 --- a/common/parsers/parsers.cpp +++ b/common/parsers/parsers.cpp @@ -2,8 +2,6 @@ #include "log.h" -#include <set> - void foreach_function(const json & tools, const std::function<void(const json &)> & fn) { for (const auto & tool : tools) { if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) { @@ -14,21 +12,14 @@ void foreach_function(const json & tools, const std::function<void(const json &) } } -void foreach_parameter(const json & function, const std::function<void(const std::string &, const json &, bool)> & fn) { - if (!function.contains("parameters") || !function.at("parameters").is_object()) { - return; - } - const auto & params = function.at("parameters"); - if (!params.contains("properties") || !params.at("properties").is_object()) { +void foreach_parameter(const json & function, const std::function<void(const common_chat_schema_property &, const common_chat_schema_document_ptr &)> & fn) { + auto params = common_chat_tool_parameters(function); + auto doc = std::make_shared<const common_chat_schema_document>(common_chat_schema_from_json(params)); + const auto * object = dynamic_cast<const common_chat_schema_object *>(doc->root.get()); + if (!object) { return; } - const auto & props = params.at("properties"); - std::set<std::string> required; - if (params.contains("required") && params.at("required").is_array()) { - required = params.at("required").get<std::set<std::string>>(); - } - for (const auto & [name, prop] : props.items()) { - bool is_required = (required.find(name) != required.end()); - fn(name, prop, is_required); + for (const auto & prop : object->properties) { + fn(prop, doc); } } diff --git a/common/parsers/parsers.h b/common/parsers/parsers.h index 7898f0007107..73fc719fddde 100644 --- a/common/parsers/parsers.h +++ b/common/parsers/parsers.h @@ -20,8 +20,8 @@ using json = common_json; // iterate over the function tools of an OpenAI-style tools array void foreach_function(const json & tools, const std::function<void(const json &)> & fn); -// iterate over the parameters of a function tool, flagging the ones listed as required -void foreach_parameter(const json & function, const std::function<void(const std::string &, const json &, bool)> & fn); +// iterate over the parameters of a function tool, with the document that owns them +void foreach_parameter(const json & function, const std::function<void(const common_chat_schema_property &, const common_chat_schema_document_ptr &)> & fn); // render a template; the override arguments let a parser feed in messages, tools or context it has rewritten std::string common_chat_template_direct_apply_impl( diff --git a/common/parsers/qwen3-coder.cpp b/common/parsers/qwen3-coder.cpp index 2c89d2986b3a..89dbb9800f2d 100644 --- a/common/parsers/qwen3-coder.cpp +++ b/common/parsers/qwen3-coder.cpp @@ -93,38 +93,65 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - std::string name = function.at("name"); - auto parameters = function.contains("parameters") ? function.at("parameters") : json::object(); - - auto schema_info = common_schema_info(); - schema_info.resolve_refs(parameters); + const auto & function = tool.at("function"); + std::string name = function.at("name"); std::vector<common_peg_parser> required_args; std::vector<common_peg_parser> optional_args; - foreach_parameter(function, [&](const std::string & param_name, const json & param_schema, bool is_required) { - auto rule_name = "tool-" + name + "-arg-" + param_name; + foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) { + auto rule_name = "tool-" + name + "-arg-" + param.name; - auto arg_open = p.tool_arg_open("<parameter=" + p.tool_arg_name(p.literal(param_name)) + ">\n"); + auto arg_open = p.tool_arg_open("<parameter=" + p.tool_arg_name(p.literal(param.name)) + ">\n"); - auto arg_value = schema_info.resolves_to_string(param_schema) ? - arg_string : - p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", param_schema)) + arg_close; - if (param_schema.contains("enum") && param_schema.at("enum").is_array() && - !param_schema.at("enum").empty() && - std::all_of(param_schema.at("enum").begin(), param_schema.at("enum").end(), - [](const json & value) { return value.is_string(); })) { + auto types = param.schema->value_types(); + + auto arg_value = p.eps(); + if (!types.has(common_chat_schema::TYPE_STRING)) { + arg_value = p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", doc, *param.schema)) + arg_close; + } else if (types.is_only(common_chat_schema::TYPE_STRING)) { + arg_value = arg_string; + } else { + // The string alternative accepts any text, so the grammar only keeps the raw string + // rule. The parser still tries the JSON alternatives first to type the value. + auto json_value = p.choice(); + if (types.has(common_chat_schema::TYPE_OBJECT)) { + json_value |= p.json_object(); + } + if (types.has(common_chat_schema::TYPE_ARRAY)) { + json_value |= p.json_array(); + } + if (types.has(common_chat_schema::TYPE_NUMBER) || types.has(common_chat_schema::TYPE_INTEGER)) { + json_value |= p.json_number(); + } + if (types.has(common_chat_schema::TYPE_BOOLEAN)) { + json_value |= p.json_bool(); + } + if (types.has(common_chat_schema::TYPE_NULL)) { + json_value |= p.json_null(); + } + arg_value = p.gbnf(p.atomic(p.tool_arg_json_value(json_value) + arg_close) | arg_string, "xml-arg-string"); + } + + if (param.schema->kind() == common_chat_schema::KIND_ENUM) { + const auto & enum_values = static_cast<const common_chat_schema_enum &>(*param.schema).values; auto values = p.choice(); - for (const auto & value : param_schema.at("enum")) { + bool all_strings = true; + for (const auto & value : enum_values) { + if (!value.is_string()) { + all_strings = false; + break; + } values |= p.literal(value.get<std::string>()) + p.peek(p.literal("\n</parameter>\n")); } - arg_value = p.tool_arg_string_value(values) + arg_close; + if (all_strings) { + arg_value = p.tool_arg_string_value(values) + arg_close; + } } auto arg_rule = p.rule(rule_name, p.tool_arg(arg_open + arg_value)); - (is_required ? required_args : optional_args).push_back(arg_rule); + (param.required ? required_args : optional_args).push_back(arg_rule); }); // Accept required arguments in any order, as Qwen does not always adhere to the @@ -168,15 +195,6 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat data.grammar_lazy = has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(inputs.tools, [&](const json & tool) { - const auto & function = tool.at("function"); - auto schema = function.contains("parameters") ? function.at("parameters") : json::object(); - builder.resolve_refs(schema); - }); - if (has_response_format) { - auto schema = inputs.json_schema; - builder.resolve_refs(schema); - } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/common/peg-parser.cpp b/common/peg-parser.cpp index 46fc29bf2f8b..10735389ea19 100644 --- a/common/peg-parser.cpp +++ b/common/peg-parser.cpp @@ -953,7 +953,7 @@ std::string common_peg_arena::dump_impl(common_peg_parser_id } else if constexpr (std::is_same_v<T, common_peg_until_parser>) { return "Until(" + string_join(p.delimiters, " | ") + ")"; } else if constexpr (std::is_same_v<T, common_peg_schema_parser>) { - return "Schema(" + dump_impl(p.child, visited) + ", " + (p.schema ? p.schema->dump() : "null") + ")"; + return "Schema(" + dump_impl(p.child, visited) + ", " + (p.node ? common_chat_schema::kind_name(p.node->kind()) : "null") + ")"; } else if constexpr (std::is_same_v<T, common_peg_rule_parser>) { return "Rule(" + p.name + ", " + dump_impl(p.child, visited) + ")"; } else if constexpr (std::is_same_v<T, common_peg_ref_parser>) { @@ -1119,8 +1119,13 @@ common_peg_parser common_peg_parser_builder::chars(const std::string & classes, return wrap(arena_.add_parser(common_peg_chars_parser{classes, ranges, negated, min, max})); } +common_peg_parser common_peg_parser_builder::schema(const common_peg_parser & p, const std::string & name, common_chat_schema_document_ptr doc, const common_chat_schema & node, bool raw) { + return wrap(arena_.add_parser(common_peg_schema_parser{p.id(), name, std::move(doc), &node, raw})); +} + common_peg_parser common_peg_parser_builder::schema(const common_peg_parser & p, const std::string & name, const common_json & schema, bool raw) { - return wrap(arena_.add_parser(common_peg_schema_parser{p.id(), name, std::make_shared<common_json>(schema), raw})); + auto doc = std::make_shared<const common_chat_schema_document>(common_chat_schema_from_json(schema)); + return this->schema(p, name, doc, *doc->root, raw); } common_peg_parser common_peg_parser_builder::rule(const std::string & name, const common_peg_parser & p, bool trigger) { @@ -1573,30 +1578,9 @@ static std::set<std::string> collect_reachable_rules( // GBNF generation implementation void common_peg_arena::build_grammar(const common_grammar_builder & builder, bool lazy) const { + // A raw string value is parsed by the child rather than constrained by the schema auto schema_delegates = [](const common_peg_schema_parser & s) -> bool { - if (!s.schema) { - return true; - } - if (s.raw && s.schema->contains("type")) { - const auto & type_val = s.schema->at("type"); - if (type_val.is_string() && type_val == "string") { - return true; - } - // Handle nullable types like ["string", "null"] - delegate when the - // non-null type is string, since the tagged format uses raw text - if (type_val.is_array()) { - for (const auto & t : type_val) { - if (t.is_string() && t.get<std::string>() != "null") { - return t.get<std::string>() == "string"; - } - } - } - } - // Delegate for enum schemas in raw mode - enum values are literal strings - if (s.raw && !s.schema->contains("type") && s.schema->contains("enum")) { - return true; - } - return false; + return !s.node || (s.raw && s.node->may_be_string()); }; // Unwrap the parser so we can properly check if it's a sequence or choice @@ -1731,7 +1715,7 @@ void common_peg_arena::build_grammar(const common_grammar_builder & builder, boo if (schema_delegates(p)) { return to_gbnf(p.child); } - return builder.add_schema(p.name, *p.schema); + return builder.add_schema(p.name, *p.node); } else if constexpr (std::is_same_v<T, common_peg_rule_parser>) { return p.name; } else if constexpr (std::is_same_v<T, common_peg_ref_parser>) { @@ -1859,7 +1843,6 @@ static common_json serialize_parser_variant(const common_peg_parser_variant & va {"type", "schema"}, {"child", p.child}, {"name", p.name}, - {"schema", p.schema ? *p.schema : json(nullptr)}, {"raw", p.raw} }; } else if constexpr (std::is_same_v<T, common_peg_rule_parser>) { @@ -1999,15 +1982,12 @@ static common_peg_parser_variant deserialize_parser_variant(const common_json & return common_peg_until_parser{j["delimiters"].get<std::vector<std::string>>()}; } if (type == "schema") { - if (!j.contains("child") || !j.contains("name") || !j.contains("schema") || !j.contains("raw")) { + if (!j.contains("child") || !j.contains("name") || !j.contains("raw")) { throw std::runtime_error("schema parser missing required fields"); } common_peg_schema_parser parser; parser.child = j["child"].get<common_peg_parser_id>(); parser.name = j["name"]; - if (!j["schema"].is_null()) { - parser.schema = std::make_shared<common_json>(j["schema"]); - } parser.raw = j["raw"].get<bool>(); return parser; } diff --git a/common/peg-parser.h b/common/peg-parser.h index ab095cc7d671..fb5d82b30fdc 100644 --- a/common/peg-parser.h +++ b/common/peg-parser.h @@ -1,5 +1,6 @@ #pragma once +#include "json-schema.h" #include "json.h" #include <memory> @@ -245,7 +246,8 @@ struct common_peg_until_parser { struct common_peg_schema_parser { common_peg_parser_id child; std::string name; - std::shared_ptr<common_json> schema; + common_chat_schema_document_ptr doc; // owns node + const common_chat_schema * node = nullptr; // Indicates if the GBNF should accept a raw string that matches the schema. bool raw; @@ -488,8 +490,10 @@ class common_peg_parser_builder { // A marker, i.e. text delimited by a pair of <> or [] common_peg_parser marker(); - // Wraps a parser with JSON schema metadata for grammar generation. - // Used internally to convert JSON schemas to GBNF grammar rules. + // Wraps a parser with the schema its GBNF is generated from, a node of the document that owns it + common_peg_parser schema(const common_peg_parser & p, const std::string & name, common_chat_schema_document_ptr doc, const common_chat_schema & node, bool raw = false); + + // Parses the JSON schema into a document of its own common_peg_parser schema(const common_peg_parser & p, const std::string & name, const common_json & schema, bool raw = false); // Creates a named rule, stores it in the grammar, and returns a ref. diff --git a/conversion/__init__.py b/conversion/__init__.py index 4d58bcd1060e..9c5d984388da 100644 --- a/conversion/__init__.py +++ b/conversion/__init__.py @@ -168,6 +168,7 @@ "Mamba2ForCausalLM": "mamba", "MambaForCausalLM": "mamba", "MambaLMHeadModel": "mamba", + "MapleForCausalLM": "maple", "MellumForCausalLM": "mellum", "MiMoV2FlashForCausalLM": "mimo", "MiMoV2ForCausalLM": "mimo", diff --git a/conversion/maple.py b/conversion/maple.py new file mode 100644 index 000000000000..fb0e87804d53 --- /dev/null +++ b/conversion/maple.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from typing import Iterable, TYPE_CHECKING, cast + +import torch + +if TYPE_CHECKING: + from torch import Tensor + +from .base import LazyTorchTensor, ModelBase, TextModel, gguf + + +@ModelBase.register("MapleForCausalLM") +@ModelBase.example("deepgrove/maple-preview") +class MapleModel(TextModel): + model_arch = gguf.MODEL_ARCH.MAPLE + + def set_gguf_parameters(self): + super().set_gguf_parameters() + hparams = self.hparams + + assert hparams["hidden_act"] == "silu" + assert hparams.get("num_shared_experts", 0) == 0 + assert hparams.get("norm_topk_prob", True) + assert hparams.get("nope_on_global_attention", False) + + head_dim = hparams.get("head_dim", hparams["hidden_size"] // hparams["num_attention_heads"]) + partial_rotary_factor = self.rope_parameters.get("partial_rotary_factor", 1.0) + + self.gguf_writer.add_vocab_size(hparams["vocab_size"]) + self.gguf_writer.add_rope_dimension_count(int(head_dim * partial_rotary_factor)) + self.gguf_writer.add_sliding_window(hparams["sliding_window"]) + self.gguf_writer.add_sliding_window_pattern([layer_type == "sliding_attention" for layer_type in hparams["layer_types"]]) + self.gguf_writer.add_expert_feed_forward_length(hparams["moe_intermediate_size"]) + # the reference clamps the MoE SwiGLU gate/up at 7.0 (modeling_maple.py) + self.gguf_writer.add_swiglu_clamp_exp([7.0] * self.block_count) + + _experts: list[dict[str, Tensor]] | None = None + + @staticmethod + def _stack_experts(tensors: list[Tensor]) -> Tensor: + shape = (len(tensors), *tensors[0].shape) + dtype = tensors[0].dtype + meta = LazyTorchTensor.meta_with_dtype_and_shape(dtype, shape) + + # tensors goes through args, not the closure, so that `func` matches + # LazyBase's single-argument shape + def stack(ts: list[Tensor]) -> Tensor: + result = torch.empty(shape, dtype=dtype) + for expert_id, tensor in enumerate(ts): + result[expert_id].copy_(LazyTorchTensor.to_eager(tensor)) + ts.clear() + return result + + return cast(torch.Tensor, LazyTorchTensor(meta=meta, args=(tensors,), func=stack)) + + def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]: + if "mlp.experts" in name: + n_experts = self.hparams["num_experts"] + assert bid is not None + + if self._experts is None: + self._experts = [{} for _ in range(self.block_count)] + + self._experts[bid][name] = data_torch + + if len(self._experts[bid]) >= n_experts * 3: + for weight_name in ("down_proj", "gate_proj", "up_proj"): + tensors = [] + + for expert_id in range(n_experts): + expert_name = f"model.layers.{bid}.mlp.experts.{expert_id}.{weight_name}.weight" + tensors.append(self._experts[bid].pop(expert_name)) + + merged_name = f"model.layers.{bid}.mlp.experts.{weight_name}.weight" + yield from super().modify_tensors(self._stack_experts(tensors), merged_name, bid) + return + + yield from super().modify_tensors(data_torch, name, bid) + + def prepare_tensors(self): + super().prepare_tensors() + + if self._experts is not None: + experts = [name for layer in self._experts for name in layer] + if experts: + raise ValueError(f"Unprocessed experts: {experts}") diff --git a/docs/backend/OPENVINO.md b/docs/backend/OPENVINO.md index 9b43807d36b3..c1e39c5bf153 100644 --- a/docs/backend/OPENVINO.md +++ b/docs/backend/OPENVINO.md @@ -719,10 +719,13 @@ Boolean flags follow a uniform convention: set to a **positive integer** (e.g. ` | `GGML_OPENVINO_STATEFUL_EXECUTION`| Boolean | `0` | Enable stateful KV cache for better performance. Recommended on CPU, GPU. | | `GGML_OPENVINO_DISABLE_CACHE` | Boolean | `0` | Disable the in-process compiled-model / decoder cache (cache is on by default). Set to `1` to disable. | | `GGML_OPENVINO_DISABLE_KV_SLICE` | Boolean | `0` | Disable the KV-cache input-tensor slicing optimization (slicing is on by default on CPU/GPU). Set to `1` to disable. | +| `GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT` | Boolean | `0` | Disable the stateful KV-state sequence-axis relayout (relayout is on by default). It moves the KV state sequence axis from dim 1 to dim 2, so the GPU plugin can append new tokens in place instead of copying the whole state every token, and the reader side no longer transposes the whole accumulated state. Set to `1` to disable. | | `GGML_OPENVINO_MANUAL_GQA_ATTN` | Boolean | device-based | Tri-state. When **unset**, manual GQA attention is enabled by default on `GPU` and disabled on other devices. Set to a positive integer to force-enable, or `0` to force-disable. | | `GGML_OPENVINO_MEMORY_OPTIMIZE` | Boolean | `0` | Umbrella switch for compile-time memory reductions. Enables `GGML_OPENVINO_REDUCE_COMPILE_MEM` and, on GPU, `GGML_OPENVINO_RELEASE_WEIGHTS` unless those fine-grained variables are explicitly set. | | `GGML_OPENVINO_REDUCE_COMPILE_MEM`| Boolean | inherits from `GGML_OPENVINO_MEMORY_OPTIMIZE` | Reduce compile-time host memory use by streaming weight requantization and avoiding extra weight-node materialization where possible. Set explicitly to override the umbrella switch. | | `GGML_OPENVINO_RELEASE_WEIGHTS` | Boolean | inherits from `GGML_OPENVINO_MEMORY_OPTIMIZE` on GPU | GPU-only. Release host weight buffers after the compiled model cache can reuse the device/plugin copy. Requires stable graph shapes; dynamic workloads that need recompilation should leave this disabled. | +| `GGML_OPENVINO_SPILL_DIR` | String | `not set` | Directory for a disk-backed weight buffer. When set, the repacked weight buffer is mapped from an unlinked file on this path instead of anonymous memory, so its pages are reclaimable under memory pressure instead of staying pinned, cutting the load-time host memory peak. Must point at real storage; a tmpfs mount (e.g. `/tmp` on many systems) backs it with RAM and makes the peak worse. | +| `GGML_OPENVINO_REQUANT_KQUANT` | String | `not set` | Requantize Q6_K/Q5_K weights (and matching MoE expert weights) to a 4-bit target instead of the default Q8_0_C, trading accuracy for less memory traffic. One of `q4_sym128` (Q6_K/Q5_K only), `q4_sym128_all` (Q4_K too, drops its per-group zero point), `q4_asym64_all` (Q6_K/Q5_K/Q4_K, keeps a real zero point at group 64), or `native` (no requantization). | | `GGML_OPENVINO_PROFILING` | Boolean | `0` | Enable execution-time profiling. | | `GGML_OPENVINO_DUMP_CGRAPH` | Boolean | `0` | Dump the GGML compute graph to `cgraph_ov.txt`. | | `GGML_OPENVINO_DUMP_IR` | Boolean | `0` | Serialize OpenVINO IR files with timestamps. | diff --git a/docs/backend/SYCL.md b/docs/backend/SYCL.md index 4a640e442ee1..91b2097419ed 100644 --- a/docs/backend/SYCL.md +++ b/docs/backend/SYCL.md @@ -790,14 +790,15 @@ User can use the device management in [docs/multi-gpu.md](https://github.com/ggm | Name | Value | Function | |-------------------|------------------|---------------------------------------------------------------------------------------------------------------------------| -| GGML_SYCL_DEBUG | 0 (default) or 1 | Enable log function by macro: GGML_SYCL_DEBUG | +| GGML_SYCL_DEBUG | 0 (default) or 1 | Enable log function: GGML_SYCL_DEBUG() for common debug. | +| GGML_SYCL_DEV_DEBUG | 0 (default) or 1 | Enable log function: GGML_SYCL_DEV_DEBUG() for developmental purposes by replacing GGML_SYCL_DEBUG() in special codes. Restore to GGML_SYCL_DEBUG() before committing code.| | GGML_SYCL_DEV2DEV_MEMCPY | 0 (default), 1, 2 | Choose the method of dev2dev memory copy.<br>Value: <br>* 0: SYCL API (default), only support dGPUs.<br>* 1: L0 API -- Better performance, only support dGPUs, found to lead to abnormal crash in some case. <br>* 2: Host Forward -- Most stable method for all cases (including iGPU + dGPU*N), but with lower performance (-2% to -5%).<br>SYCL & L0 API are easy to be impacted by Intel GPU driver issue. When you meet the garbled output or crash issues in multiple GPUs case, try with this debug flag to work around or check the issue.| | GGML_SYCL_ENABLE_FLASH_ATTN | 1 (default) or 0| Enable Flash-Attention. It can reduce memory usage. The performance impact depends on the LLM.| | GGML_SYCL_ENABLE_OPT | 0 or 1 (default)| Enable optimize features for Intel GPUs. (Recommended to 0 for Intel devices older than Gen 10) | | GGML_SYCL_ENABLE_GRAPH | 0 (default) or 1 | Enable running computations through SYCL Graphs feature. Disabled by default because SYCL Graph is still on development, no better performance. | | GGML_SYCL_ENABLE_HOST_PINNED_MEM | 0 or 1 (default) | Enable host pinned memory to speed up copy data from host to device. When disable it, host memory will common malloc() on CPU. Disable it when use `--load-model mlock`.| | GGML_SYCL_HOST_PINNED_MEM_2G | 0 (default) or 1 | Limit the max memory allocation to be no more than 2GB when enable host pinned memory. USM allocations above 2 GiB take the relaxed/large-allocation path, which serializes H2D copies with compute and prevents copy/compute overlap. It will impact the startup time. Need more test. Depend on `GGML_SYCL_ENABLE_HOST_PINNED_MEM=1`.| -| GGML_SYCL_GET_MEM_API | 0 (default) or 1 | Set to get memory info (free, total) by Level Zero or SYCL API:<br>0 - Level Zero API: support more GPUs, only run on Level Zero running time. When there is an error, fallback to call SYCL API. Depend on GGML_SYCL_SUPPORT_LEVEL_ZERO_API.<br>1 - SYCL API: legacy, support more running time, it can't get the free size of some GPUs (like Arc770). In such case, return total size for free size.| +| GGML_SYCL_GET_MEM_API | 0 (default) or 1 | Set to get memory info (free, total) by Level Zero or SYCL API:<br>0 - Level Zero API: support more GPUs, only run on Level Zero running time. When there is an error, fallback to call SYCL API. Depend on GGML_SYCL_SUPPORT_LEVEL_ZERO_API.<br>1 - SYCL API: legacy, support more running time, it can't get the free size of some GPUs (like Arc770). In such case, return the free size as value of total size.| | GGML_SYCL_USE_LEVEL_ZERO_API | 1 (default) or 0 | Use Level Zero API for device memory allocation instead of SYCL. Reduces system RAM usage on Intel dGPUs by avoiding DMA-buf/TTM host memory staging. Requires GGML_SYCL_SUPPORT_LEVEL_ZERO_API=ON at build time. SYCL backend always runs on Level Zero running time even if it's set as OFF (The SYCL api will be usage for memory allocation).| | GGML_SYCL_ENABLE_DNN | 0 or 1 (default)| Enable running computations through oneDNN and always use oneMKL. | | GGML_SYCL_FA_ONEDNN | 1 (default) or 0 | Enable the oneDNN fused SDPA (flash-attention) path on supported GPUs. Set to 0 to always use the native SYCL flash-attention kernel. | diff --git a/docs/backend/snapdragon/README.md b/docs/backend/snapdragon/README.md index 391c8bf230f0..5d32a5877ad3 100644 --- a/docs/backend/snapdragon/README.md +++ b/docs/backend/snapdragon/README.md @@ -188,7 +188,7 @@ llama_memory_breakdown_print: | - Host | 439 = Op test for MUL_MAT: ``` -~/src/llama.cpp$ ./scripts/snapdragon/run.py --target adb --hex-hostbuf 0 --devices HTP0:0 -- test-backend-ops -b HTP0:0 -o MUL_MAT +~/src/llama.cpp$ ./scripts/snapdragon/run.py --target adb --devices HTP0:0 -- test-backend-ops -b HTP0:0 -o MUL_MAT ... Backend 2/3: HTP0:0 Device description: Hexagon @@ -213,14 +213,109 @@ ggml-hex: new session: HTP0 : session-id 0 domain-id 3 uri file:///libggml-htp-v | llama 1B Q4_0 | 729.75 MiB | 1.24 B | HTP | 99 | 4 | 128 | 0 | tg64 | 51.54 ± 1.13 | ``` +## Multi-Device Execution Modes + +The Hexagon backend supports multiple execution and partitioning modes to accommodate different model sizes, memory +constraints, and single- or multi-NPU hardware topologies: + +### 1. Single-Device Mode with Dynamic Buffer Mapping + +Runs the model on a single NPU session (e.g. `HTP0` or `HTP0:0`). + +A single NPU session provides ~3.5GB of available virtual address space. For models larger than 3.5GB, the backend +automatically maps and unmaps weight buffers during graph execution. This allows large models to run on a single NPU +without manual configuration: + +```bash +./scripts/snapdragon/run.py --target adb --devices HTP0:0 -- \ + llama-cli -m models/Llama-3.2-3B-Instruct-Q4_0.gguf -ngl 99 -p "Hello" +``` + +### 2. Layer-Split Mode across Virtual Sessions (`HTP0,HTP1,...` or `HTP0:0,HTP0:1,...`) + +Partitions model layers at load time across multiple virtual sessions hosted on a single physical NPU. + +Each virtual session acts as an independent backend device from llama.cpp's perspective (similar to multiple GPUs). +Because layers are permanently distributed across sessions, each session's allocated weights remain within its private 3.5GB +address space window, eliminating runtime buffer re-mapping overhead. + +Here is an example of running the GPT-OSS-20B model on a Snapdragon device using 4 virtual sessions on a single NPU: + +```bash +./scripts/snapdragon/run.py --target adb \ + --devices HTP0:0,HTP0:1,HTP0:2,HTP0:3 -- \ + llama-cli --load-mode none -m /data/local/tmp/gguf/gpt-oss-20b-Q4_0.gguf -t 4 \ + --ctx-size 8192 --batch-size 128 -ctk q8_0 -ctv q8_0 -fa on -ngl 99 -no-cnv -f surfing.txt +``` + +Log output snippet: + +``` +... +llama_model_loader: - type f32: 289 tensors +llama_model_loader: - type q4_0: 96 tensors +llama_model_loader: - type q8_0: 2 tensors +llama_model_loader: - type mxfp4: 72 tensors +... +load_tensors: offloaded 25/25 layers to GPU +load_tensors: CPU model buffer size = 1182.09 MiB +load_tensors: HTP0:1 model buffer size = 2512.58 MiB +load_tensors: HTP0:3 model buffer size = 2093.83 MiB +load_tensors: HTP0:0 model buffer size = 2931.34 MiB +load_tensors: HTP0:2 model buffer size = 2512.58 MiB +... +llama_perf_context_print: prompt eval time = 3843.67 ms / 197 tokens ( 19.51 ms per token, 51.25 tokens per second) +llama_perf_context_print: eval time = 1686.13 ms / 31 runs ( 54.39 ms per token, 18.39 tokens per second) +llama_perf_context_print: total time = 6266.30 ms / 228 tokens +llama_memory_breakdown_print: | memory breakdown [MiB] | total free self model context compute unaccounted | +llama_memory_breakdown_print: | - HTP0:0 (Hexagon) | 2048 = 2048 + ( 0 = 0 + 0 + 0) + 0 | +llama_memory_breakdown_print: | - HTP0:1 (Hexagon) | 2048 = 2048 + ( 0 = 0 + 0 + 0) + 0 | +llama_memory_breakdown_print: | - HTP0:2 (Hexagon) | 2048 = 2048 + ( 0 = 0 + 0 + 0) + 0 | +llama_memory_breakdown_print: | - HTP0:3 (Hexagon) | 2048 = 2048 + ( 0 = 0 + 0 + 0) + 0 | +llama_memory_breakdown_print: | - Host | 1476 = 1208 + 105 + 162 | +``` + +### 3. Tensor-Split Mode across Physical Devices (`HTP0:0,HTP1:0,...`) + +Distributes model tensors across distinct physical NPU hardware cores using llama.cpp's tensor parallelism +(`--split-mode tensor`). + +Tensors are partitioned across physical NPUs for parallel execution (proportions are distributed equally by default without +needing an explicit `--tensor-split` option): + +```bash +./scripts/snapdragon/run.py --target adb \ + --devices HTP0:0,HTP1:0 -- \ + llama-cli -m models/Llama-3.2-3B-Instruct-Q4_0.gguf --split-mode tensor -ngl 99 -p "Hello" +``` + +### 4. Row-Split Multi-Device Mode via Device Grouping (`HTP0[0-1]`) + +Groups multiple physical NPU cores into a single logical device using bracket notation (`HTP0[0-1]` or `HTP0[0,1]`). + +Unlike host-level tensor-splitting, row-splitting is executed entirely inside the Hexagon backend: + +```bash +./scripts/snapdragon/run.py --target adb \ + --devices 'HTP0[0-1]' -- \ + llama-cli -m models/Llama-3.2-3B-Instruct-Q4_0.gguf -ngl 99 -p "Hello" +``` + +You can also combine row-splitting with layer-splitting across multiple grouped devices (e.g. `--devices 'HTP0[0-1],HTP1[2-3]'` +on 4 physical NPUs, or `--devices 'HTP0[0-1:0],HTP1[0-1:1]'` on 2 physical NPUs using virtual sessions 0 and 1). + ## Environment variables - `GGML_HEXAGON_DEVICES` (default: not set, defaults to HTP0 session) - Controls which NPU devices and sessions to allocate. Can be configured as: - - A single integer `N`: Allocates `N` sessions named `HTP0`, `HTP1`, ..., `HTP<N-1>` (behaves identically to `GGML_HEXAGON_NDEV=N`). - - A comma-separated list of device names in `HTP<physical_idx>:<virtual_idx>` format (or legacy `HTP<idx>` format). For example, `HTP0:0,HTP0:1` creates two virtual - sessions on the first physical NPU (useful for memory limits). `HTP0:0,HTP1:0` allocates one session on each of the two physical NPUs - on a dual-NPU device. + Controls which NPU devices and sessions to allocate. Configurable via `--devices` in `run.py`: + - `N` (single integer): Allocates `N` virtual sessions named `HTP0`, `HTP1`, ..., `HTP<N-1>` on physical NPU 0. + - `HTP<phys>:<virt>,...`: Comma-separated list of individual devices specifying physical and virtual index: + - `HTP0:0,HTP0:1`: Two virtual sessions on physical NPU 0 (layer-split on single NPU). + - `HTP0:0,HTP1:0`: One session on physical NPU 0 and one on physical NPU 1 (tensor-split across physical cores). + - `HTP<name>[<phys_spec>]`: Device grouping syntax for row-split multi-device execution: + - `HTP0[0-1]`: A single logical device `HTP0` that groups physical cores 0 and 1. + - `HTP0[0-1],HTP1[2-3]`: Two layer-split devices across 4 physical NPUs (cores 0-1 and 2-3). + - `HTP0[0-1:0],HTP1[0-1:1]`: Two layer-split devices across 2 physical NPUs using virtual sessions 0 and 1. - `GGML_HEXAGON_NDEV` (deprecated) Replaced by `GGML_HEXAGON_DEVICES`. Controls the number of virtual sessions to allocate on physical NPU `0`. @@ -229,9 +324,8 @@ ggml-hex: new session: HTP0 : session-id 0 domain-id 3 uri file:///libggml-htp-v - `GGML_HEXAGON_NHVX=0` Controls the number of HVX hardware threads to use. The default is all (actual number varies depending on the hardware version). -- `GGML_HEXAGON_HOSTBUF=1` - Controls whether the Hexagon backend allocates host buffers. By default, all buffers except for REPACK are host buffers. - This option is required for testing Ops that require REPACK buffers (MUL_MAT and MUL_MAT_ID). +- `GGML_HEXAGON_HOSTBUF=1` (default: 0, disabled) + Enables allocating host buffers for debugging. By default, host buffers are disabled. - `GGML_HEXAGON_VERBOSE=1` Enables verbose logging of Ops from the backend. Example output: @@ -246,23 +340,26 @@ ggml-hex: new session: HTP0 : session-id 0 domain-id 3 uri file:///libggml-htp-v ``` - `GGML_HEXAGON_PROFILE=1` - Enables Op profiling: + Enables Op profiling (configurable via `--hex-profile` in `run.py`): - - `1` Basic profile with per-op `usecs` and `cycles` counters - - `2` Extended profile with per-op `usecs`, `cycles` and default PMU counter data - - `0x1,...,0x8` Extended profile with per-op `usecs`, `cycles` and custom PMU counter data + - `1`: Basic profile with per-op `usecs` and `cycles` counters + - `2`: Extended profile with per-op `usecs`, `cycles` and default PMU counter data + - `0x1,...,0x8`: Extended profile with per-op `usecs`, `cycles` and custom PMU counter data - The logging output can be either saved into a file for post-processing or it can be piped directly into the post-processing tool - to generate the report. - Examples: + The logging output can be saved to a file or piped directly into the post-processing script: - `GGML_HEXAGON_PROFILE=1 ./scripts/snapdragon/run.py --target adb -- llama-cli ... |& ./scripts/snapdragon/ggml-hexagon-profile.py -` + ```bash + ./scripts/snapdragon/run.py --target adb --hex-profile 1 -- llama-cli ... |& \ + ./scripts/snapdragon/ggml-hexagon-profile.py - + ``` - `GGML_HEXAGON_OPFILTER=regex` - Allows filtering (disabling) Ops that match the regex pattern: + Filters (disables) Ops matching the regex pattern (configurable via `--hex-opfilter` in `run.py`): - Examples: - - `GGML_HEXAGON_OPFILTER="FLASH_ATTN_EXT" ./scripts/snapdragon/run.py --target adb -- llama-cli ...` - Disable Flash Attention on Hexagon (falls back to CPU or GPU) - `GGML_HEXAGON_OPFILTER="ADD\|SUB" ./scripts/snapdragon/run.py --target adb -- llama-cli ...` - Disable ADD and SUB on Hexagon (fall back to CPU or GPU) + ```bash + # Disable Flash Attention on Hexagon (falls back to CPU or GPU) + ./scripts/snapdragon/run.py --target adb --hex-opfilter "FLASH_ATTN_EXT" -- llama-cli ... + # Disable ADD and SUB on Hexagon (fall back to CPU or GPU) + ./scripts/snapdragon/run.py --target adb --hex-opfilter "ADD|SUB" -- llama-cli ... + ``` diff --git a/docs/backend/snapdragon/developer.md b/docs/backend/snapdragon/developer.md index d7d9f2a2790d..633643c16ddd 100644 --- a/docs/backend/snapdragon/developer.md +++ b/docs/backend/snapdragon/developer.md @@ -2,16 +2,16 @@ ## Backend libraries -The Hexagon backend consist of two parts: +The Hexagon backend consists of two parts: - `libggml-hexagon` - This is the regular CPU-side GGML backend library, either shared or statically linked + This is the regular CPU-side GGML backend library, either shared or statically linked. - `libggml-htp-vNN` This is the NPU-side (HTP stands for Hexagon Tensor Processor) shared library that contains the Op dispatcher and kernels. The correct library is selected automatically at runtime based on the HW version. -Here is an example of the build artifacts +Here is an example of the build artifacts: ``` ~/src/llama.cpp$ ls -l pkg-adb/llama.cpp/lib/libggml* @@ -26,75 +26,307 @@ pkg-adb/llama.cpp/lib/libggml-htp-v81.so ## Memory buffers -Hexagon NPU backend takes advantage of the Snapdragon's unified memory model where all buffers are fully accessible by the CPU and GPU. -The NPU does have a dedicated tightly-coupled memory called VTCM but that memory is used only for intermediate data (e.g. dynamically -quantized tensors) or temporary data (chunks of the weight tensors fetched via DMA). - -Please note that currently the Hexagon backend does not implement SET/GET_ROWS Ops because there is no advantage in offloading those -to the NPU at this point. - -The backend does allocates non-host buffers for the tensors with datatypes that require repacking: Q4_0, Q8_0, MXFP4. -From the MMU perspective these buffers are still regular buffers (normal access by the CPU) they are marked as non-host simply to force -the repacking. +The Hexagon NPU backend takes advantage of Snapdragon unified memory where all DDR buffers are accessible by CPU, GPU, and NPU. +The NPU has dedicated tightly-coupled memory called VTCM (Vector Tightly-Coupled Memory). VTCM is used for intermediate data (such as +dynamically quantized activations) and streaming buffers (chunks of weight and activation tensors fetched via DMA). ## Large model handling -Hexagon NPU sessions (aka Process Domains (PD) in the Hexagon SDK) are limited to a maximum memory mapping window of around 3.5GB. +Hexagon NPU sessions have a 32-bit virtual address space window of around 3.5GB. In llama.cpp/GGML, each Hexagon session is mapped to a single GGML backend device (e.g., `HTP0:0`, `HTP0:1`, etc. when using `GGML_HEXAGON_DEVICES`, or `HTP0`, `HTP1` in legacy mode). -To support running models larger than 3.5GB on a single device, the Hexagon backend dynamically maps and unmaps execution buffers -during the graph execution cycle to stay within the Process Domain window. This enables large models to run successfully on a single -NPU device. +To support running models larger than 3.5GB on a single device, the Hexagon backend dynamically maps and unmaps buffers: +- Buffers are allocated in shared DDR (RPCMEM) via file descriptors (`fastrpc_mmap` using `FASTRPC_MAP_FD_DELAYED`). +- Pinned buffers (such as KV cache and active compute buffers) remain mapped throughout execution. +- Inactive weight buffers are dynamically mapped into the NPU session via `HAP_mmap()` during batch buffer preparation + (`prep_op_bufs()` in `htp/main.c`) and unmapped via `htp_iface_munmap()` when no longer needed by the active batch. +- This dynamic sliding window allows a single NPU session to execute models that exceed the 3.5GB window. + +Alternatively, users can partition and split the model across multiple virtual sessions or physical NPUs using layer-splitting, +tensor-splitting, or row-splitting modes. For user-facing execution modes and examples, see the +[Snapdragon user guide](README.md#multi-device-execution-modes). + +## Op and Kernel Development Guidelines + +Writing high-performance operators for Hexagon requires following specific guidelines. + +### DDR -> DMA -> VTCM Execution Pipeline + +- Strongly prefer the `DDR -> DMA -> VTCM -> compute (HVX/HMX) -> VTCM -> DMA -> DDR` data flow. +- Direct HVX reads/writes from/to DDR are less efficient and should only be used as a fallback. +- The DMA queue is a strict FIFO where operations must be pushed and popped in strict order. +- Follow the pipelined multi-buffering sequence properly (typically 2x to 16x buffering) so every push has a corresponding pop: + + 1. In the prologue, push initial DDR -> VTCM transfers to prime the pipeline. + 2. In the loop body, wait for buffer N via DMA pop, launch HVX/HMX compute on buffer N, push VTCM -> DDR writeback of result N, + and push DDR -> VTCM prefetch of buffer N+2. + 3. In the epilogue, pop all remaining in-flight transfers to drain the pipeline. + +- Because every push must be matched by a pop, `dma_queue_flush()` is not required when the pipeline sequence is followed + properly. Flushing is only used in rare exceptions where a batch of operations is pushed without individual pops. +- Use the DMA queue interface from [`dma-queue.h`](../../../ggml/src/ggml-hexagon/htp/dma-queue.h) + (`dma_queue_push_ddr_to_vtcm`, `dma_queue_pop`, `dma_queue_push_vtcm_to_ddr`). + See [`cumsum-ops.c`](../../../ggml/src/ggml-hexagon/htp/cumsum-ops.c) and + [`act-ops.c`](../../../ggml/src/ggml-hexagon/htp/act-ops.c) for reference implementations. + +### Avoid Scalar Reads and Writes to VTCM + +- Access VTCM data using DMA transfers or HVX/HMX vector instructions rather than scalar reads and writes. + +### Avoid Scalar Division in Inner Loops + +- Hexagon cores do not have hardware division instructions. +- For recurring divisions across iterations or threads, use `fastdiv` from + [`hex-fastdiv.h`](../../../ggml/src/ggml-hexagon/htp/hex-fastdiv.h) with precomputed divisors (such as + `octx->ctx->mdev.count_div` or `octx->n_threads_div`). +- Do not call `init_fastdiv_values()` for single-use divisions; use standard compiler division (`/`) instead. + +### Host-Side Precomputation via `kernel_params` + +- Precompute tensor shapes, strides, scale conversions, tiling layouts, and validation checks on the host CPU during graph + preparation in [`ggml-hexagon.cpp`](../../../ggml/src/ggml-hexagon/ggml-hexagon.cpp). +- Pack precomputed parameters into the operator's fixed `kernel_params` structure in `htp_op_node` (such as + `htp_mm_kernel_params`, `htp_unary_kernel_params`, `htp_fa_kernel_params`, `htp_get_rows_kernel_params`). +- The NPU executes directly using `octx->kernel_params` without redundant runtime metadata extraction or validation. +- **Strict Host-Kernel Alignment**: + - Verify that parameters calculated by the host CPU are strictly honored by the NPU kernel. + - Ensure the kernel does not ignore host-computed fields (for example, falling back to `octx->n_threads` instead of + using `kparams->n_threads`, or ignoring precomputed `tasks_per_thread` and chunk counts). + - Both human developers and coding agents must audit both sides of the interface: ensure fields populated in `kernel_params` + in [`ggml-hexagon.cpp`](../../../ggml/src/ggml-hexagon/ggml-hexagon.cpp) are actively and consistently utilized by the + corresponding operator entry point and worker threads in `htp/*-ops.c`. + +### Tracing Instrumentation + +- All kernels must include trace events for performance profiling and timeline visualization in Perfetto + ([`hex-profile.h`](../../../ggml/src/ggml-hexagon/htp/hex-profile.h)). +- Surround compute sections with `htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) info)` and + `htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) info)`. +- Use specific event types for major phases: + - `HTP_TRACE_EVT_HVX_COMP`: Vector compute execution. + - `HTP_TRACE_EVT_DMA`: DMA transfer wait or poll cycles. + - `HTP_TRACE_EVT_FENCE`: Multi-device fence barrier synchronization. + - `HTP_TRACE_EVT_L2FLUSH`: L2 cache cleaning operations. +- Pass meaningful progress metrics (such as row index, chunk index, or token index) in the 16-bit `info` parameter. + +### Work Queue and Threading + +- Distribute parallel work across NPU worker threads using the thread pool work queue: + + ```c + work_queue_run(ctx->work_queue, worker_func, &op_ctx, n_threads); + ``` + +- Keep worker functions independent and re-entrant. Worker threads should only operate on their designated chunk of rows or elements. + +### Avoid Redundant Defensive NULL Checks + +- Do not add defensive NULL checks or assertions for internal framework pointers or required graph operands and outputs. + Internal pointers include `ctx`, `octx`, local context structs like `*ctx`, `kparams`, and worker callback `data`. +- These pointers are architectural invariants during kernel execution and host-side graph preparation. + Graph compute receives allocated nodes with valid required `node->src[N]` and `node->data` pointers. +- Do not turn an invariant violation into an unsupported operation or missed fusion. + Checks such as `if (!octx || !octx->ctx)` clutter the code, obscure intent, and hide upstream errors. +- **Distinction**: `octx->src[N]` pointers *can* be NULL by design and must be checked when optional. + Examples include attention masks, optional bias or weights in fused kernels, and frequency factors. + +### Multiline Macro Formatting + +- Keep trailing backslashes in multiline `#define` macros cleanly aligned to a consistent column. +- Avoid trailing whitespace after macro backslashes. +- Use [`scripts/snapdragon/ggml-hexagon-align-macros.py`](../../../scripts/snapdragon/ggml-hexagon-align-macros.py) to inspect, diff, + or automatically align macro definitions across Hexagon kernel sources: + + ```bash + # Check for misaligned macros + python3 scripts/snapdragon/ggml-hexagon-align-macros.py ggml/src/ggml-hexagon/htp/ + + # Fix misaligned macros in-place + python3 scripts/snapdragon/ggml-hexagon-align-macros.py --fix ggml/src/ggml-hexagon/htp/ + ``` + +## Multi-Device Partitioning (mdev) + +Multi-device (mdev) mode enables row-level tensor parallel execution across multiple physical NPU cores or virtual NPU +sessions. + +### 128-Byte Cache Line Alignment + +- Shared tensor buffers reside in DDR (RPCMEM) with a 128-byte cache line granularity + (`HEX_L2_LINE_SIZE` = 128 bytes, `HTP_TENSOR_MDEV_LINE_SIZE`). +- **Rule**: Multi-device work partitions must align destination write regions to 128-byte cache line boundaries so distinct + devices never share or overwrite the same cache line. + +### Partitioning Helpers in `htp-tensor.h` + +Common partitioning logic is factored into reusable inline helpers in +[`htp-tensor.h`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h): + +1. [`htp_tensor_mdev_rows_per_chunk`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L67): + Determines the minimum number of rows per chunk so that the chunk byte size is a multiple of 128 bytes: + + ``` + rows_per_chunk = 128 / hex_gcd_u32(row_size, 128) + ``` + + If row stride `nb[1]` is already a multiple of 128 bytes, `rows_per_chunk = 1`. + Returns `false` if the tensor cannot be safely row-partitioned (such as unaligned base pointer, permuted layout, + or non-128-byte aligned outer strides). -Alternatively, users can choose to use standard llama.cpp/GGML layer-splitting mode to partition and split the model across -multiple Hexagon devices or virtual sessions (which behave like multiple GPUs from the offload and splitting perspective). +2. [`htp_tensor_mdev_partition`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L94): + Calculates the per-device work range `struct htp_tensor_mdev_range { uint32_t start; uint32_t count; }` given + `total_units`, `units_per_chunk`, `mdev_idx`, `mdev_count`, and the precomputed `mdev_count_div`. + Handles chunk distribution across devices, assigns remainder units to the last device, and automatically triggers + single-device fallback when partitioning is unsafe. -Here is an example of running GPT-OSS-20B model on a Snapdragon device using 4 virtual sessions on a single NPU (physical index 0). +### Row-Partitioned Operators +For row-wise operators +(such as activations in [`act-ops.c`](../../../ggml/src/ggml-hexagon/htp/act-ops.c), +binary ops in [`binary-ops.c`](../../../ggml/src/ggml-hexagon/htp/binary-ops.c), +unary ops in [`unary-ops.c`](../../../ggml/src/ggml-hexagon/htp/unary-ops.c), and +sameshape copies in [`cpy-ops.c`](../../../ggml/src/ggml-hexagon/htp/cpy-ops.c)): + +```c +const uint32_t total_rows = ne01 * ne02 * ne03; +const size_t dst_row_size = dst->ne[0] * elem_size; + +uint32_t row_start = 0; +uint32_t nrows = total_rows; + +if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, elem_size, (uint32_t) dst_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition( + total_rows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; +} + +if (nrows == 0) { + return HTP_STATUS_OK; +} ``` -~/src/llama.cpp$ ./scripts/snapdragon/run.py --target adb --devices HTP0:0,HTP0:1,HTP0:2,HTP0:3 -- llama-cli --load-mode none -m /data/local/tmp/gguf/gpt-oss-20b-Q4_0.gguf -t 4 --ctx-size 8192 --batch-size 128 -ctk q8_0 -ctv q8_0 -fa on -ngl 99 -no-cnv -f surfing.txt -... -llama_model_loader: - type f32: 289 tensors -llama_model_loader: - type q4_0: 96 tensors -llama_model_loader: - type q8_0: 2 tensors -llama_model_loader: - type mxfp4: 72 tensors -... -load_tensors: offloaded 25/25 layers to GPU -load_tensors: CPU model buffer size = 1182.09 MiB -load_tensors: HTP0:1 model buffer size = 2512.58 MiB -load_tensors: HTP0:3 model buffer size = 2093.83 MiB -load_tensors: HTP0:0 model buffer size = 2931.34 MiB -load_tensors: HTP0:2 model buffer size = 2512.58 MiB -... -llama_context: n_ctx_per_seq (8192) < n_ctx_train (131072) -- the full capacity of the model will not be utilized -llama_context: CPU output buffer size = 0.77 MiB -llama_kv_cache_iswa: creating non-SWA KV cache, size = 8192 cells -llama_kv_cache: HTP0:1 KV buffer size = 25.50 MiB -llama_kv_cache: HTP0:3 KV buffer size = 25.50 MiB -llama_kv_cache: HTP0:0 KV buffer size = 25.50 MiB -llama_kv_cache: HTP0:2 KV buffer size = 25.50 MiB -llama_kv_cache: size = 102.00 MiB ( 8192 cells, 12 layers, 1/1 seqs), K (q8_0): 51.00 MiB, V (q8_0): 51.00 MiB -llama_kv_cache_iswa: creating SWA KV cache, size = 256 cells -llama_kv_cache: HTP0:1 KV buffer size = 0.80 MiB -llama_kv_cache: HTP0:3 KV buffer size = 0.53 MiB -llama_kv_cache: HTP0:0 KV buffer size = 1.06 MiB -llama_kv_cache: HTP0:2 KV buffer size = 0.80 MiB -llama_kv_cache: size = 3.19 MiB ( 256 cells, 12 layers, 1/1 seqs), K (q8_0): 1.59 MiB, V (q8_0): 1.59 MiB -llama_context: HTP0:0 compute buffer size = 16.06 MiB -llama_context: HTP0:1 compute buffer size = 16.06 MiB -llama_context: HTP0:2 compute buffer size = 16.06 MiB -llama_context: HTP0:3 compute buffer size = 16.06 MiB -llama_context: CPU compute buffer size = 98.19 MiB -... -llama_perf_context_print: prompt eval time = 3843.67 ms / 197 tokens ( 19.51 ms per token, 51.25 tokens per second) -llama_perf_context_print: eval time = 1686.13 ms / 31 runs ( 54.39 ms per token, 18.39 tokens per second) -llama_perf_context_print: total time = 6266.30 ms / 228 tokens -llama_perf_context_print: graphs reused = 30 -llama_memory_breakdown_print: | memory breakdown [MiB] | total free self model context compute unaccounted | -llama_memory_breakdown_print: | - HTP0:0 (Hexagon) | 2048 = 2048 + ( 0 = 0 + 0 + 0) + 0 | -llama_memory_breakdown_print: | - HTP0:1 (Hexagon) | 2048 = 2048 + ( 0 = 0 + 0 + 0) + 0 | -llama_memory_breakdown_print: | - HTP0:2 (Hexagon) | 2048 = 2048 + ( 0 = 0 + 0 + 0) + 0 | -llama_memory_breakdown_print: | - HTP0:3 (Hexagon) | 2048 = 2048 + ( 0 = 0 + 0 + 0) + 0 | -llama_memory_breakdown_print: | - Host | 1476 = 1208 + 105 + 162 | + +### Element-Partitioned Operators + +For flat element-wise operations (such as reshape copies in +[`cpy-ops.c`](../../../ggml/src/ggml-hexagon/htp/cpy-ops.c)): +- Partition total linear elements N = ne0 * ne1 * ne2 * ne3 in 128-byte cache line chunks (`elems_per_line = (elem_size == 4) ? 32 : 64`). +- Requires strict 1D contiguity: + [`htp_tensor_is_contiguous(dst, elem_size)`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L28) + and 128-byte aligned destination pointer + [`htp_tensor_mdev_data_aligned(dst)`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L47). +- If contiguous and aligned, pass `elems_per_line` to + [`htp_tensor_mdev_partition`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L94); + otherwise pass 0 to trigger Device 0 fallback. + +### Single-Device Fallback (Device 0) + +- Fallback to Device 0 (`mdev.idx == 0`) when partitioning would cause cache line tearing or when work cannot be evenly distributed. +- Triggers: + 1. Destination tensor cannot be safely partitioned (`rows_per_chunk == 0` or non-contiguous/unaligned buffer). + 2. Total aligned chunks < `mdev_count`. +- Device 0 processes the entire tensor `[0, total_units)`. +- Devices 1 ... N-1 receive `count = 0` and return `HTP_STATUS_OK` immediately. + +### Flatten Outer Dimensions Globally + +- **Never partition solely on `ne01` (dimension 1).** +- Partitioning only on `ne01` repeats the device boundary across every 2D slice (`ne02`, `ne03`). If each 2D slice is small, + false sharing occurs repeatedly throughout the tensor. +- Always flatten outer dimensions globally: `total_rows = ne01 * ne02 * ne03` and partition once across the combined row space. + +### Stateless Starting Coordinates + +- Do not use incremental state variables across slices that assume the thread or device starts at index 0. +- Precompute starting multidimensional coordinates at `r = row_start` (or `e = elem_start`) once using `fastdiv`. +- In inner loops, step base pointers directly (`ptr += stride`) or reset/wrap coordinates explicitly (`if (++i01 == ne01) { ... }`). + +### Clean Range Encapsulation + +- Initialize single-device default ranges at declaration: + + ```c + uint32_t row_start = 0; + uint32_t nrows = total_rows; + ``` + +- Encapsulate all multi-device logic inside `if (octx->ctx->mdev.count > 1)`. If the block is omitted or compiled out, + the operator runs standard single-device execution untouched. +- Do not propagate `mdev_` prefixes to worker functions or context structs. Worker threads are device-agnostic and + should only receive standard range parameters (`ctx.row_start`, `ctx.nrows`). +- In worker threads, calculate row intervals using standard arithmetic: + + ```c + const uint32_t ir0 = ctx->row_start + dr * ith; + const uint32_t ir1 = MIN(ir0 + dr, ctx->row_start + ctx->nrows); + ``` + + In single-device mode (`row_start == 0`), this naturally simplifies to `dr * ith` and `MIN(ir0 + dr, ctx->nrows)` with zero overhead. + +## Multi-Device Synchronization + +Multi-device execution synchronizes worker sessions across devices using explicit barriers and tensor cache flushing. + +### Synchronization Fence Protocol + +Multi-device execution synchronizes worker sessions through atomic fence slots and barriers defined in +[`htp-fence.h`](../../../ggml/src/ggml-hexagon/htp/htp-fence.h): + ``` +[NPU Session 0] [NPU Session 1] + | | + (Input Prep) (Input Prep) + | | + Pre-Op Barrier ----------------------------- Pre-Op Barrier + (mdev_sync_fence) (mdev_sync_fence) + | | + Kernel Execution Kernel Execution + (Output Slice 0) (Output Slice 1) + | | + Tensor Cache Flush Tensor Cache Flush + (htp_tensor_flush_all) (htp_tensor_flush_all) + | | + Post-Op/Batch Barrier ---------------------- Post-Op/Batch Barrier + (htp_mdev_group_barrier) (htp_mdev_group_barrier) + | | + Return Response to Host Return Response to Host +``` + +### Atomic Fence Slots and Cache Invalidation + +- Fence synchronization operates on dedicated RPCMEM shared memory mapped across all participating sessions (`ctx->mdev.fence_base`). +- Each device owns a dedicated 128-byte cache-line aligned fence slot: + + ```c + atomic_uint * my_fence = htp_mdev_fence_slot(fence_base, mdev_idx); + ``` + +- **Writing to fence ([`htp_fence_write`](../../../ggml/src/ggml-hexagon/htp/htp-fence.h#L18))**: + Stores `seq` and `status`, issues a `syncht` thread synchronization barrier, and flushes/invalidates the line + using `Q6_dccleaninva_A(fence)`. +- **Reading from peer fence ([`htp_fence_read`](../../../ggml/src/ggml-hexagon/htp/htp-fence.h#L26))**: + Executes `Q6_dccleaninva_A(fence)` and `syncht` before reading atomic values to ensure fresh data from DDR. + +### Deterministic Monotonic Sequence Numbers + +- Barrier fences use monotonically increasing sequence numbers: + + ```c + const uint32_t seq = ++ctx->mdev.fence_seq; + ``` + +- Comparing sequence numbers with signed arithmetic `(int32_t)(peer_seq - seq) >= 0` prevents race conditions or + misaligned barrier arrivals across iterations. +- If any peer reports an error status (`peer_status > HTP_STATUS_OK`), the barrier propagates the error and unblocks immediately. + +### Tensor Cache Flush and Pipeline Completion + +- In the kernel, ensure all pushed DMA operations have been popped in strict FIFO order to drain the queue. +- Use [`htp_tensor_flush_all()`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h) to flush specific dirty tensors back to DDR: + - [`htp_tensor_flush_all()`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h) flushes only modified tensor address ranges, + ensuring peer devices and the host CPU observe consistent data in DDR. +- Never signal completion before all DMA transfers are drained and dirty tensor flushes have completed. + diff --git a/docs/build.md b/docs/build.md index 28dcbc2e53ea..70fc17af2402 100644 --- a/docs/build.md +++ b/docs/build.md @@ -806,7 +806,7 @@ To read documentation for how to build on Android, [click here](./android.md) ## WebGPU -The WebGPU backend relies on [Dawn](https://dawn.googlesource.com/dawn). Follow the instructions [here](https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/quickstart-cmake.md) to install Dawn locally so that llama.cpp can find it using CMake. The current implementation is up-to-date with Dawn commit `18eb229`. +The WebGPU backend relies on [Dawn](https://dawn.googlesource.com/dawn). Follow the instructions [here](https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/quickstart-cmake.md) to install Dawn locally so that llama.cpp can find it using CMake. The current implementation is up-to-date with Dawn commit `94c3c9c`. In the llama.cpp directory, build with CMake: diff --git a/docs/development/parsing.md b/docs/development/parsing.md index a41057db2b8a..0cb372eca56a 100644 --- a/docs/development/parsing.md +++ b/docs/development/parsing.md @@ -28,7 +28,7 @@ auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { for (const auto & tool : tools) { const auto & function = tool.at("function"); std::string name = function.at("name"); - const auto & schema = function.at("parameters"); + const auto schema = common_chat_tool_parameters(function); auto tool_name = p.json_member("name", "\"" + p.literal(name) + "\""); auto tool_args = p.json_member("arguments", p.schema(p.json(), "tool-" + name + "-schema", schema)); @@ -108,6 +108,7 @@ For a more complete example, see `test_example_native()` in - **`rule(name, p, trigger)`** - Creates a named rule and returns a reference - **`trigger_rule(name, p)`** - Creates a trigger rule (entry point for lazy grammar generation) - **`schema(p, name, schema, raw)`** - Wraps parser with JSON schema metadata for grammar generation +- **`schema(p, name, doc, node, raw)`** - Same, for a node of a `common_chat_schema_document` built earlier, e.g. one tool parameter ### AST Control @@ -121,9 +122,6 @@ some exceptions. ```cpp data.grammar = build_grammar([&](const common_grammar_builder & builder) { - foreach_function(params.tools, [&](const json & fn) { - builder.resolve_refs(fn.at("parameters")); - }); parser.build_grammar(builder, data.grammar_lazy); }); ``` @@ -151,7 +149,8 @@ implementation to generate the grammar instead of the underlying parser. The `raw` option emits a grammar suitable for a raw string instead of a JSON string. In other words, it won't be wrapped in quotes or require escaping -quotes. It should only be used when `type == "string"`. +quotes. It only takes effect when the schema may be a string, as reported by +`common_chat_schema::may_be_string()`, otherwise the JSON grammar is used. The downside is that it can potentially lead to ambiguous grammars. For example, if a user provides the pattern `^.*$`, the following grammar may be diff --git a/docs/ops.md b/docs/ops.md index cc8d253820cc..ceb5d46cf819 100644 --- a/docs/ops.md +++ b/docs/ops.md @@ -44,7 +44,7 @@ Legend: | DSV4_HC_COMB | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | DSV4_HC_POST | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | DSV4_HC_PRE | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | -| DUP | ❌ | ✅ | ✅ | 🟡 | ❌ | ❌ | 🟡 | 🟡 | ✅ | ✅ | ❌ | ❌ | ❌ | +| DUP | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | 🟡 | 🟡 | ✅ | ✅ | ❌ | ❌ | ❌ | | ELU | ❌ | ✅ | ✅ | 🟡 | 🟡 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | | EXP | ❌ | ✅ | ✅ | 🟡 | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | | EXPM1 | ❌ | ❌ | ✅ | 🟡 | 🟡 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | diff --git a/docs/ops/CUDA.csv b/docs/ops/CUDA.csv index 22c84dd143d9..570bdf75ee56 100644 --- a/docs/ops/CUDA.csv +++ b/docs/ops/CUDA.csv @@ -5000,14 +5000,14 @@ "CUDA0","REPEAT_BACK","type=f32,ne=[8,6,4,2],nr=[1,1,1,2],v=1","support","1","yes","CUDA" "CUDA0","DUP","type=f32,ne=[10,10,20,1]","support","1","yes","CUDA" "CUDA0","DUP","type=f16,ne=[10,10,20,1]","support","1","yes","CUDA" -"CUDA0","DUP","type=i32,ne=[10,10,20,1]","support","0","no","CUDA" -"CUDA0","DUP","type=i16,ne=[10,10,20,1]","support","0","no","CUDA" +"CUDA0","DUP","type=i32,ne=[10,10,20,1]","support","1","yes","CUDA" +"CUDA0","DUP","type=i16,ne=[10,10,20,1]","support","1","yes","CUDA" "CUDA0","DUP","type=f32,ne=[10,10,5,1],permute=[0,2,1,3]","support","1","yes","CUDA" "CUDA0","DUP","type=f16,ne=[10,10,5,1],permute=[0,2,1,3]","support","1","yes","CUDA" "CUDA0","DUP","type=f32,ne=[10,10,5,1],permute=[1,0,2,3]","support","1","yes","CUDA" "CUDA0","DUP","type=f16,ne=[10,10,5,1],permute=[1,0,2,3]","support","1","yes","CUDA" -"CUDA0","DUP","type=i16,ne=[10,8,3,1],permute=[0,2,1,3]","support","0","no","CUDA" -"CUDA0","DUP","type=i16,ne=[10,8,3,1],permute=[1,2,0,3]","support","0","no","CUDA" +"CUDA0","DUP","type=i16,ne=[10,8,3,1],permute=[0,2,1,3]","support","1","yes","CUDA" +"CUDA0","DUP","type=i16,ne=[10,8,3,1],permute=[1,2,0,3]","support","1","yes","CUDA" "CUDA0","SET","type_src=f32,type_dst=f32,ne=[6,5,4,3],dim=1","support","1","yes","CUDA" "CUDA0","SET","type_src=f32,type_dst=f32,ne=[6,5,4,3],dim=2","support","1","yes","CUDA" "CUDA0","SET","type_src=f32,type_dst=f32,ne=[6,5,4,3],dim=3","support","1","yes","CUDA" diff --git a/examples/eval-callback/CMakeLists.txt b/examples/eval-callback/CMakeLists.txt index 63fbe59dce87..96e1e1b35327 100644 --- a/examples/eval-callback/CMakeLists.txt +++ b/examples/eval-callback/CMakeLists.txt @@ -18,7 +18,7 @@ if(LLAMA_BUILD_TESTS) -DDEST=${MODEL_DEST} -DNAME=${MODEL_NAME} -DHASH=${MODEL_HASH} - -P ${CMAKE_SOURCE_DIR}/cmake/download-models.cmake + -P ${PROJECT_SOURCE_DIR}/cmake/download-models.cmake ) set_tests_properties(${TEST_TARGET}-download-model PROPERTIES FIXTURES_SETUP ${TEST_TARGET}-download-model) add_test(NAME ${TEST_TARGET} COMMAND llama-eval-callback -m "${MODEL_DEST}" --prompt hello --seed 42 -ngl 0) diff --git a/examples/json_schema_to_grammar.py b/examples/json_schema_to_grammar.py deleted file mode 100755 index 02b7ef15ae98..000000000000 --- a/examples/json_schema_to_grammar.py +++ /dev/null @@ -1,842 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import itertools -import json -import re -import sys -from typing import Any, List, Optional, Set, Tuple, Union - -def _build_repetition(item_rule, min_items, max_items, separator_rule=None): - - if max_items == 0: - return "" - - if min_items == 0 and max_items == 1: - return f'{item_rule}?' - - if not separator_rule: - if min_items == 1 and max_items is None: - return f'{item_rule}+' - elif min_items == 0 and max_items is None: - return f'{item_rule}*' - else: - return f'{item_rule}{{{min_items},{max_items if max_items is not None else ""}}}' - - result = item_rule + ' ' + _build_repetition(f'({separator_rule} {item_rule})', min_items - 1 if min_items > 0 else 0, max_items - 1 if max_items is not None else None) - return f'({result})?' if min_items == 0 else result - -def _generate_min_max_int(min_value: Optional[int], max_value: Optional[int], out: list, decimals_left: int = 16, top_level: bool = True): - def digit_range(from_char: str, to_char: str): - out.append("[") - if from_char == to_char: - out.append(from_char) - else: - out.append(from_char) - out.append("-") - out.append(to_char) - out.append("]") - - def more_digits(min_digits: int, max_digits: int): - out.append("[0-9]") - if min_digits == max_digits and min_digits == 1: - return - out.append("{") - out.append(str(min_digits)) - if max_digits != min_digits: - out.append(",") - if max_digits != sys.maxsize: - out.append(str(max_digits)) - out.append("}") - - def uniform_range(from_str: str, to_str: str): - i = 0 - while i < len(from_str) and from_str[i] == to_str[i]: - i += 1 - if i > 0: - out.append("\"") - out.append(from_str[:i]) - out.append("\"") - if i < len(from_str): - if i > 0: - out.append(" ") - sub_len = len(from_str) - i - 1 - if sub_len > 0: - from_sub = from_str[i+1:] - to_sub = to_str[i+1:] - sub_zeros = "0" * sub_len - sub_nines = "9" * sub_len - - to_reached = False - out.append("(") - if from_sub == sub_zeros: - digit_range(from_str[i], chr(ord(to_str[i]) - 1)) - out.append(" ") - more_digits(sub_len, sub_len) - else: - out.append("[") - out.append(from_str[i]) - out.append("] ") - out.append("(") - uniform_range(from_sub, sub_nines) - out.append(")") - if ord(from_str[i]) < ord(to_str[i]) - 1: - out.append(" | ") - if to_sub == sub_nines: - digit_range(chr(ord(from_str[i]) + 1), to_str[i]) - to_reached = True - else: - digit_range(chr(ord(from_str[i]) + 1), chr(ord(to_str[i]) - 1)) - out.append(" ") - more_digits(sub_len, sub_len) - if not to_reached: - out.append(" | ") - digit_range(to_str[i], to_str[i]) - out.append(" ") - uniform_range(sub_zeros, to_sub) - out.append(")") - else: - out.append("[") - out.append(from_str[i]) - out.append("-") - out.append(to_str[i]) - out.append("]") - - if min_value is not None and max_value is not None: - if min_value < 0 and max_value < 0: - out.append("\"-\" (") - _generate_min_max_int(-max_value, -min_value, out, decimals_left, top_level=True) - out.append(")") - return - - if min_value < 0: - out.append("\"-\" (") - _generate_min_max_int(0, -min_value, out, decimals_left, top_level=True) - out.append(") | ") - min_value = 0 - - min_s = str(min_value) - max_s = str(max_value) - min_digits = len(min_s) - max_digits = len(max_s) - - for digits in range(min_digits, max_digits): - uniform_range(min_s, "9" * digits) - min_s = "1" + "0" * digits - out.append(" | ") - uniform_range(min_s, max_s) - return - - less_decimals = max(decimals_left - 1, 1) - - if min_value is not None: - if min_value < 0: - out.append("\"-\" (") - _generate_min_max_int(None, -min_value, out, decimals_left, top_level=False) - out.append(") | [0] | [1-9] ") - more_digits(0, decimals_left - 1) - elif min_value == 0: - if top_level: - out.append("[0] | [1-9] ") - more_digits(0, less_decimals) - else: - more_digits(1, decimals_left) - elif min_value <= 9: - c = str(min_value) - range_start = '1' if top_level else '0' - if c > range_start: - digit_range(range_start, chr(ord(c) - 1)) - out.append(" ") - more_digits(1, less_decimals) - out.append(" | ") - digit_range(c, "9") - out.append(" ") - more_digits(0, less_decimals) - else: - min_s = str(min_value) - length = len(min_s) - c = min_s[0] - - if c > "1": - digit_range("1" if top_level else "0", chr(ord(c) - 1)) - out.append(" ") - more_digits(length, less_decimals) - out.append(" | ") - digit_range(c, c) - out.append(" (") - _generate_min_max_int(int(min_s[1:]), None, out, less_decimals, top_level=False) - out.append(")") - if c < "9": - out.append(" | ") - digit_range(chr(ord(c) + 1), "9") - out.append(" ") - more_digits(length - 1, less_decimals) - return - - if max_value is not None: - if max_value >= 0: - if top_level: - out.append("\"-\" [1-9] ") - more_digits(0, less_decimals) - out.append(" | ") - _generate_min_max_int(0, max_value, out, decimals_left, top_level=True) - else: - out.append("\"-\" (") - _generate_min_max_int(-max_value, None, out, decimals_left, top_level=False) - out.append(")") - return - - raise RuntimeError("At least one of min_value or max_value must be set") - -class BuiltinRule: - def __init__(self, content: str, deps: list | None = None): - self.content = content - self.deps = deps or [] - -# Constraining spaces to prevent model "running away". -SPACE_RULE = '| " " | "\\n"{1,2} [ \\t]{0,20}' - -PRIMITIVE_RULES = { - 'boolean' : BuiltinRule('("true" | "false")', []), - 'decimal-part' : BuiltinRule('[0-9]{1,16}', []), - 'integral-part': BuiltinRule('[0] | [1-9] [0-9]{0,15}', []), - 'number' : BuiltinRule('("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)?', ['integral-part', 'decimal-part']), - 'integer' : BuiltinRule('("-"? integral-part)', ['integral-part']), - 'value' : BuiltinRule('object | array | string | number | boolean | null', ['object', 'array', 'string', 'number', 'boolean', 'null']), - 'object' : BuiltinRule('"{" space ( string ":" space value ("," space string ":" space value)* )? space "}"', ['string', 'value']), - 'array' : BuiltinRule('"[" space ( value ("," space value)* )? space "]"', ['value']), - 'uuid' : BuiltinRule(r'"\"" [0-9a-fA-F]{8} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{12} "\""', []), - 'char' : BuiltinRule(r'[^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})', []), - 'string' : BuiltinRule(r'"\"" char* "\""', ['char']), - 'null' : BuiltinRule('"null"', []), -} - -# TODO: support "uri", "email" string formats -STRING_FORMAT_RULES = { - 'date' : BuiltinRule('[0-9]{4} "-" ( "0" [1-9] | "1" [0-2] ) "-" ( \"0\" [1-9] | [1-2] [0-9] | "3" [0-1] )', []), - 'time' : BuiltinRule('([01] [0-9] | "2" [0-3]) ":" [0-5] [0-9] ":" [0-5] [0-9] ( "." [0-9]{3} )? ( "Z" | ( "+" | "-" ) ( [01] [0-9] | "2" [0-3] ) ":" [0-5] [0-9] )', []), - 'date-time' : BuiltinRule('date "T" time', ['date', 'time']), - 'date-string' : BuiltinRule('"\\"" date "\\""', ['date']), - 'time-string' : BuiltinRule('"\\"" time "\\""', ['time']), - 'date-time-string': BuiltinRule('"\\"" date-time "\\""', ['date-time']), -} - -DOTALL = '[\\U00000000-\\U0010FFFF]' -DOT = '[^\\x0A\\x0D]' - -RESERVED_NAMES = set(["root", "dot", *PRIMITIVE_RULES.keys(), *STRING_FORMAT_RULES.keys()]) - -INVALID_RULE_CHARS_RE = re.compile(r'[^a-zA-Z0-9-]+') -GRAMMAR_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"\\]') -GRAMMAR_RANGE_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"\]\-\\]') -GRAMMAR_LITERAL_ESCAPES = {'\r': '\\r', '\n': '\\n', '"': '\\"', '-': '\\-', ']': '\\]', '\\': '\\\\'} - -NON_LITERAL_SET = set('|.()[]{}*+?') -ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = set('^$.[]()|{}*+?') - - -class SchemaConverter: - def __init__(self, *, prop_order, allow_fetch, dotall, raw_pattern): - self._prop_order = prop_order - self._allow_fetch = allow_fetch - self._dotall = dotall - self._raw_pattern = raw_pattern - self._rules = { - 'space': SPACE_RULE, - } - self._refs = {} - self._refs_being_resolved = set() - - def _format_literal(self, literal): - escaped = GRAMMAR_LITERAL_ESCAPE_RE.sub( - lambda m: GRAMMAR_LITERAL_ESCAPES.get(m.group(0)) or m.group(0), literal - ) - return f'"{escaped}"' - - def not_literal(self, literal: str, dotall: bool = True, maybe_escaped_underscores = False) -> str: - ''' - not_literal('a') -> '[^a]' - not_literal('abc') -> '([^a] | "a" ([^b] | "b" ([^c])?)?)?' - ''' - assert len(literal) > 0, 'Empty literal not supported' - def recurse(i: int): - c = literal[i] - if maybe_escaped_underscores and c == '_': - yield f'[^{c}\\\\]' - yield ' | ' - yield f'"\\\\"? "{c}"' - else: - yield f'[^{c}]' - if i < len(literal) - 1: - yield ' | ' - yield self._format_literal(c) - yield ' (' - yield from recurse(i + 1) - yield ')?' - - return ''.join(('(', *recurse(0), ')')) - - def _not_strings(self, strings): - class TrieNode: - def __init__(self): - self.children = {} - self.is_end_of_string = False - - def insert(self, string): - node = self - for c in string: - node = node.children.setdefault(c, TrieNode()) - node.is_end_of_string = True - - trie = TrieNode() - for s in strings: - trie.insert(s) - - char_rule = self._add_primitive('char', PRIMITIVE_RULES['char']) - out = ['["] ( '] - - def visit(node): - rejects = [] - first = True - for c in sorted(node.children.keys()): - child = node.children[c] - rejects.append(c) - if first: - first = False - else: - out.append(' | ') - out.append(f'[{c}]') - if child.children: - out.append(f' (') - visit(child) - out.append(')') - elif child.is_end_of_string: - out.append(f' {char_rule}+') - if node.children: - if not first: - out.append(' | ') - out.append(f'[^"{"".join(rejects)}] {char_rule}*') - visit(trie) - - out.append(f' ){"" if trie.is_end_of_string else "?"} ["]') - return ''.join(out) - - def _add_rule(self, name, rule): - esc_name = INVALID_RULE_CHARS_RE.sub('-', name) - if esc_name not in self._rules or self._rules[esc_name] == rule: - key = esc_name - else: - i = 0 - while f'{esc_name}{i}' in self._rules and self._rules[f'{esc_name}{i}'] != rule: - i += 1 - key = f'{esc_name}{i}' - self._rules[key] = rule - return key - - def resolve_refs(self, schema: dict, url: str): - ''' - Resolves all $ref fields in the given schema, fetching any remote schemas, - replacing $ref with absolute reference URL and populating self._refs with the - respective referenced (sub)schema dictionaries. - ''' - def visit(n: dict): - if isinstance(n, list): - return [visit(x) for x in n] - elif isinstance(n, dict): - ref = n.get('$ref') - if ref is not None and ref not in self._refs: - if ref.startswith('https://'): - assert self._allow_fetch, 'Fetching remote schemas is not allowed (use --allow-fetch for force)' - import requests - - frag_split = ref.split('#') - base_url = frag_split[0] - - target = self._refs.get(base_url) - if target is None: - target = self.resolve_refs(requests.get(ref).json(), base_url) - self._refs[base_url] = target - - if len(frag_split) == 1 or frag_split[-1] == '': - return target - elif ref.startswith('#/'): - target = schema - ref = f'{url}{ref}' - n['$ref'] = ref - else: - raise ValueError(f'Unsupported ref {ref}') - - for sel in ref.split('#')[-1].split('/')[1:]: - assert target is not None, f'Error resolving ref {ref}: {sel} not in {target}' - if isinstance(target, list): - try: - sel_index = int(sel) - except ValueError: - raise ValueError(f'Error resolving ref {ref}: {sel} not in {target}') - assert 0 <= sel_index < len(target), f'Error resolving ref {ref}: {sel} not in {target}' - target = target[sel_index] - else: - assert sel in target, f'Error resolving ref {ref}: {sel} not in {target}' - target = target[sel] - - self._refs[ref] = target - else: - for v in n.values(): - visit(v) - - return n - return visit(schema) - - def _generate_union_rule(self, name, alt_schemas): - return ' | '.join(( - self.visit(alt_schema, f'{name}{"-" if name else "alternative-"}{i}') - for i, alt_schema in enumerate(alt_schemas) - )) - - def _visit_pattern(self, pattern, name): - ''' - Transforms a regular expression pattern into a GBNF rule. - - Input: https://json-schema.org/understanding-json-schema/reference/regular_expressions - Output: https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md - - Unsupported features: negative/positive lookaheads, greedy/non-greedy modifiers. - - Mostly a 1:1 translation, except for {x} / {x,} / {x,y} quantifiers for which - we define sub-rules to keep the output lean. - ''' - - assert pattern.startswith('^') and pattern.endswith('$'), 'Pattern must start with "^" and end with "$"' - pattern = pattern[1:-1] - sub_rule_ids = {} - - i = 0 - length = len(pattern) - - def to_rule(s: tuple[str, bool]) -> str: - (txt, is_literal) = s - return "\"" + txt + "\"" if is_literal else txt - - def transform() -> tuple[str, bool]: - ''' - Parse a unit at index i (advancing it), and return its string representation + whether it's a literal. - ''' - nonlocal i - nonlocal pattern - nonlocal sub_rule_ids - - start = i - # For each component of this sequence, store its string representation and whether it's a literal. - # We only need a flat structure here to apply repetition operators to the last item, and - # to merge literals at the and (we're parsing grouped ( sequences ) recursively and don't treat '|' specially - # (GBNF's syntax is luckily very close to regular expressions!) - seq: list[tuple[str, bool]] = [] - - def get_dot(): - if self._dotall: - rule = DOTALL - else: - # Accept any character... except \n and \r line break chars (\x0A and \xOD) - rule = DOT - return self._add_rule(f'dot', rule) - - def join_seq(): - nonlocal seq - ret = [] - for is_literal, g in itertools.groupby(seq, lambda x: x[1]): - if is_literal: - ret.append((''.join(x[0] for x in g), True)) - else: - ret.extend(g) - if len(ret) == 1: - return ret[0] - return (' '.join(to_rule(x) for x in seq), False) - - while i < length: - c = pattern[i] - if c == '.': - seq.append((get_dot(), False)) - i += 1 - elif c == '(': - i += 1 - if i < length: - assert pattern[i] != '?', f'Unsupported pattern syntax "{pattern[i]}" at index {i} of /{pattern}/' - seq.append((f'({to_rule(transform())})', False)) - elif c == ')': - i += 1 - assert start > 0 and pattern[start-1] == '(', f'Unbalanced parentheses; start = {start}, i = {i}, pattern = {pattern}' - return join_seq() - elif c == '[': - square_brackets = c - i += 1 - while i < length and pattern[i] != ']': - if pattern[i] == '\\': - square_brackets += pattern[i:i+2] - i += 2 - else: - square_brackets += pattern[i] - i += 1 - assert i < length, f'Unbalanced square brackets; start = {start}, i = {i}, pattern = {pattern}' - square_brackets += ']' - i += 1 - seq.append((square_brackets, False)) - elif c == '|': - seq.append(('|', False)) - i += 1 - elif c in ('*', '+', '?'): - seq[-1] = (to_rule(seq[-1]) + c, False) - i += 1 - elif c == '{': - curly_brackets = c - i += 1 - while i < length and pattern[i] != '}': - curly_brackets += pattern[i] - i += 1 - assert i < length, f'Unbalanced curly brackets; start = {start}, i = {i}, pattern = {pattern}' - curly_brackets += '}' - i += 1 - nums = [s.strip() for s in curly_brackets[1:-1].split(',')] - min_times = 0 - max_times = None - try: - if len(nums) == 1: - min_times = int(nums[0]) - max_times = min_times - else: - assert len(nums) == 2 - min_times = int(nums[0]) if nums[0] else 0 - max_times = int(nums[1]) if nums[1] else None - except ValueError: - raise ValueError(f'Invalid quantifier {curly_brackets} in /{pattern}/') - - (sub, sub_is_literal) = seq[-1] - - if not sub_is_literal: - id = sub_rule_ids.get(sub) - if id is None: - id = self._add_rule(f'{name}-{len(sub_rule_ids) + 1}', sub) - sub_rule_ids[sub] = id - sub = id - - seq[-1] = (_build_repetition(f'"{sub}"' if sub_is_literal else sub, min_times, max_times), False) - else: - literal = '' - while i < length: - if pattern[i] == '\\' and i < length - 1: - next = pattern[i + 1] - if next in ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS: - i += 1 - literal += pattern[i] - i += 1 - else: - literal += pattern[i:i+2] - i += 2 - elif pattern[i] == '"' and not self._raw_pattern: - literal += '\\"' - i += 1 - elif pattern[i] not in NON_LITERAL_SET and \ - (i == length - 1 or literal == '' or pattern[i+1] == '.' or pattern[i+1] not in NON_LITERAL_SET): - literal += pattern[i] - i += 1 - else: - break - if literal: - seq.append((literal, True)) - - return join_seq() - - return self._add_rule( - name, - to_rule(transform()) if self._raw_pattern \ - else "\"\\\"\" (" + to_rule(transform()) + ") \"\\\"\"") - - - def _resolve_ref(self, ref): - ref_fragment = ref.split('#')[-1] - ref_name = 'ref' + re.sub(r'[^a-zA-Z0-9-]+', '-', ref_fragment) - if ref_name not in self._rules and ref not in self._refs_being_resolved: - self._refs_being_resolved.add(ref) - resolved = self._refs[ref] - ref_name = self.visit(resolved, ref_name) - self._refs_being_resolved.remove(ref) - return ref_name - - def _generate_constant_rule(self, value): - return self._format_literal(json.dumps(value)) - - def visit(self, schema, name): - schema_type = schema.get('type') - schema_format = schema.get('format') - rule_name = name + '-' if name in RESERVED_NAMES else name or 'root' - - if (ref := schema.get('$ref')) is not None: - return self._add_rule(rule_name, self._resolve_ref(ref)) - - elif 'oneOf' in schema or 'anyOf' in schema: - return self._add_rule(rule_name, self._generate_union_rule(name, schema.get('oneOf') or schema['anyOf'])) - - elif isinstance(schema_type, list): - return self._add_rule(rule_name, self._generate_union_rule(name, [{**schema, 'type': t} for t in schema_type])) - - elif 'const' in schema: - return self._add_rule(rule_name, self._generate_constant_rule(schema['const'])) - - elif 'enum' in schema: - rule = '(' + ' | '.join((self._generate_constant_rule(v) for v in schema['enum'])) + ')' - return self._add_rule(rule_name, rule) - - elif schema_type in (None, 'object') and \ - ('properties' in schema or \ - ('additionalProperties' in schema and schema['additionalProperties'] is not True)): - required = set(schema.get('required', [])) - properties = list(schema.get('properties', {}).items()) - return self._add_rule(rule_name, self._build_object_rule(properties, required, name, schema.get('additionalProperties'))) - - elif schema_type in (None, 'object', 'string') and 'allOf' in schema: - required = set() - properties = [] - enum_sets = [] - hybrid_name = name - def add_component(comp_schema, is_required): - if (ref := comp_schema.get('$ref')) is not None: - comp_schema = self._refs[ref] - - if 'properties' in comp_schema: - for prop_name, prop_schema in comp_schema['properties'].items(): - properties.append((prop_name, prop_schema)) - if is_required: - required.add(prop_name) - - if 'enum' in comp_schema: - enum_sets.append(set(comp_schema['enum'])) - - for t in schema['allOf']: - if 'anyOf' in t: - for tt in t['anyOf']: - add_component(tt, is_required=False) - else: - add_component(t, is_required=True) - - if enum_sets: - enum_intersection = enum_sets[0] - for s in enum_sets[1:]: - enum_intersection &= s - - if enum_intersection: - rule = '(' + ' | '.join((self._generate_constant_rule(v) for v in sorted(enum_intersection))) + ')' - return self._add_rule(rule_name, rule) - - return self._add_rule(rule_name, self._build_object_rule(properties, required, hybrid_name, additional_properties=None)) - - elif schema_type in (None, 'array') and ('items' in schema or 'prefixItems' in schema): - items = schema.get('items', schema.get('prefixItems')) - if isinstance(items, list): - return self._add_rule( - rule_name, - '"[" space ' + - ' "," space '.join( - self.visit(item, f'{name}{"-" if name else ""}tuple-{i}') - for i, item in enumerate(items)) + - ' space "]"') - else: - item_rule_name = self.visit(items, f'{name}{"-" if name else ""}item') - min_items = schema.get("minItems", 0) - max_items = schema.get("maxItems") - return self._add_rule(rule_name, '"[" space ' + _build_repetition(item_rule_name, min_items, max_items, separator_rule='"," space') + ' space "]"') - - elif schema_type in (None, 'string') and 'pattern' in schema: - return self._visit_pattern(schema['pattern'], rule_name) - - elif schema_type in (None, 'string') and re.match(r'^uuid[1-5]?$', schema_format or ''): - return self._add_primitive( - 'root' if rule_name == 'root' else schema_format, - PRIMITIVE_RULES['uuid'] - ) - - elif schema_type in (None, 'string') and f'{schema_format}-string' in STRING_FORMAT_RULES: - prim_name = f'{schema_format}-string' - return self._add_rule(rule_name, self._add_primitive(prim_name, STRING_FORMAT_RULES[prim_name])) - - elif schema_type == 'string' and ('minLength' in schema or 'maxLength' in schema): - char_rule = self._add_primitive('char', PRIMITIVE_RULES['char']) - min_len = schema.get('minLength', 0) - max_len = schema.get('maxLength') - - return self._add_rule(rule_name, r'"\"" ' + _build_repetition(char_rule, min_len, max_len) + r' "\""') - - elif schema_type in (None, 'integer') and \ - ('minimum' in schema or 'exclusiveMinimum' in schema or 'maximum' in schema or 'exclusiveMaximum' in schema): - min_value = None - max_value = None - if 'minimum' in schema: - min_value = schema['minimum'] - elif 'exclusiveMinimum' in schema: - min_value = schema['exclusiveMinimum'] + 1 - if 'maximum' in schema: - max_value = schema['maximum'] - elif 'exclusiveMaximum' in schema: - max_value = schema['exclusiveMaximum'] - 1 - - out = ["("] - _generate_min_max_int(min_value, max_value, out) - out.append(")") - return self._add_rule(rule_name, ''.join(out)) - - elif (schema_type == 'object') or (len(schema) == 0): - return self._add_rule(rule_name, self._add_primitive('object', PRIMITIVE_RULES['object'])) - - elif schema_type is None and isinstance(schema, dict): - # No type constraint and no recognized structural keywords (e.g. {"description": "..."}). - # Per JSON Schema semantics this is equivalent to {} and accepts any value. - return self._add_rule(rule_name, self._add_primitive('value', PRIMITIVE_RULES['value'])) - - else: - assert schema_type in PRIMITIVE_RULES, f'Unrecognized schema: {schema}' - # TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero - return self._add_primitive('root' if rule_name == 'root' else schema_type, PRIMITIVE_RULES[schema_type]) - - def _add_primitive(self, name: str, rule: BuiltinRule): - n = self._add_rule(name, rule.content) - - for dep in rule.deps: - dep_rule = PRIMITIVE_RULES.get(dep) or STRING_FORMAT_RULES.get(dep) - assert dep_rule, f'Rule {dep} not known' - if dep not in self._rules: - self._add_primitive(dep, dep_rule) - return n - - def _build_object_rule(self, properties: List[Tuple[str, Any]], required: Set[str], name: str, additional_properties: Optional[Union[bool, Any]]): - prop_order = self._prop_order - # sort by position in prop_order (if specified) then by original order - sorted_props = [kv[0] for _, kv in sorted(enumerate(properties), key=lambda ikv: (prop_order.get(ikv[1][0], len(prop_order)), ikv[0]))] - - prop_kv_rule_names = {} - for prop_name, prop_schema in properties: - prop_rule_name = self.visit(prop_schema, f'{name}{"-" if name else ""}{prop_name}') - prop_kv_rule_names[prop_name] = self._add_rule( - f'{name}{"-" if name else ""}{prop_name}-kv', - fr'{self._format_literal(json.dumps(prop_name))} space ":" space {prop_rule_name}' - ) - required_props = [k for k in sorted_props if k in required] - optional_props = [k for k in sorted_props if k not in required] - - if additional_properties is not None and additional_properties != False: - sub_name = f'{name}{"-" if name else ""}additional' - value_rule = self.visit(additional_properties, f'{sub_name}-value') if isinstance(additional_properties, dict) else \ - self._add_primitive('value', PRIMITIVE_RULES['value']) - key_rule = self._add_primitive('string', PRIMITIVE_RULES['string']) if not sorted_props \ - else self._add_rule(f'{sub_name}-k', self._not_strings(sorted_props)) - - prop_kv_rule_names["*"] = self._add_rule( - f'{sub_name}-kv', - f'{key_rule} ":" space {value_rule}' - ) - optional_props.append("*") - - if not required_props and not optional_props: - return '"{" space "}"' - - rule = '"{" space ' - rule += ' "," space '.join(prop_kv_rule_names[k] for k in required_props) - - if optional_props: - rule += ' (' - if required_props: - rule += ' "," space ( ' - - def get_recursive_refs(ks, first_is_optional): - [k, *rest] = ks - kv_rule_name = prop_kv_rule_names[k] - comma_ref = f'( "," space {kv_rule_name} )' - if first_is_optional: - res = comma_ref + ('*' if k == '*' else '?') - else: - res = kv_rule_name + (' ' + comma_ref + "*" if k == '*' else '') - if len(rest) > 0: - res += ' ' + self._add_rule( - f'{name}{"-" if name else ""}{k}-rest', - get_recursive_refs(rest, first_is_optional=True) - ) - return res - - rule += ' | '.join( - get_recursive_refs(optional_props[i:], first_is_optional=False) - for i in range(len(optional_props)) - ) - if required_props: - rule += ' )' - rule += ' )?' - - rule += ' space "}"' - - return rule - - def format_grammar(self): - return '\n'.join( - f'{name} ::= {rule}' - for name, rule in sorted(self._rules.items(), key=lambda kv: kv[0]) - ) - - -def main(args_in = None): - parser = argparse.ArgumentParser( - description=''' - Generates a grammar (suitable for use in ./llama-cli) that produces JSON conforming to a - given JSON schema. Only a subset of JSON schema features are supported; more may be - added in the future. - ''', - ) - parser.add_argument( - '--prop-order', - default=[], - type=lambda s: s.split(','), - help=''' - comma-separated property names defining the order of precedence for object properties; - properties not specified here are given lower precedence than those that are, and - are kept in their original order from the schema. Required properties are always - given precedence over optional properties. - ''' - ) - parser.add_argument( - '--allow-fetch', - action='store_true', - default=False, - help='Whether to allow fetching referenced schemas over HTTPS') - parser.add_argument( - '--dotall', - action='store_true', - default=False, - help='Whether to treat dot (".") as matching all chars including line breaks in regular expression patterns') - parser.add_argument( - '--raw-pattern', - action='store_true', - default=False, - help='Treats string patterns as raw patterns w/o quotes (or quote escapes)') - - parser.add_argument('schema', help='file containing JSON schema ("-" for stdin)') - args = parser.parse_args(args_in) - - if args.schema.startswith('https://'): - url = args.schema - import requests - schema = requests.get(url).json() - elif args.schema == '-': - url = 'stdin' - schema = json.load(sys.stdin) - else: - url = f'file://{args.schema}' - with open(args.schema) as f: - schema = json.load(f) - converter = SchemaConverter( - prop_order={name: idx for idx, name in enumerate(args.prop_order)}, - allow_fetch=args.allow_fetch, - dotall=args.dotall, - raw_pattern=args.raw_pattern) - schema = converter.resolve_refs(schema, url) - converter.visit(schema, '') - print(converter.format_grammar()) - - -if __name__ == '__main__': - main() diff --git a/examples/regex_to_grammar.py b/examples/regex_to_grammar.py deleted file mode 100644 index 5cd9210a4dfc..000000000000 --- a/examples/regex_to_grammar.py +++ /dev/null @@ -1,20 +0,0 @@ -import json, subprocess, sys, os - -assert len(sys.argv) >= 2 -[_, pattern, *rest] = sys.argv - -print(subprocess.check_output( - [ - "python", - os.path.join( - os.path.dirname(os.path.realpath(__file__)), - "json_schema_to_grammar.py"), - *rest, - "-", - "--raw-pattern", - ], - text=True, - input=json.dumps({ - "type": "string", - "pattern": pattern, - }, indent=2))) diff --git a/examples/test-cmake/.gitignore b/examples/test-cmake/.gitignore index 0ddff317a4b5..b630ddb7d62f 100644 --- a/examples/test-cmake/.gitignore +++ b/examples/test-cmake/.gitignore @@ -1,3 +1,4 @@ llama-build-install install build +build-subdir diff --git a/examples/test-cmake/CMakeLists.txt b/examples/test-cmake/CMakeLists.txt index ed5cb1f3c262..6ceb3359e526 100644 --- a/examples/test-cmake/CMakeLists.txt +++ b/examples/test-cmake/CMakeLists.txt @@ -3,11 +3,20 @@ project(llama-simple) set(CMAKE_CXX_STANDARD 17) -find_package(llama 0.1.0 REQUIRED) +option(LLAMA_TEST_USE_SUBDIR "Use add_subdirectory instead of find_package" OFF) + +if(LLAMA_TEST_USE_SUBDIR) + add_subdirectory(../../ llama.cpp) +else() + find_package(llama 0.1.0 REQUIRED) +endif() add_executable(test-cmake test-cmake.cpp) target_link_libraries(test-cmake PRIVATE llama) -target_compile_definitions(test-cmake PRIVATE - LLAMA_BUILD_NUMBER=${LLAMA_BUILD_NUMBER} - LLAMA_BUILD_COMMIT="${LLAMA_BUILD_COMMIT}" -) + +if(DEFINED LLAMA_BUILD_NUMBER) + target_compile_definitions(test-cmake PRIVATE + LLAMA_BUILD_NUMBER=${LLAMA_BUILD_NUMBER} + LLAMA_BUILD_COMMIT="${LLAMA_BUILD_COMMIT}" + ) +endif() diff --git a/examples/test-cmake/README.md b/examples/test-cmake/README.md index 2f6a2fcfe9c3..03895abfb933 100644 --- a/examples/test-cmake/README.md +++ b/examples/test-cmake/README.md @@ -5,17 +5,18 @@ enable troubleshooting issues and exploration. The idea is that this can be used after making changes to llama.cpp installation cmake configuration and then verify it locally. -### Usage -The following will configure, build, and install llama.cpp +### find_package +The following will configure, build, and install llama.cpp, and the build a +project that uses find_package to use the installation. Configuring/build/install: ```console ./build-install.sh ``` The above command will create a directory named `install` in the current directory -which will have the follwing files in its lib directory: +which will have the following files in its lib directory: ```console -(venv) $ ls install/lib/ +$ ls install/lib/ cmake libggml.so libllama-common.so.0 libllama.so.0.1.0 llama.cpp libggml-base.so libggml.so.0 libllama-common.so.0.1.0 libmtmd.so pkgconfig libggml-base.so.0 libggml.so.0.19.0 libllama.so libmtmd.so.0 @@ -24,7 +25,7 @@ libggml-base.so.0.19.0 libllama-common.so libllama.so.0 libmtmd.so Build/run this project using the installation created above: ```console -(venv) $ ./build.sh +$ ./build.sh -- Configuring done (0.0s) -- Generating done (0.0s) -- Build files have been written to: /path/to/llama.cpp/examples/test-cmake/build @@ -34,3 +35,11 @@ Build/run this project using the installation created above: load_backend: loaded CPU backend from /path/to/llama.cpp/examples/test-cmake/install/lib/llama.cpp/libggml-cpu-alderlake.so [test-cmake] Backend initialized. ``` + +### add_subdirectory +The following will use add_subdirectory to include llama.cpp in a cmake project +and is intended to simulate projects that build llama.cpp in this way. + +```console +$ USE_SUBDIR=ON ./build.sh +``` diff --git a/examples/test-cmake/build.sh b/examples/test-cmake/build.sh index a212732b89d9..869a64160e6c 100755 --- a/examples/test-cmake/build.sh +++ b/examples/test-cmake/build.sh @@ -2,6 +2,17 @@ set -e -cmake -S . -B build -DCMAKE_PREFIX_PATH="${PWD}/install" -cmake --build build -LD_LIBRARY_PATH="${PWD}/install/lib/llama.cpp:${PWD}/install/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" ./build/test-cmake +if [ "${USE_SUBDIR:-OFF}" = "ON" ]; then + BUILD_DIR="build-subdir" + CMAKE_ARGS="-DLLAMA_TEST_USE_SUBDIR=ON -DLLAMA_BUILD_COMMON=ON -DLLAMA_BUILD_TOOLS=ON -DLLAMA_BUILD_SERVER=ON-DLLAMA_BUILD_TESTS=ON" + LIB_PATH="${PWD}/${BUILD_DIR}/bin" +else + BUILD_DIR="build" + CMAKE_ARGS="-DCMAKE_PREFIX_PATH=${PWD}/install" + LIB_PATH="${PWD}/install/lib/llama.cpp" +fi + +cmake --fresh -S . -B "${BUILD_DIR}" ${CMAKE_ARGS} +cmake --build "${BUILD_DIR}" -j 8 + +LD_LIBRARY_PATH="${LIB_PATH}:${PWD}/install/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "./${BUILD_DIR}/test-cmake" diff --git a/examples/test-cmake/test-cmake.cpp b/examples/test-cmake/test-cmake.cpp index dc1a9ae605a7..fea27c7e87b4 100644 --- a/examples/test-cmake/test-cmake.cpp +++ b/examples/test-cmake/test-cmake.cpp @@ -2,8 +2,12 @@ #include <cstdio> int main(void) { +#ifdef LLAMA_BUILD_NUMBER printf("[test-cmake] llama.cpp version: %s, build: %d (%s)\n", llama_version(), LLAMA_BUILD_NUMBER, LLAMA_BUILD_COMMIT); +#else + printf("[test-cmake] llama.cpp version: %s\n", llama_version()); +#endif printf("[test-cmake] ggml version: %s, commit: %s\n", ggml_version(), ggml_commit()); printf("[test-cmake] Initializing backend...\n"); llama_backend_init(); diff --git a/examples/ts-type-to-grammar.sh b/examples/ts-type-to-grammar.sh deleted file mode 100755 index 966050407888..000000000000 --- a/examples/ts-type-to-grammar.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# -# ./examples/ts-type-to-grammar.sh "{a:string,b:string,c?:string}" -# python examples/json_schema_to_grammar.py https://json.schemastore.org/tsconfig.json -# -set -euo pipefail - -readonly type="$1" - -# Create a temporary directory -TMPDIR="" -trap 'rm -fR "$TMPDIR"' EXIT -TMPDIR=$(mktemp -d) - -DTS_FILE="$TMPDIR/type.d.ts" -SCHEMA_FILE="$TMPDIR/schema.json" - -echo "export type MyType = $type" > "$DTS_FILE" - -# This is a fork of typescript-json-schema, actively maintained as of March 2024: -# https://github.com/vega/ts-json-schema-generator -npx ts-json-schema-generator --unstable --no-top-ref --path "$DTS_FILE" --type MyType -e none > "$SCHEMA_FILE" - -# Alternative, not actively maintained as of March 2024: -# https://github.com/YousefED/typescript-json-schema -# npx typescript-json-schema --defaultProps --required "$DTS_FILE" MyType | tee "$SCHEMA_FILE" >&2 - -./examples/json_schema_to_grammar.py "$SCHEMA_FILE" diff --git a/ggml/CMakeLists.txt b/ggml/CMakeLists.txt index ba9bc83b9b08..7f8a1f70616f 100644 --- a/ggml/CMakeLists.txt +++ b/ggml/CMakeLists.txt @@ -4,7 +4,7 @@ project("ggml" C CXX ASM) ### GGML Version set(GGML_VERSION_MAJOR 0) -set(GGML_VERSION_MINOR 23) +set(GGML_VERSION_MINOR 24) set(GGML_VERSION_PATCH 0) set(GGML_VERSION_BASE "${GGML_VERSION_MAJOR}.${GGML_VERSION_MINOR}.${GGML_VERSION_PATCH}") diff --git a/ggml/include/ggml-rpc.h b/ggml/include/ggml-rpc.h index cbfe400139cf..1f8cb7906cb2 100644 --- a/ggml/include/ggml-rpc.h +++ b/ggml/include/ggml-rpc.h @@ -6,7 +6,7 @@ extern "C" { #endif -#define RPC_PROTO_MAJOR_VERSION 6 +#define RPC_PROTO_MAJOR_VERSION 7 #define RPC_PROTO_MINOR_VERSION 0 #define RPC_PROTO_PATCH_VERSION 0 diff --git a/ggml/src/ggml-cpu/CMakeLists.txt b/ggml/src/ggml-cpu/CMakeLists.txt index 83088e147133..1c7338eea49c 100644 --- a/ggml/src/ggml-cpu/CMakeLists.txt +++ b/ggml/src/ggml-cpu/CMakeLists.txt @@ -675,12 +675,6 @@ function(ggml_add_cpu_backend_variant_impl tag_name) target_compile_options(${GGML_CPU_NAME} PRIVATE ${ARCH_FLAGS}) target_compile_definitions(${GGML_CPU_NAME} PRIVATE ${ARCH_DEFINITIONS}) - if (CMAKE_C_COMPILER_ID STREQUAL "GNU" AND NOT GGML_SYSTEM_ARCH STREQUAL "x86") - message(STATUS "Skipping PCH for ${GGML_CPU_NAME}: GCC PCH is only enabled for x86 (arch: ${GGML_SYSTEM_ARCH})") - else() - target_precompile_headers(${GGML_CPU_NAME} PRIVATE ggml-impl.h) - endif() - if (EMSCRIPTEN) set_target_properties(${GGML_CPU_NAME} PROPERTIES COMPILE_FLAGS "-msimd128") endif() diff --git a/ggml/src/ggml-cpu/arch/s390/quants.c b/ggml/src/ggml-cpu/arch/s390/quants.c index 70f2882d830d..52344828e39d 100644 --- a/ggml/src/ggml-cpu/arch/s390/quants.c +++ b/ggml/src/ggml-cpu/arch/s390/quants.c @@ -417,6 +417,7 @@ void ggml_vec_dot_mxfp4_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const vo sumf = vec_hsum_f32x4(v_acc); *s = sumf; #else + UNUSED(nb); UNUSED(x); UNUSED(y); UNUSED(ib); diff --git a/ggml/src/ggml-cpu/arch/s390/repack.cpp b/ggml/src/ggml-cpu/arch/s390/repack.cpp index 3990a6b0487e..abf3433adda2 100644 --- a/ggml/src/ggml-cpu/arch/s390/repack.cpp +++ b/ggml/src/ggml-cpu/arch/s390/repack.cpp @@ -70,6 +70,7 @@ void ggml_quantize_mat_q8_0_4x4(const float * GGML_RESTRICT x, void * GGML_RESTR #endif } +#if defined(__VXE__) || defined(__VXE2__) static inline int16x8_t vxe_dot_acc(const int8x16_t v_x, const int8x16_t v_y, const int16x8_t v_acc) { return vec_meadd(v_x, v_y, vec_moadd(v_x, v_y, v_acc)); } @@ -84,6 +85,7 @@ static inline int32x4_t vxe_fold(const int16x8_t v_sumi) { const int16x8_t v_ones = vec_splats((int16_t)1); return vec_add(vec_mule(v_sumi, v_ones), vec_mulo(v_sumi, v_ones)); } +#endif void ggml_gemv_q4_0_4x4_q8_0(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, const void * GGML_RESTRICT vy, int nr, int nc) { const int qk = QK8_0; diff --git a/ggml/src/ggml-cpu/ops.h b/ggml/src/ggml-cpu/ops.h index ce2b3e870b91..2728b08b6c97 100644 --- a/ggml/src/ggml-cpu/ops.h +++ b/ggml/src/ggml-cpu/ops.h @@ -5,10 +5,10 @@ // // cache line // - -#if defined(__cpp_lib_hardware_interference_size) -#define CACHE_LINE_SIZE std::hardware_destructive_interference_size -#else +// TODO: rework CACHE_LINE_SIZE so std::hardware_destructive_interference_size +// can be used consistently between C and C++ TUs; the previous macro form +// diverged based on include order and undersized the work buffer. +// ref: https://github.com/ggml-org/llama.cpp/pull/28882 #if defined(__POWER9_VECTOR__) #define CACHE_LINE_SIZE 128 #elif defined(__VXE__) || defined(__VXE2__) @@ -16,17 +16,8 @@ #else #define CACHE_LINE_SIZE 64 #endif -#endif -// -Winterference-size was introduced in GCC 12 -#if defined(__cplusplus) && defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12 -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Winterference-size" -#endif static const size_t CACHE_LINE_SIZE_F32 = CACHE_LINE_SIZE/sizeof(float); -#if defined(__cplusplus) && defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12 -#pragma GCC diagnostic pop -#endif // Work buffer size for im2col operations in CONV2D #define GGML_IM2COL_WORK_SIZE (16 * 1024 * 1024) diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index 7d14ce9067ee..2e78ae4facb9 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -329,6 +329,12 @@ static bool fp16_mma_hardware_available(const int cc) { (GGML_CUDA_CC_IS_MTHREADS(cc) && cc >= GGML_CUDA_CC_QY2); } +// To be used for feature selection of external libraries, e.g. cuBLAS. +static bool fast_bf16_hardware_available(const int cc) { + return (GGML_CUDA_CC_IS_AMD(cc) && (cc >= GGML_CUDA_CC_RDNA3 || GGML_CUDA_CC_IS_CDNA(cc))) + || (GGML_CUDA_CC_IS_NVIDIA(cc) && cc >= GGML_CUDA_CC_AMPERE); +} + static bool bf16_mma_hardware_available(const int cc) { return (GGML_CUDA_CC_IS_NVIDIA(cc) && cc >= GGML_CUDA_CC_AMPERE) || GGML_CUDA_CC_IS_CDNA(cc) || cc >= GGML_CUDA_CC_RDNA3 || diff --git a/ggml/src/ggml-cuda/cpy.cu b/ggml/src/ggml-cuda/cpy.cu index fd7ffc0bc557..7a998458540d 100644 --- a/ggml/src/ggml-cuda/cpy.cu +++ b/ggml/src/ggml-cuda/cpy.cu @@ -589,6 +589,14 @@ void ggml_cuda_cpy(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, gg ggml_cpy_scalar_cuda<int32_t, int32_t> (src0_ddc, src1_ddc, ne, ne00, ne01, ne02, nb00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb13, main_stream); } + } else if (src0->type == GGML_TYPE_I16 && src1->type == GGML_TYPE_I16) { + if (can_be_transposed) { + ggml_cpy_scalar_cuda<int16_t, int16_t, true> + (src0_ddc, src1_ddc, ne, ne00, ne01, ne02, nb00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb13, main_stream); + } else { + ggml_cpy_scalar_cuda<int16_t, int16_t> + (src0_ddc, src1_ddc, ne, ne00, ne01, ne02, nb00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb13, main_stream); + } } else if (src0->type == GGML_TYPE_F32 && src1->type == GGML_TYPE_I32) { if (contiguous_srcs) { ggml_cpy_scalar_contiguous_cuda<float, int32_t> diff --git a/ggml/src/ggml-cuda/fattn-mma-f16.cuh b/ggml/src/ggml-cuda/fattn-mma-f16.cuh index 578f6cf79c25..a290655776aa 100644 --- a/ggml/src/ggml-cuda/fattn-mma-f16.cuh +++ b/ggml/src/ggml-cuda/fattn-mma-f16.cuh @@ -181,7 +181,7 @@ static constexpr __host__ __device__ fattn_mma_config ggml_cuda_fattn_mma_get_co GGML_CUDA_FATTN_MMA_CONFIG_CASE( 64, 64, 8, 128, 1, 64, 32, 32, 32, 1, true); GGML_CUDA_FATTN_MMA_CONFIG_CASE( 64, 64, 16, 256, 2, 64, 32, 32, 32, 1, true); GGML_CUDA_FATTN_MMA_CONFIG_CASE( 64, 64, 32, 256, 2, 64, 32, 32, 32, 1, true); - GGML_CUDA_FATTN_MMA_CONFIG_CASE( 64, 64, 64, 256, 4, 64, 32, 32, 32, 1, true); + GGML_CUDA_FATTN_MMA_CONFIG_CASE( 64, 64, 64, 256, 3, 64, 32, 32, 32, 1, true); GGML_CUDA_FATTN_MMA_CONFIG_CASE( 80, 80, 8, 256, 2, 64, 40, 40, 40, 1, true); GGML_CUDA_FATTN_MMA_CONFIG_CASE( 80, 80, 16, 256, 2, 64, 40, 40, 40, 1, true); @@ -1141,7 +1141,7 @@ template<int DV, int ncols> struct mma_tile_sizes { using T_C_KQ = tile<16, 16, float>; // column-major using T_A_VKQ = tile<16, 8, half2>; // row-major using T_B_VKQ = tile<16, 8, half2>; // column-major - using T_C_VKQ = tile<16, 8, half2>; // column-major + using T_C_VKQ = tile<16, 16, float>; // column-major }; #else // Volta template<int DV, int ncols> struct mma_tile_sizes { @@ -1227,7 +1227,9 @@ static __device__ __forceinline__ void flash_attn_ext_f16_process_tile( T_C_VKQ VKQ_C[cols_per_warp == 8 ? DV/T_C_VKQ::I : DV/(2*T_C_VKQ::J)]; #elif defined(AMD_WMMA_AVAILABLE) && defined(RDNA3) T_C_VKQ VKQ_C[DV % 32 != 0 ? DV/T_C_VKQ::J : DV/(2*T_C_VKQ::J)]; -#elif defined(AMD_WMMA_AVAILABLE) || defined(AMD_MFMA_AVAILABLE) +#elif defined(AMD_MFMA_AVAILABLE) + T_C_VKQ VKQ_C[ DV/T_C_VKQ::J]; +#elif defined(AMD_WMMA_AVAILABLE) T_C_VKQ VKQ_C[ DV/(2*T_C_VKQ::J)]; #else // Volta T_C_VKQ VKQ_C[ DV/(2*T_C_VKQ::J)]; diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 501d3580c7ba..4d95fd96c30c 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -1620,11 +1620,19 @@ static void ggml_cuda_mul_mat_cublas_impl(ggml_backend_cuda_context & ctx, const } static void ggml_cuda_mul_mat_cublas(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { + const int cc = ggml_cuda_info().devices[ctx.device].cc; ggml_type compute_type = src0->type; if (ggml_is_quantized(compute_type)) { - compute_type = fast_fp16_hardware_available(ggml_cuda_info().devices[ctx.device].cc) ? GGML_TYPE_F16 : GGML_TYPE_F32; - } else if (compute_type == GGML_TYPE_F16 && !fast_fp16_hardware_available(ggml_cuda_info().devices[ctx.device].cc)) { + compute_type = fast_fp16_hardware_available(cc) ? GGML_TYPE_F16 : GGML_TYPE_F32; + } else if (compute_type == GGML_TYPE_F16 && !fast_fp16_hardware_available(cc)) { compute_type = GGML_TYPE_F32; + } else if (compute_type == GGML_TYPE_BF16 && !fast_bf16_hardware_available(cc)) { + if (GGML_CUDA_CC_IS_AMD(cc) && src1->ne[1] > 32) { + compute_type = GGML_TYPE_F32; + } + if (GGML_CUDA_CC_IS_NVIDIA(cc) && src1->ne[1] > (cc >= GGML_CUDA_CC_VOLTA ? 8 : 128)) { + compute_type = GGML_TYPE_F32; + } } if (dst->op_params[0] == GGML_PREC_F32) { compute_type = GGML_TYPE_F32; @@ -5316,10 +5324,7 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g return false; } break; case GGML_OP_DUP: - { - ggml_type src0_type = op->src[0]->type; - return src0_type != GGML_TYPE_I32 && src0_type != GGML_TYPE_I16; - } break; + return true; case GGML_OP_ARGMAX: case GGML_OP_COUNT_EQUAL: { @@ -5487,7 +5492,9 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g return true; #endif case GGML_OP_SUM_ROWS: + return op->src[0]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32 && ggml_is_contiguous_rows(op->src[0]); case GGML_OP_MEAN: + return op->src[0]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32 && ggml_is_contiguous_rows(op->src[0]); case GGML_OP_GROUP_NORM: return ggml_is_contiguous(op->src[0]); case GGML_OP_PAD: diff --git a/ggml/src/ggml-cuda/mean.cu b/ggml/src/ggml-cuda/mean.cu index a8f6046e46da..64ad7e1d534b 100644 --- a/ggml/src/ggml-cuda/mean.cu +++ b/ggml/src/ggml-cuda/mean.cu @@ -18,7 +18,7 @@ void ggml_cuda_op_mean(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { GGML_ASSERT(src0->type == GGML_TYPE_F32); GGML_ASSERT(dst->type == GGML_TYPE_F32); - GGML_ASSERT(ggml_is_contiguous(src0)); + GGML_ASSERT(ggml_is_contiguous_rows(src0)); const int64_t ncols = src0->ne[0]; const int64_t nrows = ggml_nrows(src0); @@ -65,13 +65,20 @@ void ggml_cuda_op_mean(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { // Heuristic for block size selection to optimize occupancy. // See discussion in: https://github.com/ggml-org/llama.cpp/pull/15132 + dim3 block_dims; if ((nrows / nsm) < 2) { - const dim3 block_dims(512, 1, 1); - const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, 0, stream); - ggml_cuda_kernel_launch(reduce_rows_f32</*norm=*/true>, launch_params, src0_d, dst_d, ncols); + block_dims = dim3(512, 1, 1); } else { - const dim3 block_dims(ncols < 1024 ? 32 : 128, 1, 1); - const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, 0, stream); + block_dims = dim3(ncols < 1024 ? 32 : 128, 1, 1); + } + const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, 0, stream); + + if (ggml_is_contiguous(src0)) { ggml_cuda_kernel_launch(reduce_rows_f32</*norm=*/true>, launch_params, src0_d, dst_d, ncols); + return; } + + const char * src0_d_bytes = (const char *) src0->data; + ggml_cuda_kernel_launch(reduce_rows_f32_strided</*norm=*/true>, launch_params, src0_d_bytes, dst_d, ncols, + src0->ne[1], src0->ne[2], src0->nb[1], src0->nb[2], src0->nb[3]); } diff --git a/ggml/src/ggml-cuda/mmq-config-gcn.cuh b/ggml/src/ggml-cuda/mmq-config-gcn.cuh new file mode 100644 index 000000000000..24af2ef2b05b --- /dev/null +++ b/ggml/src/ggml-cuda/mmq-config-gcn.cuh @@ -0,0 +1,281 @@ +static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_gcn(ggml_type type, int J, bool fallback) { + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q1_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q1_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q1_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_1, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q5_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_0, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_0, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_0, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q5_1, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_1, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_1, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_1, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_1, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_1, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 512, 2, 64, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q8_0, 512, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + +// --------------------------------------------------------------------------------------------- + + CASE(GGML_TYPE_Q2_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_K, 512, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_K, 512, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q2_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 512, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 512, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 512, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 512, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q2_K, 512, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q3_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q3_K, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q3_K, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q3_K, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q3_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q3_K, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q4_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_K, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q5_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_K, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q5_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 256, 3, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q5_K, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q6_K, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false); + +// --------------------------------------------------------------------------------------------- + + CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ1_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ1_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ1_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XXS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XXS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XXS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_XS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ2_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_XXS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_XXS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_XXS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ3_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_XS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_XS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_XS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_NL, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_NL, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_IQ4_NL, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false); + +// --------------------------------------------------------------------------------------------- + + CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_MXFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_MXFP4, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_MXFP4, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_MXFP4, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + + CASE(GGML_TYPE_NVFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_NVFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_NVFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_NVFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_NVFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_NVFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_NVFP4, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_NVFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_NVFP4, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_NVFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_NVFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false); + + return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true); +} diff --git a/ggml/src/ggml-cuda/mmq.cuh b/ggml/src/ggml-cuda/mmq.cuh index 24afedd1432b..6923f3510c03 100644 --- a/ggml/src/ggml-cuda/mmq.cuh +++ b/ggml/src/ggml-cuda/mmq.cuh @@ -218,6 +218,7 @@ struct ggml_cuda_mmq_config { #include "mmq-config-ampere.cuh" #include "mmq-config-blackwell.cuh" +#include "mmq-config-gcn.cuh" #include "mmq-config-cdna.cuh" #include "mmq-config-rdna2.cuh" #include "mmq-config-rdna3.cuh" @@ -228,6 +229,9 @@ struct ggml_cuda_mmq_config { static __host__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config(const ggml_type type, const int J, const bool fallback, const int cc) { if (GGML_CUDA_CC_IS_AMD(cc)) { + if (GGML_CUDA_CC_IS_GCN(cc)) { + return ggml_cuda_mmq_get_config_gcn(type, J, fallback); + } if (GGML_CUDA_CC_IS_CDNA(cc)) { return ggml_cuda_mmq_get_config_cdna(type, J, fallback); } @@ -256,7 +260,9 @@ static __host__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config(const ggml_type ty static constexpr __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config(ggml_type type, int J, bool fallback) { #ifdef GGML_USE_HIP -#ifdef CDNA +#ifdef GCN + return ggml_cuda_mmq_get_config_gcn(type, J, fallback); +#elif defined(CDNA) return ggml_cuda_mmq_get_config_cdna(type, J, fallback); #elif defined(RDNA4) return ggml_cuda_mmq_get_config_rdna4(type, J, fallback); diff --git a/ggml/src/ggml-cuda/reduce_rows.cuh b/ggml/src/ggml-cuda/reduce_rows.cuh index 968c47aa20a1..111fd838a7a3 100644 --- a/ggml/src/ggml-cuda/reduce_rows.cuh +++ b/ggml/src/ggml-cuda/reduce_rows.cuh @@ -1,11 +1,6 @@ #include "common.cuh" -// Row reduction kernel template - compute sum (norm=false) or mean (norm=true) -template <bool norm> -static __global__ void reduce_rows_f32(const float * x_ptr, float * dst_ptr, const int ncols) { - const float * GGML_CUDA_RESTRICT x = x_ptr; - float * GGML_CUDA_RESTRICT dst = dst_ptr; - const int row = blockIdx.x; +static __device__ __forceinline__ float reduce_row_f32(const float * x, const int ncols) { const int col = threadIdx.x; float sum = 0.0f; @@ -17,7 +12,7 @@ static __global__ void reduce_rows_f32(const float * x_ptr, float * dst_ptr, con for (int i = col; i < ncols;) { for (int j = 0; j < num_unroll; ++j) { if (i < ncols) { - temp[j] = x[row * ncols + i]; + temp[j] = x[i]; } else { temp[j] = 0; } @@ -35,6 +30,40 @@ static __global__ void reduce_rows_f32(const float * x_ptr, float * dst_ptr, con __shared__ float shared_vals[32]; sum = block_reduce<block_reduce_method::SUM>(sum, shared_vals); + return sum; +} + +// Row reduction kernel template - compute sum (norm=false) or mean (norm=true) +template <bool norm> +static __global__ void reduce_rows_f32(const float * x_ptr, float * dst_ptr, const int ncols) { + float * GGML_CUDA_RESTRICT dst = dst_ptr; + const int64_t row = blockIdx.x; + const int col = threadIdx.x; + + const float * GGML_CUDA_RESTRICT x = x_ptr + row*ncols; + const float sum = reduce_row_f32(x, ncols); + + if (col != 0) { + return; + } + + dst[row] = norm ? sum / ncols : sum; +} + +template <bool norm> +static __global__ void reduce_rows_f32_strided(const char * x_ptr, float * dst_ptr, const int ncols, + const int64_t ne1, const int64_t ne2, const int64_t nb1, const int64_t nb2, const int64_t nb3) { + float * GGML_CUDA_RESTRICT dst = dst_ptr; + const int64_t row = blockIdx.x; + const int col = threadIdx.x; + + const int64_t i1 = row % ne1; + const int64_t i2 = (row / ne1) % ne2; + const int64_t i3 = row / (ne1 * ne2); + + const float * GGML_CUDA_RESTRICT x = (const float *) (x_ptr + i1*nb1 + i2*nb2 + i3*nb3); + const float sum = reduce_row_f32(x, ncols); + if (col != 0) { return; } diff --git a/ggml/src/ggml-cuda/sumrows.cu b/ggml/src/ggml-cuda/sumrows.cu index 0003658ca95b..aa8342b5f3c7 100644 --- a/ggml/src/ggml-cuda/sumrows.cu +++ b/ggml/src/ggml-cuda/sumrows.cu @@ -24,24 +24,30 @@ void ggml_cuda_op_sum_rows(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { GGML_ASSERT(src0->type == GGML_TYPE_F32); GGML_ASSERT( dst->type == GGML_TYPE_F32); - GGML_ASSERT(ggml_is_contiguous(src0)); + GGML_ASSERT(ggml_is_contiguous_rows(src0)); const int64_t ncols = src0->ne[0]; const int64_t nrows = ggml_nrows(src0); + if (ggml_is_contiguous(src0)) { + sum_rows_f32_cuda(src0_d, dst_d, ncols, nrows, stream); + return; + } + const dim3 block_nums(nrows, 1, 1); const int id = ggml_cuda_get_device(); const int nsm = ggml_cuda_info().devices[id].nsm; + dim3 block_dims; if ((nrows / nsm) < 2) { // Increase num threads to 512 for small nrows to better hide the latency - const dim3 block_dims(512, 1, 1); - const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, 0, stream); - ggml_cuda_kernel_launch(reduce_rows_f32</*norm=*/false>, launch_params, src0_d, dst_d, ncols); + block_dims = dim3(512, 1, 1); } else { // Enough active SMs to hide latency, use smaller blocks to allow better scheduling - const dim3 block_dims(ncols < 1024 ? 32 : 128, 1, 1); - const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, 0, stream); - ggml_cuda_kernel_launch(reduce_rows_f32</*norm=*/false>, launch_params, src0_d, dst_d, ncols); + block_dims = dim3(ncols < 1024 ? 32 : 128, 1, 1); } + const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params(block_nums, block_dims, 0, stream); + const char * src0_d_bytes = (const char *) src0->data; + ggml_cuda_kernel_launch(reduce_rows_f32_strided</*norm=*/false>, launch_params, src0_d_bytes, dst_d, ncols, + src0->ne[1], src0->ne[2], src0->nb[1], src0->nb[2], src0->nb[3]); } diff --git a/ggml/src/ggml-hexagon/ggml-hexagon.cpp b/ggml/src/ggml-hexagon/ggml-hexagon.cpp index 112e9bae6020..ec7801388689 100644 --- a/ggml/src/ggml-hexagon/ggml-hexagon.cpp +++ b/ggml/src/ggml-hexagon/ggml-hexagon.cpp @@ -66,7 +66,6 @@ using u32vec = std::vector<uint32_t>; #define GGML_HEXAGON_MAX_SESSIONS 16 -#define GGML_HEXAGON_FENCE_BUFFER_SIZE 8192 #define GGML_HEXAGON_FENCE_SLOT_SIZE 128 struct ggml_hexagon_device_config { @@ -75,6 +74,8 @@ struct ggml_hexagon_device_config { int domain_id = 0; std::string domain_name; std::string name; + + std::vector<ggml_hexagon_device_config> mdev_group; }; static ggml_hexagon_device_config opt_device_configs[GGML_HEXAGON_MAX_SESSIONS]; @@ -350,27 +351,48 @@ struct ggml_hexagon_tensor_extra { }; static inline bool ggml_hexagon_tensor_is_fuseable(const struct ggml_tensor * t) { - if (!t || !t->extra) return false; + if (!t->extra) return false; auto extra = (const struct ggml_hexagon_tensor_extra *) t->extra; return (extra->flags & GGML_HEXAGON_TENSOR_FUSEABLE) != 0; } +static inline bool ggml_hexagon_tensors_overlap(const struct ggml_tensor * a, const struct ggml_tensor * b) { + const uintptr_t a0 = (uintptr_t) a->data; + const uintptr_t b0 = (uintptr_t) b->data; + const uintptr_t a1 = a0 + ggml_nbytes(a); + const uintptr_t b1 = b0 + ggml_nbytes(b); + + return a0 < b1 && b0 < a1; +} + struct htp_opnode; struct ggml_hexagon_opbatch; struct ggml_hexagon_opqueue; struct ggml_hexagon_shared_buffer; +struct ggml_hexagon_fence_buffer; struct ggml_hexagon_session; +struct ggml_backend_hexagon_device_context; + +struct ggml_hexagon_mdev_group { + uint32_t idx = 0; + uint32_t count = 1; + std::vector<std::unique_ptr<ggml_hexagon_session>> sessions; +}; struct ggml_backend_hexagon_comm_context { std::vector<ggml_backend_t> backends; size_t n_backends = 0; - uint32_t fence_seq = 0; + volatile uint32_t * fence_slots[GGML_HEXAGON_MAX_SESSIONS] = {}; + ggml_tensor fence_tensors[GGML_HEXAGON_MAX_SESSIONS] = {}; }; struct ggml_hexagon_event { - ggml_hexagon_session * sess = nullptr; - uint64_t seq = 0; + ggml_hexagon_session * sess = nullptr; + ggml_hexagon_session * fence_sess = nullptr; + volatile uint32_t * fence_slot = nullptr; + ggml_tensor fence_tensor = {}; + uint32_t seq = 0; }; struct ggml_hexagon_session { @@ -387,12 +409,12 @@ struct ggml_hexagon_session { bool valid_queue; bool valid_iface; - std::atomic<int> op_pending; ggml_hexagon_opbatch* op_batch; ggml_hexagon_opqueue* op_queue; std::unordered_map<int, std::unique_ptr<ggml_hexagon_shared_buffer>> cloned_buffers; - std::unordered_set<ggml_hexagon_session *> sync_peers; + std::unordered_set<ggml_hexagon_session *> virt_peers; + std::unordered_set<ggml_hexagon_session *> phys_peers; uint32_t n_threads = 0; uint32_t n_hvx = 0; @@ -400,14 +422,23 @@ struct ggml_hexagon_session { uint64_t vtcm_size = 0; size_t max_vmem = 0; size_t max_bufsize = 0; - uint32_t fence_seq; + uint32_t fence_seq = 0; + + std::atomic<uint64_t> batch_req_seq{0}; + std::atomic<uint64_t> batch_rsp_seq{0}; + std::atomic<uint32_t> last_error{HTP_STATUS_OK}; uint64_t cached_uid = 0; std::vector<htp_opnode> cached_nodes; mutable std::unordered_set<const ggml_tensor *> needs_repack; - ggml_hexagon_session(const ggml_hexagon_device_config & config, ggml_backend_dev_t dev = nullptr) noexcept(false); + ggml_hexagon_mdev_group mdev; + ggml_backend_dev_t dev = nullptr; + ggml_backend_hexagon_device_context * dev_ctx = nullptr; + ggml_hexagon_fence_buffer * fence_buf = nullptr; + + ggml_hexagon_session(const ggml_hexagon_device_config & config, ggml_backend_dev_t dev = nullptr, uint32_t mdev_idx = 0, uint32_t mdev_count = 0) noexcept(false); ~ggml_hexagon_session() noexcept(true); const char* c_name() const { return name.c_str(); } @@ -415,31 +446,36 @@ struct ggml_hexagon_session { void allocate(const ggml_hexagon_device_config & config) noexcept(false); void release() noexcept(true); + uint8_t * alloc_fence(uint32_t n_slots = 1); + void free_fence(void * ptr, uint32_t n_slots = 1); + + uint8_t * mdev_fence_slot = nullptr; + std::unordered_map<uint64_t, volatile uint32_t *> cpy_fence_slots; + + void enqueue_mdev_group(); void enqueue_op(const htp_opnode & node); void enqueue_cpy(const ggml_tensor * src, ggml_tensor * dst, const ggml_tensor * sync_tensor = nullptr, uint32_t fence_seq = 0); - void enqueue_fence(const ggml_tensor * sync_tensor, uint32_t fence_seq = 0); - void enqueue_allreduce(const ggml_tensor * dst, const std::vector<const ggml_tensor *> & src_tensors, const std::vector<const ggml_tensor *> & sync_tensors, uint32_t rank, uint32_t n_ranks, uint32_t fence_seq_entry = 0, uint32_t fence_seq_exit = 0); + void enqueue_fence(const ggml_tensor * sync_tensor, uint32_t fence_seq = 0, bool wait = true); + void enqueue_allreduce(const ggml_tensor * dst, const std::vector<const ggml_tensor *> & src_tensors, + const std::vector<const ggml_tensor *> & sync_tensors, uint32_t rank, uint32_t n_ranks, + uint32_t fence_seq_entry = 0, uint32_t fence_seq_exit = 0); - void flush(bool all = true); - void flush_pending(bool all = false); + void flush_sync(bool all = true); + void flush_async(); void flush_batch(size_t min_ops = 1); - - uint64_t record_event(); - void wait_event(uint64_t seq); + void flush_peers(); + void flush_pending(bool all = true); bool clone_buffer(const ggml_hexagon_shared_buffer*); + void release_buffer(const ggml_hexagon_shared_buffer*); + void unclone_buffer(const ggml_hexagon_shared_buffer*); - void add_sync_peer(ggml_hexagon_session * peer) { - sync_peers.insert(peer); - } - - void flush_sync_peers() { - if (sync_peers.empty()) return; - - for (auto * peer : sync_peers) { - peer->flush_batch(); + void add_peer(ggml_hexagon_session * peer) { + if (this->phys_idx == peer->phys_idx) { + virt_peers.insert(peer); + } else { + phys_peers.insert(peer); } - sync_peers.clear(); } }; @@ -451,8 +487,9 @@ struct ggml_backend_hexagon_device_context { ggml_backend_dev_t dev = nullptr; size_t max_bufsize = 0; - ggml_backend_buffer_type buffer_type = {}; - ggml_backend_buffer_type host_buffer_type = {}; + ggml_backend_buffer_type buffer_type = {}; + ggml_backend_buffer_type host_buffer_type = {}; + ggml_backend_buffer_type fence_buffer_type = {}; std::unique_ptr<ggml_hexagon_session> sess; @@ -484,6 +521,8 @@ struct ggml_hexagon_rpcmem_block { int fd = -1; size_t size = 0; + std::unordered_set<ggml_hexagon_session *> mapped_clones; + ggml_hexagon_rpcmem_block(size_t size) { base = (uint8_t *) rpcmem_alloc2(RPCMEM_HEAP_ID_SYSTEM, RPCMEM_DEFAULT_FLAGS, size); if (!base) { @@ -508,8 +547,6 @@ struct ggml_hexagon_shared_buffer { ggml_hexagon_session * sess; std::shared_ptr<ggml_hexagon_rpcmem_block> mem; std::vector<ggml_hexagon_tensor_extra *> tensor_extra; - uint32_t fence_head = 0; - size_t fences_size = 0; bool mapped; bool pinned; @@ -518,16 +555,6 @@ struct ggml_hexagon_shared_buffer { size_t size() const { return mem ? mem->size : 0; } int fd() const { return mem ? mem->fd : -1; } - uint8_t * alloc_fence() { - if (fences_size == 0) return nullptr; - int max_slots = fences_size / GGML_HEXAGON_FENCE_SLOT_SIZE; - uint32_t slot = (fence_head++) % max_slots; - - size_t guard_offset = size() - fences_size; - uint8_t * fence_ptr = base() + guard_offset + (size_t)slot * GGML_HEXAGON_FENCE_SLOT_SIZE; - return fence_ptr; - } - void mmap() { if (!this->mem) return; fastrpc_map_flags flags = this->pinned ? FASTRPC_MAP_FD : FASTRPC_MAP_FD_DELAYED; @@ -581,29 +608,24 @@ struct ggml_hexagon_shared_buffer { this->mem = nullptr; } - ggml_hexagon_shared_buffer(ggml_hexagon_session * sess, size_t size, bool pinned = false, size_t fence_size = 0) { - this->sess = sess; - this->mapped = false; - this->pinned = pinned; - this->fences_size = fence_size; + ggml_hexagon_shared_buffer(ggml_hexagon_session * sess, size_t size, bool pinned = false) { + this->sess = sess; + this->mapped = false; + this->pinned = pinned; - // Size adjustment inside the buffer class + // Size adjustment inside the buffer class: 4K aligned data size + 4K guard page size_t guard_offset = (size + 4095) & ~4095; - size_t total_size = guard_offset; - if (fence_size > 0) { - total_size += 4096 + fence_size; - } + size_t total_size = guard_offset + 4096; alloc(total_size); } // Clone constructor for cross-session mapping ggml_hexagon_shared_buffer(ggml_hexagon_session * sess, const ggml_hexagon_shared_buffer & other) { - this->sess = sess; - this->mem = other.mem; - this->mapped = false; - this->pinned = other.pinned; - this->fences_size = other.fences_size; + this->sess = sess; + this->mem = other.mem; + this->mapped = false; + this->pinned = other.pinned; } ~ggml_hexagon_shared_buffer() { @@ -614,6 +636,59 @@ struct ggml_hexagon_shared_buffer { } }; +struct ggml_hexagon_fence_buffer : public ggml_hexagon_shared_buffer { + uint32_t slot_count = 0; + uint32_t slot_head = 0; + std::vector<uint32_t> free_slots; + ggml_backend_buffer backend_buffer{}; + + ggml_hexagon_fence_buffer(ggml_hexagon_session * sess, ggml_backend_buffer_type_t buft, size_t size) + : ggml_hexagon_shared_buffer(sess, size, false /* pinned */), + slot_count(size / GGML_HEXAGON_FENCE_SLOT_SIZE), + slot_head(0) { + backend_buffer.buft = buft; + backend_buffer.context = static_cast<ggml_hexagon_shared_buffer *>(this); + backend_buffer.size = size; + } + + uint8_t * alloc_slot(uint32_t n_slots = 1) { + uint8_t * ptr = nullptr; + if (n_slots == 1 && !free_slots.empty()) { + uint32_t slot = free_slots.back(); + free_slots.pop_back(); + ptr = base() + (size_t) slot * GGML_HEXAGON_FENCE_SLOT_SIZE; + } else if (slot_head + n_slots <= slot_count) { + uint32_t slot = slot_head; + slot_head += n_slots; + ptr = base() + (size_t) slot * GGML_HEXAGON_FENCE_SLOT_SIZE; + } + if (ptr) { + memset(ptr, 0, (size_t) n_slots * GGML_HEXAGON_FENCE_SLOT_SIZE); + } + return ptr; + } + + void free_slot(void * ptr, uint32_t n_slots = 1) { + if (!ptr) return; + uint32_t slot = ((uint8_t *) ptr - base()) / GGML_HEXAGON_FENCE_SLOT_SIZE; + for (uint32_t i = 0; i < n_slots; i++) { + free_slots.push_back(slot + i); + } + } +}; + +inline uint8_t * ggml_hexagon_session::alloc_fence(uint32_t n_slots) { + uint8_t * ptr = fence_buf->alloc_slot(n_slots); + GGML_ASSERT(ptr); + return ptr; +} + +inline void ggml_hexagon_session::free_fence(void * ptr, uint32_t n_slots) { + if (fence_buf) { + fence_buf->free_slot(ptr, n_slots); + } +} + static ggml_hexagon_session * ggml_backend_hexagon_buffer_get_sess(ggml_backend_buffer_t buffer) { auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(buffer->context); return sbuf->sess; @@ -621,6 +696,7 @@ static ggml_hexagon_session * ggml_backend_hexagon_buffer_get_sess(ggml_backend_ static void ggml_backend_hexagon_buffer_free_buffer(ggml_backend_buffer_t buffer) { auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(buffer->context); + sbuf->sess->unclone_buffer(sbuf); delete sbuf; } @@ -1537,7 +1613,7 @@ static ggml_backend_buffer_t ggml_backend_hexagon_buffer_type_alloc_buffer( auto dev_ctx = static_cast<ggml_backend_hexagon_buffer_type_context *>(buffer_type->context)->dev_ctx; auto sess = dev_ctx->session(); try { - ggml_hexagon_shared_buffer * sbuf = new ggml_hexagon_shared_buffer(sess, size, false, GGML_HEXAGON_FENCE_BUFFER_SIZE); + ggml_hexagon_shared_buffer * sbuf = new ggml_hexagon_shared_buffer(sess, size, false); return ggml_backend_buffer_init(buffer_type, ggml_backend_hexagon_buffer_interface, sbuf, size); } catch (const std::exception & exc) { GGML_LOG_ERROR("ggml-hex: %s failed to allocate device buffer context: %s\n", dev_ctx->c_name(), exc.what()); @@ -1550,7 +1626,7 @@ static ggml_backend_buffer_t ggml_backend_hexagon_host_buffer_type_alloc_buffer( auto dev_ctx = static_cast<ggml_backend_hexagon_buffer_type_context *>(buffer_type->context)->dev_ctx; auto sess = dev_ctx->session(); try { - ggml_hexagon_shared_buffer * sbuf = new ggml_hexagon_shared_buffer(sess, size, false, GGML_HEXAGON_FENCE_BUFFER_SIZE); + ggml_hexagon_shared_buffer * sbuf = new ggml_hexagon_shared_buffer(sess, size, false); return ggml_backend_buffer_init(buffer_type, ggml_backend_hexagon_host_buffer_interface, sbuf, size); } catch (const std::exception & exc) { GGML_LOG_ERROR("ggml-hex: %s failed to allocate host buffer context: %s\n", dev_ctx->c_name(), exc.what()); @@ -1618,11 +1694,16 @@ ggml_backend_hexagon_device_context::ggml_backend_hexagon_device_context(int dev host_buffer_type.device = dev; host_buffer_type.iface = ggml_backend_hexagon_host_buffer_type_interface; host_buffer_type.context = new ggml_backend_hexagon_buffer_type_context(config.name + "-HOST", this); + + fence_buffer_type.device = dev; + fence_buffer_type.iface = ggml_backend_hexagon_buffer_type_interface; + fence_buffer_type.context = new ggml_backend_hexagon_buffer_type_context(config.name + "-FENCE", this); } ggml_backend_hexagon_device_context::~ggml_backend_hexagon_device_context() { delete static_cast<ggml_backend_hexagon_buffer_type_context *>(buffer_type.context); delete static_cast<ggml_backend_hexagon_buffer_type_context *>(host_buffer_type.context); + delete static_cast<ggml_backend_hexagon_buffer_type_context *>(fence_buffer_type.context); } static bool ggml_backend_buffer_is_hexagon(const struct ggml_backend_buffer * b) { @@ -1698,8 +1779,8 @@ struct ggml_hexagon_opbatch { if (it != b_map.end()) { return it->second; } // Add new buffer to the batch - int bi = n_bufs++; GGML_ASSERT(n_bufs < HTP_OP_MAX_BUFS); + int bi = n_bufs++; b_map.insert({sbuf->fd(), bi}); @@ -1902,6 +1983,12 @@ struct ggml_hexagon_opbatch { } } + void update_mdev_group(uint32_t mdev_idx) { + if (n_ops > 0 && h_ops[0].opcode == HTP_OP_MDEV_GROUP) { + h_ops[0].params[0] = (int32_t) mdev_idx; + } + } + bool try_fuse_allreduce_add(const htp_opnode & node) { if (n_ops == 0 || opt_ar_select != 2) return false; if (node.opcode != HTP_OP_ADD) return false; @@ -1910,15 +1997,16 @@ struct ggml_hexagon_opbatch { if (last_node.opcode != HTP_OP_ALLREDUCE) return false; auto * ar_kparams = (struct htp_allreduce_kernel_params *) last_node.kernel_params; - const uint32_t rank = (uint32_t) ar_kparams->rank; - const ggml_tensor * ar_local = (rank < last_node.inputs.size()) ? last_node.inputs[rank] : nullptr; + const uint32_t rank = (uint32_t) ar_kparams->rank; + const uint32_t n_ranks = (uint32_t) ar_kparams->n_ranks; + const ggml_tensor * ar_local = last_node.inputs[rank]; const ggml_tensor * add_src0 = node.src0(); const ggml_tensor * add_src1 = node.src1(); + const ggml_tensor * add_dst = node.dst(); - if (!add_src0 || !add_src1 || !ar_local) return false; if (!ggml_hexagon_tensor_is_fuseable(ar_local)) return false; - const ggml_tensor * res_tensor = nullptr; + const ggml_tensor * res_tensor; if (add_src0 == ar_local || add_src0->data == ar_local->data) { res_tensor = add_src1; } else if (add_src1 == ar_local || add_src1->data == ar_local->data) { @@ -1927,14 +2015,12 @@ struct ggml_hexagon_opbatch { return false; } - if (!res_tensor || !res_tensor->data) return false; - if (ar_local->type != res_tensor->type) return false; const bool is_same_shape = (ar_local->ne[0] == res_tensor->ne[0] && ar_local->ne[1] == res_tensor->ne[1] && ar_local->ne[2] == res_tensor->ne[2] && ar_local->ne[3] == res_tensor->ne[3]); - const bool is_row_bcast = (ar_local->ne[0] == res_tensor->ne[0] && - res_tensor->ne[1] == 1 && res_tensor->ne[2] == 1 && res_tensor->ne[3] == 1); + const bool is_row_bcast = !is_same_shape && (ar_local->ne[0] == res_tensor->ne[0] && res_tensor->ne[1] == 1 && + res_tensor->ne[2] == 1 && res_tensor->ne[3] == 1); if (!is_same_shape && !is_row_bcast) return false; @@ -1947,13 +2033,21 @@ struct ggml_hexagon_opbatch { return false; } } - if (ggml_is_contiguous(ar_local) != ggml_is_contiguous(node.dst())) { + if (ggml_is_contiguous(ar_local) != ggml_is_contiguous(add_dst)) { return false; } + for (uint32_t r = 0; r < n_ranks; r++) { + const ggml_tensor * ar_src = last_node.inputs[r]; + if (ggml_hexagon_tensors_overlap(add_dst, ar_src)) { + HEX_VERBOSE("ggml-hex: %s skip ALLREDUCE_ADD fusion: dst overlaps allreduce src %u\n", sess->c_name(), r); + return false; + } + } + struct htp_allreduce_kernel_params new_kparams; if (!ggml_hexagon_precompute_allreduce_params( - sess, node.dst(), (uint32_t) ar_kparams->rank, (uint32_t) ar_kparams->n_ranks, true, is_row_bcast, &new_kparams + sess, add_dst, (uint32_t) ar_kparams->rank, (uint32_t) ar_kparams->n_ranks, true, is_row_bcast, &new_kparams )) { HEX_VERBOSE("ggml-hex: %s skip ALLREDUCE_ADD fusion: solver failed\n", sess->c_name()); return false; @@ -1961,7 +2055,6 @@ struct ggml_hexagon_opbatch { size_t extra_bufs = 0, extra_vmem = 0, extra_tens = 0; auto fit_t = [&](const ggml_tensor * t) { - if (!t) return; if (!t_map.count(t)) { extra_tens++; auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(t->buffer->context); @@ -1972,7 +2065,7 @@ struct ggml_hexagon_opbatch { } }; fit_t(res_tensor); - fit_t(node.dst()); + fit_t(add_dst); if ((extra_bufs + n_bufs) > n_bufs_max || (extra_tens + n_tens) > n_tens_max || (extra_vmem + b_vmem) > b_vmem_max) { return false; } @@ -1981,7 +2074,7 @@ struct ggml_hexagon_opbatch { last_node.name = "ALLREDUCE+ADD"; last_node.inputs.push_back(res_tensor); last_node.outputs.clear(); - last_node.outputs.push_back(node.dst()); + last_node.outputs.push_back(add_dst); last_node.fused.push_back(node.node); memcpy(last_node.kernel_params, &new_kparams, sizeof(new_kparams)); @@ -1989,9 +2082,8 @@ struct ggml_hexagon_opbatch { o.opcode = HTP_OP_ALLREDUCE_ADD; memcpy(o.kernel_params, &new_kparams, sizeof(new_kparams)); - const uint32_t n_ranks = (uint32_t) ar_kparams->n_ranks; o.src[2 * n_ranks] = add_tensor(res_tensor); - o.dst[0] = add_tensor(node.dst()); + o.dst[0] = add_tensor(add_dst); for (uint32_t d = 1; d < HTP_OP_MAX_OUTPUTS; d++) { o.dst[d] = 0xffff; } @@ -2011,10 +2103,9 @@ struct ggml_hexagon_opbatch { const ggml_tensor * mul_src1 = node.src1(); const ggml_tensor * rms_out = last_node.dst(); - if (!mul_src0 || !mul_src1 || !rms_out) return false; if (!ggml_hexagon_tensor_is_fuseable(rms_out)) return false; - const ggml_tensor * weight = nullptr; + const ggml_tensor * weight; if (mul_src0 == rms_out || mul_src0->data == rms_out->data) { weight = mul_src1; } else if (mul_src1 == rms_out || mul_src1->data == rms_out->data) { @@ -2023,10 +2114,7 @@ struct ggml_hexagon_opbatch { return false; } - if (!weight || !weight->data) return false; - const ggml_tensor * src0 = last_node.src0(); - if (!src0 || !src0->data) return false; if (src0->ne[0] != weight->ne[0] || src0->ne[0] != node.dst()->ne[0]) { return false; @@ -2057,7 +2145,6 @@ struct ggml_hexagon_opbatch { size_t extra_bufs = 0, extra_vmem = 0, extra_tens = 0; auto fit_t = [&](const ggml_tensor * t) { - if (!t) return; if (!t_map.count(t)) { extra_tens++; auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(t->buffer->context); @@ -2112,10 +2199,9 @@ struct ggml_hexagon_opbatch { const ggml_tensor * add_src1 = node.src1(); const ggml_tensor * mm_out = last_node.dst(); - if (!add_src0 || !add_src1 || !mm_out) return false; if (!ggml_hexagon_tensor_is_fuseable(mm_out)) return false; - const ggml_tensor * src2 = nullptr; + const ggml_tensor * src2; if (add_src0 == mm_out || add_src0->data == mm_out->data) { src2 = add_src1; } else if (add_src1 == mm_out || add_src1->data == mm_out->data) { @@ -2124,11 +2210,8 @@ struct ggml_hexagon_opbatch { return false; } - if (!src2 || !src2->data) return false; - const ggml_tensor * src0 = last_node.src0(); const ggml_tensor * src1 = last_node.src1(); - if (!src0 || !src1) return false; struct htp_mm_kernel_params kparams; ggml_hexagon_precompute_fused_matmul_add_params(sess, src0, src1, src2, node.dst(), &kparams); @@ -2144,7 +2227,6 @@ struct ggml_hexagon_opbatch { size_t extra_bufs = 0, extra_vmem = 0, extra_tens = 0; auto fit_t = [&](const ggml_tensor * t) { - if (!t) return; if (!t_map.count(t)) { extra_tens++; auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(t->buffer->context); @@ -2197,7 +2279,6 @@ struct ggml_hexagon_opbatch { const ggml_tensor * w_in = node.src0(); const ggml_tensor * x_in = node.src1(); const ggml_tensor * d_in = node.dst(); - if (!w_in || !x_in || !d_in) return false; htp_opnode & last_node = ops[n_ops - 1]; @@ -2231,7 +2312,6 @@ struct ggml_hexagon_opbatch { size_t extra_bufs = 0, extra_vmem = 0, extra_tens = 0; auto fit_t = [&](const ggml_tensor * t) { - if (!t) return; if (!t_map.count(t)) { extra_tens++; auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(t->buffer->context); @@ -2282,7 +2362,6 @@ struct ggml_hexagon_opbatch { const ggml_tensor * w0 = last_node.src0(); const ggml_tensor * x = last_node.src1(); const ggml_tensor * w1 = node.src0(); - if (!w0 || !x || !w1) return false; struct htp_mm_kernel_params kparams; ggml_hexagon_precompute_fused_mmnx_params(sess, w0, x, 2, &kparams); @@ -2297,7 +2376,6 @@ struct ggml_hexagon_opbatch { size_t extra_bufs = 0, extra_vmem = 0, extra_tens = 0; auto fit_t = [&](const ggml_tensor * t) { - if (!t) return; if (!t_map.count(t)) { extra_tens++; auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(t->buffer->context); @@ -2359,7 +2437,6 @@ struct ggml_hexagon_opbatch { const ggml_tensor * x_in = node.src1(); const ggml_tensor * ids_in = node.node->src[2]; const ggml_tensor * d_in = node.dst(); - if (!w_in || !x_in || !ids_in || !d_in) return false; htp_opnode & last_node = ops[n_ops - 1]; @@ -2394,7 +2471,6 @@ struct ggml_hexagon_opbatch { size_t extra_bufs = 0, extra_vmem = 0, extra_tens = 0; auto fit_t = [&](const ggml_tensor * t) { - if (!t) return; if (!t_map.count(t)) { extra_tens++; auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(t->buffer->context); @@ -2447,7 +2523,6 @@ struct ggml_hexagon_opbatch { const ggml_tensor * x = last_node.src1(); const ggml_tensor * ids = last_node.node->src[2]; const ggml_tensor * w1 = node.src0(); - if (!w0 || !x || !ids || !w1) return false; struct htp_mm_kernel_params kparams; ggml_hexagon_precompute_fused_mmidnx_params(sess, w0, x, node.dst(), 2, &kparams); @@ -2462,7 +2537,6 @@ struct ggml_hexagon_opbatch { size_t extra_bufs = 0, extra_vmem = 0, extra_tens = 0; auto fit_t = [&](const ggml_tensor * t) { - if (!t) return; if (!t_map.count(t)) { extra_tens++; auto sbuf = static_cast<ggml_hexagon_shared_buffer *>(t->buffer->context); @@ -2540,17 +2614,14 @@ struct ggml_hexagon_opqueue { // Shared buffer for storing batches ggml_hexagon_shared_buffer *shm_buf; size_t shm_blk_size; - - uint64_t req_seq = 0; - uint64_t rsp_seq = 0; + size_t depth; using opvec = std::vector<htp_opnode>; - std::queue<unsigned int> done; // completed batch ids std::vector<opvec> op_cache; // per batch op cache std::vector<uint64_t> start_usec; // per batch start time - ggml_hexagon_opqueue(ggml_hexagon_session *sess, size_t batch_size, size_t depth) { + ggml_hexagon_opqueue(ggml_hexagon_session *sess, size_t batch_size, size_t depth) : depth(depth) { size_t n_bufs = HTP_OP_MAX_BUFS; size_t n_ops = batch_size; size_t n_tensors = n_ops * HTP_OP_MAX_OUTPUTS + n_ops * HTP_OP_MAX_INPUTS; @@ -2571,9 +2642,6 @@ struct ggml_hexagon_opqueue { op_cache.resize(depth); start_usec.resize(depth, 0); - // init done queue - for (unsigned int i = 0; i < depth; i++) { done.push(i); } - if (opt_verbose) { GGML_LOG_INFO("ggml-hex: %s allocated opqueue : batch-size %zu depth %zu shm-size %zu shm-block-size %zu\n", sess->c_name(), batch_size, depth, shm_buf->size(), shm_blk_size); @@ -2587,7 +2655,7 @@ struct ggml_hexagon_opqueue { size_t shm_size() const { return shm_buf ? shm_buf->size() : 0; } // push new batch - bool push(htp_opbatch_req& req, dspqueue_buffer& dbuf, ggml_hexagon_opbatch* op_batch) { + bool push(htp_opbatch_req& req, dspqueue_buffer& dbuf, const ggml_hexagon_opbatch* op_batch, uint64_t seq) { static_assert(sizeof(htp_opbatch_req) % 8 == 0, "sizeof(htp_opbatch_req) must be multiple of 8"); static_assert(sizeof(htp_opbatch_rsp) % 8 == 0, "sizeof(htp_opbatch_rsp) must be multiple of 8"); static_assert(sizeof(htp_buf_desc) % 8 == 0, "sizeof(htp_buf_desc) must be multiple of 8"); @@ -2595,16 +2663,17 @@ struct ggml_hexagon_opqueue { static_assert(sizeof(htp_op_desc) % 8 == 0, "sizeof(htp_op_desc) must be multiple of 8"); static_assert(sizeof(htp_prof_desc) % 8 == 0, "sizeof(htp_prof_desc) must be multiple of 8"); - if (done.empty()) { return false; } + if (seq - shm_buf->sess->batch_rsp_seq > depth) { return false; } - req.id = done.front(); done.pop(); // batch id + const uint32_t slot = (uint32_t) ((seq - 1) % depth); + + req.seq = seq; req.n_bufs = op_batch->n_bufs; req.n_tensors = op_batch->n_tens; req.n_ops = op_batch->n_ops; - req.seq = ++req_seq; - op_cache[req.id] = std::move(op_batch->ops); - start_usec[req.id] = ggml_time_us(); + op_cache[slot] = op_batch->ops; + start_usec[slot] = ggml_time_us(); const size_t b_size = sizeof(htp_buf_desc) * req.n_bufs; const size_t t_size = sizeof(htp_tensor) * req.n_tensors; @@ -2619,7 +2688,7 @@ struct ggml_hexagon_opqueue { req.n_traces = 0; } - dbuf.ptr = shm_buf->base() + (req.id * shm_blk_size); + dbuf.ptr = shm_buf->base() + ((size_t) slot * shm_blk_size); dbuf.fd = shm_buf->fd(); dbuf.flags = DSPQUEUE_BUFFER_FLAG_FLUSH_SENDER | DSPQUEUE_BUFFER_FLAG_INVALIDATE_RECIPIENT; dbuf.offset = (uint8_t*) dbuf.ptr - (uint8_t*) shm_buf->base(); @@ -2632,18 +2701,14 @@ struct ggml_hexagon_opqueue { uint8_t * t_ptr = m_ptr; m_ptr += t_size; uint8_t * o_ptr = m_ptr; - op_batch->sort_buffers(); - memcpy(b_ptr, (void *) op_batch->h_bufs.data(), b_size); memcpy(t_ptr, (void *) op_batch->h_tens.data(), t_size); memcpy(o_ptr, (void *) op_batch->h_ops.data(), o_size); - HEX_VERBOSE("ggml-hex: %s opqueue-push batch #%u : n-bufs %u n-tensors %u n-ops %u vmem %zu : b-size %zu t-size %zu o-size %zu m-size %zu\n", - shm_buf->sess->c_name(), req.id, req.n_bufs, req.n_tensors, req.n_ops, op_batch->b_vmem, + HEX_VERBOSE("ggml-hex: %s opqueue-push batch #%llu : n-bufs %u n-tensors %u n-ops %u vmem %zu : b-size %zu t-size %zu o-size %zu m-size %zu\n", + shm_buf->sess->c_name(), (unsigned long long) req.seq, req.n_bufs, req.n_tensors, req.n_ops, op_batch->b_vmem, b_size, t_size, o_size, (size_t) dbuf.size); - op_batch->reset(); - if (opt_verbose > 1) { htp_buf_desc *b = (htp_buf_desc*) b_ptr; for (unsigned int i=0; i < req.n_bufs; i++) { @@ -2662,9 +2727,7 @@ struct ggml_hexagon_opqueue { } void pop(htp_opbatch_rsp rsp, dspqueue_buffer dbuf) { - GGML_ASSERT(rsp.id < op_cache.size()); - - done.push(rsp.id); + const uint32_t slot = (uint32_t) ((rsp.seq - 1) % depth); const size_t b_size = sizeof(htp_buf_desc) * rsp.n_bufs; const size_t t_size = sizeof(htp_tensor) * rsp.n_tensors; @@ -2681,15 +2744,15 @@ struct ggml_hexagon_opqueue { const size_t m_size = b_size + t_size + o_size + p_size + tr_size; GGML_ASSERT(m_size <= shm_blk_size); - HEX_VERBOSE("ggml-hex: %s opqueue-pop batch #%u : n-bufs %u n-tensors %u n-ops %u : m-size %zu b-size %zu t-size %zu o-size %zu\n", - shm_buf->sess->c_name(), rsp.id, rsp.n_bufs, rsp.n_tensors, rsp.n_ops, + HEX_VERBOSE("ggml-hex: %s opqueue-pop batch #%llu : n-bufs %u n-tensors %u n-ops %u : m-size %zu b-size %zu t-size %zu o-size %zu\n", + shm_buf->sess->c_name(), (unsigned long long) rsp.seq, rsp.n_bufs, rsp.n_tensors, rsp.n_ops, (size_t) dbuf.size, b_size, t_size, o_size); uint8_t * m_ptr = (uint8_t*) dbuf.ptr; uint8_t * p_ptr = m_ptr + (b_size + t_size + o_size); if (rsp.n_ops > 0) { - auto & ops = op_cache[rsp.id]; + auto & ops = op_cache[slot]; GGML_ASSERT(rsp.n_ops <= ops.size()); const htp_prof_desc * pd = (const htp_prof_desc *) p_ptr; @@ -2712,16 +2775,41 @@ struct ggml_hexagon_opqueue { ggml_hexagon_dump_trace_events(shm_buf->sess->name, rsp, trace_events, n_traces); } } - - if (rsp.seq > rsp_seq) { - rsp_seq = rsp.seq; - } } }; -// Flush HTP response queue i.e wait for all outstanding requests to complete +void ggml_hexagon_session::flush_peers() { + auto vpeers = std::move(virt_peers); + virt_peers.clear(); + for (auto * peer : vpeers) { + peer->flush_sync(); + } + + auto ppeers = std::move(phys_peers); + phys_peers.clear(); + for (auto * peer : ppeers) { + peer->flush_async(); + } + + for (auto & sub : this->mdev.sessions) { + sub->flush_peers(); + } +} + +void ggml_hexagon_session::flush_async() { + flush_peers(); + flush_batch(); +} + void ggml_hexagon_session::flush_pending(bool all) { - while (this->op_pending) { + for (auto & sub : this->mdev.sessions) { + sub->flush_pending(all); + if (sub->last_error > HTP_STATUS_OK) { + this->last_error = sub->last_error.load(); + } + } + + while (this->batch_rsp_seq < this->batch_req_seq) { struct htp_opbatch_rsp rsp; uint32_t rsp_size; uint32_t flags; @@ -2746,32 +2834,64 @@ void ggml_hexagon_session::flush_pending(bool all) { GGML_ABORT("ggml-hex: %s dspcall : bad response : size %u dspbufs %u\n", this->c_name(), rsp_size, n_dbufs); } - if (rsp.status != HTP_STATUS_OK) { - GGML_LOG_ERROR("ggml-hex: %s dspcall : dsp-rsp: %s\n", this->c_name(), status_to_str(rsp.status)); - // TODO: handle errors + if (rsp.status > HTP_STATUS_OK) { + GGML_LOG_ERROR("ggml-hex: %s dspcall : dsp-rsp %s\n", this->c_name(), status_to_str(rsp.status)); + this->last_error = rsp.status; + for (auto & sub : this->mdev.sessions) { + sub->last_error = rsp.status; + } } op_queue->pop(rsp, dbuf); - this->op_pending--; // atomic dec + GGML_ASSERT(rsp.seq == this->batch_rsp_seq + 1); + this->batch_rsp_seq = rsp.seq; if (!all) break; } } +void ggml_hexagon_session::flush_sync(bool all) { + flush_async(); + flush_pending(all); +} + void ggml_hexagon_session::flush_batch(size_t min_ops) { if (op_batch->n_ops < min_ops) { return; } + op_batch->sort_buffers(); + htp_opbatch_req req {}; dspqueue_buffer dbuf{}; - if (!op_queue->push(req, dbuf, op_batch)) { + const uint64_t seq = ++this->batch_req_seq; + + op_batch->update_mdev_group(this->mdev.idx); + + if (!op_queue->push(req, dbuf, op_batch, seq)) { flush_pending(false); - op_queue->push(req, dbuf, op_batch); + op_queue->push(req, dbuf, op_batch, seq); } - // Bump pending flag (cleared in the session::flush once we get the response) - this->op_pending++; // atomic inc + for (auto & sub : this->mdev.sessions) { + htp_opbatch_req sub_req {}; + dspqueue_buffer sub_dbuf{}; + + sub->batch_req_seq = seq; + op_batch->update_mdev_group(sub->mdev.idx); + + if (!sub->op_queue->push(sub_req, sub_dbuf, op_batch, seq)) { + sub->flush_pending(false); + sub->op_queue->push(sub_req, sub_dbuf, op_batch, seq); + } + + HEX_VERBOSE("ggml-hex: %s queue-opbatch: %p size %u\n", sub->c_name(), sub_dbuf.ptr, sub_dbuf.size); + + int err = dspqueue_write(sub->queue, 0, 1, &sub_dbuf, sizeof(sub_req), (const uint8_t*) &sub_req, DSPQUEUE_TIMEOUT); + if (err != 0) { + GGML_ABORT("ggml-hex: %s dspqueue_write failed: 0x%08x\n", sub->c_name(), (unsigned) err); + } + } HEX_VERBOSE("ggml-hex: %s queue-opbatch: %p size %u\n", this->c_name(), dbuf.ptr, dbuf.size); @@ -2779,28 +2899,28 @@ void ggml_hexagon_session::flush_batch(size_t min_ops) { if (err != 0) { GGML_ABORT("ggml-hex: %s dspqueue_write failed: 0x%08x\n", this->c_name(), (unsigned) err); } -} -void ggml_hexagon_session::flush(bool all) { - flush_sync_peers(); - flush_batch(); - flush_pending(all); + op_batch->reset(); } void ggml_hexagon_session::enqueue_op(const htp_opnode & node) { - for (auto t : node.get_inputs()) { + auto clone_tensor_buffer = [this](const ggml_tensor * t) { if (t && t->buffer && ggml_backend_buffer_is_hexagon(t->buffer)) { + auto sbuf = static_cast<const ggml_hexagon_shared_buffer *>(t->buffer->context); if (ggml_backend_hexagon_buffer_get_sess(t->buffer) != this) { - this->clone_buffer(static_cast<const ggml_hexagon_shared_buffer *>(t->buffer->context)); + this->clone_buffer(sbuf); + } + for (auto & sub : this->mdev.sessions) { + sub->clone_buffer(sbuf); } } + }; + + for (auto t : node.get_inputs()) { + clone_tensor_buffer(t); } for (auto t : node.get_outputs()) { - if (t && t->buffer && ggml_backend_buffer_is_hexagon(t->buffer)) { - if (ggml_backend_hexagon_buffer_get_sess(t->buffer) != this) { - this->clone_buffer(static_cast<const ggml_hexagon_shared_buffer *>(t->buffer->context)); - } - } + clone_tensor_buffer(t); } if (opt_opfusion && op_batch->try_fuse(node)) { @@ -2808,39 +2928,84 @@ void ggml_hexagon_session::enqueue_op(const htp_opnode & node) { } if (!op_batch->fit_op(node)) { - flush_batch(); + flush_async(); } + + if (this->mdev.count > 1 && op_batch->n_ops == 0) { + enqueue_mdev_group(); + } + op_batch->add_op(node); } +void ggml_hexagon_session::enqueue_mdev_group() { + htp_opnode group_node(HTP_OP_MDEV_GROUP); + + uint8_t * fence_slot = this->mdev_fence_slot; + + static ggml_hexagon_tensor_extra fence_extra { {}, 0, GGML_HEXAGON_TENSOR_FENCE }; + ggml_tensor dummy_t {}; + dummy_t.buffer = &this->fence_buf->backend_buffer; + dummy_t.extra = &fence_extra; + dummy_t.data = (void *) fence_slot; + dummy_t.type = GGML_TYPE_I8; + dummy_t.ne[0] = HTP_FENCE_SLOT_SIZE; + dummy_t.ne[1] = (int64_t) this->mdev.count; + dummy_t.ne[2] = 1; + dummy_t.ne[3] = 1; + dummy_t.nb[0] = 1; + dummy_t.nb[1] = HTP_FENCE_SLOT_SIZE; + dummy_t.nb[2] = dummy_t.nb[1] * dummy_t.ne[1]; + dummy_t.nb[3] = dummy_t.nb[2]; + dummy_t.op = GGML_OP_NONE; + dummy_t.op_params[0] = (int32_t) this->mdev.idx; + + ggml_tensor * node = group_node.add_dummy(dummy_t); + node->src[0] = node; + group_node.init(node); + group_node.outputs.clear(); + group_node.name = "MDEV_GROUP"; + + if (this->fence_buf->sess != this) { + this->clone_buffer(this->fence_buf); + } + for (auto & sub : this->mdev.sessions) { + sub->clone_buffer(this->fence_buf); + } + + op_batch->add_op(group_node); +} + void ggml_hexagon_session::enqueue_cpy(const ggml_tensor * src, ggml_tensor * dst, const ggml_tensor * sync_tensor, uint32_t fence_seq) { - htp_opnode cpy_node(HTP_OP_CPY); + const bool with_fence = sync_tensor != nullptr; + htp_opnode cpy_node(with_fence ? HTP_OP_CPY_FENCE : HTP_OP_CPY); ggml_tensor* node = cpy_node.add_dummy(*dst); node->op = GGML_OP_CPY; node->src[0] = const_cast<ggml_tensor *>(src); - node->src[1] = sync_tensor ? cpy_node.add_dummy(*sync_tensor) : nullptr; - if (sync_tensor) { + node->src[1] = with_fence ? cpy_node.add_dummy(*sync_tensor) : nullptr; + if (with_fence) { node->op_params[0] = (int32_t) fence_seq; } cpy_node.init(node); - if (sync_tensor) { + if (with_fence) { cpy_node.name = "CPY+FENCE"; } this->enqueue_op(cpy_node); } -void ggml_hexagon_session::enqueue_fence(const ggml_tensor * sync_tensor, uint32_t fence_seq) { +void ggml_hexagon_session::enqueue_fence(const ggml_tensor * sync_tensor, uint32_t fence_seq, bool wait) { htp_opnode sync_node(HTP_OP_FENCE); ggml_tensor* node = sync_node.add_dummy(*sync_tensor); node->op = GGML_OP_NONE; node->src[0] = node; node->op_params[0] = (int32_t) fence_seq; + node->op_params[1] = wait ? 0 : 1; sync_node.init(node); - sync_node.name = "FENCE"; + sync_node.name = wait ? "FENCE_WAIT" : "FENCE_SIGNAL"; this->enqueue_op(sync_node); } @@ -2858,7 +3023,6 @@ static bool ggml_hexagon_precompute_allreduce_params( kparams->n_ranks = (int32_t) n_ranks; kparams->is_row_bcast = (has_add && is_row_bcast) ? 1 : 0; - const uint32_t n_bufs = n_ranks + 1 + (has_add ? 1 : 0); const uint32_t nelem = (uint32_t) ggml_nelements(dst); const uint32_t elem_size = (dst->type == GGML_TYPE_F16) ? sizeof(ggml_fp16_t) : sizeof(float); const bool is_contiguous = ggml_is_contiguous(dst); @@ -2902,6 +3066,7 @@ static bool ggml_hexagon_precompute_allreduce_params( const uint32_t rank_nelem = (uint32_t) kparams->rank_nelem; const uint32_t n_threads = (std::min)((uint32_t) sess->n_threads, (std::max)(1u, rank_nelem / 128)); kparams->n_threads = n_threads; + const size_t n_vtcm_buffers = htp_allreduce_vtcm_buffer_count(n_ranks, n_threads, has_add, is_row_bcast); uint32_t block_elems = 65536; if (block_elems > rank_nelem / n_threads && rank_nelem / n_threads > 128) { @@ -2911,15 +3076,15 @@ static bool ggml_hexagon_precompute_allreduce_params( kparams->block_elems = block_elems; kparams->vtcm_size_per_thread = 2 * block_elems * elem_size; - kparams->vtcm_size = n_threads * n_bufs * kparams->vtcm_size_per_thread; + kparams->vtcm_size = n_vtcm_buffers * kparams->vtcm_size_per_thread; while ((size_t) kparams->vtcm_size > sess->vtcm_size && block_elems > 128) { - const size_t max_bytes_per_buf = sess->vtcm_size / (n_threads * n_bufs * 2); + const size_t max_bytes_per_buf = sess->vtcm_size / (n_vtcm_buffers * 2); block_elems = (uint32_t) hex_align_down((size_t) (max_bytes_per_buf / elem_size), 128); if (block_elems < 128) break; kparams->block_elems = block_elems; kparams->vtcm_size_per_thread = 2 * block_elems * elem_size; - kparams->vtcm_size = n_threads * n_bufs * kparams->vtcm_size_per_thread; + kparams->vtcm_size = n_vtcm_buffers * kparams->vtcm_size_per_thread; } if (sess->vtcm_size < (size_t) kparams->vtcm_size || block_elems < 128) { @@ -2935,6 +3100,7 @@ static bool ggml_hexagon_precompute_allreduce_params( const uint32_t rank_nrows = (uint32_t) kparams->rank_nelem; const uint32_t n_threads = (std::min)((uint32_t) sess->n_threads, (std::max)(1u, rank_nrows)); kparams->n_threads = n_threads; + const size_t n_vtcm_buffers = htp_allreduce_vtcm_buffer_count(n_ranks, n_threads, has_add, is_row_bcast); const uint32_t row_bytes = ne0 * elem_size; const uint32_t row_size_aligned = (uint32_t) hex_align_up(row_bytes, 128); @@ -2946,14 +3112,14 @@ static bool ggml_hexagon_precompute_allreduce_params( kparams->block_elems = block_rows; kparams->vtcm_size_per_thread = 2 * (block_rows * row_size_aligned); - kparams->vtcm_size = n_threads * n_bufs * kparams->vtcm_size_per_thread; + kparams->vtcm_size = n_vtcm_buffers * kparams->vtcm_size_per_thread; while ((size_t) kparams->vtcm_size > sess->vtcm_size && block_rows > 1) { - const size_t max_rows_per_buf = sess->vtcm_size / (n_threads * n_bufs * 2 * row_size_aligned); + const size_t max_rows_per_buf = sess->vtcm_size / (n_vtcm_buffers * 2 * row_size_aligned); block_rows = (std::max)(1u, (uint32_t) max_rows_per_buf); kparams->block_elems = block_rows; kparams->vtcm_size_per_thread = 2 * (block_rows * row_size_aligned); - kparams->vtcm_size = n_threads * n_bufs * kparams->vtcm_size_per_thread; + kparams->vtcm_size = n_vtcm_buffers * kparams->vtcm_size_per_thread; if (max_rows_per_buf == 0) break; } @@ -3009,28 +3175,20 @@ void ggml_hexagon_session::enqueue_allreduce( this->enqueue_op(ar_node); } -void ggml_hexagon_session::wait_event(uint64_t seq) { - flush_sync_peers(); - HEX_VERBOSE("ggml-hex: %s opqueue-wait start: seq %llu, current rsp-seq %llu, pending %d\n", - this->name.c_str(), (unsigned long long)seq, (unsigned long long)op_queue->rsp_seq, (int)this->op_pending); - while (op_queue->rsp_seq < seq && this->op_pending > 0) { - this->flush_pending(false); - } - HEX_VERBOSE("ggml-hex: %s opqueue-wait end: seq %llu, current rsp-seq %llu, pending %d\n", - this->name.c_str(), (unsigned long long)seq, (unsigned long long)op_queue->rsp_seq, (int)this->op_pending); -} - -uint64_t ggml_hexagon_session::record_event() { - flush_batch(); - return op_queue->req_seq; -} - bool ggml_hexagon_session::clone_buffer(const ggml_hexagon_shared_buffer *sbuf) { - if (this->cloned_buffers.find(sbuf->fd()) != this->cloned_buffers.end()) return true; + GGML_ASSERT(sbuf && sbuf->mem); + if (sbuf->sess == this) return true; + + auto mem = sbuf->mem; + int fd = mem->fd; + + GGML_ASSERT(fd >= 0); + + if (this->cloned_buffers.find(fd) != this->cloned_buffers.end()) return true; HEX_VERBOSE("ggml-hex: %s clone-buffer: %s base %p size %zu fd %d\n", this->name.c_str(), - sbuf->c_name(), sbuf->base(), sbuf->size(), sbuf->fd()); + sbuf->c_name(), sbuf->base(), sbuf->size(), fd); auto clone = std::make_unique<ggml_hexagon_shared_buffer>(this, *sbuf); try { @@ -3040,10 +3198,38 @@ bool ggml_hexagon_session::clone_buffer(const ggml_hexagon_shared_buffer *sbuf) return false; } - this->cloned_buffers[sbuf->fd()] = std::move(clone); + this->cloned_buffers[fd] = std::move(clone); + mem->mapped_clones.insert(this); return true; } +void ggml_hexagon_session::release_buffer(const ggml_hexagon_shared_buffer * sbuf) { + GGML_ASSERT(sbuf && sbuf->mem); + + auto mem = sbuf->mem; + int fd = mem->fd; + + GGML_ASSERT(fd >= 0); + + auto it = this->cloned_buffers.find(fd); + if (it != this->cloned_buffers.end()) { + auto clone = std::move(it->second); + this->cloned_buffers.erase(it); + } + mem->mapped_clones.erase(this); +} + +void ggml_hexagon_session::unclone_buffer(const ggml_hexagon_shared_buffer * sbuf) { + GGML_ASSERT(sbuf && sbuf->mem); + + auto mem = sbuf->mem; + std::vector<ggml_hexagon_session *> sessions(mem->mapped_clones.begin(), mem->mapped_clones.end()); + + for (auto * sess : sessions) { + sess->release_buffer(sbuf); + } +} + static size_t ggml_hexagon_measure_max_vmem(ggml_hexagon_session *sess) { // Allocate a bunch pinned buffers till failure. // This is kind of expensive but handy for figuring out exactly how much we can mmap on a specific device. @@ -3082,14 +3268,16 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n this->valid_queue = false; this->valid_iface = false; - this->phys_idx = phys_idx; - this->virt_idx = virt_idx; - this->domain_id = config.domain_id; - this->session_id = 0; - this->name = config.name; - this->op_pending = 0; + this->name = config.name; + this->phys_idx = phys_idx; + this->virt_idx = virt_idx; + this->domain_id = config.domain_id; + this->session_id = 0; + this->batch_req_seq = 0; + this->batch_rsp_seq = 0; + this->last_error = HTP_STATUS_OK; - GGML_LOG_DEBUG("ggml-hex: %s allocating new session\n", this->name.c_str()); + GGML_LOG_DEBUG("ggml-hex: %s allocating new session : domain %u phys-idx %u virt-idx %u\n", this->name.c_str(), this->domain_id, phys_idx, virt_idx); if (config.domain_id < 0 || config.domain_name.empty()) { GGML_LOG_ERROR("ggml-hex: %s: invalid physical CDSP core %d\n", config.name.c_str(), config.physical_idx); @@ -3098,25 +3286,14 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n const std::string & dom_name = config.domain_name; - // Enable Unsigned PD for all domains - { - struct remote_rpc_control_unsigned_module u; - u.domain = -1; - u.enable = 1; - int err = remote_session_control(DSPRPC_CONTROL_UNSIGNED_MODULE, (void *) &u, sizeof(u)); - if (err != AEE_SUCCESS) { - GGML_LOG_ERROR("ggml-hex: %s failed to enable unsigned PD : error 0x%x\n", this->c_name(), err); - throw std::runtime_error("ggml-hex: remote_session_control(unsign) failed (see log for details)"); - } - } - // Create new session if virtual_idx > 0 if (virt_idx > 0) { - struct remote_rpc_reserve_new_session n; + struct remote_rpc_reserve_new_session n {}; n.domain_name_len = dom_name.size(); n.domain_name = const_cast<char *>(dom_name.c_str()); n.session_name = const_cast<char *>(this->name.c_str()); n.session_name_len = this->name.size(); + n.session_id = virt_idx; int err = remote_session_control(FASTRPC_RESERVE_NEW_SESSION, (void *) &n, sizeof(n)); if (err != AEE_SUCCESS) { @@ -3130,7 +3307,7 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n this->domain_id = n.effective_domain_id; this->valid_session = true; } else { - struct remote_rpc_effective_domain_id eff = {}; + struct remote_rpc_effective_domain_id eff {}; eff.domain_name = const_cast<char *>(dom_name.c_str()); eff.domain_name_len = dom_name.size(); eff.session_id = 0; @@ -3144,6 +3321,18 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n } } + // Enable unsigned modules + { + struct remote_rpc_control_unsigned_module u; + u.domain = this->domain_id; + u.enable = 1; + int err = remote_session_control(DSPRPC_CONTROL_UNSIGNED_MODULE, (void *) &u, sizeof(u)); + if (err != AEE_SUCCESS) { + GGML_LOG_ERROR("ggml-hex: %s failed to enable unsigned PD : error 0x%x\n", this->c_name(), err); + throw std::runtime_error("ggml-hex: remote_session_control(unsign) failed (see log for details)"); + } + } + char session_uri[256]; { char htp_uri[256]; @@ -3171,7 +3360,7 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n // Open session int err = htp_iface_open(session_uri, &this->handle); if (err != AEE_SUCCESS) { - GGML_LOG_ERROR("ggml-hex: %s failed to open session : error 0x%x\n", this->c_name(), err); + GGML_LOG_ERROR("ggml-hex: %s failed to open session : uri %s error 0x%x\n", this->c_name(), session_uri, err); throw std::runtime_error("ggml-hex: failed to open session (see log for details)"); } @@ -3186,8 +3375,9 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n unsigned long long hw_vtcm_size = 0; int hw_err = htp_iface_hwinfo(this->handle, &hw_n_threads, &hw_n_hvx, &hw_n_hmx, &hw_vtcm_size); if (hw_err == 0) { - this->n_threads = opt_nhvx > 0 ? (uint32_t)opt_nhvx : (uint32_t)hw_n_threads; - this->n_hvx = opt_nhvx > 0 ? (uint32_t)opt_nhvx : (uint32_t)hw_n_hvx; + const uint32_t max_n_threads = (std::min)((uint32_t) HTP_MAX_NTHREADS, (uint32_t) hw_n_threads); + this->n_threads = opt_nhvx > 0 ? (uint32_t) (std::min)(opt_nhvx, (size_t) max_n_threads) : max_n_threads; + this->n_hvx = this->n_threads; this->n_hmx = (opt_nhmx != 0) ? (uint32_t)hw_n_hmx : 0; this->vtcm_size = (uint64_t)hw_vtcm_size; GGML_LOG_INFO("ggml-hex: %s hwinfo: threads %u, hvx %u, hmx %u, vtcm %llu MB\n", @@ -3195,8 +3385,9 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n (unsigned long long)(this->vtcm_size / (1024 * 1024))); } else { GGML_LOG_WARN("ggml-hex: %s failed to query hwinfo (0x%x), using defaults\n", this->c_name(), hw_err); - this->n_threads = opt_nhvx > 0 ? (uint32_t)opt_nhvx : 8; - this->n_hvx = opt_nhvx > 0 ? (uint32_t)opt_nhvx : 8; + const uint32_t default_n_threads = (std::min)(8u, (uint32_t) HTP_MAX_NTHREADS); + this->n_threads = opt_nhvx > 0 ? (uint32_t) (std::min)(opt_nhvx, (size_t) HTP_MAX_NTHREADS) : default_n_threads; + this->n_hvx = this->n_threads; this->n_hmx = (opt_nhmx != 0) ? 1 : 0; this->vtcm_size = 8 * 1024 * 1024; } @@ -3252,6 +3443,11 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n // Allocate buffers and state for op batching this->op_queue = new ggml_hexagon_opqueue(this, opt_opbatch, opt_opqueue); + this->fence_buf = new ggml_hexagon_fence_buffer(this, &dev_ctx->fence_buffer_type, 64 * 1024); + if (this->mdev.count > 1) { + this->mdev_fence_slot = this->alloc_fence(this->mdev.count); + } + if (!opt_vmem) { opt_vmem = ggml_hexagon_measure_max_vmem(this); GGML_LOG_INFO("ggml-hex: %s measured max vmem %zu\n", this->c_name(), opt_vmem); @@ -3262,7 +3458,7 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n this->op_batch = new ggml_hexagon_opbatch(this, opt_opbatch, this->max_vmem); // Start dspqueue/opbatch processing - err = htp_iface_start(this->handle, this->session_id, this->queue_id, opt_nhvx, opt_nhmx, this->max_vmem); + err = htp_iface_start(this->handle, this->session_id, this->queue_id, this->n_threads, opt_nhmx, this->max_vmem); if (err != 0) { GGML_LOG_ERROR("ggml-hex: %s failed to start session: 0x%08x\n", this->c_name(), (unsigned) err); throw std::runtime_error("ggml-hex: iface start failed (see log for details)"); @@ -3283,6 +3479,8 @@ void ggml_hexagon_session::allocate(const ggml_hexagon_device_config & config) n void ggml_hexagon_session::release() noexcept(true) { GGML_LOG_INFO("ggml-hex: releasing session: %s\n", this->name.c_str()); + this->mdev.sessions.clear(); + int err; if (this->valid_iface) { @@ -3295,6 +3493,19 @@ void ggml_hexagon_session::release() noexcept(true) { delete this->op_batch; delete this->op_queue; + for (auto & it : this->cpy_fence_slots) { + free_fence((void *) it.second, 1); + } + this->cpy_fence_slots.clear(); + + if (this->fence_buf) { + unclone_buffer(this->fence_buf); + delete this->fence_buf; + this->fence_buf = nullptr; + } + while (!this->cloned_buffers.empty()) { + release_buffer(this->cloned_buffers.begin()->second.get()); + } if (opt_etm) { err = htp_iface_etm(this->handle, 0); @@ -3321,23 +3532,30 @@ void ggml_hexagon_session::release() noexcept(true) { if (this->valid_handle) { htp_iface_close(this->handle); } - - this->cloned_buffers.clear(); } -ggml_hexagon_session::ggml_hexagon_session(const ggml_hexagon_device_config & config, ggml_backend_dev_t dev) noexcept(false) { - op_batch = nullptr; - op_queue = nullptr; - fence_seq = ((uintptr_t)this) & 0xFFFF; +ggml_hexagon_session::ggml_hexagon_session(const ggml_hexagon_device_config & config, ggml_backend_dev_t dev, uint32_t mdev_idx, uint32_t mdev_count) noexcept(false) { + this->dev = dev; + this->dev_ctx = static_cast<ggml_backend_hexagon_device_context *>(dev->context); + this->mdev.idx = mdev_idx; + this->mdev.count = mdev_count > 0 ? mdev_count : (uint32_t) (1 + config.mdev_group.size()); + op_batch = nullptr; + op_queue = nullptr; + fence_buf = nullptr; + fence_seq = ((uintptr_t)this) & 0xFFFF; try { allocate(config); + if (this->mdev.idx == 0 && !config.mdev_group.empty()) { + for (size_t i = 0; i < config.mdev_group.size(); i++) { + this->mdev.sessions.push_back(std::make_unique<ggml_hexagon_session>( + config.mdev_group[i], this->dev, (uint32_t) (i + 1), this->mdev.count)); + } + } } catch (const std::exception & exc) { release(); throw; } - - GGML_UNUSED(dev); } ggml_hexagon_session::~ggml_hexagon_session() noexcept(true) { @@ -3563,10 +3781,6 @@ static bool ggml_hexagon_supported_gated_delta_net(const struct ggml_hexagon_ses const struct ggml_tensor * state = op->src[5]; const struct ggml_tensor * dst = op; - if (!q || !k || !v || !g || !beta || !state) { - return false; - } - if (q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F32 || v->type != GGML_TYPE_F32 || g->type != GGML_TYPE_F32 || beta->type != GGML_TYPE_F32 || state->type != GGML_TYPE_F32 || dst->type != GGML_TYPE_F32) { @@ -3754,6 +3968,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( struct htp_mm_kernel_params * kparams ) { kparams->n_hmx = 0; + kparams->n_threads = sess->n_threads; const bool is_quant = (wtype != GGML_TYPE_F16 && wtype != GGML_TYPE_F32); const int src1_nrows = ne11 * ne12 * ne13; @@ -4193,6 +4408,7 @@ static void ggml_hexagon_precompute_fused_mmnx_params( struct htp_mm_kernel_params * kparams ) { memset(kparams, 0, sizeof(*kparams)); + kparams->n_threads = sess->n_threads; const int ne00 = src0->ne[0]; const int ne01 = src0->ne[1]; @@ -4921,6 +5137,14 @@ static bool ggml_hexagon_supported_pad(const struct ggml_hexagon_session * sess, return false; } + const int32_t lp0 = ((const int32_t *) op->op_params)[0]; + const int32_t rp0 = ((const int32_t *) op->op_params)[1]; + const int32_t circular = ((const int32_t *) op->op_params)[8]; + + if (circular && (lp0 > src0->ne[0] || rp0 > src0->ne[0])) { + return false; + } + return true; GGML_UNUSED(sess); @@ -4972,10 +5196,6 @@ static bool ggml_hexagon_supported_solve_tri(const struct ggml_hexagon_session * const struct ggml_tensor * src1 = op->src[1]; // B const struct ggml_tensor * dst = op; // X - if (!src0 || !src1) { - return false; - } - if (src0->type != GGML_TYPE_F32 || src1->type != GGML_TYPE_F32 || dst->type != GGML_TYPE_F32) { return false; } @@ -5145,7 +5365,7 @@ static bool is_supported_mul_mat_id_nx_kernel(const ggml_tensor * src0, const st } static bool is_mergeable_mul_mat(const ggml_tensor * t) { - if (!t || t->op != GGML_OP_MUL_MAT) return false; + if (t->op != GGML_OP_MUL_MAT) return false; const ggml_tensor * src0 = t->src[0]; const ggml_tensor * src1 = t->src[1]; @@ -5179,7 +5399,7 @@ static bool is_mergeable_mul_mat_pair(const ggml_tensor * n1, const ggml_tensor } static bool is_mergeable_mul_mat_id(const ggml_tensor * t) { - if (!t || t->op != GGML_OP_MUL_MAT_ID) return false; + if (t->op != GGML_OP_MUL_MAT_ID) return false; const ggml_tensor * src0 = t->src[0]; return ggml_hexagon_is_repack_type(src0->type); @@ -5213,6 +5433,10 @@ static bool is_mergeable_mul_mat_id_pair(const ggml_tensor * n1, const ggml_tens static ggml_status ggml_backend_hexagon_graph_compute(ggml_backend_t backend, ggml_cgraph * graph) { auto sess = static_cast<ggml_hexagon_session *>(backend->context); + if (sess->last_error > HTP_STATUS_OK) { + return GGML_STATUS_FAILED; + } + HEX_VERBOSE("ggml-hex: %s graph-compute n_nodes %d\n", sess->c_name(), graph->n_nodes); const std::vector<htp_opnode> * nodes_ptr = nullptr; @@ -5228,6 +5452,8 @@ static ggml_status ggml_backend_hexagon_graph_compute(ggml_backend_t backend, gg auto * extra = (ggml_hexagon_tensor_extra *) graph->nodes[i]->extra; if (!extra) continue; + extra->flags &= ~GGML_HEXAGON_TENSOR_FUSEABLE; + if (graph->nodes[i]->op == GGML_OP_RMS_NORM && ggml_can_fuse(graph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL })) { extra->flags |= GGML_HEXAGON_TENSOR_FUSEABLE; } else if (graph->nodes[i]->op == GGML_OP_MUL_MAT || graph->nodes[i]->op == GGML_OP_MUL_MAT_ID) { @@ -5299,6 +5525,10 @@ static ggml_status ggml_backend_hexagon_graph_compute(ggml_backend_t backend, gg sess->enqueue_op(node); } + if (sess->last_error > HTP_STATUS_OK) { + return GGML_STATUS_FAILED; + } + return GGML_STATUS_SUCCESS; } @@ -5308,7 +5538,10 @@ static void ggml_backend_hexagon_synchronize(ggml_backend_t backend) { HEX_VERBOSE("ggml-hex: %s synchronize\n", sess->c_name()); // Wait until all pending ops complete - sess->flush(); + sess->flush_sync(); + if (sess->last_error > HTP_STATUS_OK) { + GGML_ABORT("ggml-hex: %s synchronize failed : dsp-error %s\n", sess->c_name(), status_to_str(sess->last_error)); + } } enum ggml_hexagon_mem_range_type { @@ -5543,27 +5776,38 @@ static void ggml_backend_hexagon_graph_optimize(ggml_backend_t backend, ggml_cgr GGML_UNUSED(backend); } +static uint64_t ggml_hexagon_session_key(const ggml_hexagon_session * sess) { + return ((uint64_t) (uint32_t) sess->phys_idx << 32) | (uint32_t) sess->virt_idx; +} + static bool ggml_hexagon_cpy_tensor_async_phys(ggml_backend_t backend_src, ggml_backend_t backend_dst, const ggml_tensor * src, ggml_tensor * dst) { auto sess_src = static_cast<ggml_hexagon_session *>(backend_src->context); auto sess_dst = static_cast<ggml_hexagon_session *>(backend_dst->context); auto sbuf_dst = (ggml_hexagon_shared_buffer *) dst->buffer->context; - if (sess_dst->fence_seq == 0) sess_dst->fence_seq = 1; - uint32_t fence_seq = sess_dst->fence_seq++; - if (sess_dst->fence_seq == 0) sess_dst->fence_seq = 1; + if (!sess_src->clone_buffer(sbuf_dst)) { return false; } - volatile uint32_t * fence = (volatile uint32_t *) sbuf_dst->alloc_fence(); + const uint64_t src_key = ggml_hexagon_session_key(sess_src); + auto & fence_slot = sess_dst->cpy_fence_slots[src_key]; + if (!fence_slot) { + fence_slot = (volatile uint32_t *) sess_dst->alloc_fence(1); + } + + if (!sess_src->clone_buffer(sess_dst->fence_buf)) { return false; } + + if (++sess_dst->fence_seq == 0) sess_dst->fence_seq = 1; + uint32_t fence_seq = sess_dst->fence_seq; - HEX_VERBOSE("ggml-hex: %s cpy-tensor-async %s -> %s size %zu : seq %u\n", + HEX_VERBOSE("ggml-hex: %s cpy-tensor-async %s -> %s size %zu : seq 0x%x\n", sess_dst->name.c_str(), src->name, dst->name, ggml_nbytes(src), fence_seq); - // dummy extra (must be static) + // dummy fence extra (must be static) static ggml_hexagon_tensor_extra fence_extra { {}, 0, GGML_HEXAGON_TENSOR_FENCE }; ggml_tensor fence_tensor {}; - fence_tensor.buffer = dst->buffer; + fence_tensor.buffer = &sess_dst->fence_buf->backend_buffer; fence_tensor.extra = &fence_extra; - fence_tensor.data = (void *) fence; + fence_tensor.data = (void *) fence_slot; fence_tensor.type = GGML_TYPE_I32; fence_tensor.ne[0] = 1; fence_tensor.ne[1] = 1; @@ -5576,9 +5820,9 @@ static bool ggml_hexagon_cpy_tensor_async_phys(ggml_backend_t backend_src, ggml_ fence_tensor.op = GGML_OP_NONE; sess_src->enqueue_cpy(src, dst, &fence_tensor, fence_seq); - sess_dst->enqueue_fence(&fence_tensor, fence_seq); + sess_dst->enqueue_fence(&fence_tensor, fence_seq, /* wait = */ true); - sess_dst->add_sync_peer(sess_src); + sess_dst->add_peer(sess_src); return true; } @@ -5586,15 +5830,15 @@ static bool ggml_hexagon_cpy_tensor_async_phys(ggml_backend_t backend_src, ggml_ static bool ggml_hexagon_cpy_tensor_async_virt(ggml_backend_t backend_src, ggml_backend_t backend_dst, const ggml_tensor * src, ggml_tensor * dst) { auto sess_src = static_cast<ggml_hexagon_session *>(backend_src->context); auto sess_dst = static_cast<ggml_hexagon_session *>(backend_dst->context); - auto sbuf_dst = (ggml_hexagon_shared_buffer *) dst->buffer->context; + auto sbuf_src = (ggml_hexagon_shared_buffer *) src->buffer->context; - if (!sess_src->clone_buffer(sbuf_dst)) { return false; } + if (!sess_dst->clone_buffer(sbuf_src)) { return false; } HEX_VERBOSE("ggml-hex: %s cpy-tensor-async %s -> %s size %zu\n", sess_dst->name.c_str(), src->name, dst->name, ggml_nbytes(src)); - sess_src->enqueue_cpy(src, dst); - sess_src->flush(true); + sess_dst->enqueue_cpy(src, dst); + sess_dst->add_peer(sess_src); return true; } @@ -5604,7 +5848,14 @@ static bool ggml_backend_hexagon_cpy_tensor_async(ggml_backend_t backend_src, gg return false; } - *(ggml_hexagon_tensor_extra *) dst->extra = *(const ggml_hexagon_tensor_extra *) src->extra; + // FIXME: ggml-meta needs to call init_tensor on auxiliary tensors + if (!dst->extra) { + ggml_backend_buffer_init_tensor(dst->buffer, dst); + } + + auto * dst_extra = static_cast<ggml_hexagon_tensor_extra *>(dst->extra); + const auto * src_extra = static_cast<const ggml_hexagon_tensor_extra *>(src->extra); + dst_extra->flags = src_extra->flags & ~GGML_HEXAGON_TENSOR_FUSEABLE; auto sess_src = static_cast<ggml_hexagon_session *>(backend_src->context); auto sess_dst = static_cast<ggml_hexagon_session *>(backend_dst->context); @@ -5612,7 +5863,6 @@ static bool ggml_backend_hexagon_cpy_tensor_async(ggml_backend_t backend_src, gg if (sess_src == sess_dst) { HEX_VERBOSE("ggml-hex: %s cpy-tensor-async %s -> %s size %zu\n", sess_dst->name.c_str(), src->name, dst->name, ggml_nbytes(src)); sess_src->enqueue_cpy(src, dst); - sess_src->flush_batch(); return true; } @@ -5623,8 +5873,30 @@ static bool ggml_backend_hexagon_cpy_tensor_async(ggml_backend_t backend_src, gg } static ggml_backend_event_t ggml_backend_hexagon_device_event_new(ggml_backend_dev_t dev) { + auto dev_ctx = static_cast<ggml_backend_hexagon_device_context *>(dev->context); + auto sess = dev_ctx->session(); + ggml_hexagon_event * hex_event = new ggml_hexagon_event(); - HEX_VERBOSE("ggml-hex: %s event-new : event %p\n", ggml_backend_dev_name(dev), (void *)hex_event); + hex_event->fence_sess = sess; + hex_event->sess = sess; + hex_event->fence_slot = (volatile uint32_t *) sess->alloc_fence(1); + + static ggml_hexagon_tensor_extra fence_extra { {}, 0, GGML_HEXAGON_TENSOR_FENCE }; + hex_event->fence_tensor.buffer = &sess->fence_buf->backend_buffer; + hex_event->fence_tensor.extra = &fence_extra; + hex_event->fence_tensor.data = (void *) hex_event->fence_slot; + hex_event->fence_tensor.type = GGML_TYPE_I32; + hex_event->fence_tensor.ne[0] = 1; + hex_event->fence_tensor.ne[1] = 1; + hex_event->fence_tensor.ne[2] = 1; + hex_event->fence_tensor.ne[3] = 1; + hex_event->fence_tensor.nb[0] = sizeof(int32_t); + hex_event->fence_tensor.nb[1] = sizeof(int32_t); + hex_event->fence_tensor.nb[2] = sizeof(int32_t); + hex_event->fence_tensor.nb[3] = sizeof(int32_t); + hex_event->fence_tensor.op = GGML_OP_NONE; + + HEX_VERBOSE("ggml-hex: %s event-new : event %p fence %p\n", ggml_backend_dev_name(dev), (void *)hex_event, (void *)hex_event->fence_slot); return new ggml_backend_event { /* .device = */ dev, @@ -5632,49 +5904,83 @@ static ggml_backend_event_t ggml_backend_hexagon_device_event_new(ggml_backend_d }; } -static void ggml_backend_hexagon_device_event_free(ggml_backend_dev_t dev, ggml_backend_event_t event) { - GGML_UNUSED(dev); - - if (event == nullptr) { +static void ggml_hexagon_event_synchronize(ggml_backend_dev_t dev, ggml_hexagon_event * hex_event) { + if (hex_event->seq == 0) { return; } - ggml_hexagon_event * hex_event = (ggml_hexagon_event *)event->context; + HEX_VERBOSE("ggml-hex: %s event-synchronize : event %p seq 0x%x fence %p\n", + ggml_backend_dev_name(dev), (void *)hex_event, hex_event->seq, (void *)hex_event->fence_slot); + + auto * fence = reinterpret_cast<const volatile std::atomic<uint32_t> *>(hex_event->fence_slot); + + if ((int32_t)(fence[0].load(std::memory_order_relaxed) - hex_event->seq) < 0) { + hex_event->sess->flush_async(); + } + + while (true) { + if ((int32_t)(fence[0].load(std::memory_order_acquire) - hex_event->seq) >= 0) { + uint32_t status = fence[1].load(std::memory_order_acquire); + if (status > HTP_STATUS_OK) { + GGML_ABORT("ggml-hex: %s event-synchronize failed : dsp-error %s\n", + hex_event->sess->c_name(), status_to_str(status)); + } + break; + } + std::this_thread::yield(); + } +} + +static void ggml_backend_hexagon_device_event_free(ggml_backend_dev_t dev, ggml_backend_event_t event) { + auto * hex_event = static_cast<ggml_hexagon_event *>(event->context); + ggml_hexagon_event_synchronize(dev, hex_event); HEX_VERBOSE("ggml-hex: %s event-free : event %p\n", ggml_backend_dev_name(dev), (void *)hex_event); + hex_event->fence_sess->free_fence((void *) hex_event->fence_slot, 1); delete hex_event; delete event; } static void ggml_backend_hexagon_device_event_synchronize(ggml_backend_dev_t dev, ggml_backend_event_t event) { - GGML_UNUSED(dev); - - ggml_hexagon_event * hex_event = (ggml_hexagon_event *)event->context; - HEX_VERBOSE("ggml-hex: %s event-synchronize : event %p seq %llu\n", - ggml_backend_dev_name(dev), (void *)hex_event, (unsigned long long)hex_event->seq); - if (hex_event->sess != nullptr) { - hex_event->sess->wait_event(hex_event->seq); - } + auto * hex_event = static_cast<ggml_hexagon_event *>(event->context); + ggml_hexagon_event_synchronize(dev, hex_event); } static void ggml_backend_hexagon_event_record(ggml_backend_t backend, ggml_backend_event_t event) { auto sess = static_cast<ggml_hexagon_session *>(backend->context); - ggml_hexagon_event * hex_event = (ggml_hexagon_event *)event->context; + auto hex_event = static_cast<ggml_hexagon_event *>(event->context); + if (++sess->fence_seq == 0) sess->fence_seq = 1; hex_event->sess = sess; - hex_event->seq = sess->record_event(); - HEX_VERBOSE("ggml-hex: %s event-record : event %p seq %llu\n", - sess->c_name(), (void *)hex_event, (unsigned long long)hex_event->seq); + hex_event->seq = sess->fence_seq; + + sess->enqueue_fence(&hex_event->fence_tensor, hex_event->seq, /* wait = */ false); + + HEX_VERBOSE("ggml-hex: %s event-record : event %p seq 0x%x fence %p\n", + sess->c_name(), (void *)hex_event, hex_event->seq, (void *)hex_event->fence_slot); } static void ggml_backend_hexagon_event_wait(ggml_backend_t backend, ggml_backend_event_t event) { - GGML_UNUSED(backend); + auto sess = static_cast<ggml_hexagon_session *>(backend->context); + auto hex_event = static_cast<ggml_hexagon_event *>(event->context); + + if (hex_event->seq == 0) { + return; + } + + HEX_VERBOSE("ggml-hex: %s event-wait : event %p seq 0x%x fence %p\n", + sess->c_name(), (void *)hex_event, hex_event->seq, (void *)hex_event->fence_slot); - ggml_hexagon_event * hex_event = (ggml_hexagon_event *)event->context; - if (hex_event->sess != nullptr) { - HEX_VERBOSE("ggml-hex: %s event-wait : event %p seq %llu\n", - hex_event->sess->c_name(), (void *)hex_event, (unsigned long long)hex_event->seq); - hex_event->sess->wait_event(hex_event->seq); + // same physical NPU runs sequentially in FIFO order + if (sess->phys_idx == hex_event->sess->phys_idx) { + if (sess != hex_event->sess) { + sess->add_peer(hex_event->sess); + } + return; } + + sess->clone_buffer(hex_event->fence_sess->fence_buf); + sess->add_peer(hex_event->sess); + sess->enqueue_fence(&hex_event->fence_tensor, hex_event->seq, /* wait = */ true); } static void ggml_backend_hexagon_set_tensor_async(ggml_backend_t backend, struct ggml_tensor * tensor, const void * data, size_t offset, size_t size) { @@ -5688,7 +5994,10 @@ static void ggml_backend_hexagon_get_tensor_async(ggml_backend_t backend, const auto sess = static_cast<ggml_hexagon_session *>(backend->context); HEX_VERBOSE("ggml-hex: %s get-tensor-async %s : data %p offset %zu size %zu usage %d\n", sess->c_name(), tensor->name, data, offset, size, tensor->buffer ? (int) tensor->buffer->usage : -1); - sess->flush(true); + sess->flush_sync(); + if (sess->last_error > HTP_STATUS_OK) { + GGML_ABORT("ggml-hex: %s get-tensor-async failed : dsp-error %s\n", sess->c_name(), status_to_str(sess->last_error)); + } ggml_backend_tensor_get(tensor, data, offset, size); } @@ -5717,7 +6026,10 @@ static void ggml_backend_hexagon_get_tensor_2d_async(ggml_backend_t backend, auto sess = static_cast<ggml_hexagon_session *>(backend->context); HEX_VERBOSE("ggml-hex: %s get-tensor-2d-async %s : data %p offset %zu size %zu n_copies %zu stride_tensor %zu stride_data %zu usage %d\n", sess->c_name(), tensor->name, data, offset, size, n_copies, stride_tensor, stride_data, tensor->buffer ? (int) tensor->buffer->usage : -1); - sess->flush(true); + sess->flush_sync(); + if (sess->last_error > HTP_STATUS_OK) { + GGML_ABORT("ggml-hex: %s get-tensor-2d-async failed : dsp-error %s\n", sess->c_name(), status_to_str(sess->last_error)); + } ggml_backend_tensor_get_2d(tensor, data, offset, size, n_copies, stride_tensor, stride_data); } @@ -6083,13 +6395,7 @@ static bool ggml_backend_hexagon_device_supports_op(ggml_backend_dev_t dev, cons static bool ggml_backend_hexagon_device_supports_buft(ggml_backend_dev_t dev, ggml_backend_buffer_type_t buft) { auto dev_ctx = static_cast<ggml_backend_hexagon_device_context *>(dev->context); - // Technically we can clone hexagon buffers from any session but for some reason the output is garbled with layer-split, - // tensor-split works correctly, so it needs mode debugging and investigation. For now accept only our own buffers. -#if 0 - bool supp = (buft->iface.get_alignment == ggml_backend_hexagon_buffer_type_get_alignment); -#else bool supp = (buft == &dev_ctx->host_buffer_type) || (buft == &dev_ctx->buffer_type); -#endif HEX_VERBOSE("ggml-hex: %s device-supports-buft %s %s\n", dev_ctx->c_name(), ggml_backend_buft_name(buft), supp ? "yes" : "no"); return supp; @@ -6122,6 +6428,19 @@ ggml_hexagon_registry::ggml_hexagon_registry(ggml_backend_reg_t reg) { // Create devices for (size_t i = 0; i < opt_ndev; i++) { + const auto & cfg = opt_device_configs[i]; + if (cfg.mdev_group.empty()) { + GGML_LOG_INFO("ggml-hex: device %zu: %s (phys=%d, virt=%d, domain=%s:%d)\n", + i, cfg.name.c_str(), cfg.physical_idx, cfg.virtual_idx, cfg.domain_name.c_str(), cfg.domain_id); + } else { + std::string peers_str; + for (const auto & p : cfg.mdev_group) { + if (!peers_str.empty()) peers_str += ", "; + peers_str += p.name + " (phys=" + std::to_string(p.physical_idx) + ")"; + } + GGML_LOG_INFO("ggml-hex: device %zu: %s (phys=%d, virt=%d, domain=%s:%d) [mdev peers: %s]\n", + i, cfg.name.c_str(), cfg.physical_idx, cfg.virtual_idx, cfg.domain_name.c_str(), cfg.domain_id, peers_str.c_str()); + } devices[i].iface = ggml_backend_hexagon_device_i; devices[i].reg = reg; devices[i].context = new ggml_backend_hexagon_device_context(i, opt_device_configs[i], &devices[i]); @@ -6172,17 +6491,51 @@ static void * ggml_backend_hexagon_comm_init(ggml_backend_t * backends, size_t n } } + for (size_t i = 0; i < n_backends; i++) { + auto sess_i = static_cast<ggml_hexagon_session *>(backends[i]->context); + for (size_t j = i + 1; j < n_backends; j++) { + auto sess_j = static_cast<ggml_hexagon_session *>(backends[j]->context); + if (sess_i->phys_idx == sess_j->phys_idx) { + return nullptr; + } + } + } + auto * ctx = new ggml_backend_hexagon_comm_context(); ctx->backends.assign(backends, backends + n_backends); ctx->n_backends = n_backends; - ctx->fence_seq = (((uintptr_t) ctx) & 0xFFFF) | 1; + + static ggml_hexagon_tensor_extra fence_extra { {}, 0, GGML_HEXAGON_TENSOR_FENCE }; + for (size_t i = 0; i < n_backends; i++) { + auto sess_i = static_cast<ggml_hexagon_session *>(backends[i]->context); + ctx->fence_slots[i] = (volatile uint32_t *) sess_i->alloc_fence(1); + ctx->fence_tensors[i] = {}; + ctx->fence_tensors[i].buffer = &sess_i->fence_buf->backend_buffer; + ctx->fence_tensors[i].extra = &fence_extra; + ctx->fence_tensors[i].data = (void *) ctx->fence_slots[i]; + ctx->fence_tensors[i].type = GGML_TYPE_I32; + ctx->fence_tensors[i].ne[0] = 4; + ctx->fence_tensors[i].ne[1] = 1; + ctx->fence_tensors[i].ne[2] = 1; + ctx->fence_tensors[i].ne[3] = 1; + ctx->fence_tensors[i].nb[0] = sizeof(int32_t); + ctx->fence_tensors[i].nb[1] = sizeof(int32_t); + ctx->fence_tensors[i].nb[2] = sizeof(int32_t); + ctx->fence_tensors[i].nb[3] = sizeof(int32_t); + ctx->fence_tensors[i].op = GGML_OP_NONE; + } return ctx; } static void ggml_backend_hexagon_comm_free(void * comm_ctx_v) { if (!comm_ctx_v) return; - delete static_cast<ggml_backend_hexagon_comm_context *>(comm_ctx_v); + auto * ctx = static_cast<ggml_backend_hexagon_comm_context *>(comm_ctx_v); + for (size_t i = 0; i < ctx->n_backends; i++) { + auto sess_i = static_cast<ggml_hexagon_session *>(ctx->backends[i]->context); + sess_i->free_fence((void *) ctx->fence_slots[i], 1); + } + delete ctx; } static bool ggml_backend_hexagon_comm_allreduce_tensor(void * comm_ctx_v, struct ggml_tensor ** tensors) { @@ -6192,6 +6545,16 @@ static bool ggml_backend_hexagon_comm_allreduce_tensor(void * comm_ctx_v, struct if (n_backends < 2 || n_backends > 4) return false; + for (size_t i = 0; i < n_backends; i++) { + auto sess_i = static_cast<ggml_hexagon_session *>(comm_ctx->backends[i]->context); + for (size_t j = i + 1; j < n_backends; j++) { + auto sess_j = static_cast<ggml_hexagon_session *>(comm_ctx->backends[j]->context); + if (sess_i->phys_idx == sess_j->phys_idx) { + return false; + } + } + } + for (size_t i = 0; i < n_backends; i++) { if (!tensors[i] || !tensors[i]->buffer || !ggml_backend_buffer_is_hexagon(tensors[i]->buffer)) { return false; @@ -6219,42 +6582,28 @@ static bool ggml_backend_hexagon_comm_allreduce_tensor(void * comm_ctx_v, struct } } - if (comm_ctx->fence_seq == 0) comm_ctx->fence_seq = 1; - uint32_t fence_seq_entry = comm_ctx->fence_seq++; - if (comm_ctx->fence_seq == 0) comm_ctx->fence_seq = 1; - uint32_t fence_seq_exit = comm_ctx->fence_seq++; - if (comm_ctx->fence_seq == 0) comm_ctx->fence_seq = 1; - - volatile uint32_t * fences[GGML_HEXAGON_MAX_SESSIONS]; - for (size_t i = 0; i < n_backends; i++) { - auto sbuf = (ggml_hexagon_shared_buffer *) tensors[i]->buffer->context; - fences[i] = (volatile uint32_t *) sbuf->alloc_fence(); + uint32_t max_seq = static_cast<ggml_hexagon_session *>(comm_ctx->backends[0]->context)->fence_seq; + for (size_t i = 1; i < n_backends; i++) { + auto sess_i = static_cast<ggml_hexagon_session *>(comm_ctx->backends[i]->context); + if ((int32_t)(sess_i->fence_seq - max_seq) > 0) { + max_seq = sess_i->fence_seq; + } } + if (++max_seq == 0) max_seq = 1; + uint32_t fence_seq_entry = max_seq; + if (++max_seq == 0) max_seq = 1; + uint32_t fence_seq_exit = max_seq; - static ggml_hexagon_tensor_extra fence_extra { {}, 0, GGML_HEXAGON_TENSOR_FENCE }; - ggml_tensor fence_tensors[GGML_HEXAGON_MAX_SESSIONS]; for (size_t i = 0; i < n_backends; i++) { - fence_tensors[i] = {}; - fence_tensors[i].buffer = tensors[i]->buffer; - fence_tensors[i].extra = &fence_extra; - fence_tensors[i].data = (void *) fences[i]; - fence_tensors[i].type = GGML_TYPE_I32; - fence_tensors[i].ne[0] = 4; - fence_tensors[i].ne[1] = 1; - fence_tensors[i].ne[2] = 1; - fence_tensors[i].ne[3] = 1; - fence_tensors[i].nb[0] = sizeof(int32_t); - fence_tensors[i].nb[1] = sizeof(int32_t); - fence_tensors[i].nb[2] = sizeof(int32_t); - fence_tensors[i].nb[3] = sizeof(int32_t); - fence_tensors[i].op = GGML_OP_NONE; + auto sess_i = static_cast<ggml_hexagon_session *>(comm_ctx->backends[i]->context); + sess_i->fence_seq = max_seq; } std::vector<const ggml_tensor *> data_tensors(n_backends); std::vector<const ggml_tensor *> sync_tensors(n_backends); for (size_t i = 0; i < n_backends; i++) { data_tensors[i] = tensors[i]; - sync_tensors[i] = &fence_tensors[i]; + sync_tensors[i] = &comm_ctx->fence_tensors[i]; } for (size_t r = 0; r < n_backends; r++) { @@ -6262,7 +6611,7 @@ static bool ggml_backend_hexagon_comm_allreduce_tensor(void * comm_ctx_v, struct sess->enqueue_allreduce(tensors[r], data_tensors, sync_tensors, (uint32_t) r, (uint32_t) n_backends, fence_seq_entry, fence_seq_exit); for (size_t j = 0; j < n_backends; j++) { if (r != j) { - sess->add_sync_peer(static_cast<ggml_hexagon_session *>(comm_ctx->backends[j]->context)); + sess->add_peer(static_cast<ggml_hexagon_session *>(comm_ctx->backends[j]->context)); } } } @@ -6270,8 +6619,23 @@ static bool ggml_backend_hexagon_comm_allreduce_tensor(void * comm_ctx_v, struct return true; } +static ggml_backend_buffer_type_t ggml_backend_hexagon_split_buffer_type(int main_device, const float * tensor_split) { + GGML_UNUSED(tensor_split); + auto reg = ggml_backend_hexagon_reg(); + auto dev = ggml_backend_reg_dev_get(reg, main_device); + if (!dev) { + dev = ggml_backend_reg_dev_get(reg, 0); + } + if (!dev) return nullptr; + auto dev_ctx = static_cast<ggml_backend_hexagon_device_context *>(dev->context); + return &dev_ctx->buffer_type; +} + static void * ggml_backend_hexagon_get_proc_address(ggml_backend_reg_t reg, const char * name) { GGML_UNUSED(reg); + if (strcmp(name, "ggml_backend_split_buffer_type") == 0) { + return (void *) ggml_backend_hexagon_split_buffer_type; + } if (strcmp(name, "ggml_backend_comm_init") == 0) { return (void *) ggml_backend_hexagon_comm_init; } @@ -6304,6 +6668,41 @@ template<typename T, int BASE=10> std::string vec_to_str(std::vector<T> v) { return str; } +static void ggml_hexagon_resolve_device_domain(ggml_hexagon_device_config & cfg, bool discovery_supported, const std::unordered_map<int, fastrpc_domain> & cdsp_map) { + if (discovery_supported) { + auto it = cdsp_map.find(cfg.physical_idx); + if (it != cdsp_map.end()) { + cfg.domain_id = it->second.id; + cfg.domain_name = it->second.name; + } else { + GGML_LOG_ERROR("ggml-hex: physical CDSP core %d not found on device (%zu CDSP core(s) available)\n", + cfg.physical_idx, cdsp_map.size()); + cfg.domain_id = -1; + cfg.domain_name = ""; + } + } else { + switch (cfg.physical_idx) { + case 0: + cfg.domain_id = 3; + cfg.domain_name = CDSP_DOMAIN_NAME; + break; + case 1: + cfg.domain_id = 4; + cfg.domain_name = "cdsp1"; + break; + default: + GGML_LOG_ERROR("ggml-hex: physical CDSP core %d not supported without dynamic discovery\n", + cfg.physical_idx); + cfg.domain_id = -1; + cfg.domain_name = ""; + break; + } + } + for (auto & sub_cfg : cfg.mdev_group) { + ggml_hexagon_resolve_device_domain(sub_cfg, discovery_supported, cdsp_map); + } +} + // Enumerate NPU (aka CDSP) domains via FASTRPC_GET_DOMAINS if supported, // and populate domain_id and domain_name for all configured devices. static void ggml_hexagon_discover_devices() { @@ -6350,36 +6749,7 @@ static void ggml_hexagon_discover_devices() { // Populate domain IDs and names for all configured devices for (size_t i = 0; i < opt_ndev; i++) { - auto & cfg = opt_device_configs[i]; - if (discovery_supported) { - auto it = cdsp_map.find(cfg.physical_idx); - if (it != cdsp_map.end()) { - cfg.domain_id = it->second.id; - cfg.domain_name = it->second.name; - } else { - GGML_LOG_ERROR("ggml-hex: physical CDSP core %d not found on device (%zu CDSP core(s) available)\n", - cfg.physical_idx, cdsp_map.size()); - cfg.domain_id = -1; - cfg.domain_name = ""; - } - } else { - switch (cfg.physical_idx) { - case 0: - cfg.domain_id = 3; - cfg.domain_name = CDSP_DOMAIN_NAME; - break; - case 1: - cfg.domain_id = 4; - cfg.domain_name = "cdsp1"; - break; - default: - GGML_LOG_ERROR("ggml-hex: physical CDSP core %d not supported without dynamic discovery\n", - cfg.physical_idx); - cfg.domain_id = -1; - cfg.domain_name = ""; - break; - } - } + ggml_hexagon_resolve_device_domain(opt_device_configs[i], discovery_supported, cdsp_map); } } @@ -6487,21 +6857,126 @@ static void ggml_hexagon_init(ggml_backend_reg * reg) { opt_device_configs[i].physical_idx = 0; opt_device_configs[i].virtual_idx = (int)i; opt_device_configs[i].name = "HTP" + std::to_string(i); + opt_device_configs[i].mdev_group.clear(); } } else { std::string s_devices(str_devices); - std::stringstream ss(s_devices); - std::string item; - opt_ndev = 0; - while (std::getline(ss, item, ',')) { - size_t start = item.find_first_not_of(" \t\r\n"); - size_t end = item.find_last_not_of(" \t\r\n"); - if (start == std::string::npos) { - continue; + std::vector<std::string> items; + std::string curr_item; + int bracket_depth = 0; + for (char ch : s_devices) { + if (ch == '[') { + bracket_depth++; + curr_item += ch; + } else if (ch == ']') { + if (bracket_depth > 0) bracket_depth--; + curr_item += ch; + } else if (ch == ',' && bracket_depth == 0) { + size_t s = curr_item.find_first_not_of(" \t\r\n"); + size_t e = curr_item.find_last_not_of(" \t\r\n"); + if (s != std::string::npos) { + items.push_back(curr_item.substr(s, e - s + 1)); + } + curr_item.clear(); + } else { + curr_item += ch; } - item = item.substr(start, end - start + 1); + } + size_t s = curr_item.find_first_not_of(" \t\r\n"); + size_t e = curr_item.find_last_not_of(" \t\r\n"); + if (s != std::string::npos) { + items.push_back(curr_item.substr(s, e - s + 1)); + } + + opt_ndev = 0; + for (const auto & item : items) { + size_t b_open = item.find('['); + size_t b_close = item.rfind(']'); + + if (b_open != std::string::npos && b_close != std::string::npos && b_close > b_open) { + // Grouped / composite syntax: Name[phys_spec:virt] or Name[phys_spec] + std::string dev_name = item.substr(0, b_open); + std::string content = item.substr(b_open + 1, b_close - b_open - 1); - if (item.rfind("HTP", 0) == 0) { + int virt = 0; + std::string phys_spec = content; + size_t colon_pos = content.find(':'); + if (colon_pos != std::string::npos) { + phys_spec = content.substr(0, colon_pos); + try { + virt = std::stoi(content.substr(colon_pos + 1)); + } catch (...) { + virt = 0; + } + } else { + size_t dev_colon = dev_name.find(':'); + if (dev_colon != std::string::npos) { + try { + virt = std::stoi(dev_name.substr(dev_colon + 1)); + } catch (...) { + virt = 0; + } + } + } + + // Parse physical indices from phys_spec (e.g. 0-1, 0,1, 0-3, etc.) + std::vector<int> phys_list; + std::stringstream pss(phys_spec); + std::string p_part; + while (std::getline(pss, p_part, ',')) { + size_t ps = p_part.find_first_not_of(" \t\r\n"); + size_t pe = p_part.find_last_not_of(" \t\r\n"); + if (ps == std::string::npos) continue; + p_part = p_part.substr(ps, pe - ps + 1); + + size_t dash_pos = p_part.find('-'); + if (dash_pos != std::string::npos) { + try { + int p_start = std::stoi(p_part.substr(0, dash_pos)); + int p_end = std::stoi(p_part.substr(dash_pos + 1)); + for (int p = p_start; p <= p_end; p++) { + if (std::find(phys_list.begin(), phys_list.end(), p) == phys_list.end()) { + phys_list.push_back(p); + } + } + } catch (...) { + GGML_LOG_WARN("ggml-hex: failed to parse physical range in '%s'\n", p_part.c_str()); + } + } else { + try { + int p = std::stoi(p_part); + if (std::find(phys_list.begin(), phys_list.end(), p) == phys_list.end()) { + phys_list.push_back(p); + } + } catch (...) { + GGML_LOG_WARN("ggml-hex: failed to parse physical index in '%s'\n", p_part.c_str()); + } + } + } + + if (phys_list.empty()) { + phys_list.push_back(0); + } + + if (opt_ndev < GGML_HEXAGON_MAX_SESSIONS) { + auto & cfg = opt_device_configs[opt_ndev]; + cfg.name = dev_name; + cfg.physical_idx = phys_list[0]; + cfg.virtual_idx = virt; + cfg.mdev_group.clear(); + + for (size_t k = 1; k < phys_list.size(); k++) { + ggml_hexagon_device_config sub_cfg; + sub_cfg.physical_idx = phys_list[k]; + sub_cfg.virtual_idx = virt; + sub_cfg.name = "HTP" + std::to_string(phys_list[k]) + ":" + std::to_string(virt); + cfg.mdev_group.push_back(sub_cfg); + } + opt_ndev++; + } else { + GGML_LOG_WARN("ggml-hex: max sessions limit reached (%d), ignoring device %s\n", GGML_HEXAGON_MAX_SESSIONS, item.c_str()); + } + } else if (item.rfind("HTP", 0) == 0) { std::string rest = item.substr(3); size_t colon_pos = rest.find(':'); int phys = 0; @@ -6525,6 +7000,7 @@ static void ggml_hexagon_init(ggml_backend_reg * reg) { opt_device_configs[opt_ndev].name = colon_pos == std::string::npos ? "HTP" + std::to_string(phys) : "HTP" + std::to_string(phys) + ":" + std::to_string(virt); + opt_device_configs[opt_ndev].mdev_group.clear(); opt_ndev++; } else { GGML_LOG_WARN("ggml-hex: max sessions limit reached (%d), ignoring device %s\n", GGML_HEXAGON_MAX_SESSIONS, item.c_str()); @@ -6539,6 +7015,7 @@ static void ggml_hexagon_init(ggml_backend_reg * reg) { opt_device_configs[0].physical_idx = 0; opt_device_configs[0].virtual_idx = 0; opt_device_configs[0].name = "HTP0"; + opt_device_configs[0].mdev_group.clear(); } #if defined(__ANDROID__) diff --git a/ggml/src/ggml-hexagon/htp-opnode.h b/ggml/src/ggml-hexagon/htp-opnode.h index b083e26718bd..ef7b5184fc70 100644 --- a/ggml/src/ggml-hexagon/htp-opnode.h +++ b/ggml/src/ggml-hexagon/htp-opnode.h @@ -344,6 +344,12 @@ struct htp_opformat { } else if (htp_op_is_unary(node.opcode)) { const auto * kparams = (const struct htp_unary_kernel_params *) node.kernel_params; snprintf(str, max_size, "%s vtcm %d", kparams->col_tile ? "wide-row" : "row-block", (int) kparams->vtcm_size); + } else if (node.opcode == HTP_OP_MDEV_GROUP && node.node) { + snprintf(str, max_size, "idx %d count %d", (int) node.node->op_params[0], (int) node.dst()->ne[1]); + } else if ((node.opcode == HTP_OP_FENCE || node.opcode == HTP_OP_CPY_FENCE) && node.node) { + snprintf(str, max_size, "seq 0x%x", (uint32_t) node.node->op_params[0]); + } else if (node.opcode == HTP_OP_ALLREDUCE && node.node) { + snprintf(str, max_size, "seq 0x%x -> 0x%x", (uint32_t) node.node->op_params[0], (uint32_t) node.node->op_params[1]); } else { snprintf(str, max_size, "----"); } diff --git a/ggml/src/ggml-hexagon/htp/act-ops.c b/ggml/src/ggml-hexagon/htp/act-ops.c index ac00b447d989..5fff372f2817 100644 --- a/ggml/src/ggml-hexagon/htp/act-ops.c +++ b/ggml/src/ggml-hexagon/htp/act-ops.c @@ -3,7 +3,6 @@ #pragma clang diagnostic ignored "-Wunused-but-set-variable" #include <HAP_farf.h> -#include <HAP_perf.h> #include <math.h> #include <string.h> @@ -15,7 +14,7 @@ #include "ggml-common.h" #include "htp-ctx.h" #include "htp-ops.h" -#include "htp-ops.h" +#include "hex-common.h" #include "htp-tensor.h" #include "htp-vtcm.h" @@ -80,6 +79,7 @@ struct htp_act_context { uint32_t block; uint32_t src0_nrows; uint32_t src0_nrows_per_thread; + uint32_t row_start; int nc; uint8_t * vtcm_src0; @@ -329,104 +329,104 @@ static void geglu_f32(const float * restrict src0, } } -#define DEFINE_GLU_PER_THREAD(NAME, OP_STR, CORE_EXPR) \ - static void glu_##NAME##_f32_per_thread(unsigned int nth, unsigned int ith, void * data) { \ - struct htp_act_context * actx = (struct htp_act_context *) data; \ - htp_act_preamble; \ - \ - struct htp_thread_trace * tr = actx->octx->ctx ? &actx->octx->ctx->trace[ith] : NULL; \ - \ - size_t src0_row_size = actx->src0_row_size; \ - size_t src1_row_size = actx->src1_row_size; \ - size_t dst_row_size = actx->dst_row_size; \ - \ - size_t src0_row_stride = actx->src0_row_stride; \ - size_t src1_row_stride = actx->src1_row_stride; \ - \ - const uint32_t src0_nrows = actx->src0_nrows; \ - const uint32_t src0_nrows_per_thread = actx->src0_nrows_per_thread; \ - \ - const uint32_t src0_start_row = src0_nrows_per_thread * ith; \ - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); \ - \ - /* no work for this thread */ \ - if (src0_start_row >= src0_end_row) { \ - return; \ - } \ - \ - const uint8_t * restrict data_src0 = actx->data_src0; \ - const uint8_t * restrict data_src1 = actx->data_src1; \ - uint8_t * restrict data_dst = actx->data_dst; \ - \ - const size_t src0_row_size_aligned = actx->src0_row_size_aligned; \ - const size_t src1_row_size_aligned = actx->src1_row_size_aligned; \ - const size_t dst_row_size_aligned = actx->dst_row_size_aligned; \ - \ - uint8_t * restrict src0_spad_data = actx->vtcm_src0 + (ith * actx->vtcm_src0_size_per_thread); \ - uint8_t * restrict src1_spad_data = actx->vtcm_src1 + (ith * actx->vtcm_src1_size_per_thread); \ - uint8_t * restrict dst_spad_data = actx->vtcm_dst + (ith * actx->vtcm_dst_size_per_thread); \ - \ - size_t src0_spad_half_size = actx->src0_spad_half_size; \ - size_t src1_spad_half_size = actx->src1_spad_half_size; \ - size_t dst_spad_half_size = actx->dst_spad_half_size; \ - \ - const int BLOCK = actx->block; \ - if (BLOCK == 0) { \ - FARF(ERROR, \ - OP_STR \ - " : current VTCM reservation %zu is too small for even 1 row per thread, needed at least %zu\n", \ - actx->vtcm_src0_size_per_thread, src0_row_size_aligned); \ - return; \ - } \ - \ - dma_queue * dma_queue = actx->octx->ctx->dma[ith]; \ - \ - /* See discussion: https://github.com/ggml-org/llama.cpp/pull/18151#issuecomment-3678235379 */ \ - for (uint32_t ir = src0_start_row, spad_idx = 0; ir < src0_end_row && spad_idx < 2; ir += BLOCK, spad_idx++) { \ - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); \ - \ - /* Dummy DMA transation for sequencing (interleaving dst,src,dst,...) */ \ - dma_queue_push_vtcm_to_ddr(dma_queue, \ - dma_make_ptr(data_dst, dst_spad_data + (spad_idx * dst_spad_half_size)), \ - dst_row_size, dst_row_size_aligned, 0); \ - \ - dma_queue_push( \ - dma_queue, \ - dma_make_ptr(src0_spad_data + (spad_idx * src0_spad_half_size), data_src0 + (ir * src0_row_stride)), \ - src0_row_size_aligned, src0_row_stride, src0_row_size, block_size); \ - dma_queue_push( \ - dma_queue, \ - dma_make_ptr(src1_spad_data + (spad_idx * src1_spad_half_size), data_src1 + (ir * src1_row_stride)), \ - src1_row_size_aligned, src1_row_stride, src1_row_size, block_size); \ - } \ - \ - for (uint32_t ir = src0_start_row; ir < src0_end_row; ir += BLOCK) { \ - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); \ - \ - float * dst_spad = (float *) dma_queue_pop(dma_queue).src; \ - float * src0_spad = (float *) dma_queue_pop(dma_queue).dst; \ - float * src1_spad = (float *) dma_queue_pop(dma_queue).dst; \ - \ - htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, ir); \ - CORE_EXPR; \ - htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, ir); \ - \ - dma_queue_push_vtcm_to_ddr(dma_queue, dma_make_ptr(data_dst + (ir * dst_row_size), dst_spad), \ - dst_row_size, dst_row_size_aligned, block_size); \ - \ - /* prefetch N+2 loop iteration if any */ \ - const uint32_t pref_block = (ir + BLOCK * 2); \ - if (pref_block < src0_end_row) { \ - const uint32_t pref_block_size = MIN(BLOCK, src0_end_row - pref_block); \ - dma_queue_push(dma_queue, dma_make_ptr(src0_spad, data_src0 + (pref_block * src0_row_stride)), \ - src0_row_size_aligned, src0_row_stride, src0_row_size, pref_block_size); \ - dma_queue_push(dma_queue, dma_make_ptr(src1_spad, data_src1 + (pref_block * src1_row_stride)), \ - src1_row_size_aligned, src1_row_stride, src1_row_size, pref_block_size); \ - } \ - } \ - \ - dma_queue_flush(dma_queue); \ - \ +#define DEFINE_GLU_PER_THREAD(NAME, OP_STR, CORE_EXPR) \ + static void glu_##NAME##_f32_per_thread(unsigned int nth, unsigned int ith, void * data) { \ + struct htp_act_context * actx = (struct htp_act_context *) data; \ + htp_act_preamble; \ + \ + struct htp_thread_trace * tr = actx->octx->ctx ? &actx->octx->ctx->trace[ith] : NULL; \ + \ + size_t src0_row_size = actx->src0_row_size; \ + size_t src1_row_size = actx->src1_row_size; \ + size_t dst_row_size = actx->dst_row_size; \ + \ + size_t src0_row_stride = actx->src0_row_stride; \ + size_t src1_row_stride = actx->src1_row_stride; \ + \ + const uint32_t src0_nrows = actx->src0_nrows; \ + const uint32_t src0_nrows_per_thread = actx->src0_nrows_per_thread; \ + \ + const uint32_t src0_start_row = actx->row_start + src0_nrows_per_thread * ith; \ + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, actx->row_start + src0_nrows); \ + \ + /* no work for this thread */ \ + if (src0_start_row >= src0_end_row) { \ + return; \ + } \ + \ + const uint8_t * restrict data_src0 = actx->data_src0; \ + const uint8_t * restrict data_src1 = actx->data_src1; \ + uint8_t * restrict data_dst = actx->data_dst; \ + \ + const size_t src0_row_size_aligned = actx->src0_row_size_aligned; \ + const size_t src1_row_size_aligned = actx->src1_row_size_aligned; \ + const size_t dst_row_size_aligned = actx->dst_row_size_aligned; \ + \ + uint8_t * restrict src0_spad_data = actx->vtcm_src0 + (ith * actx->vtcm_src0_size_per_thread); \ + uint8_t * restrict src1_spad_data = actx->vtcm_src1 + (ith * actx->vtcm_src1_size_per_thread); \ + uint8_t * restrict dst_spad_data = actx->vtcm_dst + (ith * actx->vtcm_dst_size_per_thread); \ + \ + size_t src0_spad_half_size = actx->src0_spad_half_size; \ + size_t src1_spad_half_size = actx->src1_spad_half_size; \ + size_t dst_spad_half_size = actx->dst_spad_half_size; \ + \ + const int BLOCK = actx->block; \ + if (BLOCK == 0) { \ + FARF(ERROR, \ + OP_STR \ + " : current VTCM reservation %zu is too small for even 1 row per thread, needed at least %zu\n", \ + actx->vtcm_src0_size_per_thread, src0_row_size_aligned); \ + return; \ + } \ + \ + dma_queue * dma_queue = actx->octx->ctx->dma[ith]; \ + \ + /* See discussion: https://github.com/ggml-org/llama.cpp/pull/18151#issuecomment-3678235379 */ \ + for (uint32_t ir = src0_start_row, spad_idx = 0; ir < src0_end_row && spad_idx < 2; ir += BLOCK, spad_idx++) { \ + const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); \ + \ + /* Dummy DMA transation for sequencing (interleaving dst,src,dst,...) */ \ + dma_queue_push_vtcm_to_ddr(dma_queue, \ + dma_make_ptr(data_dst, dst_spad_data + (spad_idx * dst_spad_half_size)), \ + dst_row_size, dst_row_size_aligned, 0); \ + \ + dma_queue_push( \ + dma_queue, \ + dma_make_ptr(src0_spad_data + (spad_idx * src0_spad_half_size), data_src0 + (ir * src0_row_stride)), \ + src0_row_size_aligned, src0_row_stride, src0_row_size, block_size); \ + dma_queue_push( \ + dma_queue, \ + dma_make_ptr(src1_spad_data + (spad_idx * src1_spad_half_size), data_src1 + (ir * src1_row_stride)), \ + src1_row_size_aligned, src1_row_stride, src1_row_size, block_size); \ + } \ + \ + for (uint32_t ir = src0_start_row; ir < src0_end_row; ir += BLOCK) { \ + const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); \ + \ + float * dst_spad = (float *) dma_queue_pop(dma_queue).src; \ + float * src0_spad = (float *) dma_queue_pop(dma_queue).dst; \ + float * src1_spad = (float *) dma_queue_pop(dma_queue).dst; \ + \ + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, ir); \ + CORE_EXPR; \ + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, ir); \ + \ + dma_queue_push_vtcm_to_ddr(dma_queue, dma_make_ptr(data_dst + (ir * dst_row_size), dst_spad), \ + dst_row_size, dst_row_size_aligned, block_size); \ + \ + /* prefetch N+2 loop iteration if any */ \ + const uint32_t pref_block = (ir + BLOCK * 2); \ + if (pref_block < src0_end_row) { \ + const uint32_t pref_block_size = MIN(BLOCK, src0_end_row - pref_block); \ + dma_queue_push(dma_queue, dma_make_ptr(src0_spad, data_src0 + (pref_block * src0_row_stride)), \ + src0_row_size_aligned, src0_row_stride, src0_row_size, pref_block_size); \ + dma_queue_push(dma_queue, dma_make_ptr(src1_spad, data_src1 + (pref_block * src1_row_stride)), \ + src1_row_size_aligned, src1_row_stride, src1_row_size, pref_block_size); \ + } \ + } \ + \ + dma_queue_flush(dma_queue); \ + \ } DEFINE_GLU_PER_THREAD(swiglu, "swiglu-f32", swiglu_f32(src0_spad, src1_spad, dst_spad, block_size, actx)) @@ -473,14 +473,30 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { } const uint32_t src0_nrows = src0->ne[1] * src0->ne[2] * src0->ne[3]; - const uint32_t n_threads = MIN(octx->n_threads, src0_nrows); + const size_t dst_row_size = dst->ne[0] * SIZEOF_FP32; + + uint32_t row_start = 0; + uint32_t nrows = src0_nrows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, sizeof(float), (uint32_t) dst_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(src0_nrows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; // row_size = bytes of useful data per row (what the kernel touches / what DMA copies). // row_stride = bytes between successive rows in DDR (may exceed row_size for non-contig src). - const size_t nc_bytes = dst->ne[0] * SIZEOF_FP32; - const size_t src0_row_size = nc_bytes; - const size_t src1_row_size = nc_bytes; - const size_t dst_row_size = nc_bytes; + const size_t nc_bytes = dst_row_size; + const size_t src0_row_size = nc_bytes; + const size_t src1_row_size = nc_bytes; const size_t src0_row_stride = src0->nb[1]; const size_t src1_row_stride = src1 ? src1->nb[1] : src0->nb[1]; @@ -518,7 +534,7 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { struct htp_act_context actx; actx.octx = octx; - actx.src0_nrows_per_thread = (src0_nrows + n_threads - 1) / n_threads; + actx.src0_nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); actx.src0_row_size = src0_row_size; actx.src1_row_size = src1_row_size; @@ -545,7 +561,8 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { actx.dst_spad_half_size = L.dst_bytes_per_thread / 2; actx.block = actx.src0_spad_half_size / actx.src0_row_size_aligned; - actx.src0_nrows = src0_nrows; + actx.src0_nrows = nrows; + actx.row_start = row_start; actx.nc = dst->ne[0]; @@ -570,7 +587,7 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { actx.data_src1 = data_src1; actx.data_dst = (uint8_t *) dst->data; - worker_pool_run_func(octx->ctx->worker_pool, act_op_func, &actx, n_threads); + work_queue_run(octx->ctx->work_queue, act_op_func, &actx, n_threads); return HTP_STATUS_OK; } diff --git a/ggml/src/ggml-hexagon/htp/allreduce-ops.c b/ggml/src/ggml-hexagon/htp/allreduce-ops.c index d35f685a6dc0..d6e7f0d10c85 100644 --- a/ggml/src/ggml-hexagon/htp/allreduce-ops.c +++ b/ggml/src/ggml-hexagon/htp/allreduce-ops.c @@ -17,6 +17,7 @@ #include "hex-dma.h" #include "hex-profile.h" #include "allreduce-ops.h" +#include "htp-fence.h" struct htp_allreduce_context { struct htp_ops_context * octx; @@ -242,7 +243,42 @@ DEFINE_ALLREDUCE_THREAD_DMA_2D(add_f32, float, hvx_add_f32_aaa, 1, 0) DEFINE_ALLREDUCE_THREAD_DMA_2D(add_bcast_f16, __fp16, hvx_add_f16_aaa, 1, 1) DEFINE_ALLREDUCE_THREAD_DMA_2D(add_bcast_f32, float, hvx_add_f32_aaa, 1, 1) +static int validate_allreduce( + struct htp_ops_context * octx, + const struct htp_allreduce_kernel_params * kparams, + uint32_t n_ranks +) { + if (!htp_ops_context_set_n_threads(octx, (uint32_t) kparams->n_threads)) { + return HTP_STATUS_INVAL_PARAMS; + } + + if (kparams->vtcm_size_per_thread <= 0 || kparams->vtcm_size <= 0) { + return HTP_STATUS_INVAL_PARAMS; + } + + const bool has_add = (octx->op == HTP_OP_ALLREDUCE_ADD); + const size_t n_vtcm_buffers = htp_allreduce_vtcm_buffer_count( + n_ranks, octx->n_threads, has_add, kparams->is_row_bcast != 0); + const size_t vtcm_size = n_vtcm_buffers * (size_t) kparams->vtcm_size_per_thread; + if (vtcm_size != (size_t) kparams->vtcm_size) { + return HTP_STATUS_INVAL_PARAMS; + } + if (vtcm_size > octx->ctx->vtcm_size) { + return HTP_STATUS_VTCM_TOO_SMALL; + } + + if (octx->dst->type != HTP_TYPE_F16 && octx->dst->type != HTP_TYPE_F32) { + return HTP_STATUS_NO_SUPPORT; + } + + return HTP_STATUS_OK; +} + int op_allreduce(struct htp_ops_context * octx) { + if (octx->ctx->mdev.count > 1 && octx->ctx->mdev.idx > 0) { + return HTP_STATUS_OK; + } + const struct htp_allreduce_kernel_params * kparams = (const struct htp_allreduce_kernel_params *) octx->kernel_params; const struct htp_tensor * dst = octx->dst; @@ -253,38 +289,53 @@ int op_allreduce(struct htp_ops_context * octx) { return HTP_STATUS_INVAL_PARAMS; } - if (dst->type != HTP_TYPE_F16 && dst->type != HTP_TYPE_F32) { - return HTP_STATUS_NO_SUPPORT; + const uint32_t fence_seq_entry = (uint32_t) octx->op_params[0]; + const uint32_t fence_seq_exit = (uint32_t) octx->op_params[1]; + + const struct htp_tensor * my_sync = octx->src[n_ranks + rank]; + atomic_uint * my_fence = (atomic_uint *) (uintptr_t) my_sync->data; + + const int status = validate_allreduce(octx, kparams, n_ranks); + if (status != HTP_STATUS_OK) { + if (status == HTP_STATUS_NO_SUPPORT) { + FARF(ERROR, "ggml-hex: allreduce unsupported type %d : rank %u\n", dst->type, rank); + } + htp_fence_write(my_fence, fence_seq_exit, status); + return status; } + const bool has_add = (octx->op == HTP_OP_ALLREDUCE_ADD); const uint32_t nelem = dst->ne[0] * dst->ne[1] * dst->ne[2] * dst->ne[3]; - const uint32_t fence_seq_entry = (uint32_t) octx->op_params[0]; - const uint32_t fence_seq_exit = (uint32_t) octx->op_params[1]; // 1. Entry Barrier: Synchronize all ranks before reading struct htp_thread_trace * tr0 = &octx->ctx->trace[0]; htp_trace_event_start(tr0, HTP_TRACE_EVT_FENCE, (uint16_t) fence_seq_entry); - const struct htp_tensor * my_sync = octx->src[n_ranks + rank]; - atomic_uint * my_fence = (atomic_uint *) my_sync->data; - - atomic_store(&my_fence[0], fence_seq_entry); - asm volatile ("syncht" : : : "memory"); - Q6_dccleaninva_A((void *) my_fence); + htp_fence_write(my_fence, fence_seq_entry, octx->status); for (uint32_t j = 0; j < n_ranks; j++) { if (j == rank) continue; const struct htp_tensor * peer_sync = octx->src[n_ranks + j]; - atomic_uint * peer_fence = (atomic_uint *) peer_sync->data; + atomic_uint * peer_fence = (atomic_uint *) (uintptr_t) peer_sync->data; uint64_t spins = 0; while (1) { - Q6_dccleaninva_A((void *) peer_fence); - uint32_t val = atomic_load(&peer_fence[0]); - if (val == fence_seq_entry || val == fence_seq_exit) { + uint32_t peer_seq; + uint32_t peer_status; + htp_fence_read(peer_fence, &peer_seq, &peer_status); + if ((int32_t)(peer_seq - fence_seq_entry) >= 0) { + if (peer_status > HTP_STATUS_OK) { + FARF(ERROR, "ggml-hex: allreduce entry peer %u failed with status %u\n", j, peer_status); + htp_fence_write(my_fence, fence_seq_exit, peer_status); + htp_trace_event_stop(tr0, HTP_TRACE_EVT_FENCE, (uint16_t) fence_seq_entry); + return peer_status; + } break; } if (++spins > HTP_FENCE_TIMEOUT) { - FARF(ERROR, "ggml-hex: allreduce entry fence-wait TIMEOUT: rank %u waiting on %u (fence %p seq %u)\n", rank, j, peer_fence, fence_seq_entry); + FARF(ERROR, "ggml-hex: allreduce entry fence-wait TIMEOUT : rank %u waiting on %u fence %p seq 0x%x peer-seq 0x%x\n", + rank, j, peer_fence, fence_seq_entry, peer_seq); + htp_fence_write(my_fence, fence_seq_exit, HTP_STATUS_INTERNAL_ERR); + htp_trace_event_stop(tr0, HTP_TRACE_EVT_FENCE, (uint16_t) fence_seq_entry); return HTP_STATUS_INTERNAL_ERR; } hex_pause(); @@ -301,8 +352,6 @@ int op_allreduce(struct htp_ops_context * octx) { const uint32_t elems_per_thread = (uint32_t) kparams->elems_per_thread; const uint32_t vtcm_size_per_thread = (uint32_t) kparams->vtcm_size_per_thread; - const bool has_add = (octx->op == HTP_OP_ALLREDUCE_ADD); - struct htp_allreduce_context actx; actx.octx = octx; actx.n_ranks = n_ranks; @@ -339,6 +388,8 @@ int op_allreduce(struct htp_ops_context * octx) { } break; default: + FARF(ERROR, "ggml-hex: allreduce unsupported kernel %d : rank %u\n", kparams->kernel_type, rank); + htp_fence_write(my_fence, fence_seq_exit, HTP_STATUS_NO_SUPPORT); return HTP_STATUS_NO_SUPPORT; } @@ -368,23 +419,31 @@ int op_allreduce(struct htp_ops_context * octx) { // 4. Exit Barrier: Synchronize all ranks after writing htp_trace_event_start(tr0, HTP_TRACE_EVT_FENCE, (uint16_t) fence_seq_exit); - atomic_store(&my_fence[0], fence_seq_exit); - asm volatile ("syncht" : : : "memory"); - Q6_dccleaninva_A((void *) my_fence); + htp_fence_write(my_fence, fence_seq_exit, octx->status); for (uint32_t j = 0; j < n_ranks; j++) { if (j == rank) continue; const struct htp_tensor * peer_sync = octx->src[n_ranks + j]; - atomic_uint * peer_fence = (atomic_uint *) peer_sync->data; + atomic_uint * peer_fence = (atomic_uint *) (uintptr_t) peer_sync->data; uint64_t spins = 0; while (1) { - Q6_dccleaninva_A((void *) peer_fence); - uint32_t val = atomic_load(&peer_fence[0]); - if (val == fence_seq_exit) { + uint32_t peer_seq; + uint32_t peer_status; + htp_fence_read(peer_fence, &peer_seq, &peer_status); + if ((int32_t)(peer_seq - fence_seq_exit) >= 0) { + if (peer_status > HTP_STATUS_OK) { + FARF(ERROR, "ggml-hex: allreduce exit peer %u failed with status %u\n", j, peer_status); + htp_fence_write(my_fence, fence_seq_exit, peer_status); + htp_trace_event_stop(tr0, HTP_TRACE_EVT_FENCE, (uint16_t) fence_seq_exit); + return peer_status; + } break; } if (++spins > HTP_FENCE_TIMEOUT) { - FARF(ERROR, "ggml-hex: allreduce exit fence-wait TIMEOUT: rank %u waiting on %u (fence %p seq %u)\n", rank, j, peer_fence, fence_seq_exit); + FARF(ERROR, "ggml-hex: allreduce exit fence-wait TIMEOUT : rank %u waiting on %u fence %p seq 0x%x peer-seq 0x%x\n", + rank, j, peer_fence, fence_seq_exit, peer_seq); + htp_fence_write(my_fence, fence_seq_exit, HTP_STATUS_INTERNAL_ERR); + htp_trace_event_stop(tr0, HTP_TRACE_EVT_FENCE, (uint16_t) fence_seq_exit); return HTP_STATUS_INTERNAL_ERR; } hex_pause(); @@ -394,5 +453,5 @@ int op_allreduce(struct htp_ops_context * octx) { htp_trace_event_stop(tr0, HTP_TRACE_EVT_FENCE, (uint16_t) fence_seq_exit); - return HTP_STATUS_OK; + return octx->status; } diff --git a/ggml/src/ggml-hexagon/htp/allreduce-ops.h b/ggml/src/ggml-hexagon/htp/allreduce-ops.h index de447d87e912..0aed2b8b7e67 100644 --- a/ggml/src/ggml-hexagon/htp/allreduce-ops.h +++ b/ggml/src/ggml-hexagon/htp/allreduce-ops.h @@ -2,6 +2,8 @@ #define ALLREDUCE_OPS_H #include <stdint.h> +#include <stddef.h> +#include <stdbool.h> #define HTP_ALLREDUCE_MAX_RANKS 4 @@ -15,6 +17,15 @@ enum htp_allreduce_kernel_type { HTP_ALLREDUCE_KERNEL_DMA_2D, }; +static inline size_t htp_allreduce_vtcm_buffer_count( + uint32_t n_ranks, + uint32_t n_threads, + bool has_add, + bool is_row_bcast +) { + return (size_t) (n_ranks + 1) * n_threads + (has_add ? (is_row_bcast ? 1 : n_threads) : 0); +} + struct htp_allreduce_kernel_params { int32_t rank; int32_t n_ranks; diff --git a/ggml/src/ggml-hexagon/htp/argsort-ops.c b/ggml/src/ggml-hexagon/htp/argsort-ops.c index 774faef5f388..e3c49e763d41 100644 --- a/ggml/src/ggml-hexagon/htp/argsort-ops.c +++ b/ggml/src/ggml-hexagon/htp/argsort-ops.c @@ -11,9 +11,10 @@ #include "hvx-utils.h" #include "hex-dma.h" +#include "hex-common.h" #include "htp-ctx.h" #include "htp-ops.h" -#include "htp-ops.h" +#include "htp-tensor.h" #ifndef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) @@ -22,6 +23,9 @@ struct htp_argsort_context { struct htp_ops_context * octx; uint32_t nrows_per_thread; + uint32_t total_rows; + uint32_t row_start; + uint32_t row_end; uint8_t * vtcm_base; size_t vtcm_per_thread; }; @@ -336,10 +340,9 @@ static void htp_argsort_f32_##ne00##_##order_name(unsigned int n, unsigned int i const struct htp_tensor * src0 = octx->src[0]; \ const struct htp_tensor * dst = octx->dst; \ uint8_t * spad = actx->vtcm_base + actx->vtcm_per_thread * i; \ - uint32_t total_rows = src0->ne[1] * src0->ne[2] * src0->ne[3]; \ uint32_t rows_per_thread = actx->nrows_per_thread; \ - uint32_t start_row = rows_per_thread * i; \ - uint32_t end_row = MIN(start_row + rows_per_thread, total_rows); \ + uint32_t start_row = actx->row_start + rows_per_thread * i; \ + uint32_t end_row = MIN(start_row + rows_per_thread, actx->row_end); \ size_t values_size = hex_round_up(ne00 * sizeof(float), 128); \ float * values_buf = (float *) spad; \ int32_t * indices_buf = (int32_t *) (spad + values_size); \ @@ -386,9 +389,6 @@ static void htp_argsort_f32_fallback(unsigned int n, unsigned int i, void * data // Dimensions uint32_t ne00 = src0->ne[0]; - uint32_t ne01 = src0->ne[1]; - uint32_t ne02 = src0->ne[2]; - uint32_t ne03 = src0->ne[3]; uint32_t nb01 = src0->nb[1]; @@ -398,10 +398,9 @@ static void htp_argsort_f32_fallback(unsigned int n, unsigned int i, void * data enum ggml_sort_order order = (enum ggml_sort_order) octx->op_params[0]; // Rows to process - uint32_t total_rows = ne01 * ne02 * ne03; uint32_t rows_per_thread = actx->nrows_per_thread; - uint32_t start_row = rows_per_thread * i; - uint32_t end_row = MIN(start_row + rows_per_thread, total_rows); + uint32_t start_row = actx->row_start + rows_per_thread * i; + uint32_t end_row = MIN(start_row + rows_per_thread, actx->row_end); size_t values_size = hex_round_up(ne00 * sizeof(float), 128); uint32_t num_vec_ind_values = hmx_ceil_div(ne00, VLEN/(sizeof(int32_t))); @@ -451,8 +450,28 @@ int op_argsort(struct htp_ops_context * octx) { return HTP_STATUS_NO_SUPPORT; } - const uint32_t total_rows = octx->src[0]->ne[1] * octx->src[0]->ne[2] * octx->src[0]->ne[3]; - const uint32_t n_threads = MIN(total_rows, octx->n_threads); + const struct htp_tensor * src0 = octx->src[0]; + const struct htp_tensor * dst = octx->dst; + + const uint32_t total_rows = src0->ne[1] * src0->ne[2] * src0->ne[3]; + const size_t dst_row_size = dst->ne[0] * sizeof(int32_t); + + uint32_t row_start = 0; + uint32_t row_end = total_rows; + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, sizeof(int32_t), (uint32_t) dst_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_rows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + row_end = range.start + range.count; + } + + const uint32_t nrows = row_end - row_start; + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; // Allocate scratchpad // We need 1 row of float + 1 row of int32 per thread. @@ -478,7 +497,10 @@ int op_argsort(struct htp_ops_context * octx) { struct htp_argsort_context actx; actx.octx = octx; - actx.nrows_per_thread = (total_rows + n_threads - 1) / n_threads; + actx.nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); + actx.total_rows = nrows; + actx.row_start = row_start; + actx.row_end = row_end; actx.vtcm_base = (uint8_t *) octx->ctx->vtcm_base; actx.vtcm_per_thread = spad_per_thread; @@ -508,7 +530,7 @@ int op_argsort(struct htp_ops_context * octx) { } // Run jobs - worker_pool_run_func(octx->ctx->worker_pool, job_func, &actx, n_threads); + work_queue_run(octx->ctx->work_queue, job_func, &actx, n_threads); return HTP_STATUS_OK; } diff --git a/ggml/src/ggml-hexagon/htp/binary-ops.c b/ggml/src/ggml-hexagon/htp/binary-ops.c index db6177963541..bfa849e0edbf 100644 --- a/ggml/src/ggml-hexagon/htp/binary-ops.c +++ b/ggml/src/ggml-hexagon/htp/binary-ops.c @@ -13,9 +13,10 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" +#include "hex-common.h" +#include "hex-profile.h" #include "htp-ctx.h" #include "htp-ops.h" -#include "htp-ops.h" #include "htp-tensor.h" #ifndef MIN @@ -36,6 +37,8 @@ struct htp_binary_context { uint32_t block_max; uint32_t nrows_per_thread; + uint32_t total_rows; + uint32_t row_start; size_t src0_row_size_aligned; size_t src1_row_size_aligned; size_t dst_row_size_aligned; @@ -48,27 +51,27 @@ struct htp_binary_context { const struct htp_tensor * src0 = octx->src[0]; \ const struct htp_tensor * src1 = octx->src[1]; \ const struct htp_tensor * dst = octx->dst; \ - \ - const uint32_t ne00 = src0->ne[0]; \ - const uint32_t ne01 = src0->ne[1]; \ - const uint32_t ne02 = src0->ne[2]; \ - const uint32_t ne03 = src0->ne[3]; \ - \ - const uint32_t ne10 = src1->ne[0]; \ - const uint32_t ne11 = src1->ne[1]; \ - const uint32_t ne12 = src1->ne[2]; \ - const uint32_t ne13 = src1->ne[3]; \ - \ - const uint32_t nb01 = src0->nb[1]; \ - const uint32_t nb02 = src0->nb[2]; \ - const uint32_t nb03 = src0->nb[3]; \ - \ - const uint32_t nb11 = src1->nb[1]; \ - const uint32_t nb12 = src1->nb[2]; \ - const uint32_t nb13 = src1->nb[3]; \ - \ - const uint32_t nb1 = dst->nb[1]; \ - const uint32_t nb2 = dst->nb[2]; \ + \ + const uint32_t ne00 = src0->ne[0]; \ + const uint32_t ne01 = src0->ne[1]; \ + const uint32_t ne02 = src0->ne[2]; \ + const uint32_t ne03 = src0->ne[3]; \ + \ + const uint32_t ne10 = src1->ne[0]; \ + const uint32_t ne11 = src1->ne[1]; \ + const uint32_t ne12 = src1->ne[2]; \ + const uint32_t ne13 = src1->ne[3]; \ + \ + const uint32_t nb01 = src0->nb[1]; \ + const uint32_t nb02 = src0->nb[2]; \ + const uint32_t nb03 = src0->nb[3]; \ + \ + const uint32_t nb11 = src1->nb[1]; \ + const uint32_t nb12 = src1->nb[2]; \ + const uint32_t nb13 = src1->nb[3]; \ + \ + const uint32_t nb1 = dst->nb[1]; \ + const uint32_t nb2 = dst->nb[2]; \ const uint32_t nb3 = dst->nb[3]; static inline uint32_t calc_block_size(struct htp_binary_context * bctx, uint32_t ir, uint32_t end_row, uint32_t ne01, uint32_t ne02) { @@ -93,87 +96,87 @@ static inline uint32_t calc_block_size(struct htp_binary_context * bctx, uint32_ } // Macro for scalar op switch -#define COMPUTE_SCALAR_OP(DST, SRC, VAL, TYPE, N) \ - if(TYPE == HTP_TYPE_F32) { \ - switch (octx->op) { \ - case HTP_OP_ADD: hvx_add_scalar_f32_aa(DST, SRC, *(float *)VAL, N); break; \ - case HTP_OP_SUB: hvx_sub_scalar_f32_aa(DST, SRC, *(float *)VAL, N); break; \ - case HTP_OP_MUL: hvx_mul_scalar_f32_aa(DST, SRC, *(float *)VAL, N); break; \ +#define COMPUTE_SCALAR_OP(DST, SRC, VAL, TYPE, N) \ + if(TYPE == HTP_TYPE_F32) { \ + switch (octx->op) { \ + case HTP_OP_ADD: hvx_add_scalar_f32_aa(DST, SRC, *(float *)VAL, N); break; \ + case HTP_OP_SUB: hvx_sub_scalar_f32_aa(DST, SRC, *(float *)VAL, N); break; \ + case HTP_OP_MUL: hvx_mul_scalar_f32_aa(DST, SRC, *(float *)VAL, N); break; \ case HTP_OP_DIV: hvx_mul_scalar_f32_aa(DST, SRC, 1.0f / (*(float *)VAL), N); break; \ - default: break; \ - } \ - } \ - else { \ - switch (octx->op) { \ - case HTP_OP_ADD: hvx_add_scalar_f16_aa(DST, SRC, *(_Float16 *)VAL, N); break; \ - case HTP_OP_SUB: hvx_sub_scalar_f16_aa(DST, SRC, *(_Float16 *)VAL, N); break; \ - case HTP_OP_MUL: hvx_mul_scalar_f16_aa(DST, SRC, *(_Float16 *)VAL, N); break; \ - case HTP_OP_DIV: hvx_div_scalar_f16_aa(DST, SRC, *(_Float16 *)VAL, N); break; \ - default: break; \ - } \ + default: break; \ + } \ + } \ + else { \ + switch (octx->op) { \ + case HTP_OP_ADD: hvx_add_scalar_f16_aa(DST, SRC, *(_Float16 *)VAL, N); break; \ + case HTP_OP_SUB: hvx_sub_scalar_f16_aa(DST, SRC, *(_Float16 *)VAL, N); break; \ + case HTP_OP_MUL: hvx_mul_scalar_f16_aa(DST, SRC, *(_Float16 *)VAL, N); break; \ + case HTP_OP_DIV: hvx_div_scalar_f16_aa(DST, SRC, *(_Float16 *)VAL, N); break; \ + default: break; \ + } \ } // Macro for vector op switch (All Aligned) -#define COMPUTE_VECTOR_OP_AAA(DST, SRC0, SRC1, TYPE, N) \ - if(TYPE == HTP_TYPE_F32) { \ - switch (octx->op) { \ +#define COMPUTE_VECTOR_OP_AAA(DST, SRC0, SRC1, TYPE, N) \ + if(TYPE == HTP_TYPE_F32) { \ + switch (octx->op) { \ case HTP_OP_ADD: hvx_add_f32_aaa(DST, SRC0, SRC1, N); break; \ case HTP_OP_SUB: hvx_sub_f32_aaa(DST, SRC0, SRC1, N); break; \ case HTP_OP_MUL: hvx_mul_f32_aaa(DST, SRC0, SRC1, N); break; \ case HTP_OP_DIV: hvx_div_f32_aaa(DST, SRC0, SRC1, N); break; \ - default: break; \ - } \ - } \ - else { \ - switch (octx->op) { \ + default: break; \ + } \ + } \ + else { \ + switch (octx->op) { \ case HTP_OP_ADD: hvx_add_f16_aaa(DST, SRC0, SRC1, N); break; \ case HTP_OP_SUB: hvx_sub_f16_aaa(DST, SRC0, SRC1, N); break; \ case HTP_OP_MUL: hvx_mul_f16_aaa(DST, SRC0, SRC1, N); break; \ case HTP_OP_DIV: hvx_div_f16_aaa(DST, SRC0, SRC1, N); break; \ - default: break; \ - } \ + default: break; \ + } \ } // Macro for vector op switch (Dst Aligned, Src0 Aligned, Src1 Unaligned) -#define COMPUTE_VECTOR_OP_AAU(DST, SRC0, SRC1, TYPE, N) \ - if(TYPE == HTP_TYPE_F32) { \ - switch (octx->op) { \ +#define COMPUTE_VECTOR_OP_AAU(DST, SRC0, SRC1, TYPE, N) \ + if(TYPE == HTP_TYPE_F32) { \ + switch (octx->op) { \ case HTP_OP_ADD: hvx_add_f32_aau(DST, SRC0, SRC1, N); break; \ case HTP_OP_SUB: hvx_sub_f32_aau(DST, SRC0, SRC1, N); break; \ case HTP_OP_MUL: hvx_mul_f32_aau(DST, SRC0, SRC1, N); break; \ case HTP_OP_DIV: hvx_div_f32_aau(DST, SRC0, SRC1, N); break; \ - default: break; \ - } \ - } \ - else { \ - switch (octx->op) { \ + default: break; \ + } \ + } \ + else { \ + switch (octx->op) { \ case HTP_OP_ADD: hvx_add_f16_aau(DST, SRC0, SRC1, N); break; \ case HTP_OP_SUB: hvx_sub_f16_aau(DST, SRC0, SRC1, N); break; \ case HTP_OP_MUL: hvx_mul_f16_aau(DST, SRC0, SRC1, N); break; \ case HTP_OP_DIV: hvx_div_f16_aau(DST, SRC0, SRC1, N); break; \ - default: break; \ - } \ + default: break; \ + } \ } // Macro for vector op switch (All Unaligned - generic loop used in element repeat) -#define COMPUTE_VECTOR_OP_UUU(DST, SRC0, SRC1, TYPE, N) \ - if(TYPE == HTP_TYPE_F32) { \ - switch (octx->op) { \ +#define COMPUTE_VECTOR_OP_UUU(DST, SRC0, SRC1, TYPE, N) \ + if(TYPE == HTP_TYPE_F32) { \ + switch (octx->op) { \ case HTP_OP_ADD: hvx_add_f32_uuu(DST, SRC0, SRC1, N); break; \ case HTP_OP_SUB: hvx_sub_f32_uuu(DST, SRC0, SRC1, N); break; \ case HTP_OP_MUL: hvx_mul_f32_uuu(DST, SRC0, SRC1, N); break; \ case HTP_OP_DIV: hvx_div_f32_uuu(DST, SRC0, SRC1, N); break; \ - default: break; \ - } \ - } \ - else { \ - switch (octx->op) { \ + default: break; \ + } \ + } \ + else { \ + switch (octx->op) { \ case HTP_OP_ADD: hvx_add_f16_uuu(DST, SRC0, SRC1, N); break; \ case HTP_OP_SUB: hvx_sub_f16_uuu(DST, SRC0, SRC1, N); break; \ case HTP_OP_MUL: hvx_mul_f16_uuu(DST, SRC0, SRC1, N); break; \ case HTP_OP_DIV: hvx_div_f16_uuu(DST, SRC0, SRC1, N); break; \ - default: break; \ - } \ + default: break; \ + } \ } // 1. Scalar src1 (ne10 == 1) @@ -184,9 +187,8 @@ static void binary_job_scalar(unsigned int nth, unsigned int ith, void * data) { const uint32_t src0_type = octx->src[0]->type; const uint32_t row_size_bytes = (src0_type == HTP_TYPE_F32) ? ne00 * sizeof(float) : ne00 * sizeof(_Float16); - const uint32_t total_rows = ne01 * ne02 * ne03; - const uint32_t start_row = bctx->nrows_per_thread * ith; - const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, total_rows); + const uint32_t start_row = bctx->row_start + bctx->nrows_per_thread * ith; + const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, bctx->row_start + bctx->total_rows); if (start_row >= end_row) return; FARF(HIGH, "binary-scalar: %d/%d (%u:%u) row-size %u (%u)", ith, nth, start_row, end_row, nb01, bctx->dst_row_size_aligned); @@ -222,6 +224,8 @@ static void binary_job_scalar(unsigned int nth, unsigned int ith, void * data) { } // Main loop + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = start_row; ir < end_row; ) { uint32_t current_block_size = calc_block_size(bctx, ir, end_row, ne01, ne02); @@ -242,12 +246,14 @@ static void binary_job_scalar(unsigned int nth, unsigned int ith, void * data) { uint8_t * src1_ptr = (uint8_t *)src1->data + i13 * nb13 + i12 * nb12 + i11 * nb11; uint32_t s1_stride = (ne11 == 1) ? 0 : nb11; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); for (uint32_t r = 0; r < current_block_size; r++) { uint8_t * r_src0 = s0_spad + r * bctx->src0_row_size_aligned; uint8_t * r_dst = d_spad + r * bctx->dst_row_size_aligned; COMPUTE_SCALAR_OP(r_dst, r_src0, src1_ptr, src0_type, ne00); src1_ptr += s1_stride; } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); uint8_t * dst_curr = (uint8_t *)dst->data + i03 * nb3 + i02 * nb2 + i01 * nb1; dma_queue_push(q, dma_make_ptr(dst_curr, d_spad), nb1, bctx->dst_row_size_aligned, row_size_bytes, current_block_size); @@ -266,6 +272,7 @@ static void binary_job_scalar(unsigned int nth, unsigned int ith, void * data) { } ir += current_block_size; } + dma_queue_flush(q); } @@ -277,9 +284,8 @@ static void binary_job_vector_same_shape(unsigned int nth, unsigned int ith, voi const uint32_t src0_type = octx->src[0]->type; const uint32_t row_size_bytes = (src0_type == HTP_TYPE_F32) ? ne00 * sizeof(float) : ne00 * sizeof(_Float16); - const uint32_t total_rows = ne01 * ne02 * ne03; - const uint32_t start_row = bctx->nrows_per_thread * ith; - const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, total_rows); + const uint32_t start_row = bctx->row_start + bctx->nrows_per_thread * ith; + const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, bctx->row_start + bctx->total_rows); if (start_row >= end_row) return; FARF(HIGH, "binary-same-shape: %d/%d (%u:%u) row-size %u (%u)", ith, nth, start_row, end_row, nb01, bctx->dst_row_size_aligned); @@ -323,18 +329,22 @@ static void binary_job_vector_same_shape(unsigned int nth, unsigned int ith, voi spad_idx ^= 1; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = start_row; ir < end_row; ) { uint32_t current_block_size = calc_block_size(bctx, ir, end_row, ne01, ne02); uint8_t * d_spad = (uint8_t *) dma_queue_pop(q).src; uint8_t * s0_spad = (uint8_t *) dma_queue_pop(q).dst; uint8_t * s1_spad = (uint8_t *) dma_queue_pop(q).dst; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); for (uint32_t r = 0; r < current_block_size; r++) { uint8_t * r_src0 = s0_spad + r * bctx->src0_row_size_aligned; uint8_t * r_src1 = s1_spad + r * bctx->src1_row_size_aligned; uint8_t * r_dst = d_spad + r * bctx->dst_row_size_aligned; COMPUTE_VECTOR_OP_AAA(r_dst, r_src0, r_src1, src0_type, ne00); } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); uint32_t i03, i02, i01, rem; i03 = fastdiv(ir, &bctx->src0_dim12_div); @@ -366,6 +376,7 @@ static void binary_job_vector_same_shape(unsigned int nth, unsigned int ith, voi } ir += current_block_size; } + dma_queue_flush(q); } @@ -377,9 +388,8 @@ static void binary_job_vector_row_broadcast(unsigned int nth, unsigned int ith, const uint32_t src0_type = octx->src[0]->type; const uint32_t row_size_bytes = (src0_type == HTP_TYPE_F32) ? ne00 * sizeof(float) : ne00 * sizeof(_Float16); - const uint32_t total_rows = ne01 * ne02 * ne03; - const uint32_t start_row = bctx->nrows_per_thread * ith; - const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, total_rows); + const uint32_t start_row = bctx->row_start + bctx->nrows_per_thread * ith; + const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, bctx->row_start + bctx->total_rows); if (start_row >= end_row) return; FARF(HIGH, "binary-row-bcast: %d/%d (%u:%u) row-size %u (%u)", ith, nth, start_row, end_row, nb01, bctx->dst_row_size_aligned); @@ -416,17 +426,21 @@ static void binary_job_vector_row_broadcast(unsigned int nth, unsigned int ith, spad_idx ^= 1; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = start_row; ir < end_row; ) { uint32_t current_block_size = calc_block_size(bctx, ir, end_row, ne01, ne02); uint8_t * d_spad = (uint8_t *) dma_queue_pop(q).src; uint8_t * s0_spad = (uint8_t *) dma_queue_pop(q).dst; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); for (uint32_t r = 0; r < current_block_size; r++) { uint8_t * r_src0 = s0_spad + r * bctx->src0_row_size_aligned; uint8_t * r_src1 = (uint8_t *)s1_ptr; // Constant uint8_t * r_dst = d_spad + r * bctx->dst_row_size_aligned; COMPUTE_VECTOR_OP_AAA(r_dst, r_src0, r_src1, src0_type, ne00); } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); uint32_t i03 = fastdiv(ir, &bctx->src0_dim12_div); uint32_t rem = ir - i03 * (ne02 * ne01); @@ -447,6 +461,7 @@ static void binary_job_vector_row_broadcast(unsigned int nth, unsigned int ith, } ir += current_block_size; } + dma_queue_flush(q); } @@ -458,9 +473,8 @@ static void binary_job_vector_complex(unsigned int nth, unsigned int ith, void * const uint32_t src0_type = octx->src[0]->type; const uint32_t row_size_bytes = (src0_type == HTP_TYPE_F32) ? ne00 * sizeof(float) : ne00 * sizeof(_Float16); - const uint32_t total_rows = ne01 * ne02 * ne03; - const uint32_t start_row = bctx->nrows_per_thread * ith; - const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, total_rows); + const uint32_t start_row = bctx->row_start + bctx->nrows_per_thread * ith; + const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, bctx->row_start + bctx->total_rows); if (start_row >= end_row) return; FARF(HIGH, "binary-complex: %d/%d (%u:%u) row-size %u (%u)", ith, nth, start_row, end_row, nb01, bctx->dst_row_size_aligned); @@ -493,6 +507,8 @@ static void binary_job_vector_complex(unsigned int nth, unsigned int ith, void * spad_idx ^= 1; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = start_row; ir < end_row; ) { uint32_t current_block_size = calc_block_size(bctx, ir, end_row, ne01, ne02); uint8_t * d_spad = (uint8_t *) dma_queue_pop(q).src; @@ -503,6 +519,7 @@ static void binary_job_vector_complex(unsigned int nth, unsigned int ith, void * uint32_t i02 = fastdiv(rem, &bctx->src0_dim1_div); uint32_t i01 = rem - i02 * ne01; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); for (uint32_t r = 0; r < current_block_size; r++) { uint32_t r_i01 = i01 + r; uint32_t i13 = fastmodulo(i03, ne13, &bctx->src1_dim3_div); @@ -516,6 +533,7 @@ static void binary_job_vector_complex(unsigned int nth, unsigned int ith, void * // Read src1 from DDR (unaligned) COMPUTE_VECTOR_OP_AAU(r_dst, r_src0, r_src1, src0_type, ne00); } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); uint8_t * dst_curr = (uint8_t *)dst->data + i03 * nb3 + i02 * nb2 + i01 * nb1; dma_queue_push(q, dma_make_ptr(dst_curr, d_spad), nb1, bctx->dst_row_size_aligned, row_size_bytes, current_block_size); @@ -532,6 +550,7 @@ static void binary_job_vector_complex(unsigned int nth, unsigned int ith, void * } ir += current_block_size; } + dma_queue_flush(q); } @@ -544,9 +563,8 @@ static void binary_job_element_repeat(unsigned int nth, unsigned int ith, void * const uint32_t src0_type = octx->src[0]->type; const uint32_t elem_size_bytes = (src0_type == HTP_TYPE_F32) ? sizeof(float) : sizeof(_Float16); const uint32_t row_size_bytes = ne00 * elem_size_bytes;; - const uint32_t total_rows = ne01 * ne02 * ne03; - const uint32_t start_row = bctx->nrows_per_thread * ith; - const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, total_rows); + const uint32_t start_row = bctx->row_start + bctx->nrows_per_thread * ith; + const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, bctx->row_start + bctx->total_rows); if (start_row >= end_row) return; uint8_t * src0_spad_base = octx->src0_spad.data + (ith * octx->src0_spad.size_per_thread); @@ -579,6 +597,8 @@ static void binary_job_element_repeat(unsigned int nth, unsigned int ith, void * spad_idx ^= 1; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = start_row; ir < end_row; ) { uint32_t current_block_size = calc_block_size(bctx, ir, end_row, ne01, ne02); uint8_t * d_spad = (uint8_t *) dma_queue_pop(q).src; @@ -589,6 +609,7 @@ static void binary_job_element_repeat(unsigned int nth, unsigned int ith, void * uint32_t i02 = fastdiv(rem, &bctx->src0_dim1_div); uint32_t i01 = rem - i02 * ne01; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); for (uint32_t r = 0; r < current_block_size; r++) { uint32_t r_i01 = i01 + r; uint32_t i13 = fastmodulo(i03, ne13, &bctx->src1_dim3_div); @@ -606,6 +627,7 @@ static void binary_job_element_repeat(unsigned int nth, unsigned int ith, void * COMPUTE_VECTOR_OP_UUU(r_dst + c * elem_size_bytes, r_src0 + c * elem_size_bytes, r_src1_row, src0_type, len); } } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); uint8_t * dst_curr = (uint8_t *)dst->data + i03 * nb3 + i02 * nb2 + i01 * nb1; dma_queue_push(q, dma_make_ptr(dst_curr, d_spad), nb1, bctx->dst_row_size_aligned, row_size_bytes, current_block_size); @@ -622,6 +644,7 @@ static void binary_job_element_repeat(unsigned int nth, unsigned int ith, void * } ir += current_block_size; } + dma_queue_flush(q); } @@ -650,9 +673,8 @@ static void binary_job_add_id(unsigned int nth, unsigned int ith, void * data) { const uint32_t nb2 = dst->nb[2]; const uint32_t nb3 = dst->nb[3]; - const uint32_t total_rows = ne01 * ne02 * ne03; - const uint32_t start_row = bctx->nrows_per_thread * ith; - const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, total_rows); + const uint32_t start_row = bctx->row_start + bctx->nrows_per_thread * ith; + const uint32_t end_row = MIN(start_row + bctx->nrows_per_thread, bctx->row_start + bctx->total_rows); if (start_row >= end_row) return; uint8_t * src0_spad_base = octx->src0_spad.data + (ith * octx->src0_spad.size_per_thread); @@ -683,6 +705,8 @@ static void binary_job_add_id(unsigned int nth, unsigned int ith, void * data) { spad_idx ^= 1; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = start_row; ir < end_row; ) { uint32_t current_block_size = calc_block_size(bctx, ir, end_row, ne01, ne02); uint8_t * d_spad = (uint8_t *) dma_queue_pop(q).src; @@ -693,6 +717,7 @@ static void binary_job_add_id(unsigned int nth, unsigned int ith, void * data) { uint32_t i02 = fastdiv(rem, &bctx->src0_dim1_div); uint32_t i01 = rem - i02 * ne01; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); for (uint32_t r = 0; r < current_block_size; r++) { uint32_t r_i01 = i01 + r; // linear within block since we split at ne01 @@ -704,6 +729,7 @@ static void binary_job_add_id(unsigned int nth, unsigned int ith, void * data) { hvx_add_f32_aau(r_dst, r_src0, r_src1, ne00); } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); uint8_t * dst_curr = (uint8_t *)dst->data + i03 * nb3 + i02 * nb2 + i01 * nb1; dma_queue_push(q, dma_make_ptr(dst_curr, d_spad), nb1, bctx->dst_row_size_aligned, ne00 * sizeof(float), current_block_size); @@ -720,6 +746,7 @@ static void binary_job_add_id(unsigned int nth, unsigned int ith, void * data) { } ir += current_block_size; } + dma_queue_flush(q); } @@ -729,15 +756,31 @@ static int execute_op_binary(struct htp_ops_context * octx) { const struct htp_tensor * dst = octx->dst; const uint32_t src0_nrows = src0->ne[1] * src0->ne[2] * src0->ne[3]; - const uint32_t n_threads = MIN(octx->n_threads, src0_nrows); - // Use packed row sizes for VTCM allocation + // Use packed row sizes for VTCM allocation and alignment const uint32_t src0_type = octx->src[0]->type; const size_t elem_size = (src0_type == HTP_TYPE_F32) ? sizeof(float) : sizeof(_Float16); const size_t src0_row_size = src0->ne[0] * elem_size; const size_t src1_row_size = src1->ne[0] * elem_size; const size_t dst_row_size = dst->ne[0] * elem_size; + uint32_t row_start = 0; + uint32_t nrows = src0_nrows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, (uint32_t) elem_size, (uint32_t) dst_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(src0_nrows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + size_t src0_row_size_aligned = hex_round_up(src0_row_size, VLEN); size_t src1_row_size_aligned = hex_round_up(src1_row_size, VLEN); size_t dst_row_size_aligned = hex_round_up(dst_row_size, VLEN); @@ -815,7 +858,9 @@ static int execute_op_binary(struct htp_ops_context * octx) { struct htp_binary_context bctx; bctx.octx = octx; - bctx.nrows_per_thread = (src0_nrows + n_threads - 1) / n_threads; + bctx.nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); + bctx.total_rows = nrows; + bctx.row_start = row_start; bctx.block_max = rows_per_buffer; bctx.src0_row_size_aligned = src0_row_size_aligned; bctx.src1_row_size_aligned = src1_row_size_aligned; @@ -850,7 +895,7 @@ static int execute_op_binary(struct htp_ops_context * octx) { dma_queue_pop(q); } - worker_pool_run_func(octx->ctx->worker_pool, worker_func, &bctx, n_threads); + work_queue_run(octx->ctx->work_queue, worker_func, &bctx, n_threads); return HTP_STATUS_OK; } @@ -870,4 +915,3 @@ int op_binary(struct htp_ops_context * octx) { return HTP_STATUS_NO_SUPPORT; } - diff --git a/ggml/src/ggml-hexagon/htp/concat-ops.c b/ggml/src/ggml-hexagon/htp/concat-ops.c index 51d39e8d98f5..966e867b3976 100644 --- a/ggml/src/ggml-hexagon/htp/concat-ops.c +++ b/ggml/src/ggml-hexagon/htp/concat-ops.c @@ -1,5 +1,8 @@ +#include "hex-common.h" +#include "hex-profile.h" #include "htp-ctx.h" #include "htp-ops.h" +#include "htp-tensor.h" #include "hexagon_types.h" #include "hexagon_protos.h" #include "hvx_hexagon_protos.h" @@ -13,6 +16,10 @@ struct htp_concat_context { struct htp_ops_context * octx; uint32_t dim; uint32_t nrows_per_thread; + uint32_t row_start; + uint32_t nrows; + uint32_t elem_start; + uint32_t nelems; struct fastdiv_values div_ne0; struct fastdiv_values div_ne1; struct fastdiv_values div_ne2; @@ -28,10 +35,10 @@ static void concat_2d_f32_transposed(unsigned int nth, unsigned int ith, void * const uint32_t src0_ne0 = src0->ne[0]; const uint32_t src1_ne0 = src1->ne[0]; - const uint32_t ne1 = dst->ne[1]; - const uint32_t start_i = ith * cctx->nrows_per_thread; - const uint32_t end_i = (start_i + cctx->nrows_per_thread < ne1) ? (start_i + cctx->nrows_per_thread) : ne1; + const uint32_t row_end = cctx->row_start + cctx->nrows; + const uint32_t start_i = cctx->row_start + ith * cctx->nrows_per_thread; + const uint32_t end_i = (start_i + cctx->nrows_per_thread < row_end) ? (start_i + cctx->nrows_per_thread) : row_end; if (start_i >= end_i) return; dma_queue * q = octx->ctx->dma[ith]; @@ -51,6 +58,8 @@ static void concat_2d_f32_transposed(unsigned int nth, unsigned int ith, void * const uint32_t spad0_row_bytes = hex_round_up((src0_ne0 + src1_ne0_padded) * sizeof(float), VLEN); uint32_t mu = src1_ne0_padded * spad1_stride; + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t i = start_i; i < end_i; i += block_i) { uint32_t current_block_i = (end_i - i < block_i) ? (end_i - i) : block_i; @@ -66,6 +75,7 @@ static void concat_2d_f32_transposed(unsigned int nth, unsigned int ith, void * HVX_Vector * vtcm_tmp = (HVX_Vector *)(spad1_base + src1_ne0_padded * spad1_stride); + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) i); for (uint32_t j = 0; j < src1_ne0_padded; j += 32) { #pragma unroll(4) for (uint32_t ii = 0; ii < current_block_i; ii++) { @@ -75,6 +85,7 @@ static void concat_2d_f32_transposed(unsigned int nth, unsigned int ith, void * hvx_vmemu(dst_ptr) = vtcm_tmp[ii]; } } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) i); dma_queue_pop(q); // src0 @@ -95,10 +106,10 @@ static void concat_2d_f16_transposed(unsigned int nth, unsigned int ith, void * const uint32_t src0_ne0 = src0->ne[0]; const uint32_t src1_ne0 = src1->ne[0]; - const uint32_t ne1 = dst->ne[1]; - const uint32_t start_i = ith * cctx->nrows_per_thread; - const uint32_t end_i = (start_i + cctx->nrows_per_thread < ne1) ? (start_i + cctx->nrows_per_thread) : ne1; + const uint32_t row_end = cctx->row_start + cctx->nrows; + const uint32_t start_i = cctx->row_start + ith * cctx->nrows_per_thread; + const uint32_t end_i = (start_i + cctx->nrows_per_thread < row_end) ? (start_i + cctx->nrows_per_thread) : row_end; if (start_i >= end_i) return; dma_queue * q = octx->ctx->dma[ith]; @@ -118,6 +129,8 @@ static void concat_2d_f16_transposed(unsigned int nth, unsigned int ith, void * const uint32_t spad0_row_bytes = hex_round_up((src0_ne0 + src1_ne0_padded) * sizeof(__fp16), VLEN); uint32_t mu = src1_ne0_padded * spad1_stride; + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t i = start_i; i < end_i; i += block_i) { uint32_t current_block_i = (end_i - i < block_i) ? (end_i - i) : block_i; @@ -133,6 +146,7 @@ static void concat_2d_f16_transposed(unsigned int nth, unsigned int ith, void * HVX_Vector * vtcm_tmp = (HVX_Vector *)(spad1_base + src1_ne0_padded * spad1_stride); + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) i); for (uint32_t j = 0; j < src1_ne0_padded; j += 64) { #pragma unroll(4) for (uint32_t ii = 0; ii < current_block_i; ii++) { @@ -142,6 +156,7 @@ static void concat_2d_f16_transposed(unsigned int nth, unsigned int ith, void * hvx_vmemu(dst_ptr) = vtcm_tmp[ii]; } } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) i); dma_queue_pop(q); // src0 @@ -164,11 +179,14 @@ static void concat_generic(unsigned int nth, unsigned int ith, void * data) { const uint32_t type_size = (dst->type == HTP_TYPE_F32 || dst->type == HTP_TYPE_I32) ? 4 : 2; const uint32_t ne[4] = {dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3]}; - const uint32_t total_elements = ne[0] * ne[1] * ne[2] * ne[3]; - const uint32_t chunk_size = (total_elements + nth - 1) / nth; - const uint32_t start_idx = MIN(ith * chunk_size, total_elements); - const uint32_t end_idx = MIN(start_idx + chunk_size, total_elements); + // Per-device element range aligned to prevent false sharing + const uint32_t elem_start = cctx->elem_start; + const uint32_t nelems = cctx->nelems; + const uint32_t chunk_size = (nelems + nth - 1) / nth; + + const uint32_t start_idx = MIN(elem_start + ith * chunk_size, elem_start + nelems); + const uint32_t end_idx = MIN(start_idx + chunk_size, elem_start + nelems); // Naive scalar element-wise copy for (uint32_t idx = start_idx; idx < end_idx; idx++) { @@ -236,13 +254,28 @@ int op_concat(struct htp_ops_context * octx) { void (*worker_func)(unsigned int, unsigned int, void *) = concat_generic; if (dim == 0 && is_2d && is_src1_transposed && !is_src0_transposed) { - n_threads = MIN(dst->ne[1], n_threads); - if (n_threads < 1) { - n_threads = 1; + const uint32_t total_rows = dst->ne[1]; + const size_t dst_data_row_size = dst->ne[0] * type_size; + uint32_t row_start = 0; + uint32_t nrows = total_rows; + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, type_size, (uint32_t) dst_data_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_rows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + cctx.row_start = row_start; + cctx.nrows = nrows; + uint32_t block_i = (type_size == 4) ? 32 : 64; - cctx.nrows_per_thread = hmx_ceil_div(dst->ne[1], n_threads); + cctx.nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); // Allocate VTCM uint32_t spad1_stride = block_i * type_size; @@ -270,8 +303,26 @@ int op_concat(struct htp_ops_context * octx) { } else { worker_func = concat_2d_f16_transposed; } + } else { + const uint32_t total_elements = dst->ne[0] * dst->ne[1] * dst->ne[2] * dst->ne[3]; + uint32_t elem_start = 0; + uint32_t nelems = total_elements; + if (octx->ctx->mdev.count > 1) { + const uint32_t elems_per_chunk = HEX_L2_LINE_SIZE / type_size; + const bool can_split = htp_tensor_mdev_data_aligned(dst) && htp_tensor_is_contiguous(dst, type_size) && !htp_tensor_is_permuted(dst); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_elements, can_split ? elems_per_chunk : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + elem_start = range.start; + nelems = range.count; + } + + if (nelems == 0) { + return HTP_STATUS_OK; + } + + cctx.elem_start = elem_start; + cctx.nelems = nelems; } - worker_pool_run_func(octx->ctx->worker_pool, worker_func, &cctx, n_threads); + work_queue_run(octx->ctx->work_queue, worker_func, &cctx, n_threads); return HTP_STATUS_OK; } diff --git a/ggml/src/ggml-hexagon/htp/cpy-ops.c b/ggml/src/ggml-hexagon/htp/cpy-ops.c index b151b757f413..7f01a8c1e043 100644 --- a/ggml/src/ggml-hexagon/htp/cpy-ops.c +++ b/ggml/src/ggml-hexagon/htp/cpy-ops.c @@ -16,6 +16,7 @@ #include "htp-ops.h" #include "hvx-utils.h" #include "htp-tensor.h" +#include "htp-fence.h" struct htp_copy_context { struct htp_ops_context * octx; @@ -29,7 +30,23 @@ struct htp_copy_context { uint32_t src0_blocks_per_row; uint32_t dst_blocks_per_row; + uint32_t elem_start; + uint32_t nelem; + uint32_t elem_per_thread; + uint32_t src0_nrows_per_thread; + uint32_t row_start; + uint32_t nrows; + + struct fastdiv_values div_ne01; + struct fastdiv_values div_ne02_ne01; + + struct fastdiv_values div_ne0; + struct fastdiv_values div_ne1_ne0; + struct fastdiv_values div_ne2_ne1_ne0; + struct fastdiv_values div_ne00; + struct fastdiv_values div_ne01_ne00; + struct fastdiv_values div_ne02_ne01_ne00; }; #define cpy_preamble \ @@ -54,131 +71,113 @@ struct htp_copy_context { const uint32_t nb0 = dst->nb[0]; \ const uint32_t nb1 = dst->nb[1]; \ const uint32_t nb2 = dst->nb[2]; \ - const uint32_t nb3 = dst->nb[3]; \ - \ - const uint32_t nr = ne01; - -#define DEFINE_CPY_SAMESHAPE(NAME, ELEM_TYPE, ELEM_SIZE) \ -static void cpy_thread_##NAME##_sameshape(unsigned int nth, unsigned int ith, void * data) { \ - struct htp_copy_context * ct = (struct htp_copy_context *) data; \ - struct htp_ops_context * octx = ct->octx; \ - cpy_preamble; \ - const uint32_t dr = ct->src0_nrows_per_thread; \ - const uint32_t ir0 = dr * ith; \ - const uint32_t ir1 = (ir0 + dr) < nr ? (ir0 + dr) : nr; \ - if (ir0 >= nr) return; \ - for (uint32_t i03 = 0; i03 < ne03; i03++) { \ - for (uint32_t i02 = 0; i02 < ne02; i02++) { \ - _Pragma("unroll(4)") \ - for (uint32_t i01 = ir0; i01 < ir1; i01++) { \ - uint8_t* dst_ptr = (uint8_t*) dst->data + i01*nb1 + i02*nb2 + i03*nb3; \ - uint8_t* src0_ptr = (uint8_t*) src0->data + i01*nb01 + i02*nb02 + i03*nb03; \ - hex_l2fetch(src0_ptr, ne00 * ELEM_SIZE, nb01, 2); \ - hvx_copy_uu(dst_ptr, src0_ptr, ne00, ELEM_SIZE); \ - } \ - } \ - } \ + const uint32_t nb3 = dst->nb[3]; + +#define DEFINE_CPY_SAMESHAPE(NAME, ELEM_TYPE, ELEM_SIZE) \ +static void cpy_thread_##NAME##_sameshape(unsigned int nth, unsigned int ith, void * data) { \ + struct htp_copy_context * ct = (struct htp_copy_context *) data; \ + struct htp_ops_context * octx = ct->octx; \ + cpy_preamble; \ + const uint32_t dr = ct->src0_nrows_per_thread; \ + const uint32_t ir0 = ct->row_start + dr * ith; \ + const uint32_t ir1 = MIN(ir0 + dr, ct->row_start + ct->nrows); \ + if (ir0 >= ir1) return; \ + const bool contiguous = (nb01 == ne00 * ELEM_SIZE) && (nb1 == nb01) && \ + (nb02 == ne01 * nb01) && (nb2 == nb02) && \ + (nb03 == ne02 * nb02) && (nb3 == nb03); \ + const uint32_t ne02_ne01 = ne02 * ne01; \ + uint32_t i03 = fastdiv(ir0, &ct->div_ne02_ne01); \ + uint32_t rem = ir0 - i03 * ne02_ne01; \ + uint32_t i02 = fastdiv(rem, &ct->div_ne01); \ + uint32_t i01 = rem - i02 * ne01; \ + uint8_t * dst_ptr = (uint8_t *) dst->data + i01*nb1 + i02*nb2 + i03*nb3; \ + uint8_t * src0_ptr = (uint8_t *) src0->data + i01*nb01 + i02*nb02 + i03*nb03; \ + if (contiguous) { \ + hvx_copy_uu(dst_ptr, src0_ptr, (ir1 - ir0) * ne00, ELEM_SIZE); \ + return; \ + } \ + for (uint32_t r = ir0; r < ir1; r++) { \ + hex_l2fetch(src0_ptr, ne00 * ELEM_SIZE, nb01, 2); \ + hvx_copy_uu(dst_ptr, src0_ptr, ne00, ELEM_SIZE); \ + dst_ptr += nb1; \ + src0_ptr += nb01; \ + if (++i01 == ne01) { \ + i01 = 0; \ + if (++i02 == ne02) { \ + i02 = 0; \ + i03++; \ + } \ + dst_ptr = (uint8_t *) dst->data + i02*nb2 + i03*nb3; \ + src0_ptr = (uint8_t *) src0->data + i02*nb02 + i03*nb03; \ + } \ + } \ } DEFINE_CPY_SAMESHAPE(f32, float, 4) DEFINE_CPY_SAMESHAPE(f16, __fp16, 2) -#define DEFINE_CPY_RESHAPE(NAME, ELEM_TYPE, ELEM_SIZE) \ -static void cpy_thread_##NAME##_reshape(unsigned int nth, unsigned int ith, void * data) { \ - struct htp_copy_context * ct = (struct htp_copy_context *) data; \ - struct htp_ops_context * octx = ct->octx; \ - cpy_preamble; \ - const uint32_t dr = ct->src0_nrows_per_thread; \ - const uint32_t ir0 = dr * ith; \ - const uint32_t ir1 = (ir0 + dr) < nr ? (ir0 + dr) : nr; \ - if (ir0 >= nr) return; \ - const bool src0_contig = (nb00 == ELEM_SIZE) && \ - (nb01 == ne00 * nb00) && \ - (nb02 == ne01 * nb01) && \ - (nb03 == ne02 * nb02); \ - const bool dst_contig = (nb0 == ELEM_SIZE) && \ - (nb1 == ne0 * nb0) && \ - (nb2 == ne1 * nb1) && \ - (nb3 == ne2 * nb2); \ - if (src0_contig && dst_contig) { \ - for (int64_t i03 = 0; i03 < ne03; i03++) { \ - for (int64_t i02 = 0; i02 < ne02; i02++) { \ - uint8_t * src_ptr = (uint8_t *) src0->data + i03*nb03 + i02*nb02 + ir0*nb01; \ - uint32_t flat = ((i03*ne02 + i02)*ne01 + ir0) * ne00; \ - uint8_t * dst_ptr = (uint8_t *) dst->data + flat * ELEM_SIZE; \ - hvx_copy_uu(dst_ptr, src_ptr, (ir1 - ir0) * ne00, ELEM_SIZE); \ - } \ - } \ - return; \ - } \ - const bool reshape_flat_fast = (ne03 == 1 && ne2 == 1 && ne3 == 1) && \ - (ne0 == ne00 * ne01) && (ne1 == ne02) && \ - (nb00 == ELEM_SIZE) && (nb0 == ELEM_SIZE); \ - if (reshape_flat_fast) { \ - for (uint32_t i02 = 0; i02 < ne02; i02++) { \ - for (uint32_t i01 = ir0; i01 < ir1; i01++) { \ - uint8_t * src0_ptr = (uint8_t *) src0->data + i01 * nb01 + i02 * nb02; \ - uint8_t * dst_ptr = (uint8_t *) dst->data + i01 * ne00 * ELEM_SIZE + i02 * nb1; \ - hvx_copy_uu(dst_ptr, src0_ptr, ne00, ELEM_SIZE); \ - } \ - } \ - return; \ - } \ - int64_t k10 = 0; \ - int64_t i11 = 0; \ - int64_t i12 = 0; \ - int64_t i13 = 0; \ - const int64_t nk00 = ct->src0_blocks_per_row; \ - const int64_t nk0 = ct->dst_blocks_per_row; \ - for (int64_t i03 = 0; i03 < ne03; i03++) { \ - for (int64_t i02 = 0; i02 < ne02; i02++) { \ - k10 += nk00 * ir0; \ - while (k10 >= nk0) { \ - k10 -= nk0; \ - if (++i11 == ne1) { \ - i11 = 0; \ - if (++i12 == ne2) { \ - i12 = 0; \ - if (++i13 == ne3) { \ - i13 = 0; \ - } \ - } \ - } \ - } \ - for (int64_t i01 = ir0; i01 < ir1; i01++) { \ - for (int64_t k00 = 0; k00 < nk00; k00++) { \ - const char * src0_ptr = ((char *) src0->data + k00*nb00 + i01*nb01 + i02*nb02 + i03*nb03); \ - char * dst_ptr = ((char *) dst->data + k10*nb0 + i11*nb1 + i12*nb2 + i13*nb3); \ - memcpy(dst_ptr, src0_ptr, ELEM_SIZE); \ - if (++k10 == nk0) { \ - k10 = 0; \ - if (++i11 == ne1) { \ - i11 = 0; \ - if (++i12 == ne2) { \ - i12 = 0; \ - if (++i13 == ne3) { \ - i13 = 0; \ - } \ - } \ - } \ - } \ - } \ - } \ - k10 += nk00 * (ne01 - ir1); \ - while (k10 >= nk0) { \ - k10 -= nk0; \ - if (++i11 == ne1) { \ - i11 = 0; \ - if (++i12 == ne2) { \ - i12 = 0; \ - if (++i13 == ne3) { \ - i13 = 0; \ - } \ - } \ - } \ - } \ - } \ - } \ +#define DEFINE_CPY_RESHAPE(NAME, ELEM_TYPE, ELEM_SIZE) \ +static void cpy_thread_##NAME##_reshape(unsigned int nth, unsigned int ith, void * data) { \ + struct htp_copy_context * ct = (struct htp_copy_context *) data; \ + struct htp_ops_context * octx = ct->octx; \ + cpy_preamble; \ + const uint32_t th_nelem = ct->elem_per_thread; \ + const uint32_t th_start = ct->elem_start + ith * th_nelem; \ + const uint32_t th_end = MIN(th_start + th_nelem, ct->elem_start + ct->nelem); \ + if (th_start >= th_end) return; \ + \ + const uint32_t ne01_ne00 = ne01 * ne00; \ + const uint32_t ne02_ne01_ne00 = ne02 * ne01_ne00; \ + const uint32_t ne1_ne0 = ne1 * ne0; \ + const uint32_t ne2_ne1_ne0 = ne2 * ne1_ne0; \ + \ + uint32_t e = th_start; \ + uint32_t i13 = fastdiv(e, &ct->div_ne2_ne1_ne0); \ + uint32_t rem = e - i13 * ne2_ne1_ne0; \ + uint32_t i12 = fastdiv(rem, &ct->div_ne1_ne0); \ + uint32_t rem2 = rem - i12 * ne1_ne0; \ + uint32_t i11 = fastdiv(rem2, &ct->div_ne0); \ + uint32_t i10 = rem2 - i11 * ne0; \ + \ + uint32_t i03 = fastdiv(e, &ct->div_ne02_ne01_ne00); \ + uint32_t rem_s = e - i03 * ne02_ne01_ne00; \ + uint32_t i02 = fastdiv(rem_s, &ct->div_ne01_ne00); \ + uint32_t rem2_s = rem_s - i02 * ne01_ne00; \ + uint32_t i01 = fastdiv(rem2_s, &ct->div_ne00); \ + uint32_t i00 = rem2_s - i01 * ne00; \ + \ + char * dst_ptr = (char *) dst->data + i10*nb0 + i11*nb1 + i12*nb2 + i13*nb3; \ + const char * src0_ptr = (const char *) src0->data + i00*nb00 + i01*nb01 + i02*nb02 + i03*nb03; \ + \ + for (; e < th_end; e++) { \ + *((ELEM_TYPE *) dst_ptr) = *((const ELEM_TYPE *) src0_ptr); \ + \ + dst_ptr += nb0; \ + if (++i10 == ne0) { \ + i10 = 0; \ + if (++i11 == ne1) { \ + i11 = 0; \ + if (++i12 == ne2) { \ + i12 = 0; \ + i13++; \ + } \ + } \ + dst_ptr = (char *) dst->data + i11*nb1 + i12*nb2 + i13*nb3; \ + } \ + \ + src0_ptr += nb00; \ + if (++i00 == ne00) { \ + i00 = 0; \ + if (++i01 == ne01) { \ + i01 = 0; \ + if (++i02 == ne02) { \ + i02 = 0; \ + i03++; \ + } \ + } \ + src0_ptr = (const char *) src0->data + i01*nb01 + i02*nb02 + i03*nb03; \ + } \ + } \ } DEFINE_CPY_RESHAPE(f32, float, 4) @@ -189,22 +188,33 @@ static void cpy_thread_f16_f32_sameshape(unsigned int nth, unsigned int ith, voi struct htp_ops_context * octx = ct->octx; cpy_preamble; - // parallelize by src0 rows const uint32_t dr = ct->src0_nrows_per_thread; - const uint32_t ir0 = dr * ith; - const uint32_t ir1 = (ir0 + dr) < nr ? (ir0 + dr) : nr; - if (ir0 >= nr) return; - - // copy by rows - for (uint32_t i03 = 0; i03 < ne03; i03++) { - for (uint32_t i02 = 0; i02 < ne02; i02++) { - #pragma unroll(2) - for (uint32_t i01 = ir0; i01 < ir1; i01++) { - uint8_t* dst_ptr = (uint8_t*) dst->data + i01*nb1 + i02*nb2 + i03*nb3; - uint8_t* src0_ptr = (uint8_t*) src0->data + i01*nb01 + i02*nb02 + i03*nb03; - hex_l2fetch(src0_ptr, ne00 * sizeof(float), nb01, 2); - hvx_copy_f16_f32_uu(dst_ptr, src0_ptr, ne00); + const uint32_t ir0 = ct->row_start + dr * ith; + const uint32_t ir1 = MIN(ir0 + dr, ct->row_start + ct->nrows); + if (ir0 >= ir1) return; + + const uint32_t ne02_ne01 = ne02 * ne01; + uint32_t i03 = fastdiv(ir0, &ct->div_ne02_ne01); + uint32_t rem = ir0 - i03 * ne02_ne01; + uint32_t i02 = fastdiv(rem, &ct->div_ne01); + uint32_t i01 = rem - i02 * ne01; + + uint8_t* dst_ptr = (uint8_t*) dst->data + i01*nb1 + i02*nb2 + i03*nb3; + uint8_t* src0_ptr = (uint8_t*) src0->data + i01*nb01 + i02*nb02 + i03*nb03; + + for (uint32_t r = ir0; r < ir1; r++) { + hex_l2fetch(src0_ptr, ne00 * sizeof(float), nb01, 2); + hvx_copy_f16_f32_uu(dst_ptr, src0_ptr, ne00); + dst_ptr += nb1; + src0_ptr += nb01; + if (++i01 == ne01) { + i01 = 0; + if (++i02 == ne02) { + i02 = 0; + i03++; } + dst_ptr = (uint8_t*) dst->data + i02*nb2 + i03*nb3; + src0_ptr = (uint8_t*) src0->data + i02*nb02 + i03*nb03; } } } @@ -214,22 +224,33 @@ static void cpy_thread_f32_f16_sameshape(unsigned int nth, unsigned int ith, voi struct htp_ops_context * octx = ct->octx; cpy_preamble; - // parallelize by src0 rows const uint32_t dr = ct->src0_nrows_per_thread; - const uint32_t ir0 = dr * ith; - const uint32_t ir1 = (ir0 + dr) < nr ? (ir0 + dr) : nr; - if (ir0 >= nr) return; - - // copy by rows - for (uint32_t i03 = 0; i03 < ne03; i03++) { - for (uint32_t i02 = 0; i02 < ne02; i02++) { - #pragma unroll(2) - for (uint32_t i01 = ir0; i01 < ir1; i01++) { - uint8_t* dst_ptr = (uint8_t*) dst->data + i01*nb1 + i02*nb2 + i03*nb3; - uint8_t* src0_ptr = (uint8_t*) src0->data + i01*nb01 + i02*nb02 + i03*nb03; - hex_l2fetch(src0_ptr, ne00 * sizeof(__fp16), nb01, 2); - hvx_copy_f32_f16_uu(dst_ptr, src0_ptr, ne00); + const uint32_t ir0 = ct->row_start + dr * ith; + const uint32_t ir1 = MIN(ir0 + dr, ct->row_start + ct->nrows); + if (ir0 >= ir1) return; + + const uint32_t ne02_ne01 = ne02 * ne01; + uint32_t i03 = fastdiv(ir0, &ct->div_ne02_ne01); + uint32_t rem = ir0 - i03 * ne02_ne01; + uint32_t i02 = fastdiv(rem, &ct->div_ne01); + uint32_t i01 = rem - i02 * ne01; + + uint8_t* dst_ptr = (uint8_t*) dst->data + i01*nb1 + i02*nb2 + i03*nb3; + uint8_t* src0_ptr = (uint8_t*) src0->data + i01*nb01 + i02*nb02 + i03*nb03; + + for (uint32_t r = ir0; r < ir1; r++) { + hex_l2fetch(src0_ptr, ne00 * sizeof(__fp16), nb01, 2); + hvx_copy_f32_f16_uu(dst_ptr, src0_ptr, ne00); + dst_ptr += nb1; + src0_ptr += nb01; + if (++i01 == ne01) { + i01 = 0; + if (++i02 == ne02) { + i02 = 0; + i03++; } + dst_ptr = (uint8_t*) dst->data + i02*nb2 + i03*nb3; + src0_ptr = (uint8_t*) src0->data + i02*nb02 + i03*nb03; } } } @@ -250,15 +271,19 @@ static inline void cpy_dma_sametype_sameshape( dma_queue * q = octx->ctx->dma[0]; if (contiguous_outer) { - dma_queue_push(q, dma_make_ptr((void *) dst->data, (const void *) src0->data), nb1, nb01, ne00 * elem_size, ne01 * ne02 * ne03); - dma_queue_pop(q); + if (!dma_queue_push(q, dma_make_ptr((void *) dst->data, (const void *) src0->data), nb1, nb01, ne00 * elem_size, ne01 * ne02 * ne03)) { + dma_queue_flush(q); + dma_queue_push(q, dma_make_ptr((void *) dst->data, (const void *) src0->data), nb1, nb01, ne00 * elem_size, ne01 * ne02 * ne03); + } + dma_queue_flush(q); return; } for (uint32_t i03 = 0; i03 < ne03; i03++) { for (uint32_t i02 = 0; i02 < ne02; i02++) { - uint8_t* dst_ptr = (uint8_t*) dst->data + i02*nb2 + i03*nb3; - uint8_t* src0_ptr = (uint8_t*) src0->data + i02*nb02 + i03*nb03; + uint8_t * dst_ptr = (uint8_t *) dst->data + i02 * nb2 + i03 * nb3; + uint8_t * src0_ptr = (uint8_t *) src0->data + i02 * nb02 + i03 * nb03; + if (!dma_queue_push(q, dma_make_ptr(dst_ptr, src0_ptr), nb1, nb01, ne00 * elem_size, ne01)) { dma_queue_flush(q); dma_queue_push(q, dma_make_ptr(dst_ptr, src0_ptr), nb1, nb01, ne00 * elem_size, ne01); @@ -269,10 +294,9 @@ static inline void cpy_dma_sametype_sameshape( dma_queue_flush(q); } -int op_cpy(struct htp_ops_context * octx) { +static int exec_cpy(struct htp_ops_context * octx, bool * use_dma) { cpy_preamble; - - const uint32_t n_threads = MIN(nr, octx->n_threads); + *use_dma = false; struct htp_copy_context ct; ct.octx = octx; @@ -296,59 +320,117 @@ int op_cpy(struct htp_ops_context * octx) { } const bool sametype = (src0->type == dst->type); - const bool transposed = (nb00 > nb01) || (nb0 > nb1); + const bool transposed = (nb00 > nb01) || (nb0 > nb1) || + (nb00 != ct.src0_type_size) || (nb0 != ct.dst_type_size) || + (nb01 < ne00 * ct.src0_type_size) || (nb1 < ne0 * ct.dst_type_size); const bool sameshape = !transposed && (ne00 == ne0 && ne01 == ne1 && ne02 == ne2 && ne03 == ne3); - ct.src0_nrows_per_thread = (nr + n_threads - 1) / n_threads; + const uint32_t n_threads = octx->n_threads; - worker_callback_t copy_fun = NULL; - bool use_dma = false; + const bool dst_is_contiguous = htp_tensor_is_contiguous(dst, ct.dst_type_size); - if (sametype && sameshape) { - use_dma = true; - } else if (sameshape) { - /**/ if (dst->type == HTP_TYPE_F16 && src0->type == HTP_TYPE_F32) - copy_fun = cpy_thread_f16_f32_sameshape; - else if (dst->type == HTP_TYPE_F32 && src0->type == HTP_TYPE_F16) - copy_fun = cpy_thread_f32_f16_sameshape; - else - return HTP_STATUS_NO_SUPPORT; - } else if (sametype) { - if (src0->type == HTP_TYPE_F32) { - copy_fun = cpy_thread_f32_reshape; + if (sameshape) { + const uint32_t total_rows = ne01 * ne02 * ne03; + const uint32_t row_size = ne00 * ct.dst_type_size; + + ct.div_ne01 = init_fastdiv_values(ne01); + ct.div_ne02_ne01 = init_fastdiv_values(ne02 * ne01); + + uint32_t row_start = 0; + uint32_t nrows = total_rows; + + if (octx->ctx->mdev.count > 1) { + const uint32_t rows_per_chunk = (row_size > 0) ? (HEX_L2_LINE_SIZE / hex_gcd_u32(row_size, HEX_L2_LINE_SIZE)) : 1; + const bool can_split = htp_tensor_mdev_data_aligned(dst) && dst_is_contiguous; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_rows, can_split ? rows_per_chunk : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + ct.row_start = row_start; + ct.nrows = nrows; + ct.src0_nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); + + if (sametype && octx->ctx->mdev.count <= 1) { + *use_dma = true; + cpy_dma_sametype_sameshape(octx, dst, src0, ct.src0_type_size, ne00, ne01, ne02, ne03, nb01, nb02, nb03, nb1, nb2, nb3); } else { - copy_fun = cpy_thread_f16_reshape; + work_queue_func_t copy_fun = NULL; + if (sametype) { + copy_fun = (src0->type == HTP_TYPE_F32) ? cpy_thread_f32_sameshape : cpy_thread_f16_sameshape; + } else if (dst->type == HTP_TYPE_F16 && src0->type == HTP_TYPE_F32) { + copy_fun = cpy_thread_f16_f32_sameshape; + } else if (dst->type == HTP_TYPE_F32 && src0->type == HTP_TYPE_F16) { + copy_fun = cpy_thread_f32_f16_sameshape; + } else { + return HTP_STATUS_NO_SUPPORT; + } + work_queue_run(octx->ctx->work_queue, copy_fun, &ct, n_threads); + } + } else if (sametype) { + const uint32_t total_elems = ne0 * ne1 * ne2 * ne3; + const uint32_t elems_per_line = (ct.dst_type_size == 4) ? 32 : 64; + + ct.div_ne0 = init_fastdiv_values(ne0); + ct.div_ne1_ne0 = init_fastdiv_values(ne1 * ne0); + ct.div_ne2_ne1_ne0 = init_fastdiv_values(ne2 * ne1 * ne0); + ct.div_ne00 = init_fastdiv_values(ne00); + ct.div_ne01_ne00 = init_fastdiv_values(ne01 * ne00); + ct.div_ne02_ne01_ne00 = init_fastdiv_values(ne02 * ne01 * ne00); + + uint32_t elem_start = 0; + uint32_t nelem = total_elems; + + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_mdev_data_aligned(dst) && dst_is_contiguous; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_elems, can_split ? elems_per_line : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + elem_start = range.start; + nelem = range.count; + } + + if (nelem == 0) { + return HTP_STATUS_OK; } + + ct.elem_start = elem_start; + ct.nelem = nelem; + ct.elem_per_thread = fastdiv(nelem + n_threads - 1, &octx->n_threads_div); + + work_queue_func_t copy_fun = (src0->type == HTP_TYPE_F32) ? cpy_thread_f32_reshape : cpy_thread_f16_reshape; + work_queue_run(octx->ctx->work_queue, copy_fun, &ct, n_threads); } else { return HTP_STATUS_NO_SUPPORT; } - FARF(HIGH, "cpy-%s-%s: (%ux%ux%ux%u) -> (%ux%ux%ux%u) : use_dma=%d n_threads %u\n", - src0->type == HTP_TYPE_F32 ? "f32" : "f16", dst->type == HTP_TYPE_F32 ? "f32" : "f16", - ne00, ne01, ne02, ne03, ne0, ne1, ne2, ne3, use_dma, n_threads); + return HTP_STATUS_OK; +} - if (use_dma) { - cpy_dma_sametype_sameshape(octx, dst, src0, ct.src0_type_size, ne00, ne01, ne02, ne03, nb01, nb02, nb03, nb1, nb2, nb3); - } else { - worker_pool_run_func(octx->ctx->worker_pool, copy_fun, &ct, n_threads); - } +int op_cpy(struct htp_ops_context * octx) { + bool use_dma = false; + int status = exec_cpy(octx, &use_dma); + + htp_ops_context_set_status(octx, status); - const struct htp_tensor *sync = octx->src[1]; - if (sync && (sync->flags & HTP_TENSOR_FENCE)) { + if (octx->op == HTP_OP_CPY_FENCE) { if (!use_dma) { - // htp_tensor_flush_all(octx->ctx, octx->dsts, 1); - qurt_mem_cache_clean((qurt_addr_t) 0, 0, QURT_MEM_CACHE_FLUSH_INVALIDATE_ALL, QURT_MEM_DCACHE); + htp_flush_dirty_ranges(octx->ctx); } - atomic_uint * sync_fence = (atomic_uint *) sync->data; - const uint32_t seq = (uint32_t) octx->op_params[0]; + htp_mdev_group_barrier(octx); - atomic_store(&sync_fence[0], seq); - asm volatile ("syncht" : : : "memory"); - Q6_dccleaninva_A((void *) sync_fence); + if (octx->ctx->mdev.idx == 0) { + const struct htp_tensor * sync = octx->src[1]; + const uint32_t seq = (uint32_t) octx->op_params[0]; + atomic_uint * sync_fence = (atomic_uint *) (uintptr_t) sync->data; + htp_fence_write(sync_fence, seq, octx->status); - FARF(HIGH, "ggml-hex: sync-release : fence %p seq %u\n", sync_fence, seq); + FARF(HIGH, "ggml-hex: sync-release : fence %p seq 0x%x status %d\n", sync_fence, seq, octx->status); + } } - return HTP_STATUS_OK; + return octx->status; } diff --git a/ggml/src/ggml-hexagon/htp/cumsum-ops.c b/ggml/src/ggml-hexagon/htp/cumsum-ops.c index 2d45c39f23b5..971fa3bccb3a 100644 --- a/ggml/src/ggml-hexagon/htp/cumsum-ops.c +++ b/ggml/src/ggml-hexagon/htp/cumsum-ops.c @@ -7,6 +7,8 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" +#include "hex-common.h" +#include "hex-profile.h" #include "htp-ctx.h" #include "htp-ops.h" #include "htp-tensor.h" @@ -17,25 +19,25 @@ #define htp_cumsum_tensors_preamble \ const struct htp_tensor * restrict src0 = octx->src[0]; \ const struct htp_tensor * restrict dst = octx->dst; \ - \ - const uint32_t ne00 = src0->ne[0]; \ - const uint32_t ne01 = src0->ne[1]; \ - const uint32_t ne02 = src0->ne[2]; \ - const uint32_t ne03 = src0->ne[3]; \ - \ - const uint32_t ne0 = dst->ne[0]; \ - const uint32_t ne1 = dst->ne[1]; \ - const uint32_t ne2 = dst->ne[2]; \ - const uint32_t ne3 = dst->ne[3]; \ - \ - const uint32_t nb00 = src0->nb[0]; \ - const uint32_t nb01 = src0->nb[1]; \ - const uint32_t nb02 = src0->nb[2]; \ - const uint32_t nb03 = src0->nb[3]; \ - \ - const uint32_t nb0 = dst->nb[0]; \ - const uint32_t nb1 = dst->nb[1]; \ - const uint32_t nb2 = dst->nb[2]; \ + \ + const uint32_t ne00 = src0->ne[0]; \ + const uint32_t ne01 = src0->ne[1]; \ + const uint32_t ne02 = src0->ne[2]; \ + const uint32_t ne03 = src0->ne[3]; \ + \ + const uint32_t ne0 = dst->ne[0]; \ + const uint32_t ne1 = dst->ne[1]; \ + const uint32_t ne2 = dst->ne[2]; \ + const uint32_t ne3 = dst->ne[3]; \ + \ + const uint32_t nb00 = src0->nb[0]; \ + const uint32_t nb01 = src0->nb[1]; \ + const uint32_t nb02 = src0->nb[2]; \ + const uint32_t nb03 = src0->nb[3]; \ + \ + const uint32_t nb0 = dst->nb[0]; \ + const uint32_t nb1 = dst->nb[1]; \ + const uint32_t nb2 = dst->nb[2]; \ const uint32_t nb3 = dst->nb[3]; struct htp_cumsum_context { @@ -46,6 +48,7 @@ struct htp_cumsum_context { size_t dst_row_size_aligned; uint32_t rows_per_thread; uint32_t total_rows; + uint32_t row_start; }; #define htp_cumsum_preamble \ @@ -116,11 +119,8 @@ static inline void hvx_cumsum_row_f32(const float * restrict src, float * restri static void cumsum_thread_f32_dma(unsigned int nth, unsigned int ith, void * data) { htp_cumsum_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - - const uint32_t ir0 = cctx->rows_per_thread * ith; - const uint32_t ir1 = MIN(ir0 + cctx->rows_per_thread, cctx->total_rows); + const uint32_t ir0 = cctx->row_start + cctx->rows_per_thread * ith; + const uint32_t ir1 = MIN(ir0 + cctx->rows_per_thread, cctx->row_start + cctx->total_rows); if (ir0 >= ir1) { return; @@ -149,11 +149,15 @@ static void cumsum_thread_f32_dma(unsigned int nth, unsigned int ith, void * dat src_row_size_aligned, src_row_size, 1); } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = ir0; ir < ir1; ir++) { float * dst_spad_row = (float *) dma_queue_pop(dma_queue).src; float * src_spad_row = (float *) dma_queue_pop(dma_queue).dst; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); hvx_cumsum_row_f32(src_spad_row, dst_spad_row, ne00); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); dma_queue_push_vtcm_to_ddr(dma_queue, dma_make_ptr(dst_data + (ir * dst_row_size), (uint8_t *) dst_spad_row), @@ -168,12 +172,10 @@ static void cumsum_thread_f32_dma(unsigned int nth, unsigned int ith, void * dat } dma_queue_flush(dma_queue); - t2 = HAP_perf_get_qtimer_count(); - FARF(HIGH, "cumsum-f32-dma %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u usec %u\n", + FARF(HIGH, "cumsum-f32-dma %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u\n", ith, nth, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], ir0, ir1, - dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3]); } // --------------------------------------------------------------------------- @@ -183,14 +185,14 @@ static void cumsum_thread_f32_dma(unsigned int nth, unsigned int ith, void * dat static void cumsum_thread_f32(unsigned int nth, unsigned int ith, void * data) { htp_cumsum_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - const uint8_t * src_data = (const uint8_t *) src0->data; uint8_t * dst_data = (uint8_t *) dst->data; - const uint32_t ir0 = cctx->rows_per_thread * ith; - const uint32_t ir1 = MIN(ir0 + cctx->rows_per_thread, cctx->total_rows); + const uint32_t ir0 = cctx->row_start + cctx->rows_per_thread * ith; + const uint32_t ir1 = MIN(ir0 + cctx->rows_per_thread, cctx->row_start + cctx->total_rows); + + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir0); for (uint32_t ir = ir0; ir < ir1; ir++) { const float * restrict src_row = (const float *) (src_data + ir * cctx->src_row_size); @@ -198,12 +200,11 @@ static void cumsum_thread_f32(unsigned int nth, unsigned int ith, void * data) { hvx_cumsum_row_f32(src_row, dst_row, ne00); } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir0); - FARF(HIGH, "cumsum-f32 %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u usec %u\n", + FARF(HIGH, "cumsum-f32 %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u\n", ith, nth, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], ir0, ir1, - dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3]); } int op_cumsum_f32(struct htp_ops_context * octx) { @@ -214,8 +215,25 @@ int op_cumsum_f32(struct htp_ops_context * octx) { return HTP_STATUS_OK; } - const uint32_t total_rows = src0->ne[1] * src0->ne[2] * src0->ne[3]; - const uint32_t n_threads = MIN(octx->n_threads, total_rows); + const uint32_t total_rows = src0->ne[1] * src0->ne[2] * src0->ne[3]; + const size_t dst_data_row_size = dst->ne[0] * sizeof(float); + + uint32_t row_start = 0; + uint32_t nrows = total_rows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, sizeof(float), (uint32_t) dst_data_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_rows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; const size_t src_row_size = src0->nb[1]; const size_t dst_row_size = dst->nb[1]; @@ -240,14 +258,15 @@ int op_cumsum_f32(struct htp_ops_context * octx) { .dst_row_size = dst_row_size, .src_row_size_aligned = src_row_size_aligned, .dst_row_size_aligned = dst_row_size_aligned, - .rows_per_thread = (total_rows + n_threads - 1) / n_threads, - .total_rows = total_rows, + .rows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div), + .total_rows = nrows, + .row_start = row_start, }; if (octx->ctx->vtcm_size < spad_per_thread * n_threads) { - worker_pool_run_func(octx->ctx->worker_pool, cumsum_thread_f32, &cctx, n_threads); + work_queue_run(octx->ctx->work_queue, cumsum_thread_f32, &cctx, n_threads); } else { - worker_pool_run_func(octx->ctx->worker_pool, cumsum_thread_f32_dma, &cctx, n_threads); + work_queue_run(octx->ctx->work_queue, cumsum_thread_f32_dma, &cctx, n_threads); } return HTP_STATUS_OK; diff --git a/ggml/src/ggml-hexagon/htp/diag-ops.c b/ggml/src/ggml-hexagon/htp/diag-ops.c index 9b3194d90846..a69fd89d38b3 100644 --- a/ggml/src/ggml-hexagon/htp/diag-ops.c +++ b/ggml/src/ggml-hexagon/htp/diag-ops.c @@ -5,8 +5,11 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" +#include "hex-common.h" +#include "hex-profile.h" #include "htp-ctx.h" #include "htp-ops.h" +#include "htp-tensor.h" #include "hvx-types.h" #include "hex-utils.h" #include "hvx-copy.h" @@ -15,17 +18,17 @@ #define htp_diag_tensors_preamble \ const struct htp_tensor * restrict src0 = octx->src[0]; \ const struct htp_tensor * restrict dst = octx->dst; \ - \ - const uint32_t ne02 = src0->ne[2]; \ - \ - const uint32_t ne0 = dst->ne[0]; \ - const uint32_t ne1 = dst->ne[1]; \ - \ - const uint32_t nb02 = src0->nb[2]; \ - const uint32_t nb03 = src0->nb[3]; \ - \ - const uint32_t nb1 = dst->nb[1]; \ - const uint32_t nb2 = dst->nb[2]; \ + \ + const uint32_t ne02 = src0->ne[2]; \ + \ + const uint32_t ne0 = dst->ne[0]; \ + const uint32_t ne1 = dst->ne[1]; \ + \ + const uint32_t nb02 = src0->nb[2]; \ + const uint32_t nb03 = src0->nb[3]; \ + \ + const uint32_t nb1 = dst->nb[1]; \ + const uint32_t nb2 = dst->nb[2]; \ const uint32_t nb3 = dst->nb[3]; struct htp_diag_context { @@ -36,6 +39,7 @@ struct htp_diag_context { size_t dst_row_size_aligned; uint32_t batches_per_thread; uint32_t total_batches; + uint32_t batch_start; }; #define htp_diag_preamble \ @@ -57,11 +61,8 @@ static void diag_thread_f32_dma(unsigned int nth, unsigned int ith, void * data) htp_diag_preamble; dma_queue * dma_queue = octx->ctx->dma[ith]; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - - const uint32_t ib0 = dctx->batches_per_thread * ith; - const uint32_t ib1 = MIN(ib0 + dctx->batches_per_thread, dctx->total_batches); + const uint32_t ib0 = dctx->batch_start + dctx->batches_per_thread * ith; + const uint32_t ib1 = MIN(ib0 + dctx->batches_per_thread, dctx->batch_start + dctx->total_batches); if (ib0 >= ib1) { return; @@ -79,6 +80,8 @@ static void diag_thread_f32_dma(unsigned int nth, unsigned int ith, void * data) uint8_t * src_spad = octx->src0_spad.data + (ith * src_batch_size_aligned); uint8_t * dst_spad = octx->dst_spad.data + (ith * dst_row_size_aligned); + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ib = ib0; ib < ib1; ib++) { const uint32_t i3 = ib / ne02; const uint32_t i2 = ib % ne02; @@ -96,7 +99,9 @@ static void diag_thread_f32_dma(unsigned int nth, unsigned int ith, void * data) for (uint32_t i1 = 0; i1 < ne1; i1++) { // Compute row in VTCM + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) (ib * ne1 + i1)); hvx_diag_row_f32(src_spad_f32, dst_spad_f32, i1, ne0); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) (ib * ne1 + i1)); // Write completed row back to DDR uint8_t * dst_row = dst_data + i3 * nb3 + i2 * nb2 + i1 * nb1; @@ -107,12 +112,9 @@ static void diag_thread_f32_dma(unsigned int nth, unsigned int ith, void * data) } } - t2 = HAP_perf_get_qtimer_count(); - - FARF(HIGH, "diag-f32-dma %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u usec %u\n", + FARF(HIGH, "diag-f32-dma %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u\n", ith, nth, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], ib0, ib1, - dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3]); } // --------------------------------------------------------------------------- @@ -122,14 +124,14 @@ static void diag_thread_f32_dma(unsigned int nth, unsigned int ith, void * data) static void diag_thread_f32(unsigned int nth, unsigned int ith, void * data) { htp_diag_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - const uint8_t * src_data = (const uint8_t *) src0->data; uint8_t * dst_data = (uint8_t *) dst->data; - const uint32_t ib0 = dctx->batches_per_thread * ith; - const uint32_t ib1 = MIN(ib0 + dctx->batches_per_thread, dctx->total_batches); + const uint32_t ib0 = dctx->batch_start + dctx->batches_per_thread * ith; + const uint32_t ib1 = MIN(ib0 + dctx->batches_per_thread, dctx->batch_start + dctx->total_batches); + + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ib0); for (uint32_t ib = ib0; ib < ib1; ib++) { const uint32_t i3 = ib / ne02; @@ -143,12 +145,11 @@ static void diag_thread_f32(unsigned int nth, unsigned int ith, void * data) { } } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ib0); - FARF(HIGH, "diag-f32 %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u usec %u\n", + FARF(HIGH, "diag-f32 %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u\n", ith, nth, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], ib0, ib1, - dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3]); } int op_diag_f32(struct htp_ops_context * octx) { @@ -160,7 +161,36 @@ int op_diag_f32(struct htp_ops_context * octx) { } const uint32_t total_batches = src0->ne[2] * src0->ne[3]; - const uint32_t n_threads = MIN(octx->n_threads, total_batches); + const size_t dst_batch_size = dst->ne[1] * dst->nb[1]; + + uint32_t batch_start = 0; + uint32_t nbatches = total_batches; + + if (octx->ctx->mdev.count > 1) { + bool can_split = htp_tensor_mdev_data_aligned(dst) && (dst->ne[0] == 1 || dst->nb[0] == sizeof(float)) && !htp_tensor_is_permuted(dst); + uint32_t batches_per_chunk = 1; + if (can_split) { + if (dst->ne[2] > 1 && (dst->nb[2] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0 && + (dst->ne[3] <= 1 || (dst->nb[3] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0)) { + batches_per_chunk = 1; + } else if (dst->nb[2] == dst_batch_size && + (dst->ne[3] <= 1 || dst->nb[3] == dst->nb[2] * dst->ne[2])) { + batches_per_chunk = (dst_batch_size > 0) ? (HEX_L2_LINE_SIZE / hex_gcd_u32(dst_batch_size, HEX_L2_LINE_SIZE)) : 1; + } else { + can_split = false; + } + } + + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_batches, can_split ? batches_per_chunk : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + batch_start = range.start; + nbatches = range.count; + } + + if (nbatches == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; const size_t src_batch_size = src0->ne[0] * sizeof(float); const size_t dst_row_size = dst->ne[0] * sizeof(float); @@ -185,14 +215,15 @@ int op_diag_f32(struct htp_ops_context * octx) { .dst_row_size = dst_row_size, .src_batch_size_aligned = src_batch_size_aligned, .dst_row_size_aligned = dst_row_size_aligned, - .batches_per_thread = (total_batches + n_threads - 1) / n_threads, - .total_batches = total_batches, + .batches_per_thread = fastdiv(nbatches + n_threads - 1, &octx->n_threads_div), + .total_batches = nbatches, + .batch_start = batch_start, }; if (octx->ctx->vtcm_size < spad_per_thread * n_threads) { - worker_pool_run_func(octx->ctx->worker_pool, diag_thread_f32, &dctx, n_threads); + work_queue_run(octx->ctx->work_queue, diag_thread_f32, &dctx, n_threads); } else { - worker_pool_run_func(octx->ctx->worker_pool, diag_thread_f32_dma, &dctx, n_threads); + work_queue_run(octx->ctx->work_queue, diag_thread_f32_dma, &dctx, n_threads); } return HTP_STATUS_OK; diff --git a/ggml/src/ggml-hexagon/htp/fill-ops.c b/ggml/src/ggml-hexagon/htp/fill-ops.c index 3ccfbe74ee45..1f6eaafada93 100644 --- a/ggml/src/ggml-hexagon/htp/fill-ops.c +++ b/ggml/src/ggml-hexagon/htp/fill-ops.c @@ -3,10 +3,11 @@ #pragma clang diagnostic ignored "-Wunused-but-set-variable" #include <HAP_farf.h> -#include <HAP_perf.h> - #include <string.h> +#include "hex-common.h" +#include "hex-profile.h" + #include "hvx-copy.h" #include "hvx-utils.h" @@ -14,28 +15,30 @@ #include "ggml-common.h" #include "htp-ctx.h" #include "htp-ops.h" +#include "htp-tensor.h" // ggml op_params layout for FILL: // op_params[0] (as float) - the scalar fill value -#define fill_preamble \ +#define fill_preamble \ const struct htp_tensor * dst = octx->dst; \ - \ - const uint32_t ne0 = dst->ne[0]; \ - const uint32_t ne1 = dst->ne[1]; \ - const uint32_t ne2 = dst->ne[2]; \ - const uint32_t ne3 = dst->ne[3]; \ - \ - const uint32_t nb1 = dst->nb[1]; \ - const uint32_t nb2 = dst->nb[2]; \ - const uint32_t nb3 = dst->nb[3]; \ - \ + \ + const uint32_t ne0 = dst->ne[0]; \ + const uint32_t ne1 = dst->ne[1]; \ + const uint32_t ne2 = dst->ne[2]; \ + const uint32_t ne3 = dst->ne[3]; \ + \ + const uint32_t nb1 = dst->nb[1]; \ + const uint32_t nb2 = dst->nb[2]; \ + const uint32_t nb3 = dst->nb[3]; \ + \ const uint32_t nr = ne1 * ne2 * ne3; struct htp_fill_context { struct htp_ops_context * octx; uint32_t nrows_per_thread; uint32_t total_rows; // ne1 * ne2 * ne3 + uint32_t row_start; bool opt_path; HVX_Vector splat_vec; uint32_t elem_size; @@ -47,10 +50,15 @@ static void fill_thread(unsigned int nth, unsigned int ith, void * data) { fill_preamble; // Parallelise over the flat row index spanning ne1*ne2*ne3 - const uint32_t ir0 = fctx->nrows_per_thread * ith; - const uint32_t ir1 = MIN(ir0 + fctx->nrows_per_thread, fctx->total_rows); + const uint32_t ir0 = fctx->row_start + fctx->nrows_per_thread * ith; + const uint32_t ir1 = MIN(ir0 + fctx->nrows_per_thread, fctx->row_start + fctx->total_rows); - uint64_t t1 = HAP_perf_get_qtimer_count(); + if (ir0 >= ir1) { + return; + } + + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir0); if (fctx->opt_path) { // Opt path: tensor is fully contiguous, treat as flat array @@ -69,9 +77,8 @@ static void fill_thread(unsigned int nth, unsigned int ith, void * data) { } } - uint64_t t2 = HAP_perf_get_qtimer_count(); - FARF(HIGH, "fill %u/%u: rows %u:%u usec %u\n", - ith, nth, ir0, ir1, (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir1); + FARF(HIGH, "fill %u/%u: rows %u:%u\n", ith, nth, ir0, ir1); } int op_fill(struct htp_ops_context * octx) { @@ -85,8 +92,23 @@ int op_fill(struct htp_ops_context * octx) { return HTP_STATUS_OK; } + uint32_t row_start = 0; + uint32_t nrows = nr; + + if (octx->ctx->mdev.count > 1) { + const uint32_t row_size = nb1; + const uint32_t rows_per_chunk = (row_size > 0) ? (HEX_L2_LINE_SIZE / hex_gcd_u32(row_size, HEX_L2_LINE_SIZE)) : 1; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(nr, htp_tensor_mdev_data_aligned(dst) ? rows_per_chunk : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + // nr = ne1*ne2*ne3 (flat row count across all outer dims); parallelise over it. - const uint32_t n_threads = MIN(nr, octx->n_threads); + const uint32_t n_threads = octx->n_threads; // Optimize if fully contiguous: skip stride arithmetic, treat as flat array const bool opt_path = (nb2 == nb1 * ne1) && (nb3 == nb2 * ne2); @@ -99,8 +121,9 @@ int op_fill(struct htp_ops_context * octx) { struct htp_fill_context fctx = { .octx = octx, - .nrows_per_thread = (nr + n_threads - 1) / n_threads, - .total_rows = nr, + .nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div), + .total_rows = nrows, + .row_start = row_start, .opt_path = opt_path, }; @@ -117,7 +140,7 @@ int op_fill(struct htp_ops_context * octx) { return HTP_STATUS_NO_SUPPORT; } - worker_pool_run_func(octx->ctx->worker_pool, fill_thread, &fctx, n_threads); + work_queue_run(octx->ctx->work_queue, fill_thread, &fctx, n_threads); return HTP_STATUS_OK; } diff --git a/ggml/src/ggml-hexagon/htp/flash-attn-ops.c b/ggml/src/ggml-hexagon/htp/flash-attn-ops.c index c76b4d3a3ac6..8a1caba22b79 100644 --- a/ggml/src/ggml-hexagon/htp/flash-attn-ops.c +++ b/ggml/src/ggml-hexagon/htp/flash-attn-ops.c @@ -5,7 +5,6 @@ #include <assert.h> #include <HAP_compute_res.h> #include <HAP_farf.h> -#include <HAP_perf.h> #include <math.h> #include <stdbool.h> #include <stdatomic.h> @@ -75,6 +74,7 @@ struct htp_fa_context { uint32_t qrows; uint32_t qrows_per_thread; + uint32_t qrow_start; bool is_q_fp32; @@ -89,8 +89,6 @@ struct htp_fa_context { const struct htp_tensor * k; const struct htp_tensor * v; - - uint64_t t_start; }; struct hmx_fa_context { @@ -206,10 +204,9 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * const uint32_t nb3 = dst->nb[3]; // total rows in q - const uint32_t nr = factx->qrows; - const uint32_t dr = factx->qrows_per_thread; - const uint32_t ir0 = dr * ith; - const uint32_t ir1 = MIN(ir0 + dr, nr); + const uint32_t dr = factx->qrows_per_thread; + const uint32_t ir0 = factx->qrow_start + dr * ith; + const uint32_t ir1 = MIN(ir0 + dr, factx->qrow_start + factx->qrows); if (ir0 >= ir1) return; @@ -1888,6 +1885,24 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { const uint32_t n_threads = factx.n_threads; const uint32_t G = factx.G; + // Multi-device: split Q blocks across devices + const uint32_t n_q_blocks = (neq1 + Br - 1) / Br; + uint32_t q_start_min = 0; + uint32_t q_start_max = neq1; + + if (octx->ctx->mdev.count > 1) { + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(n_q_blocks, htp_tensor_mdev_data_aligned(dst) ? 1 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + const uint32_t block_start = range.start; + const uint32_t block_end = range.start + range.count; + + if (block_start >= block_end) { + return HTP_STATUS_OK; + } + + q_start_min = block_start * Br; + q_start_max = MIN(block_end * Br, neq1); + } + // ======== VTCM allocation (GQA-aware) ======== // K/V row sizes drive the DMA descriptors (not the VTCM layout) and are used // throughout the KV loop below. @@ -1977,7 +1992,7 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { // ======== Main loop ======== for (uint32_t ib3 = 0; ib3 < neq3; ++ib3) { const uint32_t im3 = mask ? fastmodulo(ib3, mask->ne[3], &factx.src3_div3) : 0; - for (uint32_t q_start = 0; q_start < neq1; q_start += Br) { + for (uint32_t q_start = q_start_min; q_start < q_start_max; q_start += Br) { const uint32_t n_rows_q = hex_smin(Br, neq1 - q_start); const size_t n_rows_g = n_rows_q * G; const size_t g_br_actual = hex_align_up(n_rows_g, HMX_FP16_TILE_N_ROWS); @@ -1991,8 +2006,9 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { // 1. Push Q and KV DMAs for the very first iteration. // Subsequent iterations are enqueued early at the end of the previous iteration. - if (ib3 == 0 && q_start == 0 && kv_head == 0) { - const uint8_t * q_ptr = (const uint8_t *) q->data; + if (ib3 == 0 && q_start == q_start_min && kv_head == 0) { + const uint8_t * q_ptr = (const uint8_t *) q->data + q_start * q->nb[1] + + (kv_head * factx.G) * q->nb[2] + ib3 * q->nb[3]; const size_t q_row_bytes = q_transposed ? n_rows_q * q_row_bytes_trans_factor : q_row_bytes_untransposed; const size_t n_rows = q_transposed ? factx.G : n_rows_q; dma_queue_push(dma, dma_make_ptr(factx.vtcm_q_dma, q_ptr), q_row_bytes, hex_smax(q_src_stride, q_row_bytes), q_row_bytes, n_rows); @@ -2311,8 +2327,8 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { if (next_kv_head >= n_kv_heads) { next_kv_head = 0; next_q_start = q_start + Br; - if (next_q_start >= neq1) { - next_q_start = 0; + if (next_q_start >= q_start_max) { + next_q_start = q_start_min; next_ib3 = ib3 + 1; } } @@ -2398,6 +2414,10 @@ int op_flash_attn_ext(struct htp_ops_context * octx) { return HTP_STATUS_NO_SUPPORT; } + if (!htp_ops_context_set_n_threads(octx, kparams->n_threads)) { + return HTP_STATUS_INVAL_PARAMS; + } + if (kparams->kernel_type == HTP_FA_KERNEL_HMX) { return hmx_flash_attn_ext(octx); } @@ -2407,8 +2427,6 @@ int op_flash_attn_ext(struct htp_ops_context * octx) { factx.k = k; factx.v = v; - factx.t_start = HAP_perf_get_qtimer_count(); - factx.src0_div21 = kparams->u.hvx.src0_div21; factx.src0_div1 = kparams->u.hvx.src0_div1; @@ -2451,8 +2469,30 @@ int op_flash_attn_ext(struct htp_ops_context * octx) { } // total rows in q - factx.qrows = kparams->qrows; - factx.qrows_per_thread = kparams->qrows_per_thread; + const uint32_t neq1 = q->ne[1]; + const uint32_t neq2 = q->ne[2]; + const uint32_t neq3 = q->ne[3]; + const uint32_t total_qrows = neq1 * neq2 * neq3; + + uint32_t qrow_start = 0; + uint32_t qrows = total_qrows; + + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_mdev_data_aligned(dst) && ((dst->nb[1] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_qrows, can_split ? 1 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + qrow_start = range.start; + qrows = range.count; + } + + if (qrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + + factx.qrows = qrows; + factx.qrow_start = qrow_start; + factx.qrows_per_thread = fastdiv(qrows + n_threads - 1, &octx->n_threads_div); size_t size_vkq_acc = hex_round_up(v->ne[0] * sizeof(float), 128); // VKQ32 @@ -2461,18 +2501,18 @@ int op_flash_attn_ext(struct htp_ops_context * octx) { uint8_t * vtcm_cur = octx->ctx->vtcm_base; - factx.spad_q = vtcm_seq_alloc(&vtcm_cur, size_q_block * octx->n_threads); - factx.spad_k = vtcm_seq_alloc(&vtcm_cur, factx.size_k_block * 2 * octx->n_threads); - factx.spad_v = vtcm_seq_alloc(&vtcm_cur, factx.size_v_block * 2 * octx->n_threads); - factx.spad_m = vtcm_seq_alloc(&vtcm_cur, (mask ? factx.size_m_block * HVX_FA_DMA_CACHE_SIZE : 0) * octx->n_threads); - factx.spad_a = vtcm_seq_alloc(&vtcm_cur, size_vkq_acc * octx->n_threads); + factx.spad_q = vtcm_seq_alloc(&vtcm_cur, size_q_block * n_threads); + factx.spad_k = vtcm_seq_alloc(&vtcm_cur, factx.size_k_block * 2 * n_threads); + factx.spad_v = vtcm_seq_alloc(&vtcm_cur, factx.size_v_block * 2 * n_threads); + factx.spad_m = vtcm_seq_alloc(&vtcm_cur, (mask ? factx.size_m_block * HVX_FA_DMA_CACHE_SIZE : 0) * n_threads); + factx.spad_a = vtcm_seq_alloc(&vtcm_cur, size_vkq_acc * n_threads); if ((size_t) (vtcm_cur - octx->ctx->vtcm_base) > octx->ctx->vtcm_size) { return HTP_STATUS_VTCM_TOO_SMALL; } if (!(octx->flags & HTP_OPFLAGS_SKIP_COMPUTE)) { - work_queue_run(octx->ctx->work_queue, flash_attn_ext_f16_thread, &factx, octx->n_threads); + work_queue_run(octx->ctx->work_queue, flash_attn_ext_f16_thread, &factx, n_threads); } return HTP_STATUS_OK; diff --git a/ggml/src/ggml-hexagon/htp/flash-attn-ops.h b/ggml/src/ggml-hexagon/htp/flash-attn-ops.h index c4d19063169b..0278454114ec 100644 --- a/ggml/src/ggml-hexagon/htp/flash-attn-ops.h +++ b/ggml/src/ggml-hexagon/htp/flash-attn-ops.h @@ -51,6 +51,7 @@ struct htp_fa_kernel_params { uint32_t qrows; uint32_t qrows_per_thread; + uint32_t qrow_start; float m0; float m1; uint32_t n_head_log2; diff --git a/ggml/src/ggml-hexagon/htp/gated-delta-net-ops.c b/ggml/src/ggml-hexagon/htp/gated-delta-net-ops.c index 96655215298a..0b6529571d15 100644 --- a/ggml/src/ggml-hexagon/htp/gated-delta-net-ops.c +++ b/ggml/src/ggml-hexagon/htp/gated-delta-net-ops.c @@ -4,10 +4,13 @@ #include "hvx-utils.h" #include "hex-fastdiv.h" +#include "hex-common.h" +#include "hex-profile.h" #define GGML_COMMON_DECL_C #include "ggml-common.h" #include "htp-ctx.h" +#include "htp-tensor.h" #ifndef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) @@ -22,6 +25,8 @@ struct htp_gdn_context { size_t state_bytes; uint8_t * vtcm_base; size_t vtcm_per_thread; + uint32_t row_start; + uint32_t nrows; }; static inline HVX_Vector gdn_mul_dot_f32(float * restrict dst, const float * restrict mul, const float * restrict dot, uint32_t n) { @@ -586,8 +591,9 @@ static void gated_delta_net_f32_pp_thread(unsigned int nth, unsigned int ith, vo const uint32_t n_seqs = v->ne[3]; const uint32_t K = octx->op_params[0]; - const uint32_t total_rows = H * n_seqs; - if (ith >= total_rows) { + const uint32_t row_end = gctx->row_start + gctx->nrows; + + if (ith >= gctx->nrows) { return; } @@ -621,11 +627,11 @@ static void gated_delta_net_f32_pp_thread(unsigned int nth, unsigned int ith, vo const uint64_t state_seq_stride = state->nb[3] / sizeof(float); const uint64_t state_size_per_snap = (uint64_t) S_v * S_v * H * n_seqs; - uint32_t ir_prefetch = ith; + uint32_t ir_prefetch = gctx->row_start + ith; int spad_idx = 0; // Prefetch preamble (up to 2 steps) - for (int k = 0; k < 2 && ir_prefetch < total_rows; k++) { + for (int k = 0; k < 2 && ir_prefetch < row_end; k++) { const uint32_t piv1 = fastmodulo(ir_prefetch, H, &fd_H); const uint32_t piv3 = fastdiv(ir_prefetch, &fd_H); const float * ps_in = state_in_base + (uint64_t) piv3 * state_seq_stride + (uint64_t) piv1 * S_v * S_v; @@ -646,8 +652,11 @@ static void gated_delta_net_f32_pp_thread(unsigned int nth, unsigned int ith, vo spad_idx ^= 1; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) (gctx->row_start + ith)); + int curr_spad_idx = 0; - for (uint32_t ir = ith; ir < total_rows; ir += nth) { + for (uint32_t ir = gctx->row_start + ith; ir < row_end; ir += nth) { dma_queue_pop(dma); dma_queue_pop(dma); @@ -812,7 +821,7 @@ static void gated_delta_net_f32_pp_thread(unsigned int nth, unsigned int ith, vo S_v * sizeof(float), S_v); // Prefetch next block (if any) - if (ir_prefetch < total_rows) { + if (ir_prefetch < row_end) { const uint32_t piv1 = fastmodulo(ir_prefetch, H, &fd_H); const uint32_t piv3 = fastdiv(ir_prefetch, &fd_H); const float * ps_in = state_in_base + (uint64_t) piv3 * state_seq_stride + (uint64_t) piv1 * S_v * S_v; @@ -828,6 +837,7 @@ static void gated_delta_net_f32_pp_thread(unsigned int nth, unsigned int ith, vo curr_spad_idx ^= 1; } dma_queue_flush(dma); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) row_end); } @@ -847,8 +857,9 @@ static void gated_delta_net_f32_tg_thread(unsigned int nth, unsigned int ith, vo const uint32_t H = v->ne[1]; const uint32_t n_seqs = v->ne[3]; - const uint32_t total_rows = H * n_seqs; - if (ith >= total_rows) { + const uint32_t row_end = gctx->row_start + gctx->nrows; + + if (ith >= gctx->nrows) { return; } @@ -881,11 +892,11 @@ static void gated_delta_net_f32_tg_thread(unsigned int nth, unsigned int ith, vo const uint64_t state_seq_stride = state->nb[3] / sizeof(float); - uint32_t ir_prefetch = ith; + uint32_t ir_prefetch = gctx->row_start + ith; int spad_idx = 0; // Prefetch preamble (up to 2 steps) - for (int k = 0; k < 2 && ir_prefetch < total_rows; k++) { + for (int k = 0; k < 2 && ir_prefetch < row_end; k++) { const uint32_t piv1 = fastmodulo(ir_prefetch, H, &fd_H); const uint32_t piv3 = fastdiv(ir_prefetch, &fd_H); const float * ps_in = state_in_base + (uint64_t) piv3 * state_seq_stride + (uint64_t) piv1 * S_v * S_v; @@ -906,8 +917,11 @@ static void gated_delta_net_f32_tg_thread(unsigned int nth, unsigned int ith, vo spad_idx ^= 1; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) (gctx->row_start + ith)); + int curr_spad_idx = 0; - for (uint32_t ir = ith; ir < total_rows; ir += nth) { + for (uint32_t ir = gctx->row_start + ith; ir < row_end; ir += nth) { dma_queue_pop(dma); dma_queue_pop(dma); @@ -1057,7 +1071,7 @@ static void gated_delta_net_f32_tg_thread(unsigned int nth, unsigned int ith, vo S_v * sizeof(float), S_v); // Prefetch next block (if any) - if (ir_prefetch < total_rows) { + if (ir_prefetch < row_end) { const uint32_t piv1 = fastmodulo(ir_prefetch, H, &fd_H); const uint32_t piv3 = fastdiv(ir_prefetch, &fd_H); const float * ps_in = state_in_base + (uint64_t) piv3 * state_seq_stride + (uint64_t) piv1 * S_v * S_v; @@ -1073,6 +1087,7 @@ static void gated_delta_net_f32_tg_thread(unsigned int nth, unsigned int ith, vo curr_spad_idx ^= 1; } dma_queue_flush(dma); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) row_end); } @@ -1085,10 +1100,6 @@ int op_gated_delta_net(struct htp_ops_context * octx) { const struct htp_tensor * state = octx->src[5]; const struct htp_tensor * dst = octx->dst; - if (!q || !k || !v || !g || !beta || !state || !dst) { - return HTP_STATUS_INVAL_PARAMS; - } - if (q->type != HTP_TYPE_F32 || k->type != HTP_TYPE_F32 || v->type != HTP_TYPE_F32 || g->type != HTP_TYPE_F32 || beta->type != HTP_TYPE_F32 || state->type != HTP_TYPE_F32 || dst->type != HTP_TYPE_F32) { @@ -1124,16 +1135,37 @@ int op_gated_delta_net(struct htp_ops_context * octx) { return HTP_STATUS_OK; } + const uint32_t total_rows = H * n_seqs; + + uint32_t row_start = 0; + uint32_t nrows = total_rows; + + if (octx->ctx->mdev.count > 1) { + const uint32_t head_bytes = S_v * sizeof(float); + const uint32_t rows_per_chunk = (head_bytes > 0) ? (HEX_L2_LINE_SIZE / hex_gcd_u32(head_bytes, HEX_L2_LINE_SIZE)) : 1; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_rows, htp_tensor_mdev_data_aligned(dst) ? rows_per_chunk : 0, + octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + struct htp_gdn_context gctx; gctx.octx = octx; - gctx.rows_per_thread = (H * n_seqs + octx->n_threads - 1) / octx->n_threads; + gctx.row_start = row_start; + gctx.nrows = nrows; + gctx.rows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); gctx.state_bytes = (size_t) S_v * S_v * sizeof(float); size_t state_aligned = (size_t) S_v * S_v * sizeof(float); state_aligned = (state_aligned + 127) & ~(size_t)127; - assert(octx->ctx->vtcm_base != NULL); - assert(octx->ctx->vtcm_size >= 2 * state_aligned * octx->n_threads); + assert(octx->ctx->vtcm_size >= 2 * state_aligned * n_threads); gctx.vtcm_base = octx->ctx->vtcm_base; gctx.vtcm_per_thread = 2 * state_aligned; @@ -1148,9 +1180,9 @@ int op_gated_delta_net(struct htp_ops_context * octx) { gctx.vtcm_per_thread * octx->n_threads, octx->n_threads); if (n_tokens == 1) { - worker_pool_run_func(octx->ctx->worker_pool, gated_delta_net_f32_tg_thread, &gctx, octx->n_threads); + work_queue_run(octx->ctx->work_queue, gated_delta_net_f32_tg_thread, &gctx, n_threads); } else { - worker_pool_run_func(octx->ctx->worker_pool, gated_delta_net_f32_pp_thread, &gctx, octx->n_threads); + work_queue_run(octx->ctx->work_queue, gated_delta_net_f32_pp_thread, &gctx, n_threads); } return HTP_STATUS_OK; diff --git a/ggml/src/ggml-hexagon/htp/get-rows-ops.c b/ggml/src/ggml-hexagon/htp/get-rows-ops.c index a87962d22910..d294ba57a042 100644 --- a/ggml/src/ggml-hexagon/htp/get-rows-ops.c +++ b/ggml/src/ggml-hexagon/htp/get-rows-ops.c @@ -10,6 +10,7 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" +#include "hex-common.h" #include "htp-ctx.h" #include "htp-ops.h" #include "htp-tensor.h" @@ -23,9 +24,12 @@ struct get_rows_context { const struct htp_get_rows_kernel_params * kparams; struct htp_get_rows_vtcm_layout vtcm_layout; uint8_t * vtcm_base; + uint32_t task_start; + uint32_t tasks; + uint32_t tasks_per_thread; }; -#define get_rows_preamble \ +#define get_rows_preamble \ const uint32_t ne00 = octx->src[0]->ne[0]; \ const uint32_t ne01 = octx->src[0]->ne[1]; \ const uint32_t ne02 = octx->src[0]->ne[2]; \ @@ -61,12 +65,12 @@ static void get_rows_thread_st_##IDX_TYPE(unsigned int nth, unsigned int ith, vo struct htp_ops_context * octx = grctx->octx; \ const struct htp_get_rows_kernel_params * kparams = grctx->kparams; \ get_rows_preamble; \ - const uint32_t dr = kparams->tasks_per_thread; \ - const uint32_t ir0 = dr * ith; \ - if (ir0 >= kparams->total_tasks) { \ + const uint32_t dr = grctx->tasks_per_thread; \ + const uint32_t ir0 = grctx->task_start + dr * ith; \ + if (ir0 >= grctx->task_start + grctx->tasks) { \ return; \ } \ - const uint32_t ir1 = MIN(ir0 + dr, kparams->total_tasks); \ + const uint32_t ir1 = MIN(ir0 + dr, grctx->task_start + grctx->tasks); \ const uint32_t row_size_bytes = htp_tensor_get_row_size(octx->src[0]->type, ne00); \ dma_queue * dma_queue = octx->ctx->dma[ith]; \ for (uint32_t i = ir0; i < ir1; ++i) { \ @@ -101,12 +105,12 @@ static void get_rows_thread_##TYPE_NAME##_##IDX_TYPE(unsigned int nth, unsigned const struct htp_get_rows_kernel_params * kparams = grctx->kparams; \ get_rows_preamble; \ struct htp_thread_trace * tr = &octx->ctx->trace[ith]; \ - const uint32_t dr = kparams->tasks_per_thread; \ - const uint32_t ir0 = dr * ith; \ - if (ir0 >= kparams->total_tasks) { \ + const uint32_t dr = grctx->tasks_per_thread; \ + const uint32_t ir0 = grctx->task_start + dr * ith; \ + if (ir0 >= grctx->task_start + grctx->tasks) { \ return; \ } \ - const uint32_t ir1 = MIN(ir0 + dr, kparams->total_tasks); \ + const uint32_t ir1 = MIN(ir0 + dr, grctx->task_start + grctx->tasks); \ const uint32_t chunks_per_row = kparams->chunks_per_row; \ const uint32_t chunk_size = kparams->chunk_size; \ dma_queue * dma_queue = octx->ctx->dma[ith]; \ @@ -225,13 +229,41 @@ int op_get_rows(struct htp_ops_context * octx) { return HTP_STATUS_OK; } + const struct htp_tensor * dst = octx->dst; + const uint32_t total_tasks = kparams->total_tasks; + const size_t dst_row_size = htp_tensor_get_row_size(dst->type, dst->ne[0]); + + uint32_t task_start = 0; + uint32_t tasks = total_tasks; + + if (octx->ctx->mdev.count > 1) { + uint32_t tasks_per_chunk = 1; + htp_tensor_mdev_rows_per_chunk(dst, dst_row_size / dst->ne[0], (uint32_t) dst_row_size, &tasks_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_tasks, tasks_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + task_start = range.start; + tasks = range.count; + } + + if (tasks == 0) { + return HTP_STATUS_OK; + } + + if (!htp_ops_context_set_n_threads(octx, (uint32_t) kparams->n_threads)) { + return HTP_STATUS_INVAL_PARAMS; + } + + const uint32_t n_threads = octx->n_threads; + struct get_rows_context grctx; grctx.octx = octx; grctx.kparams = kparams; grctx.vtcm_base = (uint8_t *)octx->ctx->vtcm_base; + grctx.task_start = task_start; + grctx.tasks = tasks; + grctx.tasks_per_thread = fastdiv(tasks + n_threads - 1, &octx->n_threads_div); const uint32_t ne00 = octx->src[0]->ne[0]; - htp_get_rows_vtcm_layout_build(&grctx.vtcm_layout, octx->src[0]->type, ne00, kparams->n_threads); + htp_get_rows_vtcm_layout_build(&grctx.vtcm_layout, octx->src[0]->type, ne00, n_threads); const bool is_i32 = (octx->src[1]->type == HTP_TYPE_I32); @@ -247,14 +279,14 @@ int op_get_rows(struct htp_ops_context * octx) { } } - FARF(HIGH, "get-rows: (%ux%ux%ux%u) x (%ux%ux%ux%u) -> (%ux%ux%ux%u) : src0-vtcm-size %zu dst-vtcm-size %zu use_dma=%d n_threads %d\n", + FARF(HIGH, "get-rows: (%ux%ux%ux%u) x (%ux%ux%ux%u) -> (%ux%ux%ux%u) : src0-vtcm-size %zu dst-vtcm-size %zu use-dma %d n-threads %d\n", octx->src[0]->ne[0], octx->src[0]->ne[1], octx->src[0]->ne[2], octx->src[0]->ne[3], octx->src[1]->ne[0], octx->src[1]->ne[1], octx->src[1]->ne[2], octx->src[1]->ne[3], octx->dst->ne[0], octx->dst->ne[1], octx->dst->ne[2], octx->dst->ne[3], - grctx.vtcm_layout.src0_bytes_per_thread * kparams->n_threads, - grctx.vtcm_layout.dst_bytes_per_thread * kparams->n_threads, - kparams->use_dma, kparams->n_threads); + grctx.vtcm_layout.src0_bytes_per_thread * n_threads, + grctx.vtcm_layout.dst_bytes_per_thread * n_threads, + kparams->use_dma, n_threads); - work_queue_run(octx->ctx->work_queue, q_func, &grctx, kparams->n_threads); + work_queue_run(octx->ctx->work_queue, q_func, &grctx, n_threads); return HTP_STATUS_OK; } diff --git a/ggml/src/ggml-hexagon/htp/hex-common.h b/ggml/src/ggml-hexagon/htp/hex-common.h index 4714486a042f..e6a52540d58b 100644 --- a/ggml/src/ggml-hexagon/htp/hex-common.h +++ b/ggml/src/ggml-hexagon/htp/hex-common.h @@ -77,4 +77,13 @@ static inline bool hex_add_overflow(size_t a, size_t b, size_t *out) { return false; } +static inline uint32_t hex_gcd_u32(uint32_t a, uint32_t b) { + while (b != 0) { + uint32_t t = b; + b = a % b; + a = t; + } + return a; +} + #endif // HEX_COMMON_H diff --git a/ggml/src/ggml-hexagon/htp/hex-utils.h b/ggml/src/ggml-hexagon/htp/hex-utils.h index 1b3965030009..853f1c1b2d8d 100644 --- a/ggml/src/ggml-hexagon/htp/hex-utils.h +++ b/ggml/src/ggml-hexagon/htp/hex-utils.h @@ -39,7 +39,6 @@ static inline void hex_l2fetch_block(const void * addr, size_t size) { #define HEX_L2_LINE_SIZE 128 #define HEX_L2_BLOCK_SIZE (HEX_L2_LINE_SIZE * 4) // flush granularity (lines per loop iteration) -#define HEX_L2_FLUSH_IL_THRESHOLD 1024 // inline flush threshold #define HEX_L2_FLUSH_WQ_THRESHOLD (4 * 1024) #define HEX_L2_FLUSH_ALL_THRESHOLD (4 * 1024 * 1024) diff --git a/ggml/src/ggml-hexagon/htp/hmx-utils.h b/ggml/src/ggml-hexagon/htp/hmx-utils.h index 2a61ca7349df..ad295cb7df71 100644 --- a/ggml/src/ggml-hexagon/htp/hmx-utils.h +++ b/ggml/src/ggml-hexagon/htp/hmx-utils.h @@ -27,7 +27,7 @@ static inline void hmx_init_column_scales(void *out_scales, HVX_Vector v_scale) // vscatter offsets for fused dequant+transpose: write K-values directly to [K][N] tile. // word[i] = i*128 maps K-row-pair i to byte offset i*128. // Column offset (n*4) is added at runtime. Entries 0..15 cover one tile (region 2047); -// entries 16..31 cover the next adjacent tile (region 4095) — pick region size at the +// entries 16..31 cover the next adjacent tile (region 4095) - pick region size at the // call site to scatter into one tile (masked) or two contiguous tiles (unmasked). static const int32_t hmx_transpose_scatter_offsets[32] __attribute__((aligned(VLEN))) = { 0 * 128, 1 * 128, 2 * 128, 3 * 128, 4 * 128, 5 * 128, 6 * 128, 7 * 128, 8 * 128, 9 * 128, 10 * 128, @@ -198,16 +198,16 @@ static inline void hmx_interleave_cols_to_tiles(__fp16 * restrict tiles_out, } // --- HMX inline asm macros for load-store packetization --- -#define HMX_LOAD_MPY_F16(act, wt, range) \ - "{\n" \ +#define HMX_LOAD_MPY_F16(act, wt, range) \ + "{\n" \ " activation.hf = mxmem(" act ", " range ")\n" \ - " weight.hf = mxmem(" wt ", " range ")\n" \ + " weight.hf = mxmem(" wt ", " range ")\n" \ "}\n" -#define HMX_LOAD_MPY_DEEP_F16(act, wt, range) \ - "{\n" \ +#define HMX_LOAD_MPY_DEEP_F16(act, wt, range) \ + "{\n" \ " activation.hf = mxmem(" act ", " range "):deep\n" \ - " weight.hf = mxmem(" wt ", " range ")\n" \ + " weight.hf = mxmem(" wt ", " range ")\n" \ "}\n" #define HMX_STORE_AFTER_F16(out, scale_reg) \ diff --git a/ggml/src/ggml-hexagon/htp/htp-ctx.h b/ggml/src/ggml-hexagon/htp/htp-ctx.h index c8a909d61907..3b60c8bdb08c 100644 --- a/ggml/src/ggml-hexagon/htp/htp-ctx.h +++ b/ggml/src/ggml-hexagon/htp/htp-ctx.h @@ -19,7 +19,7 @@ #endif #define HTP_MAX_MMAPS 16 -#define HTP_MAX_DIRTY_RANGES 16 +#define HTP_MAX_DIRTY_RANGES 32 // Memory mapping struct htp_mmap { @@ -29,6 +29,11 @@ struct htp_mmap { uint32_t reserved; }; +struct htp_dirty_range { + uint32_t start; + uint32_t end; +}; + // Scratchpad state struct htp_spad { const struct htp_tensor * src; // original src of the data (for reuse) @@ -38,6 +43,14 @@ struct htp_spad { uint32_t size_per_thread; // size per thread }; +struct htp_mdev_group { + uint16_t idx; + uint16_t count; + struct fastdiv_values count_div; + uint8_t * fence_base; + uint32_t fence_seq; +}; + struct htp_context; // Context while processing an Op @@ -65,8 +78,10 @@ struct htp_ops_context { struct htp_spad src3_spad; struct htp_spad dst_spad; - uint32_t n_threads; - uint32_t flags; + uint32_t flags; + uint32_t n_threads; + struct fastdiv_values n_threads_div; + int status; }; // Main context for htp DSP backend @@ -76,6 +91,7 @@ struct htp_context { struct htp_mmap mmap[HTP_MAX_MMAPS]; dma_queue_t dma[HTP_MAX_NTHREADS]; dma_queue_t dma_cached[HTP_MAX_NTHREADS]; + struct htp_thread_trace trace[HTP_MAX_NTHREADS + 1]; work_queue_t work_queue; hmx_queue_t hmx_queue; @@ -88,7 +104,6 @@ struct htp_context { bool hmx_enabled; bool etm; uint32_t profiler; - struct htp_thread_trace trace[HTP_MAX_NTHREADS + 1]; uint8_t * vtcm_base; size_t vtcm_size; @@ -97,16 +112,13 @@ struct htp_context { atomic_bool vtcm_needs_release; uint64_t max_vmem; - struct htp_dirty_range { - uint32_t start; - uint32_t end; - uint32_t bi; - } dirty_ranges[HTP_MAX_DIRTY_RANGES]; + struct htp_dirty_range dirty_ranges[HTP_MAX_DIRTY_RANGES]; // Persistent DDR scratchpad for MUL_MAT_ID mappings void * ddr_spad_base; size_t ddr_spad_size; + struct htp_mdev_group mdev; struct htp_ops_context octx; qurt_thread_t main_thread; @@ -115,6 +127,27 @@ struct htp_context { size_t footprint; }; +static inline bool htp_ops_context_set_n_threads(struct htp_ops_context * octx, uint32_t n_threads) { + if (n_threads == 0 || n_threads > octx->ctx->n_threads) { + return false; + } + + if (n_threads != octx->n_threads) { + octx->n_threads = n_threads; + octx->n_threads_div = n_threads == octx->ctx->n_threads + ? octx->ctx->n_threads_div + : init_fastdiv_values(n_threads); + } + + return true; +} + +static inline void htp_ops_context_set_status(struct htp_ops_context * octx, int status) { + if (status > HTP_STATUS_OK && octx->status == HTP_STATUS_OK) { + octx->status = status; + } +} + int op_matmul(struct htp_ops_context * octx); int op_matmul_id(struct htp_ops_context * octx); int op_matmul_nx(struct htp_ops_context * octx); diff --git a/ggml/src/ggml-hexagon/htp/htp-fence.h b/ggml/src/ggml-hexagon/htp/htp-fence.h new file mode 100644 index 000000000000..7450b5de5363 --- /dev/null +++ b/ggml/src/ggml-hexagon/htp/htp-fence.h @@ -0,0 +1,89 @@ +#ifndef HTP_FENCE_H +#define HTP_FENCE_H + +#include <stdatomic.h> +#include <stdint.h> + +#include <HAP_farf.h> + +#include "hex-utils.h" +#include "htp-ops.h" +#include "htp-ctx.h" + +static inline atomic_uint * htp_mdev_fence_slot(const void * fence_base, uint32_t idx) { + return (atomic_uint *) ((const uint8_t *) fence_base + (size_t) idx * HTP_FENCE_SLOT_SIZE); +} + +static inline void htp_fence_write(void * fence_ptr, uint32_t seq, uint32_t status) { + atomic_uint * fence = (atomic_uint *) fence_ptr; + atomic_store(&fence[1], status); + atomic_store(&fence[0], seq); + asm volatile ("syncht" : : : "memory"); + Q6_dccleaninva_A((void *) fence); +} + +static inline void htp_fence_read(const void * fence_ptr, uint32_t * seq, uint32_t * status) { + const atomic_uint * fence = (const atomic_uint *) fence_ptr; + Q6_dccleaninva_A((void *) fence); + asm volatile ("syncht" : : : "memory"); + *seq = atomic_load(&fence[0]); + *status = atomic_load(&fence[1]); +} + +static inline void htp_mdev_group_barrier(struct htp_ops_context * octx) { + struct htp_context * ctx = octx->ctx; + if (ctx->mdev.count <= 1) { + return; + } + + const uint32_t seq = ++ctx->mdev.fence_seq; + + struct htp_thread_trace * tr = &ctx->trace[0]; + htp_trace_event_start(tr, HTP_TRACE_EVT_FENCE, (uint16_t) seq); + + const uint32_t mdev_idx = ctx->mdev.idx; + const uint32_t mdev_count = ctx->mdev.count; + + uint8_t * fence_base = ctx->mdev.fence_base; + atomic_uint * my_fence = htp_mdev_fence_slot(fence_base, mdev_idx); + htp_fence_write(my_fence, seq, octx->status); + + for (uint32_t d = 0; d < mdev_count; d++) { + if (d == mdev_idx) continue; + atomic_uint * peer_fence = htp_mdev_fence_slot(fence_base, d); + uint64_t spins = 0; + while (1) { + uint32_t peer_seq; + uint32_t peer_status; + htp_fence_read(peer_fence, &peer_seq, &peer_status); + if ((int32_t)(peer_seq - seq) >= 0) { + if (peer_status > HTP_STATUS_OK) { + FARF(ERROR, "ggml-hex: mdev %u peer %u failed with status %u : seq 0x%08x\n", + mdev_idx, d, peer_status, seq); + htp_ops_context_set_status(octx, peer_status); + } + break; + } + if (++spins == 10000) { + FARF(ALWAYS, "ggml-hex: mdev %u waiting for mdev %u : seq 0x%08x (b %u op %u) my-fence %p peer-fence %p peer-seq 0x%08x (diff %d)\n", + mdev_idx, d, seq, seq >> 12, seq & 0xfff, my_fence, peer_fence, peer_seq, (int32_t)(peer_seq - seq)); + } + if (spins > HTP_FENCE_TIMEOUT) { + FARF(ERROR, "ggml-hex: mdev %u timeout waiting for mdev %u : seq 0x%08x (b %u op %u) peer-fence %p peer-seq 0x%08x\n", + mdev_idx, d, seq, seq >> 12, seq & 0xfff, peer_fence, peer_seq); + htp_ops_context_set_status(octx, HTP_STATUS_INTERNAL_ERR); + break; + } + hex_pause(); + } + } + asm volatile ("syncht" : : : "memory"); + + if (octx->status > HTP_STATUS_OK) { + htp_fence_write(my_fence, seq, octx->status); + } + + htp_trace_event_stop(tr, HTP_TRACE_EVT_FENCE, (uint16_t) seq); +} + +#endif // HTP_FENCE_H diff --git a/ggml/src/ggml-hexagon/htp/htp-ops.h b/ggml/src/ggml-hexagon/htp/htp-ops.h index 12a61b67f261..869b19b8c2de 100644 --- a/ggml/src/ggml-hexagon/htp/htp-ops.h +++ b/ggml/src/ggml-hexagon/htp/htp-ops.h @@ -77,6 +77,7 @@ enum htp_op_code { HTP_OP_GET_ROWS, HTP_OP_SCALE, HTP_OP_CPY, + HTP_OP_CPY_FENCE, HTP_OP_ARGSORT, HTP_OP_SQR, HTP_OP_SQRT, @@ -100,6 +101,7 @@ enum htp_op_code { HTP_OP_ALLREDUCE, HTP_OP_ALLREDUCE_ADD, HTP_OP_GLU_SWIGLU_CLAMP, + HTP_OP_MDEV_GROUP, HTP_OP_INVALID }; @@ -114,6 +116,7 @@ enum htp_op_code { #define HTP_OP_MAX_TENSORS 8192 // must stay under 64K (uint16) #define HTP_FENCE_TIMEOUT (1000000000ULL) +#define HTP_FENCE_SLOT_SIZE 128 #define HTP_OP_MAX_VMEM_DEFAULT (3355443200u) @@ -214,30 +217,26 @@ struct htp_prof_desc { }; struct htp_opbatch_req { - uint32_t id; // Batch id + uint64_t seq; // Sequence number uint32_t n_bufs; // Number of buffers uint32_t n_tensors; // Number of tensors uint32_t n_ops; // Number of ops uint32_t n_traces; // Number of trace descriptors per thread - uint32_t pad; // unused - uint64_t seq; // Sequence number // struct htp_buf_desc bufs[]; -- dspqueue buf 0 // struct htp_tensor tensors[]; -- dspqueue buf 0 // struct htp_op_desc ops[]; -- dspqueue buf 0 }; struct htp_opbatch_rsp { - uint32_t id; // Batch id - uint32_t status; // HTP_STATUS_... - uint32_t n_bufs; // Number of buffers - uint32_t n_tensors; // Number of tensors - uint32_t n_ops; // Number of op profile descriptors - uint32_t n_traces[HTP_MAX_NTHREADS + 1]; - uint32_t usecs; // Number of usec - uint32_t pad; // align to 8 bytes + uint64_t seq; // Sequence number uint64_t cycles_start; // Start cycle counter uint64_t cycles_stop; // Stop cycle counter - uint64_t seq; // Sequence number + uint32_t status; // HTP_STATUS_... + uint32_t n_bufs; // Number of buffers + uint32_t n_tensors; // Number of tensors + uint32_t n_ops; // Number of op profile descriptors + uint32_t usecs; // Number of usec + uint32_t n_traces[HTP_MAX_NTHREADS + 1]; // struct htp_prof_desc profs[]; -- dspqueue buf 0 }; diff --git a/ggml/src/ggml-hexagon/htp/htp-tensor.c b/ggml/src/ggml-hexagon/htp/htp-tensor.c index ae377c9221ff..760ccd8313a4 100644 --- a/ggml/src/ggml-hexagon/htp/htp-tensor.c +++ b/ggml/src/ggml-hexagon/htp/htp-tensor.c @@ -20,7 +20,7 @@ struct l2flush_range { struct l2flush_multi_task { struct htp_thread_trace * trace; - struct l2flush_range ranges[HTP_OP_MAX_INPUTS]; + struct l2flush_range ranges[HTP_MAX_DIRTY_RANGES]; uint32_t n_ranges; uint32_t total_blocks; uint32_t blocks_per_thread; @@ -73,6 +73,27 @@ static void l2flush_multi_worker(unsigned int n, unsigned int i, void * data) { htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, gb_first); } +static void merge_dirty_ranges(struct htp_context * ctx) { + for (uint32_t i = 0; i < HTP_MAX_DIRTY_RANGES; i++) { + struct htp_dirty_range * r = &ctx->dirty_ranges[i]; + if (!r->start) continue; + + for (uint32_t j = 0; j < HTP_MAX_DIRTY_RANGES;) { + struct htp_dirty_range * s = &ctx->dirty_ranges[j]; + if (i == j || !s->start || r->end < s->start || s->end < r->start) { + j++; + continue; + } + + r->start = MIN(r->start, s->start); + r->end = MAX(r->end, s->end); + s->start = 0; + s->end = 0; + j = 0; + } + } +} + void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n) { const struct htp_tensor * pending[HTP_OP_MAX_OUTPUTS]; uint32_t n_pending = 0; @@ -83,11 +104,6 @@ void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * co continue; } - if (t->size <= HEX_L2_FLUSH_IL_THRESHOLD) { - hex_l2flush((void *) (uintptr_t) t->data, t->size); - continue; - } - uint32_t t_start = t->data; uint32_t t_end = t_start + t->size; @@ -110,6 +126,8 @@ void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * co } } + merge_dirty_ranges(ctx); + if (n_pending == 0) { return; } @@ -132,8 +150,8 @@ void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * co struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; r->start = pending[i]->data; r->end = pending[i]->data + pending[i]->size; - r->bi = pending[i]->bi; } + merge_dirty_ranges(ctx); return; } @@ -151,12 +169,12 @@ void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * co struct htp_dirty_range * r = &ctx->dirty_ranges[i]; r->start = pending[i]->data; r->end = pending[i]->data + pending[i]->size; - r->bi = pending[i]->bi; } + merge_dirty_ranges(ctx); return; } - if (total_evict_size > HEX_L2_FLUSH_WQ_THRESHOLD && ctx->n_threads > 1 && n_evict <= HTP_OP_MAX_INPUTS) { + if (total_evict_size > HEX_L2_FLUSH_WQ_THRESHOLD && ctx->n_threads > 1 && n_evict <= HTP_MAX_DIRTY_RANGES) { struct l2flush_multi_task task; task.trace = ctx->trace; task.n_ranges = n_evict; @@ -195,7 +213,6 @@ void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * co struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; r->start = pending[i]->data; r->end = pending[i]->data + pending[i]->size; - r->bi = pending[i]->bi; } for (uint32_t i = 0; i < n_empty; i++) { @@ -203,8 +220,9 @@ void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * co struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; r->start = pending[n_evict + i]->data; r->end = pending[n_evict + i]->data + pending[n_evict + i]->size; - r->bi = pending[n_evict + i]->bi; } + + merge_dirty_ranges(ctx); } static void make_tensor_clean(struct htp_context * ctx, const struct htp_tensor * t) { @@ -242,17 +260,50 @@ static inline bool is_tensor_dirty(struct htp_context * ctx, const struct htp_te return false; } -void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n) { - const struct htp_tensor * dirty_tensors[HTP_OP_MAX_INPUTS]; - uint32_t n_dirty = 0; +static void flush_dirty_ranges(struct htp_context * ctx, const struct htp_dirty_range * ranges, uint32_t n_ranges, uint64_t total_dirty) { + if (total_dirty >= HEX_L2_FLUSH_WQ_THRESHOLD && ctx->n_threads > 1) { + struct l2flush_multi_task task; + task.trace = ctx->trace; + task.n_ranges = n_ranges; + + uint32_t block_acc = 0; + for (uint32_t i = 0; i < n_ranges; i++) { + const struct htp_dirty_range * r = &ranges[i]; + struct l2flush_range * rg = &task.ranges[i]; + rg->start = hex_align_down((size_t) r->start, HEX_L2_LINE_SIZE); + rg->end = hex_align_up((size_t) r->end, HEX_L2_LINE_SIZE); + rg->block_first = block_acc; + rg->n_blocks = (rg->end - rg->start + HEX_L2_BLOCK_SIZE - 1) / HEX_L2_BLOCK_SIZE; + block_acc += rg->n_blocks; + } + + task.total_blocks = block_acc; + task.blocks_per_thread = fastdiv(block_acc + ctx->n_threads - 1, &ctx->n_threads_div); + + work_queue_run(ctx->work_queue, l2flush_multi_worker, &task, ctx->n_threads); + } else { + struct htp_thread_trace * tr = &ctx->trace[0]; + htp_trace_event_start(tr, HTP_TRACE_EVT_L2FLUSH, 0); + for (uint32_t i = 0; i < n_ranges; i++) { + const struct htp_dirty_range * r = &ranges[i]; + hex_l2flush((void *) (uintptr_t) r->start, r->end - r->start); + } + htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, 0); + } +} + +void htp_flush_dirty_ranges(struct htp_context * ctx) { + struct htp_dirty_range ranges[HTP_MAX_DIRTY_RANGES]; + uint32_t n_ranges = 0; uint64_t total_dirty = 0; - for (uint32_t i = 0; i < n; i++) { - const struct htp_tensor * t = tensors[i]; - if (t && !(t->flags & (HTP_TENSOR_WEIGHT | HTP_TENSOR_FENCE)) && is_tensor_dirty(ctx, t)) { - dirty_tensors[n_dirty++] = t; - total_dirty += t->size; + for (uint32_t i = 0; i < HTP_MAX_DIRTY_RANGES; i++) { + const struct htp_dirty_range * r = &ctx->dirty_ranges[i]; + if (!r->start) { + continue; } + ranges[n_ranges++] = *r; + total_dirty += r->end - r->start; } if (total_dirty == 0) { @@ -264,37 +315,37 @@ void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * co return; } - if (total_dirty >= HEX_L2_FLUSH_WQ_THRESHOLD && ctx->n_threads > 1) { - struct l2flush_multi_task task; - task.trace = ctx->trace; - task.n_ranges = 0; + flush_dirty_ranges(ctx, ranges, n_ranges, total_dirty); + memset(ctx->dirty_ranges, 0, sizeof(ctx->dirty_ranges)); +} - uint32_t block_acc = 0; - for (uint32_t i = 0; i < n_dirty; i++) { - const struct htp_tensor * t = dirty_tensors[i]; - make_tensor_clean(ctx, t); +void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n) { + const struct htp_tensor * dirty_tensors[HTP_OP_MAX_INPUTS]; + struct htp_dirty_range ranges[HTP_OP_MAX_INPUTS]; + uint32_t n_dirty = 0; + uint64_t total_dirty = 0; - struct l2flush_range * rg = &task.ranges[task.n_ranges++]; - rg->start = hex_align_down((size_t) t->data, HEX_L2_LINE_SIZE); - rg->end = hex_align_up((size_t) t->data + t->size, HEX_L2_LINE_SIZE); - rg->block_first = block_acc; - rg->n_blocks = (rg->end - rg->start + HEX_L2_BLOCK_SIZE - 1) / HEX_L2_BLOCK_SIZE; - block_acc += rg->n_blocks; + for (uint32_t i = 0; i < n; i++) { + const struct htp_tensor * t = tensors[i]; + if (t && is_tensor_dirty(ctx, t)) { + dirty_tensors[n_dirty++] = t; + ranges[n_dirty - 1].start = t->data; + ranges[n_dirty - 1].end = t->data + t->size; + total_dirty += t->size; } + } - task.total_blocks = block_acc; - task.blocks_per_thread = fastdiv(block_acc + ctx->n_threads - 1, &ctx->n_threads_div); + if (total_dirty == 0) { + return; + } - work_queue_run(ctx->work_queue, l2flush_multi_worker, &task, ctx->n_threads); + if (total_dirty > HEX_L2_FLUSH_ALL_THRESHOLD) { + flush_all_dcache(ctx); return; } - struct htp_thread_trace * tr = &ctx->trace[0]; + flush_dirty_ranges(ctx, ranges, n_dirty, total_dirty); for (uint32_t i = 0; i < n_dirty; i++) { - const struct htp_tensor * t = dirty_tensors[i]; - htp_trace_event_start(tr, HTP_TRACE_EVT_L2FLUSH, t->ti); - hex_l2flush((void *) (uintptr_t) t->data, t->size); - htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, t->ti); - make_tensor_clean(ctx, t); + make_tensor_clean(ctx, dirty_tensors[i]); } } diff --git a/ggml/src/ggml-hexagon/htp/htp-tensor.h b/ggml/src/ggml-hexagon/htp/htp-tensor.h index c9cadbae3f23..3afff6917000 100644 --- a/ggml/src/ggml-hexagon/htp/htp-tensor.h +++ b/ggml/src/ggml-hexagon/htp/htp-tensor.h @@ -2,8 +2,20 @@ #define HTP_TENSOR_H #include <stdint.h> +#include <stdbool.h> #include "htp-ops.h" #include "hex-bitmap.h" +#include "hex-common.h" +#include "hex-fastdiv.h" + +enum { + HTP_TENSOR_MDEV_LINE_SIZE = 128, +}; + +struct htp_tensor_mdev_range { + uint32_t start; + uint32_t count; +}; static inline void * htp_tensor_data(const struct htp_tensor * t) { return (void *) (uintptr_t) t->data; @@ -13,6 +25,102 @@ static inline uint32_t * htp_tensor_flags(const struct htp_tensor * t) { return (uint32_t *) &t->flags; } +static inline bool htp_tensor_is_contiguous(const struct htp_tensor * t, uint32_t type_size) { + uint32_t next_nb = type_size; + if (t->ne[0] != 1 && t->nb[0] != next_nb) { + return false; + } + next_nb *= t->ne[0]; + for (int i = 1; i < HTP_OP_MAX_DIMS; i++) { + if (t->ne[i] != 1 && t->nb[i] != next_nb) { + return false; + } + next_nb *= t->ne[i]; + } + return true; +} + +static inline bool htp_tensor_is_permuted(const struct htp_tensor * t) { + return t->nb[0] > t->nb[1] || t->nb[1] > t->nb[2] || t->nb[2] > t->nb[3]; +} + +static inline bool htp_tensor_mdev_data_aligned(const struct htp_tensor * t) { + return ((uintptr_t) t->data & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0; +} + +static inline bool htp_tensor_can_row_partition(const struct htp_tensor * t, uint32_t elem_size) { + if (!htp_tensor_mdev_data_aligned(t)) { + return false; + } + if (t->ne[0] != 1 && t->nb[0] != elem_size) { + return false; + } + if (htp_tensor_is_permuted(t)) { + return false; + } + if (t->ne[1] > 1 && (t->nb[1] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) != 0) return false; + if (t->ne[2] > 1 && (t->nb[2] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) != 0) return false; + if (t->ne[3] > 1 && (t->nb[3] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) != 0) return false; + return true; +} + +static inline bool htp_tensor_mdev_rows_per_chunk(const struct htp_tensor * t, uint32_t elem_size, uint32_t row_size, uint32_t * rows_per_chunk) { + *rows_per_chunk = 0; + + if (!htp_tensor_mdev_data_aligned(t)) { + return false; + } + if (t->ne[0] != 1 && t->nb[0] != elem_size) { + return false; + } + if (htp_tensor_is_permuted(t)) { + return false; + } + if (t->ne[1] > 1 && (t->nb[1] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0 && + (t->ne[2] <= 1 || (t->nb[2] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0) && + (t->ne[3] <= 1 || (t->nb[3] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0)) { + *rows_per_chunk = 1; + return true; + } + if (t->nb[1] == row_size && + (t->ne[2] <= 1 || t->nb[2] == t->nb[1] * t->ne[1]) && + (t->ne[3] <= 1 || t->nb[3] == t->nb[2] * t->ne[2])) { + *rows_per_chunk = (row_size > 0) ? (HTP_TENSOR_MDEV_LINE_SIZE / hex_gcd_u32(row_size, HTP_TENSOR_MDEV_LINE_SIZE)) : 1; + return true; + } + return false; +} + +static inline struct htp_tensor_mdev_range htp_tensor_mdev_partition(uint32_t total_units, uint32_t units_per_chunk, uint32_t mdev_idx, uint32_t mdev_count, const struct fastdiv_values * mdev_count_div) { + struct htp_tensor_mdev_range range = { 0, total_units }; + + if (mdev_count <= 1) { + return range; + } + + if (units_per_chunk == 0) { + range.start = (mdev_idx == 0) ? 0 : total_units; + range.count = (mdev_idx == 0) ? total_units : 0; + return range; + } + + const uint32_t total_chunks = total_units / units_per_chunk; + if (total_chunks < mdev_count) { + range.start = (mdev_idx == 0) ? 0 : total_units; + range.count = (mdev_idx == 0) ? total_units : 0; + return range; + } + + const uint32_t chunks_per_mdev = fastdiv(total_chunks + mdev_count - 1, mdev_count_div); + range.start = MIN(mdev_idx * chunks_per_mdev * units_per_chunk, total_units); + if (mdev_idx == mdev_count - 1) { + range.count = total_units - range.start; + } else { + range.count = MIN(chunks_per_mdev * units_per_chunk, total_units - range.start); + } + return range; +} + static inline uint32_t htp_tensor_get_row_size(int type, uint32_t ne00) { switch (type) { case HTP_TYPE_F32: return ne00 * 4; @@ -23,6 +131,7 @@ static inline uint32_t htp_tensor_get_row_size(int type, uint32_t ne00) { } struct htp_context; +void htp_flush_dirty_ranges(struct htp_context * ctx); void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n); void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n); diff --git a/ggml/src/ggml-hexagon/htp/hvx-arith.h b/ggml/src/ggml-hexagon/htp/hvx-arith.h index fe5477c1be48..6cbead74c70e 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-arith.h +++ b/ggml/src/ggml-hexagon/htp/hvx-arith.h @@ -16,25 +16,25 @@ #define UNUSED(x) (void)(x) #define hvx_arith_loop_body(dst_type, src0_type, src1_type, elem_size, vec_store, vec_op) \ - do { \ - dst_type * vdst = (dst_type *) dst; \ - src0_type * vsrc0 = (src0_type *) src0; \ - src1_type * vsrc1 = (src1_type *) src1; \ - \ - 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] = vec_op(vsrc0[i], vsrc1[i]); \ - } \ - if (nloe) { \ - HVX_Vector v = vec_op(vsrc0[i], vsrc1[i]); \ - vec_store((void *) &vdst[i], nloe * (elem_size), v); \ - } \ + do { \ + dst_type * vdst = (dst_type *) dst; \ + src0_type * vsrc0 = (src0_type *) src0; \ + src1_type * vsrc1 = (src1_type *) src1; \ + \ + 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] = vec_op(vsrc0[i], vsrc1[i]); \ + } \ + if (nloe) { \ + HVX_Vector v = vec_op(vsrc0[i], vsrc1[i]); \ + vec_store((void *) &vdst[i], nloe * (elem_size), v); \ + } \ } while(0) #if __HVX_ARCH__ < 79 @@ -56,43 +56,43 @@ #define HVX_OP_MUL_F16(a, b) hvx_vec_mul_f16_f16(a, b) // Generic macro to define alignment permutations for an op -#define DEFINE_HVX_BINARY_OP_VARIANTS(OP_NAME, OP_MACRO, ELEM_TYPE) \ -static inline void OP_NAME##_aaa(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - assert((uintptr_t) src0 % 128 == 0); \ - assert((uintptr_t) src1 % 128 == 0); \ - hvx_arith_loop_body(HVX_Vector, HVX_Vector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ -} \ -static inline void OP_NAME##_aau(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - assert((uintptr_t) src0 % 128 == 0); \ - hvx_arith_loop_body(HVX_Vector, HVX_Vector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ -} \ -static inline void OP_NAME##_aua(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - assert((uintptr_t) src1 % 128 == 0); \ - hvx_arith_loop_body(HVX_Vector, HVX_UVector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ -} \ -static inline void OP_NAME##_auu(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - hvx_arith_loop_body(HVX_Vector, HVX_UVector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ -} \ -static inline void OP_NAME##_uaa(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ - assert((uintptr_t) src0 % 128 == 0); \ - assert((uintptr_t) src1 % 128 == 0); \ - hvx_arith_loop_body(HVX_UVector, HVX_Vector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ -} \ -static inline void OP_NAME##_uau(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ - assert((uintptr_t) src0 % 128 == 0); \ - hvx_arith_loop_body(HVX_UVector, HVX_Vector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ -} \ -static inline void OP_NAME##_uua(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ - assert((uintptr_t) src1 % 128 == 0); \ - hvx_arith_loop_body(HVX_UVector, HVX_UVector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ -} \ -static inline void OP_NAME##_uuu(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ +#define DEFINE_HVX_BINARY_OP_VARIANTS(OP_NAME, OP_MACRO, ELEM_TYPE) \ +static inline void OP_NAME##_aaa(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ + assert((uintptr_t) dst % 128 == 0); \ + assert((uintptr_t) src0 % 128 == 0); \ + assert((uintptr_t) src1 % 128 == 0); \ + hvx_arith_loop_body(HVX_Vector, HVX_Vector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ +} \ +static inline void OP_NAME##_aau(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ + assert((uintptr_t) dst % 128 == 0); \ + assert((uintptr_t) src0 % 128 == 0); \ + hvx_arith_loop_body(HVX_Vector, HVX_Vector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ +} \ +static inline void OP_NAME##_aua(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ + assert((uintptr_t) dst % 128 == 0); \ + assert((uintptr_t) src1 % 128 == 0); \ + hvx_arith_loop_body(HVX_Vector, HVX_UVector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ +} \ +static inline void OP_NAME##_auu(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ + assert((uintptr_t) dst % 128 == 0); \ + hvx_arith_loop_body(HVX_Vector, HVX_UVector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ +} \ +static inline void OP_NAME##_uaa(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ + assert((uintptr_t) src0 % 128 == 0); \ + assert((uintptr_t) src1 % 128 == 0); \ + hvx_arith_loop_body(HVX_UVector, HVX_Vector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ +} \ +static inline void OP_NAME##_uau(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ + assert((uintptr_t) src0 % 128 == 0); \ + hvx_arith_loop_body(HVX_UVector, HVX_Vector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ +} \ +static inline void OP_NAME##_uua(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ + assert((uintptr_t) src1 % 128 == 0); \ + hvx_arith_loop_body(HVX_UVector, HVX_UVector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ +} \ +static inline void OP_NAME##_uuu(uint8_t * dst, const uint8_t * src0, const uint8_t * src1, uint32_t n) { \ hvx_arith_loop_body(HVX_UVector, HVX_UVector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ -} \ +} \ DEFINE_HVX_BINARY_OP_VARIANTS(hvx_add_f32, HVX_OP_ADD_F32, float) DEFINE_HVX_BINARY_OP_VARIANTS(hvx_sub_f32, HVX_OP_SUB_F32, float) @@ -103,25 +103,25 @@ DEFINE_HVX_BINARY_OP_VARIANTS(hvx_sub_f16, HVX_OP_SUB_F16, _Float16) DEFINE_HVX_BINARY_OP_VARIANTS(hvx_mul_f16, HVX_OP_MUL_F16, _Float16) // Dispatcher logic -#define HVX_BINARY_DISPATCHER(OP_NAME) \ +#define HVX_BINARY_DISPATCHER(OP_NAME) \ static inline void OP_NAME(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, const uint32_t num_elems) { \ - if (hex_is_aligned((void *) dst, 128)) { \ - if (hex_is_aligned((void *) src0, 128)) { \ - if (hex_is_aligned((void *) src1, 128)) OP_NAME##_aaa(dst, src0, src1, num_elems); \ - else OP_NAME##_aau(dst, src0, src1, num_elems); \ - } else { \ - if (hex_is_aligned((void *) src1, 128)) OP_NAME##_aua(dst, src0, src1, num_elems); \ - else OP_NAME##_auu(dst, src0, src1, num_elems); \ - } \ - } else { \ - if (hex_is_aligned((void *) src0, 128)) { \ - if (hex_is_aligned((void *) src1, 128)) OP_NAME##_uaa(dst, src0, src1, num_elems); \ - else OP_NAME##_uau(dst, src0, src1, num_elems); \ - } else { \ - if (hex_is_aligned((void *) src1, 128)) OP_NAME##_uua(dst, src0, src1, num_elems); \ - else OP_NAME##_uuu(dst, src0, src1, num_elems); \ - } \ - } \ + if (hex_is_aligned((void *) dst, 128)) { \ + if (hex_is_aligned((void *) src0, 128)) { \ + if (hex_is_aligned((void *) src1, 128)) OP_NAME##_aaa(dst, src0, src1, num_elems); \ + else OP_NAME##_aau(dst, src0, src1, num_elems); \ + } else { \ + if (hex_is_aligned((void *) src1, 128)) OP_NAME##_aua(dst, src0, src1, num_elems); \ + else OP_NAME##_auu(dst, src0, src1, num_elems); \ + } \ + } else { \ + if (hex_is_aligned((void *) src0, 128)) { \ + if (hex_is_aligned((void *) src1, 128)) OP_NAME##_uaa(dst, src0, src1, num_elems); \ + else OP_NAME##_uau(dst, src0, src1, num_elems); \ + } else { \ + if (hex_is_aligned((void *) src1, 128)) OP_NAME##_uua(dst, src0, src1, num_elems); \ + else OP_NAME##_uuu(dst, src0, src1, num_elems); \ + } \ + } \ } HVX_BINARY_DISPATCHER(hvx_add_f32) @@ -166,44 +166,44 @@ static inline void hvx_mul_mul_f32_aa(uint8_t * restrict dst, const uint8_t * re // Scalar Operations -#define hvx_scalar_loop_body(dst_type, src_type, elem_size, vec_store, scalar_op_macro) \ - do { \ - dst_type * restrict vdst = (dst_type *) dst; \ - src_type * restrict vsrc = (src_type *) src; \ - \ - 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++) { \ - HVX_Vector v = vsrc[i]; \ - vdst[i] = scalar_op_macro(v); \ - } \ - if (nloe) { \ - HVX_Vector v = vsrc[i]; \ - v = scalar_op_macro(v); \ - vec_store((void *) &vdst[i], nloe * (elem_size), v); \ - } \ +#define hvx_scalar_loop_body(dst_type, src_type, elem_size, vec_store, scalar_op_macro) \ + do { \ + dst_type * restrict vdst = (dst_type *) dst; \ + src_type * restrict vsrc = (src_type *) src; \ + \ + 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++) { \ + HVX_Vector v = vsrc[i]; \ + vdst[i] = scalar_op_macro(v); \ + } \ + if (nloe) { \ + HVX_Vector v = vsrc[i]; \ + v = scalar_op_macro(v); \ + vec_store((void *) &vdst[i], nloe * (elem_size), v); \ + } \ } while(0) -#define HVX_OP_ADD_SCALAR_F32(v) \ - ({ \ +#define HVX_OP_ADD_SCALAR_F32(v) \ + ({ \ const HVX_VectorPred pred_inf = Q6_Q_vcmp_eq_VwVw(inf, v); \ - HVX_Vector out = HVX_OP_ADD_F32(v, val_vec); \ - Q6_V_vmux_QVV(pred_inf, inf, out); \ + HVX_Vector out = HVX_OP_ADD_F32(v, val_vec); \ + Q6_V_vmux_QVV(pred_inf, inf, out); \ }) #define HVX_OP_MUL_SCALAR_F32(v) HVX_OP_MUL_F32(v, val_vec) #define HVX_OP_SUB_SCALAR_F32(v) HVX_OP_SUB_F32(v, val_vec) -#define HVX_OP_ADD_SCALAR_F16(v) \ - ({ \ +#define HVX_OP_ADD_SCALAR_F16(v) \ + ({ \ const HVX_VectorPred pred_inf = Q6_Q_vcmp_eq_VhVh(inf, v); \ - HVX_Vector out = HVX_OP_ADD_F16(v, val_vec); \ - Q6_V_vmux_QVV(pred_inf, inf, out); \ + HVX_Vector out = HVX_OP_ADD_F16(v, val_vec); \ + Q6_V_vmux_QVV(pred_inf, inf, out); \ }) #define HVX_OP_MUL_SCALAR_F16(v) HVX_OP_MUL_F16(v, val_vec) @@ -212,31 +212,31 @@ static inline void hvx_mul_mul_f32_aa(uint8_t * restrict dst, const uint8_t * re // Scalar Variants // Generic macro to define alignment permutations for an op -#define DEFINE_HVX_BINARY_SCALAR_OP_VARIANTS(OP_NAME, OP_MACRO, SPLAT_MACRO, ELEM_TYPE) \ +#define DEFINE_HVX_BINARY_SCALAR_OP_VARIANTS(OP_NAME, OP_MACRO, SPLAT_MACRO, ELEM_TYPE) \ static inline void OP_NAME##_aa(uint8_t * restrict dst, const uint8_t * restrict src, const ELEM_TYPE val, uint32_t n) { \ - const HVX_Vector val_vec = SPLAT_MACRO(val); \ - const HVX_Vector inf = SPLAT_MACRO((ELEM_TYPE)INFINITY); UNUSED(inf); \ - assert((uintptr_t) dst % 128 == 0); \ - assert((uintptr_t) src % 128 == 0); \ - hvx_scalar_loop_body(HVX_Vector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ -} \ + const HVX_Vector val_vec = SPLAT_MACRO(val); \ + const HVX_Vector inf = SPLAT_MACRO((ELEM_TYPE)INFINITY); UNUSED(inf); \ + assert((uintptr_t) dst % 128 == 0); \ + assert((uintptr_t) src % 128 == 0); \ + hvx_scalar_loop_body(HVX_Vector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ +} \ static inline void OP_NAME##_au(uint8_t * restrict dst, const uint8_t * restrict src, const ELEM_TYPE val, uint32_t n) { \ - const HVX_Vector val_vec = SPLAT_MACRO(val); \ - const HVX_Vector inf = SPLAT_MACRO((ELEM_TYPE)INFINITY); UNUSED(inf); \ - assert((uintptr_t) dst % 128 == 0); \ - hvx_scalar_loop_body(HVX_Vector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ -} \ + const HVX_Vector val_vec = SPLAT_MACRO(val); \ + const HVX_Vector inf = SPLAT_MACRO((ELEM_TYPE)INFINITY); UNUSED(inf); \ + assert((uintptr_t) dst % 128 == 0); \ + hvx_scalar_loop_body(HVX_Vector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_a, OP_MACRO); \ +} \ static inline void OP_NAME##_ua(uint8_t * restrict dst, const uint8_t * restrict src, const ELEM_TYPE val, uint32_t n) { \ - const HVX_Vector val_vec = SPLAT_MACRO(val); \ - const HVX_Vector inf = SPLAT_MACRO((ELEM_TYPE)INFINITY); UNUSED(inf); \ - assert((uintptr_t) src % 128 == 0); \ - hvx_scalar_loop_body(HVX_UVector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ -} \ + const HVX_Vector val_vec = SPLAT_MACRO(val); \ + const HVX_Vector inf = SPLAT_MACRO((ELEM_TYPE)INFINITY); UNUSED(inf); \ + assert((uintptr_t) src % 128 == 0); \ + hvx_scalar_loop_body(HVX_UVector, HVX_Vector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ +} \ static inline void OP_NAME##_uu(uint8_t * restrict dst, const uint8_t * restrict src, const ELEM_TYPE val, uint32_t n) { \ - const HVX_Vector val_vec = SPLAT_MACRO(val); \ - const HVX_Vector inf = SPLAT_MACRO((ELEM_TYPE)INFINITY); UNUSED(inf); \ - hvx_scalar_loop_body(HVX_UVector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ -} \ + const HVX_Vector val_vec = SPLAT_MACRO(val); \ + const HVX_Vector inf = SPLAT_MACRO((ELEM_TYPE)INFINITY); UNUSED(inf); \ + hvx_scalar_loop_body(HVX_UVector, HVX_UVector, sizeof(ELEM_TYPE), hvx_vec_store_u, OP_MACRO); \ +} \ DEFINE_HVX_BINARY_SCALAR_OP_VARIANTS(hvx_add_scalar_f32, HVX_OP_ADD_SCALAR_F32, hvx_vec_splat_f32, float) DEFINE_HVX_BINARY_SCALAR_OP_VARIANTS(hvx_sub_scalar_f32, HVX_OP_SUB_SCALAR_F32, hvx_vec_splat_f32, float) @@ -247,17 +247,17 @@ DEFINE_HVX_BINARY_SCALAR_OP_VARIANTS(hvx_sub_scalar_f16, HVX_OP_SUB_SCALAR_F16, DEFINE_HVX_BINARY_SCALAR_OP_VARIANTS(hvx_mul_scalar_f16, HVX_OP_MUL_SCALAR_F16, hvx_vec_splat_f16, _Float16) // Dispatcher logic -#define HVX_BINARY_SCALAR_DISPATCHER(OP_NAME, ELEM_TYPE) \ +#define HVX_BINARY_SCALAR_DISPATCHER(OP_NAME, ELEM_TYPE) \ static inline void OP_NAME(uint8_t * restrict dst, const uint8_t * restrict src, const ELEM_TYPE val, const uint32_t num_elems) { \ - if (hex_is_aligned((void *) dst, 128) && hex_is_aligned((void *) src, 128)) { \ - OP_NAME##_aa(dst, src, val, num_elems); \ - } else if (hex_is_aligned((void *) dst, 128)) { \ - OP_NAME##_au(dst, src, val, num_elems); \ - } else if (hex_is_aligned((void *) src, 128)) { \ - OP_NAME##_ua(dst, src, val, num_elems); \ - } else { \ - OP_NAME##_uu(dst, src, val, num_elems); \ - } \ + if (hex_is_aligned((void *) dst, 128) && hex_is_aligned((void *) src, 128)) { \ + OP_NAME##_aa(dst, src, val, num_elems); \ + } else if (hex_is_aligned((void *) dst, 128)) { \ + OP_NAME##_au(dst, src, val, num_elems); \ + } else if (hex_is_aligned((void *) src, 128)) { \ + OP_NAME##_ua(dst, src, val, num_elems); \ + } else { \ + OP_NAME##_uu(dst, src, val, num_elems); \ + } \ } HVX_BINARY_SCALAR_DISPATCHER(hvx_add_scalar_f32, float) @@ -350,12 +350,12 @@ static inline void hvx_max_scalar_f32(uint8_t * restrict dst, const uint8_t * re // CLAMP Scalar variants -#define HVX_OP_CLAMP_SCALAR(v) \ - ({ \ +#define HVX_OP_CLAMP_SCALAR(v) \ + ({ \ HVX_VectorPred pred_cap_right = Q6_Q_vcmp_gt_VsfVsf(v, max_vec); \ HVX_VectorPred pred_cap_left = Q6_Q_vcmp_gt_VsfVsf(min_vec, v); \ - HVX_Vector tmp = Q6_V_vmux_QVV(pred_cap_right, max_vec, v); \ - Q6_V_vmux_QVV(pred_cap_left, min_vec, tmp); \ + HVX_Vector tmp = Q6_V_vmux_QVV(pred_cap_right, max_vec, v); \ + Q6_V_vmux_QVV(pred_cap_left, min_vec, tmp); \ }) static inline void hvx_clamp_scalar_f32_aa(uint8_t * restrict dst, const uint8_t * restrict src, const float min, const float max, uint32_t n) { diff --git a/ggml/src/ggml-hexagon/htp/hvx-div.h b/ggml/src/ggml-hexagon/htp/hvx-div.h index 53ee304e749b..bb7ab0519dae 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-div.h +++ b/ggml/src/ggml-hexagon/htp/hvx-div.h @@ -219,64 +219,64 @@ static inline HVX_Vector hvx_vec_hybrid_div_f16(HVX_Vector vec1, HVX_Vector vec2 } while(0) // Generic macro to define alignment permutations for an op -#define DEFINE_HVX_DIV_OP_VARIANTS(OP_NAME, OP_LOOP_BODY) \ +#define DEFINE_HVX_DIV_OP_VARIANTS(OP_NAME, OP_LOOP_BODY) \ static inline void OP_NAME##_aaa(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - assert((uintptr_t) src0 % 128 == 0); \ - assert((uintptr_t) src1 % 128 == 0); \ - OP_LOOP_BODY(HVX_Vector, HVX_Vector, HVX_Vector, hvx_vec_store_a); \ -} \ + assert((uintptr_t) dst % 128 == 0); \ + assert((uintptr_t) src0 % 128 == 0); \ + assert((uintptr_t) src1 % 128 == 0); \ + OP_LOOP_BODY(HVX_Vector, HVX_Vector, HVX_Vector, hvx_vec_store_a); \ +} \ static inline void OP_NAME##_aau(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - assert((uintptr_t) src0 % 128 == 0); \ - OP_LOOP_BODY(HVX_Vector, HVX_Vector, HVX_UVector, hvx_vec_store_a); \ -} \ + assert((uintptr_t) dst % 128 == 0); \ + assert((uintptr_t) src0 % 128 == 0); \ + OP_LOOP_BODY(HVX_Vector, HVX_Vector, HVX_UVector, hvx_vec_store_a); \ +} \ static inline void OP_NAME##_aua(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - assert((uintptr_t) src1 % 128 == 0); \ - OP_LOOP_BODY(HVX_Vector, HVX_UVector, HVX_Vector, hvx_vec_store_a); \ -} \ + assert((uintptr_t) dst % 128 == 0); \ + assert((uintptr_t) src1 % 128 == 0); \ + OP_LOOP_BODY(HVX_Vector, HVX_UVector, HVX_Vector, hvx_vec_store_a); \ +} \ static inline void OP_NAME##_auu(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - OP_LOOP_BODY(HVX_Vector, HVX_UVector, HVX_UVector, hvx_vec_store_a); \ -} \ + assert((uintptr_t) dst % 128 == 0); \ + OP_LOOP_BODY(HVX_Vector, HVX_UVector, HVX_UVector, hvx_vec_store_a); \ +} \ static inline void OP_NAME##_uaa(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { \ - assert((uintptr_t) src0 % 128 == 0); \ - assert((uintptr_t) src1 % 128 == 0); \ - OP_LOOP_BODY(HVX_UVector, HVX_Vector, HVX_Vector, hvx_vec_store_u); \ -} \ + assert((uintptr_t) src0 % 128 == 0); \ + assert((uintptr_t) src1 % 128 == 0); \ + OP_LOOP_BODY(HVX_UVector, HVX_Vector, HVX_Vector, hvx_vec_store_u); \ +} \ static inline void OP_NAME##_uau(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { \ - assert((uintptr_t) src0 % 128 == 0); \ - OP_LOOP_BODY(HVX_UVector, HVX_Vector, HVX_UVector, hvx_vec_store_u); \ -} \ + assert((uintptr_t) src0 % 128 == 0); \ + OP_LOOP_BODY(HVX_UVector, HVX_Vector, HVX_UVector, hvx_vec_store_u); \ +} \ static inline void OP_NAME##_uua(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { \ - assert((uintptr_t) src1 % 128 == 0); \ - OP_LOOP_BODY(HVX_UVector, HVX_UVector, HVX_Vector, hvx_vec_store_u); \ -} \ + assert((uintptr_t) src1 % 128 == 0); \ + OP_LOOP_BODY(HVX_UVector, HVX_UVector, HVX_Vector, hvx_vec_store_u); \ +} \ static inline void OP_NAME##_uuu(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { \ - OP_LOOP_BODY(HVX_UVector, HVX_UVector, HVX_UVector, hvx_vec_store_u); \ -} \ + OP_LOOP_BODY(HVX_UVector, HVX_UVector, HVX_UVector, hvx_vec_store_u); \ +} \ // Dispatcher logic -#define HVX_DIV_DISPATCHER(OP_NAME) \ +#define HVX_DIV_DISPATCHER(OP_NAME) \ static inline void OP_NAME(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, const uint32_t num_elems) { \ - if (hex_is_aligned((void *) dst, 128)) { \ - if (hex_is_aligned((void *) src0, 128)) { \ - if (hex_is_aligned((void *) src1, 128)) OP_NAME##_aaa(dst, src0, src1, num_elems); \ - else OP_NAME##_aau(dst, src0, src1, num_elems); \ - } else { \ - if (hex_is_aligned((void *) src1, 128)) OP_NAME##_aua(dst, src0, src1, num_elems); \ - else OP_NAME##_auu(dst, src0, src1, num_elems); \ - } \ - } else { \ - if (hex_is_aligned((void *) src0, 128)) { \ - if (hex_is_aligned((void *) src1, 128)) OP_NAME##_uaa(dst, src0, src1, num_elems); \ - else OP_NAME##_uau(dst, src0, src1, num_elems); \ - } else { \ - if (hex_is_aligned((void *) src1, 128)) OP_NAME##_uua(dst, src0, src1, num_elems); \ - else OP_NAME##_uuu(dst, src0, src1, num_elems); \ - } \ - } \ + if (hex_is_aligned((void *) dst, 128)) { \ + if (hex_is_aligned((void *) src0, 128)) { \ + if (hex_is_aligned((void *) src1, 128)) OP_NAME##_aaa(dst, src0, src1, num_elems); \ + else OP_NAME##_aau(dst, src0, src1, num_elems); \ + } else { \ + if (hex_is_aligned((void *) src1, 128)) OP_NAME##_aua(dst, src0, src1, num_elems); \ + else OP_NAME##_auu(dst, src0, src1, num_elems); \ + } \ + } else { \ + if (hex_is_aligned((void *) src0, 128)) { \ + if (hex_is_aligned((void *) src1, 128)) OP_NAME##_uaa(dst, src0, src1, num_elems); \ + else OP_NAME##_uau(dst, src0, src1, num_elems); \ + } else { \ + if (hex_is_aligned((void *) src1, 128)) OP_NAME##_uua(dst, src0, src1, num_elems); \ + else OP_NAME##_uuu(dst, src0, src1, num_elems); \ + } \ + } \ } DEFINE_HVX_DIV_OP_VARIANTS(hvx_div_f32, hvx_div_f32_loop_body) diff --git a/ggml/src/ggml-hexagon/htp/hvx-inverse.h b/ggml/src/ggml-hexagon/htp/hvx-inverse.h index f2054f45baca..256a8843ba1b 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-inverse.h +++ b/ggml/src/ggml-hexagon/htp/hvx-inverse.h @@ -169,36 +169,36 @@ static inline HVX_Vector hvx_vec_inverse_f16_guard(HVX_Vector v_sf, HVX_Vector n } while(0) // Generic macro to define alignment permutations for an op -#define DEFINE_HVX_INV_OP_VARIANTS(OP_NAME, OP_LOOP_BODY) \ +#define DEFINE_HVX_INV_OP_VARIANTS(OP_NAME, OP_LOOP_BODY) \ static inline void OP_NAME##_aa(uint8_t * restrict dst, const uint8_t * restrict src, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - assert((uintptr_t) src % 128 == 0); \ - OP_LOOP_BODY(HVX_Vector, HVX_Vector, hvx_vec_store_a); \ -} \ + assert((uintptr_t) dst % 128 == 0); \ + assert((uintptr_t) src % 128 == 0); \ + OP_LOOP_BODY(HVX_Vector, HVX_Vector, hvx_vec_store_a); \ +} \ static inline void OP_NAME##_au(uint8_t * restrict dst, const uint8_t * restrict src, uint32_t n) { \ - assert((uintptr_t) dst % 128 == 0); \ - OP_LOOP_BODY(HVX_Vector, HVX_UVector, hvx_vec_store_a); \ -} \ + assert((uintptr_t) dst % 128 == 0); \ + OP_LOOP_BODY(HVX_Vector, HVX_UVector, hvx_vec_store_a); \ +} \ static inline void OP_NAME##_ua(uint8_t * restrict dst, const uint8_t * restrict src, uint32_t n) { \ - assert((uintptr_t) src % 128 == 0); \ - OP_LOOP_BODY(HVX_UVector, HVX_Vector, hvx_vec_store_u); \ -} \ + assert((uintptr_t) src % 128 == 0); \ + OP_LOOP_BODY(HVX_UVector, HVX_Vector, hvx_vec_store_u); \ +} \ static inline void OP_NAME##_uu(uint8_t * restrict dst, const uint8_t * restrict src, uint32_t n) { \ - OP_LOOP_BODY(HVX_UVector, HVX_UVector, hvx_vec_store_u); \ -} \ + OP_LOOP_BODY(HVX_UVector, HVX_UVector, hvx_vec_store_u); \ +} \ // Dispatcher logic -#define HVX_INV_DISPATCHER(OP_NAME) \ +#define HVX_INV_DISPATCHER(OP_NAME) \ static inline void OP_NAME(uint8_t * restrict dst, const uint8_t * restrict src, const uint32_t num_elems) { \ - if (hex_is_aligned((void *) dst, 128) && hex_is_aligned((void *) src, 128)) { \ - OP_NAME##_aa(dst, src, num_elems); \ - } else if (hex_is_aligned((void *) dst, 128)) { \ - OP_NAME##_au(dst, src, num_elems); \ - } else if (hex_is_aligned((void *) src, 128)) { \ - OP_NAME##_ua(dst, src, num_elems); \ - } else { \ - OP_NAME##_uu(dst, src, num_elems); \ - } \ + if (hex_is_aligned((void *) dst, 128) && hex_is_aligned((void *) src, 128)) { \ + OP_NAME##_aa(dst, src, num_elems); \ + } else if (hex_is_aligned((void *) dst, 128)) { \ + OP_NAME##_au(dst, src, num_elems); \ + } else if (hex_is_aligned((void *) src, 128)) { \ + OP_NAME##_ua(dst, src, num_elems); \ + } else { \ + OP_NAME##_uu(dst, src, num_elems); \ + } \ } DEFINE_HVX_INV_OP_VARIANTS(hvx_inverse_f32, hvx_inverse_f32_loop_body) diff --git a/ggml/src/ggml-hexagon/htp/hvx-scale.h b/ggml/src/ggml-hexagon/htp/hvx-scale.h index 9b1a28f529a2..5d0650307ef8 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-scale.h +++ b/ggml/src/ggml-hexagon/htp/hvx-scale.h @@ -68,30 +68,30 @@ static inline void hvx_scale_f32(uint8_t * restrict dst, const uint8_t * restric } } -#define hvx_scale_offset_f32_loop_body(dst_type, src_type, vec_store) \ - do { \ - dst_type * restrict vdst = (dst_type *) dst; \ - src_type * restrict vsrc = (src_type *) src; \ - \ - HVX_Vector vs = hvx_vec_splat_f32(scale); \ - HVX_Vector vo = hvx_vec_splat_f32(offset); \ - \ - 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) { \ +#define hvx_scale_offset_f32_loop_body(dst_type, src_type, vec_store) \ + do { \ + dst_type * restrict vdst = (dst_type *) dst; \ + src_type * restrict vsrc = (src_type *) src; \ + \ + HVX_Vector vs = hvx_vec_splat_f32(scale); \ + HVX_Vector vo = hvx_vec_splat_f32(offset); \ + \ + 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) { \ HVX_Vector v = Q6_Vqf32_vadd_Vqf32Vsf(Q6_Vqf32_vmpy_VsfVsf(vsrc[i], vs), vo); \ - vdst[i] = Q6_Vsf_equals_Vqf32(v); \ - } \ - if (nloe) { \ + vdst[i] = Q6_Vsf_equals_Vqf32(v); \ + } \ + if (nloe) { \ HVX_Vector v = Q6_Vqf32_vadd_Vqf32Vsf(Q6_Vqf32_vmpy_VsfVsf(vsrc[i], vs), vo); \ - vec_store((void *) &vdst[i], nloe * elem_size, Q6_Vsf_equals_Vqf32(v)); \ - } \ + vec_store((void *) &vdst[i], nloe * elem_size, Q6_Vsf_equals_Vqf32(v)); \ + } \ } while(0) static inline void hvx_scale_offset_f32_aa(uint8_t * restrict dst, const uint8_t * restrict src, const int n, const float scale, const float offset) { diff --git a/ggml/src/ggml-hexagon/htp/hvx-sigmoid.h b/ggml/src/ggml-hexagon/htp/hvx-sigmoid.h index dd66dd84c95a..552017309d19 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-sigmoid.h +++ b/ggml/src/ggml-hexagon/htp/hvx-sigmoid.h @@ -68,50 +68,50 @@ static inline HVX_Vector hvx_vec_tanh_f32(HVX_Vector x) { return Q6_Vsf_equals_Vqf32(res); } -#define hvx_sigmoid_loop_body(dst_type, src_type, vec_store) \ - do { \ - dst_type * restrict vdst = (dst_type *) dst; \ - src_type * restrict vsrc = (src_type *) src; \ - \ - const HVX_Vector one = hvx_vec_splat_f32(1.f); \ - const HVX_Vector max_exp = hvx_vec_splat_f32(87.f); \ - const HVX_Vector min_exp = hvx_vec_splat_f32(-87.f); \ - \ - const uint32_t epv = 128 / sizeof(float); \ - 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_fast_sigmoid_f32_guard(vsrc[i], one, max_exp, min_exp); \ - } \ - if (nloe) { \ +#define hvx_sigmoid_loop_body(dst_type, src_type, vec_store) \ + do { \ + dst_type * restrict vdst = (dst_type *) dst; \ + src_type * restrict vsrc = (src_type *) src; \ + \ + const HVX_Vector one = hvx_vec_splat_f32(1.f); \ + const HVX_Vector max_exp = hvx_vec_splat_f32(87.f); \ + const HVX_Vector min_exp = hvx_vec_splat_f32(-87.f); \ + \ + const uint32_t epv = 128 / sizeof(float); \ + 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_fast_sigmoid_f32_guard(vsrc[i], one, max_exp, min_exp); \ + } \ + if (nloe) { \ HVX_Vector tmp = hvx_vec_fast_sigmoid_f32_guard(vsrc[i], one, max_exp, min_exp); \ - vec_store((void *) &vdst[i], nloe * sizeof(float), tmp); \ - } \ + vec_store((void *) &vdst[i], nloe * sizeof(float), tmp); \ + } \ } while(0) -#define hvx_tanh_loop_body(dst_type, src_type, vec_store) \ - do { \ - dst_type * restrict vdst = (dst_type *) dst; \ - src_type * restrict vsrc = (src_type *) src; \ - \ - const uint32_t epv = 128 / sizeof(float); \ - 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_tanh_f32(vsrc[i]); \ - } \ - if (nloe) { \ - HVX_Vector tmp = hvx_vec_tanh_f32(vsrc[i]); \ +#define hvx_tanh_loop_body(dst_type, src_type, vec_store) \ + do { \ + dst_type * restrict vdst = (dst_type *) dst; \ + src_type * restrict vsrc = (src_type *) src; \ + \ + const uint32_t epv = 128 / sizeof(float); \ + 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_tanh_f32(vsrc[i]); \ + } \ + if (nloe) { \ + HVX_Vector tmp = hvx_vec_tanh_f32(vsrc[i]); \ vec_store((void *) &vdst[i], nloe * sizeof(float), tmp); \ - } \ + } \ } while(0) static inline void hvx_sigmoid_f32_aa(uint8_t * restrict dst, const uint8_t * restrict src, uint32_t n) { diff --git a/ggml/src/ggml-hexagon/htp/im2col-ops.c b/ggml/src/ggml-hexagon/htp/im2col-ops.c index 35fc103df8fe..52bbc37d1b0a 100644 --- a/ggml/src/ggml-hexagon/htp/im2col-ops.c +++ b/ggml/src/ggml-hexagon/htp/im2col-ops.c @@ -3,11 +3,12 @@ #pragma clang diagnostic ignored "-Wunused-but-set-variable" #include <HAP_farf.h> -#include <HAP_perf.h> #include <hexagon_protos.h> #include <hexagon_types.h> #include <string.h> +#include "hex-common.h" + #define GGML_COMMON_DECL_C #include "ggml-common.h" #include "htp-ctx.h" @@ -16,14 +17,19 @@ #include "hex-dma.h" #include "hex-profile.h" #include "htp-vtcm.h" +#include "htp-tensor.h" struct htp_im2col_context { struct htp_ops_context * octx; + uint32_t patch_base; // first patch index assigned to this dev + uint32_t npatches; // number of patches assigned to this dev uint32_t npatches_per_thread; // patches = N*OH*OW (pure-DDR kernel) - uint32_t pe_rows_per_thread; // N*OH rows per worker - uint32_t pe_src_row_bytes; // one output row's source: IC*KH*IW*4, rounded 256 - uint32_t pe_dst_row_bytes; // one output row's dst: OW*patch_stride*2, rounded 256 + uint32_t pe_row_base; // first N*OH row index assigned to this dev (DMA path) + uint32_t pe_nrows; // number of N*OH rows assigned to this dev (DMA path) + uint32_t pe_rows_per_thread; // N*OH rows per worker + uint32_t pe_src_row_bytes; // one output row's source: IC*KH*IW*4, rounded 256 + uint32_t pe_dst_row_bytes; // one output row's dst: OW*patch_stride*2, rounded 256 // Patch-embed DMA path VTCM ping-pong. uint8_t * pe_vtcm_src; // base of the 2x src buffers region @@ -58,33 +64,27 @@ static inline void htp_im2col_vtcm_layout_build(struct htp_im2col_vtcm_layout * struct htp_im2col_context * ictx = (struct htp_im2col_context *) data; \ struct htp_ops_context * octx = ictx->octx; \ struct htp_thread_trace * restrict tr = &octx->ctx->trace[ith]; \ + const struct htp_tensor * restrict src0 = octx->src[0]; \ const struct htp_tensor * restrict src1 = octx->src[1]; \ const struct htp_tensor * restrict dst = octx->dst; \ - const int32_t s0 = octx->op_params[0]; \ - const int32_t s1 = octx->op_params[1]; \ - const int32_t p0 = octx->op_params[2]; \ - const int32_t p1 = octx->op_params[3]; \ - const int32_t d0 = octx->op_params[4]; \ - const int32_t d1 = octx->op_params[5]; \ - const uint32_t N = src1->ne[3]; \ - const uint32_t IC = src1->ne[2]; \ - const uint32_t IH = src1->ne[1]; \ - const uint32_t IW = src1->ne[0]; \ - const uint32_t KH = octx->src[0]->ne[1]; \ - const uint32_t KW = octx->src[0]->ne[0]; \ + const int32_t s0 = octx->op_params[0], s1 = octx->op_params[1]; \ + const int32_t p0 = octx->op_params[2], p1 = octx->op_params[3]; \ + const int32_t d0 = octx->op_params[4], d1 = octx->op_params[5]; \ + const uint32_t N = src1->ne[3], IC = src1->ne[2], IH = src1->ne[1], IW = src1->ne[0]; \ + const uint32_t KH = src0->ne[1], KW = src0->ne[0]; \ const uint32_t OH = dst->ne[2]; \ const uint32_t OW = dst->ne[1]; \ const uint32_t patch_stride = IC * KH * KW; \ const float * restrict src_data = (const float *) src1->data; \ DST_CTYPE * restrict dst_data = (DST_CTYPE *) dst->data; \ - const uint32_t npatches = N * OH * OW; \ - const uint32_t patch_start = ictx->npatches_per_thread * ith; \ - const uint32_t patch_end = MIN(patch_start + ictx->npatches_per_thread, npatches); \ - if (patch_start >= patch_end) { \ + const uint32_t patch_end = ictx->patch_base + ictx->npatches; \ + const uint32_t patch_start = ictx->patch_base + ictx->npatches_per_thread * ith; \ + const uint32_t patch_stop = MIN(patch_start + ictx->npatches_per_thread, patch_end);\ + if (patch_start >= patch_stop) { \ return; \ } \ htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, patch_start); \ - for (uint32_t p = patch_start; p < patch_end; p++) { \ + for (uint32_t p = patch_start; p < patch_stop; p++) { \ const uint32_t iow = p % OW; \ const uint32_t ioh = (p / OW) % OH; \ const uint32_t in = p / (OW * OH); \ @@ -154,10 +154,10 @@ IM2COL_PATCHEMBED_BODY(im2col_patchembed_f32_thread, float, hvx_copy_f32_uu, hvx uint8_t * dst_base = ictx->pe_vtcm_dst + ith * ictx->pe_dst_size_per_thread; \ float * srcb = (float *) src_base; \ DST_CTYPE * dstb = (DST_CTYPE *) dst_base; \ - const uint32_t nrows = N * OH; \ + const uint32_t row_end_max = ictx->pe_row_base + ictx->pe_nrows; \ const uint32_t per_thread = ictx->pe_rows_per_thread; \ - const uint32_t row_start = per_thread * ith; \ - const uint32_t row_end = MIN(row_start + per_thread, nrows); \ + const uint32_t row_start = ictx->pe_row_base + per_thread * ith; \ + const uint32_t row_end = MIN(row_start + per_thread, row_end_max); \ if (row_start >= row_end) \ return; \ for (uint32_t r = row_start; r < row_end; r++) { \ @@ -266,26 +266,55 @@ int op_im2col(struct htp_ops_context * octx) { return HTP_STATUS_NO_SUPPORT; } - const uint32_t N = src1->ne[3]; - const uint32_t OH = dst->ne[2]; - const uint32_t OW = dst->ne[1]; - const uint32_t npatches = N * OH * OW; - const uint32_t n_threads = MIN(octx->n_threads, npatches); + if (octx->flags & HTP_OPFLAGS_SKIP_COMPUTE) { + return HTP_STATUS_OK; + } + + const uint32_t N = src1->ne[3]; + const uint32_t OH = dst->ne[2]; + const uint32_t OW = dst->ne[1]; + const uint32_t total_patches = N * OH * OW; + const uint32_t total_rows = N * OH; + + uint32_t patch_base = 0; + uint32_t npatches = total_patches; + if (octx->ctx->mdev.count > 1) { + const uint32_t patch_size = dst->nb[1]; + const uint32_t patches_per_chunk = (patch_size > 0) ? (HEX_L2_LINE_SIZE / hex_gcd_u32(patch_size, HEX_L2_LINE_SIZE)) : 1; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_patches, htp_tensor_mdev_data_aligned(dst) ? patches_per_chunk : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + patch_base = range.start; + npatches = range.count; + } + + uint32_t row_base = 0; + uint32_t nrows = total_rows; + if (octx->ctx->mdev.count > 1) { + const uint32_t row_size = dst->nb[2]; + const uint32_t rows_per_chunk = (row_size > 0) ? (HEX_L2_LINE_SIZE / hex_gcd_u32(row_size, HEX_L2_LINE_SIZE)) : 1; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_rows, htp_tensor_mdev_data_aligned(dst) ? rows_per_chunk : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_base = range.start; + nrows = range.count; + } - if ((octx->flags & HTP_OPFLAGS_SKIP_COMPUTE) || n_threads == 0) { + if (npatches == 0 && nrows == 0) { return HTP_STATUS_OK; } + const uint32_t n_threads = MIN(octx->n_threads, MAX(npatches, 1)); + struct htp_im2col_context ictx = { 0 }; - ictx.octx = octx; - ictx.npatches_per_thread = (npatches + n_threads - 1) / n_threads; + ictx.octx = octx; + ictx.patch_base = patch_base; + ictx.npatches = npatches; + ictx.npatches_per_thread = (npatches + n_threads - 1) / n_threads; // Clean non-overlapping patch-embed -> DMA kernel (if it fits VTCM); // everything else (padding/dilation/stride edges) -> pure-DDR kernel. - if (im2col_use_patchembed_dma(octx)) { - const uint32_t nrows = N * OH; - const uint32_t pth = MIN(octx->n_threads, nrows); + if (im2col_use_patchembed_dma(octx) && nrows > 0) { + const uint32_t pth = MIN(octx->n_threads, nrows); if (pth > 0 && im2col_patchembed_dma_fits(octx, &ictx, pth)) { + ictx.pe_row_base = row_base; + ictx.pe_nrows = nrows; ictx.pe_rows_per_thread = (nrows + pth - 1) / pth; if (dst->type == HTP_TYPE_F16) { work_queue_run(octx->ctx->work_queue, im2col_patchembed_dma_thread, &ictx, pth); @@ -297,6 +326,10 @@ int op_im2col(struct htp_ops_context * octx) { // else: doesn't fit -> fall through to the pure-DDR kernel below. } + if (npatches == 0) { + return HTP_STATUS_OK; + } + if (dst->type == HTP_TYPE_F16) { work_queue_run(octx->ctx->work_queue, im2col_patchembed_thread, &ictx, n_threads); } else { diff --git a/ggml/src/ggml-hexagon/htp/main.c b/ggml/src/ggml-hexagon/htp/main.c index be54d4fe911c..1d291e16b463 100644 --- a/ggml/src/ggml-hexagon/htp/main.c +++ b/ggml/src/ggml-hexagon/htp/main.c @@ -34,6 +34,7 @@ #include "work-queue.h" #include "hex-profile.h" #include "allreduce-ops.h" +#include "htp-fence.h" #define HMX_QUEUE_CAPACITY 16 #define HMX_QUEUE_STACK_SIZE 16384 @@ -710,22 +711,43 @@ static inline void profile_stop(uint32_t mode, struct profile_data * d) { static int op_fence(struct htp_ops_context * octx) { struct htp_context *ctx = octx->ctx; struct htp_thread_trace * tr = &ctx->trace[0]; - const uint32_t seq = (uint32_t) octx->op_params[0]; + const uint32_t seq = (uint32_t) octx->op_params[0]; + const uint32_t mode = (uint32_t) octx->op_params[1]; htp_trace_event_start(tr, HTP_TRACE_EVT_FENCE, (uint16_t) seq); const struct htp_tensor * sync = octx->src[0]; - atomic_uint * sync_fence = (atomic_uint *) sync->data; + atomic_uint * sync_fence = (atomic_uint *) (uintptr_t) sync->data; + + if (mode == 1) { + htp_flush_dirty_ranges(ctx); + + htp_mdev_group_barrier(octx); + + if (ctx->mdev.idx == 0) { + htp_fence_write(sync_fence, seq, octx->status); + } + htp_trace_event_stop(tr, HTP_TRACE_EVT_FENCE, (uint16_t) seq); + FARF(HIGH, "ggml-hex: sync-signal : fence %p seq 0x%x status %d\n", sync_fence, seq, octx->status); + return octx->status; + } + + int status = HTP_STATUS_OK; uint64_t spins = 0; while (1) { - Q6_dccleaninva_A((void *) sync_fence); - asm volatile ("syncht" : : : "memory"); - uint32_t val = atomic_load(&sync_fence[0]); - if ((int32_t)(val - seq) >= 0) { + uint32_t sync_seq; + uint32_t sync_status; + htp_fence_read(sync_fence, &sync_seq, &sync_status); + if ((int32_t)(sync_seq - seq) >= 0) { + if (sync_status > HTP_STATUS_OK) { + FARF(ERROR, "ggml-hex: sync-wait peer failed with status %u : fence %p seq 0x%x\n", sync_status, sync_fence, seq); + status = sync_status; + } break; } if (++spins > HTP_FENCE_TIMEOUT) { - FARF(ERROR, "ggml-hex: sync-wait TIMEOUT : fence %p spins %llu seq %u\n", sync_fence, spins, seq); + FARF(ERROR, "ggml-hex: sync-wait TIMEOUT : fence %p spins %llu seq 0x%x\n", sync_fence, spins, seq); + status = HTP_STATUS_INTERNAL_ERR; break; } hex_pause(); @@ -733,12 +755,27 @@ static int op_fence(struct htp_ops_context * octx) { htp_trace_event_stop(tr, HTP_TRACE_EVT_FENCE, (uint16_t) seq); - FARF(HIGH, "ggml-hex: sync-done : fence %p spins %llu seq %u\n", sync_fence, spins, seq); + FARF(HIGH, "ggml-hex: sync-done : fence %p spins %llu seq 0x%x\n", sync_fence, spins, seq); + return status; +} + +static int op_mdev_group(struct htp_ops_context * octx) { + struct htp_context * ctx = octx->ctx; + const struct htp_tensor * sync = octx->src[0]; + ctx->mdev.idx = (uint16_t) octx->op_params[0]; + ctx->mdev.count = (uint16_t) sync->ne[1]; + if (ctx->mdev.count > 1) { + ctx->mdev.count_div = init_fastdiv_values(ctx->mdev.count); + ctx->mdev.fence_base = (uint8_t *) sync->data; + } return HTP_STATUS_OK; } static int execute_op(struct htp_ops_context * octx) { switch (octx->op) { + case HTP_OP_MDEV_GROUP: + return op_mdev_group(octx); + case HTP_OP_FENCE: return op_fence(octx); @@ -812,6 +849,7 @@ static int execute_op(struct htp_ops_context * octx) { return op_sum_rows(octx); case HTP_OP_CPY: + case HTP_OP_CPY_FENCE: return op_cpy(octx); case HTP_OP_REPEAT: @@ -855,7 +893,7 @@ static int execute_op(struct htp_ops_context * octx) { } FARF(ERROR, "Unknown Op %u", octx->op); - return -1; + return HTP_STATUS_NO_SUPPORT; } static inline bool reuse_buf(struct htp_context *ctx, uint32_t *m_reuse, struct htp_buf_desc *b) { @@ -984,11 +1022,19 @@ static void prep_tensors(struct htp_context *ctx, struct htp_buf_desc *bufs, str } } -static int proc_op_req(struct htp_ops_context * octx, struct htp_tensor *tens, uint32_t idx, struct htp_op_desc * op) { - memcpy(octx->op_params, op->params, sizeof(octx->op_params)); +static void mdev_group_init(struct htp_context * ctx, const struct htp_opbatch_req * req) { + memset(&ctx->mdev, 0, sizeof(ctx->mdev)); + ctx->mdev.fence_seq = (uint32_t)((req->seq & 0xfffff) << 12); +} + +static int proc_op_req(struct htp_ops_context * octx, struct htp_buf_desc * bufs, uint32_t n_bufs, + struct htp_tensor * tens, uint32_t idx, struct htp_op_desc * op) { + memcpy(octx->op_params, op->params, sizeof(octx->op_params)); memcpy(octx->kernel_params, op->kernel_params, sizeof(octx->kernel_params)); - octx->flags = op->flags; - octx->op = op->opcode; + octx->flags = op->flags; + octx->op = op->opcode; + octx->n_threads = octx->ctx->n_threads; + octx->n_threads_div = octx->ctx->n_threads_div; FARF(HIGH, "proc-op #%u: opcode %u flags 0x%x", idx, octx->op, octx->flags); @@ -1027,9 +1073,13 @@ static int proc_op_req(struct htp_ops_context * octx, struct htp_tensor *tens, u dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3]); } + htp_tensor_dirty_all(octx->ctx, octx->dsts, HTP_OP_MAX_OUTPUTS); + + htp_mdev_group_barrier(octx); + int status = execute_op(octx); - htp_tensor_dirty_all(octx->ctx, octx->dsts, HTP_OP_MAX_OUTPUTS); + htp_ops_context_set_status(octx, status); octx->src0_spad.src = NULL; octx->src1_spad.src = NULL; @@ -1037,7 +1087,7 @@ static int proc_op_req(struct htp_ops_context * octx, struct htp_tensor *tens, u octx->src3_spad.src = NULL; octx->dst_spad.src = NULL; - return status; + return octx->status; } static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_req * req, const struct dspqueue_buffer * dbuf) { @@ -1059,7 +1109,7 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r return; } - FARF(HIGH, "processing opbatch #%u: n-bufs %u n-tensors %u n-ops %u n-traces %u : m-size %u b-size %u t-size %u o-size %u", req->id, + FARF(HIGH, "processing opbatch #%llu: n-bufs %u n-tensors %u n-ops %u n-traces %u : m-size %u b-size %u t-size %u o-size %u", (unsigned long long) req->seq, n_bufs, n_tens, n_ops, req->n_traces, dbuf->size, b_size, t_size, o_size); // Setup descriptor pointers @@ -1096,8 +1146,11 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r struct htp_ops_context *octx = &ctx->octx; memset(octx, 0, sizeof(*octx)); - octx->n_threads = ctx->n_threads; - octx->ctx = ctx; + octx->n_threads = ctx->n_threads; + octx->n_threads_div = ctx->n_threads_div; + octx->ctx = ctx; + + mdev_group_init(ctx, req); work_queue_wakeup(ctx->work_queue); if (ctx->hmx_queue) { @@ -1105,15 +1158,18 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r } int op_status = HTP_STATUS_OK; - for (uint32_t i = 0; i < n_ops && op_status == HTP_STATUS_OK; i++) { + octx->status = HTP_STATUS_OK; + for (uint32_t i = 0; i < n_ops; i++) { struct profile_data prof; profile_start(ctx->profiler, &prof); - op_status = proc_op_req(octx, tens, i, &ops[i]); + op_status = proc_op_req(octx, bufs, n_bufs, tens, i, &ops[i]); profile_stop(ctx->profiler, &prof); + htp_ops_context_set_status(octx, op_status); + if (ctx->profiler) { pds[i].opcode = ops[i].opcode; pds[i].usecs = prof.usecs; @@ -1136,19 +1192,20 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r qurt_mem_cache_clean((qurt_addr_t) 0, 0, QURT_MEM_CACHE_FLUSH_INVALIDATE_ALL, QURT_MEM_DCACHE); htp_trace_event_stop(&ctx->trace[0], HTP_TRACE_EVT_L2FLUSH, 0); + htp_mdev_group_barrier(octx); + profile_stop(HTP_PROF_BASIC, &batch_prof); struct htp_opbatch_rsp rsp; memset(&rsp, 0, sizeof(rsp)); - rsp.id = req->id; - rsp.status = op_status; + rsp.seq = req->seq; + rsp.status = octx->status; rsp.n_bufs = n_bufs; rsp.n_tensors = n_tens; rsp.n_ops = n_ops; rsp.usecs = batch_prof.usecs; rsp.cycles_start = batch_prof.cycles_start; rsp.cycles_stop = batch_prof.cycles_stop; - rsp.seq = req->seq; if (ctx->profiler == HTP_PROF_TRACE) { for (int t = 0; t <= HTP_MAX_NTHREADS; t++) { diff --git a/ggml/src/ggml-hexagon/htp/matmul-ops.c b/ggml/src/ggml-hexagon/htp/matmul-ops.c index 2a87dd19ee8c..1b597dcd9f20 100644 --- a/ggml/src/ggml-hexagon/htp/matmul-ops.c +++ b/ggml/src/ggml-hexagon/htp/matmul-ops.c @@ -21,6 +21,7 @@ #include "ggml-common.h" #include "htp-ctx.h" #include "htp-ops.h" +#include "htp-tensor.h" #include "matmul-ops.h" #include "htp-vtcm.h" @@ -89,6 +90,8 @@ struct htp_mm_context { // Precomputed values uint32_t src0_nrows_per_thread; + uint32_t src0_row_start; + uint32_t src0_row_end; uint32_t src0_row_size_padded; uint32_t src1_nrows; @@ -135,6 +138,23 @@ struct htp_mm_context { uint32_t vtcm_dst_size_per_thread; }; +static int htp_mm_init_context( + struct htp_ops_context * octx, + const struct htp_mm_kernel_params * kparams +) { + if (!htp_ops_context_set_n_threads(octx, (uint32_t) kparams->n_threads)) { + return HTP_STATUS_INVAL_PARAMS; + } + + if (kparams->n_hmx) { + if (kparams->n_act_threads <= 0 || kparams->n_act_threads > (int32_t) octx->n_threads) { + return HTP_STATUS_INVAL_PARAMS; + } + } + + return HTP_STATUS_OK; +} + // vdelta control to expand first 32 e8m0 values into 32 uint32 elements static const uint8_t __attribute__((aligned(128))) expand_x32_e8m0[128] = { 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x02, 0x00, 0x08, 0x08, 0x01, 0x02, 0x00, 0x04, 0x04, 0x00, 0x00, @@ -238,22 +258,24 @@ static void hvx_mm_4d(unsigned int nth, unsigned int ith, void * data) { // This is the size of the rest of the dimensions of the result const uint32_t nr1 = ne1 * ne2 * ne3; + const uint32_t src0_nrows = mmctx->src0_row_end - mmctx->src0_row_start; + // distribute the thread work across the inner or outer loop based on which one is larger uint32_t dr0, dr1, ith0, ith1; if (nr0 > nr1) { - dr0 = fastdiv(nr0 + nth - 1, &octx->ctx->n_threads_div); + dr0 = fastdiv(src0_nrows + nth - 1, &octx->n_threads_div); dr1 = nr1; ith0 = ith; ith1 = 0; } else { - dr0 = nr0; - dr1 = fastdiv(nr1 + nth - 1, &octx->ctx->n_threads_div); + dr0 = src0_nrows; + dr1 = fastdiv(nr1 + nth - 1, &octx->n_threads_div); ith0 = 0; ith1 = ith; } - const uint32_t ir0_start = dr0 * ith0; - const uint32_t ir0_end = MIN(ir0_start + dr0, nr0); + const uint32_t ir0_start = mmctx->src0_row_start + dr0 * ith0; + const uint32_t ir0_end = MIN(ir0_start + dr0, mmctx->src0_row_end); const uint32_t ir1_start = dr1 * ith1; const uint32_t ir1_end = MIN(ir1_start + dr1, nr1); @@ -312,11 +334,11 @@ static void hvx_mm_4d(unsigned int nth, unsigned int ith, void * data) { static void hvx_mm_2d_repacked_##SUFFIX(unsigned int nth, unsigned int ith, void * data) { \ htp_matmul_preamble; \ \ - const uint32_t src0_nrows = ne01 * ne02 * ne03; \ + const uint32_t src0_nrows = mmctx->src0_row_end - mmctx->src0_row_start; \ const uint32_t src1_nrows = ne11 * ne12 * ne13; \ \ - const uint32_t src0_start_row = src0_nrows_per_thread * ith; \ - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); \ + const uint32_t src0_start_row = mmctx->src0_row_start + src0_nrows_per_thread * ith; \ + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, mmctx->src0_row_end); \ \ struct htp_thread_trace * tr = &octx->ctx->trace[ith]; \ \ @@ -414,10 +436,10 @@ static void hvx_mm_2d_repacked_##SUFFIX(unsigned int nth, unsigned int ith, void static void hvx_mv_2d_repacked_##SUFFIX(unsigned int nth, unsigned int ith, void * data) { \ htp_matmul_preamble; \ \ - const uint32_t src0_nrows = ne01; \ + const uint32_t src0_nrows = mmctx->src0_row_end - mmctx->src0_row_start; \ \ - const uint32_t src0_start_row = src0_nrows_per_thread * ith; \ - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); \ + const uint32_t src0_start_row = mmctx->src0_row_start + src0_nrows_per_thread * ith; \ + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, mmctx->src0_row_end); \ \ struct htp_thread_trace * tr = &octx->ctx->trace[ith]; \ \ @@ -549,12 +571,22 @@ static void hvx_mm_nx_2d_repacked_##SUFFIX(unsigned int nth, unsigned int ith, v uint32_t n_k_tiles_w = ne00 / 32; \ uint32_t tile_row_stride = n_k_tiles_w * tile_size; \ \ - const uint32_t src0_nrows = ne01 * src_w->ne[2] * src_w->ne[3]; \ - uint32_t src0_nrows_per_thread = fastdiv(src0_nrows + nth - 1, &octx->ctx->n_threads_div); \ + uint32_t src0_start_row = 0; \ + uint32_t src0_end_row = ne01; \ + if (octx->ctx->mdev.count > 1) { \ + const bool can_split = htp_tensor_can_row_partition(dst, sizeof(float)); \ + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(ne01, can_split ? 32 : 0, \ + octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); \ + src0_start_row = range.start; \ + src0_end_row = range.start + range.count; \ + } \ + \ + const uint32_t nrows = src0_end_row - src0_start_row; \ + uint32_t src0_nrows_per_thread = fastdiv(nrows + nth - 1, &octx->n_threads_div); \ src0_nrows_per_thread = hex_round_up(src0_nrows_per_thread, 32); \ \ - const uint32_t start_row = src0_nrows_per_thread * ith; \ - const uint32_t end_row = MIN(start_row + src0_nrows_per_thread, src0_nrows); \ + const uint32_t start_row = src0_start_row + src0_nrows_per_thread * ith; \ + const uint32_t end_row = MIN(start_row + src0_nrows_per_thread, src0_end_row); \ if (start_row >= end_row) continue; \ \ uint32_t ct_start = start_row / 32; \ @@ -735,11 +767,11 @@ static void hvx_mm_2d(unsigned int nth, unsigned int ith, void * data) { assert(n_prefetch >= 2 && n_prefetch <= HTP_MM_MAX_PREFETCH && (n_prefetch & (n_prefetch - 1)) == 0); const uint32_t prefetch_mask = n_prefetch - 1; - const uint32_t src0_nrows = ne01 * ne02 * ne03; // src0 rows - const uint32_t src1_nrows = ne11 * ne12 * ne13; // src1 rows + const uint32_t src0_nrows = mmctx->src0_row_end - mmctx->src0_row_start; // src0 rows + const uint32_t src1_nrows = ne11 * ne12 * ne13; // src1 rows - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_start_row = mmctx->src0_row_start + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, mmctx->src0_row_end); const uint32_t src0_end_row_x2 = src0_start_row + ((src0_end_row - src0_start_row) & ~1U); struct htp_thread_trace * tr = &octx->ctx->trace[ith]; @@ -781,7 +813,7 @@ static void hvx_mm_2d(unsigned int nth, unsigned int ith, void * data) { const uint8_t * ss0 = dma_queue_pop(dma_queue).dst; htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, ir0); - // Process src1 columns in pairs (2×2 tiling) + // Process src1 columns in pairs (2x2 tiling) uint32_t ir1 = 0; for (; ir1 + 1 < src1_nrows; ir1 += 2) { const uint8_t * restrict src1_col0 = (const uint8_t *) (src1_data + (ir1+0) * src1_stride); @@ -791,7 +823,7 @@ static void hvx_mm_2d(unsigned int nth, unsigned int ith, void * data) { mmctx->vec_dot_2x2(ne00, &dst_row0[ir0], &dst_row1[ir0], ss0, ss0 + src0_stride, src1_col0, src1_col1); } - // Handle remaining src1 rows (fallback to 2×1) + // Handle remaining src1 rows (fallback to 2x1) for (; ir1 < src1_nrows; ++ir1) { const uint8_t * restrict src1_col = (const uint8_t *) (src1_data + ir1 * src1_stride); float * restrict dst_row = (float *) (dst->data + (ir1 * dst_row_size)); @@ -833,10 +865,10 @@ static void hvx_mm_2d(unsigned int nth, unsigned int ith, void * data) { static void hvx_mv_2d(unsigned int nth, unsigned int ith, void * data) { htp_matmul_preamble; - const uint32_t src0_nrows = ne01; + const uint32_t src0_nrows = mmctx->src0_row_end - mmctx->src0_row_start; - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_start_row = mmctx->src0_row_start + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, mmctx->src0_row_end); struct htp_thread_trace * tr = &octx->ctx->trace[ith]; @@ -943,13 +975,10 @@ static void hvx_mm_id(unsigned int nth, unsigned int ith, void * data) { const struct htp_tensor * restrict ids = octx->src[2]; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - - const uint32_t src0_nrows = ne01; // src0 rows per expert + const uint32_t src0_nrows = mmctx->src0_row_end - mmctx->src0_row_start; // src0 rows per expert const uint32_t src1_nrows = ne11; - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_start_row = mmctx->src0_row_start + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, mmctx->src0_row_end); hvx_mm_run_quant_task(mmctx, ith); @@ -1036,9 +1065,9 @@ static void hvx_mv_id(unsigned int nth, unsigned int ith, void * data) { const struct htp_tensor * restrict ids = octx->src[2]; - const uint32_t src0_nrows = ne01; // src0 rows per expert - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_nrows = mmctx->src0_row_end - mmctx->src0_row_start; // src0 rows per expert + const uint32_t src0_start_row = mmctx->src0_row_start + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, mmctx->src0_row_end); hvx_mm_run_quant_task(mmctx, ith); @@ -1143,12 +1172,22 @@ static void hvx_mv_id_nx(unsigned int nth, unsigned int ith, void * data) { const struct htp_tensor * restrict dst = octx->dsts[p]; if (!src_w || !dst) continue; - const uint32_t src0_nrows = src_w->ne[1]; - uint32_t src0_nrows_per_thread = fastdiv(src0_nrows + nth - 1, &octx->ctx->n_threads_div); + const uint32_t ne01 = src_w->ne[1]; + uint32_t start_row = 0; + uint32_t end_row = ne01; + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_can_row_partition(dst, sizeof(float)); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(ne01, can_split ? 32 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + start_row = range.start; + end_row = range.start + range.count; + } + + const uint32_t nrows = end_row - start_row; + uint32_t src0_nrows_per_thread = fastdiv(nrows + nth - 1, &octx->n_threads_div); src0_nrows_per_thread = hex_round_up(src0_nrows_per_thread, 32); - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_start_row = start_row + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, end_row); if (src0_start_row >= src0_end_row) continue; const uint8_t * restrict src0_row = (const uint8_t *) src_w->data + eid * src_w->nb[2]; @@ -1227,12 +1266,22 @@ static void hvx_mm_id_nx(unsigned int nth, unsigned int ith, void * data) { const struct htp_tensor * restrict dst = octx->dsts[p]; if (!src_w || !dst) continue; - const uint32_t src0_nrows = src_w->ne[1]; - uint32_t src0_nrows_per_thread = fastdiv(src0_nrows + nth - 1, &octx->ctx->n_threads_div); + const uint32_t ne01 = src_w->ne[1]; + uint32_t start_row = 0; + uint32_t end_row = ne01; + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_can_row_partition(dst, sizeof(float)); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(ne01, can_split ? 32 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + start_row = range.start; + end_row = range.start + range.count; + } + + const uint32_t nrows = end_row - start_row; + uint32_t src0_nrows_per_thread = fastdiv(nrows + nth - 1, &octx->n_threads_div); src0_nrows_per_thread = hex_round_up(src0_nrows_per_thread, 32); - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_start_row = start_row + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, end_row); if (src0_start_row >= src0_end_row) continue; const uint8_t * src0_row = (const uint8_t *) src_w->data + cur_a * src_w->nb[2]; @@ -1323,15 +1372,33 @@ static int hvx_mm_matmul(struct htp_ops_context * octx) { const struct htp_mm_kernel_params * kparams = (const struct htp_mm_kernel_params *) octx->kernel_params; - const uint32_t src0_nrows = ne01 * ne02 * ne03; + const uint32_t src0_nrows = ne01; const uint32_t src1_nrows = ne11 * ne12 * ne13; + uint32_t src0_row_start = 0; + uint32_t src0_row_end = src0_nrows; + + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_can_row_partition(dst, sizeof(float)); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(src0_nrows, can_split ? 32 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + src0_row_start = range.start; + src0_row_end = range.start + range.count; + } + + if (src0_row_start >= src0_row_end) { + return HTP_STATUS_OK; + } + + const uint32_t nrows = src0_row_end - src0_row_start; + mmctx->src0_row_start = src0_row_start; + mmctx->src0_row_end = src0_row_end; + bool is_repacked = (src0->type == HTP_TYPE_Q4_0 || src0->type == HTP_TYPE_Q4_1 || src0->type == HTP_TYPE_Q8_0 || src0->type == HTP_TYPE_IQ4_NL || src0->type == HTP_TYPE_MXFP4); // Compute src0_nrows_per_thread - mmctx->src0_nrows_per_thread = fastdiv(src0_nrows + octx->n_threads - 1, &octx->ctx->n_threads_div); + mmctx->src0_nrows_per_thread = fastdiv(nrows + octx->n_threads - 1, &octx->n_threads_div); if (is_repacked) { mmctx->src0_nrows_per_thread = hex_round_up(mmctx->src0_nrows_per_thread, 32); } else { @@ -1503,13 +1570,13 @@ static int hvx_mm_matmul(struct htp_ops_context * octx) { kparams->kernel_type == HTP_MM_KERNEL_HVX_QUANT_BLOCK) { mmctx->vtcm_src1_size_per_thread = L.src1_bytes; } else { - mmctx->vtcm_src1_size_per_thread = fastdiv(L.src1_bytes, &octx->ctx->n_threads_div); + mmctx->vtcm_src1_size_per_thread = fastdiv(L.src1_bytes, &octx->n_threads_div); } - mmctx->vtcm_src0_size_per_thread = fastdiv(L.src0_bytes, &octx->ctx->n_threads_div); - mmctx->vtcm_dst_size_per_thread = fastdiv(L.dst_bytes, &octx->ctx->n_threads_div); + mmctx->vtcm_src0_size_per_thread = fastdiv(L.src0_bytes, &octx->n_threads_div); + mmctx->vtcm_dst_size_per_thread = fastdiv(L.dst_bytes, &octx->n_threads_div); - size_t vtcm_size = kparams->vtcm_size > 0 ? (size_t)kparams->vtcm_size : L.total_bytes; + const size_t vtcm_size = L.total_bytes; FARF(HIGH, "matmul-%s : src0-vtcm-size %zu src1-vtcm-size %zu dst-vtcm-size %zu (%zu)\n", mmctx->type, L.src0_bytes, L.src1_bytes, L.dst_bytes, vtcm_size); @@ -1583,13 +1650,21 @@ static void hvx_mm_nx_2d(unsigned int nth, unsigned int ith, void * data) { const uint32_t ne00 = src_w->ne[0]; const uint32_t ne01 = src_w->ne[1]; - const uint32_t src0_nrows = ne01 * src_w->ne[2] * src_w->ne[3]; + uint32_t start_row = 0; + uint32_t end_row = ne01; + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_can_row_partition(dst, sizeof(float)); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(ne01, can_split ? 32 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + start_row = range.start; + end_row = range.start + range.count; + } - uint32_t src0_nrows_per_thread = fastdiv(src0_nrows + nth - 1, &octx->ctx->n_threads_div); + const uint32_t nrows = end_row - start_row; + uint32_t src0_nrows_per_thread = fastdiv(nrows + nth - 1, &octx->n_threads_div); src0_nrows_per_thread += (src0_nrows_per_thread & 1); - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_start_row = start_row + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, end_row); const uint32_t src0_end_row_x2 = src0_start_row + ((src0_end_row - src0_start_row) & ~1U); if (src0_start_row >= src0_end_row) continue; @@ -2638,10 +2713,6 @@ static int hmx_mm_nx_2d_f32(struct htp_ops_context * octx, const struct htp_mm_k const struct htp_tensor * restrict src0 = octx->src[0]; const struct htp_tensor * restrict act = octx->src[n_weights]; - if (!src0 || !act) { - return HTP_STATUS_INVAL_PARAMS; - } - const int weight_type = (int) src0->type; const int k = (int) act->ne[0]; const int k_valid = (int) act->ne[0]; @@ -2714,16 +2785,31 @@ static int hmx_mm_nx_2d_f32(struct htp_ops_context * octx, const struct htp_mm_k hmx_init_column_scales(vtcm_scales, Q6_V_vsplat_R(0x3c00)); // scale: 1.0, bias: 0.0 in FP16 - FARF(HIGH, "hmx-mm-nx-2d: n_weights %u m %d k %d wtype %d mc %d nc %d vtcm %zu/%zu", - n_weights, m, k, weight_type, m_chunk_n_rows, n_chunk_n_cols, L.total_bytes, vtcm_budget); + int m_start = 0; + int m_rows = m; + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_can_row_partition(octx->dsts[0], sizeof(float)); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition((uint32_t) m, can_split ? 1 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + m_start = (int) range.start; + m_rows = (int) range.count; + } + + if (m_rows == 0) { + return HTP_STATUS_OK; + } + + FARF(HIGH, "hmx-mm-nx-2d: n_weights %u m %d (%d..%d) k %d wtype %d mc %d nc %d vtcm %zu/%zu", + n_weights, m, m_start, m_start + m_rows, k, weight_type, m_chunk_n_rows, n_chunk_n_cols, L.total_bytes, vtcm_budget); htp_trace_event_stop(tr, HTP_TRACE_EVT_INIT, 0); + const size_t mr_end = (size_t)(m_start + m_rows); + if (pipeline) { hmx_matmul_job_t job_slots[2]; - for (size_t mr = 0; mr < (size_t) m; mr += m_chunk_n_rows) { - const size_t n_rows = hex_smin(m - mr, m_chunk_n_rows); + for (size_t mr = (size_t) m_start; mr < mr_end; mr += m_chunk_n_rows) { + const size_t n_rows = hex_smin(mr_end - mr, m_chunk_n_rows); void *vtcm_weight_bufs[2] = { vtcm_scratch0, vtcm_scratch1 }; void *vtcm_output_bufs[2] = { vtcm_output, vtcm_scratch2 }; @@ -2822,8 +2908,8 @@ static int hmx_mm_nx_2d_f32(struct htp_ops_context * octx, const struct htp_mm_k } } else { hmx_matmul_job_t job; - for (size_t mr = 0; mr < (size_t) m; mr += m_chunk_n_rows) { - const size_t n_rows = hex_smin(m - mr, m_chunk_n_rows); + for (size_t mr = (size_t) m_start; mr < mr_end; mr += m_chunk_n_rows) { + const size_t n_rows = hex_smin(mr_end - mr, m_chunk_n_rows); struct activation_transfer_params act_params = { .ctx = ctx, @@ -3095,7 +3181,7 @@ static int hmx_mm_f16_f32_batched(struct htp_context *ctx, const hmx_mm_f16_f32_ int chunk_dst_cols = params->n - (int)nc; if (chunk_dst_cols > 0) { transfer_output_chunk_threaded(ctx, output, src2_chunk, vtcm_output, (int) n_rows, (int) n_cols, - params->dst_stride, params->src2_stride, chunk_dst_cols, ctx->n_threads); + params->dst_stride, params->src2_stride, chunk_dst_cols, n_threads); } } } @@ -3216,7 +3302,10 @@ static int hmx_mm_id_2d_f32(struct htp_context *ctx, int weight_type, const struct mmid_row_mapping *matrix_rows, int cur_a, - int mapping_stride) { + int mapping_stride, + int m_start, + int m_end, + int n_threads) { struct htp_thread_trace * tr = &ctx->trace[0]; htp_trace_event_start(tr, HTP_TRACE_EVT_INIT, 0); @@ -3247,7 +3336,6 @@ static int hmx_mm_id_2d_f32(struct htp_context *ctx, const int n_k_tiles = k / HTP_MM_HMX_TILE_N_COLS; const struct fastdiv_values n_k_tiles_div = init_fastdiv_values(n_k_tiles); - const int n_threads = ctx->n_threads; const bool is_quant = (weight_type != HTP_TYPE_F16 && weight_type != HTP_TYPE_F32); const size_t vec_dot_size = k * sizeof(__fp16); @@ -3303,8 +3391,8 @@ static int hmx_mm_id_2d_f32(struct htp_context *ctx, hmx_matmul_job_t job; - for (size_t mr = 0; mr < (size_t) m_padded; mr += m_chunk_n_rows) { - const size_t n_rows = hex_smin(m_padded - mr, m_chunk_n_rows); + for (size_t mr = (size_t) m_start; mr < (size_t) m_end; mr += m_chunk_n_rows) { + const size_t n_rows = hex_smin((size_t) m_end - mr, m_chunk_n_rows); const size_t n_row_tiles = hmx_ceil_div(n_rows, HTP_MM_HMX_TILE_N_ROWS); transfer_activation_chunk_gathered_threaded( @@ -3368,31 +3456,48 @@ static int hmx_mm_op_matmul(struct htp_ops_context * octx, const struct htp_mm_k const int act_stride = (int)(src1->nb[1] / sizeof(float)); const int wgt_stride = (int)(src0->nb[1] / sizeof(__fp16)); + int m_start = 0; + int m_rows = m_total; + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_can_row_partition(dst, sizeof(float)); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition((uint32_t) m_total, can_split ? 1 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + m_start = (int) range.start; + m_rows = (int) range.count; + } + + if (m_rows == 0) { + return HTP_STATUS_OK; + } + const float * src2_ptr = NULL; uint32_t src2_stride = 0; size_t src2_nb2 = 0; size_t src2_nb3 = 0; if (src2) { - src2_ptr = (const float *) src2->data; src2_stride = (src2->ne[1] == 1) ? 0 : (uint32_t) (src2->nb[1] / sizeof(float)); + src2_ptr = (const float *) src2->data + m_start * src2_stride; src2_nb2 = (src2->ne[2] == 1) ? 0 : src2->nb[2]; src2_nb3 = (src2->ne[3] == 1) ? 0 : src2->nb[3]; } + const int dst_stride = (int)(dst->nb[1] / sizeof(float)); + float * dst_ptr = (float *) dst->data + m_start * dst_stride; + const float * act_ptr = (const float *) src1->data + m_start * act_stride; + int ret = -1; - const int n_threads = MIN(kparams->n_threads, (int) octx->n_threads); + const int n_threads = kparams->n_threads; if (kparams->kernel_type == HTP_MM_KERNEL_HMX_F16_BATCHED) { hmx_mm_f16_f32_batched_params_t batch_params = { - .dst = (float *) dst->data, + .dst = dst_ptr, .src2 = src2_ptr, - .activation = (float *) src1->data, + .activation = act_ptr, .weight = (const __fp16 *) src0->data, - .m = m_total, + .m = m_rows, .k = k, .n = n, .act_stride = act_stride, .weight_stride = wgt_stride, - .dst_stride = (int) (dst->nb[1] / sizeof(float)), + .dst_stride = dst_stride, .src2_stride = src2_stride, .ne02 = ne02, .ne03 = ne03, @@ -3420,9 +3525,9 @@ static int hmx_mm_op_matmul(struct htp_ops_context * octx, const struct htp_mm_k kparams->vtcm_size); } else { ret = hmx_mm_2d_f32( - octx->ctx, (float*) dst->data, src2_ptr, (float*) src1->data, (const uint8_t *) src0->data, - m_total, k, n, act_stride, (int) src0->nb[1], (int) src0->type, (int) src1->ne[0], - (int)(dst->nb[1] / sizeof(float)), src2_stride, (int)dst->ne[0], + octx->ctx, dst_ptr, src2_ptr, act_ptr, (const uint8_t *) src0->data, + m_rows, k, n, act_stride, (int) src0->nb[1], (int) src0->type, (int) src1->ne[0], + dst_stride, src2_stride, (int)dst->ne[0], kparams->m_chunk, kparams->n_chunk, kparams->pipeline, n_threads, kparams->n_act_threads, &kparams->div_n_act_threads, @@ -3441,6 +3546,11 @@ static int hmx_mm_op_matmul(struct htp_ops_context * octx, const struct htp_mm_k int op_matmul(struct htp_ops_context * octx) { const struct htp_mm_kernel_params * kparams = (const struct htp_mm_kernel_params *) octx->kernel_params; + const int status = htp_mm_init_context(octx, kparams); + if (status != HTP_STATUS_OK) { + return status; + } + if (kparams->n_hmx) { return hmx_mm_op_matmul(octx, kparams); } @@ -3463,6 +3573,16 @@ static int hmx_mm_op_matmul_id( const int32_t cne1 = matrix_row_counts[cur_a]; if (cne1 == 0) continue; + const int m_padded = hex_align_up(cne1, 32); + int m_start = 0, m_end = m_padded; + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_mdev_data_aligned(dst) && (uint32_t) cne1 >= octx->ctx->mdev.count; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition((uint32_t) m_padded, can_split ? 1 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + m_start = (int) range.start; + m_end = (int) (range.start + range.count); + } + if (m_start >= m_end) continue; + int ret = hmx_mm_id_2d_f32(octx->ctx, (float*) dst->data, (float*) src1->data, (const uint8_t *) src0->data + cur_a * nb02, cne1, ne00, ne01, @@ -3471,7 +3591,8 @@ static int hmx_mm_op_matmul_id( nb11, nb12, nb1, nb2, (int) src0->nb[1], (int) src0->type, - matrix_rows, cur_a, mmctx->mapping_stride); + matrix_rows, cur_a, mmctx->mapping_stride, + m_start, m_end, (int) octx->n_threads); if (ret != 0) { FARF(ERROR, "HMX matmul failed for expert %u, error %d\n", cur_a, ret); return HTP_STATUS_NO_SUPPORT; @@ -3524,7 +3645,7 @@ static int hvx_mm_matmul_id( htp_mm_hvx_vtcm_layout_build(&L, kparams->kernel_type, src0->type, ne10, src1_nrows, octx->n_threads, 0, src0_row_size, src1_row_size, 0, kparams->n_prefetch, true, false); - size_t vtcm_size = kparams->vtcm_size > 0 ? (size_t)kparams->vtcm_size : L.total_bytes; + const size_t vtcm_size = L.total_bytes; FARF(HIGH, "matmul-id-%s : src0-spad-size %zu src1-spad-size %zu src2-spad-size 0 dst-spad-size %zu (%zu)\n", mmctx->type, L.src0_bytes, L.src1_bytes, L.dst_bytes, vtcm_size); @@ -3554,10 +3675,10 @@ static int hvx_mm_matmul_id( mmctx->vtcm_src0_stride = src0_row_size_padded; mmctx->vtcm_src1_stride = src1_row_size; - mmctx->vtcm_src0_size_per_thread = fastdiv(L.src0_bytes, &octx->ctx->n_threads_div); + mmctx->vtcm_src0_size_per_thread = fastdiv(L.src0_bytes, &octx->n_threads_div); mmctx->vtcm_src1_size_per_thread = L.src1_bytes; mmctx->vtcm_src2_size_per_thread = 0; - mmctx->vtcm_dst_size_per_thread = fastdiv(L.dst_bytes, &octx->ctx->n_threads_div); + mmctx->vtcm_dst_size_per_thread = fastdiv(L.dst_bytes, &octx->n_threads_div); mmctx->n_quant_rows_per_thread = (src1_nrows + n_quant_tasks - 1) / n_quant_tasks; mmctx->quant_task_func = quant_task_func; @@ -3587,6 +3708,20 @@ static int hmx_mm_op_matmul_id_nx( const int32_t cne1 = matrix_row_counts[cur_a]; if (cne1 == 0) continue; + const int m_padded = hex_align_up(cne1, 32); + int m_start = 0, m_end = m_padded; + if (octx->ctx->mdev.count > 1) { + bool can_split = (uint32_t) cne1 >= octx->ctx->mdev.count; + for (uint32_t p = 0; p < n_weights && can_split; ++p) { + const struct htp_tensor * restrict dst = octx->dsts[p]; + can_split = !dst || htp_tensor_mdev_data_aligned(dst); + } + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition((uint32_t) m_padded, can_split ? 1 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + m_start = (int) range.start; + m_end = (int) (range.start + range.count); + } + if (m_start >= m_end) continue; + for (uint32_t p = 0; p < n_weights; ++p) { const struct htp_tensor * restrict src_w = octx->src[p]; const struct htp_tensor * restrict dst = octx->dsts[p]; @@ -3600,7 +3735,8 @@ static int hmx_mm_op_matmul_id_nx( act->nb[1], act->nb[2], dst->nb[1], dst->nb[2], (int) src_w->nb[1], (int) src_w->type, - matrix_rows, cur_a, mmctx->mapping_stride); + matrix_rows, cur_a, mmctx->mapping_stride, + m_start, m_end, (int) octx->n_threads); if (ret != 0) { FARF(ERROR, "HMX matmul ID NX failed for expert %u weight %u, error %d\n", cur_a, p, ret); return HTP_STATUS_NO_SUPPORT; @@ -3656,7 +3792,7 @@ static int hvx_mm_matmul_id_nx( htp_mm_hvx_vtcm_layout_build(&L, kparams->kernel_type, src0->type, act->ne[0], src1_nrows, octx->n_threads, 0, src0_row_size, src1_row_size, 0, kparams->n_prefetch, true, false); - size_t vtcm_size = kparams->vtcm_size > 0 ? (size_t)kparams->vtcm_size : L.total_bytes; + const size_t vtcm_size = L.total_bytes; if (octx->ctx->vtcm_size < vtcm_size) { FARF(ERROR, "matmul-id-nx: current VTCM reservation %zu is too small, needed %zu\n", @@ -3678,9 +3814,9 @@ static int hvx_mm_matmul_id_nx( mmctx->vtcm_src0_stride = 0; mmctx->vtcm_src1_stride = src1_row_size; - mmctx->vtcm_src0_size_per_thread = fastdiv(L.src0_bytes, &octx->ctx->n_threads_div); + mmctx->vtcm_src0_size_per_thread = fastdiv(L.src0_bytes, &octx->n_threads_div); mmctx->vtcm_src1_size_per_thread = L.src1_bytes; - mmctx->vtcm_dst_size_per_thread = fastdiv(L.dst_bytes, &octx->ctx->n_threads_div); + mmctx->vtcm_dst_size_per_thread = fastdiv(L.dst_bytes, &octx->n_threads_div); mmctx->n_quant_rows_per_thread = (src1_nrows + n_quant_tasks - 1) / n_quant_tasks; mmctx->quant_task_func = quant_task_func; @@ -3769,16 +3905,21 @@ static inline void scan_expert_ids( int op_matmul_id(struct htp_ops_context * octx) { htp_matmul_tensors_preamble; + const struct htp_mm_kernel_params * kparams = (const struct htp_mm_kernel_params *) octx->kernel_params; + struct htp_mm_context mmctx_struct = {0}; + struct htp_mm_context * mmctx = &mmctx_struct; + + const int status = htp_mm_init_context(octx, kparams); + if (status != HTP_STATUS_OK) { + return status; + } + struct htp_thread_trace * tr = &octx->ctx->trace[0]; htp_trace_event_start(tr, HTP_TRACE_EVT_INIT, 0); - struct htp_mm_context mmctx_struct = {0}; - struct htp_mm_context * mmctx = &mmctx_struct; mmctx->octx = octx; mmctx->act = src1; - const struct htp_mm_kernel_params * kparams = (const struct htp_mm_kernel_params *) octx->kernel_params; - const struct htp_tensor * restrict ids = octx->src[2]; const size_t src0_row_size = nb01; @@ -3789,9 +3930,6 @@ int op_matmul_id(struct htp_ops_context * octx) { const uint32_t src0_nrows = ne01; // per expert const uint32_t src1_nrows = ne11 * ne12 * ne13; - mmctx->src0_nrows_per_thread = fastdiv(src0_nrows + octx->n_threads - 1, &octx->ctx->n_threads_div); - mmctx->src0_nrows_per_thread = hex_round_up(mmctx->src0_nrows_per_thread, 32); - // row groups const int n_ids = ids->ne[0]; // n_expert_used const int n_as = ne02; // n_expert @@ -3843,6 +3981,29 @@ int op_matmul_id(struct htp_ops_context * octx) { if (kparams->n_hmx) { s = hmx_mm_op_matmul_id(octx, mmctx); } else { + uint32_t src0_row_start = 0; + uint32_t src0_row_end = src0_nrows; + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_can_row_partition(dst, sizeof(float)); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(src0_nrows, can_split ? 32 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + src0_row_start = range.start; + src0_row_end = range.start + range.count; + } + + if (src0_row_start >= src0_row_end) { + if (mapping_buf != octx->ctx->ddr_spad_base) { + free(mapping_buf); + } + return HTP_STATUS_OK; + } + + const uint32_t nrows = src0_row_end - src0_row_start; + mmctx->src0_row_start = src0_row_start; + mmctx->src0_row_end = src0_row_end; + + mmctx->src0_nrows_per_thread = fastdiv(nrows + octx->n_threads - 1, &octx->n_threads_div); + mmctx->src0_nrows_per_thread = hex_round_up(mmctx->src0_nrows_per_thread, 32); + if (hvx_mm_init_vec_dot(mmctx, src0->type) == 0) { s = hvx_mm_matmul_id(octx, mmctx, src1_nrows > 1 ? hvx_mm_id : hvx_mv_id); } else { @@ -3858,29 +4019,31 @@ int op_matmul_id(struct htp_ops_context * octx) { } int op_matmul_id_nx(struct htp_ops_context * octx) { + const struct htp_mm_kernel_params * kparams = (const struct htp_mm_kernel_params *) octx->kernel_params; + struct htp_mm_context mmctx_struct = {0}; + struct htp_mm_context * mmctx = &mmctx_struct; + + const int status = htp_mm_init_context(octx, kparams); + if (status != HTP_STATUS_OK) { + return status; + } + struct htp_thread_trace * tr = &octx->ctx->trace[0]; htp_trace_event_start(tr, HTP_TRACE_EVT_INIT, 0); - const struct htp_mm_kernel_params * kparams = (const struct htp_mm_kernel_params *) octx->kernel_params; + mmctx->octx = octx; const uint32_t n_weights = kparams->n_weights; const struct htp_tensor * restrict src0 = octx->src[0]; const struct htp_tensor * restrict act = octx->src[n_weights]; const struct htp_tensor * restrict ids = octx->src[n_weights + 1]; - struct htp_mm_context mmctx_struct = {0}; - struct htp_mm_context * mmctx = &mmctx_struct; - mmctx->octx = octx; mmctx->act = act; const size_t src0_row_size = src0->nb[1]; const size_t src0_row_size_padded = hex_round_up(src0_row_size, 128); - const uint32_t src0_nrows = src0->ne[1]; const uint32_t src1_nrows = act->ne[1] * act->ne[2] * act->ne[3]; - mmctx->src0_nrows_per_thread = fastdiv(src0_nrows + octx->n_threads - 1, &octx->ctx->n_threads_div); - mmctx->src0_nrows_per_thread = hex_round_up(mmctx->src0_nrows_per_thread, 32); - const int n_ids = ids->ne[0]; const int n_as = src0->ne[2]; @@ -3946,6 +4109,12 @@ int op_matmul_id_nx(struct htp_ops_context * octx) { } int op_matmul_nx(struct htp_ops_context * octx) { const struct htp_mm_kernel_params * kparams = (const struct htp_mm_kernel_params *) octx->kernel_params; + + const int status = htp_mm_init_context(octx, kparams); + if (status != HTP_STATUS_OK) { + return status; + } + if (kparams->n_hmx) { return hmx_mm_nx_2d_f32(octx, kparams); } @@ -4012,7 +4181,7 @@ int op_matmul_nx(struct htp_ops_context * octx) { htp_mm_hvx_vtcm_layout_build(&L, kparams->kernel_type, src0->type, act->ne[0], src1_nrows, octx->n_threads, 0, src0_row_size, src1_row_size, 0, kparams->n_prefetch, false, true); - size_t vtcm_size = kparams->vtcm_size > 0 ? (size_t)kparams->vtcm_size : L.total_bytes; + const size_t vtcm_size = L.total_bytes; if (octx->ctx->vtcm_size < vtcm_size) { FARF(ERROR, "matmul-nx: current VTCM reservation %zu is too small, needed %zu\n", @@ -4034,9 +4203,9 @@ int op_matmul_nx(struct htp_ops_context * octx) { mmctx->vtcm_src0_stride = is_repacked ? 0 : src0_row_size_padded; mmctx->vtcm_src1_stride = src1_row_size; - mmctx->vtcm_src0_size_per_thread = fastdiv(L.src0_bytes, &octx->ctx->n_threads_div); + mmctx->vtcm_src0_size_per_thread = fastdiv(L.src0_bytes, &octx->n_threads_div); mmctx->vtcm_src1_size_per_thread = L.src1_bytes; - mmctx->vtcm_dst_size_per_thread = fastdiv(L.dst_bytes, &octx->ctx->n_threads_div); + mmctx->vtcm_dst_size_per_thread = fastdiv(L.dst_bytes, &octx->n_threads_div); mmctx->n_quant_rows_per_thread = (src1_nrows + n_quant_tasks - 1) / n_quant_tasks; mmctx->quant_task_func = quant_task_func; diff --git a/ggml/src/ggml-hexagon/htp/pad-ops.c b/ggml/src/ggml-hexagon/htp/pad-ops.c index aaa72b31590c..0222f24dcb59 100644 --- a/ggml/src/ggml-hexagon/htp/pad-ops.c +++ b/ggml/src/ggml-hexagon/htp/pad-ops.c @@ -12,8 +12,11 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" +#include "hex-common.h" +#include "hex-profile.h" #include "htp-ctx.h" #include "htp-ops.h" +#include "htp-tensor.h" /* Circular wrap: maps any integer x into [0, n) */ static inline uint32_t wrap_around(int32_t x, uint32_t n) { @@ -68,6 +71,7 @@ struct htp_pad_context { uint32_t nrows_per_thread; uint32_t total_dst_rows; + uint32_t row_start; size_t type_size; @@ -78,39 +82,39 @@ struct htp_pad_context { size_t dst_row_size_aligned; }; -#define htp_pad_preamble \ - const struct htp_tensor * src = octx->src[0]; \ - const struct htp_tensor * dst = octx->dst; \ - \ - const uint32_t ne00 = src->ne[0]; \ - const uint32_t nb00 = src->nb[0]; \ - \ - const uint32_t ne0 = dst->ne[0]; \ - const uint32_t ne1 = dst->ne[1]; \ - const uint32_t ne2 = dst->ne[2]; \ - const uint32_t ne3 = dst->ne[3]; \ - \ - const uint32_t nb1 = dst->nb[1]; \ - const uint32_t nb2 = dst->nb[2]; \ - const uint32_t nb3 = dst->nb[3]; \ - \ - const int32_t lp0 = pctx->lp0, rp0 = pctx->rp0; \ - const int32_t lp1 = pctx->lp1, rp1 = pctx->rp1; \ - const int32_t lp2 = pctx->lp2, rp2 = pctx->rp2; \ - const int32_t lp3 = pctx->lp3, rp3 = pctx->rp3; \ - \ - const size_t type_size = pctx->type_size; \ - \ - const uint32_t row_start = pctx->nrows_per_thread * ith; \ - const uint32_t row_end = MIN(row_start + pctx->nrows_per_thread, pctx->total_dst_rows); - - -#define htp_pad_dma_preamble \ - const size_t src_row_size = pctx->src_row_size; \ - const size_t src_row_size_aligned = pctx->src_row_size_aligned; \ - const size_t dst_row_size = pctx->dst_row_size; \ - const size_t dst_row_size_aligned = pctx->dst_row_size_aligned; \ - \ +#define htp_pad_preamble \ + const struct htp_tensor * src = octx->src[0]; \ + const struct htp_tensor * dst = octx->dst; \ + \ + const uint32_t ne00 = src->ne[0]; \ + const uint32_t nb00 = src->nb[0]; \ + \ + const uint32_t ne0 = dst->ne[0]; \ + const uint32_t ne1 = dst->ne[1]; \ + const uint32_t ne2 = dst->ne[2]; \ + const uint32_t ne3 = dst->ne[3]; \ + \ + const uint32_t nb1 = dst->nb[1]; \ + const uint32_t nb2 = dst->nb[2]; \ + const uint32_t nb3 = dst->nb[3]; \ + \ + const int32_t lp0 = pctx->lp0, rp0 = pctx->rp0; \ + const int32_t lp1 = pctx->lp1, rp1 = pctx->rp1; \ + const int32_t lp2 = pctx->lp2, rp2 = pctx->rp2; \ + const int32_t lp3 = pctx->lp3, rp3 = pctx->rp3; \ + \ + const size_t type_size = pctx->type_size; \ + \ + const uint32_t row_start = pctx->row_start + pctx->nrows_per_thread * ith; \ + const uint32_t row_end = MIN(row_start + pctx->nrows_per_thread, pctx->row_start + pctx->total_dst_rows); + + +#define htp_pad_dma_preamble \ + const size_t src_row_size = pctx->src_row_size; \ + const size_t src_row_size_aligned = pctx->src_row_size_aligned; \ + const size_t dst_row_size = pctx->dst_row_size; \ + const size_t dst_row_size_aligned = pctx->dst_row_size_aligned; \ + \ uint8_t * src_spad_base = octx->src0_spad.data + ith * octx->src0_spad.size_per_thread; \ uint8_t * dst_spad_base = octx->dst_spad.data + ith * octx->dst_spad.size_per_thread; \ \ @@ -125,8 +129,8 @@ static void pad_job_per_thread_hvx(unsigned int nth, unsigned int ith, void * da struct htp_ops_context * octx = pctx->octx; htp_pad_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, row_start); for (uint32_t dst_row = row_start; dst_row < row_end; dst_row++) { uint32_t i1, i2, i3; @@ -165,18 +169,17 @@ static void pad_job_per_thread_hvx(unsigned int nth, unsigned int ith, void * da } } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, row_start); - FARF(HIGH, "pad-hvx %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u usec %u\n", + FARF(HIGH, "pad-hvx %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u\n", ith, nth, src->ne[0], src->ne[1], src->ne[2], src->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - row_start, row_end, - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + row_start, row_end); } // --------------------------------------------------------------------------- -// HVX + DMA PAD kernel — aligned, double-buffered +// HVX + DMA PAD kernel - aligned, double-buffered // --------------------------------------------------------------------------- static void pad_job_per_thread_hvx_dma(unsigned int nth, unsigned int ith, void * data) { @@ -185,9 +188,6 @@ static void pad_job_per_thread_hvx_dma(unsigned int nth, unsigned int ith, void htp_pad_preamble; htp_pad_dma_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - // ----------------------------------------------------------------------- // Priming phase: push 2 pairs of (dummy_dst_DMA, src_DMA) to seed the // double-buffer pipeline before the main loop begins. @@ -222,6 +222,8 @@ static void pad_job_per_thread_hvx_dma(unsigned int nth, unsigned int ith, void // Main loop: pop completed DMAs, compute in VTCM with aligned HVX ops, // push dst DMA and prefetch src for the next+1 row. // ----------------------------------------------------------------------- + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = row_start; ir < row_end; ir++) { uint8_t * dst_spad_cur = (uint8_t *) dma_queue_pop(dma).src; uint8_t * src_spad_cur = (uint8_t *) dma_queue_pop(dma).dst; @@ -236,6 +238,7 @@ static void pad_job_per_thread_hvx_dma(unsigned int nth, unsigned int ith, void lp2, rp2, ne2, lp3, rp3, ne3); + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); if (!interior) { hvx_splat_f32_a(dst_spad_cur, 0.0f, ne0); } else { @@ -249,6 +252,7 @@ static void pad_job_per_thread_hvx_dma(unsigned int nth, unsigned int ith, void hvx_copy_f32_ua(dst_interior, src_spad_cur, ne00); } } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); dma_queue_push_vtcm_to_ddr(dma, dma_make_ptr(dst_ptr, dst_spad_cur), @@ -274,14 +278,11 @@ static void pad_job_per_thread_hvx_dma(unsigned int nth, unsigned int ith, void dma_queue_flush(dma); - t2 = HAP_perf_get_qtimer_count(); - - FARF(HIGH, "pad-hvx-dma %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u usec %u\n", + FARF(HIGH, "pad-hvx-dma %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u\n", ith, nth, src->ne[0], src->ne[1], src->ne[2], src->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - row_start, row_end, - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + row_start, row_end); } // --------------------------------------------------------------------------- @@ -293,8 +294,8 @@ static void pad_job_per_thread_hvx_circular(unsigned int nth, unsigned int ith, struct htp_ops_context * octx = pctx->octx; htp_pad_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, row_start); for (uint32_t dst_row = row_start; dst_row < row_end; dst_row++) { uint32_t i1, i2, i3; @@ -344,18 +345,17 @@ static void pad_job_per_thread_hvx_circular(unsigned int nth, unsigned int ith, } } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, row_start); - FARF(HIGH, "pad-hvx-circ %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u usec %u\n", + FARF(HIGH, "pad-hvx-circ %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u\n", ith, nth, src->ne[0], src->ne[1], src->ne[2], src->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - row_start, row_end, - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + row_start, row_end); } // --------------------------------------------------------------------------- -// HVX + DMA circular PAD kernel — aligned, double-buffered +// HVX + DMA circular PAD kernel - aligned, double-buffered // --------------------------------------------------------------------------- static void pad_job_per_thread_hvx_circular_dma(unsigned int nth, unsigned int ith, void * data) { @@ -364,9 +364,6 @@ static void pad_job_per_thread_hvx_circular_dma(unsigned int nth, unsigned int i htp_pad_preamble; htp_pad_dma_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - // ----------------------------------------------------------------------- // Priming phase: push 2 pairs of (dummy_dst_DMA, src_DMA) to seed the // double-buffer pipeline. Every row is a real src DMA (no null DMAs). @@ -390,6 +387,8 @@ static void pad_job_per_thread_hvx_circular_dma(unsigned int nth, unsigned int i // Main loop: pop completed DMAs, assemble circular row in VTCM with // aligned HVX ops, push dst DMA and prefetch src for the next+1 row. // ----------------------------------------------------------------------- + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + for (uint32_t ir = row_start; ir < row_end; ir++) { uint8_t * dst_spad_cur = (uint8_t *) dma_queue_pop(dma).src; uint8_t * src_spad_cur = (uint8_t *) dma_queue_pop(dma).dst; @@ -398,7 +397,7 @@ static void pad_job_per_thread_hvx_circular_dma(unsigned int nth, unsigned int i pad_decompose_row(ir, ne1, ne2, &i1, &i2, &i3); uint8_t * dst_ptr = (uint8_t *) dst->data + i1 * nb1 + i2 * nb2 + i3 * nb3; - + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); if (lp0 > 0) { uint8_t * dst_left = dst_spad_cur; const uint8_t * src_left = src_spad_cur + (size_t)(ne00 - (uint32_t)lp0) * type_size; @@ -430,6 +429,7 @@ static void pad_job_per_thread_hvx_circular_dma(unsigned int nth, unsigned int i } } } + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir); dma_queue_push_vtcm_to_ddr(dma, dma_make_ptr(dst_ptr, dst_spad_cur), @@ -448,14 +448,11 @@ static void pad_job_per_thread_hvx_circular_dma(unsigned int nth, unsigned int i dma_queue_flush(dma); - t2 = HAP_perf_get_qtimer_count(); - - FARF(HIGH, "pad-hvx-circ-dma %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u usec %u\n", + FARF(HIGH, "pad-hvx-circ-dma %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u\n", ith, nth, src->ne[0], src->ne[1], src->ne[2], src->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - row_start, row_end, - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + row_start, row_end); } int op_pad(struct htp_ops_context * octx) { @@ -489,19 +486,33 @@ int op_pad(struct htp_ops_context * octx) { const uint32_t ne00 = src0->ne[0]; const uint32_t total_dst_rows = dst->ne[1] * dst->ne[2] * dst->ne[3]; - const uint32_t n_threads = MIN(octx->n_threads, total_dst_rows > 0 ? total_dst_rows : 1); + const size_t dst_row_size = (size_t)ne0 * type_size; + + uint32_t row_start = 0; + uint32_t nrows = total_dst_rows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, type_size, (uint32_t) dst_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_dst_rows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; const size_t src_row_size = (size_t)ne00 * type_size; - const size_t dst_row_size = (size_t)ne0 * type_size; const size_t src_row_size_aligned = hex_round_up(src_row_size, VLEN); const size_t dst_row_size_aligned = hex_round_up(dst_row_size, VLEN); // Total VTCM needed: 2 buffers (ping+pong) for src and dst, per thread const size_t vtcm_needed = (size_t)n_threads * 2 * (src_row_size_aligned + dst_row_size_aligned); - const int use_dma = (src0->nb[0] == (uint32_t)type_size) && - (ne00 >= 512) && - (octx->ctx->vtcm_base != NULL) && + const int use_dma = (src0->nb[0] == (uint32_t)type_size) && (ne00 >= 512) && (octx->ctx->vtcm_size >= vtcm_needed); if (use_dma) { @@ -521,8 +532,9 @@ int op_pad(struct htp_ops_context * octx) { .lp1 = lp1, .rp1 = rp1, .lp2 = lp2, .rp2 = rp2, .lp3 = lp3, .rp3 = rp3, - .nrows_per_thread = (total_dst_rows + n_threads - 1) / n_threads, - .total_dst_rows = total_dst_rows, + .nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div), + .total_dst_rows = nrows, + .row_start = row_start, .type_size = type_size, .src_row_size = src_row_size, .src_row_size_aligned = src_row_size_aligned, @@ -537,11 +549,10 @@ int op_pad(struct htp_ops_context * octx) { dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], lp0, rp0, lp1, rp1, lp2, rp2, lp3, rp3); - if (circular && use_dma) { worker_pool_run_func(octx->ctx->worker_pool, pad_job_per_thread_hvx_circular_dma, &pctx, n_threads); } - else if (circular) { worker_pool_run_func(octx->ctx->worker_pool, pad_job_per_thread_hvx_circular, &pctx, n_threads); } - else if (use_dma) { worker_pool_run_func(octx->ctx->worker_pool, pad_job_per_thread_hvx_dma, &pctx, n_threads); } - else { worker_pool_run_func(octx->ctx->worker_pool, pad_job_per_thread_hvx, &pctx, n_threads); } + if (circular && use_dma) { work_queue_run(octx->ctx->work_queue, pad_job_per_thread_hvx_circular_dma, &pctx, n_threads); } + else if (circular) { work_queue_run(octx->ctx->work_queue, pad_job_per_thread_hvx_circular, &pctx, n_threads); } + else if (use_dma) { work_queue_run(octx->ctx->work_queue, pad_job_per_thread_hvx_dma, &pctx, n_threads); } + else { work_queue_run(octx->ctx->work_queue, pad_job_per_thread_hvx, &pctx, n_threads); } return HTP_STATUS_OK; } - diff --git a/ggml/src/ggml-hexagon/htp/repeat-ops.c b/ggml/src/ggml-hexagon/htp/repeat-ops.c index a6f2f0ed5f3f..530279d6503b 100644 --- a/ggml/src/ggml-hexagon/htp/repeat-ops.c +++ b/ggml/src/ggml-hexagon/htp/repeat-ops.c @@ -12,8 +12,10 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" #include "htp-ctx.h" +#include "hex-common.h" +#include "hex-profile.h" #include "htp-ops.h" -#include "htp-ops.h" +#include "htp-tensor.h" struct htp_repeat_context { struct htp_ops_context * octx; @@ -25,6 +27,7 @@ struct htp_repeat_context { uint32_t nrows_per_thread; uint32_t total_dst_rows; // ne1 * ne2 * ne3 + uint32_t row_start; size_t type_size; }; @@ -62,11 +65,11 @@ static void repeat_job_per_thread(unsigned int nth, unsigned int ith, void * dat const size_t row_bytes = ne00 * rctx->type_size; - const uint32_t row_start = rctx->nrows_per_thread * ith; - const uint32_t row_end = MIN(row_start + rctx->nrows_per_thread, rctx->total_dst_rows); + const uint32_t row_start = rctx->row_start + rctx->nrows_per_thread * ith; + const uint32_t row_end = MIN(row_start + rctx->nrows_per_thread, rctx->row_start + rctx->total_dst_rows); - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, row_start); for (uint32_t dst_row = row_start; dst_row < row_end; dst_row++) { // Decompose flat dst row index into (i1, i2, i3) @@ -89,12 +92,12 @@ static void repeat_job_per_thread(unsigned int nth, unsigned int ith, void * dat } } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, row_start); - FARF(HIGH, "repeat %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u usec %u\n", + FARF(HIGH, "repeat %d/%d: (%ux%ux%ux%u) -> (%ux%ux%ux%u) rows %u:%u\n", ith, nth, src->ne[0], src->ne[1], src->ne[2], src->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - row_start, row_end, (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + row_start, row_end); } int op_repeat(struct htp_ops_context * octx) { @@ -119,21 +122,39 @@ int op_repeat(struct htp_ops_context * octx) { return HTP_STATUS_NO_SUPPORT; } - const uint32_t total_dst_rows = dst->ne[1] * dst->ne[2] * dst->ne[3]; - const uint32_t n_threads = MIN(octx->n_threads, total_dst_rows); - if (octx->flags & HTP_OPFLAGS_SKIP_COMPUTE) { return HTP_STATUS_OK; } + const uint32_t total_dst_rows = dst->ne[1] * dst->ne[2] * dst->ne[3]; + const size_t dst_row_size = dst->ne[0] * type_size; + + uint32_t row_start = 0; + uint32_t nrows = total_dst_rows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, type_size, (uint32_t) dst_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_dst_rows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + struct htp_repeat_context rctx = { .octx = octx, .nr0 = dst->ne[0] / src0->ne[0], .nr1 = dst->ne[1] / src0->ne[1], .nr2 = dst->ne[2] / src0->ne[2], .nr3 = dst->ne[3] / src0->ne[3], - .nrows_per_thread = (total_dst_rows + n_threads - 1) / n_threads, - .total_dst_rows = total_dst_rows, + .nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div), + .total_dst_rows = nrows, + .row_start = row_start, .type_size = type_size, }; @@ -142,7 +163,7 @@ int op_repeat(struct htp_ops_context * octx) { dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], rctx.nr0, rctx.nr1, rctx.nr2, rctx.nr3); - worker_pool_run_func(octx->ctx->worker_pool, repeat_job_per_thread, &rctx, n_threads); + work_queue_run(octx->ctx->work_queue, repeat_job_per_thread, &rctx, n_threads); return HTP_STATUS_OK; } diff --git a/ggml/src/ggml-hexagon/htp/rope-ops.c b/ggml/src/ggml-hexagon/htp/rope-ops.c index 0a4b31ccb1d8..c36976ed03fa 100644 --- a/ggml/src/ggml-hexagon/htp/rope-ops.c +++ b/ggml/src/ggml-hexagon/htp/rope-ops.c @@ -80,6 +80,8 @@ struct htp_rope_context { size_t dst_row_stride; size_t src0_row_size_aligned; uint32_t src0_nrows; + uint32_t row_start; + uint32_t nrows; struct fastdiv_values div_ne2_ne1; struct fastdiv_values div_ne1; @@ -539,11 +541,11 @@ static void rope_job_f32(unsigned int nth, unsigned int ith, void * data) { htp_rope_preamble; - const uint32_t src0_nrows = rctx->src0_nrows; + const uint32_t src0_nrows = rctx->nrows; const uint32_t src0_nrows_per_thread = rctx->src0_nrows_per_thread; - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_start_row = rctx->row_start + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, rctx->row_start + src0_nrows); // no work for this thread if (src0_start_row >= src0_end_row) { @@ -706,9 +708,32 @@ static int execute_op_rope_f32(struct htp_ops_context * octx) { } const struct htp_rope_kernel_params * kparams = (const struct htp_rope_kernel_params *) octx->kernel_params; - assert(kparams->n_threads > 0); + if (!htp_ops_context_set_n_threads(octx, kparams->n_threads)) { + return HTP_STATUS_INVAL_PARAMS; + } assert(octx->ctx->vtcm_size >= kparams->vtcm_size); + const uint32_t total_rows = src0->ne[1] * src0->ne[2] * src0->ne[3]; + const size_t dst_data_row_size = dst->ne[0] * sizeof(float); + + uint32_t row_start = 0; + uint32_t nrows = total_rows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, sizeof(float), (uint32_t) dst_data_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition( + total_rows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + const uint32_t ne0 = dst->ne[0]; const size_t src0_row_size = src0->ne[0] * sizeof(float); const size_t src0_row_stride = src0->nb[1]; @@ -752,15 +777,17 @@ static int execute_op_rope_f32(struct htp_ops_context * octx) { rctx.dst_row_stride = dst_row_stride; rctx.src0_row_size_aligned = kparams->src0_row_size_aligned; - rctx.src0_nrows = kparams->src0_nrows; - rctx.src0_nrows_per_thread = kparams->src0_nrows_per_thread; + rctx.src0_nrows = nrows; + rctx.nrows = nrows; + rctx.row_start = row_start; + rctx.src0_nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); rctx.div_ne2_ne1 = kparams->div_ne2_ne1; rctx.div_ne1 = kparams->div_ne1; FARF(HIGH, "rope-f32 n-rows %u n-dims %d ne0 %u ext-factor %.6f theta-scale %.6f attn-factor %.6f\n", rctx.src0_nrows, rctx.n_dims, ne0, rctx.ext_factor, rctx.theta_scale, rctx.attn_factor); - work_queue_run(octx->ctx->work_queue, rope_job_f32, &rctx, kparams->n_threads); + work_queue_run(octx->ctx->work_queue, rope_job_f32, &rctx, n_threads); return err; } diff --git a/ggml/src/ggml-hexagon/htp/set-rows-ops.c b/ggml/src/ggml-hexagon/htp/set-rows-ops.c index 340a497f7a2c..fbd5162a7c00 100644 --- a/ggml/src/ggml-hexagon/htp/set-rows-ops.c +++ b/ggml/src/ggml-hexagon/htp/set-rows-ops.c @@ -18,6 +18,7 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" +#include "hex-common.h" #include "htp-ctx.h" #include "htp-ops.h" #include "htp-tensor.h" @@ -58,6 +59,9 @@ struct set_rows_context { const struct htp_set_rows_kernel_params * kparams; struct htp_set_rows_vtcm_layout vtcm_layout; uint8_t * vtcm_base; + uint32_t task_start; + uint32_t tasks; + uint32_t tasks_per_thread; }; #define SET_ROWS_THREAD_DMA_FN(TYPE_NAME, IDX_TYPE, COMPUTE_EXPR) \ @@ -67,12 +71,12 @@ static void set_rows_thread_dma_##TYPE_NAME##_##IDX_TYPE(unsigned int nth, unsig const struct htp_set_rows_kernel_params * kparams = srctx->kparams; \ set_rows_preamble; \ struct htp_thread_trace * tr = &octx->ctx->trace[ith]; \ - const uint32_t dr = kparams->tasks_per_thread; \ - const uint32_t ir0 = dr * ith; \ - if (ir0 >= kparams->total_tasks) { \ + const uint32_t dr = srctx->tasks_per_thread; \ + const uint32_t ir0 = srctx->task_start + dr * ith; \ + if (ir0 >= srctx->task_start + srctx->tasks) { \ return; \ } \ - const uint32_t ir1 = MIN(ir0 + dr, kparams->total_tasks); \ + const uint32_t ir1 = MIN(ir0 + dr, srctx->task_start + srctx->tasks); \ dma_queue * dma_queue = octx->ctx->dma[ith]; \ const struct htp_set_rows_vtcm_layout * vtcm_layout = &srctx->vtcm_layout; \ uint8_t * vtcm_src0 = srctx->vtcm_base + vtcm_layout->off_src0 + ith * vtcm_layout->src0_bytes_per_thread; \ @@ -192,18 +196,44 @@ int op_set_rows(struct htp_ops_context * octx) { return HTP_STATUS_NO_SUPPORT; } - if (octx->src[1]->type != HTP_TYPE_I32 && octx->src[1]->type != HTP_TYPE_I64) { - return HTP_STATUS_NO_SUPPORT; + if (octx->flags & HTP_OPFLAGS_SKIP_COMPUTE) { + return HTP_STATUS_OK; + } + + const struct htp_tensor * dst = octx->dst; + const uint32_t total_tasks = kparams->total_tasks; + + uint32_t task_start = 0; + uint32_t tasks = total_tasks; + + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_mdev_data_aligned(dst) && (dst->nb[1] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0 && !htp_tensor_is_permuted(dst); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_tasks, can_split ? 1 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + task_start = range.start; + tasks = range.count; } + if (tasks == 0) { + return HTP_STATUS_OK; + } + + if (!htp_ops_context_set_n_threads(octx, (uint32_t) kparams->n_threads)) { + return HTP_STATUS_INVAL_PARAMS; + } + + const uint32_t n_threads = octx->n_threads; + // l2fetch the src1 (indices) tensor in the main thread hex_l2fetch_block((const void *)octx->src[1]->data, octx->src[1]->ne[3] * octx->src[1]->nb[3]); struct set_rows_context srctx; srctx.octx = octx; srctx.kparams = kparams; + srctx.task_start = task_start; + srctx.tasks = tasks; + srctx.tasks_per_thread = fastdiv(tasks + n_threads - 1, &octx->n_threads_div); - htp_set_rows_vtcm_layout_build(&srctx.vtcm_layout, octx->dst->type, ne00, kparams->n_threads); + htp_set_rows_vtcm_layout_build(&srctx.vtcm_layout, octx->dst->type, ne00, n_threads); srctx.vtcm_base = (uint8_t *)octx->ctx->vtcm_base; work_queue_func_t q_func = NULL; @@ -216,15 +246,15 @@ int op_set_rows(struct htp_ops_context * octx) { default: return HTP_STATUS_NO_SUPPORT; } - FARF(HIGH, "set-rows: (%ux%ux%ux%u) x (%ux%ux%ux%u) -> (%ux%ux%ux%u) : src0-vtcm-size %zu dst-vtcm-size %zu n_threads %d\n", + FARF(HIGH, "set-rows: (%ux%ux%ux%u) x (%ux%ux%ux%u) -> (%ux%ux%ux%u) : src0-vtcm-size %zu dst-vtcm-size %zu n-threads %d\n", octx->src[0]->ne[0], octx->src[0]->ne[1], octx->src[0]->ne[2], octx->src[0]->ne[3], octx->src[1]->ne[0], octx->src[1]->ne[1], octx->src[1]->ne[2], octx->src[1]->ne[3], octx->dst->ne[0], octx->dst->ne[1], octx->dst->ne[2], octx->dst->ne[3], - srctx.vtcm_layout.src0_bytes_per_thread * kparams->n_threads, - srctx.vtcm_layout.dst_bytes_per_thread * kparams->n_threads, - kparams->n_threads); + srctx.vtcm_layout.src0_bytes_per_thread * n_threads, + srctx.vtcm_layout.dst_bytes_per_thread * n_threads, + n_threads); - work_queue_run(octx->ctx->work_queue, q_func, &srctx, kparams->n_threads); + work_queue_run(octx->ctx->work_queue, q_func, &srctx, n_threads); return HTP_STATUS_OK; } diff --git a/ggml/src/ggml-hexagon/htp/softmax-ops.c b/ggml/src/ggml-hexagon/htp/softmax-ops.c index d78bcc0eb24e..2497ec76320c 100644 --- a/ggml/src/ggml-hexagon/htp/softmax-ops.c +++ b/ggml/src/ggml-hexagon/htp/softmax-ops.c @@ -14,9 +14,11 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" +#include "hex-common.h" +#include "hex-profile.h" #include "htp-ctx.h" #include "htp-ops.h" -#include "htp-ops.h" +#include "htp-tensor.h" #define htp_softmax_preamble3 \ const uint32_t ne00 = src0->ne[0]; \ @@ -69,6 +71,8 @@ struct htp_softmax_context { struct fastdiv_values fastdiv_ne13; // For mask broadcasting uint32_t src0_nrows_per_thread; + uint32_t row_start; + uint32_t nrows; }; static void apply_mask(float * restrict wp0, @@ -223,19 +227,17 @@ static void softmax_job_f32(unsigned int nth, unsigned int ith, void * data) { htp_softmax_preamble3; - const uint32_t src0_nrows = ne01 * ne02 * ne03; // src0 rows + const uint32_t src0_nrows = smctx->nrows; const uint32_t src0_nrows_per_thread = smctx->src0_nrows_per_thread; - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const uint32_t src0_start_row = smctx->row_start + src0_nrows_per_thread * ith; + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, smctx->row_start + src0_nrows); // no work for this thread if (src0_start_row >= src0_end_row) { return; } - uint64_t qt = HAP_perf_get_qtimer_count(); - int is_aligned = 1; int opt_path = 0; @@ -262,6 +264,9 @@ static void softmax_job_f32(unsigned int nth, unsigned int ith, void * data) { uint32_t prev_i2 = (uint32_t)-1; float slope = 1.0f; + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, src0_start_row); + for (uint32_t r = src0_start_row; r < src0_end_row; ++r) { uint32_t i1 = fastmodulo(r, ne01, &smctx->fastdiv_ne01); uint32_t r_div_ne01 = fastdiv(r, &smctx->fastdiv_ne01); @@ -323,10 +328,11 @@ static void softmax_job_f32(unsigned int nth, unsigned int ith, void * data) { } } - qt = HAP_perf_qtimer_count_to_us(HAP_perf_get_qtimer_count() - qt); - FARF(HIGH, "softmax-f32 %d/%d: %ux%ux%ux%u (%u:%u) x %ux%ux%ux%u -> %ux%ux%ux%u : opt %u f16 %u usec %u\n", ith, nth, + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, src0_start_row); + + FARF(HIGH, "softmax-f32 %d/%d: %ux%ux%ux%u (%u:%u) x %ux%ux%ux%u -> %ux%ux%ux%u : opt %u f16 %u\n", ith, nth, ne00, ne01, ne02, ne03, src0_start_row, src0_end_row, ne10, ne11, ne12, ne13, - ne0, ne1, ne2, ne3, opt_path, smctx->use_f16, (unsigned) qt); + ne0, ne1, ne2, ne3, opt_path, smctx->use_f16); } static int execute_op_softmax_f32(struct htp_ops_context * octx) { @@ -342,13 +348,32 @@ static int execute_op_softmax_f32(struct htp_ops_context * octx) { init_softmax_ctx(&smctx, octx); const uint32_t src0_nrows = src0->ne[1] * src0->ne[2] * src0->ne[3]; - const uint32_t n_threads = MIN(octx->n_threads, src0_nrows); + const size_t elem_size = sizeof(float); + const size_t dst_row_size = dst->nb[1]; + + uint32_t row_start = 0; + uint32_t nrows = src0_nrows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, (uint32_t) elem_size, (uint32_t) dst_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(src0_nrows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } - smctx.src0_nrows_per_thread = (src0_nrows + n_threads - 1) / n_threads; + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + + smctx.src0_nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); + smctx.row_start = row_start; + smctx.nrows = nrows; const size_t src0_row_size = src0->nb[1]; const size_t src1_row_size = src0_row_size; - const size_t dst_row_size = dst->nb[1]; // VTCM scratchpads for all tensors // 4 rows per thread, padded to HVX vector size @@ -383,9 +408,7 @@ static int execute_op_softmax_f32(struct htp_ops_context * octx) { octx->src1_spad.data = octx->src0_spad.data + octx->src0_spad.size; octx->src1_spad.src = NULL; octx->dst_spad.data = octx->src1_spad.data + octx->src1_spad.size; octx->dst_spad.src = NULL; - if (octx->flags & HTP_OPFLAGS_SKIP_COMPUTE) return err; - - worker_pool_run_func(octx->ctx->worker_pool, softmax_job_f32, &smctx, n_threads); + work_queue_run(octx->ctx->work_queue, softmax_job_f32, &smctx, n_threads); return err; } diff --git a/ggml/src/ggml-hexagon/htp/solve-tri-ops.c b/ggml/src/ggml-hexagon/htp/solve-tri-ops.c index ae8e1a50495f..847a78712de4 100644 --- a/ggml/src/ggml-hexagon/htp/solve-tri-ops.c +++ b/ggml/src/ggml-hexagon/htp/solve-tri-ops.c @@ -1,13 +1,16 @@ #pragma clang diagnostic ignored "-Wunused-but-set-variable" #include <HAP_farf.h> -#include <HAP_perf.h> #include <string.h> +#include "hex-common.h" +#include "hex-profile.h" + #define GGML_COMMON_DECL_C #include "ggml-common.h" #include "htp-ctx.h" #include "htp-ops.h" +#include "htp-tensor.h" #include "hvx-types.h" #include "hvx-utils.h" @@ -15,6 +18,7 @@ struct htp_solve_tri_context { struct htp_ops_context * octx; uint32_t jobs_per_thread; uint32_t total_jobs; + uint32_t job_start; uint32_t k_chunks; uint32_t col_block; }; @@ -89,11 +93,11 @@ static void solve_tri_batch_thread_f32(unsigned int nth, unsigned int ith, void const uint32_t col_block = VLEN_FP32; const uint32_t k_full = (k / col_block) * col_block; - const uint32_t start_batch = sctx->jobs_per_thread * ith; - const uint32_t end_batch = MIN(start_batch + sctx->jobs_per_thread, sctx->total_jobs); + const uint32_t start_batch = sctx->job_start + sctx->jobs_per_thread * ith; + const uint32_t end_batch = MIN(start_batch + sctx->jobs_per_thread, sctx->job_start + sctx->total_jobs); - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) start_batch); for (uint32_t batch = start_batch; batch < end_batch; ++batch) { const uint32_t i03 = batch / ne02; @@ -127,11 +131,10 @@ static void solve_tri_batch_thread_f32(unsigned int nth, unsigned int ith, void } } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) end_batch); - FARF(HIGH, "solve-tri-batch %d/%d: A=(%ux%u) B=(%ux%u) batch %u:%u usec %u\n", - ith, nth, n, n, k, n, start_batch, end_batch, - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + FARF(HIGH, "solve-tri-batch %d/%d: A=(%ux%u) B=(%ux%u) batch %u:%u\n", + ith, nth, n, n, k, n, start_batch, end_batch); } // Chunk-level thread: each job is one (batch, col_chunk) pair. @@ -148,11 +151,11 @@ static void solve_tri_chunk_thread_f32(unsigned int nth, unsigned int ith, void const uint32_t ne02 = src0->ne[2]; - const uint32_t start_job = sctx->jobs_per_thread * ith; - const uint32_t end_job = MIN(start_job + sctx->jobs_per_thread, sctx->total_jobs); + const uint32_t start_job = sctx->job_start + sctx->jobs_per_thread * ith; + const uint32_t end_job = MIN(start_job + sctx->jobs_per_thread, sctx->job_start + sctx->total_jobs); - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) start_job); for (uint32_t job = start_job; job < end_job; ++job) { const uint32_t batch = job / sctx->k_chunks; @@ -161,16 +164,14 @@ static void solve_tri_chunk_thread_f32(unsigned int nth, unsigned int ith, void const uint32_t i03 = batch / ne02; const uint32_t i02 = batch - i03 * ne02; - const uint32_t col0 = chunk * sctx->col_block; - const uint32_t coln = MIN(sctx->col_block, k - col0); - const float * A_batch = (const float *) ((const uint8_t *) (uintptr_t) src0->data + i02 * src0->nb[2] + i03 * src0->nb[3]); const float * B_batch = (const float *) ((const uint8_t *) (uintptr_t) src1->data + i02 * src1->nb[2] + i03 * src1->nb[3]); float * X_batch = (float *) ((uint8_t *) (uintptr_t) dst->data + i02 * dst->nb[2] + i03 * dst->nb[3]); - const bool use_hvx = (coln >= 8); + const uint32_t col0 = chunk * sctx->col_block; + const uint32_t coln = MIN(sctx->col_block, k - col0); for (uint32_t row = 0; row < n; ++row) { const float diag = A_batch[row * n + row]; @@ -179,7 +180,7 @@ static void solve_tri_chunk_thread_f32(unsigned int nth, unsigned int ith, void const float * A_row = A_batch + row * n; const float * B_row = B_batch + row * k; - if (use_hvx) { + if (coln >= 8) { solve_tri_row_hvx(A_row, B_row, X_batch, row, k, col0, coln, inv_diag); } else { solve_tri_row_scalar(A_row, B_row, X_batch, row, k, col0, coln, inv_diag); @@ -187,11 +188,10 @@ static void solve_tri_chunk_thread_f32(unsigned int nth, unsigned int ith, void } } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) end_job); - FARF(HIGH, "solve-tri-chunk %d/%d: A=(%ux%u) B=(%ux%u) job %u:%u usec %u\n", - ith, nth, n, n, k, n, start_job, end_job, - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + FARF(HIGH, "solve-tri-chunk %d/%d: A=(%ux%u) B=(%ux%u) jobs %u:%u\n", + ith, nth, n, n, k, n, start_job, end_job); } int op_solve_tri(struct htp_ops_context * octx) { @@ -235,32 +235,64 @@ int op_solve_tri(struct htp_ops_context * octx) { dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], batched); if (batched) { + uint32_t job_start = 0; + uint32_t njobs = total_batches; + + if (octx->ctx->mdev.count > 1) { + const uint32_t batch_size = dst->nb[2]; + const uint32_t batches_per_chunk = (batch_size > 0) ? (HEX_L2_LINE_SIZE / hex_gcd_u32(batch_size, HEX_L2_LINE_SIZE)) : 1; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_batches, htp_tensor_mdev_data_aligned(dst) ? batches_per_chunk : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + job_start = range.start; + njobs = range.count; + } + + if (njobs == 0) { + return HTP_STATUS_OK; + } + // Batch-level parallelism - const uint32_t n_threads = MIN((uint32_t) octx->n_threads, total_batches); + const uint32_t n_threads = octx->n_threads; struct htp_solve_tri_context sctx = { .octx = octx, - .jobs_per_thread = (total_batches + n_threads - 1) / n_threads, - .total_jobs = total_batches, + .jobs_per_thread = fastdiv(njobs + n_threads - 1, &octx->n_threads_div), + .total_jobs = njobs, + .job_start = job_start, .k_chunks = k_chunks, .col_block = col_block, }; - worker_pool_run_func(octx->ctx->worker_pool, solve_tri_batch_thread_f32, &sctx, n_threads); + work_queue_run(octx->ctx->work_queue, solve_tri_batch_thread_f32, &sctx, n_threads); } else { // Chunk-level parallelism const uint32_t total_jobs = total_batches * k_chunks; - const uint32_t n_threads = MIN((uint32_t) octx->n_threads, MAX(total_jobs, 1)); + + uint32_t job_start = 0; + uint32_t njobs = total_jobs; + + if (octx->ctx->mdev.count > 1) { + const bool can_split = htp_tensor_mdev_data_aligned(dst) && ((dst->nb[1] & (HTP_TENSOR_MDEV_LINE_SIZE - 1)) == 0); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(total_jobs, can_split ? 1 : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + job_start = range.start; + njobs = range.count; + } + + if (njobs == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; struct htp_solve_tri_context sctx = { .octx = octx, - .jobs_per_thread = (total_jobs + n_threads - 1) / n_threads, - .total_jobs = total_jobs, + .jobs_per_thread = fastdiv(njobs + n_threads - 1, &octx->n_threads_div), + .total_jobs = njobs, + .job_start = job_start, .k_chunks = k_chunks, .col_block = col_block, }; - worker_pool_run_func(octx->ctx->worker_pool, solve_tri_chunk_thread_f32, &sctx, n_threads); + work_queue_run(octx->ctx->work_queue, solve_tri_chunk_thread_f32, &sctx, n_threads); } return HTP_STATUS_OK; diff --git a/ggml/src/ggml-hexagon/htp/ssm-conv.c b/ggml/src/ggml-hexagon/htp/ssm-conv.c index a48bc9ed86b2..bef1425368e1 100644 --- a/ggml/src/ggml-hexagon/htp/ssm-conv.c +++ b/ggml/src/ggml-hexagon/htp/ssm-conv.c @@ -4,7 +4,6 @@ #include <HAP_farf.h> #include <HAP_mem.h> -#include <HAP_perf.h> #include <HAP_ps.h> #include <hexagon_protos.h> #include <hexagon_types.h> @@ -16,8 +15,9 @@ #include "ggml-common.h" #include "htp-ctx.h" #include "hex-dma.h" +#include "hex-profile.h" #include "htp-ops.h" -#include "htp-ops.h" +#include "htp-tensor.h" #include "hvx-utils.h" #define htp_ssm_conv_tensors_preamble \ @@ -63,6 +63,8 @@ struct htp_ssm_conv_context { uint32_t nrows_per_thread; uint32_t d_inner_tile; uint64_t t_start; + uint32_t row_start; + uint32_t nrows; }; #define htp_ssm_conv_preamble \ @@ -75,9 +77,6 @@ struct htp_ssm_conv_context { static void ssm_conv_thread_f32_f32(unsigned int nth, unsigned int ith, void *data) { htp_ssm_conv_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - const uint32_t d_conv = src1->ne[0]; const uint32_t d_inner = src0->ne[1]; const uint32_t n_t = dst->ne[1]; @@ -95,14 +94,17 @@ static void ssm_conv_thread_f32_f32(unsigned int nth, unsigned int ith, void *da // Calculate row range for this thread const uint32_t d_inner_per_thread = scctx->nrows_per_thread; - const uint32_t d_inner_start = d_inner_per_thread * ith; - const uint32_t d_inner_end = MIN(d_inner_start + d_inner_per_thread, d_inner); + const uint32_t d_inner_start = scctx->row_start + d_inner_per_thread * ith; + const uint32_t d_inner_end = MIN(d_inner_start + d_inner_per_thread, scctx->row_start + scctx->nrows); // No work for this thread if (d_inner_start >= d_inner_end) { return; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) d_inner_start); + for (uint32_t i3 = 0; i3 < n_s; ++i3) { for (uint32_t i2 = 0; i2 < n_t; ++i2) { for (uint32_t i1 = d_inner_start; i1 < d_inner_end; ++i1) { @@ -121,12 +123,12 @@ static void ssm_conv_thread_f32_f32(unsigned int nth, unsigned int ith, void *da } } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) d_inner_end); - FARF(HIGH, "ssm-conv-f32 %d/%d: %ux%ux%ux%u (%u:%u) * %ux%ux%ux%u -> %ux%ux%ux%u usec %u\n", + FARF(HIGH, "ssm-conv-f32 %d/%d: %ux%ux%ux%u (%u:%u) * %ux%ux%ux%u -> %ux%ux%ux%u\n", ith, nth, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], d_inner_start, d_inner_end, src1->ne[0], src1->ne[1], src1->ne[2], src1->ne[3], dst->ne[0], dst->ne[1], - dst->ne[2], dst->ne[3], (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + dst->ne[2], dst->ne[3]); } @@ -257,9 +259,6 @@ static inline void transpose_src0_block(const float * src0_block, static void ssm_conv_thread_f32_f32_hvx(unsigned int nth, unsigned int ith, void *data) { htp_ssm_conv_preamble; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - const uint32_t d_conv = src1->ne[0]; const uint32_t d_inner = src0->ne[1]; const uint32_t n_t = dst->ne[1]; @@ -273,13 +272,16 @@ static void ssm_conv_thread_f32_f32_hvx(unsigned int nth, unsigned int ith, void const uint32_t dst_stride_seq = dst->nb[2] / sizeof(float); const uint32_t dr = scctx->nrows_per_thread; - const uint32_t ir0 = dr * ith; - const uint32_t ir1 = MIN(ir0 + dr, d_inner); + const uint32_t ir0 = scctx->row_start + dr * ith; + const uint32_t ir1 = MIN(ir0 + dr, scctx->row_start + scctx->nrows); if (ir0 >= ir1) { return; } + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir0); + const uint32_t d_inner_per_thread = ir1 - ir0; const uint32_t d_inner_stride = scctx->nrows_per_thread; const uint32_t d_inner_tile = scctx->d_inner_tile; @@ -319,97 +321,118 @@ static void ssm_conv_thread_f32_f32_hvx(unsigned int nth, unsigned int ith, void HVX_Vector w = *(const HVX_Vector *) (src1_T + j * d_inner_stride + tile_off + cb); acc = Q6_Vqf32_vadd_Vqf32Vqf32(acc, Q6_Vqf32_vmpy_VsfVsf(x, w)); } - HVX_Vector res = Q6_Vsf_equals_Vqf32(acc); - float * dst_ptr = dst_data + i3 * dst_stride_seq + t * dst_stride_token + (ir0 + tile_off + cb); + HVX_Vector y = Q6_Vsf_equals_Vqf32(acc); + + float * dst_ptr = dst_data + (ir0 + tile_off + cb) + t * dst_stride_token + i3 * dst_stride_seq; if (cb_n == C_TILE) { - *(HVX_UVector *) dst_ptr = res; + *(HVX_UVector *) dst_ptr = y; } else { - hvx_vec_store_u(dst_ptr, cb_n * sizeof(float), res); + hvx_vec_store_u(dst_ptr, cb_n * sizeof(float), y); } } } } } - t2 = HAP_perf_get_qtimer_count(); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) ir1); - FARF(HIGH, "ssm-conv-f32-hvx %d/%d: %ux%ux%ux%u (%u:%u) tile=%u * %ux%ux%ux%u -> %ux%ux%ux%u usec %u\n", - ith, nth, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], ir0, ir1, d_inner_tile, + FARF(HIGH, "ssm-conv-f32-hvx %d/%d: %ux%ux%ux%u (%u:%u) * %ux%ux%ux%u -> %ux%ux%ux%u\n", + ith, nth, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], ir0, ir1, src1->ne[0], src1->ne[1], src1->ne[2], src1->ne[3], dst->ne[0], dst->ne[1], - dst->ne[2], dst->ne[3], (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + dst->ne[2], dst->ne[3]); } int op_ssm_conv_f32(struct htp_ops_context * octx) { - htp_ssm_conv_tensors_preamble; + const struct htp_tensor * src0 = octx->src[0]; + const struct htp_tensor * src1 = octx->src[1]; + const struct htp_tensor * dst = octx->dst; if (src0->type != HTP_TYPE_F32 || src1->type != HTP_TYPE_F32 || dst->type != HTP_TYPE_F32) { - FARF(ERROR, "ssm_conv: only (F32 x F32 -> F32) OPs supported"); return HTP_STATUS_NO_SUPPORT; } - struct htp_ssm_conv_context scctx = { 0 }; - scctx.octx = octx; - const uint32_t d_conv = src1->ne[0]; const uint32_t d_inner = src0->ne[1]; const uint32_t n_t = dst->ne[1]; // tokens per sequence const uint32_t n_s = dst->ne[2]; // number of sequences in the batch - const uint32_t n_threads = MIN(octx->n_threads, d_inner); + if (octx->flags & HTP_OPFLAGS_SKIP_COMPUTE) { + return HTP_STATUS_OK; + } - if (!(octx->flags & HTP_OPFLAGS_SKIP_COMPUTE)) { - uint32_t use_hvx = 0; - if (d_inner >= VLEN_FP32 && n_t >= VLEN_FP32) { - use_hvx = 1; - } + uint32_t row_start = 0; + uint32_t nrows = d_inner; + + if (octx->ctx->mdev.count > 1) { + const uint32_t elems_per_chunk = VLEN_FP32; + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(d_inner, htp_tensor_mdev_data_aligned(dst) ? elems_per_chunk : 0, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + + struct htp_ssm_conv_context scctx = { 0 }; + scctx.octx = octx; + scctx.row_start = row_start; + scctx.nrows = nrows; + + uint32_t use_hvx = 0; + if (nrows >= VLEN_FP32 && n_t >= VLEN_FP32) { + use_hvx = 1; + } + + const uint32_t raw_rpt = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); + scctx.nrows_per_thread = hex_round_up(raw_rpt, VLEN_FP32); - scctx.nrows_per_thread = hex_round_up((d_inner + n_threads - 1) / n_threads, VLEN_FP32); + const uint32_t d_inner_per_thread = scctx.nrows_per_thread; + const uint32_t ncs = src0->ne[0]; - const uint32_t d_inner_per_thread = scctx.nrows_per_thread; - const uint32_t ncs = src0->ne[0]; + const uint32_t src1_T_size = hex_round_up(d_conv * d_inner_per_thread * sizeof(float), 256); + const uint32_t src0_T_max = HTP_SSM_CONV_VTCM_BUDGET > src1_T_size ? HTP_SSM_CONV_VTCM_BUDGET - src1_T_size : 0; - const uint32_t src1_T_size = hex_round_up(d_conv * d_inner_per_thread * sizeof(float), 256); - const uint32_t src0_T_max = HTP_SSM_CONV_VTCM_BUDGET > src1_T_size ? HTP_SSM_CONV_VTCM_BUDGET - src1_T_size : 0; + uint32_t d_inner_tile = (src0_T_max / sizeof(float)) / ncs; + d_inner_tile -= (d_inner_tile % VLEN_FP32); + if (d_inner_tile == 0) { + FARF(HIGH, "ssm_conv-f32: inner tile rounds to 0 (ncs=%u), falling back to scalar\n", ncs); + use_hvx = 0; + } else { + scctx.d_inner_tile = d_inner_tile; - uint32_t d_inner_tile = (src0_T_max / sizeof(float)) / ncs; - d_inner_tile -= (d_inner_tile % VLEN_FP32); - if (d_inner_tile == 0) { - FARF(HIGH, "ssm_conv-f32: inner tile rounds to 0 (ncs=%u), falling back to scalar\n", ncs); + octx->src0_spad.size_per_thread = hex_round_up(d_inner_tile * ncs * sizeof(float), 256); + octx->src1_spad.size_per_thread = src1_T_size; + octx->dst_spad.size_per_thread = 0; + + octx->src0_spad.size = octx->src0_spad.size_per_thread * n_threads; + octx->src1_spad.size = octx->src1_spad.size_per_thread * n_threads; + octx->dst_spad.size = 0; + + octx->src0_spad.data = octx->ctx->vtcm_base; + octx->src1_spad.data = octx->src0_spad.data + octx->src0_spad.size; + octx->src0_spad.src = NULL; + octx->src1_spad.src = NULL; + + const size_t total_spad = octx->src0_spad.size + octx->src1_spad.size; + if (total_spad > octx->ctx->vtcm_size) { + FARF(HIGH, "ssm_conv-f32: scratchpad %zu exceeds VTCM %zu, falling back to scalar\n", + total_spad, octx->ctx->vtcm_size); use_hvx = 0; - } else { - scctx.d_inner_tile = d_inner_tile; - - octx->src0_spad.size_per_thread = hex_round_up(d_inner_tile * ncs * sizeof(float), 256); - octx->src1_spad.size_per_thread = src1_T_size; - octx->dst_spad.size_per_thread = 0; - - octx->src0_spad.size = octx->src0_spad.size_per_thread * n_threads; - octx->src1_spad.size = octx->src1_spad.size_per_thread * n_threads; - octx->dst_spad.size = 0; - - octx->src0_spad.data = octx->ctx->vtcm_base; - octx->src1_spad.data = octx->src0_spad.data + octx->src0_spad.size; - octx->src0_spad.src = NULL; - octx->src1_spad.src = NULL; - - const size_t total_spad = octx->src0_spad.size + octx->src1_spad.size; - if (total_spad > octx->ctx->vtcm_size) { - FARF(HIGH, "ssm_conv-f32: scratchpad %zu exceeds VTCM %zu, falling back to scalar\n", - total_spad, octx->ctx->vtcm_size); - use_hvx = 0; - } } + } - FARF(HIGH, "ssm-conv-f32: (%ux%ux%ux%u) x (%ux%ux%ux%u) -> (%ux%ux%ux%u) : use_hvx %d\n", src0->ne[0], - src0->ne[1], src0->ne[2], src0->ne[3], src1->ne[0], src1->ne[1], src1->ne[2], src1->ne[3], dst->ne[0], - dst->ne[1], dst->ne[2], dst->ne[3], use_hvx); + FARF(HIGH, "ssm-conv-f32: (%ux%ux%ux%u) x (%ux%ux%ux%u) -> (%ux%ux%ux%u) : use_hvx %d\n", src0->ne[0], + src0->ne[1], src0->ne[2], src0->ne[3], src1->ne[0], src1->ne[1], src1->ne[2], src1->ne[3], dst->ne[0], + dst->ne[1], dst->ne[2], dst->ne[3], use_hvx); - if (use_hvx) { - worker_pool_run_func(octx->ctx->worker_pool, ssm_conv_thread_f32_f32_hvx, &scctx, n_threads); - } else { - worker_pool_run_func(octx->ctx->worker_pool, ssm_conv_thread_f32_f32, &scctx, n_threads); - } + if (use_hvx) { + work_queue_run(octx->ctx->work_queue, ssm_conv_thread_f32_f32_hvx, &scctx, n_threads); + } else { + work_queue_run(octx->ctx->work_queue, ssm_conv_thread_f32_f32, &scctx, n_threads); } return HTP_STATUS_OK; diff --git a/ggml/src/ggml-hexagon/htp/sum-rows-ops.c b/ggml/src/ggml-hexagon/htp/sum-rows-ops.c index 874c41ab2ac7..faf716b4bc18 100644 --- a/ggml/src/ggml-hexagon/htp/sum-rows-ops.c +++ b/ggml/src/ggml-hexagon/htp/sum-rows-ops.c @@ -13,35 +13,38 @@ #define GGML_COMMON_DECL_C #include "ggml-common.h" +#include "hex-common.h" +#include "hex-profile.h" #include "htp-ctx.h" #include "htp-ops.h" -#include "htp-ops.h" +#include "htp-tensor.h" #define sum_rows_preamble \ const struct htp_tensor *src0 = octx->src[0]; \ const struct htp_tensor *dst = octx->dst; \ \ - const uint32_t ne00 = src0->ne[0]; \ - const uint32_t ne01 = src0->ne[1]; \ - const uint32_t ne02 = src0->ne[2]; \ - const uint32_t ne03 = src0->ne[3]; \ - \ - const uint32_t nb00 = src0->nb[0]; \ - const uint32_t nb01 = src0->nb[1]; \ - const uint32_t nb02 = src0->nb[2]; \ - const uint32_t nb03 = src0->nb[3]; \ - \ - const uint32_t ne0 = dst->ne[0]; \ - const uint32_t ne1 = dst->ne[1]; \ - const uint32_t ne2 = dst->ne[2]; \ - const uint32_t ne3 = dst->ne[3]; \ - \ - const uint32_t nb0 = dst->nb[0]; \ - const uint32_t nb1 = dst->nb[1]; \ - const uint32_t nb2 = dst->nb[2]; \ - const uint32_t nb3 = dst->nb[3]; \ + const uint32_t ne00 = src0->ne[0]; \ + const uint32_t ne01 = src0->ne[1]; \ + const uint32_t ne02 = src0->ne[2]; \ + const uint32_t ne03 = src0->ne[3]; \ + \ + const uint32_t nb00 = src0->nb[0]; \ + const uint32_t nb01 = src0->nb[1]; \ + const uint32_t nb02 = src0->nb[2]; \ + const uint32_t nb03 = src0->nb[3]; \ + \ + const uint32_t ne0 = dst->ne[0]; \ + const uint32_t ne1 = dst->ne[1]; \ + const uint32_t ne2 = dst->ne[2]; \ + const uint32_t ne3 = dst->ne[3]; \ + \ + const uint32_t nb0 = dst->nb[0]; \ + const uint32_t nb1 = dst->nb[1]; \ + const uint32_t nb2 = dst->nb[2]; \ + const uint32_t nb3 = dst->nb[3]; \ struct sum_rows_context { + struct htp_ops_context * octx; const uint8_t * src_data; uint8_t * dst_data; uint32_t ne00; @@ -76,6 +79,9 @@ static void sum_rows_thread_f32(unsigned int nth, unsigned int ith, void *data) // Calculate actual number of rows for this thread const uint32_t n_rows = end_row - start_row; + struct htp_thread_trace * tr = &smctx->octx->ctx->trace[ith]; + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) start_row); + for (uint32_t ir = 0; ir < n_rows; ir++) { const float * restrict src_local = src_th + (ir * (src_stride / sizeof(float))); @@ -89,6 +95,8 @@ static void sum_rows_thread_f32(unsigned int nth, unsigned int ith, void *data) dst_th[ir] = hvx_reduce_sum_f32((const uint8_t *) src_local, ne00); } } + + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) start_row); } int op_sum_rows(struct htp_ops_context * octx) { @@ -102,9 +110,26 @@ int op_sum_rows(struct htp_ops_context * octx) { return HTP_STATUS_OK; } - const uint32_t src0_nrows = ne01 * ne02 * ne03; - const uint32_t n_threads = MIN(octx->n_threads, src0_nrows); - const uint32_t rows_per_thread = (src0_nrows + n_threads - 1) / n_threads; + const uint32_t src0_nrows = ne01 * ne02 * ne03; + const size_t dst_data_row_size = dst->ne[0] * sizeof(float); + + uint32_t row_start = 0; + uint32_t nrows = src0_nrows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, sizeof(float), (uint32_t) dst_data_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(src0_nrows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + const uint32_t rows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div); bool opt_path = false; if ((0 == hex_is_aligned((void *) src0->data, VLEN)) && !(nb01 & (VLEN - 1))) { @@ -112,17 +137,18 @@ int op_sum_rows(struct htp_ops_context * octx) { } struct sum_rows_context smctx = { - .src_data = (const uint8_t *) src0->data, - .dst_data = (uint8_t *) dst->data, + .octx = octx, + .src_data = (const uint8_t *) src0->data + row_start * nb01, + .dst_data = (uint8_t *) dst->data + row_start * nb1, .ne00 = ne00, .src_stride = nb01, .dst_stride = nb1, .rows_per_thread = rows_per_thread, - .total_rows = src0_nrows, + .total_rows = nrows, .opt_path = opt_path, }; - worker_pool_run_func(octx->ctx->worker_pool, sum_rows_thread_f32, &smctx, n_threads); + work_queue_run(octx->ctx->work_queue, sum_rows_thread_f32, &smctx, n_threads); return HTP_STATUS_OK; } diff --git a/ggml/src/ggml-hexagon/htp/unary-ops.c b/ggml/src/ggml-hexagon/htp/unary-ops.c index 7850ab27e00a..cb82bfa3c2d9 100644 --- a/ggml/src/ggml-hexagon/htp/unary-ops.c +++ b/ggml/src/ggml-hexagon/htp/unary-ops.c @@ -46,6 +46,7 @@ struct htp_unary_context { uint32_t block; uint32_t src0_nrows; uint32_t src0_nrows_per_thread; + uint32_t row_start; uint32_t nc; uint32_t col_tile; // tiled mode bool broadcast_weight; @@ -496,7 +497,7 @@ static void tri_f32(const float * restrict src, } if (boundary > ne0) boundary = ne0; - // Full HVX vectors — each starts at a 128-byte aligned offset + // Full HVX vectors - each starts at a 128-byte aligned offset for (uint32_t i = 0; i < nvec; i++) { const uint32_t vec_start = i * VLEN_FP32; const uint32_t vec_end = vec_start + VLEN_FP32; @@ -563,7 +564,7 @@ static void softplus_f32(const float * restrict src, for (uint32_t i = 0; i < ne0; i++) { float x = src_f[i]; - // For x > 20: softplus(x) ≈ x (avoids exp overflow) + // For x > 20: softplus(x) ~ x (avoids exp overflow) dst_f[i] = (x > 20.0f) ? x : logf(1.0f + expf(x)); } } @@ -661,8 +662,8 @@ static void unary_task_##SUFFIX##_##NAME(unsigned int nth, unsigned int ith, voi const size_t dst_row_size_aligned = uctx->dst_row_size_aligned; \ \ const uint32_t src0_nrows = uctx->src0_nrows; \ - const uint32_t src0_start_row = src0_nrows_per_thread * ith; \ - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); \ + const uint32_t src0_start_row = uctx->row_start + src0_nrows_per_thread * ith; \ + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, uctx->row_start + src0_nrows); \ \ if (src0_start_row >= src0_end_row) { \ return; \ @@ -833,124 +834,126 @@ DEFINE_UNARY_TASK_IMPL(unary_abs, _Float16, f16, false, false, abs_f16(src0_vtcm DEFINE_UNARY_TASK_IMPL(unary_log, _Float16, f16, false, false, log_f16(src0_vtcm, dst_vtcm, block_size, uctx)) // Apply a pointwise unary op to one column tile that is already in VTCM. -#define DEFINE_UNARY_TILED_TASK(NAME, IS_TRI, CORE_TILE_EXPR) \ -static void unary_task_f32_tiled_##NAME(unsigned int nth, unsigned int ith, void * data) { \ - const struct htp_unary_context * uctx = (const struct htp_unary_context *) data; \ - struct htp_ops_context * octx = uctx->octx; \ - const struct htp_tensor * src = octx->src[0]; \ - const struct htp_tensor * dst = octx->dst; \ - struct htp_thread_trace * tr = &octx->ctx->trace[ith]; \ - \ - htp_unary_preamble; \ - \ - int32_t * op_params = octx->op_params; \ - const uint32_t col_tile = uctx->col_tile; \ - \ - const uint32_t src0_nrows = uctx->src0_nrows; \ - const uint32_t src0_start_row = uctx->src0_nrows_per_thread * ith; \ - const uint32_t src0_end_row = MIN(src0_start_row + uctx->src0_nrows_per_thread, src0_nrows); \ - \ - if (src0_start_row >= src0_end_row) { \ - return; \ - } \ - \ - const uint8_t * restrict data_src = uctx->data_src0; \ - uint8_t * restrict data_dst = uctx->data_dst; \ - \ - uint8_t * src0_vtcm_data = uctx->vtcm_src0 + (ith * uctx->vtcm_src0_size_per_thread); \ - uint8_t * dst_vtcm_data = uctx->vtcm_dst + (ith * uctx->vtcm_dst_size_per_thread); \ - \ - const size_t src0_half = uctx->src0_vtcm_half_size; \ - const size_t dst_half = uctx->dst_vtcm_half_size; \ - \ - dma_queue * dmaq = octx->ctx->dma[ith]; \ - \ - const struct fastdiv_values * div_ne01 = &uctx->kparams->div_ne01; \ - const struct fastdiv_values * div_ne02 = &uctx->kparams->div_ne02; \ - const struct fastdiv_values * div_ne012 = &uctx->kparams->div_ne012; \ - const struct fastdiv_values * div_tpr = &uctx->kparams->div_tpr; \ - \ - const uint32_t tiles_per_row = (ne0 + col_tile - 1) / col_tile; \ - const int32_t tri_ttype = (IS_TRI) ? op_params[0] : 0; \ - \ - const bool src0_contig = (nb02 == (size_t)ne01 * nb01) && \ - (nb03 == (size_t)ne02 * nb02); \ - const bool dst_contig = (nb2 == (size_t)ne1 * nb1) && \ - (nb3 == (size_t)ne2 * nb2); \ - \ - const uint32_t total_tiles = (src0_end_row - src0_start_row) * tiles_per_row; \ - \ - for (uint32_t t = 0, vtcm_idx = 0; t < total_tiles && vtcm_idx < 2; t++, vtcm_idx++) { \ - const uint32_t row = src0_start_row + t / tiles_per_row; \ - const uint32_t col = (t % tiles_per_row) * col_tile; \ - const uint32_t tw = MIN(col_tile, ne0 - col); \ - const size_t tb = (size_t) tw * sizeof(float); \ - const size_t soff = (src0_contig ? (row * nb01) : \ - unary_row_offset(row, ne01, ne02, div_ne01, div_ne02, div_ne012, nb01, nb02, nb03)) +\ - (size_t) col * sizeof(float); \ - \ - dma_queue_push(dmaq, dma_make_ptr(data_dst, dst_vtcm_data + (vtcm_idx * dst_half)), 0, 0, 0, 0); \ - dma_queue_push(dmaq, dma_make_ptr(src0_vtcm_data + (vtcm_idx * src0_half), data_src + soff), tb, tb, tb, 1);\ - } \ - \ - uint32_t row = src0_start_row; \ - uint32_t col = 0; \ - uint32_t tile_in_row = 0; \ - uint32_t i01 = fastmodulo(row, ne01, div_ne01); \ - \ - uint32_t prow = src0_start_row + fastdiv(2, div_tpr); \ - uint32_t pcol = fastmodulo(2, tiles_per_row, div_tpr) * col_tile; \ - uint32_t ptile_in_row = fastmodulo(2, tiles_per_row, div_tpr); \ - \ - for (uint32_t t = 0; t < total_tiles; t++) { \ - uint8_t * dst_vtcm = (uint8_t *) dma_queue_pop(dmaq).src; \ - uint8_t * src_vtcm = (uint8_t *) dma_queue_pop(dmaq).dst; \ - \ - const uint32_t tw = MIN(col_tile, ne0 - col); \ - \ - htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, t); \ - CORE_TILE_EXPR; \ - htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, t); \ - \ - const size_t doff = (dst_contig ? (row * nb1) : \ - unary_row_offset(row, ne1, ne2, div_ne01, div_ne02, div_ne012, nb1, nb2, nb3)) + \ - (size_t) col * sizeof(float); \ - const size_t tb = (size_t) tw * sizeof(float); \ - dma_queue_push(dmaq, dma_make_ptr(data_dst + doff, dst_vtcm), tb, tb, tb, 1); \ - \ - const uint32_t pt = t + 2; \ - if (pt < total_tiles) { \ - const uint32_t ptw = MIN(col_tile, ne0 - pcol); \ - const size_t ptb = (size_t) ptw * sizeof(float); \ - const size_t psoff = (src0_contig ? (prow * nb01) : \ - unary_row_offset(prow, ne01, ne02, div_ne01, div_ne02, div_ne012, nb01, nb02, \ - nb03)) + \ - (size_t) pcol * sizeof(float); \ - dma_queue_push(dmaq, dma_make_ptr(src_vtcm, data_src + psoff), ptb, ptb, ptb, 1); \ - } \ - \ - tile_in_row++; \ - col += col_tile; \ - if (tile_in_row == tiles_per_row) { \ - tile_in_row = 0; \ - col = 0; \ - row++; \ - i01++; \ - if (i01 == ne01) { \ - i01 = 0; \ - } \ - } \ - \ - ptile_in_row++; \ - pcol += col_tile; \ - if (ptile_in_row == tiles_per_row) { \ - ptile_in_row = 0; \ - pcol = 0; \ - prow++; \ - } \ - } \ - \ - dma_queue_flush(dmaq); \ +#define DEFINE_UNARY_TILED_TASK(NAME, IS_TRI, CORE_TILE_EXPR) \ +static void unary_task_f32_tiled_##NAME(unsigned int nth, unsigned int ith, void * data) { \ + const struct htp_unary_context * uctx = (const struct htp_unary_context *) data; \ + struct htp_ops_context * octx = uctx->octx; \ + const struct htp_tensor * src = octx->src[0]; \ + const struct htp_tensor * dst = octx->dst; \ + struct htp_thread_trace * tr = &octx->ctx->trace[ith]; \ + \ + htp_unary_preamble; \ + \ + uint32_t src0_nrows_per_thread = uctx->src0_nrows_per_thread; \ + \ + int32_t * op_params = octx->op_params; \ + const uint32_t col_tile = uctx->col_tile; \ + \ + const uint32_t src0_nrows = uctx->src0_nrows; \ + const uint32_t src0_start_row = uctx->row_start + src0_nrows_per_thread * ith; \ + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, uctx->row_start + src0_nrows); \ + \ + if (src0_start_row >= src0_end_row) { \ + return; \ + } \ + \ + const uint8_t * restrict data_src = uctx->data_src0; \ + uint8_t * restrict data_dst = uctx->data_dst; \ + \ + uint8_t * src0_vtcm_data = uctx->vtcm_src0 + (ith * uctx->vtcm_src0_size_per_thread); \ + uint8_t * dst_vtcm_data = uctx->vtcm_dst + (ith * uctx->vtcm_dst_size_per_thread); \ + \ + const size_t src0_half = uctx->src0_vtcm_half_size; \ + const size_t dst_half = uctx->dst_vtcm_half_size; \ + \ + dma_queue * dmaq = octx->ctx->dma[ith]; \ + \ + const struct fastdiv_values * div_ne01 = &uctx->kparams->div_ne01; \ + const struct fastdiv_values * div_ne02 = &uctx->kparams->div_ne02; \ + const struct fastdiv_values * div_ne012 = &uctx->kparams->div_ne012; \ + const struct fastdiv_values * div_tpr = &uctx->kparams->div_tpr; \ + \ + const uint32_t tiles_per_row = (ne0 + col_tile - 1) / col_tile; \ + const int32_t tri_ttype = (IS_TRI) ? op_params[0] : 0; \ + \ + const bool src0_contig = (nb02 == (size_t)ne01 * nb01) && \ + (nb03 == (size_t)ne02 * nb02); \ + const bool dst_contig = (nb2 == (size_t)ne1 * nb1) && \ + (nb3 == (size_t)ne2 * nb2); \ + \ + const uint32_t total_tiles = (src0_end_row - src0_start_row) * tiles_per_row; \ + \ + for (uint32_t t = 0, vtcm_idx = 0; t < total_tiles && vtcm_idx < 2; t++, vtcm_idx++) { \ + const uint32_t row = src0_start_row + t / tiles_per_row; \ + const uint32_t col = (t % tiles_per_row) * col_tile; \ + const uint32_t tw = MIN(col_tile, ne0 - col); \ + const size_t tb = (size_t) tw * sizeof(float); \ + const size_t soff = (src0_contig ? (row * nb01) : \ + unary_row_offset(row, ne01, ne02, div_ne01, div_ne02, div_ne012, nb01, nb02, nb03)) + \ + (size_t) col * sizeof(float); \ + \ + dma_queue_push(dmaq, dma_make_ptr(data_dst, dst_vtcm_data + (vtcm_idx * dst_half)), 0, 0, 0, 0); \ + dma_queue_push(dmaq, dma_make_ptr(src0_vtcm_data + (vtcm_idx * src0_half), data_src + soff), tb, tb, tb, 1); \ + } \ + \ + uint32_t row = src0_start_row; \ + uint32_t col = 0; \ + uint32_t tile_in_row = 0; \ + uint32_t i01 = fastmodulo(row, ne01, div_ne01); \ + \ + uint32_t prow = src0_start_row + fastdiv(2, div_tpr); \ + uint32_t pcol = fastmodulo(2, tiles_per_row, div_tpr) * col_tile; \ + uint32_t ptile_in_row = fastmodulo(2, tiles_per_row, div_tpr); \ + \ + for (uint32_t t = 0; t < total_tiles; t++) { \ + uint8_t * dst_vtcm = (uint8_t *) dma_queue_pop(dmaq).src; \ + uint8_t * src_vtcm = (uint8_t *) dma_queue_pop(dmaq).dst; \ + \ + const uint32_t tw = MIN(col_tile, ne0 - col); \ + \ + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, t); \ + CORE_TILE_EXPR; \ + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, t); \ + \ + const size_t doff = (dst_contig ? (row * nb1) : \ + unary_row_offset(row, ne1, ne2, div_ne01, div_ne02, div_ne012, nb1, nb2, nb3)) + \ + (size_t) col * sizeof(float); \ + const size_t tb = (size_t) tw * sizeof(float); \ + dma_queue_push(dmaq, dma_make_ptr(data_dst + doff, dst_vtcm), tb, tb, tb, 1); \ + \ + const uint32_t pt = t + 2; \ + if (pt < total_tiles) { \ + const uint32_t ptw = MIN(col_tile, ne0 - pcol); \ + const size_t ptb = (size_t) ptw * sizeof(float); \ + const size_t psoff = (src0_contig ? (prow * nb01) : \ + unary_row_offset(prow, ne01, ne02, div_ne01, div_ne02, div_ne012, nb01, nb02, \ + nb03)) + \ + (size_t) pcol * sizeof(float); \ + dma_queue_push(dmaq, dma_make_ptr(src_vtcm, data_src + psoff), ptb, ptb, ptb, 1); \ + } \ + \ + tile_in_row++; \ + col += col_tile; \ + if (tile_in_row == tiles_per_row) { \ + tile_in_row = 0; \ + col = 0; \ + row++; \ + i01++; \ + if (i01 == ne01) { \ + i01 = 0; \ + } \ + } \ + \ + ptile_in_row++; \ + pcol += col_tile; \ + if (ptile_in_row == tiles_per_row) { \ + ptile_in_row = 0; \ + pcol = 0; \ + prow++; \ + } \ + } \ + \ + dma_queue_flush(dmaq); \ } static inline void tile_scale_f32(uint8_t * dst_vtcm, const uint8_t * src_vtcm, uint32_t tw, const int32_t * op_params) { @@ -1146,14 +1149,32 @@ static int execute_op_unary(struct htp_ops_context * octx) { const struct htp_unary_kernel_params * kparams = (const struct htp_unary_kernel_params *) octx->kernel_params; - const uint32_t src0_nrows = src0->ne[1] * src0->ne[2] * src0->ne[3]; - const uint32_t n_threads = kparams->n_threads; + if (!htp_ops_context_set_n_threads(octx, kparams->n_threads)) { + return HTP_STATUS_INVAL_PARAMS; + } + const uint32_t src0_nrows = src0->ne[1] * src0->ne[2] * src0->ne[3]; const size_t elem_size = is_f16 ? sizeof(_Float16) : sizeof(float); - const size_t src0_data_row_size = src0->ne[0] * elem_size; const size_t dst_data_row_size = dst->ne[0] * elem_size; + uint32_t row_start = 0; + uint32_t nrows = src0_nrows; + + if (octx->ctx->mdev.count > 1) { + uint32_t rows_per_chunk = 0; + htp_tensor_mdev_rows_per_chunk(dst, (uint32_t) elem_size, (uint32_t) dst_data_row_size, &rows_per_chunk); + const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition(src0_nrows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); + row_start = range.start; + nrows = range.count; + } + + if (nrows == 0) { + return HTP_STATUS_OK; + } + + const uint32_t n_threads = octx->n_threads; + const size_t src0_row_size_aligned = kparams->src0_row_size_aligned; const size_t dst_row_size_aligned = kparams->dst_row_size_aligned; @@ -1191,8 +1212,9 @@ static int execute_op_unary(struct htp_ops_context * octx) { struct htp_unary_context uctx = { .octx = octx, .kparams = kparams, - .src0_nrows_per_thread = (src0_nrows + n_threads - 1) / n_threads, - .src0_nrows = src0_nrows, + .src0_nrows_per_thread = fastdiv(nrows + n_threads - 1, &octx->n_threads_div), + .src0_nrows = nrows, + .row_start = row_start, .data_src0 = (const uint8_t *)src0->data, .data_src1 = (octx->op == HTP_OP_RMS_NORM_MUL) ? (const uint8_t *)src1->data : NULL, @@ -1287,7 +1309,7 @@ static int execute_op_unary(struct htp_ops_context * octx) { } if (task_func) { - worker_pool_run_func(octx->ctx->worker_pool, task_func, &uctx, n_threads); + work_queue_run(octx->ctx->work_queue, task_func, &uctx, n_threads); } else { FARF(ERROR, "execute_op_unary: task function is NULL for op %d\n", octx->op); err = HTP_STATUS_NO_SUPPORT; diff --git a/ggml/src/ggml-metal/ggml-metal-ops.cpp b/ggml/src/ggml-metal/ggml-metal-ops.cpp index b4e87cb2cc2a..da0040a0cf8a 100644 --- a/ggml/src/ggml-metal/ggml-metal-ops.cpp +++ b/ggml/src/ggml-metal/ggml-metal-ops.cpp @@ -2923,6 +2923,7 @@ static int ggml_metal_op_flash_attn_ext_n_kv_max_sparse(const ggml_tensor * op) const bool dk_dv_ok = (dk == 32 && dv == 32) || (dk == 64 && dv == 64) || (dk == 96 && dv == 96) || + (dk == 96 && dv == 64) || (dk == 128 && dv == 128) || (dk == 192 && dv == 128) || (dk == 192 && dv == 192) || diff --git a/ggml/src/ggml-metal/ggml-metal-tuning.cpp b/ggml/src/ggml-metal/ggml-metal-tuning.cpp index 2323269c4e62..a1d28638ac67 100644 --- a/ggml/src/ggml-metal/ggml-metal-tuning.cpp +++ b/ggml/src/ggml-metal/ggml-metal-tuning.cpp @@ -34,6 +34,9 @@ int fa_vec_baseline_ne(int dk, int dv) { if (dk == 96 && dv == 96) { return 4; } + if (dk == 96 && dv == 64) { + return 4; + } if (dk == 128 && dv == 128) { return 1; } diff --git a/ggml/src/ggml-metal/kernels/fa.metal b/ggml/src/ggml-metal/kernels/fa.metal index d0e928d732cc..71e6e373eeb1 100644 --- a/ggml/src/ggml-metal/kernels/fa.metal +++ b/ggml/src/ggml-metal/kernels/fa.metal @@ -930,6 +930,7 @@ template [[host_name("kernel_flash_attn_ext_f32_dk64_dv64" )]] kernel flash_at template [[host_name("kernel_flash_attn_ext_f32_dk72_dv72" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_F32, float4x4, 1, dequantize_f32, float4x4, 1, dequantize_f32, 72, 72>; template [[host_name("kernel_flash_attn_ext_f32_dk80_dv80" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_F32, float4x4, 1, dequantize_f32, float4x4, 1, dequantize_f32, 80, 80>; template [[host_name("kernel_flash_attn_ext_f32_dk96_dv96" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_F32, float4x4, 1, dequantize_f32, float4x4, 1, dequantize_f32, 96, 96>; +template [[host_name("kernel_flash_attn_ext_f32_dk96_dv64" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_F32, float4x4, 1, dequantize_f32, float4x4, 1, dequantize_f32, 96, 64>; template [[host_name("kernel_flash_attn_ext_f32_dk112_dv112")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_F32, float4x4, 1, dequantize_f32, float4x4, 1, dequantize_f32, 112, 112>; template [[host_name("kernel_flash_attn_ext_f32_dk128_dv128")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_F32, float4x4, 1, dequantize_f32, float4x4, 1, dequantize_f32, 128, 128>; template [[host_name("kernel_flash_attn_ext_f32_dk192_dv192")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_F32, float4x4, 1, dequantize_f32, float4x4, 1, dequantize_f32, 192, 192>; @@ -946,6 +947,7 @@ template [[host_name("kernel_flash_attn_ext_f16_dk64_dv64" )]] kernel flash_at template [[host_name("kernel_flash_attn_ext_f16_dk72_dv72" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, half4x4, 1, dequantize_f16, half4x4, 1, dequantize_f16, 72, 72>; template [[host_name("kernel_flash_attn_ext_f16_dk80_dv80" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, half4x4, 1, dequantize_f16, half4x4, 1, dequantize_f16, 80, 80>; template [[host_name("kernel_flash_attn_ext_f16_dk96_dv96" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, half4x4, 1, dequantize_f16, half4x4, 1, dequantize_f16, 96, 96>; +template [[host_name("kernel_flash_attn_ext_f16_dk96_dv64" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, half4x4, 1, dequantize_f16, half4x4, 1, dequantize_f16, 96, 64>; template [[host_name("kernel_flash_attn_ext_f16_dk112_dv112")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, half4x4, 1, dequantize_f16, half4x4, 1, dequantize_f16, 112, 112>; template [[host_name("kernel_flash_attn_ext_f16_dk128_dv128")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, half4x4, 1, dequantize_f16, half4x4, 1, dequantize_f16, 128, 128>; template [[host_name("kernel_flash_attn_ext_f16_dk192_dv192")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, half4x4, 1, dequantize_f16, half4x4, 1, dequantize_f16, 192, 192>; @@ -963,6 +965,7 @@ template [[host_name("kernel_flash_attn_ext_bf16_dk64_dv64" )]] kernel flash_at template [[host_name("kernel_flash_attn_ext_bf16_dk72_dv72" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_BF, bfloat4x4, 1, dequantize_bf16, bfloat4x4, 1, dequantize_bf16, 72, 72>; template [[host_name("kernel_flash_attn_ext_bf16_dk80_dv80" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_BF, bfloat4x4, 1, dequantize_bf16, bfloat4x4, 1, dequantize_bf16, 80, 80>; template [[host_name("kernel_flash_attn_ext_bf16_dk96_dv96" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_BF, bfloat4x4, 1, dequantize_bf16, bfloat4x4, 1, dequantize_bf16, 96, 96>; +template [[host_name("kernel_flash_attn_ext_bf16_dk96_dv64" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_BF, bfloat4x4, 1, dequantize_bf16, bfloat4x4, 1, dequantize_bf16, 96, 64>; template [[host_name("kernel_flash_attn_ext_bf16_dk112_dv112")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_BF, bfloat4x4, 1, dequantize_bf16, bfloat4x4, 1, dequantize_bf16, 112, 112>; template [[host_name("kernel_flash_attn_ext_bf16_dk128_dv128")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_BF, bfloat4x4, 1, dequantize_bf16, bfloat4x4, 1, dequantize_bf16, 128, 128>; template [[host_name("kernel_flash_attn_ext_bf16_dk192_dv192")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES_BF, bfloat4x4, 1, dequantize_bf16, bfloat4x4, 1, dequantize_bf16, 192, 192>; @@ -980,6 +983,7 @@ template [[host_name("kernel_flash_attn_ext_q4_0_dk64_dv64" )]] kernel flash_at template [[host_name("kernel_flash_attn_ext_q4_0_dk72_dv72" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_0, 2, dequantize_q4_0, block_q4_0, 2, dequantize_q4_0, 72, 72>; template [[host_name("kernel_flash_attn_ext_q4_0_dk80_dv80" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_0, 2, dequantize_q4_0, block_q4_0, 2, dequantize_q4_0, 80, 80>; template [[host_name("kernel_flash_attn_ext_q4_0_dk96_dv96" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_0, 2, dequantize_q4_0, block_q4_0, 2, dequantize_q4_0, 96, 96>; +template [[host_name("kernel_flash_attn_ext_q4_0_dk96_dv64" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_0, 2, dequantize_q4_0, block_q4_0, 2, dequantize_q4_0, 96, 64>; template [[host_name("kernel_flash_attn_ext_q4_0_dk112_dv112")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_0, 2, dequantize_q4_0, block_q4_0, 2, dequantize_q4_0, 112, 112>; template [[host_name("kernel_flash_attn_ext_q4_0_dk128_dv128")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_0, 2, dequantize_q4_0, block_q4_0, 2, dequantize_q4_0, 128, 128>; template [[host_name("kernel_flash_attn_ext_q4_0_dk192_dv192")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_0, 2, dequantize_q4_0, block_q4_0, 2, dequantize_q4_0, 192, 192>; @@ -996,6 +1000,7 @@ template [[host_name("kernel_flash_attn_ext_q4_1_dk64_dv64" )]] kernel flash_at template [[host_name("kernel_flash_attn_ext_q4_1_dk72_dv72" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_1, 2, dequantize_q4_1, block_q4_1, 2, dequantize_q4_1, 72, 72>; template [[host_name("kernel_flash_attn_ext_q4_1_dk80_dv80" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_1, 2, dequantize_q4_1, block_q4_1, 2, dequantize_q4_1, 80, 80>; template [[host_name("kernel_flash_attn_ext_q4_1_dk96_dv96" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_1, 2, dequantize_q4_1, block_q4_1, 2, dequantize_q4_1, 96, 96>; +template [[host_name("kernel_flash_attn_ext_q4_1_dk96_dv64" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_1, 2, dequantize_q4_1, block_q4_1, 2, dequantize_q4_1, 96, 64>; template [[host_name("kernel_flash_attn_ext_q4_1_dk112_dv112")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_1, 2, dequantize_q4_1, block_q4_1, 2, dequantize_q4_1, 112, 112>; template [[host_name("kernel_flash_attn_ext_q4_1_dk128_dv128")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_1, 2, dequantize_q4_1, block_q4_1, 2, dequantize_q4_1, 128, 128>; template [[host_name("kernel_flash_attn_ext_q4_1_dk192_dv192")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q4_1, 2, dequantize_q4_1, block_q4_1, 2, dequantize_q4_1, 192, 192>; @@ -1012,6 +1017,7 @@ template [[host_name("kernel_flash_attn_ext_q5_0_dk64_dv64" )]] kernel flash_at template [[host_name("kernel_flash_attn_ext_q5_0_dk72_dv72" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_0, 2, dequantize_q5_0, block_q5_0, 2, dequantize_q5_0, 72, 72>; template [[host_name("kernel_flash_attn_ext_q5_0_dk80_dv80" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_0, 2, dequantize_q5_0, block_q5_0, 2, dequantize_q5_0, 80, 80>; template [[host_name("kernel_flash_attn_ext_q5_0_dk96_dv96" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_0, 2, dequantize_q5_0, block_q5_0, 2, dequantize_q5_0, 96, 96>; +template [[host_name("kernel_flash_attn_ext_q5_0_dk96_dv64" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_0, 2, dequantize_q5_0, block_q5_0, 2, dequantize_q5_0, 96, 64>; template [[host_name("kernel_flash_attn_ext_q5_0_dk112_dv112")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_0, 2, dequantize_q5_0, block_q5_0, 2, dequantize_q5_0, 112, 112>; template [[host_name("kernel_flash_attn_ext_q5_0_dk128_dv128")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_0, 2, dequantize_q5_0, block_q5_0, 2, dequantize_q5_0, 128, 128>; template [[host_name("kernel_flash_attn_ext_q5_0_dk192_dv192")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_0, 2, dequantize_q5_0, block_q5_0, 2, dequantize_q5_0, 192, 192>; @@ -1028,6 +1034,7 @@ template [[host_name("kernel_flash_attn_ext_q5_1_dk64_dv64" )]] kernel flash_at template [[host_name("kernel_flash_attn_ext_q5_1_dk72_dv72" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_1, 2, dequantize_q5_1, block_q5_1, 2, dequantize_q5_1, 72, 72>; template [[host_name("kernel_flash_attn_ext_q5_1_dk80_dv80" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_1, 2, dequantize_q5_1, block_q5_1, 2, dequantize_q5_1, 80, 80>; template [[host_name("kernel_flash_attn_ext_q5_1_dk96_dv96" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_1, 2, dequantize_q5_1, block_q5_1, 2, dequantize_q5_1, 96, 96>; +template [[host_name("kernel_flash_attn_ext_q5_1_dk96_dv64" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_1, 2, dequantize_q5_1, block_q5_1, 2, dequantize_q5_1, 96, 64>; template [[host_name("kernel_flash_attn_ext_q5_1_dk112_dv112")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_1, 2, dequantize_q5_1, block_q5_1, 2, dequantize_q5_1, 112, 112>; template [[host_name("kernel_flash_attn_ext_q5_1_dk128_dv128")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_1, 2, dequantize_q5_1, block_q5_1, 2, dequantize_q5_1, 128, 128>; template [[host_name("kernel_flash_attn_ext_q5_1_dk192_dv192")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q5_1, 2, dequantize_q5_1, block_q5_1, 2, dequantize_q5_1, 192, 192>; @@ -1044,6 +1051,7 @@ template [[host_name("kernel_flash_attn_ext_q8_0_dk64_dv64" )]] kernel flash_at template [[host_name("kernel_flash_attn_ext_q8_0_dk72_dv72" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q8_0, 2, dequantize_q8_0, block_q8_0, 2, dequantize_q8_0, 72, 72>; template [[host_name("kernel_flash_attn_ext_q8_0_dk80_dv80" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q8_0, 2, dequantize_q8_0, block_q8_0, 2, dequantize_q8_0, 80, 80>; template [[host_name("kernel_flash_attn_ext_q8_0_dk96_dv96" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q8_0, 2, dequantize_q8_0, block_q8_0, 2, dequantize_q8_0, 96, 96>; +template [[host_name("kernel_flash_attn_ext_q8_0_dk96_dv64" )]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q8_0, 2, dequantize_q8_0, block_q8_0, 2, dequantize_q8_0, 96, 64>; template [[host_name("kernel_flash_attn_ext_q8_0_dk112_dv112")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q8_0, 2, dequantize_q8_0, block_q8_0, 2, dequantize_q8_0, 112, 112>; template [[host_name("kernel_flash_attn_ext_q8_0_dk128_dv128")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q8_0, 2, dequantize_q8_0, block_q8_0, 2, dequantize_q8_0, 128, 128>; template [[host_name("kernel_flash_attn_ext_q8_0_dk192_dv192")]] kernel flash_attn_ext_t kernel_flash_attn_ext<FA_TYPES, block_q8_0, 2, dequantize_q8_0, block_q8_0, 2, dequantize_q8_0, 192, 192>; @@ -1905,6 +1913,29 @@ template [[host_name("kernel_flash_attn_ext_vec_q8_0_dk96_dv96")]] kernel flas template [[host_name("kernel_flash_attn_ext_vec_q8_0_dk96_dv96_q2_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q8_0, 8, dequantize_q8_0_t4, block_q8_0, 8, dequantize_q8_0_t4, 96, 96, 4, 2>; template [[host_name("kernel_flash_attn_ext_vec_q8_0_dk96_dv96_q4_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q8_0, 8, dequantize_q8_0_t4, block_q8_0, 8, dequantize_q8_0_t4, 96, 96, 4, 4>; +template [[host_name("kernel_flash_attn_ext_vec_f32_dk96_dv64")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES_F32, float4, 1, dequantize_f32_t4, float4, 1, dequantize_f32_t4, 96, 64, 4>; +template [[host_name("kernel_flash_attn_ext_vec_f16_dk96_dv64")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, half4, 1, dequantize_f16_t4, half4, 1, dequantize_f16_t4, 96, 64, 4>; +template [[host_name("kernel_flash_attn_ext_vec_f16_dk96_dv64_q2_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, half4, 1, dequantize_f16_t4, half4, 1, dequantize_f16_t4, 96, 64, 4, 2>; +template [[host_name("kernel_flash_attn_ext_vec_f16_dk96_dv64_q4_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, half4, 1, dequantize_f16_t4, half4, 1, dequantize_f16_t4, 96, 64, 4, 4>; +#if defined(GGML_METAL_HAS_BF16) +template [[host_name("kernel_flash_attn_ext_vec_bf16_dk96_dv64")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, bfloat4, 1, dequantize_bf16_t4, bfloat4, 1, dequantize_bf16_t4, 96, 64, 4>; +#endif +template [[host_name("kernel_flash_attn_ext_vec_q4_0_dk96_dv64")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q4_0, 8, dequantize_q4_0_t4, block_q4_0, 8, dequantize_q4_0_t4, 96, 64, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q4_0_dk96_dv64_q2_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q4_0, 8, dequantize_q4_0_t4, block_q4_0, 8, dequantize_q4_0_t4, 96, 64, 4, 2>; +template [[host_name("kernel_flash_attn_ext_vec_q4_0_dk96_dv64_q4_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q4_0, 8, dequantize_q4_0_t4, block_q4_0, 8, dequantize_q4_0_t4, 96, 64, 4, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q4_1_dk96_dv64")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q4_1, 8, dequantize_q4_1_t4, block_q4_1, 8, dequantize_q4_1_t4, 96, 64, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q4_1_dk96_dv64_q2_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q4_1, 8, dequantize_q4_1_t4, block_q4_1, 8, dequantize_q4_1_t4, 96, 64, 4, 2>; +template [[host_name("kernel_flash_attn_ext_vec_q4_1_dk96_dv64_q4_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q4_1, 8, dequantize_q4_1_t4, block_q4_1, 8, dequantize_q4_1_t4, 96, 64, 4, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q5_0_dk96_dv64")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q5_0, 8, dequantize_q5_0_t4, block_q5_0, 8, dequantize_q5_0_t4, 96, 64, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q5_0_dk96_dv64_q2_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q5_0, 8, dequantize_q5_0_t4, block_q5_0, 8, dequantize_q5_0_t4, 96, 64, 4, 2>; +template [[host_name("kernel_flash_attn_ext_vec_q5_0_dk96_dv64_q4_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q5_0, 8, dequantize_q5_0_t4, block_q5_0, 8, dequantize_q5_0_t4, 96, 64, 4, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q5_1_dk96_dv64")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q5_1, 8, dequantize_q5_1_t4, block_q5_1, 8, dequantize_q5_1_t4, 96, 64, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q5_1_dk96_dv64_q2_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q5_1, 8, dequantize_q5_1_t4, block_q5_1, 8, dequantize_q5_1_t4, 96, 64, 4, 2>; +template [[host_name("kernel_flash_attn_ext_vec_q5_1_dk96_dv64_q4_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q5_1, 8, dequantize_q5_1_t4, block_q5_1, 8, dequantize_q5_1_t4, 96, 64, 4, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q8_0_dk96_dv64")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q8_0, 8, dequantize_q8_0_t4, block_q8_0, 8, dequantize_q8_0_t4, 96, 64, 4>; +template [[host_name("kernel_flash_attn_ext_vec_q8_0_dk96_dv64_q2_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q8_0, 8, dequantize_q8_0_t4, block_q8_0, 8, dequantize_q8_0_t4, 96, 64, 4, 2>; +template [[host_name("kernel_flash_attn_ext_vec_q8_0_dk96_dv64_q4_ne4")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, block_q8_0, 8, dequantize_q8_0_t4, block_q8_0, 8, dequantize_q8_0_t4, 96, 64, 4, 4>; + template [[host_name("kernel_flash_attn_ext_vec_f32_dk128_dv128")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES_F32, float4, 1, dequantize_f32_t4, float4, 1, dequantize_f32_t4, 128, 128, 1>; template [[host_name("kernel_flash_attn_ext_vec_f16_dk128_dv128")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, half4, 1, dequantize_f16_t4, half4, 1, dequantize_f16_t4, 128, 128, 1>; template [[host_name("kernel_flash_attn_ext_vec_f16_dk128_dv128_q1_ne2")]] kernel flash_attn_ext_vec_t kernel_flash_attn_ext_vec<FA_TYPES, half4, 1, dequantize_f16_t4, half4, 1, dequantize_f16_t4, 128, 128, 2, 1>; diff --git a/ggml/src/ggml-opencl/CMakeLists.txt b/ggml/src/ggml-opencl/CMakeLists.txt index 716577bb77e0..45a7075b291f 100644 --- a/ggml/src/ggml-opencl/CMakeLists.txt +++ b/ggml/src/ggml-opencl/CMakeLists.txt @@ -185,6 +185,7 @@ set(GGML_OPENCL_KERNELS gemv_noshuffle_q4_k_f32_o4 gemv_noshuffle_q4_k_f32_tiled gemm_noshuffle_q4_k_f32 + gemv_noshuffle_q4_k_f32_32b_trans gemv_noshuffle_q6_k_f32 gemv_noshuffle_q6_k_f32_o4 gemv_noshuffle_q6_k_f32_tiled diff --git a/ggml/src/ggml-opencl/ggml-opencl.cpp b/ggml/src/ggml-opencl/ggml-opencl.cpp index 231be2cf3ac4..5b99f5d00536 100644 --- a/ggml/src/ggml-opencl/ggml-opencl.cpp +++ b/ggml/src/ggml-opencl/ggml-opencl.cpp @@ -203,39 +203,67 @@ static ggml_cl_version get_opencl_platform_version(cl_platform_id platform) { return parse_cl_version(param_value); } +// Returns the DEVICE's OpenCL version. On an error returns ggml_cl_version with all zeroes. +static ggml_cl_version get_opencl_device_version(cl_device_id device) { + size_t param_size; + if (clGetDeviceInfo(device, CL_DEVICE_VERSION, 0, nullptr, ¶m_size) != CL_SUCCESS || !param_size) { + return {}; + } + std::unique_ptr<char[]> param_storage(new char[param_size]); + if (clGetDeviceInfo(device, CL_DEVICE_VERSION, param_size, param_storage.get(), nullptr) != CL_SUCCESS) { + return {}; + } + + auto param_value = std::string_view(param_storage.get(), param_size); + const std::string version_prefix = "OpenCL "; // "OpenCL <major>.<minor> <device-specific-info>" + if (param_value.find(version_prefix) != 0) { + return {}; + } + param_value.remove_prefix(version_prefix.length()); + return parse_cl_version(param_value); +} + // Return a version to use in OpenCL C compilation. On an error returns ggml_cl_version with all zeroes. static ggml_cl_version get_opencl_c_version(ggml_cl_version platform_version, cl_device_id device) { size_t param_size; #if CL_TARGET_OPENCL_VERSION >= 300 - if (platform_version.major >= 3) { - CL_CHECK(clGetDeviceInfo(device, CL_DEVICE_OPENCL_C_ALL_VERSIONS, 0, nullptr, ¶m_size)); - if (!param_size) { - return {}; - } + // CL_DEVICE_OPENCL_C_ALL_VERSIONS is an OpenCL 3.0 *device* query, so gating it on the + // *platform* version is not enough: a 3.0 platform can expose 2.0 devices, where the + // query returns CL_INVALID_VALUE and the old CL_CHECK aborted during backend init. + // Gate on the device version, and treat a failure as "fall back to the legacy query" + // rather than fatal -- a device may advertise 3.0 and still refuse the property. + const ggml_cl_version device_version = get_opencl_device_version(device); + if (platform_version.major >= 3 && device_version.major >= 3) { + cl_int err = clGetDeviceInfo(device, CL_DEVICE_OPENCL_C_ALL_VERSIONS, 0, nullptr, ¶m_size); + if (err == CL_SUCCESS && param_size) { + std::unique_ptr<cl_name_version[]> versions(new cl_name_version[param_size]); + err = clGetDeviceInfo(device, CL_DEVICE_OPENCL_C_ALL_VERSIONS, param_size, versions.get(), nullptr); + if (err == CL_SUCCESS) { + unsigned versions_count = param_size / sizeof(cl_name_version); - std::unique_ptr<cl_name_version[]> versions(new cl_name_version[param_size]); - CL_CHECK(clGetDeviceInfo(device, CL_DEVICE_OPENCL_C_ALL_VERSIONS, param_size, versions.get(), nullptr)); - unsigned versions_count = param_size / sizeof(cl_name_version); + cl_version version_max = 0; + for (unsigned i = 0; i < versions_count; i++) { + version_max = std::max<cl_version>(versions[i].version, version_max); + } - cl_version version_max = 0; - for (unsigned i = 0; i < versions_count; i++) { - version_max = std::max<cl_version>(versions[i].version, version_max); + return { CL_VERSION_MAJOR(version_max), CL_VERSION_MINOR(version_max) }; + } } - - return { CL_VERSION_MAJOR(version_max), CL_VERSION_MINOR(version_max) }; + // fall through to CL_DEVICE_OPENCL_C_VERSION below } #else GGML_UNUSED(platform_version); #endif // CL_TARGET_OPENCL_VERSION >= 300 - CL_CHECK(clGetDeviceInfo(device, CL_DEVICE_OPENCL_C_VERSION, 0, nullptr, ¶m_size)); - if (!param_size) { + if (clGetDeviceInfo(device, CL_DEVICE_OPENCL_C_VERSION, 0, nullptr, ¶m_size) != CL_SUCCESS || !param_size) { return {}; } std::unique_ptr<char[]> param_storage(new char[param_size]); - CL_CHECK(clGetDeviceInfo(device, CL_DEVICE_OPENCL_C_VERSION, param_size, param_storage.get(), nullptr)); + if (clGetDeviceInfo(device, CL_DEVICE_OPENCL_C_VERSION, param_size, param_storage.get(), nullptr) != CL_SUCCESS) { + return {}; + } auto param_value = std::string_view(param_storage.get(), param_size); const std::string version_prefix = "OpenCL C "; // Suffix: "XX.YY <platform-specific-info>" @@ -954,6 +982,7 @@ struct ggml_backend_opencl_context { // [size_idx][kda][tgpp] where size_idx: 0=S_V=16, 1=32, 2=64, 3=128; kda: 0 or 1. // tgpp 0 = TG variant (COLS_PER_LANE_GROUP=1), tgpp 1 = prefill variant (COLS_PER_LANE_GROUP=4). cl_kernel kernel_gated_delta_net_f32[4][2][2] = {}; + cl_kernel kernel_ssm_scan_f32 = nullptr; cl_kernel kernel_ssm_scan_f32_mamba2_d128 = nullptr; cl_kernel kernel_ssm_scan_f32_mamba2_d256 = nullptr; @@ -1115,6 +1144,18 @@ struct ggml_backend_opencl_context { } void enqueue_ndrange_kernel(cl_kernel kernel, cl_uint work_dim, size_t *global_work_size, size_t *local_work_size, const ggml_tensor * tensor) { + // From the spec on clEnqueueNDRangeKernel: + // If the device associated with command_queue is an OpenCL 2.1 or newer device, + // and global_work_size is NULL or the value in any passed dimension is zero, + // then the kernel command will trivially succeed after its event dependencies + // are satisfied and subsequently update its completion event. + // So this ensures such cases always return trivially without causing errors in + // case of an older device. + for (cl_uint i = 0; i < work_dim; i++) { + if (global_work_size[i] == 0) { + return; + } + } #ifdef GGML_OPENCL_PROFILING cl_event evt; CL_CHECK(clEnqueueNDRangeKernel(queue, kernel, work_dim, NULL, global_work_size, local_work_size, 0, NULL, &evt)); @@ -1185,6 +1226,8 @@ struct ggml_backend_opencl_context { cl_kernel kernel_convert_block_q4_k_tiled_ns; // tiled-wide convert (opt-in) cl_kernel kernel_gemv_noshuffle_q4_k_f32_mc3; // multi-column (N=3) verify GEMV cl_kernel kernel_gemm_noshuffle_q4_k_f32; + cl_kernel kernel_gemm_noshuffle_q4_k_f32_32b_trans_ila_a8_bin; + cl_kernel kernel_gemv_noshuffle_q4_k_f32_32b_trans; cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a = nullptr; // dp4a (int8) dense prefill GEMM cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg = nullptr; // dp4a dense prefill GEMM, weights via texture (X1 opt-in) cl_kernel kernel_gemm_noshuffle_q5_k_q8_1_dp4a = nullptr; // dp4a (int8) dense q5_K prefill GEMM @@ -3415,7 +3458,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { GGML_LOG_CONT("."); } - // ssm_scan (Mamba-2 fused per-token recurrent step; d_state in {128, 256}) + // ssm_scan { #ifdef GGML_OPENCL_EMBED_KERNELS const std::string kernel_src { @@ -3427,8 +3470,34 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); + CL_CHECK((backend_ctx->kernel_ssm_scan_f32 = clCreateKernel(prog, "kernel_ssm_scan_f32", &err), err)); CL_CHECK((backend_ctx->kernel_ssm_scan_f32_mamba2_d128 = clCreateKernel(prog, "kernel_ssm_scan_f32_mamba2_d128", &err), err)); CL_CHECK((backend_ctx->kernel_ssm_scan_f32_mamba2_d256 = clCreateKernel(prog, "kernel_ssm_scan_f32_mamba2_d256", &err), err)); + + cl_kernel * kernels[] = { + &backend_ctx->kernel_ssm_scan_f32_mamba2_d128, + &backend_ctx->kernel_ssm_scan_f32_mamba2_d256 + }; + + // specialized kernels use subgroups and assume subgroup size is 64, + // if device does not support subgroups or subgroup size is not 64, + // release these kernels + for (int i = 0; i < 2; ++i) { + size_t subgroup_size = 0; +#if CL_TARGET_OPENCL_VERSION >= 210 + const size_t local_work_size[] = { 64, 1 }; + const cl_int subgroup_err = clGetKernelSubGroupInfo(*kernels[i], backend_ctx->device, CL_KERNEL_MAX_SUB_GROUP_SIZE_FOR_NDRANGE, + sizeof(local_work_size), local_work_size, sizeof(subgroup_size), &subgroup_size, nullptr); + if (subgroup_err != CL_SUCCESS) { + subgroup_size = 0; + } +#endif + // The specialized kernels reduce over one 64-lane subgroup. + if (subgroup_size != 64) { + CL_CHECK(clReleaseKernel(*kernels[i])); + *kernels[i] = nullptr; + } + } CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); } @@ -4260,6 +4329,43 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { GGML_LOG_CONT("."); } + backend_ctx->kernel_gemv_noshuffle_q4_k_f32_32b_trans = nullptr; + backend_ctx->kernel_gemm_noshuffle_q4_k_f32_32b_trans_ila_a8_bin = nullptr; + if (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E) { + { + std::string opts = std::string("-cl-std=") + opencl_c_std + + " -cl-mad-enable " + " -DSIMDGROUP_WIDTH=" + + std::to_string(backend_ctx->adreno_wave_size); +#ifdef GGML_OPENCL_EMBED_KERNELS + const std::string kernel_src { + #include "gemv_noshuffle_q4_k_f32_32b_trans.cl.h" + }; +#else + const std::string kernel_src = read_file("gemv_noshuffle_q4_k_f32_32b_trans.cl"); +#endif + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), opts); + CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q4_k_f32_32b_trans = + clCreateKernel(prog, "gemv_noshuffle_q4_k_f32_32b_trans", &err), err)); + CL_CHECK(clReleaseProgram(prog)); + GGML_LOG_CONT("."); + } + + if (use_adreno_bin_kernels(backend_ctx)) { + size_t bin_size = 0; + const char * kernel_bin = (const char *)backend_ctx->get_adreno_bin_kernel("gemm_noshuffle_q4_k_f32_32b_trans_ila_a8", &bin_size); + if (kernel_bin && bin_size > 0) { + cl_program bin_prog = + build_program_from_binary(backend_ctx->context, backend_ctx->device, kernel_bin, "", bin_size); + + CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_k_f32_32b_trans_ila_a8_bin = + clCreateKernel(bin_prog, "kernel_gemm_noshuffle_q4_k_f32_32b_trans_ila_a8", &err), err)); + CL_CHECK(clReleaseProgram(bin_prog)); + GGML_LOG_CONT("."); + } + } + } + std::string CL_moe_compile_opts = std::string("-cl-std=") + opencl_c_std + " -cl-mad-enable " " -cl-fast-relaxed-math"; @@ -7722,6 +7828,7 @@ static void ggml_cl_moe_combine_fused(ggml_backend_t backend, const ggml_tensor } inline bool use_q4k_tiled(const ggml_backend_opencl_context *backend_ctx, const ggml_tensor *tensor); // defined below (used by the GLU-subgraph fuse check) +inline bool use_q4_k_bin_kernels(const ggml_backend_opencl_context *backend_ctx, const ggml_tensor *tensor); inline bool use_adreno_kernels(const ggml_backend_opencl_context *backend_ctx, const ggml_tensor *tensor); // defined below static bool ggml_opencl_can_fuse(const ggml_backend_opencl_context * backend_ctx, const struct ggml_cgraph * cgraph, int node_idx, std::initializer_list<enum ggml_op> ops) { @@ -7776,6 +7883,10 @@ static bool ggml_opencl_can_fuse(const ggml_backend_opencl_context * backend_ctx if (use_q4k_tiled(backend_ctx, gate->src[0]) || use_q4k_tiled(backend_ctx, up->src[0])) { return false; } + // q4_K bin kernel requires 32b transposed layout, not compatible with the fused gemv + if (use_q4_k_bin_kernels(backend_ctx, gate->src[0]) || use_q4_k_bin_kernels(backend_ctx, up->src[0])) { + return false; + } // that noshuffle layout is only produced at set_tensor time when // use_adreno_kernels() accepts the weight (ne0 >= 512 && ne1 >= 512). // Smaller weights stay in the plain q4_K layout, which this kernel would @@ -8218,9 +8329,20 @@ inline bool use_adreno_kernels(const ggml_backend_opencl_context *backend_ctx, c bool threashold_ok = tensor->ne[0] >= threshold_ne0 && tensor->ne[1] >= threshold_ne1 && tensor->ne[2] == 1 && tensor->ne[3] == 1; - // q6_K adreno kernels requires ne1 is multiple of 128 - if (tensor->type == GGML_TYPE_Q6_K) { - return threashold_ok && tensor->ne[1] % 128 == 0; + // The noshuffle layout packs 2 rows per 32-bit texel and the GEMV reads it at an + // ne1/2 texel stride with an exact-cover dispatch, so it is only addressable when + // ne1 is a multiple of 64; an unaligned ne1 truncates the stride and the weight is + // read misaligned. That is a property of the layout, not of one quant -- q4_K, q5_K + // and q8_0 read the same packing as q6_K. The bound is 64, not 128: a q8_0 attention + // weight of ne1 = 2880 is a multiple of 64 but not 128 and is correct. + switch (tensor->type) { + case GGML_TYPE_Q4_K: + case GGML_TYPE_Q5_K: + case GGML_TYPE_Q6_K: + case GGML_TYPE_Q8_0: + return threashold_ok && tensor->ne[1] % 64 == 0; + default: + break; } return threashold_ok; } @@ -8349,7 +8471,7 @@ inline bool enable_adreno_trans_weight_q5_K(const ggml_backend_opencl_context *b qh_img_width <= backend_ctx->image_max_buffer_size; } -inline bool use_q4_0_ila_kernels(const ggml_backend_opencl_context *backend_ctx, const ggml_tensor *tensor) { +inline bool use_q4_0_bin_kernels(const ggml_backend_opencl_context *backend_ctx, const ggml_tensor *tensor) { #ifdef GGML_OPENCL_USE_ADRENO_KERNELS if (!backend_ctx->kernel_gemv_noshuffle_q4_0_f32_32b_trans || !backend_ctx->kernel_gemm_noshuffle_q4_0_f32_32b_trans_ila_a8_bin) { @@ -8442,6 +8564,21 @@ static inline bool use_flat_gemv_for_large_m_q6_K(const ggml_backend_opencl_cont && tensor->ne[2] == 1 && tensor->ne[3] == 1; } +inline bool use_q4_k_bin_kernels(const ggml_backend_opencl_context *backend_ctx, const ggml_tensor *tensor) { +#ifdef GGML_OPENCL_USE_ADRENO_KERNELS + if (!backend_ctx->kernel_gemv_noshuffle_q4_k_f32_32b_trans || + !backend_ctx->kernel_gemm_noshuffle_q4_k_f32_32b_trans_ila_a8_bin) { + return false; + } + return (tensor->ne[0] % 256 == 0) && (tensor->ne[1] % 64 == 0) && + !use_q4k_tiled(backend_ctx, tensor) && !use_flat_gemv_for_large_m_q4_K(backend_ctx, tensor); +#else + GGML_UNUSED(backend_ctx); + GGML_UNUSED(tensor); + return false; +#endif +} + static bool ggml_opencl_supports_op(ggml_backend_dev_t dev, const struct ggml_tensor * op) { ggml_backend_opencl_device_context * dev_ctx = (ggml_backend_opencl_device_context *)dev->context; ggml_backend_opencl_context * backend_ctx = dev_ctx->backend_ctx; @@ -8624,22 +8761,16 @@ static bool ggml_opencl_supports_op(ggml_backend_dev_t dev, const struct ggml_te case GGML_OP_SSM_CONV: return (op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32); case GGML_OP_SSM_SCAN: { - // Mamba-2 fused per-token scan. Requires src3->ne[0] == 1 (scalar - // A per head); d_state in {128, 256}; all sources f32. Falls back - // to CPU otherwise (incl. Mamba-1 element-wise A). - for (int i = 0; i < 6; ++i) { - if (op->src[i]->type != GGML_TYPE_F32) { + 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_F32 || + op->src[3]->type != GGML_TYPE_F32 || op->src[4]->type != GGML_TYPE_F32 || + op->src[5]->type != GGML_TYPE_F32 || op->src[6]->type != GGML_TYPE_I32) { return false; } + + const int64_t d_state = op->src[0]->ne[0]; + return d_state >= 1 && d_state <= 256 && (d_state & (d_state - 1)) == 0; } - if (op->type != GGML_TYPE_F32) { - return false; - } - const int K = ggml_get_op_params_i32(op, 0); - const int d_state = (int) op->src[0]->ne[0]; - const bool is_mamba2 = (op->src[3]->ne[0] == 1); - return is_mamba2 && (d_state == 128 || d_state == 256) && (K == 1); - } case GGML_OP_GATED_DELTA_NET: { // Match the Vulkan backend: only F32 -> F32, S_v in {16, 32, 64, 128}. @@ -9400,6 +9531,96 @@ static enum ggml_status ggml_backend_opencl_buffer_init_tensor(ggml_backend_buff return GGML_STATUS_SUCCESS; } +// Allocate a temporary upload buffer of `nbytes` and populate it with `data` +// from host. On Adreno X1-85 the device-only pool intermittently fails to +// allocate at hundreds of MB once model weights fragment the heap (observed +// on Qwen3.5-9B output.weight Q6_K at 834 MB). Three-step retry: +// 1. CL_MEM_READ_WRITE alloc + clEnqueueWriteBuffer (normal fast path). +// 2. clFinish + retry (drains in-flight allocs that may be holding heap; +// mirrors the proven pattern at the FD-split partial buffer alloc). +// 3. CL_MEM_ALLOC_HOST_PTR + map(WRITE_INVALIDATE) + memcpy + unmap — +// different memory pool (host-pinned); true zero-copy on Adreno per +// QCOM guidance. (CL_MEM_USE_HOST_PTR is NOT zero-copy on Adreno: the +// driver triggers an internal copy because arbitrary host pages aren't +// guaranteed mappable/coherent, AND it draws from the same exhausted +// device pool — so it doesn't solve the problem.) +// Returns the ready-to-read buffer (caller must clReleaseMemObject) or NULL +// if all three strategies fail. The buffer is opaque to the caller — it can +// be passed as a kernel argument like any normal cl_mem. +static cl_mem ggml_cl_create_temp_upload_buffer( + cl_context context, cl_command_queue queue, + size_t nbytes, const void * data, + const char * tensor_name_for_log) +{ + cl_int err; + cl_mem buf = clCreateBuffer(context, CL_MEM_READ_WRITE, nbytes, NULL, &err); + if (err != CL_SUCCESS) { + clFinish(queue); + buf = clCreateBuffer(context, CL_MEM_READ_WRITE, nbytes, NULL, &err); + } + if (err == CL_SUCCESS) { + const cl_int werr = clEnqueueWriteBuffer(queue, buf, CL_TRUE, 0, nbytes, data, 0, NULL, NULL); + if (werr == CL_SUCCESS) { + return buf; + } + clReleaseMemObject(buf); + } + buf = clCreateBuffer(context, + CL_MEM_READ_ONLY | CL_MEM_ALLOC_HOST_PTR | CL_MEM_HOST_WRITE_ONLY, + nbytes, NULL, &err); + if (err != CL_SUCCESS) { + return NULL; + } + void * mapped = clEnqueueMapBuffer(queue, buf, CL_TRUE, + CL_MAP_WRITE_INVALIDATE_REGION, 0, nbytes, 0, NULL, NULL, &err); + if (err != CL_SUCCESS) { + clReleaseMemObject(buf); + return NULL; + } + memcpy(mapped, data, nbytes); + const cl_int uerr = clEnqueueUnmapMemObject(queue, buf, mapped, 0, NULL, NULL); + if (uerr != CL_SUCCESS) { + clReleaseMemObject(buf); + return NULL; + } + if (tensor_name_for_log) { + GGML_LOG_INFO("ggml_opencl: %s (%.1f MiB) — device alloc failed, using CL_MEM_ALLOC_HOST_PTR fallback\n", + tensor_name_for_log, nbytes / 1024.0 / 1024.0); + } + return buf; +} + +// Allocate a temporary download buffer of `nbytes`. The caller runs a kernel +// that writes into it, then reads it back to host via clEnqueueReadBuffer (or +// equivalent). Mirrors ggml_cl_create_temp_upload_buffer; the host-pinned +// fallback flags are flipped (CL_MEM_WRITE_ONLY | HOST_READ_ONLY) and the +// helper doesn't populate the buffer. +static cl_mem ggml_cl_create_temp_download_buffer( + cl_context context, cl_command_queue queue, + size_t nbytes, const char * tensor_name_for_log) +{ + cl_int err; + cl_mem buf = clCreateBuffer(context, CL_MEM_READ_WRITE, nbytes, NULL, &err); + if (err != CL_SUCCESS) { + clFinish(queue); + buf = clCreateBuffer(context, CL_MEM_READ_WRITE, nbytes, NULL, &err); + } + if (err == CL_SUCCESS) { + return buf; + } + buf = clCreateBuffer(context, + CL_MEM_WRITE_ONLY | CL_MEM_ALLOC_HOST_PTR | CL_MEM_HOST_READ_ONLY, + nbytes, NULL, &err); + if (err != CL_SUCCESS) { + return NULL; + } + if (tensor_name_for_log) { + GGML_LOG_INFO("ggml_opencl: %s download (%.1f MiB) — device alloc failed, using CL_MEM_ALLOC_HOST_PTR fallback\n", + tensor_name_for_log, nbytes / 1024.0 / 1024.0); + } + return buf; +} + static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor, const void * data, size_t offset, size_t size) { ggml_backend_opencl_device_context * dev_ctx = (ggml_backend_opencl_device_context *) buffer->buft->device->context; ggml_backend_opencl_context * backend_ctx = dev_ctx->backend_ctx; @@ -9508,12 +9729,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(size_d + size_q == ggml_nbytes(tensor) && "Incorrect tensor size"); cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); - CL_CHECK(clEnqueueWriteBuffer( - queue, data_device, CL_TRUE, 0, - ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "set_tensor: temp upload buffer alloc failed"); // We consider the specified offset arg as always, although For weights // the offset arg should be 0 (we do not assert this). @@ -9625,7 +9842,7 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(K % 32 == 0); - if (use_q4_0_ila_kernels(backend_ctx, tensor)) { + if (use_q4_0_bin_kernels(backend_ctx, tensor)) { cl_int err; cl_image_format wimg_fmt; cl_image_desc wimg_desc; @@ -9671,12 +9888,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(size_d + size_m + size_q == ggml_nbytes(tensor) && "Incorrect tensor size"); cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); - CL_CHECK(clEnqueueWriteBuffer( - queue, data_device, CL_TRUE, 0, - ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "set_tensor: temp upload buffer alloc failed"); cl_buffer_region region; @@ -9803,12 +10016,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(size_d + size_qs + size_qh == ggml_nbytes(tensor) && "Incorrect tensor size"); cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); - CL_CHECK(clEnqueueWriteBuffer( - queue, data_device, CL_TRUE, 0, - ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "set_tensor: temp upload buffer alloc failed"); cl_buffer_region region; @@ -9967,12 +10176,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(size_d + size_m + size_qs + size_qh == ggml_nbytes(tensor) && "Incorrect tensor size"); cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); - CL_CHECK(clEnqueueWriteBuffer( - queue, data_device, CL_TRUE, 0, - ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "set_tensor: temp upload buffer alloc failed"); cl_buffer_region region; @@ -10120,12 +10325,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(size_e + size_q == ggml_nbytes(tensor) && "Incorrect tensor size"); cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); - CL_CHECK(clEnqueueWriteBuffer( - queue, data_device, CL_TRUE, 0, - ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "set_tensor: temp upload buffer alloc failed"); // The original tensor memory is divided into scales and quants, i.e., // we first store scales, then quants. @@ -10231,12 +10432,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(size_d + size_q == ggml_nbytes(tensor) && "Incorrect tensor size"); cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); - CL_CHECK(clEnqueueWriteBuffer( - queue, data_device, CL_TRUE, 0, - ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "set_tensor: temp upload buffer alloc failed"); // The original tensor memory is divided into scales and quants, i.e., // we first store scales, then quants. @@ -10335,12 +10532,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(size_d + size_q == ggml_nbytes(tensor) && "Incorrect tensor size"); cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); - CL_CHECK(clEnqueueWriteBuffer( - queue, data_device, CL_TRUE, 0, - ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "set_tensor: temp upload buffer alloc failed"); cl_buffer_region region; @@ -10419,12 +10612,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(size_d + size_dm + size_s + size_q == ggml_nbytes(tensor) && "Incorrect tensor size"); cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); - CL_CHECK(clEnqueueWriteBuffer( - queue, data_device, CL_TRUE, 0, - ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "q4_K set_tensor: temp upload buffer alloc failed"); cl_buffer_region region; @@ -10576,8 +10765,25 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, GGML_ASSERT(K % 32 == 0); - // Transpose q, d, dm as ushort - transpose_2d_as_16b(backend_ctx, extra->q, extra->q, size_q, K/4, M); + if (use_q4_k_bin_kernels(backend_ctx, tensor)) { + cl_int err; + cl_image_format wimg_fmt; + cl_image_desc wimg_desc; + + // transpose quants as 32-bit words (M-first) + GGML_ASSERT(M % 64 == 0); + transpose_2d_as_32b(backend_ctx, extra->q, extra->q, size_q, K/8, M); + + wimg_fmt = { CL_R, CL_UNSIGNED_INT32 }; + memset(&wimg_desc, 0, sizeof(wimg_desc)); + wimg_desc.image_type = CL_MEM_OBJECT_IMAGE1D_BUFFER; + wimg_desc.image_width = (size_t)M * K / 8; + wimg_desc.buffer = extra->q; + CL_CHECK((extra->q_img = clCreateImage(context, CL_MEM_READ_ONLY, &wimg_fmt, &wimg_desc, NULL, &err), err)); + } else { + // Transpose q as ushort + transpose_2d_as_16b(backend_ctx, extra->q, extra->q, size_q, K/4, M); + } transpose_2d_as_16b(backend_ctx, extra->d, extra->d, size_d, K/256, M); transpose_2d_as_16b(backend_ctx, extra->dm, extra->dm, size_dm, K/256, M); @@ -10604,9 +10810,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, "Incorrect tensor size"); cl_int err; - cl_mem data_device; - CL_CHECK((data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, ggml_nbytes(tensor), NULL, &err), err)); - CL_CHECK(clEnqueueWriteBuffer(queue, data_device, CL_TRUE, 0, ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "q5_K set_tensor: temp upload buffer alloc failed"); cl_buffer_region region; @@ -10792,9 +10997,8 @@ static void ggml_backend_opencl_buffer_set_tensor(ggml_backend_buffer_t buffer, "Incorrect tensor size"); cl_int err; - cl_mem data_device; - CL_CHECK((data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, ggml_nbytes(tensor), NULL, &err), err)); - CL_CHECK(clEnqueueWriteBuffer(queue, data_device, CL_TRUE, 0, ggml_nbytes(tensor), data, 0, NULL, NULL)); + cl_mem data_device = ggml_cl_create_temp_upload_buffer(context, queue, ggml_nbytes(tensor), data, tensor->name); + GGML_ASSERT(data_device != NULL && "q6_K set_tensor: temp upload buffer alloc failed"); cl_buffer_region region; @@ -11135,9 +11339,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, cl_int err; cl_kernel kernel = backend_ctx->kernel_restore_block_q4_0_trans4_ns; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); int ne00 = tensor->ne[0]; int ne01 = tensor->ne[1]; @@ -11180,7 +11383,7 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, buf_trans_d.allocate(backend_ctx->context, size_d); buf_unpacked.allocate(backend_ctx->context, ggml_nbytes(tensor)); - if (use_q4_0_ila_kernels(backend_ctx, tensor)) { + if (use_q4_0_bin_kernels(backend_ctx, tensor)) { transpose_2d_as_32b(backend_ctx, extra->q, buf_trans_q.buffer, size_q, M, K / 8); } else { transpose_2d_as_16b(backend_ctx, extra->q, buf_trans_q.buffer, size_q, M, K / 4); @@ -11206,10 +11409,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, } #endif - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_kernel kernel = backend_ctx->kernel_restore_block_q4_0; CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &extra->q)); @@ -11234,10 +11435,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, #ifdef GGML_OPENCL_USE_ADRENO_KERNELS if (use_adreno_moe_kernels(backend_ctx, tensor)) { - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_kernel kernel = backend_ctx->kernel_restore_block_q4_1_trans4_ns; int ne00 = tensor->ne[0]; @@ -11309,10 +11508,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, } #endif - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_kernel kernel = backend_ctx->kernel_restore_block_q4_1; CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &extra->q)); @@ -11340,9 +11537,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, if (use_adreno_moe_kernels(backend_ctx, tensor)) { cl_int err; // TODO: use ggml_cl_buffer to manage this temporary buffer - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_kernel kernel = backend_ctx->kernel_restore_block_q5_0_trans4_ns; @@ -11444,9 +11640,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, if (use_adreno_moe_kernels(backend_ctx, tensor)) { cl_int err; // TODO: use ggml_cl_buffer to manage this temporary buffer - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_kernel kernel = backend_ctx->kernel_restore_block_q5_1_trans4_ns; @@ -11551,10 +11746,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, if (tensor->type == GGML_TYPE_MXFP4) { ggml_tensor_extra_cl_mxfp4 * extra = (ggml_tensor_extra_cl_mxfp4 *)tensor->extra; - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); #ifdef GGML_OPENCL_USE_ADRENO_KERNELS if (use_adreno_moe_kernels(backend_ctx, tensor)) { @@ -11616,10 +11809,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * extra_src = tensor->view_src != nullptr ? tensor->view_src : tensor; ggml_tensor_extra_cl_q8_0 * extra = (ggml_tensor_extra_cl_q8_0 *)extra_src->extra; - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); #ifdef GGML_OPENCL_USE_ADRENO_KERNELS if (enable_adreno_trans_weight(backend_ctx, tensor)) { @@ -11672,10 +11863,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, if (tensor->type == GGML_TYPE_IQ4_NL) { ggml_tensor_extra_cl_iq4_nl * extra = (ggml_tensor_extra_cl_iq4_nl *)tensor->extra; - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); #ifdef GGML_OPENCL_USE_ADRENO_KERNELS if (use_adreno_kernels(backend_ctx, tensor)) { @@ -11744,10 +11933,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, if (tensor->type == GGML_TYPE_Q4_K) { ggml_tensor_extra_cl_q4_K * extra = (ggml_tensor_extra_cl_q4_K *)tensor->extra; - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_uchar mask_0F = 0x0F; cl_uchar mask_F0 = 0xF0; @@ -11802,10 +11989,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, return; } if (use_adreno_moe_kernels(backend_ctx, tensor)) { - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_kernel kernel = backend_ctx->kernel_restore_block_q4_k_trans4_ns; @@ -11855,7 +12040,11 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, buf_trans_s.allocate(backend_ctx->context, size_s); // Transpose q, d, dm, s back - transpose_2d_as_16b(backend_ctx, extra->q, buf_trans_q.buffer, size_q, M, K/4); + if (use_q4_k_bin_kernels(backend_ctx, tensor)) { + transpose_2d_as_32b(backend_ctx, extra->q, buf_trans_q.buffer, size_q, M, K/8); + } else { + transpose_2d_as_16b(backend_ctx, extra->q, buf_trans_q.buffer, size_q, M, K/4); + } transpose_2d_as_16b(backend_ctx, extra->d, buf_trans_d.buffer, size_d, M, K/256); transpose_2d_as_16b(backend_ctx, extra->dm, buf_trans_dm.buffer, size_dm, M, K/256); transpose_2d_as_8b (backend_ctx, extra->s, buf_trans_s.buffer, size_s, M, K/256*12, true, true); @@ -11906,20 +12095,16 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, if (tensor->type == GGML_TYPE_Q5_K) { ggml_tensor_extra_cl_q5_K * extra = (ggml_tensor_extra_cl_q5_K *)tensor->extra; - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_uchar mask_0F = 0x0F; cl_uchar mask_F0 = 0xF0; #ifdef GGML_OPENCL_USE_ADRENO_KERNELS if (use_adreno_moe_kernels(backend_ctx, tensor)) { - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_kernel kernel = backend_ctx->kernel_restore_block_q5_k_trans4_ns; int ne00 = tensor->ne[0]; @@ -12079,10 +12264,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, return; } if (use_adreno_moe_kernels(backend_ctx, tensor)) { - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_kernel kernel = backend_ctx->kernel_restore_block_q6_k_trans4_ns; @@ -12169,10 +12352,8 @@ static void ggml_backend_opencl_buffer_get_tensor(ggml_backend_buffer_t buffer, } #endif // GGML_OPENCL_USE_ADRENO_KERNELS - cl_int err; - cl_mem data_device = clCreateBuffer(context, CL_MEM_READ_WRITE, - ggml_nbytes(tensor), NULL, &err); - CL_CHECK(err); + cl_mem data_device = ggml_cl_create_temp_download_buffer(context, queue, ggml_nbytes(tensor), tensor->name); + GGML_ASSERT(data_device != NULL && "get_tensor: temp download buffer alloc failed"); cl_uchar mask = 0xFF; cl_ulong n_blk = ggml_nelements(tensor)/ggml_blck_size(tensor->type); @@ -12300,6 +12481,21 @@ static ggml_backend_buffer_t ggml_backend_opencl_buffer_type_alloc_buffer(ggml_b cl_int err; cl_mem mem = clCreateBuffer(backend_ctx->context, CL_MEM_READ_WRITE, size, NULL, &err); + // On Adreno X1-85 the device pool intermittently fails at hundreds of MB + // once the heap fragments (e.g. graph-allocator compute-buffer reserve + // after model load). Four-step retry: + // 1. normal alloc (fast path) + // 2. clFinish + retry (drains in-flight allocs) + // 3. cl_qcom_large_buffer (X2-class driver only, OpenCL 3.0 only) + // 4. ALLOC_HOST_PTR (host-pinned pool) — last-resort fallback. This + // buffer backs compute scratch read/written by every kernel in the + // graph, so kernel accesses fall to host memory and runtime perf + // degrades meaningfully. Better than failing to load, but the user + // should see the warning and consider -ngl reduction. + if (err != CL_SUCCESS) { + clFinish(backend_ctx->queue); + mem = clCreateBuffer(backend_ctx->context, CL_MEM_READ_WRITE, size, NULL, &err); + } #if GGML_OPENCL_TARGET_VERSION >= 300 // clCreateBufferWithProperties and cl_mem_properties are OpenCL 3.0. Drivers older than // that do not export the symbol, so a build targeting them fails to link. The large @@ -12310,9 +12506,20 @@ static ggml_backend_buffer_t ggml_backend_opencl_buffer_type_alloc_buffer(ggml_b mem = clCreateBufferWithProperties(backend_ctx->context, props, CL_MEM_READ_WRITE, size, NULL, &err); } #endif + if (err != CL_SUCCESS) { + mem = clCreateBuffer(backend_ctx->context, CL_MEM_READ_WRITE | CL_MEM_ALLOC_HOST_PTR, size, NULL, &err); + if (err == CL_SUCCESS) { + GGML_LOG_WARN("%s: %.2f MiB allocated via CL_MEM_ALLOC_HOST_PTR fallback — " + "device pool exhausted; runtime perf will be degraded. " + "Consider lowering -ngl or context size.\n", + __func__, size / 1024.0 / 1024.0); + } + } if (err != CL_SUCCESS) { - GGML_LOG_INFO("%s: failed to allocate %.2f MiB\n", __func__, size / 1024.0 / 1024.0); + GGML_LOG_ERROR("%s: failed to allocate %.2f MiB (err=%d). " + "Consider reducing -ngl, lowering -c / -ub, or using quantized KV cache.\n", + __func__, size / 1024.0 / 1024.0, err); return nullptr; } @@ -13067,6 +13274,7 @@ static void ggml_cl_set_rows(ggml_backend_t backend, const ggml_tensor * src0, c (size_t)ne03}; size_t local_work_size[] = {(size_t)nth0, (size_t)rows_per_workgroup, 1}; + // ne01 == 0 makes global_work_size[0] zero here; enqueue_ndrange_kernel drops the empty range. backend_ctx->enqueue_ndrange_kernel(kernel, 3, global_work_size, local_work_size, dst); } @@ -13856,81 +14064,109 @@ static void ggml_cl_mean(ggml_backend_t backend, const ggml_tensor * src0, const } static void ggml_cl_ssm_scan(ggml_backend_t backend, ggml_tensor * dst) { - const ggml_tensor * src0 = dst->src[0]; // s - const ggml_tensor * src1 = dst->src[1]; // x - const ggml_tensor * src2 = dst->src[2]; // dt - const ggml_tensor * src3 = dst->src[3]; // A - const ggml_tensor * src4 = dst->src[4]; // B - const ggml_tensor * src5 = dst->src[5]; // C - const ggml_tensor * src6 = dst->src[6]; // ids - - GGML_ASSERT(src0 && src1 && src2 && src3 && src4 && src5 && src6 && dst); + GGML_ASSERT(dst); + GGML_ASSERT(dst->extra); + GGML_ASSERT(dst->src[0]); + GGML_ASSERT(dst->src[0]->extra); + GGML_ASSERT(dst->src[1]); + GGML_ASSERT(dst->src[1]->extra); + GGML_ASSERT(dst->src[2]); + GGML_ASSERT(dst->src[2]->extra); + GGML_ASSERT(dst->src[3]); + GGML_ASSERT(dst->src[3]->extra); + GGML_ASSERT(dst->src[4]); + GGML_ASSERT(dst->src[4]->extra); + GGML_ASSERT(dst->src[5]); + GGML_ASSERT(dst->src[5]->extra); + GGML_ASSERT(dst->src[6]); + GGML_ASSERT(dst->src[6]->extra); ggml_backend_opencl_context * backend_ctx = (ggml_backend_opencl_context *) backend->context; - ggml_tensor_extra_cl * e0 = (ggml_tensor_extra_cl *) src0->extra; - ggml_tensor_extra_cl * e1 = (ggml_tensor_extra_cl *) src1->extra; - ggml_tensor_extra_cl * e2 = (ggml_tensor_extra_cl *) src2->extra; - ggml_tensor_extra_cl * e3 = (ggml_tensor_extra_cl *) src3->extra; - ggml_tensor_extra_cl * e4 = (ggml_tensor_extra_cl *) src4->extra; - ggml_tensor_extra_cl * e5 = (ggml_tensor_extra_cl *) src5->extra; - ggml_tensor_extra_cl * e6 = (ggml_tensor_extra_cl *) src6->extra; - ggml_tensor_extra_cl * ed = (ggml_tensor_extra_cl *) dst->extra; - - cl_ulong o0 = e0->offset + src0->view_offs; - cl_ulong o1 = e1->offset + src1->view_offs; - cl_ulong o2 = e2->offset + src2->view_offs; - cl_ulong o3 = e3->offset + src3->view_offs; - cl_ulong o4 = e4->offset + src4->view_offs; - cl_ulong o5 = e5->offset + src5->view_offs; - cl_ulong o6 = e6->offset + src6->view_offs; - cl_ulong od = ed->offset + dst->view_offs; - - const int d_state = (int) src0->ne[0]; - const int head_dim = (int) src0->ne[1]; - const int n_head = (int) src1->ne[1]; - const int n_group = (int) src4->ne[1]; - const int n_tokens = (int) src1->ne[2]; - const int n_seqs = (int) src1->ne[3]; - - // Mirror CPU ref: s_off = ggml_nelements(src1) * sizeof(float) - const cl_ulong s_off_bytes = (cl_ulong) ggml_nelements(src1) * sizeof(float); - - cl_kernel kernel = (d_state == 128) - ? backend_ctx->kernel_ssm_scan_f32_mamba2_d128 - : backend_ctx->kernel_ssm_scan_f32_mamba2_d256; - GGML_ASSERT(kernel != nullptr); + ggml_tensor_extra_cl * extra0 = (ggml_tensor_extra_cl *) dst->src[0]->extra; + ggml_tensor_extra_cl * extra1 = (ggml_tensor_extra_cl *) dst->src[1]->extra; + ggml_tensor_extra_cl * extra2 = (ggml_tensor_extra_cl *) dst->src[2]->extra; + ggml_tensor_extra_cl * extra3 = (ggml_tensor_extra_cl *) dst->src[3]->extra; + ggml_tensor_extra_cl * extra4 = (ggml_tensor_extra_cl *) dst->src[4]->extra; + ggml_tensor_extra_cl * extra5 = (ggml_tensor_extra_cl *) dst->src[5]->extra; + ggml_tensor_extra_cl * extra6 = (ggml_tensor_extra_cl *) dst->src[6]->extra; + ggml_tensor_extra_cl * extrad = (ggml_tensor_extra_cl *) dst->extra; + + const cl_ulong offset0 = extra0->offset + dst->src[0]->view_offs; + const cl_ulong offset1 = extra1->offset + dst->src[1]->view_offs; + const cl_ulong offset2 = extra2->offset + dst->src[2]->view_offs; + const cl_ulong offset3 = extra3->offset + dst->src[3]->view_offs; + const cl_ulong offset4 = extra4->offset + dst->src[4]->view_offs; + const cl_ulong offset5 = extra5->offset + dst->src[5]->view_offs; + const cl_ulong offset6 = extra6->offset + dst->src[6]->view_offs; + const cl_ulong offsetd = extrad->offset + dst->view_offs; + + const ggml_tensor * s = dst->src[0]; + const ggml_tensor * x = dst->src[1]; + const ggml_tensor * dt = dst->src[2]; + const ggml_tensor * A = dst->src[3]; + const ggml_tensor * B = dst->src[4]; + const ggml_tensor * C = dst->src[5]; + + const cl_ulong s_nb1 = s->nb[1]; + const cl_ulong s_nb2 = s->nb[2]; + const cl_ulong s_nb3 = s->nb[3]; + const cl_ulong x_nb1 = x->nb[1]; + const cl_ulong x_nb2 = x->nb[2]; + const cl_ulong x_nb3 = x->nb[3]; + const cl_ulong dt_nb1 = dt->nb[1]; + const cl_ulong dt_nb2 = dt->nb[2]; + const cl_ulong A_nb1 = A->nb[1]; + const cl_ulong B_nb1 = B->nb[1]; + const cl_ulong B_nb2 = B->nb[2]; + const cl_ulong B_nb3 = B->nb[3]; + const cl_ulong C_nb1 = C->nb[1]; + const cl_ulong C_nb2 = C->nb[2]; + const cl_ulong C_nb3 = C->nb[3]; + + const cl_uint A_ne0 = A->ne[0]; + const cl_uint d_state = s->ne[0]; + const cl_int head_dim = x->ne[0]; + const cl_int n_head = x->ne[1]; + const cl_int n_group = B->ne[1]; + const cl_int n_tokens = x->ne[2]; + const cl_uint n_seqs = x->ne[3]; + const cl_uint K = ggml_get_op_params_i32(dst, 0); + const cl_ulong s_off_bytes = (cl_ulong) ggml_nelements(x) * sizeof(float); + + cl_kernel kernel = backend_ctx->kernel_ssm_scan_f32; + size_t nth = d_state; + if (A_ne0 == 1 && K == 1) { + cl_kernel kernel_mamba2 = nullptr; + if (d_state == 128) { + kernel_mamba2 = backend_ctx->kernel_ssm_scan_f32_mamba2_d128; + } else if (d_state == 256) { + kernel_mamba2 = backend_ctx->kernel_ssm_scan_f32_mamba2_d256; + } + if (kernel_mamba2 != nullptr) { + kernel = kernel_mamba2; + nth = 64; + } + } - cl_ulong s0_nb2 = src0->nb[2]; - cl_ulong s0_nb3 = src0->nb[3]; - cl_ulong x_nb2 = src1->nb[2]; - cl_ulong x_nb3 = src1->nb[3]; - cl_ulong dt_nb1 = src2->nb[1]; - cl_ulong dt_nb2 = src2->nb[2]; - cl_ulong A_nb1 = src3->nb[1]; - cl_ulong B_nb2 = src4->nb[2]; - cl_ulong B_nb3 = src4->nb[3]; - cl_ulong C_nb2 = src5->nb[2]; - cl_ulong C_nb3 = src5->nb[3]; - - CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &e0->data_device)); - CL_CHECK(clSetKernelArg(kernel, 1, sizeof(cl_ulong), &o0)); - CL_CHECK(clSetKernelArg(kernel, 2, sizeof(cl_mem), &e1->data_device)); - CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_ulong), &o1)); - CL_CHECK(clSetKernelArg(kernel, 4, sizeof(cl_mem), &e2->data_device)); - CL_CHECK(clSetKernelArg(kernel, 5, sizeof(cl_ulong), &o2)); - CL_CHECK(clSetKernelArg(kernel, 6, sizeof(cl_mem), &e3->data_device)); - CL_CHECK(clSetKernelArg(kernel, 7, sizeof(cl_ulong), &o3)); - CL_CHECK(clSetKernelArg(kernel, 8, sizeof(cl_mem), &e4->data_device)); - CL_CHECK(clSetKernelArg(kernel, 9, sizeof(cl_ulong), &o4)); - CL_CHECK(clSetKernelArg(kernel, 10, sizeof(cl_mem), &e5->data_device)); - CL_CHECK(clSetKernelArg(kernel, 11, sizeof(cl_ulong), &o5)); - CL_CHECK(clSetKernelArg(kernel, 12, sizeof(cl_mem), &e6->data_device)); - CL_CHECK(clSetKernelArg(kernel, 13, sizeof(cl_ulong), &o6)); - CL_CHECK(clSetKernelArg(kernel, 14, sizeof(cl_mem), &ed->data_device)); - CL_CHECK(clSetKernelArg(kernel, 15, sizeof(cl_ulong), &od)); - CL_CHECK(clSetKernelArg(kernel, 16, sizeof(cl_ulong), &s0_nb2)); - CL_CHECK(clSetKernelArg(kernel, 17, sizeof(cl_ulong), &s0_nb3)); + CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &extra0->data_device)); + CL_CHECK(clSetKernelArg(kernel, 1, sizeof(cl_ulong), &offset0)); + CL_CHECK(clSetKernelArg(kernel, 2, sizeof(cl_mem), &extra1->data_device)); + CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_ulong), &offset1)); + CL_CHECK(clSetKernelArg(kernel, 4, sizeof(cl_mem), &extra2->data_device)); + CL_CHECK(clSetKernelArg(kernel, 5, sizeof(cl_ulong), &offset2)); + CL_CHECK(clSetKernelArg(kernel, 6, sizeof(cl_mem), &extra3->data_device)); + CL_CHECK(clSetKernelArg(kernel, 7, sizeof(cl_ulong), &offset3)); + CL_CHECK(clSetKernelArg(kernel, 8, sizeof(cl_mem), &extra4->data_device)); + CL_CHECK(clSetKernelArg(kernel, 9, sizeof(cl_ulong), &offset4)); + CL_CHECK(clSetKernelArg(kernel, 10, sizeof(cl_mem), &extra5->data_device)); + CL_CHECK(clSetKernelArg(kernel, 11, sizeof(cl_ulong), &offset5)); + CL_CHECK(clSetKernelArg(kernel, 12, sizeof(cl_mem), &extra6->data_device)); + CL_CHECK(clSetKernelArg(kernel, 13, sizeof(cl_ulong), &offset6)); + CL_CHECK(clSetKernelArg(kernel, 14, sizeof(cl_mem), &extrad->data_device)); + CL_CHECK(clSetKernelArg(kernel, 15, sizeof(cl_ulong), &offsetd)); + CL_CHECK(clSetKernelArg(kernel, 16, sizeof(cl_ulong), &s_nb2)); + CL_CHECK(clSetKernelArg(kernel, 17, sizeof(cl_ulong), &s_nb3)); CL_CHECK(clSetKernelArg(kernel, 18, sizeof(cl_ulong), &x_nb2)); CL_CHECK(clSetKernelArg(kernel, 19, sizeof(cl_ulong), &x_nb3)); CL_CHECK(clSetKernelArg(kernel, 20, sizeof(cl_ulong), &dt_nb1)); @@ -13941,15 +14177,30 @@ static void ggml_cl_ssm_scan(ggml_backend_t backend, ggml_tensor * dst) { CL_CHECK(clSetKernelArg(kernel, 25, sizeof(cl_ulong), &C_nb2)); CL_CHECK(clSetKernelArg(kernel, 26, sizeof(cl_ulong), &C_nb3)); CL_CHECK(clSetKernelArg(kernel, 27, sizeof(cl_ulong), &s_off_bytes)); - CL_CHECK(clSetKernelArg(kernel, 28, sizeof(int), &head_dim)); - CL_CHECK(clSetKernelArg(kernel, 29, sizeof(int), &n_head)); - CL_CHECK(clSetKernelArg(kernel, 30, sizeof(int), &n_group)); - CL_CHECK(clSetKernelArg(kernel, 31, sizeof(int), &n_tokens)); + CL_CHECK(clSetKernelArg(kernel, 28, sizeof(cl_int), &head_dim)); + CL_CHECK(clSetKernelArg(kernel, 29, sizeof(cl_int), &n_head)); + CL_CHECK(clSetKernelArg(kernel, 30, sizeof(cl_int), &n_group)); + CL_CHECK(clSetKernelArg(kernel, 31, sizeof(cl_int), &n_tokens)); + + if (kernel == backend_ctx->kernel_ssm_scan_f32) { + CL_CHECK(clSetKernelArg(kernel, 32, sizeof(cl_ulong), &s_nb1)); + CL_CHECK(clSetKernelArg(kernel, 33, sizeof(cl_ulong), &x_nb1)); + CL_CHECK(clSetKernelArg(kernel, 34, sizeof(cl_ulong), &B_nb1)); + CL_CHECK(clSetKernelArg(kernel, 35, sizeof(cl_ulong), &C_nb1)); + CL_CHECK(clSetKernelArg(kernel, 36, sizeof(cl_uint), &A_ne0)); + CL_CHECK(clSetKernelArg(kernel, 37, sizeof(cl_uint), &d_state)); + CL_CHECK(clSetKernelArg(kernel, 38, sizeof(cl_uint), &n_seqs)); + CL_CHECK(clSetKernelArg(kernel, 39, sizeof(cl_uint), &K)); + CL_CHECK(clSetKernelArg(kernel, 40, d_state * sizeof(float), nullptr)); + } - size_t global_work_size[] = { (size_t)n_head * head_dim * 64, (size_t)n_seqs, 1 }; - size_t local_work_size[] = { 64, 1, 1 }; + size_t global_work_size[] = { + (size_t) head_dim * (size_t) n_head * nth, + (size_t) n_seqs, + }; + size_t local_work_size[] = { nth, 1 }; - backend_ctx->enqueue_ndrange_kernel(kernel, 3, global_work_size, local_work_size, dst); + backend_ctx->enqueue_ndrange_kernel(kernel, 2, global_work_size, local_work_size, dst); } static void ggml_cl_ssm_conv(ggml_backend_t backend, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { @@ -18639,9 +18890,9 @@ static void ggml_cl_mul_mat_q4_0_f32_adreno(ggml_backend_t backend, const ggml_t static const bool q40_mc3 = (getenv("GGML_OPENCL_Q40_MC3") != nullptr); const bool use_q40_mc3 = q40_mc3 && (ne1 >= 2 && ne1 <= 4) && (ne01 < 32768); - const bool use_ila = use_q4_0_ila_kernels(backend_ctx, src0); + const bool use_bin = use_q4_0_bin_kernels(backend_ctx, src0); - if (use_ila) { + if (use_bin) { if (use_q40_mc3) { static bool warned = false; if (!warned) { @@ -20196,6 +20447,145 @@ static void ggml_cl_mul_mat_q8_0_f32_adreno(ggml_backend_t backend, const ggml_t #endif } +#ifdef GGML_OPENCL_USE_ADRENO_KERNELS +static void ggml_cl_mul_mat_q4_k_f32_adreno_ila(ggml_backend_t backend, const ggml_tensor * src0, + const ggml_tensor * src1, ggml_tensor * dst) { + GGML_ASSERT(src0); + GGML_ASSERT(src0->extra); + GGML_ASSERT(src1); + GGML_ASSERT(src1->extra); + GGML_ASSERT(dst); + GGML_ASSERT(dst->extra); + + ggml_backend_opencl_context *backend_ctx = (ggml_backend_opencl_context *)backend->context; + + ggml_tensor_extra_cl * extra1 = (ggml_tensor_extra_cl *)src1->extra; + ggml_tensor_extra_cl * extrad = (ggml_tensor_extra_cl *)dst->extra; + ggml_tensor_extra_cl_q4_K * extra0_q4_k = (ggml_tensor_extra_cl_q4_K *)src0->extra; + + cl_ulong offset1 = extra1->offset + src1->view_offs; + cl_ulong offsetd = extrad->offset + dst->view_offs; + + const int ne00 = src0->ne[0]; + const int ne01 = src0->ne[1]; + + const int ne1 = dst->ne[1]; + + GGML_ASSERT(ne00 % ggml_blck_size(src0->type) == 0); + + cl_context context = backend_ctx->context; + cl_kernel kernel; + + cl_int err; + cl_image_format img_fmt; + cl_image_desc img_desc; + cl_buffer_region region; + + int M = ne01; + int N = ne1; + int K = ne00; + + if (ne1 == 1) { + cl_mem b_sub_buf = nullptr; + cl_mem b_img = nullptr; + + region.origin = offset1; + region.size = (size_t)K * N * sizeof(float); + CL_CHECK((b_sub_buf = clCreateSubBuffer(extra1->data_device, 0, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err), err)); + + img_fmt = { CL_RGBA, CL_FLOAT }; + memset(&img_desc, 0, sizeof(img_desc)); + img_desc.image_type = CL_MEM_OBJECT_IMAGE1D_BUFFER; + img_desc.image_width = (size_t)K * N / 4; + img_desc.buffer = b_sub_buf; + CL_CHECK((b_img = clCreateImage(context, CL_MEM_READ_ONLY, &img_fmt, &img_desc, NULL, &err), err)); + + kernel = backend_ctx->kernel_gemv_noshuffle_q4_k_f32_32b_trans; + CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &extra0_q4_k->q_img)); + CL_CHECK(clSetKernelArg(kernel, 1, sizeof(cl_mem), &extra0_q4_k->d)); + CL_CHECK(clSetKernelArg(kernel, 2, sizeof(cl_mem), &extra0_q4_k->dm)); + CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_mem), &extra0_q4_k->s)); + CL_CHECK(clSetKernelArg(kernel, 4, sizeof(cl_mem), &b_img)); + CL_CHECK(clSetKernelArg(kernel, 5, sizeof(cl_mem), &extrad->data_device)); + CL_CHECK(clSetKernelArg(kernel, 6, sizeof(cl_ulong), &offsetd)); + CL_CHECK(clSetKernelArg(kernel, 7, sizeof(cl_int), &ne00)); + CL_CHECK(clSetKernelArg(kernel, 8, sizeof(cl_int), &ne01)); + + size_t local_work_size[3] = { 64, 8, 1 }; + size_t global_work_size[3] = { (size_t)ne01, 8, 1 }; + backend_ctx->enqueue_ndrange_kernel(kernel, 3, global_work_size, local_work_size, dst); + + CL_CHECK(clReleaseMemObject(b_sub_buf)); + CL_CHECK(clReleaseMemObject(b_img)); + } else { + const int gemm_tile_n = 64; + int N_pad = CEIL_DIV(N, gemm_tile_n) * gemm_tile_n; + + cl_mem b_sub_buf = nullptr; + cl_mem b_padded = nullptr; + cl_mem b_buf = nullptr; + if (N_pad == N) { + region.origin = offset1; + region.size = (size_t)K * N * sizeof(float); + CL_CHECK((b_sub_buf = clCreateSubBuffer(extra1->data_device, 0, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err), err)); + b_buf = b_sub_buf; + } else { + CL_CHECK((b_padded = clCreateBuffer(context, CL_MEM_READ_WRITE, (size_t)K * N_pad * sizeof(float), NULL, &err), err)); + const float zero = 0.0f; + CL_CHECK(clEnqueueFillBuffer(backend_ctx->queue, b_padded, &zero, sizeof(zero), 0, (size_t)K * N_pad * sizeof(float), 0, NULL, NULL)); + CL_CHECK(clEnqueueCopyBuffer(backend_ctx->queue, extra1->data_device, b_padded, offset1, 0, (size_t)K * N * sizeof(float), 0, NULL, NULL)); + b_buf = b_padded; + } + + img_fmt = { CL_R, CL_FLOAT }; + memset(&img_desc, 0, sizeof(img_desc)); + img_desc.image_type = CL_MEM_OBJECT_IMAGE1D_BUFFER; + img_desc.image_width = (size_t)K * N_pad; + img_desc.buffer = b_buf; + cl_mem b_img; + CL_CHECK((b_img = clCreateImage(context, CL_MEM_READ_ONLY, &img_fmt, &img_desc, NULL, &err), err)); + + region.origin = offsetd; + region.size = (size_t)M * N * sizeof(float); + cl_mem d_sub_buf; + CL_CHECK((d_sub_buf = clCreateSubBuffer(extrad->data_device, 0, CL_BUFFER_CREATE_TYPE_REGION, ®ion, &err), err)); + img_fmt = { CL_R, CL_FLOAT }; + memset(&img_desc, 0, sizeof(img_desc)); + img_desc.image_type = CL_MEM_OBJECT_IMAGE1D_BUFFER; + img_desc.image_width = (size_t)M * N; + img_desc.buffer = d_sub_buf; + cl_mem d_img; + CL_CHECK((d_img = clCreateImage(context, CL_MEM_WRITE_ONLY, &img_fmt, &img_desc, NULL, &err), err)); + + kernel = backend_ctx->kernel_gemm_noshuffle_q4_k_f32_32b_trans_ila_a8_bin; + CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &extra0_q4_k->q_img)); + CL_CHECK(clSetKernelArg(kernel, 1, sizeof(cl_mem), &extra0_q4_k->d)); + CL_CHECK(clSetKernelArg(kernel, 2, sizeof(cl_mem), &extra0_q4_k->dm)); + CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_mem), &extra0_q4_k->s)); + CL_CHECK(clSetKernelArg(kernel, 4, sizeof(cl_mem), &b_img)); + CL_CHECK(clSetKernelArg(kernel, 5, sizeof(cl_mem), &d_img)); + CL_CHECK(clSetKernelArg(kernel, 6, sizeof(cl_uint), &ne00)); + CL_CHECK(clSetKernelArg(kernel, 7, sizeof(cl_uint), &ne01)); + CL_CHECK(clSetKernelArg(kernel, 8, sizeof(int), &N)); + + size_t local_work_size[3] = { 64, 2, 2 }; + size_t m_tiles = (size_t)CEIL_DIV(M, 64); + size_t global_work_size[3] = { 64, m_tiles, (size_t)CEIL_DIV(N_pad, gemm_tile_n) }; + backend_ctx->enqueue_ndrange_kernel(kernel, 3, global_work_size, local_work_size, dst); + + CL_CHECK(clReleaseMemObject(b_img)); + if (b_sub_buf) { + CL_CHECK(clReleaseMemObject(b_sub_buf)); + } + if (b_padded) { + CL_CHECK(clReleaseMemObject(b_padded)); + } + CL_CHECK(clReleaseMemObject(d_img)); + CL_CHECK(clReleaseMemObject(d_sub_buf)); + } +} +#endif // GGML_OPENCL_USE_ADRENO_KERNELS + static void ggml_cl_mul_mat_q4_k_f32_adreno(ggml_backend_t backend, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { #ifdef GGML_OPENCL_USE_ADRENO_KERNELS GGML_ASSERT(src0); @@ -20248,6 +20638,20 @@ static void ggml_cl_mul_mat_q4_k_f32_adreno(ggml_backend_t backend, const ggml_t // unified routes batched Q6_K lm_head to CPU). Per-layer mc3 is byte-identical. const bool use_mc3 = q4k_mc3 && (ne1 == 3) && (ne01 < 32768); + const bool use_bin = use_q4_k_bin_kernels(backend_ctx, src0); + + if (use_bin) { + if (use_mc3) { + static bool warned = false; + if (!warned) { + GGML_LOG_WARN("ggml_opencl: GGML_OPENCL_Q4K_MC3 is bypassed by Q4_K binary kernels\n"); + warned = true; + } + } + ggml_cl_mul_mat_q4_k_f32_adreno_ila(backend, src0, src1, dst); + return; + } + if (ne1 == 1 || use_mc3) { cl_mem q_img = nullptr; cl_mem b_sub_buf = nullptr; diff --git a/ggml/src/ggml-opencl/kernels/gemv_noshuffle_q4_k_f32_32b_trans.cl b/ggml/src/ggml-opencl/kernels/gemv_noshuffle_q4_k_f32_32b_trans.cl new file mode 100644 index 000000000000..2dbd943fda29 --- /dev/null +++ b/ggml/src/ggml-opencl/kernels/gemv_noshuffle_q4_k_f32_32b_trans.cl @@ -0,0 +1,134 @@ +#pragma OPENCL EXTENSION cl_khr_fp16 : enable +#pragma OPENCL EXTENSION cl_khr_subgroups : enable +#pragma OPENCL EXTENSION cl_qcom_reqd_sub_group_size : enable + +#define QK_K 256 +#define K_SCALE_SIZE 12 +#define N_SIMDGROUP 8 +#define SIMDGROUP_WIDTH 64 + +inline void get_scale_min_k4( + int j, + global const uchar * q, + uint stride, + uchar * d, + uchar * m +) { + if (j < 4) { + *d = q[j*stride] & 63; + *m = q[(j+4)*stride] & 63; + } else { + *d = (q[(j+4)*stride] & 0x0F) | ((q[(j-4)*stride] & 0xC0) >> 2); + *m = ((q[(j+4)*stride] >> 4) & 0x0F) | ((q[j*stride] & 0xC0) >> 2); + } +} + +static inline float8 q4_k_to_fp32_packed8(ushort2 q4x8, float scale, float minv) { + float8 fp32x8; + fp32x8.s0 = (q4x8.s0 & 0x000F) * scale - minv; + fp32x8.s1 = ((q4x8.s0 & 0x00F0) >> 4) * scale - minv; + fp32x8.s2 = ((q4x8.s0 & 0x0F00) >> 8) * scale - minv; + fp32x8.s3 = ((q4x8.s0 & 0xF000) >> 12) * scale - minv; + fp32x8.s4 = (q4x8.s1 & 0x000F) * scale - minv; + fp32x8.s5 = ((q4x8.s1 & 0x00F0) >> 4) * scale - minv; + fp32x8.s6 = ((q4x8.s1 & 0x0F00) >> 8) * scale - minv; + fp32x8.s7 = ((q4x8.s1 & 0xF000) >> 12) * scale - minv; + return fp32x8; +} + +__attribute__((qcom_reqd_sub_group_size("half"))) +__kernel void gemv_noshuffle_q4_k_f32_32b_trans( + read_only image1d_buffer_t src0_q, + __global half * src0_d, + __global half * src0_dm, + __global uchar * src0_s, + __read_only image1d_buffer_t src1, + __global float * dst, + ulong offsetd, + int ne00, + int ne01 +) { + uint i01 = get_global_id(0); + uint sgid = get_local_id(1); + uint slid = get_sub_group_local_id(); + + int num_subblocks = ne00 / 32; + + __private float sum = 0.0f; + + // Loop over sub-blocks of 32 elements, N_SIMDGROUP sub-blocks per iter + for (uint ib = sgid; ib < num_subblocks; ib += N_SIMDGROUP) { + uint sb = ib / 8; + uint j = ib % 8; + + // Load d and dmin for this super-block + half d_val = src0_d[sb * ne01 + i01]; + half dm_val = src0_dm[sb * ne01 + i01]; + + // Load sub-block scale and min. s is transposed [nb][12][M]; stride ne01 per code. + global const uchar * sc = src0_s + sb * K_SCALE_SIZE * ne01 + i01; + uchar sv, mn; + get_scale_min_k4(j, sc, ne01, &sv, &mn); + + float scale = (float)d_val * (float)sv; + float minv = (float)dm_val * (float)mn; + + // Load 4 uints of quants (32 nibbles = 32 elements), column-major stride ne01 + uint q_base = ib * ne01 * 4 + i01; + + uint4 regQ; + regQ.s0 = read_imageui(src0_q, q_base).x; + regQ.s1 = read_imageui(src0_q, q_base + ne01).x; + regQ.s2 = read_imageui(src0_q, q_base + ne01 * 2).x; + regQ.s3 = read_imageui(src0_q, q_base + ne01 * 3).x; + + // Load activations: 32 floats = 8 float4s + uint y_offset = ib * 8; + + float4 y_local = (slid < 8) ? read_imagef(src1, (y_offset + slid)) : (float4)0.0f; + float4 y0 = sub_group_broadcast(y_local, 0); + float4 y1 = sub_group_broadcast(y_local, 1); + float4 y2 = sub_group_broadcast(y_local, 2); + float4 y3 = sub_group_broadcast(y_local, 3); + float4 y4 = sub_group_broadcast(y_local, 4); + float4 y5 = sub_group_broadcast(y_local, 5); + float4 y6 = sub_group_broadcast(y_local, 6); + float4 y7 = sub_group_broadcast(y_local, 7); + + float8 fp32x8 = q4_k_to_fp32_packed8(as_ushort2(regQ.s0), scale, minv); + float4 acc = y0 * fp32x8.lo; + acc += y1 * fp32x8.hi; + + fp32x8 = q4_k_to_fp32_packed8(as_ushort2(regQ.s1), scale, minv); + acc += y2 * fp32x8.lo; + acc += y3 * fp32x8.hi; + + fp32x8 = q4_k_to_fp32_packed8(as_ushort2(regQ.s2), scale, minv); + acc += y4 * fp32x8.lo; + acc += y5 * fp32x8.hi; + + fp32x8 = q4_k_to_fp32_packed8(as_ushort2(regQ.s3), scale, minv); + acc += y6 * fp32x8.lo; + acc += y7 * fp32x8.hi; + + sum += ((acc.s0 + acc.s1) + (acc.s2 + acc.s3)); + } + + // reduction in local memory over N_SIMDGROUP subgroups + __local float reduceLM[SIMDGROUP_WIDTH * (N_SIMDGROUP - 1)]; + if (sgid > 0) { + reduceLM[SIMDGROUP_WIDTH * (sgid - 1) + slid] = sum; + } + barrier(CLK_LOCAL_MEM_FENCE); + if (sgid == 0) { + for (uint i = 0; i < N_SIMDGROUP - 1; ++i) { + sum += reduceLM[SIMDGROUP_WIDTH * i + slid]; + } + } + + // 1 output per thread in subgroup 0 + if (sgid == 0) { + dst = dst + (offsetd >> 2); + dst[i01] = sum; + } +} diff --git a/ggml/src/ggml-opencl/kernels/ssm_scan.cl b/ggml/src/ggml-opencl/kernels/ssm_scan.cl index 37698d123f41..1889b74cdb92 100644 --- a/ggml/src/ggml-opencl/kernels/ssm_scan.cl +++ b/ggml/src/ggml-opencl/kernels/ssm_scan.cl @@ -214,3 +214,133 @@ kernel void kernel_ssm_scan_f32_mamba2_d256( s_warp[tid + 128] = state2; s_warp[tid + 192] = state3; } + +kernel void kernel_ssm_scan_f32( + global const char * s_buf, + ulong s_off, + global const char * x_buf, + ulong x_off, + global const char * dt_buf, + ulong dt_off, + global const char * A_buf, + ulong A_off, + global const char * B_buf, + ulong B_off, + global const char * C_buf, + ulong C_off, + global const char * ids_buf, + ulong ids_off, + global char * dst_buf, + ulong dst_off, + ulong s_nb2, + ulong s_nb3, + ulong x_nb2, + ulong x_nb3, + ulong dt_nb1, + ulong dt_nb2, + ulong A_nb1, + ulong B_nb2, + ulong B_nb3, + ulong C_nb2, + ulong C_nb3, + ulong state_off, + int head_dim, + int n_head, + int n_group, + int n_tokens, + ulong s_nb1, + ulong x_nb1, + ulong B_nb1, + ulong C_nb1, + uint A_ne0, + uint d_state, + uint n_seqs, + uint K, + local float * reduce +) { + global const char * s_data = s_buf + s_off; + global const char * x_data = x_buf + x_off; + global const char * dt_data = dt_buf + dt_off; + global const char * A_data = A_buf + A_off; + global const char * B_data = B_buf + B_off; + global const char * C_data = C_buf + C_off; + global const int * ids_data = (global const int *) (ids_buf + ids_off); + global float * dst = (global float *) (dst_buf + dst_off); + const uint y_elems = state_off / sizeof(float); + + const uint tid = get_local_id(0); + const uint inner_idx = get_group_id(0); + const uint seq_idx = get_group_id(1); + const uint head_idx = inner_idx / head_dim; + const uint dim_idx = inner_idx - head_idx * head_dim; + const uint group_idx = head_idx / (n_head / n_group); + const uint state_slot = (uint) ids_data[seq_idx]; + + const ulong s_idx = (ulong) state_slot * s_nb3 + + (ulong) head_idx * s_nb2 + + (ulong) dim_idx * s_nb1 + + (ulong) tid * sizeof(float); + float state = *((global const float *) (s_data + s_idx)); + + const ulong A_idx = (ulong) head_idx * A_nb1 + + (ulong) (tid % A_ne0) * sizeof(float); + const float A_value = *((global const float *) (A_data + A_idx)); + + for (int token_idx = 0; token_idx < n_tokens; ++token_idx) { + const ulong x_idx = (ulong) head_idx * x_nb1 + + (ulong) token_idx * x_nb2 + + (ulong) seq_idx * x_nb3 + + (ulong) dim_idx * sizeof(float); + const ulong dt_idx = (ulong) token_idx * dt_nb1 + + (ulong) seq_idx * dt_nb2 + + (ulong) head_idx * sizeof(float); + const ulong B_idx = (ulong) group_idx * B_nb1 + + (ulong) token_idx * B_nb2 + + (ulong) seq_idx * B_nb3 + + (ulong) tid * sizeof(float); + const ulong C_idx = (ulong) group_idx * C_nb1 + + (ulong) token_idx * C_nb2 + + (ulong) seq_idx * C_nb3 + + (ulong) tid * sizeof(float); + + const float x_value = *((global const float *) (x_data + x_idx)); + const float dt_value = *((global const float *) (dt_data + dt_idx)); + const float B_value = *((global const float *) (B_data + B_idx)); + const float C_value = *((global const float *) (C_data + C_idx)); + const float dt_soft_plus = dt_value > 20.0f ? dt_value : log(1.0f + exp(dt_value)); + const float dA = exp(dt_soft_plus * A_value); + const float x_dt = x_value * dt_soft_plus; + + state = mad(state, dA, B_value * x_dt); + reduce[tid] = state * C_value; + barrier(CLK_LOCAL_MEM_FENCE); + + for (uint stride = d_state / 2; stride > 0; stride >>= 1) { + if (tid < stride) { + reduce[tid] += reduce[tid + stride]; + } + barrier(CLK_LOCAL_MEM_FENCE); + } + + if (tid == 0) { + const uint y_idx = dim_idx + head_idx * head_dim + + token_idx * n_head * head_dim + + seq_idx * n_tokens * n_head * head_dim; + dst[y_idx] = reduce[0]; + } + + const uint snapshot_slot = n_tokens - 1 - token_idx; + if (snapshot_slot > 0 && snapshot_slot < K) { + const uint snapshot_idx = y_elems + tid + dim_idx * d_state + + head_idx * d_state * head_dim + + (snapshot_slot * n_seqs + seq_idx) * d_state * head_dim * n_head; + dst[snapshot_idx] = state; + } + barrier(CLK_LOCAL_MEM_FENCE); + } + + const uint state_idx = y_elems + tid + dim_idx * d_state + + head_idx * d_state * head_dim + + seq_idx * d_state * head_dim * n_head; + dst[state_idx] = state; +} diff --git a/ggml/src/ggml-openvino/ggml-decoder.cpp b/ggml/src/ggml-openvino/ggml-decoder.cpp index 006e005cb7aa..0b99834aa88a 100644 --- a/ggml/src/ggml-openvino/ggml-decoder.cpp +++ b/ggml/src/ggml-openvino/ggml-decoder.cpp @@ -117,16 +117,7 @@ bool is_same_shape(const ggml_tensor * a, const ggml_tensor * b) { bool is_conv_states_all_tensor(const ggml_tensor * tensor) { return tensor != nullptr && strncmp(tensor->name, "conv_states_all", strlen("conv_states_all")) == 0; } - -// CPY writing the tail of conv_input (the concat of the previous conv state and the new tokens) -// back into a slot block of the recurrent state cache. Detected structurally because the rollback -// variant (cparams.n_rs_seq > 0) emits one such CPY per snapshot slot without naming them. -bool is_conv_state_writeback(const ggml_tensor * node) { - return node->op == GGML_OP_CPY && node->view_src != nullptr && GgmlOvDecoder::is_kvcache(node->view_src, nullptr) && - node->src[0] != nullptr && node->src[0]->op == GGML_OP_VIEW && node->src[0]->src[0] != nullptr && - node->src[0]->src[0]->op == GGML_OP_CONCAT && node->src[1] != nullptr && node->src[1]->op == GGML_OP_VIEW && - node->src[1]->view_src == node->view_src; -} +} // namespace // MoE expert aggregation (build_moe_ffn in llama-graph.cpp): each expert plane is // `ggml_view_2d(experts, n_embd, n_tokens, experts->nb[2], i*experts->nb[1])` and the planes @@ -174,20 +165,31 @@ bool is_moe_expert_sum_add(const ggml_tensor * node) { return base != nullptr && base->ne[1] > 1 && plane_indices.size() == static_cast<size_t>(base->ne[1]); } -} // namespace -static std::string get_tensor_ov_name(const ggml_cgraph * cgraph, const ggml_tensor * tensor) { +std::string GgmlOvDecoder::get_tensor_name(const ggml_cgraph * cgraph, const ggml_tensor * tensor) { if (tensor == nullptr) { return ""; } - const size_t hash_pos = ggml_hash_find(&cgraph->visited_hash_set, tensor); - if (((tensor->flags & GGML_TENSOR_FLAG_COMPUTE) || GgmlOvDecoder::is_kvcache(tensor, nullptr)) && - hash_pos != GGML_HASHSET_FULL && ggml_bitset_get(cgraph->visited_hash_set.used, hash_pos)) { - return std::string(tensor->name) + "#" + std::to_string(hash_pos); + if ((tensor->flags & GGML_TENSOR_FLAG_COMPUTE) || is_kvcache(tensor, nullptr)) { + // Hash-table slots depend on tensor addresses and differ between contexts. + // Graph ordinals disambiguate duplicate names while keeping compiled-model + // ports identical for equivalent graphs in different contexts. + const auto * node = std::find(cgraph->nodes, cgraph->nodes + cgraph->n_nodes, tensor); + if (node != cgraph->nodes + cgraph->n_nodes) { + return std::string(tensor->name) + "#n" + std::to_string(node - cgraph->nodes); + } + const auto * leaf = std::find(cgraph->leafs, cgraph->leafs + cgraph->n_leafs, tensor); + if (leaf != cgraph->leafs + cgraph->n_leafs) { + return std::string(tensor->name) + "#l" + std::to_string(leaf - cgraph->leafs); + } } return tensor->name; } +static std::string get_tensor_ov_name(const ggml_cgraph * cgraph, const ggml_tensor * tensor) { + return GgmlOvDecoder::get_tensor_name(cgraph, tensor); +} + static std::string get_tensor_graph_input_ov_name(const GgmlOvDecoder * decoder, const ggml_cgraph * cgraph, const ggml_tensor * tensor, @@ -198,8 +200,20 @@ static std::string get_tensor_graph_input_ov_name(const GgmlOvDecoder * decoder, if (GgmlOvDecoder::is_inp_emb(tensor, op)) { return "embd"; } - if (decoder->is_stateful() && GgmlOvDecoder::is_inp_mask(tensor, op)) { - return std::string(tensor->name).find("swa") == std::string::npos ? "self_kq_mask" : "self_kq_mask_swa"; + if (GgmlOvDecoder::is_inp_mask(tensor, op)) { + // Give the two attention masks distinct OV parameter names. build_attn_inp_kq_mask() + // names the full-attention mask and the sliding-window mask identically, so keying a + // parameter off the name alone makes the second mask overwrite the first and both + // attention types read one parameter. Tell them apart by tensor identity, using the + // SWA classification computed in compute_llm_params(). An empty swa_layers set means + // there is only one mask in play and the plain name is correct. + const bool is_swa = decoder->is_swa_mask(tensor); + if (decoder->is_stateful()) { + return is_swa ? "self_kq_mask_swa" : "self_kq_mask"; + } + if (is_swa) { + return get_tensor_ov_name(cgraph, tensor) + "_swa"; + } } return get_tensor_ov_name(cgraph, tensor); } @@ -318,9 +332,7 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const { break; } case GGML_OP_MUL_MAT: { - if (node->src[0]->op == GGML_OP_VIEW && node->src[1]->op == GGML_OP_VIEW) { - op_case = 3; - } else if (node->src[1]->op == GGML_OP_SOFT_MAX) { + if (node->src[1]->op == GGML_OP_SOFT_MAX) { // In the case of `-fa off`, softmax is used, v_trans=true, the dynamic dim is ne[0] for cache_v op_case = 2; } @@ -441,7 +453,7 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const { if (node->src[0]->op == GGML_OP_VIEW) { if (node->src[0]->src[0]->op == GGML_OP_GATED_DELTA_NET) { op_case = 1; - } else if (is_conv_state_writeback(node)) { + } else if (GgmlOvDecoder::is_conv_state_writeback(node)) { op_case = 2; break; } else if (is_conv_states_all_tensor(node->view_src) && node->src[1] != nullptr && @@ -532,6 +544,40 @@ std::optional<int> extract_layer_from_name(const std::string & name) { return layer; } +// Recover the sliding window width from ggml's own SWA mask. llama.cpp never passes n_swa to a +// backend, but fill_mask() writes it into the mask: a query row keeps exactly the cells inside +// its window, so the widest row counts min(pos + 1, n_swa) unmasked cells. Counting rather than +// looking for a contiguous band is what makes this work on the KV-cache mask, where columns are +// physical cache cells in arbitrary order, not positions. +// Assumes LLAMA_SWA_TYPE_STANDARD, the only type the caller reconstructs. +static int get_swa_window_from_mask(const ggml_tensor * mask) { + if (mask->data == nullptr || !ggml_backend_buffer_is_host(mask->buffer)) { + return -1; + } + if (mask->type != GGML_TYPE_F16 && mask->type != GGML_TYPE_F32) { + return -1; + } + + const int64_t n_kv = mask->ne[0]; + const int64_t n_tokens = mask->ne[1]; + int64_t window = 0; + + for (int64_t r = 0; r < n_tokens; r++) { + int64_t kept = 0; + for (int64_t c = 0; c < n_kv; c++) { + const size_t i = (size_t) r * n_kv + c; + const float v = mask->type == GGML_TYPE_F16 ? ggml_fp16_to_fp32(((const ggml_fp16_t *) mask->data)[i]) : + ((const float *) mask->data)[i]; + if (v > -INFINITY) { + kept++; + } + } + window = std::max(window, kept); + } + + return window > 0 ? (int) window : -1; +} + std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgraph * cgraph, bool is_static) { ModelParams model_params; ComputeParams compute_params; @@ -597,6 +643,97 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr return -1; }; + // Resolve the attention mask an attention node consumes, mirroring the src layout that + // get_attention_pattern_case() classifies. Used by the SWA pre-pass below. + auto get_attention_op_mask = [&get_attention_pattern_case](const ggml_tensor * node) -> const ggml_tensor * { + switch (get_attention_pattern_case(node)) { + case 0: + case 1: + return node->src[3]; + case 2: + case 3: + return node->src[1]; + default: + return nullptr; + } + }; + + // Pre-pass: classify sliding-window vs full-attention layers. + // + // An interleaved-SWA model keeps two KV caches and two attention masks, and hands each layer + // whichever pair matches its attention type. The mask tensor does not say which is which: both + // are named "attn_inp_kq_mask" by build_attn_inp_kq_mask(), and both carry the same n_kv because + // llama_kv_cache::get_n_kv() pads occupancy up to a common multiple. + // + // The KV cache does say. Each cache allocates cache_k_l<N> once at load time with its own cell + // count: the windowed cache is sized from the window + // (PAD(min(size_base, n_swa*(unified ? n_seq_max : 1) + n_ubatch), 256), see + // llama_kv_cache_iswa), the full-attention one spans the whole context. Read the LEAF buffer + // behind the VIEW rather than the VIEW itself: the leaf extent is a constant per layer, known + // from the first graph onwards, while the view grows with context depth and would invert the + // comparison at shallow depth. + // + // Layers whose leaf is smaller than the largest leaf are the windowed ones. When every layer + // reports the same extent there is no distinction to draw -- either the model has no windowed + // layers, or the window is at least as large as the context so the two caches coincide, in + // which case a windowed layer and a full-attention one compute the same thing. + // + // Getting this wrong is silent and severe: with the windowed layers classified as + // full-attention, permute's KV slicing uses attention_size instead of attention_size_swa. The + // two agree while the context is shorter than the window, then diverge, and the mask add fails + // shape inference ("Failed to broadcast-merge input shapes") partway into a long prompt. + { + std::map<int, int64_t> layer_extent; // layer -> leaf cache_k cell count + std::map<int, const ggml_tensor *> layer_mask; // layer -> mask it consumes + int64_t max_extent = 0; + + for (int i = 0; i < cgraph->n_nodes; i++) { + const ggml_tensor * mask = get_attention_op_mask(cgraph->nodes[i]); + if (mask == nullptr) { + continue; + } + const ggml_tensor * cache_k_permute = nullptr; + switch (get_attention_pattern_case(cgraph->nodes[i])) { + case 0: cache_k_permute = cgraph->nodes[i]->src[1]; break; + case 1: cache_k_permute = cgraph->nodes[i]->src[1]->src[0]; break; + case 2: cache_k_permute = cgraph->nodes[i]->src[0]->src[0]; break; + default: cache_k_permute = cgraph->nodes[i]->src[0]->src[0]->src[0]; break; + } + const ggml_tensor * cache_k_view = cache_k_permute->src[0]; + if (cache_k_view->op != GGML_OP_VIEW) { + continue; + } + const ggml_tensor * leaf = cache_k_view->src[0]; + auto layer = extract_layer_from_name(leaf->name); + if (!layer.has_value()) { + continue; + } + layer_extent[layer.value()] = leaf->ne[1]; + layer_mask[layer.value()] = mask; + max_extent = std::max(max_extent, leaf->ne[1]); + } + + for (const auto & [layer, extent] : layer_extent) { + if (extent < max_extent) { + model_params.swa_layers.push_back(layer); + if (model_params.swa_mask == nullptr) { + model_params.swa_mask = layer_mask[layer]; + } + } + } + std::sort(model_params.swa_layers.begin(), model_params.swa_layers.end()); + + if (ggml_openvino_getenv_int("GGML_OPENVINO_LOG_SWA_LAYERS")) { + std::string per_layer; + for (const auto & [layer, extent] : layer_extent) { + per_layer += " " + std::to_string(layer) + ":" + std::to_string(extent) + + (extent < max_extent ? "(swa)" : ""); + } + GGML_LOG_WARN("ov-swa: attn_layers=%zu max_extent=%ld swa_layers=%zu |%s\n", layer_extent.size(), + (long) max_extent, model_params.swa_layers.size(), per_layer.c_str()); + } + } + bool rope_seen = false; for (int i = 0; i < cgraph->n_nodes; i++) { auto * node = cgraph->nodes[i]; @@ -654,11 +791,14 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr ggml_tensor * cache_k = cache_k_view->src[0]; int layer = extract_layer_from_name(cache_k->name).value(); - std::string mask_name(mask->name); + // Classified by the pre-pass above, which groups layers by mask tensor identity. The + // mask NAME cannot be used: build_attn_inp_kq_mask() gives both masks the same name. + const bool layer_is_swa = std::find(model_params.swa_layers.begin(), model_params.swa_layers.end(), + layer) != model_params.swa_layers.end(); model_params.kv_buffer_ctx_id = ggml_backend_openvino_buffer_get_ctx_id(cache_k->buffer); - if (mask_name.find("swa") != std::string::npos) { - model_params.swa_layers.push_back(layer); + model_params.n_heads_kv_per_layer[layer] = cache_k_permute->ne[2]; + if (layer_is_swa) { model_params.ctx_per_seq_swa = cache_k->ne[1]; } else { model_params.ctx_per_seq = cache_k->ne[1]; @@ -671,8 +811,9 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr memcpy(&offset, cache_k_view->op_params, sizeof(size_t)); compute_params.seq_active_start = offset / seq_size; - if (mask_name.find("swa") != std::string::npos) { + if (layer_is_swa) { compute_params.attention_size_swa = mask->ne[0]; + compute_params.swa_window = get_swa_window_from_mask(mask); } else { compute_params.attention_size = mask->ne[0]; } @@ -708,11 +849,11 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr // mixed SWA/non-SWA layers with different n_dims or freq_base), we cannot // share a single precomputed rope_sin/rope_cos. Track divergence so the // translator falls back to per-op make_sin_cos in that case. - static_assert(sizeof(model_params.rope_params) == sizeof(int32_t) * 15, "rope_params size"); + static_assert(sizeof(model_params.rope_params) == sizeof(int32_t) * 16, "rope_params size"); if (!rope_seen) { - memcpy(model_params.rope_params, node->op_params, sizeof(int32_t) * 15); + memcpy(model_params.rope_params, node->op_params, sizeof(int32_t) * 16); rope_seen = true; - } else if (memcmp(model_params.rope_params, node->op_params, sizeof(int32_t) * 15) != 0) { + } else if (memcmp(model_params.rope_params, node->op_params, sizeof(int32_t) * 16) != 0) { model_params.mixed_rope_params = true; } } @@ -752,8 +893,41 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr } } } + if (model_params.n_heads_kv == -1) { + for (int i = 0; i < cgraph->n_nodes; i++) { + const auto * node = cgraph->nodes[i]; + const ggml_tensor * mask = nullptr; + if (node->op == GGML_OP_SOFT_MAX) { + mask = node->src[1]; + } else if (node->op == GGML_OP_FLASH_ATTN_EXT) { + mask = node->src[3]; + } else { + continue; + } + if (mask == nullptr || mask->op != GGML_OP_NONE || !(mask->flags & GGML_TENSOR_FLAG_INPUT) || + node->src[0] == nullptr) { + continue; + } + model_params.is_cacheless_attn = true; + model_params.n_seq = 1; + model_params.ctx_per_seq = mask->ne[0]; + compute_params.input_len = node->src[0]->ne[1]; + compute_params.token_len_per_seq = compute_params.input_len; + break; + } + } + auto * output_tensor = cgraph->nodes[cgraph->n_nodes - 1]; compute_params.output_len = output_tensor->ne[1]; + if (model_params.is_cacheless_attn) { + for (int i = 0; i < cgraph->n_nodes; i++) { + const auto * node = cgraph->nodes[i]; + if (node->op == GGML_OP_GET_ROWS && is_output_idx(node->src[1], node)) { + compute_params.output_len = node->src[1]->ne[0]; + break; + } + } + } // for NPU, output_len is always 1 except for llama-perplexity if (is_static && compute_params.output_len == 0) { compute_params.output_len = 1; @@ -790,6 +964,10 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op, // output index input_shape = ov::PartialShape{1, 1, 1, m_is_static ? m_compute_params.output_len : -1}; + } else if (is_inp_mean(input, op)) { + input_shape = m_is_static ? ov::PartialShape{1, 1, input->ne[1], m_prefill_chunk_size} : + ov::PartialShape{1, 1, -1, -1}; + } else if (is_inp_mask(input, op)) { // mask if (m_is_static) { @@ -814,11 +992,19 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op, 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]. + // NOTE: Gemma4 uses per-layer-type KV shapes, so no single scalar describes every + // layer. E2B varies only the head size (sliding 256, full 512); 12B also varies the + // head COUNT (sliding 8 x 256, full 1 x 512). Take the head count for this tensor's + // own layer type and derive the head size from its own combined dim, so both layer + // types get the correct split. Using the model-level count split 12B's sliding + // states as 1 x 2048 and decoded garbage. assert(input_shape.size() == 4 && input_shape[0] == 1 && input_shape[1] == 1 && - input_shape[2].is_dynamic() && - input_shape[3] == (m_model_params.n_heads_kv * m_model_params.head_size)); - input_shape = {input_shape[0], ov::Dimension::dynamic(), m_model_params.n_heads_kv, - m_model_params.head_size}; + input_shape[2].is_dynamic() && input_shape[3].is_static()); + const int n_heads_kv = get_n_heads_kv_for_tensor(input); + assert(n_heads_kv > 0 && input_shape[3].get_length() % n_heads_kv == 0); + const int64_t combined_dim = input_shape[3].get_length(); // n_heads_kv * head_size + const int64_t head_size = combined_dim / n_heads_kv; + input_shape = {input_shape[0], ov::Dimension::dynamic(), n_heads_kv, head_size}; } } else if (is_kv_idx(input, op)) { @@ -840,8 +1026,14 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op, if (op->op == GGML_OP_SOFT_MAX && op->src[1] != nullptr && op->src[1]->op == GGML_OP_NONE && op->src[1]->flags & GGML_TENSOR_FLAG_INPUT && op->src[1] == input) { // for softmax input mask, the shape is [1, 1, seq_active, seq_active], where seq_active is determined by the input active sequence length instead of the kv cache sequence length - input_shape[2] = -1; - input_shape[3] = -1; + if (m_is_static) { + const int64_t seq_active = m_is_prefill ? m_prefill_chunk_size : 1; + input_shape[2] = seq_active; + input_shape[3] = seq_active; + } else { + input_shape[2] = -1; + input_shape[3] = -1; + } } return input_shape; } @@ -894,6 +1086,10 @@ void GgmlOvDecoder::add_extra_inputs() { if (m_compute_params.attention_size_swa != -1) { create_1d_input("attention_size_swa", m_compute_params.attention_size_swa); } + // only the stateful SWA mask consumes this + if (is_stateful() && m_compute_params.swa_window != -1) { + create_1d_input("swa_window", m_compute_params.swa_window); + } create_1d_input("n_seq_active", m_compute_params.n_seq_active); create_1d_input("seq_active_start", m_compute_params.seq_active_start); create_1d_input("seq_active_end", m_compute_params.seq_active_start + m_compute_params.n_seq_active); diff --git a/ggml/src/ggml-openvino/ggml-decoder.h b/ggml/src/ggml-openvino/ggml-decoder.h index 74cb7385029a..7f9d45a48a87 100644 --- a/ggml/src/ggml-openvino/ggml-decoder.h +++ b/ggml/src/ggml-openvino/ggml-decoder.h @@ -21,18 +21,28 @@ struct ModelParams { int ctx_per_seq_swa = -1; int n_seq = 1; int n_heads_kv = -1; + // Per-layer KV head count. gemma-4 12B interleaves 8 x 256 sliding layers with 1 x 512 + // full-attention layers, so no single scalar describes every layer. Keyed by layer, not by + // layer TYPE, because the SWA classification depends on the context size (extents tie at a + // small -c) while the head count does not. + std::map<int, int> n_heads_kv_per_layer; int head_size = -1; int state_size = -1; // for SSM molels, eg qwen35 - int32_t rope_params[15]; + int32_t rope_params[16]; bool mixed_rope_params = false; + bool is_cacheless_attn = false; std::vector<int> swa_layers; + // The sliding-window mask tensor, identified in compute_llm_params() by grouping attention + // layers on the mask they consume. Only used to tell the two masks apart when naming OV + // parameters -- both carry the same tensor name. Null when the graph has a single mask. + const ggml_tensor * swa_mask = nullptr; std::vector<std::string> kv_names; size_t kv_buffer_ctx_id = 0; bool same_rope_params(const ModelParams & other) const { return mixed_rope_params == other.mixed_rope_params && - memcmp(rope_params, other.rope_params, sizeof(int32_t) * 15) == 0; + memcmp(rope_params, other.rope_params, sizeof(int32_t) * 16) == 0; } bool can_reuse_dynamically(const ModelParams & other) const { return same_rope_params(other); } @@ -48,6 +58,11 @@ struct ComputeParams { int attention_size = -1; int attention_size_swa = -1; int attention_size_static = -1; // encoder/cross-attn KV fill level (whisper) + // Sliding window width, read back from the band of ggml's own SWA mask. ggml never passes + // n_swa down to a backend, but fill_mask() bakes it into the mask contents, so the widest + // unmasked row recovers it. Shorter than n_swa while the sequence is still short, which is + // harmless: every causal pair is inside the window then anyway. + int swa_window = -1; int input_len = -1; int token_len_per_seq = -1; int past_kv_len = -1; @@ -96,8 +111,15 @@ struct ComputeParams { // models use a fixed end-anchored offset in the translator. }; +// defined below; declared here because GgmlOvDecoder uses it inline +std::optional<int> extract_layer_from_name(const std::string & name); + +// detects the MoE expert-plane-sum ADD chain (see definition); used by supports_op too +bool is_moe_expert_sum_add(const ggml_tensor * node); + class GgmlOvDecoder : public ov::frontend::ggml::GgmlDecoder { public: + static std::string get_tensor_name(const ggml_cgraph * cgraph, const ggml_tensor * tensor); struct NodeInfo { ggml_tensor * node; std::string node_name; @@ -250,6 +272,21 @@ class GgmlOvDecoder : public ov::frontend::ggml::GgmlDecoder { m_model_params.swa_layers.end(); } + // KV head count for one layer. Sliding and full layers can differ (gemma-4 12B), so callers + // that reinterpret a KV buffer must use this and not the model-level n_heads_kv. + int get_n_heads_kv_for_layer(int layer) const { + auto it = m_model_params.n_heads_kv_per_layer.find(layer); + return it != m_model_params.n_heads_kv_per_layer.end() ? it->second : m_model_params.n_heads_kv; + } + + // Same, for a KV cache tensor: its layer comes from the leaf name (cache_k_l<N>). + int get_n_heads_kv_for_tensor(const ggml_tensor * kv_tensor) const { + if (auto layer = extract_layer_from_name(std::string(kv_tensor->name)); layer.has_value()) { + return get_n_heads_kv_for_layer(layer.value()); + } + return m_model_params.n_heads_kv; + } + int get_past_kv_len() const { return m_compute_params.past_kv_len; } int get_input_len() const { return m_compute_params.input_len; } @@ -340,6 +377,12 @@ class GgmlOvDecoder : public ov::frontend::ggml::GgmlDecoder { (op->op == GGML_OP_SOFT_MAX && tensor == op->src[1]); } + inline static bool is_inp_mean(const ggml_tensor * tensor, const ggml_tensor * op) { + return op->op == GGML_OP_MUL_MAT && tensor == op->src[1] && tensor->op == GGML_OP_NONE && + (tensor->flags & GGML_TENSOR_FLAG_INPUT) && tensor->type == GGML_TYPE_F32 && + op->src[0] != nullptr && op->src[0]->op != GGML_OP_NONE; + } + inline static bool is_rope_freqs_weight(const ggml_tensor * tensor, const ggml_tensor * op) { return op->op == GGML_OP_ROPE && tensor == op->src[2]; } @@ -353,10 +396,21 @@ class GgmlOvDecoder : public ov::frontend::ggml::GgmlDecoder { (op != nullptr && op->op == GGML_OP_SET_ROWS && op->src[2] == tensor); } + inline static bool is_conv_state_writeback(const ggml_tensor * node) { + return node->op == GGML_OP_CPY && node->view_src != nullptr && is_kvcache(node->view_src, nullptr) && + node->src[0] != nullptr && node->src[0]->op == GGML_OP_VIEW && node->src[0]->src[0] != nullptr && + node->src[0]->src[0]->op == GGML_OP_CONCAT && node->src[1] != nullptr && + node->src[1]->op == GGML_OP_VIEW && node->src[1]->view_src == node->view_src; + } + inline static bool is_kv_idx(const ggml_tensor * tensor, const ggml_tensor * op) { return op->op == GGML_OP_SET_ROWS && op->src[1] == tensor; } + bool is_swa_mask(const ggml_tensor * tensor) const { + return m_model_params.swa_mask != nullptr && tensor == m_model_params.swa_mask; + } + inline static bool is_output_idx(const ggml_tensor * tensor, const ggml_tensor * op) { return op->op == GGML_OP_GET_ROWS && tensor == op->src[1] && op->src[0]->op != GGML_OP_NONE && op->src[1]->op == GGML_OP_NONE; @@ -375,8 +429,22 @@ class GgmlOvDecoder : public ov::frontend::ggml::GgmlDecoder { if (is_inp_emb(tensor, op)) { return "embd"; } - if (is_stateful() && is_inp_mask(tensor, op)) { - return std::string(tensor->name).find("swa") == std::string::npos ? "self_kq_mask" : "self_kq_mask_swa"; + if (is_inp_mask(tensor, op)) { + // Give the two attention masks distinct OV parameter names. + // + // An interleaved-SWA model builds one full-attention mask and one sliding-window mask, + // but build_attn_inp_kq_mask() names them identically, so keying a parameter off + // tensor->name alone makes the second mask OVERWRITE the first in m_model_inputs: both + // attention types then read a single parameter, and the windowed layers silently run + // against an unbanded mask. Disambiguate using the SWA layer set computed in + // compute_llm_params(), which classifies by mask tensor identity rather than by name. + // + // When no SWA layer was found there is only one mask in play, so the plain name is + // correct and no _swa parameter is created. + if (m_model_params.swa_layers.empty()) { + return "self_kq_mask"; + } + return is_swa_mask(tensor) ? "self_kq_mask_swa" : "self_kq_mask"; } return tensor->name; } diff --git a/ggml/src/ggml-openvino/ggml-openvino-extra.cpp b/ggml/src/ggml-openvino/ggml-openvino-extra.cpp index 36dfa4d9471b..52e1a297c2d7 100644 --- a/ggml/src/ggml-openvino/ggml-openvino-extra.cpp +++ b/ggml/src/ggml-openvino/ggml-openvino-extra.cpp @@ -31,6 +31,7 @@ void ggml_openvino_device_config::init() { // String values (use ggml_openvino_getenv_str) "GGML_OPENVINO_DEVICE", "GGML_OPENVINO_CACHE_DIR", + "GGML_OPENVINO_SPILL_DIR", "GGML_OPENVINO_DEBUG_NODE", "GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR", "GGML_OPENVINO_NPU_COMPILE_CONFIG", @@ -56,6 +57,11 @@ void ggml_openvino_device_config::init() { "GGML_OPENVINO_RELEASE_WEIGHTS", "GGML_OPENVINO_REDUCE_COMPILE_MEM", "GGML_OPENVINO_LOG_UNSUPPORTED_OPS", + "GGML_OPENVINO_LOG_SWA_LAYERS", + "GGML_OPENVINO_NATIVE_SOFTPLUS", + "GGML_OPENVINO_DISABLE_REMOTE_OUTPUTS", + "GGML_OPENVINO_REQUANT_KQUANT", + "GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT", }; for (const char * const & env_var : env_var_names) { @@ -263,9 +269,81 @@ std::optional<ExtraQuantType> ggml_openvino_get_requant_type(const ggml_tensor * if (ggml_openvino_is_npu()) { return ExtraQuantType::Q4_0_128; } + // By default Q6_K/Q5_K are requantized to Q8_0_C, which *inflates* 6- and 5-bit weights to 8 + // while the rest of the model stays at 4 bits, and Q4_K keeps its native group-32 layout + // (an f16 scale plus an f16 zero point per 32 weights = 0.125 B/weight of metadata). + // Decode of a large model is bandwidth-bound, so both cost throughput. + // + // GGML_OPENVINO_REQUANT_KQUANT selects a 4-bit target instead. Names are + // q4_<sym|asym><group>[_all]: <sym|asym> says whether a per-group zero point is kept, <group> + // is the group size, and the _all suffix sends Q4_K down the same path (without it only + // Q6_K/Q5_K are touched): + // q4_sym128 Q6_K/Q5_K -> Q4_0_128 (u4, group 128, symmetric) + // q4_sym128_all and Q4_K too -- drops Q4_K's per-32 zero point, which costs some accuracy + // q4_asym64_all Q6_K/Q5_K and Q4_K -> Q4_1_64 (u4, group 64, asymmetric) -- most of the + // metadata saving while keeping a real zero point + // native no requantization at all (keep Q6_K/Q5_K as they are) + // + // The asymmetric target is only offered in its _all form: leaving Q4_K at its native group 32 + // while Q6_K/Q5_K move to group 64 gives the Q/K/V projections different group counts, and the + // GPU plugin's FullyConnectedHorizontalFusion concatenates their scale constants, which then + // fails shape inference. Requantizing all three keeps the group size uniform. + const char * rq = ggml_openvino_getenv_str("GGML_OPENVINO_REQUANT_KQUANT"); + auto is_opt = [rq](const char * name) { + return rq && strcmp(rq, name) == 0; + }; + const bool sym128 = is_opt("q4_sym128"); + const bool sym128_all = is_opt("q4_sym128_all"); + const bool asym64_all = is_opt("q4_asym64_all"); + + if (tensor->type == GGML_TYPE_Q4_K) { + if (sym128_all) { + return ExtraQuantType::Q4_0_128; + } + if (asym64_all) { + return ExtraQuantType::Q4_1_64; + } + } + // MoE expert weights (3D, ne[2] = n_expert) stored as Q5_1/Q8_0 are the expert-side + // equivalent of Q6_K/Q5_K: kept at 8 bits by default while the rest of the model is at 4 + // (gemma-4 26B-A4B keeps its down projection there). Send them to 4 bits under the same + // option, at group 64 rather than 128: the down expert has k=704, which 64 divides + // (704/64 = 11) and 128 does not. + if (tensor->ne[2] > 1 && (tensor->type == GGML_TYPE_Q5_1 || tensor->type == GGML_TYPE_Q8_0)) { + if (sym128 || sym128_all) { + return ExtraQuantType::Q4_0_64; + } + if (asym64_all) { + return ExtraQuantType::Q4_1_64; + } + // TODO: temporary workaround for a known OpenVINO GPU-plugin bug -- remove once the + // plugin computes grouped 8-bit GatherMatmulCompressed correctly. This costs accuracy + // (5/8-bit -> 4-bit) on any model it applies to, so it must not outlive the bug. + // + // On GPU these would otherwise stay in their native *grouped 8-bit* layout, which the GPU + // plugin's GatherMatmulCompressed computes incorrectly -- gemma-4 26B-A4B (whose down + // projection is Q5_1) produces garbage, while the same graph is correct on CPU. It is + // specific to grouped 8 bit: the gate/up experts are grouped u4 *with* a zero point and + // are fine, and Qwen3.5 / granite are fine because their Q5_K/Q6_K down projections + // already requantize to per-channel Q8_0_C (grouped=0). Sending these to grouped 4 bit + // avoids the broken layout and restores correct output. + // Opt out with GGML_OPENVINO_REQUANT_KQUANT=native. + if (ggml_openvino_get_device_name() == "GPU" && !is_opt("native")) { + return ExtraQuantType::Q4_0_64; + } + } switch (tensor->type) { case GGML_TYPE_Q6_K: case GGML_TYPE_Q5_K: + if (sym128 || sym128_all) { + return ExtraQuantType::Q4_0_128; + } + if (asym64_all) { + return ExtraQuantType::Q4_1_64; + } + if (is_opt("native")) { + return std::nullopt; + } return ExtraQuantType::Q8_0_C; default: return std::nullopt; @@ -331,6 +409,16 @@ ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_ten layout.weights_per_block = 128; layout.is_symmetric = true; break; + case ExtraQuantType::Q4_1_64: + layout.is_u4 = true; + layout.weights_per_block = 64; + layout.is_symmetric = false; + break; + case ExtraQuantType::Q4_0_64: + layout.is_u4 = true; + layout.weights_per_block = 64; + layout.is_symmetric = true; + break; case ExtraQuantType::Q4_0_C: layout.is_u4 = true; layout.weights_per_block = tensor->ne[0]; diff --git a/ggml/src/ggml-openvino/ggml-openvino-extra.h b/ggml/src/ggml-openvino/ggml-openvino-extra.h index 0916b416258f..9d827d969452 100644 --- a/ggml/src/ggml-openvino/ggml-openvino-extra.h +++ b/ggml/src/ggml-openvino/ggml-openvino-extra.h @@ -15,7 +15,10 @@ #include <string> // ExtraQuantType enum - defines requantization target formats -enum class ExtraQuantType { F16, Q4_0_C, Q8_1_C, Q4_0_128, Q8_0_C, Q8_0_32 }; +// Q4_1_64: u4, group 64, *true* asymmetric (per-group scale and zero point). Note that +// Q4_0_128/Q4_0_C are symmetric despite taking the unsigned branch of quantize_q4_0 -- that branch +// pins zp to 8 with d = max/-8, which is algebraically symmetric. +enum class ExtraQuantType { F16, Q4_0_C, Q8_1_C, Q4_0_128, Q4_0_64, Q8_0_C, Q8_0_32, Q4_1_64 }; ov::Core & ov_singleton_core(); diff --git a/ggml/src/ggml-openvino/ggml-openvino.cpp b/ggml/src/ggml-openvino/ggml-openvino.cpp index a7956227830b..044b4da1c90f 100644 --- a/ggml/src/ggml-openvino/ggml-openvino.cpp +++ b/ggml/src/ggml-openvino/ggml-openvino.cpp @@ -10,7 +10,10 @@ #include "ggml.h" #include <atomic> +#include <cerrno> +#include <climits> #include <cstdint> +#include <cstdio> #include <cstdlib> #include <cstring> #include <memory> @@ -25,6 +28,11 @@ #include <string> #include <vector> +#ifndef _WIN32 +# include <sys/mman.h> +# include <unistd.h> +#endif + #if defined(_WIN32) # define WIN32_LEAN_AND_MEAN # ifndef NOMINMAX @@ -64,6 +72,11 @@ struct ggml_backend_openvino_buffer_context { size_t size; bool is_remote; + // Set when the buffer is a file-backed spill mapping (GGML_OPENVINO_SPILL_DIR); it must be + // munmap'd rather than freed. + void * spill_mapping = nullptr; + size_t spill_size = 0; + // Wrapping of the buffer std::shared_ptr<ov::Tensor> ov_buffer; @@ -98,10 +111,56 @@ struct ggml_backend_openvino_buffer_context { data = usm_tensor.get(); ov_buffer = std::make_shared<ov::intel_gpu::ocl::USMTensor>(std::move(usm_tensor)); } else { - data = ggml_aligned_malloc(size); - GGML_ASSERT(data); - memset(data, 0, size); - ov_buffer = std::make_shared<ov::Tensor>(ov::element::u8, ov::Shape{size}, data); +#ifndef _WIN32 + if (const char * spill_dir = ggml_openvino_getenv_str("GGML_OPENVINO_SPILL_DIR")) { + // Disk-backed weight buffer: back the repacked weights with a temp file via MAP_SHARED + // instead of anonymous memory. Anonymous pages can only be evicted to swap, so the + // repacked buffer stays pinned alongside the mmap'd source and both are resident at once + // -- that double residency is the load-time peak. File-backed pages are reclaimable: the + // kernel can write them back and drop them under pressure, then re-read on demand, so RSS + // becomes a working set rather than the whole buffer. The file is unlinked immediately, + // so it disappears when the process exits. + // + // The directory must be real storage. Pointing this at a tmpfs mount (/tmp on many + // systems) backs the "spill" with RAM and makes matters worse. + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/ggml-ov-weights-%d-XXXXXX", spill_dir, (int) getpid()); + int fd = mkstemp(path); + if (fd < 0) { + GGML_LOG_ERROR("%s: mkstemp(%s) failed: %s\n", __func__, path, strerror(errno)); + return; + } + unlink(path); // anonymous-but-file-backed: freed on process exit + if (ftruncate(fd, (off_t) size) != 0) { + GGML_LOG_ERROR("%s: ftruncate(%zu) failed: %s\n", __func__, size, strerror(errno)); + close(fd); + return; + } + void * m = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); // the mapping keeps the file alive + if (m == MAP_FAILED) { + GGML_LOG_ERROR("%s: mmap(%zu) failed: %s\n", __func__, size, strerror(errno)); + return; + } + data = m; + spill_mapping = m; + spill_size = size; + GGML_LOG_INFO("%s: weight buffer spilled to %s (%zu MB, file-backed)\n", __func__, spill_dir, + size / 1024 / 1024); + ov_buffer = std::make_shared<ov::Tensor>(ov::element::u8, ov::Shape{size}, data); + } else +#endif + { +#ifdef _WIN32 + if (ggml_openvino_getenv_str("GGML_OPENVINO_SPILL_DIR")) { + GGML_LOG_WARN("%s: GGML_OPENVINO_SPILL_DIR is not supported on Windows, ignoring\n", __func__); + } +#endif + data = ggml_aligned_malloc(size); + GGML_ASSERT(data); + memset(data, 0, size); + ov_buffer = std::make_shared<ov::Tensor>(ov::element::u8, ov::Shape{size}, data); + } } if (data == nullptr) { @@ -124,6 +183,11 @@ struct ggml_backend_openvino_buffer_context { delete pair.second; } tensor_extras.clear(); +#ifndef _WIN32 + if (spill_mapping != nullptr) { + munmap(spill_mapping, spill_size); + } else +#endif if (!is_remote && data != nullptr) { ggml_aligned_free(data, size); } @@ -611,9 +675,7 @@ GGML_BACKEND_API ggml_backend_buffer_type_t ggml_backend_openvino_buffer_type(in static const char * ggml_backend_openvino_host_buffer_type_get_name(ggml_backend_buffer_type_t buft) { ggml_backend_openvino_buffer_type_context * ctx = (ggml_backend_openvino_buffer_type_context *) buft->context; - static std::string name; - name = ctx->name + "_HOST"; - return name.c_str(); + return ctx->name.c_str(); } static bool ggml_backend_openvino_host_buffer_type_is_host(ggml_backend_buffer_type_t buft) { @@ -646,7 +708,7 @@ GGML_BACKEND_API ggml_backend_buffer_type_t ggml_backend_openvino_host_buffer_ty for (int i = 0; i < device_count; i++) { buffer_type_contexts[i].device = i; - buffer_type_contexts[i].name = std::string(GGML_OPENVINO_NAME) + std::to_string(i); + buffer_type_contexts[i].name = std::string(GGML_OPENVINO_NAME) + std::to_string(i) + "_HOST"; buffer_types[i] = ggml_backend_buffer_type{ /* .iface = */ ggml_backend_openvino_host_buffer_type_interface, @@ -711,13 +773,16 @@ static void ggml_backend_openvino_free(ggml_backend_t backend) { if (ctx->runtime_context) { auto r_ctx = std::static_pointer_cast<ov_runtime_context>(ctx->runtime_context); - if (--r_ctx->backend_count == 0) { + auto cache = r_ctx->compiled_cache; + r_ctx->clear_caches(); + std::lock_guard<std::mutex> cache_lock(cache->mutex); + if (--cache->backend_count == 0) { // If host weight buffers were released (GGML_OPENVINO_RELEASE_WEIGHTS), the // dropped pages can never be repopulated, so a recompile is impossible. Keep // the compiled-model cache alive across backend teardown so the next context // reuses it instead of recompiling against zeroed weights. if (!ggml_openvino_weight_buffers_released()) { - r_ctx->clear_caches(); + cache->graphs.clear(); } } } @@ -766,12 +831,14 @@ static ggml_guid_t ggml_backend_openvino_guid(void) { } static std::shared_ptr<ov_runtime_context> get_ov_runtime_context_ptr() { - static std::shared_ptr<ov_runtime_context> r_ctx = [] { - auto ctx = std::make_shared<ov_runtime_context>(); - ctx->device = ggml_openvino_get_device_name(); - ctx->stateful = is_stateful_enabled() && !ggml_openvino_is_npu(); - return ctx; - }(); + // Share compiled models, but give every backend its own requests and KV state. + static auto cache = std::make_shared<ov_compiled_model_cache>(); + auto r_ctx = std::make_shared<ov_runtime_context>(); + r_ctx->device = ggml_openvino_get_device_name(); + r_ctx->stateful = is_stateful_enabled() && !ggml_openvino_is_npu(); + r_ctx->compiled_cache = cache; + std::lock_guard<std::mutex> cache_lock(cache->mutex); + ++cache->backend_count; return r_ctx; } @@ -795,9 +862,6 @@ GGML_BACKEND_API ggml_backend_t ggml_backend_openvino_init(int device) { return nullptr; } - std::shared_ptr<ov_runtime_context> r_ctx = std::static_pointer_cast<ov_runtime_context>(ctx->runtime_context); - r_ctx->backend_count++; - ggml_backend_t openvino_backend = new ggml_backend{ /* .guid = */ ggml_backend_openvino_guid(), /* .interface = */ ggml_backend_openvino_interface, @@ -928,6 +992,10 @@ static bool is_supported_flash_attn_pattern(const ggml_tensor * op) { if (src->src[0] == nullptr || src->src[0]->view_src != nullptr) { return false; } + } else if (src->op == GGML_OP_CPY) { + if (src->src[0] == nullptr || src->src[0]->op != GGML_OP_PERMUTE || src->src[0]->src[0] == nullptr) { + return false; + } } else { return false; } @@ -995,7 +1063,7 @@ static bool cpy_output_view_is_supported(const ggml_tensor * op) { return false; } - return ggml_nbytes(op) == 0 || ggml_is_contiguous(op); + return ggml_nbytes(op) == 0 || ggml_is_contiguous(op) || GgmlOvDecoder::is_conv_state_writeback(op); } static bool mul_mat_id_requires_large_tmp(const ggml_tensor * op) { @@ -1123,6 +1191,10 @@ static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) { if (op->src[1]->op == GGML_OP_PERMUTE) { return {false, "ADD/MUL/SUB with PERMUTE src1 is not supported"}; } + // >8-expert MoE ReduceSum drifts past the 1e-7 tolerance (f32 order vs CPU); intermittent. + if (op->op == GGML_OP_ADD && is_moe_expert_sum_add(op) && op->src[1]->src[0]->ne[1] > 8) { + return {false, "MoE expert-plane sum with more than 8 experts is not supported"}; + } for (int i = 0; i < 4; i++) { if (op->src[0]->ne[i] != op->src[1]->ne[i] && (op->src[0]->ne[i] != 1 && op->src[1]->ne[i] != 1)) { return {false, "ADD/MUL/SUB with incompatible broadcast shapes: src0->ne[" + std::to_string(i) + "]=" + @@ -1207,8 +1279,11 @@ static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) { break; } case GGML_OP_CPY: { - if (op->src[0]->type == GGML_TYPE_BF16 || op->src[1]->type == GGML_TYPE_BF16) { - return {false, "CPY with BF16 src type is not supported"}; + if (op->src[0]->type != GGML_TYPE_BF16 && op->src[1]->type == GGML_TYPE_BF16) { + return {false, "CPY with BF16 src[1] type is not supported"}; + } + if (ggml_openvino_get_device_name() == "NPU" && (op->src[0]->type == GGML_TYPE_BF16 || op->src[1]->type == GGML_TYPE_BF16)) { + return {false, "CPY with BF16 is not supported is not supported on NPU"}; } // CPY to a quantized destination (e.g. f32 -> q4_0) is numerically unstable with OpenVINO backend. if (ggml_is_quantized(op->type)) { @@ -1238,6 +1313,10 @@ static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) { op->src[0]->ne[0] == 256 && op->src[1]->ne[0] == 256) { return {false, "MUL_MAT quantized benchmark test case on GPU is not supported"}; } + if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_F32 && op->ne[0] == 1 && op->ne[1] == 1 && + (op->src[0]->buffer == nullptr || op->src[0]->buffer->usage != GGML_BACKEND_BUFFER_USAGE_WEIGHTS)) { + return {false, "MUL_MAT scalar dot product with non-weight src[0] on GPU is not supported"}; + } if (op->src[0]->ne[3] != op->src[1]->ne[3] && op->src[0]->ne[3] != 1 && op->src[1]->ne[3] != 1) { return {false, "MUL_MAT with incompatible broadcast on ne[3]: src0->ne[3]=" + std::to_string(op->src[0]->ne[3]) + ", src1->ne[3]=" + std::to_string(op->src[1]->ne[3])}; @@ -1254,14 +1333,23 @@ static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) { return {false, "MUL_MAT_ID with single-expert or empty ne[2] <= 1 (ne[2]=" + std::to_string(op->src[0]->ne[2]) + ") is not supported"}; } - if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->type == GGML_TYPE_BF16) { - return {false, "MUL_MAT_ID with BF16 weights on GPU is not supported"}; + if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && !ggml_is_quantized(op->src[0]->type)) { + return {false, "MUL_MAT_ID with non-quantized weights on GPU is not supported"}; + } + // The GPU plugin's GatherMatmul returns wrong values for the layouts test-backend-ops + // produces: it builds a rank-4 input layout ([n_used, n_tokens, k, 1]) instead of rank 3 + // and the kernel misreads it, silently returning garbage (NMSE ~86) rather than asserting. + // The same graph is correct on the CPU plugin, and correct on GPU for every real model, + // which always feeds experts from a bound tensor buffer. Standalone op-test tensors have + // no buffer at all, so use that to exclude them and let the scheduler run them on CPU. + if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->buffer == nullptr) { + return {false, "MUL_MAT_ID with unbound expert tensors on GPU is not supported"}; } - // GPU MUL_MAT_ID uses a Gather+MatMul fallback because the GPU plugin rejects internal - // GatherMatmul for these test shapes. Skip cases that would materialize a large selected - // expert-weight temporary. - if (ggml_openvino_get_device_name() == "GPU" && mul_mat_id_requires_large_tmp(op)) { - return {false, "MUL_MAT_ID requires large temporary on GPU"}; + // Only MXFP4 still needs the large-temporary guard; every other quantized type goes + // through GatherMatmul, which never materializes the selected expert weights. + if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->type == GGML_TYPE_MXFP4 && + mul_mat_id_requires_large_tmp(op)) { + return {false, "MUL_MAT_ID with MXFP4 weights requires large temporary on GPU"}; } break; } @@ -1269,36 +1357,39 @@ static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) { const int32_t * op_params = op->op_params; const int n_dims = op_params[1]; const int mode = op_params[2]; - if (op_params[15] != 0) { - // FIXME: support ggml_rope_set_offset - return {false, "ggml_rope_set_offset is not supported"}; - } + const int64_t n_offs = op_params[15]; if (mode != GGML_ROPE_TYPE_NORMAL && mode != GGML_ROPE_TYPE_NEOX && mode != GGML_ROPE_TYPE_IMROPE) { return {false, "ROPE with mode " + std::to_string(mode) + " is not supported"}; } + if (n_offs < 0 || (n_offs % 2) != 0) { + return {false, "ROPE with invalid n_offs=" + std::to_string(n_offs)}; + } const int64_t head_dim = op->src[0]->ne[0]; const int64_t rope_dims = n_dims == 0 ? head_dim : n_dims; - if (rope_dims <= 0 || rope_dims > head_dim || (rope_dims % 2) != 0) { - return {false, "ROPE with n_dims=" + std::to_string(n_dims) + ", head_dim=" + std::to_string(head_dim) + " is not supported"}; + if (rope_dims <= 0 || rope_dims + n_offs > head_dim || (rope_dims % 2) != 0) { + return {false, "ROPE with n_dims=" + std::to_string(n_dims) + ", n_offs=" + std::to_string(n_offs) + + ", head_dim=" + std::to_string(head_dim) + " is not supported"}; } if (op->type != GGML_TYPE_F32 && op->type != GGML_TYPE_F16) { return {false, "ROPE with type " + std::string(ggml_type_name(op->type)) + " is not supported"}; } - if (op->src[0]->op == GGML_OP_VIEW) { - const struct ggml_tensor * view = op->src[0]; - const struct ggml_tensor * view_src = view->view_src; - if (view_src->ne[1] != view->ne[1] || view_src->ne[2] != view->ne[2] || view_src->ne[3] != view->ne[3]) { - return {false, "ROPE with view_src->ne [" + std::to_string(view_src->ne[1]) + ", " + - std::to_string(view_src->ne[2]) + ", " + std::to_string(view_src->ne[3]) + - "] != view->ne [" + std::to_string(view->ne[1]) + ", " + - std::to_string(view->ne[2]) + ", " + std::to_string(view->ne[3]) + - "] is not supported"}; - } + if (op->view_src != nullptr && !ggml_is_contiguous(op->src[0])) { + return {false, "ROPE on VIEW / non-contiguous input is not supported"}; + } + if (op->src[0]->ne[3] > 1) { + // translate_rope's cos/sin tables cover one sequence only; ne[3] > 1 fails to broadcast. + return {false, "ROPE with multiple sequences (ne[3]=" + std::to_string(op->src[0]->ne[3]) + + ") is not supported"}; } + float freq_scale; + float ext_factor; + float attn_factor; + memcpy(&freq_scale, op_params + 6, sizeof(float)); + memcpy(&ext_factor, op_params + 7, sizeof(float)); + memcpy(&attn_factor, op_params + 8, sizeof(float)); if (mode == GGML_ROPE_TYPE_IMROPE && - (op->src[2] != 0 || ((const float *) op_params)[6] != 1 || ((const float *) op_params)[7] != 0 || - ((const float *) op_params)[8] != 1)) { - return {false, "IMROPE with freq_factors, freq_scale, ext_factor, and attn_factor is not supported"}; + (op->src[2] != nullptr || freq_scale != 1.0f || ext_factor != 0.0f || attn_factor != 1.0f)) { + return {false, "IMROPE with freq_factors, freq_scale, ext_factor, or attn_factor is not supported"}; } break; } diff --git a/ggml/src/ggml-openvino/ggml-quants.cpp b/ggml/src/ggml-openvino/ggml-quants.cpp index 120db01e17cd..93f9e8254aa6 100644 --- a/ggml/src/ggml-openvino/ggml-quants.cpp +++ b/ggml/src/ggml-openvino/ggml-quants.cpp @@ -851,7 +851,8 @@ std::shared_ptr<ov::Node> requantize_to_buffers(const ggml_tensor * tensor, const auto * type_traits = ggml_get_type_traits(tensor->type); const size_t src_row_bytes = ggml_row_size(tensor->type, ne0); - bool is_u4 = (requant_type == ExtraQuantType::Q4_0_C || requant_type == ExtraQuantType::Q4_0_128); + bool is_u4 = (requant_type == ExtraQuantType::Q4_0_C || requant_type == ExtraQuantType::Q4_0_128 || + requant_type == ExtraQuantType::Q4_0_64 || requant_type == ExtraQuantType::Q4_1_64); // Streaming dequant (opt-in via GGML_OPENVINO_REDUCE_COMPILE_MEM or // GGML_OPENVINO_MEMORY_OPTIMIZE): instead of @@ -879,7 +880,9 @@ std::shared_ptr<ov::Node> requantize_to_buffers(const ggml_tensor * tensor, result->set_friendly_name(tensor->name); return result; } - if (is_u4) { + if (requant_type == ExtraQuantType::Q4_1_64) { + quantize_q4_1_asym(weights_f32.data(), weights, scales, zp, n_elements, block_size); + } else if (is_u4) { quantize_q4_0(weights_f32.data(), weights, scales, zp, n_elements, block_size); } else if (requant_type == ExtraQuantType::Q8_1_C) { quantize_q8_1(weights_f32.data(), weights, scales, zp, n_elements, block_size); @@ -1178,6 +1181,71 @@ void quantize_q4_0(const float * x, } } +// Asymmetric u4 quantization with a per-group scale and zero point. +// +// Unlike quantize_q4_0's unsigned branch, which pins the zero point to 8 and is therefore +// symmetric, this keeps a real per-group zero point, so a group whose values are not centred on +// zero does not waste half its range. +void quantize_q4_1_asym(const float * x, + ov::Tensor & weights_arr, + ov::Tensor & scales_arr, + ov::Tensor & zp_arr, + int64_t k, + int64_t qk) { + assert(k % qk == 0); + const int nb = k / qk; + + auto * weights = static_cast<uint8_t *>(weights_arr.data()); + auto * scales = scales_arr.data<ov::element_type_traits<ov::element::f16>::value_type>(); + auto * zp = static_cast<uint8_t *>(zp_arr.data()); + + // u4 zero points are packed two per byte, low nibble first, indexed by group -- the same + // convention as the unsigned branch of quantize_q4_0. + auto store_zp = [zp](int i, uint8_t v) { + if (i % 2 == 0) { + zp[i / 2] = v & 0x0F; + } else { + zp[i / 2] |= (uint8_t) ((v & 0x0F) << 4); + } + }; + + for (int i = 0; i < nb; i++) { + float vmin = x[i * qk]; + float vmax = x[i * qk]; + for (int j = 1; j < qk; j++) { + const float v = x[i * qk + j]; + vmin = std::min(vmin, v); + vmax = std::max(vmax, v); + } + // Include 0 in the range so an all-positive or all-negative group still represents zero + // exactly -- these are weights, so an exact zero matters. + vmin = std::min(vmin, 0.0f); + vmax = std::max(vmax, 0.0f); + + const float d = (vmax - vmin) / 15.0f; + if (d == 0.0f) { + scales[i] = ov::float16(1.0f); + store_zp(i, 0); + memset(weights + i * qk / 2, 0, qk / 2); + continue; + } + const float id = 1.0f / d; + + // The zero point is itself a 4-bit integer, so round it and dequantize as (q - zq) * d. + const int zq = std::max(0, std::min(15, (int) lroundf(-vmin * id))); + scales[i] = ov::float16(d); + store_zp(i, (uint8_t) zq); + + for (int j = 0; j < qk / 2; ++j) { + const float x0 = x[i * qk + 2 * j] * id; + const float x1 = x[i * qk + 2 * j + 1] * id; + const uint8_t q0 = (uint8_t) std::max(0, std::min(15, (int) lroundf(x0) + zq)); + const uint8_t q1 = (uint8_t) std::max(0, std::min(15, (int) lroundf(x1) + zq)); + weights[i * qk / 2 + j] = (uint8_t) (q0 | (q1 << 4)); + } + } +} + void quantize_q8_0(const float * x, ov::Tensor & weights_arr, ov::Tensor & scales_arr, diff --git a/ggml/src/ggml-openvino/ggml-quants.h b/ggml/src/ggml-openvino/ggml-quants.h index e247255a7f77..d5273727e87d 100644 --- a/ggml/src/ggml-openvino/ggml-quants.h +++ b/ggml/src/ggml-openvino/ggml-quants.h @@ -122,6 +122,10 @@ inline const char * extra_quant_type_name(ExtraQuantType t) { return "Q8_0_32"; case ExtraQuantType::Q8_1_C: return "Q8_1_C"; + case ExtraQuantType::Q4_0_64: + return "Q4_0_64"; + case ExtraQuantType::Q4_1_64: + return "Q4_1_64"; default: return "unknown"; } @@ -166,6 +170,12 @@ void quantize_q8_1(const float * x, int64_t k, int64_t qk, int64_t block_offset = 0); +void quantize_q4_1_asym(const float * x, + ov::Tensor & weights_arr, + ov::Tensor & scales_arr, + ov::Tensor & zp_arr, + int64_t k, + int64_t qk); void quantize_q8_0(const float * x, ov::Tensor & weights_arr, ov::Tensor & scales_arr, diff --git a/ggml/src/ggml-openvino/openvino/frontend.cpp b/ggml/src/ggml-openvino/openvino/frontend.cpp index c2ba14e66e6e..88de86feacae 100644 --- a/ggml/src/ggml-openvino/openvino/frontend.cpp +++ b/ggml/src/ggml-openvino/openvino/frontend.cpp @@ -3,6 +3,7 @@ #include "input_model.h" #include "op_table.h" #include "translate_session.h" +#include <openvino/core/type.hpp> namespace ov { namespace frontend { @@ -11,7 +12,7 @@ namespace ggml { FrontEnd::FrontEnd() {} std::shared_ptr<Model> FrontEnd::convert(const InputModel::Ptr & model, bool naive) { - auto ggml_model = std::dynamic_pointer_cast<ggml::InputModel>(model); + auto ggml_model = ov::as_type_ptr<ggml::InputModel>(model); FRONT_END_GENERAL_CHECK(ggml_model, "Invalid input model"); std::shared_ptr<Model> converted_model; const auto & supported_ops = get_supported_ops(); diff --git a/ggml/src/ggml-openvino/openvino/node_context.h b/ggml/src/ggml-openvino/openvino/node_context.h index 2e2756037703..f1ea0e4f0eac 100644 --- a/ggml/src/ggml-openvino/openvino/node_context.h +++ b/ggml/src/ggml-openvino/openvino/node_context.h @@ -143,6 +143,10 @@ class NodeContext : public frontend::NodeContext { bool has_input(const std::string & name) const { return m_tensor_map->find(name) != m_tensor_map->end(); } + void put_shared(const std::string & name, const Output<Node> & value) const { + m_tensor_map->insert({name, value}); + } + const std::string & get_name() const override { return m_decoder->get_op_name(m_node_idx); } ov::Any get_attribute_as_any(const std::string & name) const override { return m_decoder->get_attribute(name); } diff --git a/ggml/src/ggml-openvino/openvino/op/add.cpp b/ggml/src/ggml-openvino/openvino/op/add.cpp index c43eb67f8d28..a45520d92e5b 100644 --- a/ggml/src/ggml-openvino/openvino/op/add.cpp +++ b/ggml/src/ggml-openvino/openvino/op/add.cpp @@ -5,6 +5,7 @@ #include <memory> #include <openvino/op/add.hpp> #include <openvino/op/constant.hpp> +#include <openvino/op/convert.hpp> #include <openvino/op/reduce_sum.hpp> #include <openvino/op/unsqueeze.hpp> @@ -35,7 +36,20 @@ OutputVector translate_add(const NodeContext & context) { auto input_0 = process_view_input_new(context, 0); auto input_1 = process_view_input_new(context, 1); - auto res = std::make_shared<ov::op::v1::Add>(input_0, input_1); + // opset1::Add needs matching types (e.g. fused ADD_ADD mixes f16/f32); add in f32, cast once. + auto output_type = context.get_output_type(); + if (input_0.get_element_type() != input_1.get_element_type()) { + if (input_0.get_element_type() != ov::element::f32) { + input_0 = std::make_shared<ov::op::v0::Convert>(input_0, ov::element::f32); + } + if (input_1.get_element_type() != ov::element::f32) { + input_1 = std::make_shared<ov::op::v0::Convert>(input_1, ov::element::f32); + } + } + ov::Output<ov::Node> res = std::make_shared<ov::op::v1::Add>(input_0, input_1); + if (res.get_element_type() != output_type) { + res = std::make_shared<ov::op::v0::Convert>(res, output_type); + } return rename_outputs_with_suffix({res}, context.get_name()); } diff --git a/ggml/src/ggml-openvino/openvino/op/diag.cpp b/ggml/src/ggml-openvino/openvino/op/diag.cpp index dacea2f05b4a..05e064892e17 100644 --- a/ggml/src/ggml-openvino/openvino/op/diag.cpp +++ b/ggml/src/ggml-openvino/openvino/op/diag.cpp @@ -3,11 +3,8 @@ #include "../utils.h" #include <openvino/op/constant.hpp> -#include <openvino/op/equal.hpp> +#include <openvino/op/eye.hpp> #include <openvino/op/multiply.hpp> -#include <openvino/op/range.hpp> -#include <openvino/op/reshape.hpp> -#include <openvino/op/select.hpp> namespace ov { namespace frontend { @@ -23,31 +20,13 @@ namespace op { OutputVector translate_diag(const NodeContext & context) { num_inputs_check(context, 1, 1); - auto x = context.get_input(0); // OV shape: [ne3, ne2, 1, ne0] + auto x = process_view_input_new(context, 0); // OV shape: [ne3, ne2, 1, ne0] - auto out_shape = context.get_output_shape().to_shape(); - int64_t n = static_cast<int64_t>(out_shape[3]); // ne0 + auto n = get_dimensions(x.get_node_shared_ptr(), {3}); + auto zero_diag = ov::op::v0::Constant::create(ov::element::i64, {}, {0}); - // Build index range [0, 1, ..., n-1] - auto start = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(0)}); - auto stop = ov::op::v0::Constant::create(ov::element::i64, {}, {n}); - auto step = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(1)}); - auto range = std::make_shared<ov::op::v4::Range>(start, stop, step, ov::element::i64); - - // col_idx shape [1, 1, 1, n] - auto col_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, 1, n}); - auto col_idx = std::make_shared<ov::op::v1::Reshape>(range, col_shape, false); - - // row_idx shape [1, 1, n, 1] - auto row_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, n, 1}); - auto row_idx = std::make_shared<ov::op::v1::Reshape>(range, row_shape, false); - - // mask: true where col == row (diagonal) - auto mask = std::make_shared<ov::op::v1::Equal>(col_idx, row_idx); - - // Broadcast input from [ne3, ne2, 1, ne0] to [ne3, ne2, ne0, ne0] via select - auto zero = ov::op::v0::Constant::create(ov::element::f32, {}, {0.0f}); - auto res = std::make_shared<ov::op::v1::Select>(mask, x, zero); + auto eye = std::make_shared<ov::op::v9::Eye>(n, n, zero_diag, x.get_element_type()); + auto res = std::make_shared<ov::op::v1::Multiply>(x, eye); return rename_outputs_with_suffix({res}, context.get_name()); } diff --git a/ggml/src/ggml-openvino/openvino/op/div.cpp b/ggml/src/ggml-openvino/openvino/op/div.cpp index 11dd9decec7a..2089ffd4c819 100644 --- a/ggml/src/ggml-openvino/openvino/op/div.cpp +++ b/ggml/src/ggml-openvino/openvino/op/div.cpp @@ -4,12 +4,14 @@ #include "ggml.h" #include <memory> +#include <openvino/core/type.hpp> #include <openvino/op/constant.hpp> #include <openvino/op/convert.hpp> #include <openvino/op/divide.hpp> #include <openvino/op/multiply.hpp> #include <openvino/op/shape_of.hpp> #include <openvino/op/sigmoid.hpp> +#include <openvino/op/swish.hpp> #include <openvino/op/tile.hpp> #include <openvino/op/util/precision_sensitive_attribute.hpp> #include <vector> @@ -33,22 +35,12 @@ bool is_silu_div_pattern(const ov::Output<ov::Node> & numerator, return false; } - auto mul = std::dynamic_pointer_cast<ov::op::v1::Multiply>(numerator.get_node_shared_ptr()); - if (!mul) { - return false; - } - const auto denom_node = denominator.get_node_shared_ptr(); - const auto mul_input_0 = mul->input_value(0).get_node_shared_ptr(); - const auto mul_input_1 = mul->input_value(1).get_node_shared_ptr(); - auto sigmoid = std::dynamic_pointer_cast<ov::op::v0::Sigmoid>(mul_input_1); - if (mul_input_0 == denom_node && sigmoid && sigmoid->input_value(0).get_node_shared_ptr() == denom_node) { - return true; + if (auto swish = ov::as_type_ptr<ov::op::v4::Swish>(numerator.get_node_shared_ptr())) { + return swish->input_value(0).get_node_shared_ptr() == denom_node; } - - sigmoid = std::dynamic_pointer_cast<ov::op::v0::Sigmoid>(mul_input_0); - return mul_input_1 == denom_node && sigmoid && sigmoid->input_value(0).get_node_shared_ptr() == denom_node; + return false; } ov::Output<ov::Node> repeat_input_to_match(const NodeContext & context, diff --git a/ggml/src/ggml-openvino/openvino/op/glu_geglu_quick.cpp b/ggml/src/ggml-openvino/openvino/op/glu_geglu_quick.cpp index c6d64aed43aa..385d75f5ffd6 100644 --- a/ggml/src/ggml-openvino/openvino/op/glu_geglu_quick.cpp +++ b/ggml/src/ggml-openvino/openvino/op/glu_geglu_quick.cpp @@ -6,8 +6,8 @@ #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> +#include <openvino/op/swish.hpp> namespace ov { namespace frontend { @@ -50,9 +50,7 @@ OutputVector translate_glu_geglu_quick(const NodeContext & context) { // 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 gated = std::make_shared<ov::op::v4::Swish>(src0, coef); auto res = std::make_shared<ov::op::v1::Multiply>(gated, src1); return rename_outputs_with_suffix({res}, context.get_name()); diff --git a/ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp b/ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp index d81fc53b5d02..7eea81d96e59 100644 --- a/ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp +++ b/ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp @@ -9,9 +9,10 @@ #include <openvino/op/add.hpp> #include <openvino/op/clamp.hpp> #include <openvino/op/constant.hpp> +#include <openvino/op/convert.hpp> #include <openvino/op/multiply.hpp> -#include <openvino/op/sigmoid.hpp> #include <openvino/op/slice.hpp> +#include <openvino/op/swish.hpp> namespace ov { namespace frontend { @@ -61,8 +62,7 @@ static std::pair<ov::Output<ov::Node>, ov::Output<ov::Node>> get_glu_inputs(cons OutputVector translate_glu_swiglu(const NodeContext & context) { auto [src0, src1] = get_glu_inputs(context); - auto sigmoid = std::make_shared<ov::op::v0::Sigmoid>(src0); - auto silu = std::make_shared<ov::op::v1::Multiply>(src0, sigmoid); + auto silu = std::make_shared<ov::op::v4::Swish>(src0); auto res = std::make_shared<ov::op::v1::Multiply>(silu, src1); return rename_outputs_with_suffix({res}, context.get_name()); @@ -77,9 +77,7 @@ OutputVector translate_glu_swiglu_oai(const NodeContext & context) { auto gate = std::make_shared<ov::op::v0::Clamp>(src0, -std::numeric_limits<float>::infinity(), limit); auto alpha_const = ov::op::v0::Constant::create(ov::element::f32, {}, {alpha}); - auto scaled_gate = std::make_shared<ov::op::v1::Multiply>(gate, alpha_const); - auto sigmoid = std::make_shared<ov::op::v0::Sigmoid>(scaled_gate); - auto out_glu = std::make_shared<ov::op::v1::Multiply>(gate, sigmoid); + auto out_glu = std::make_shared<ov::op::v4::Swish>(gate, alpha_const); auto up = std::make_shared<ov::op::v0::Clamp>(src1, -limit, limit); auto one = ov::op::v0::Constant::create(ov::element::f32, {}, {1.0f}); @@ -95,11 +93,22 @@ OutputVector translate_glu_swiglu_clamp(const NodeContext & context) { const int32_t * params = context.get_output_op_params(); const float limit = reinterpret_cast<const float *>(params)[3]; + // Compute in f32: f16 Swish/Clamp rounding drifts past the 1e-7 test tolerance. + auto output_type = context.get_output_type(); + if (src0.get_element_type() != ov::element::f32) { + src0 = std::make_shared<ov::op::v0::Convert>(src0, ov::element::f32); + } + if (src1.get_element_type() != ov::element::f32) { + src1 = std::make_shared<ov::op::v0::Convert>(src1, ov::element::f32); + } + auto gate = std::make_shared<ov::op::v0::Clamp>(src0, -std::numeric_limits<float>::infinity(), limit); - auto sigmoid = std::make_shared<ov::op::v0::Sigmoid>(gate); - auto silu = std::make_shared<ov::op::v1::Multiply>(gate, sigmoid); + auto silu = std::make_shared<ov::op::v4::Swish>(gate); auto up = std::make_shared<ov::op::v0::Clamp>(src1, -limit, limit); - auto res = std::make_shared<ov::op::v1::Multiply>(silu, up); + ov::Output<ov::Node> res = std::make_shared<ov::op::v1::Multiply>(silu, up); + if (res.get_element_type() != output_type) { + res = std::make_shared<ov::op::v0::Convert>(res, output_type); + } return rename_outputs_with_suffix({res}, context.get_name()); } diff --git a/ggml/src/ggml-openvino/openvino/op/moe_compressed.hpp b/ggml/src/ggml-openvino/openvino/op/moe_compressed.hpp new file mode 100644 index 000000000000..07e94c690152 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/op/moe_compressed.hpp @@ -0,0 +1,90 @@ +// Copyright (C) 2018-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// Local mirror of OpenVINO's internal ov::op::internal::MOE and MOECompressed ops. +// +// The class bodies are provided by the linked libopenvino.so; only the declarations are +// needed here so the backend can construct the node directly (same approach as +// GatherMatmul and GatedDeltaNet). The class layout must stay in sync with +// openvino/src/core/dev_api/openvino/op/moe.hpp +// openvino/src/common/transformations/include/ov_ops/moe_compressed.hpp +// +// \note MOE op classes are under development and subject to change. + +#pragma once + +#include <optional> + +#include "openvino/core/type/element_type.hpp" +#include "openvino/op/op.hpp" + +namespace ov::op::internal { + +class OPENVINO_API MOE : public ov::op::Op { +public: + OPENVINO_OP("MOE") + + MOE() = default; + + MOE(const OutputVector & args) : Op(args) {} + + enum class Expert_type { GEMM2_BIAS_SWIGLU_CLAMP, GEMM3_SWIGLU }; + + enum class Activation_type { SWIGLU, GEGLU_TANH, GEGLU_ERF }; + + struct Config { + Expert_type expert_type{ Expert_type::GEMM2_BIAS_SWIGLU_CLAMP }; + float expert_alpha{ 0.0f }; + float expert_beta{ 1.0f }; + size_t gate_idx{ 0 }; + Activation_type activation_type{ Activation_type::SWIGLU }; + }; + + MOE(const OutputVector & args, const Config & config); + + const Config & get_config() const; + void set_config(const Config & config); + + bool visit_attributes(AttributeVisitor & visitor) override; + void validate_and_infer_types() override; + std::shared_ptr<Node> clone_with_new_inputs(const OutputVector & new_args) const override; + +private: + Config m_config; +}; + +class OPENVINO_API MOECompressed : public MOE { +public: + OPENVINO_OP("MOECompressed", "", ov::op::internal::MOE) + + MOECompressed() = default; + + struct Config : public MOE::Config { + size_t hidden_size = 0; + size_t inter_size = 0; + size_t num_expert = 0; + size_t num_shared_expert = 0; + size_t top_k = 0; + // numeric_limits<size_t>::max() means per_channel compression (single group) + size_t group_size = 0; + bool has_batch_dim = false; + bool has_zp = false; + ov::element::Type out_type = ov::element::dynamic; + std::optional<float> scale_factor; + }; + + MOECompressed(const OutputVector & args, const Config & config); + + const Config & get_config() const { return m_config; } + + void set_scale_factor(float scale_factor) { m_config.scale_factor = scale_factor; } + + bool visit_attributes(AttributeVisitor & visitor) override; + void validate_and_infer_types() override; + std::shared_ptr<Node> clone_with_new_inputs(const OutputVector & new_args) const override; + +protected: + Config m_config; +}; + +} // namespace ov::op::internal diff --git a/ggml/src/ggml-openvino/openvino/op/mul_mat_id.cpp b/ggml/src/ggml-openvino/openvino/op/mul_mat_id.cpp index f1b28c85d401..0de6161bed85 100644 --- a/ggml/src/ggml-openvino/openvino/op/mul_mat_id.cpp +++ b/ggml/src/ggml-openvino/openvino/op/mul_mat_id.cpp @@ -56,54 +56,6 @@ ov::Output<ov::Node> static_shape_dims_or_shapeof(const ov::Output<ov::Node> & i return get_dimensions(shape, dims); } -ov::Output<ov::Node> translate_mul_mat_id_gather_matmul_fallback(const NodeContext & context, - ov::Output<ov::Node> expert_weights, - ov::Output<ov::Node> activations, - ov::Output<ov::Node> ids) { - auto gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {0}); - ov::Output<ov::Node> selected_weights = std::make_shared<ov::op::v8::Gather>(expert_weights, ids, gather_axis); - - const auto output_type = context.get_output_type(); - if (selected_weights.get_element_type() != ov::element::f32) { - selected_weights = std::make_shared<ov::op::v0::Convert>(selected_weights, ov::element::f32); - } - if (activations.get_element_type() != ov::element::f32) { - activations = std::make_shared<ov::op::v0::Convert>(activations, ov::element::f32); - } - - auto activations_shape = std::make_shared<ov::op::v3::ShapeOf>(activations, ov::element::i64); - auto ids_shape = std::make_shared<ov::op::v3::ShapeOf>(ids, ov::element::i64); - ov::Output<ov::Node> acts_target_dims = std::make_shared<ov::op::v0::Concat>( - ov::OutputVector{ - get_dimensions(activations_shape, {0}), - get_dimensions(ids_shape, {1}), - get_dimensions(activations_shape, {2}), - }, - 0); - ov::Output<ov::Node> acts_broadcasted = - std::make_shared<ov::op::v3::Broadcast>(activations, acts_target_dims, ov::op::BroadcastType::BIDIRECTIONAL); - - auto activations_expanded = std::make_shared<ov::op::v0::Unsqueeze>(acts_broadcasted, const_i64({2})); - ov::Output<ov::Node> result = - std::make_shared<ov::op::v0::MatMul>(activations_expanded, selected_weights, false, true); - - auto output_shape = context.get_output_shape(); - FRONT_END_OP_CONVERSION_CHECK(output_shape.rank().is_static() && output_shape.rank().get_length() == 4, - "Unexpected MUL_MAT_ID output rank"); - FRONT_END_OP_CONVERSION_CHECK(output_shape[3].is_static(), "Expected static row dimension for MUL_MAT_ID output"); - - auto batch_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {1}); - auto row_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[3].get_length()}); - auto result_target_dims = std::make_shared<ov::op::v0::Concat>( - ov::OutputVector{batch_dim, get_dimensions(ids_shape, {0, 1}), row_dim}, 0); - result = std::make_shared<ov::op::v1::Reshape>(result, result_target_dims, false); - - if (result.get_element_type() != output_type) { - result = std::make_shared<ov::op::v0::Convert>(result, output_type); - } - return result; -} - ov::Output<ov::Node> translate_mul_mat_id_mxfp4_packed(const NodeContext & context, ov::Output<ov::Node> expert_weights, ov::Output<ov::Node> activations, @@ -229,7 +181,6 @@ OutputVector translate_mul_mat_id(const NodeContext & context) { auto expert_weights_rank = expert_weights.get_partial_shape().rank(); FRONT_END_OP_CONVERSION_CHECK(expert_weights_rank.is_static(), "Expected static rank for MUL_MAT_ID expert weights"); - const bool use_gpu_fallback = ggml_openvino_get_device_name() == "GPU"; if (expert_weights_rank.get_length() == 4) { auto expert_weights_shape_3d = static_shape_dims_or_shapeof(expert_weights, {1, 2, 3}); expert_weights = std::make_shared<ov::op::v1::Reshape>(expert_weights, expert_weights_shape_3d, false); @@ -246,14 +197,9 @@ OutputVector translate_mul_mat_id(const NodeContext & context) { } const auto output_type = context.get_output_type(); - if (activations.get_element_type() != ov::element::f32) { - activations = std::make_shared<ov::op::v0::Convert>(activations, ov::element::f32); - } - - if (use_gpu_fallback || !expert_weights.get_partial_shape().is_static() || !activations.get_partial_shape().is_static() || - !ids.get_partial_shape().is_static()) { - return rename_outputs_with_suffix({translate_mul_mat_id_gather_matmul_fallback(context, expert_weights, activations, ids)}, - context.get_name()); + const auto activations_type = ggml_openvino_get_device_name() == "GPU" ? ov::element::f16 : ov::element::f32; + if (activations.get_element_type() != activations_type) { + activations = std::make_shared<ov::op::v0::Convert>(activations, activations_type); } // GatherMatmul's A input is [n_used_or_1, n_tokens, k]; activations_3d is diff --git a/ggml/src/ggml-openvino/openvino/op/mulmat.cpp b/ggml/src/ggml-openvino/openvino/op/mulmat.cpp index 41d7c54ae6be..9d4315aa4abc 100644 --- a/ggml/src/ggml-openvino/openvino/op/mulmat.cpp +++ b/ggml/src/ggml-openvino/openvino/op/mulmat.cpp @@ -29,19 +29,11 @@ OutputVector translate_mulmat(const NodeContext & context) { int op_case = context.get_op_case(); - ov::Output<Node> res; - ov::Output<ov::Node> B; - ov::Output<ov::Node> A; - if (op_case == 3) { - B = process_view_input(context, 0); - A = process_view_input(context, 1); - } else { - B = process_view_input_new(context, 0); - A = process_view_input_new(context, 1); - } + ov::Output<ov::Node> B = process_view_input_new(context, 0); + ov::Output<ov::Node> A = process_view_input_new(context, 1); if (A.get_element_type() != B.get_element_type()) { - B = std::make_shared<ov::op::v0::Convert>(context.get_input(0), context.get_input_type(1)); + B = std::make_shared<ov::op::v0::Convert>(B, context.get_input_type(1)); } auto B_shape = context.get_input_shape(0).to_shape(); @@ -84,7 +76,7 @@ OutputVector translate_mulmat(const NodeContext & context) { } bool transpose_b = true; - res = std::make_shared<ov::op::v0::MatMul>(A, B, false, transpose_b); + ov::Output<Node> res = std::make_shared<ov::op::v0::MatMul>(A, B, false, transpose_b); const auto output_type = context.get_output_type(); if (res.get_element_type() != output_type) { diff --git a/ggml/src/ggml-openvino/openvino/op/norm.cpp b/ggml/src/ggml-openvino/openvino/op/norm.cpp index c8bedb6dbf59..8660c6521b70 100644 --- a/ggml/src/ggml-openvino/openvino/op/norm.cpp +++ b/ggml/src/ggml-openvino/openvino/op/norm.cpp @@ -2,15 +2,10 @@ #include "../op_table.h" #include "../utils.h" +#include <cstring> #include <memory> -#include <openvino/op/add.hpp> #include <openvino/op/constant.hpp> -#include <openvino/op/divide.hpp> -#include <openvino/op/multiply.hpp> -#include <openvino/op/power.hpp> -#include <openvino/op/reduce_mean.hpp> -#include <openvino/op/sqrt.hpp> -#include <openvino/op/subtract.hpp> +#include <openvino/op/mvn.hpp> namespace ov { namespace frontend { @@ -21,33 +16,11 @@ OutputVector translate_norm(const NodeContext & context) { num_inputs_check(context, 1, 1); auto input_node = process_view_input_new(context, 0); - - // Step 1: Calculate mean along the last dimension - // mean = reduce_mean(input, axis=-1, keepdims=true) - auto mean = std::make_shared<ov::op::v1::ReduceMean>( - input_node, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}), true); - - // Step 2: Calculate (input - mean) - auto centered = std::make_shared<ov::op::v1::Subtract>(input_node, mean); - - // Step 3: Calculate squared differences (input - mean)^2 - auto squared = std::make_shared<ov::op::v1::Power>( - centered, ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {2.0f})); - - // Step 4: Calculate variance = mean((input - mean)^2) - auto variance = std::make_shared<ov::op::v1::ReduceMean>( - squared, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}), true); - - // Step 5: Get epsilon from op_params float eps; memcpy(&eps, context.get_output_op_params(), sizeof(float)); - // Step 6: Calculate std = sqrt(variance + eps) - auto std_dev = std::make_shared<ov::op::v0::Sqrt>(std::make_shared<ov::op::v1::Add>( - variance, ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {eps}))); - - // Step 7: Normalize: output = (input - mean) / std - auto res = std::make_shared<ov::op::v1::Divide>(centered, std_dev); + auto axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}); + auto res = std::make_shared<ov::op::v6::MVN>(input_node, axes, true, eps, ov::op::MVNEpsMode::INSIDE_SQRT); return rename_outputs_with_suffix({res}, context.get_name()); } diff --git a/ggml/src/ggml-openvino/openvino/op/pad.cpp b/ggml/src/ggml-openvino/openvino/op/pad.cpp index 492033d1b787..d2b8611423cb 100644 --- a/ggml/src/ggml-openvino/openvino/op/pad.cpp +++ b/ggml/src/ggml-openvino/openvino/op/pad.cpp @@ -60,9 +60,7 @@ OutputVector translate_pad(const NodeContext & context) { auto input = process_view_input_new(context, 0); if (context.get_input_shape(0) == context.get_output_shape()) { - auto input_shape = std::make_shared<ov::op::v3::ShapeOf>(input); - auto res = std::make_shared<ov::op::v1::Reshape>(input, input_shape, false); - return rename_outputs_with_suffix({res}, context.get_name()); + return rename_outputs_with_suffix({input}, context.get_name()); } const int32_t * op_params = context.get_output_op_params(); diff --git a/ggml/src/ggml-openvino/openvino/op/permute.cpp b/ggml/src/ggml-openvino/openvino/op/permute.cpp index 85550bff396b..df4f038984c5 100644 --- a/ggml/src/ggml-openvino/openvino/op/permute.cpp +++ b/ggml/src/ggml-openvino/openvino/op/permute.cpp @@ -45,11 +45,22 @@ OutputVector translate_permute(const NodeContext & context) { static_cast<int64_t>(perm_values.size() - 1 - input_axis); } } - auto perm = ov::op::v0::Constant::create(ov::element::i64, {4}, perm_values); - if (op_case == 1 || context.is_stateful()) { + // The stateful path carries hidden-state tensors in a rank-3 layout (the + // leading batch dim is dropped, e.g. Gemma4's per-layer-embedding path). The + // perm above is rank-4; when the actual input is rank-3, drop the batch axis + // (perm[0], which is always the identity 0 here) and shift the rest down by 1 + // so the transpose order matches the input rank. + std::vector<int64_t> perm_used = perm_values; + const auto & src_ps = src.get_partial_shape(); + if (src_ps.rank().is_static() && src_ps.rank().get_length() == 3 && perm_values.size() == 4 && + perm_values[0] == 0) { + perm_used = {perm_values[1] - 1, perm_values[2] - 1, perm_values[3] - 1}; + } + auto perm = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{perm_used.size()}, perm_used); res = std::make_shared<ov::op::v1::Transpose>(src, perm); } else if (op_case == 2) { + auto perm = ov::op::v0::Constant::create(ov::element::i64, {4}, perm_values); auto output_shape = context.get_output_shape().to_shape(); auto n_heads = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[1]}); auto head_size = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[3]}); @@ -68,6 +79,7 @@ OutputVector translate_permute(const NodeContext & context) { auto reshaped = std::make_shared<ov::op::v1::Reshape>(src, new_shape, true); res = std::make_shared<ov::op::v1::Transpose>(reshaped, perm); } else { + auto perm = ov::op::v0::Constant::create(ov::element::i64, {4}, perm_values); auto cache_shape = src.get_partial_shape(); auto output_shape = context.get_output_shape().to_shape(); int64_t head_size = output_shape[3]; diff --git a/ggml/src/ggml-openvino/openvino/op/rope.cpp b/ggml/src/ggml-openvino/openvino/op/rope.cpp index 8f20a0d196eb..a3da7d1fbb4f 100644 --- a/ggml/src/ggml-openvino/openvino/op/rope.cpp +++ b/ggml/src/ggml-openvino/openvino/op/rope.cpp @@ -11,16 +11,11 @@ #include <openvino/op/concat.hpp> #include <openvino/op/constant.hpp> #include <openvino/op/convert.hpp> -#include <openvino/op/cos.hpp> -#include <openvino/op/gather.hpp> #include <openvino/op/multiply.hpp> #include <openvino/op/reshape.hpp> -#include <openvino/op/shape_of.hpp> -#include <openvino/op/sin.hpp> #include <openvino/op/slice.hpp> #include <openvino/op/split.hpp> #include <openvino/op/subtract.hpp> -#include <openvino/op/transpose.hpp> #include <openvino/op/unsqueeze.hpp> #include <openvino/op/variadic_split.hpp> #include <vector> @@ -37,13 +32,14 @@ OutputVector translate_rope(const NodeContext & context) { ov::Output<Node> res; - auto data_node = context.get_input(0).get_node_shared_ptr(); + auto data_node = process_view_input_new(context, 0).get_node_shared_ptr(); auto output_shape = context.get_output_shape().to_shape(); int32_t * op_params = context.get_output_op_params(); const int mode = op_case; const int64_t head_dim = static_cast<int64_t>(output_shape[3]); const int64_t configured_n_dims = static_cast<int64_t>(op_params[1]); const int64_t n_dims = configured_n_dims == 0 ? head_dim : configured_n_dims; + const int64_t n_offs = static_cast<int64_t>(op_params[15]); constexpr int TYPE_NORMAL = 0; constexpr int TYPE_NEOX = 1; @@ -55,27 +51,27 @@ OutputVector translate_rope(const NodeContext & context) { cos_theta_node = context.get_input("rope_cos"); sin_theta_node = context.get_input("rope_sin"); } else { - auto inp_pos = context.get_input(1).get_node_shared_ptr(); - std::shared_ptr<ov::Node> rope_freqs_weight; + std::string cache_key = "rope_sin_cos"; + for (int i = 0; i < 15; i++) { + cache_key += "_" + std::to_string(op_params[i]); + } if (context.get_input_size() == 3) { - rope_freqs_weight = context.get_input(2).get_node_shared_ptr(); + cache_key += "_ff_" + context.get_input_names()[2]; } - auto sin_cos = make_sin_cos(op_params, inp_pos, rope_freqs_weight, mode == TYPE_IMROPE, false); - sin_theta_node = sin_cos.first; - cos_theta_node = sin_cos.second; - } - - if (context.get_view_input_size(0) > 0) { - data_node = process_view_input_new(context, 0).get_node_shared_ptr(); - if (context.is_stateful()) { - auto data_shape = ov::op::v0::Constant::create( - ov::element::i64, {3}, std::vector<int64_t>{-1, (int64_t) output_shape[2], (int64_t) output_shape[3]}); - data_node = std::make_shared<ov::op::v1::Reshape>(data_node, data_shape, false); + if (context.has_input(cache_key + "_cos")) { + cos_theta_node = context.get_input(cache_key + "_cos"); + sin_theta_node = context.get_input(cache_key + "_sin"); } else { - auto data_shape = ov::op::v0::Constant::create( - ov::element::i64, {4}, - std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]}); - data_node = std::make_shared<ov::op::v1::Reshape>(data_node, data_shape, false); + auto inp_pos = context.get_input(1).get_node_shared_ptr(); + std::shared_ptr<ov::Node> rope_freqs_weight; + if (context.get_input_size() == 3) { + rope_freqs_weight = context.get_input(2).get_node_shared_ptr(); + } + auto sin_cos = make_sin_cos(op_params, inp_pos, rope_freqs_weight, mode == TYPE_IMROPE, false); + sin_theta_node = sin_cos.first; + cos_theta_node = sin_cos.second; + context.put_shared(cache_key + "_cos", cos_theta_node); + context.put_shared(cache_key + "_sin", sin_theta_node); } } @@ -84,52 +80,34 @@ OutputVector translate_rope(const NodeContext & context) { data_node = std::make_shared<ov::op::v0::Convert>(data_node, ov::element::f32); } - FRONT_END_OP_CONVERSION_CHECK(n_dims > 0 && n_dims <= head_dim && (n_dims % 2 == 0), - "ROPE expects even n_dims in [1, head_dim]"); - - // TODO(openvino-gpu-rope-fusion): TEMPORARY WORKAROUND - do NOT revert until the - // OpenVINO GPU plugin is updated. - // + FRONT_END_OP_CONVERSION_CHECK(n_offs >= 0 && (n_offs % 2 == 0), + "ROPE expects non-negative even n_offs"); + FRONT_END_OP_CONVERSION_CHECK(n_dims > 0 && n_dims + n_offs <= head_dim && (n_dims % 2 == 0), + "ROPE expects even n_dims in [1, head_dim - n_offs]"); + + // RoPEFusionFlux requires rank_equals(4) on x, t_cos and t_sin. The cos/sin + // tables are already built rank-4 ([1, S, 1, head_size/2]) for both modes. In + // stateful mode the data arrives rank-3 ([S, n_heads, head_size]), so lift it + // to rank-4 ([1, S, n_heads, head_size]) here. Stateful RoPE already produced + // rank-4 output, so downstream attention is unaffected. + if (context.is_stateful()) { + auto r4_shape = ov::op::v0::Constant::create( + ov::element::i64, {4}, + std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]}); + data_node = std::make_shared<ov::op::v1::Reshape>(data_node, r4_shape, false); + } // For TYPE_NORMAL rope (both stateful and stateless) we emit the Flux-style // interleaved pattern below so the GPU plugin's RoPEFusionFlux matcher folds it - // into ov::op::internal::RoPE. The matcher requires rank-4 inputs, which is why - // the original even/odd Slice translation (kept in the `else if (mode == - // TYPE_NORMAL)` branch below for reference) does not get fused. - // - // Once the GPU plugin's RoPE fusion is extended to also recognize the original - // even/odd Slice form, this Flux rewrite should be removed and both modes should - // be restored to the captured even/odd translation. Until then, keep both paths: - // the active Flux rewrite here and the previous translation preserved below. + // into ov::op::internal::RoPE. if (mode == TYPE_NORMAL) { auto axis_last = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}); - auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0}); auto step_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1}); - // Emit the Flux-style interleaved-RoPE pattern so the GPU plugin's - // RoPEFusionFlux matcher folds this subgraph into ov::op::internal::RoPE: - // x_paired = Reshape(x_rot, [1, S, n_heads, n_dims/2, 2]) - // x0, x1 = Split(x_paired, axis=-1, num_splits=2) - // x1_neg = x1 * -1 - // x_rotated = Reshape(Concat([x1_neg, x0], axis=-1), [1, S, n_heads, n_dims]) - // y_rot = x_rot * t_cos + x_rotated * t_sin - // y = Concat([y_rot, x_tail], axis=-1) if n_dims < head_dim - // Mathematically equivalent to the even/odd Slice form below. - // - // RoPEFusionFlux requires rank_equals(4) on x, t_cos and t_sin. The cos/sin - // tables are already built rank-4 ([1, S, 1, head_size/2]) for both modes. In - // stateful mode the data arrives rank-3 ([S, n_heads, head_size]), so lift it - // to rank-4 ([1, S, n_heads, head_size]) here. Stateful RoPE already produced - // rank-4 output, so downstream attention is unaffected. - if (context.is_stateful()) { - auto r4_shape = ov::op::v0::Constant::create( - ov::element::i64, {4}, - std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]}); - data_node = std::make_shared<ov::op::v1::Reshape>(data_node, r4_shape, false); - } const int64_t n_heads = static_cast<int64_t>(output_shape[2]); const int64_t half = n_dims / 2; - auto rot_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_dims}); - auto rot_data = std::make_shared<ov::op::v8::Slice>(data_node, zero, rot_end, step_one, axis_last); + auto rot_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs}); + auto rot_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs + n_dims}); + auto rot_data = std::make_shared<ov::op::v8::Slice>(data_node, rot_start, rot_end, step_one, axis_last); auto neg_one_f = ov::op::v0::Constant::create(data_node->get_element_type(), ov::Shape{}, {-1.0f}); @@ -153,7 +131,7 @@ OutputVector translate_rope(const NodeContext & context) { // Expand cos/sin from [..., n_dims/2] to [..., n_dims] by repeating each // entry twice. Use special_zero on the final Reshape so the seq dim passes // through dynamically. Final rank is 4 to satisfy the matcher's predicate. - auto expand_cos_sin = [&](Output<Node> cs) { + auto expand_cos_sin = [&](const Output<Node>& cs) { auto cs_unsq = std::make_shared<ov::op::v0::Unsqueeze>( cs, ov::op::v0::Constant::create(ov::element::i64, {1}, {-1})); auto bcast_target = ov::op::v0::Constant::create( @@ -170,123 +148,80 @@ OutputVector translate_rope(const NodeContext & context) { auto y2 = std::make_shared<ov::op::v1::Multiply>(x_rotated, sin_full); auto rotated = std::make_shared<ov::op::v1::Add>(y1, y2); - if (n_dims < head_dim) { - auto tail_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_dims}); + ov::OutputVector concat_parts; + if (n_offs > 0) { + auto head_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {0}); + auto head_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs}); + auto head = std::make_shared<ov::op::v8::Slice>(data_node, head_start, head_end, step_one, axis_last); + concat_parts.push_back(head); + } + concat_parts.push_back(rotated); + if (n_offs + n_dims < head_dim) { + auto tail_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs + n_dims}); auto tail_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {head_dim}); auto tail = std::make_shared<ov::op::v8::Slice>(data_node, tail_start, tail_end, step_one, axis_last); - res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{rotated, tail}, -1); - } else { - res = rotated; + concat_parts.push_back(tail); } - } - // PRESERVED PREVIOUS TRANSLATION - Re-enable this branch (and remove the Flux branch above) once - // the GPU plugin's RoPE fusion is updated to recognize the even/odd Slice form; - // see the TODO(openvino-gpu-rope-fusion) note above. Do not delete. - // - // Original even/odd Slice form. In stateless mode it ran on rank-4 data - // ([1, S, n_heads, head_size]); in stateful mode on rank-3 data - // ([S, n_heads, head_size]). Either way it does not match RoPEFusionFlux - // (which needs rank-4 x in the interleaved layout), so the RoPE stays as - // discrete elementwise ops. - // - // } else if (mode == TYPE_NORMAL) { - // auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}); - // auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0}); - // auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1}); - // auto two = ov::op::v0::Constant::create(ov::element::i64, {1}, {2}); - // auto end = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[3]}); - // Output<Node> even_slice; - // Output<Node> odd_slice; - // // stateful data is rank 3 (unsqueeze at axis 3), stateless is rank 4 (axis 4) - // int32_t unsqueeze_dim = context.is_stateful() ? 3 : 4; - // even_slice = std::make_shared<ov::op::v8::Slice>(data_node, zero, end, two, neg_one); - // odd_slice = std::make_shared<ov::op::v8::Slice>(data_node, one, end, two, neg_one); - // - // Output<Node> first_half = - // std::make_shared<ov::op::v1::Subtract>(std::make_shared<ov::op::v1::Multiply>(even_slice, cos_theta_node), - // std::make_shared<ov::op::v1::Multiply>(odd_slice, sin_theta_node)); - // Output<Node> second_half = - // std::make_shared<ov::op::v1::Add>(std::make_shared<ov::op::v1::Multiply>(even_slice, sin_theta_node), - // std::make_shared<ov::op::v1::Multiply>(odd_slice, cos_theta_node)); - // - // first_half = std::make_shared<ov::op::v0::Unsqueeze>(first_half, - // ov::op::v0::Constant::create(ov::element::i64, {1}, {unsqueeze_dim})); - // second_half = std::make_shared<ov::op::v0::Unsqueeze>(second_half, - // ov::op::v0::Constant::create(ov::element::i64, {1}, {unsqueeze_dim})); - // auto stack = std::make_shared<ov::op::v0::Concat>(OutputVector{first_half, second_half}, unsqueeze_dim); - // - // auto data_shape = ov::op::v0::Constant::create( - // ov::element::i64, {4}, std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]}); - // res = std::make_shared<ov::op::v1::Reshape>(stack, data_shape, false); - else if (mode == TYPE_NEOX) { - // In stateful mode the data arrives rank-3 ([S, n_heads, head_size]) while the - // cos/sin tables are rank-4 ([1, S, 1, n_dims/2]). The resulting mixed-rank - // broadcast in the Multiply below is miscomputed by the OpenVINO GPU plugin, - // corrupting the rotated Q/K. Lift the data to rank-4 ([1, S, n_heads, head_size]) - // first so the RoPE Multiplies are equal-rank, matching the TYPE_NORMAL branch. - // Stateful RoPE already produced rank-4 output, so downstream attention is unaffected. - if (context.is_stateful()) { - auto r4_shape = ov::op::v0::Constant::create( - ov::element::i64, {4}, - std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]}); - data_node = std::make_shared<ov::op::v1::Reshape>(data_node, r4_shape, false); + if (concat_parts.size() == 1) { + res = rotated; + } else { + res = std::make_shared<ov::op::v0::Concat>(concat_parts, -1); } - auto axis_last = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {-1}); - std::vector<int64_t> split_lengths = {n_dims / 2, n_dims / 2}; - if (n_dims < head_dim) { - split_lengths.push_back(head_dim - n_dims); + } else if (mode == TYPE_NEOX || mode == TYPE_IMROPE) { + if (mode == TYPE_IMROPE) { + auto cos_sin_shape = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{4}, + std::vector<int64_t>{1, -1, 1, (n_dims >> 1)}); + cos_theta_node = std::make_shared<ov::op::v1::Reshape>(cos_theta_node, cos_sin_shape, true); + sin_theta_node = std::make_shared<ov::op::v1::Reshape>(sin_theta_node, cos_sin_shape, true); } - auto data_split = std::make_shared<ov::op::v1::VariadicSplit>( - data_node, axis_last, - ov::op::v0::Constant::create(ov::element::i64, {split_lengths.size()}, split_lengths)); - Output<Node> slice_data_node_0 = data_split->outputs()[0]; - Output<Node> slice_data_node_1 = data_split->outputs()[1]; - - auto first_half_node = std::make_shared<ov::op::v1::Subtract>( - std::make_shared<ov::op::v1::Multiply>(slice_data_node_0, cos_theta_node), - std::make_shared<ov::op::v1::Multiply>(slice_data_node_1, sin_theta_node)); - - auto second_half_node = std::make_shared<ov::op::v1::Add>( - std::make_shared<ov::op::v1::Multiply>(slice_data_node_0, sin_theta_node), - std::make_shared<ov::op::v1::Multiply>(slice_data_node_1, cos_theta_node)); + auto axis_last = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}); + auto step_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1}); - if (n_dims < head_dim) { - Output<Node> tail = data_split->outputs()[2]; - res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{first_half_node, second_half_node, tail}, -1); - } else { - res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{first_half_node, second_half_node}, -1); + Output<Node> rot_data = data_node; + if (n_offs > 0 || n_offs + n_dims < head_dim) { + auto rot_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs}); + auto rot_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs + n_dims}); + rot_data = std::make_shared<ov::op::v8::Slice>(data_node, rot_start, rot_end, step_one, axis_last); } - } else if (mode == TYPE_IMROPE) { - auto cos_sin_shape = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{4}, - std::vector<int64_t>{1, -1, 1, (n_dims >> 1)}); - auto cos_reshaped = std::make_shared<ov::op::v1::Reshape>(cos_theta_node, cos_sin_shape, true); - auto sin_reshaped = std::make_shared<ov::op::v1::Reshape>(sin_theta_node, cos_sin_shape, true); + + const int64_t half = n_dims / 2; + auto neg_one_f = ov::op::v0::Constant::create(data_node->get_element_type(), ov::Shape{}, {-1.0f}); auto split_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {3}); - std::vector<int64_t> split_lengths = {n_dims / 2, n_dims / 2}; - if (n_dims < head_dim) { - split_lengths.push_back(head_dim - n_dims); - } + auto split_lengths = ov::op::v0::Constant::create(ov::element::i64, {2}, {half, half}); + auto data_split = std::make_shared<ov::op::v1::VariadicSplit>(rot_data, split_axis, split_lengths); + Output<Node> x1 = data_split->outputs()[0]; + Output<Node> x2 = data_split->outputs()[1]; + + auto x2_neg = std::make_shared<ov::op::v1::Multiply>(x2, neg_one_f); + auto x_rotate_half = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{x2_neg, x1}, -1); - auto split_a = std::make_shared<ov::op::v1::VariadicSplit>( - data_node, split_axis, - ov::op::v0::Constant::create(ov::element::i64, {split_lengths.size()}, split_lengths)); - auto x0 = split_a->output(0); - auto x1 = split_a->output(1); - auto mul_a = std::make_shared<ov::op::v1::Multiply>(x0, cos_reshaped); - auto mul_b = std::make_shared<ov::op::v1::Multiply>(x1, sin_reshaped); - auto sub = std::make_shared<ov::op::v1::Subtract>(mul_a, mul_b); + auto cos_full = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{cos_theta_node, cos_theta_node}, -1); + auto sin_full = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sin_theta_node, sin_theta_node}, -1); - auto mul_c = std::make_shared<ov::op::v1::Multiply>(x0, sin_reshaped); - auto mul_d = std::make_shared<ov::op::v1::Multiply>(x1, cos_reshaped); - auto add = std::make_shared<ov::op::v1::Add>(mul_c, mul_d); + auto y1 = std::make_shared<ov::op::v1::Multiply>(rot_data, cos_full); + auto y2 = std::make_shared<ov::op::v1::Multiply>(x_rotate_half, sin_full); + auto rotated = std::make_shared<ov::op::v1::Add>(y1, y2); - if (n_dims < head_dim) { - auto tail = split_a->output(2); - res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sub, add, tail}, 3); + ov::OutputVector concat_parts; + if (n_offs > 0) { + auto head_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {0}); + auto head_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs}); + auto head = std::make_shared<ov::op::v8::Slice>(data_node, head_start, head_end, step_one, axis_last); + concat_parts.push_back(head); + } + concat_parts.push_back(rotated); + if (n_offs + n_dims < head_dim) { + auto tail_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_offs + n_dims}); + auto tail_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {head_dim}); + auto tail = std::make_shared<ov::op::v8::Slice>(data_node, tail_start, tail_end, step_one, axis_last); + concat_parts.push_back(tail); + } + if (concat_parts.size() == 1) { + res = rotated; } else { - res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sub, add}, 3); + res = std::make_shared<ov::op::v0::Concat>(concat_parts, -1); } } diff --git a/ggml/src/ggml-openvino/openvino/op/set_rows.cpp b/ggml/src/ggml-openvino/openvino/op/set_rows.cpp index 0fe8e0a8d067..3b606c82accb 100644 --- a/ggml/src/ggml-openvino/openvino/op/set_rows.cpp +++ b/ggml/src/ggml-openvino/openvino/op/set_rows.cpp @@ -7,6 +7,7 @@ #include <memory> #include <openvino/core/node.hpp> #include <openvino/core/node_output.hpp> +#include <openvino/core/type.hpp> #include <openvino/frontend/exception.hpp> #include <openvino/op/broadcast.hpp> #include <openvino/op/concat.hpp> @@ -75,7 +76,7 @@ OutputVector translate_set_rows(const NodeContext & context) { res = std::make_shared<ov::op::v3::ScatterUpdate>(dst, ind_squeezed, data_reshaped, axes); } - auto dst_reshape = std::dynamic_pointer_cast<ov::op::v1::Reshape>(dst.get_node_shared_ptr()); + auto dst_reshape = ov::as_type_ptr<ov::op::v1::Reshape>(dst.get_node_shared_ptr()); if (!multidim_indices && dst_reshape) { // Fix the case of multiple sequences, reshape back to original shape [1, n_seq, ctx_per_seq, emb] // ctx_per_seq is not fixed due to llama-bench compatibility diff --git a/ggml/src/ggml-openvino/openvino/op/transpose.cpp b/ggml/src/ggml-openvino/openvino/op/transpose.cpp index 8d89ca556d68..0651a410a1a0 100644 --- a/ggml/src/ggml-openvino/openvino/op/transpose.cpp +++ b/ggml/src/ggml-openvino/openvino/op/transpose.cpp @@ -14,9 +14,7 @@ OutputVector translate_transpose(const NodeContext & context) { // Compute permute order from input/output shape and stride information // so it adapts to different input and output layouts. - auto input_shape = context.get_input_shape(0).to_shape(); auto input_stride = context.get_input_stride(0); - auto output_shape = context.get_output_shape().to_shape(); auto output_stride = context.get_output_stride(); // Compute permute order by matching output and input stride rankings. diff --git a/ggml/src/ggml-openvino/openvino/op/unary_silu.cpp b/ggml/src/ggml-openvino/openvino/op/unary_silu.cpp deleted file mode 100644 index 48ee0431ff76..000000000000 --- a/ggml/src/ggml-openvino/openvino/op/unary_silu.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "../node_context.h" -#include "../op_table.h" -#include "../utils.h" - -#include <openvino/core/node_output.hpp> -#include <openvino/op/multiply.hpp> -#include <openvino/op/sigmoid.hpp> - -namespace ov { -namespace frontend { -namespace ggml { -namespace op { - -OutputVector translate_unary_silu(const NodeContext & context) { - num_inputs_check(context, 1, 1); - - auto input = process_view_input_new(context, 0); - auto sigmoid = std::make_shared<ov::op::v0::Sigmoid>(input); - auto res = std::make_shared<ov::op::v1::Multiply>(input, sigmoid); - - return rename_outputs_with_suffix({res}, context.get_name()); -} - -} // namespace op -} // namespace ggml -} // namespace frontend -} // namespace ov diff --git a/ggml/src/ggml-openvino/openvino/op/unary_softplus.cpp b/ggml/src/ggml-openvino/openvino/op/unary_softplus.cpp index 756d9c33d736..a9e495c372f7 100644 --- a/ggml/src/ggml-openvino/openvino/op/unary_softplus.cpp +++ b/ggml/src/ggml-openvino/openvino/op/unary_softplus.cpp @@ -1,6 +1,7 @@ #include "../node_context.h" #include "../op_table.h" #include "../utils.h" +#include "ggml-openvino/ggml-openvino-extra.h" #include <openvino/op/abs.hpp> #include <openvino/op/add.hpp> @@ -9,6 +10,7 @@ #include <openvino/op/log.hpp> #include <openvino/op/negative.hpp> #include <openvino/op/relu.hpp> +#include <openvino/op/softplus.hpp> namespace ov { namespace frontend { @@ -18,6 +20,10 @@ namespace op { OutputVector translate_unary_softplus(const NodeContext & context) { num_inputs_check(context, 1, 1); + if (ggml_openvino_getenv_int("GGML_OPENVINO_NATIVE_SOFTPLUS") != 0) { + return translate_1to1_match_1_input<ov::op::v4::SoftPlus>(context); + } + auto input = process_view_input_new(context, 0); const auto element_type = input.get_element_type(); auto one = ov::op::v0::Constant::create(element_type, ov::Shape{}, {1.0f}); diff --git a/ggml/src/ggml-openvino/openvino/op_table.cpp b/ggml/src/ggml-openvino/openvino/op_table.cpp index d4f5ac307329..f249a06bb8a0 100644 --- a/ggml/src/ggml-openvino/openvino/op_table.cpp +++ b/ggml/src/ggml-openvino/openvino/op_table.cpp @@ -13,6 +13,7 @@ #include <openvino/op/relu.hpp> #include <openvino/op/sigmoid.hpp> #include <openvino/op/subtract.hpp> +#include <openvino/op/swish.hpp> #include <openvino/op/tanh.hpp> namespace ov { @@ -50,10 +51,9 @@ std::unordered_map<std::string, CreatorFunction> get_supported_ops() { {"GGML_OP_TRANSPOSE", op::translate_transpose }, {"GGML_UNARY_OP_GELU", op::translate_1to1_match_1_input<v7::Gelu> }, {"GGML_UNARY_OP_SIGMOID", op::translate_1to1_match_1_input<v0::Sigmoid> }, - {"GGML_UNARY_OP_SILU", op::translate_unary_silu }, + {"GGML_UNARY_OP_SILU", op::translate_1to1_match_1_input<v4::Swish> }, {"GGML_UNARY_OP_SOFTPLUS", op::translate_unary_softplus }, {"GGML_UNARY_OP_TANH", op::translate_1to1_match_1_input<v0::Tanh> }, - {"GGML_UNARY_OP_SIGMOID", op::translate_1to1_match_1_input<v0::Sigmoid> }, {"GGML_UNARY_OP_EXP", op::translate_1to1_match_1_input<v0::Exp> }, {"GGML_UNARY_OP_NEG", op::translate_1to1_match_1_input<v0::Negative> }, {"GGML_UNARY_OP_RELU", op::translate_1to1_match_1_input<v0::Relu> }, diff --git a/ggml/src/ggml-openvino/openvino/op_table.h b/ggml/src/ggml-openvino/openvino/op_table.h index a0a42bff337d..3dc98bd96763 100644 --- a/ggml/src/ggml-openvino/openvino/op_table.h +++ b/ggml/src/ggml-openvino/openvino/op_table.h @@ -30,7 +30,6 @@ GGML_OP_CONVERTER(translate_sqr); GGML_OP_CONVERTER(translate_rope); GGML_OP_CONVERTER(translate_scale); GGML_OP_CONVERTER(translate_sqrt); -GGML_OP_CONVERTER(translate_unary_silu); GGML_OP_CONVERTER(translate_unary_softplus); GGML_OP_CONVERTER(translate_soft_max); GGML_OP_CONVERTER(translate_transpose); diff --git a/ggml/src/ggml-openvino/openvino/pass/fuse_moe_compressed.cpp b/ggml/src/ggml-openvino/openvino/pass/fuse_moe_compressed.cpp new file mode 100644 index 000000000000..c4872ac2e9c5 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/pass/fuse_moe_compressed.cpp @@ -0,0 +1,273 @@ +#include "fuse_moe_compressed.h" + +#include <limits> +#include <set> +#include <memory> +#include <openvino/core/graph_util.hpp> +#include <openvino/core/rt_info.hpp> +#include <openvino/op/constant.hpp> +#include <openvino/op/convert.hpp> +#include <openvino/op/multiply.hpp> +#include <openvino/op/reduce_sum.hpp> +#include <openvino/op/reshape.hpp> +#include <openvino/op/squeeze.hpp> +#include <openvino/op/subtract.hpp> +#include <openvino/op/swish.hpp> +#include <openvino/op/transpose.hpp> +#include <openvino/op/unsqueeze.hpp> +#include <openvino/pass/constant_folding.hpp> +#include <openvino/pass/pattern/op/optional.hpp> +#include <openvino/pass/pattern/op/wrap_type.hpp> + +#include "../op/gather_matmul.hpp" +#include "../op/moe_compressed.hpp" + +namespace ov { +namespace frontend { +namespace ggml { +namespace pass { + +namespace { + +struct dequant_inputs { + ov::Output<ov::Node> weight; + ov::Output<ov::Node> scale; + ov::Output<ov::Node> zp; + bool has_zp = false; + bool ok = false; +}; + +// Peel the chain built by make_int4_weights/make_int8_weights back to its Constant inputs. +// Grouped weights keep the pre-Reshape rank-4 form [n_expert, n, k/group, group] with scale +// and zp at [n_expert, n, k/group, 1], which is the layout MOECompressed expects. Channel-wise +// weights stay rank-3 with a rank-3 scale and carry no zp. +dequant_inputs unwrap_dequant(const ov::Output<ov::Node> & b) { + dequant_inputs res; + + auto node = b.get_node_shared_ptr(); + while (ov::is_type<ov::op::v0::Convert>(node) || ov::is_type<ov::op::v1::Reshape>(node)) { + node = node->get_input_node_shared_ptr(0); + } + + auto mul = ov::as_type_ptr<ov::op::v1::Multiply>(node); + if (!mul) { + return res; + } + res.scale = mul->input_value(1); + + auto lhs = mul->get_input_node_shared_ptr(0); + if (auto sub = ov::as_type_ptr<ov::op::v1::Subtract>(lhs)) { + // Take the zero point down to its Constant: an integer zp is wrapped in a Convert to f16, + // and the op wants the integer form. A natively quantized expert instead carries an exact + // f16 zp (-min/scale) with no integer behind it, which the MoE kernel does not accept. + auto zp_node = sub->get_input_node_shared_ptr(1); + while (ov::is_type<ov::op::v0::Convert>(zp_node)) { + zp_node = zp_node->get_input_node_shared_ptr(0); + } + res.zp = zp_node->output(0); + res.has_zp = true; + lhs = sub->get_input_node_shared_ptr(0); + } + while (ov::is_type<ov::op::v0::Convert>(lhs)) { + lhs = lhs->get_input_node_shared_ptr(0); + } + if (!ov::is_type<ov::op::v0::Constant>(lhs)) { + return res; + } + + res.weight = lhs->output(0); + res.ok = res.scale.get_partial_shape().is_static() && res.weight.get_partial_shape().is_static(); + return res; +} + +size_t logical_k(const ov::Shape & shape) { + return shape.size() == 4 ? shape[2] * shape[3] : shape.back(); +} + +} // namespace + +FuseMoeCompressed::FuseMoeCompressed() { + using namespace ov::pass::pattern; + + // The gate and up projections each get their own Reshape/Transpose of the hidden state and + // their own Reshape of the routing ids, so every branch needs its own sub-pattern. On GPU + // mul_mat_id also converts the activations to f16 before the op and back to f32 after it, + // so those Converts are matched as optional. + auto hidden_gate_m = any_input(); + auto a_gate_reshape_m = wrap_type<ov::op::v1::Reshape>({ hidden_gate_m, any_input() }); + auto a_gate_m = + wrap_type<ov::op::v1::Transpose>({ optional<ov::op::v0::Convert>({ a_gate_reshape_m }), any_input() }); + auto hidden_up_m = any_input(); + auto a_up_m = wrap_type<ov::op::v1::Transpose>( + { optional<ov::op::v0::Convert>({ wrap_type<ov::op::v1::Reshape>({ hidden_up_m, any_input() }) }), + any_input() }); + + auto gate_w_m = any_input(); + auto up_w_m = any_input(); + auto down_w_m = any_input(); + auto ids_gate_m = any_input(); + auto ids_up_m = any_input(); + auto ids_down_m = any_input(); + + auto bgm_gate_m = wrap_type<ov::op::internal::GatherMatmul>({ a_gate_m, gate_w_m, ids_gate_m, any_input() }); + auto gate_u_m = optional<ov::op::v0::Convert>({ wrap_type<ov::op::v0::Unsqueeze>( + { wrap_type<ov::op::v1::Transpose>({ bgm_gate_m, any_input() }), any_input() }) }); + + auto silu_m = wrap_type<ov::op::v4::Swish>({ gate_u_m }); + + auto bgm_up_m = wrap_type<ov::op::internal::GatherMatmul>({ a_up_m, up_w_m, ids_up_m, any_input() }); + auto up_u_m = optional<ov::op::v0::Convert>({ wrap_type<ov::op::v0::Unsqueeze>( + { wrap_type<ov::op::v1::Transpose>({ bgm_up_m, any_input() }), any_input() }) }); + auto swiglu_m = wrap_type<ov::op::v1::Multiply>({ silu_m, up_u_m }); + + auto d_t_m = wrap_type<ov::op::v1::Transpose>( + { optional<ov::op::v0::Convert>({ wrap_type<ov::op::v1::Reshape>({ swiglu_m, any_input() }) }), + any_input() }); + auto bgm_down_m = wrap_type<ov::op::internal::GatherMatmul>({ d_t_m, down_w_m, ids_down_m, any_input() }); + auto down_u_m = optional<ov::op::v0::Convert>({ wrap_type<ov::op::v0::Unsqueeze>( + { wrap_type<ov::op::v1::Transpose>({ bgm_down_m, any_input() }), any_input() }) }); + + auto routing_m = any_input(); + auto weighted_m = wrap_type<ov::op::v1::Multiply>({ down_u_m, routing_m }); + auto root_m = wrap_type<ov::op::v1::ReduceSum>({ weighted_m, any_input() }); + + const auto callback = [=](Matcher & m) { + auto & pm = m.get_pattern_value_map(); + + const auto gate = unwrap_dequant(pm.at(gate_w_m)); + const auto up = unwrap_dequant(pm.at(up_w_m)); + const auto down = unwrap_dequant(pm.at(down_w_m)); + if (!gate.ok || !up.ok || !down.ok) { + return false; + } + + const auto gate_shape = gate.weight.get_shape(); + const auto up_shape = up.weight.get_shape(); + const auto down_shape = down.weight.get_shape(); + if (gate_shape != up_shape || gate_shape.size() < 3 || down_shape.size() < 3) { + return false; + } + + // MOECompressed carries one group_size and one has_zp for all three projections, so a + // model whose down-proj is quantized differently from gate/up cannot be described. This + // happens when ggml requantizes Q5_K/Q6_K experts to channel-wise int8. + if (gate.has_zp != down.has_zp || gate_shape.size() != down_shape.size()) { + return false; + } + + // The kernel only takes an integer zero point (moe_3gemm_swiglu_opt validate_impl). + if (gate.has_zp) { + static const std::set<ov::element::Type> int_zp_types = { ov::element::u4, ov::element::i4, + ov::element::u8, ov::element::i8 }; + if (int_zp_types.count(gate.zp.get_element_type()) == 0 || + int_zp_types.count(down.zp.get_element_type()) == 0) { + return false; + } + } + + // Config holds a single group_size for all three projections. + const auto group_of = [](const dequant_inputs & w) { + const auto s = w.weight.get_shape(); + return s.size() == 4 ? s[3] : logical_k(s); + }; + if (group_of(gate) != group_of(up) || group_of(gate) != group_of(down)) { + return false; + } + + // all three branches must route the same hidden state through the same experts + if (pm.at(hidden_gate_m) != pm.at(hidden_up_m)) { + return false; + } + + auto ids = pm.at(ids_down_m); + const auto ids_pshape = ids.get_partial_shape(); + if (ids_pshape.rank().is_dynamic() || ids_pshape[ids_pshape.rank().get_length() - 1].is_dynamic()) { + return false; + } + const size_t top_k = ids_pshape[ids_pshape.rank().get_length() - 1].get_length(); + + // routing weights arrive as [1, n_tokens, top_k, 1]; the op wants [..., top_k] + auto routing = pm.at(routing_m); + const auto routing_pshape = routing.get_partial_shape(); + if (routing_pshape.rank().is_dynamic() || routing_pshape.rank().get_length() != 4 || + routing_pshape[3] != 1) { + return false; + } + // MOE requires routing weights and ids to have the same shape. Drop the trailing 1 of the + // routing weights and give the ids the leading batch dim, so both become [1, n_tokens, top_k]. + routing = std::make_shared<ov::op::v0::Squeeze>( + routing, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ 1 }, { 3 })); + if (ids_pshape.rank().get_length() == 2) { + ids = std::make_shared<ov::op::v0::Unsqueeze>( + ids, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{ 1 }, { 0 })); + } + if (routing.get_partial_shape() != ids.get_partial_shape()) { + return false; + } + + const size_t down_k = logical_k(down_shape); + const auto down_scale_shape = down.scale.get_shape(); + const size_t down_groups = down_scale_shape.size() >= 3 ? down_scale_shape[2] : 1; + + ov::op::internal::MOECompressed::Config config; + config.expert_type = ov::op::internal::MOE::Expert_type::GEMM3_SWIGLU; + config.activation_type = ov::op::internal::MOE::Activation_type::SWIGLU; + config.expert_alpha = 0.0f; + config.expert_beta = 1.0f; + config.gate_idx = 0; + config.hidden_size = logical_k(gate_shape); + config.inter_size = gate_shape[1]; + config.num_expert = gate_shape[0]; + config.num_shared_expert = 0; + config.top_k = top_k; + config.group_size = down_groups <= 1 ? std::numeric_limits<size_t>::max() : down_k / down_groups; + config.has_batch_dim = true; + config.has_zp = gate.has_zp; + // dynamic makes the output follow the hidden state, so the plugin can lower this region + // to f16 together with the rest of the graph + config.out_type = ov::element::dynamic; + + auto absent_zp = [] { + auto zp = std::make_shared<ov::op::v0::Constant>(ov::element::dynamic, ov::Shape{ 0 }); + ov::pass::disable_constant_folding(zp); + return zp->output(0); + }; + + // MOE takes its output type from the hidden state. Transpose the activations before the + // f16 Convert that mul_mat_id adds on GPU, so the op stays f32 like the block it replaces + // and the plugin can lower the whole region uniformly. + const auto a_transpose = pm.at(a_gate_m).get_node_shared_ptr(); + ov::Output<ov::Node> hidden = + std::make_shared<ov::op::v1::Transpose>(pm.at(a_gate_reshape_m), a_transpose->input_value(1)); + + const ov::OutputVector args = { + hidden, routing, ids, + gate.weight, gate.scale, gate.has_zp ? gate.zp : absent_zp(), + up.weight, up.scale, up.has_zp ? up.zp : absent_zp(), + down.weight, down.scale, down.has_zp ? down.zp : absent_zp(), + }; + + auto moe = std::make_shared<ov::op::internal::MOECompressed>(args, config); + + // MOE takes its output type from the hidden state, which is f16 on GPU, while the rest of + // the ggml graph works in f32. + ov::Output<ov::Node> result = moe->output(0); + const auto root_type = m.get_match_root()->get_output_element_type(0); + if (result.get_element_type() != root_type) { + result = std::make_shared<ov::op::v0::Convert>(result, root_type); + } + + result.get_node_shared_ptr()->set_friendly_name(m.get_match_root()->get_friendly_name()); + ov::copy_runtime_info(m.get_matched_nodes(), result.get_node_shared_ptr()); + ov::replace_node(m.get_match_root(), result.get_node_shared_ptr()); + register_new_node(moe); + return true; + }; + + register_matcher(std::make_shared<Matcher>(root_m, "ov::frontend::ggml::pass::FuseMoeCompressed"), callback); +} + +} // namespace pass +} // namespace ggml +} // namespace frontend +} // namespace ov diff --git a/ggml/src/ggml-openvino/openvino/pass/fuse_moe_compressed.h b/ggml/src/ggml-openvino/openvino/pass/fuse_moe_compressed.h new file mode 100644 index 000000000000..5500bed68af8 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/pass/fuse_moe_compressed.h @@ -0,0 +1,19 @@ +#include "openvino/pass/matcher_pass.hpp" + +namespace ov { +namespace frontend { +namespace ggml { +namespace pass { + +// Folds the MoE expert block emitted for MUL_MAT_ID (3 GatherMatmul + SwiGLU + routing +// weighting + expert reduction) into a single ov::op::internal::MOECompressed. +class FuseMoeCompressed : public ov::pass::MatcherPass { +public: + OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::FuseMoeCompressed") + FuseMoeCompressed(); +}; + +} // namespace pass +} // namespace ggml +} // namespace frontend +} // namespace ov diff --git a/ggml/src/ggml-openvino/openvino/pass/kv_state_seq_axis.cpp b/ggml/src/ggml-openvino/openvino/pass/kv_state_seq_axis.cpp new file mode 100644 index 000000000000..c9952b1d5201 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/pass/kv_state_seq_axis.cpp @@ -0,0 +1,114 @@ +#include "kv_state_seq_axis.h" + +#include <memory> +#include <openvino/core/graph_util.hpp> +#include <openvino/op/assign.hpp> +#include <openvino/op/concat.hpp> +#include <openvino/op/constant.hpp> +#include <openvino/op/read_value.hpp> +#include <openvino/op/transpose.hpp> +#include <vector> + +namespace ov { +namespace frontend { +namespace ggml { +namespace pass { + +namespace { + +const std::vector<int64_t> & seq_axis_perm() { + // [1, seq, n_heads_kv, head_size] <-> [1, n_heads_kv, seq, head_size] + static const std::vector<int64_t> perm{0, 2, 1, 3}; + return perm; +} + +// True when the state still has the frontend's stateful KV layout, so the sequence axis +// can be moved: rank 4, batch and both head dims static, and seq the only dynamic dim, +// at dim 1. Any KV head count is fine. With a single head the rewrite is pure metadata +// ([1, seq, 1, head] and [1, 1, seq, head] are the same memory); with several heads it +// also drops the reader-side transpose of the whole accumulated state, which is where +// most of the gain comes from at depth. +bool can_move_seq_axis(const ov::PartialShape & shape) { + return shape.rank().is_static() && shape.rank().get_length() == 4 && shape[0].is_static() && + shape[1].is_dynamic() && shape[2].is_static() && shape[3].is_static(); +} + +std::shared_ptr<ov::op::v0::Concat> match_kv_append(const std::shared_ptr<ov::op::v6::Assign> & assign) { + auto concat = ov::as_type_ptr<ov::op::v0::Concat>(assign->get_input_node_shared_ptr(0)); + if (!concat || concat->get_input_size() != 2 || concat->get_axis() != 1) { + return nullptr; + } + auto read_value = ov::as_type_ptr<ov::op::v6::ReadValue>(concat->get_input_node_shared_ptr(0)); + if (!read_value || read_value->get_variable() != assign->get_variable()) { + return nullptr; + } + if (!can_move_seq_axis(read_value->get_output_partial_shape(0))) { + return nullptr; + } + return concat; +} + +} // namespace + +bool KVStateSeqAxis::run_on_model(const std::shared_ptr<ov::Model> & model) { + std::vector<std::shared_ptr<ov::op::v6::Assign>> assigns; + for (const auto & op : model->get_ops()) { + if (auto assign = ov::as_type_ptr<ov::op::v6::Assign>(op)) { + assigns.push_back(assign); + } + } + + bool changed = false; + for (const auto & assign : assigns) { + auto concat = match_kv_append(assign); + if (!concat) { + continue; + } + auto read_value = ov::as_type_ptr<ov::op::v6::ReadValue>(concat->get_input_node_shared_ptr(0)); + + auto variable = read_value->get_variable(); + auto info = variable->get_info(); + const auto & shape = info.data_shape; + info.data_shape = ov::PartialShape{shape[0], shape[2], shape[1], shape[3]}; + variable->update(info); + read_value->validate_and_infer_types(); + + auto readers = concat->output(0).get_target_inputs(); + + auto new_rows = concat->input_value(1); + auto perm_in = ov::op::v0::Constant::create(ov::element::i64, {4}, seq_axis_perm()); + concat->set_argument(1, std::make_shared<ov::op::v1::Transpose>(new_rows, perm_in)); + concat->set_axis(2); + concat->validate_and_infer_types(); + + // Readers still expect seq at dim 1. A reader that is itself the inverse + // Transpose wanted seq at dim 2 all along, so drop it; give anything else the + // inverse Transpose so its input is unchanged. + for (auto & reader : readers) { + auto * node = reader.get_node(); + if (ov::is_type<ov::op::v6::Assign>(node)) { + continue; + } + bool dropped = false; + if (auto * transpose = ov::as_type<ov::op::v1::Transpose>(node)) { + auto order = ov::as_type_ptr<ov::op::v0::Constant>(transpose->get_input_node_shared_ptr(1)); + if (order && order->cast_vector<int64_t>() == seq_axis_perm()) { + ov::replace_output_update_name(transpose->output(0), concat->output(0)); + dropped = true; + } + } + if (!dropped) { + auto perm_out = ov::op::v0::Constant::create(ov::element::i64, {4}, seq_axis_perm()); + reader.replace_source_output(std::make_shared<ov::op::v1::Transpose>(concat->output(0), perm_out)); + } + } + changed = true; + } + + return changed; +} + +} // namespace pass +} // namespace ggml +} // namespace frontend +} // namespace ov diff --git a/ggml/src/ggml-openvino/openvino/pass/kv_state_seq_axis.h b/ggml/src/ggml-openvino/openvino/pass/kv_state_seq_axis.h new file mode 100644 index 000000000000..579022c45c59 --- /dev/null +++ b/ggml/src/ggml-openvino/openvino/pass/kv_state_seq_axis.h @@ -0,0 +1,24 @@ +#include "openvino/pass/pass.hpp" + +namespace ov { +namespace frontend { +namespace ggml { +namespace pass { + +// Moves the sequence axis of the stateful KV cache from dim 1 to dim 2, i.e. from +// [1, seq, n_heads_kv, head_size] to [1, n_heads_kv, seq, head_size], and updates the +// Concat that appends to it. Two wins: the GPU plugin only appends new tokens in place +// when the growing axis is a spatial axis, and the reader no longer has to transpose the +// whole accumulated state every token (that cost grows with context length, so it is the +// larger win at depth for a model with several KV heads). Only rewrites states that still +// match the frontend layout, so it no-ops if that layout ever changes. +class KVStateSeqAxis : public ov::pass::ModelPass { +public: + OPENVINO_MODEL_PASS_RTTI("ov::frontend::ggml::pass::KVStateSeqAxis") + bool run_on_model(const std::shared_ptr<ov::Model> & model) override; +}; + +} // namespace pass +} // namespace ggml +} // namespace frontend +} // namespace ov diff --git a/ggml/src/ggml-openvino/openvino/pass/squeeze_matmul.cpp b/ggml/src/ggml-openvino/openvino/pass/squeeze_matmul.cpp index 20a3a374934b..09c213f3e17c 100644 --- a/ggml/src/ggml-openvino/openvino/pass/squeeze_matmul.cpp +++ b/ggml/src/ggml-openvino/openvino/pass/squeeze_matmul.cpp @@ -2,6 +2,7 @@ #include <openvino/core/graph_util.hpp> #include <openvino/core/rt_info.hpp> +#include <openvino/core/type.hpp> #include <openvino/op/constant.hpp> #include <openvino/op/matmul.hpp> #include <openvino/op/squeeze.hpp> @@ -26,7 +27,7 @@ SqueezeMatmul::SqueezeMatmul() { const auto callback = [=](ov::pass::pattern::Matcher & m) { const auto & pattern_map = m.get_pattern_value_map(); auto matmul_node = - std::dynamic_pointer_cast<ov::op::v0::MatMul>(pattern_map.at(m_matmul).get_node_shared_ptr()); + ov::as_type_ptr<ov::op::v0::MatMul>(pattern_map.at(m_matmul).get_node_shared_ptr()); auto act = pattern_map.at(m_act); auto wei = pattern_map.at(m_wei); auto act_shape = act.get_partial_shape(); diff --git a/ggml/src/ggml-openvino/openvino/translate_session.cpp b/ggml/src/ggml-openvino/openvino/translate_session.cpp index df3a72f3286c..3170c2e4ce05 100644 --- a/ggml/src/ggml-openvino/openvino/translate_session.cpp +++ b/ggml/src/ggml-openvino/openvino/translate_session.cpp @@ -5,7 +5,9 @@ #include "ggml-openvino/openvino/node_context.h" #include "ggml-openvino/openvino/utils.h" #include "input_model.h" +#include "pass/fuse_moe_compressed.h" #include "pass/fuse_to_conv.h" +#include "pass/kv_state_seq_axis.h" #include "pass/mark_decompression_convert_constant_folding.h" #include "pass/mark_dequantization_subgraph.h" #include "pass/squeeze_matmul.h" @@ -19,28 +21,36 @@ #include <openvino/core/node.hpp> #include <openvino/core/preprocess/pre_post_process.hpp> #include <openvino/core/shape.hpp> +#include <openvino/core/type.hpp> #include <openvino/core/type/element_type.hpp> #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/convert_like.hpp> #include <openvino/op/cos.hpp> #include <openvino/op/divide.hpp> #include <openvino/op/gather.hpp> +#include <openvino/op/greater_eq.hpp> +#include <openvino/op/less.hpp> +#include <openvino/op/logical_and.hpp> #include <openvino/op/multiply.hpp> #include <openvino/op/parameter.hpp> #include <openvino/op/range.hpp> #include <openvino/op/reshape.hpp> #include <openvino/op/result.hpp> +#include <openvino/op/select.hpp> #include <openvino/op/sin.hpp> #include <openvino/op/slice.hpp> #include <openvino/op/squeeze.hpp> #include <openvino/op/strided_slice.hpp> +#include <openvino/op/subtract.hpp> #include <openvino/op/transpose.hpp> #include <openvino/op/unsqueeze.hpp> #include <openvino/pass/constant_folding.hpp> #include <openvino/pass/make_stateful.hpp> +#include <limits> #include <sstream> namespace ov { @@ -143,6 +153,64 @@ void add_sliced_mask_stateful(TensorMap & tensor_map) { create_sliced_mask("self_kq_mask_swa", "KQ_mask_swa_sliced"); } +// Rebuild the sliding-window mask from absolute positions. +// ggml caps self_kq_mask_swa at the size of its own SWA cache, but the stateful KV state is +// Concat-appended and grows without bound, so past that cap the two disagree on length and the +// mask add fails. A pure-Concat state is ordered by position, so positions can rebuild the mask. +// swa_window holds the real n_swa, read back from the ggml mask in ggml-decoder.cpp. +// No-op when the graph has no SWA mask, or when the window could not be read back. +void add_position_mask_stateful_swa(TensorMap & tensor_map) { + if (tensor_map.find("self_kq_mask_swa") == tensor_map.end() || tensor_map.find("inp_pos") == tensor_map.end() || + tensor_map.find("swa_window") == tensor_map.end()) { + return; + } + + auto inp_pos = tensor_map.at("inp_pos").get_node_shared_ptr(); + + auto zero_i64 = ov::op::v0::Constant::create(ov::element::i64, {1}, {0}); + auto one_i64 = ov::op::v0::Constant::create(ov::element::i64, {1}, {1}); + auto three = ov::op::v0::Constant::create(ov::element::i64, {1}, {3}); + auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}); + + auto query_pos = std::make_shared<ov::op::v0::Convert>(inp_pos, ov::element::i64); + auto query_pos_1d = std::make_shared<ov::op::v1::Reshape>( + query_pos, ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}), false); + + auto last_pos = std::make_shared<ov::op::v8::Gather>(inp_pos, neg_one, three); + auto last_pos_1d = std::make_shared<ov::op::v1::Reshape>(last_pos, one_i64, false); + auto last_pos_cvt = std::make_shared<ov::op::v0::Convert>(last_pos_1d, ov::element::i64); + auto total_len = std::make_shared<ov::op::v1::Add>(last_pos_cvt, one_i64); + auto total_len_scalar = std::make_shared<ov::op::v0::Squeeze>(total_len); + + auto cached_pos = std::make_shared<ov::op::v4::Range>( + ov::op::v0::Constant::create(ov::element::i64, {}, {0}), total_len_scalar, + ov::op::v0::Constant::create(ov::element::i64, {}, {1}), ov::element::i64); + + auto query_col = std::make_shared<ov::op::v1::Reshape>( + query_pos_1d, ov::op::v0::Constant::create(ov::element::i64, {2}, {-1, 1}), false); + auto cached_row = std::make_shared<ov::op::v1::Reshape>( + cached_pos, ov::op::v0::Constant::create(ov::element::i64, {2}, {1, -1}), false); + auto diff = std::make_shared<ov::op::v1::Subtract>(query_col, cached_row); + + auto swa_window = tensor_map.at("swa_window").get_node_shared_ptr(); + auto window = std::make_shared<ov::op::v0::Convert>(swa_window, ov::element::i64); + auto causal_ok = std::make_shared<ov::op::v1::GreaterEqual>(diff, zero_i64); + auto window_ok = std::make_shared<ov::op::v1::Less>(diff, window); + auto keep = std::make_shared<ov::op::v1::LogicalAnd>(causal_ok, window_ok); + + auto zero_f = ov::op::v0::Constant::create(ov::element::f32, {}, {0.0f}); + auto neg_inf_f = ov::op::v0::Constant::create(ov::element::f32, {}, {-std::numeric_limits<float>::infinity()}); + std::shared_ptr<ov::Node> mask = std::make_shared<ov::op::v1::Select>(keep, zero_f, neg_inf_f); + + auto batch_axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {0}); + mask = std::make_shared<ov::op::v0::Unsqueeze>(mask, batch_axis); + mask = std::make_shared<ov::op::v0::Unsqueeze>(mask, batch_axis); + mask = std::make_shared<ov::op::v0::Convert>(mask, ov::element::f16); + mask->set_friendly_name("KQ_mask_swa_sliced"); + + tensor_map["KQ_mask_swa_sliced"] = mask->output(0); +} + void add_rope_sin_cos(TensorMap & tensor_map, GgmlDecoder & ggml_model_decoder) { // When ROPE ops in the graph have divergent op_params (e.g. gemma4's mixed // SWA/non-SWA layers with different n_dims or freq_base), a shared sin/cos @@ -175,6 +243,7 @@ void add_rope_sin_cos(TensorMap & tensor_map, GgmlDecoder & ggml_model_decoder) void preprocess(TensorMap & tensor_map, GgmlDecoder & ggml_model_decoder) { if (ggml_model_decoder.is_stateful()) { add_sliced_mask_stateful(tensor_map); + add_position_mask_stateful_swa(tensor_map); } // This optimization is error-prone // add_rope_sin_cos(tensor_map, ggml_model_decoder); @@ -204,7 +273,7 @@ std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputMo auto tensor_map = std::make_shared<TensorMap>(); std::shared_ptr<Model> resulting_model; - const auto & ggml_model = std::dynamic_pointer_cast<InputModel>(input_model); + const auto & ggml_model = ov::as_type_ptr<InputModel>(input_model); std::shared_ptr<GgmlDecoder> ggml_model_decoder = ggml_model->get_model_decoder(); for (const auto & it : ggml_model_decoder->get_model_inputs()) { @@ -216,7 +285,7 @@ std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputMo for (const auto & it : ggml_model_decoder->get_model_extra_inputs()) { auto input_node = create_extra_input(it.first, it.second); if (it.second.is_parameter) { - params.push_back(std::dynamic_pointer_cast<ov::op::v0::Parameter>(input_node)); + params.push_back(ov::as_type_ptr<ov::op::v0::Parameter>(input_node)); } (*tensor_map)[it.first] = input_node; } @@ -387,7 +456,7 @@ std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputMo } std::shared_ptr<Model> TranslateSession::apply_transformations(std::shared_ptr<Model> model) { - auto ggml_model_decoder = std::dynamic_pointer_cast<InputModel>(m_input_model)->get_model_decoder(); + auto ggml_model_decoder = ov::as_type_ptr<InputModel>(m_input_model)->get_model_decoder(); { ov::pass::Manager manager; manager.set_per_pass_validation(true); @@ -400,10 +469,20 @@ std::shared_ptr<Model> TranslateSession::apply_transformations(std::shared_ptr<M std::vector<ov::element::Type>{ov::element::u8, ov::element::i8, ov::element::u4, ov::element::i4}); manager.register_pass<pass::FuseToConv>(); + // MOECompressed has no CPU plugin implementation, so keep the GatherMatmul path + // everywhere else. Opt-in while the fused path is being brought up. + if (ggml_openvino_get_device_name() == "GPU" && getenv("GGML_OPENVINO_MOE_OP")) { + manager.register_pass<pass::FuseMoeCompressed>(); + } + if (ggml_model_decoder->is_stateful()) { const auto kv_param_res_names = ggml_model_decoder->get_kv_param_res_names(); const auto kv_param_res_pairs = get_kv_param_res_pairs(model, kv_param_res_names); manager.register_pass<ov::pass::MakeStateful>(kv_param_res_pairs); + // Must run after MakeStateful, which is what creates the ReadValue/Assign pairs. + if (!ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT")) { + manager.register_pass<pass::KVStateSeqAxis>(); + } } if (ggml_model_decoder->is_static()) { diff --git a/ggml/src/ggml-openvino/utils.cpp b/ggml/src/ggml-openvino/utils.cpp index 93b1ccbe9075..44a9b2c78895 100644 --- a/ggml/src/ggml-openvino/utils.cpp +++ b/ggml/src/ggml-openvino/utils.cpp @@ -1,6 +1,7 @@ #include "utils.h" #include "ggml-impl.h" +#include "ggml-openvino.h" #include "ggml-openvino-extra.h" #include "ggml-openvino/ggml-decoder.h" #include "ggml.h" @@ -42,6 +43,11 @@ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +// Both execution paths use two cache levels: +// 1. Reuse this backend's decoder/request via graph_key and compatibility checks. +// 2. On a local miss, look up compiled_graph_key in the shared compilation cache, +// compile if needed, then create a private request from the compiled model. +// The shared lock covers compilation and frontend cleanup, never inference. enum ggml_status ov_graph_compute(ggml_cgraph * cgraph, ggml_backend_t backend) { ggml_backend_openvino_context * ctx = (ggml_backend_openvino_context *) backend->context; try { @@ -54,6 +60,7 @@ enum ggml_status ov_graph_compute(ggml_cgraph * cgraph, ggml_backend_t backend) GGML_ASSERT(ctx->runtime_context != nullptr); std::shared_ptr<ov_runtime_context> r_ctx = std::static_pointer_cast<ov_runtime_context>(ctx->runtime_context); + std::lock_guard<std::mutex> execution_lock(r_ctx->execution_mutex); return is_static ? ov_graph_compute_static(cgraph, r_ctx) : ov_graph_compute_dynamic(cgraph, r_ctx); } catch (const ov::Exception & e) { @@ -143,7 +150,7 @@ static uint64_t ggml_openvino_model_cache_extra_cfg(const std::string & device, ggml_openvino_getenv_int("GGML_OPENVINO_MANUAL_GQA_ATTN") > 0 : device == "GPU"; - uint64_t extra_cfg = 0; + uint64_t extra_cfg = 1; // Graph-ordinal port names (invalidate older disk-cache blobs). extra_cfg = extra_cfg * 131 + (stateful ? 1u : 0u); extra_cfg = extra_cfg * 131 + (ggml_openvino_reduce_compile_mem_enabled() ? 1u : 0u); extra_cfg = extra_cfg * 131 + (ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_KV_SLICE") ? 1u : 0u); @@ -151,6 +158,91 @@ static uint64_t ggml_openvino_model_cache_extra_cfg(const std::string & device, return extra_cfg; } +static std::map<std::string, std::shared_ptr<ov::Node>> get_weight_names(ggml_cgraph * cgraph) { + std::map<std::string, std::shared_ptr<ov::Node>> names; + for (const auto & name : GgmlOvDecoder::collect_weight_names(cgraph)) { + names[name] = nullptr; + } + return names; +} + +// A conservative, exact in-process key, evaluated only on a context-local cache +// miss. Include topology, layouts, op parameters, constant extra inputs and weight +// allocation identities. Never use a sampled weight hash or a graph name alone: +// different models can have identical topology. OV buffer IDs survive address reuse. +static std::string compiled_graph_key(const ggml_cgraph * graph, const GgmlOvDecoder & decoder, + const std::string & device, int prefill_chunk_size = 0) { + std::string key; + auto append = [&key](const auto & value) { + key.append(reinterpret_cast<const char *>(&value), sizeof(value)); + }; + auto append_string = [&](const std::string & value) { + append(value.size()); + key.append(value); + }; + append_string(device); + append(decoder.is_static()); + append(decoder.is_stateful()); + append(prefill_chunk_size); + bool has_weight_buffer_id = false; + std::unordered_map<const ggml_tensor *, size_t> ids; + std::function<void(const ggml_tensor *)> visit = [&](const ggml_tensor * tensor) { + if (!tensor) { + append(size_t(0)); + return; + } + auto inserted = ids.emplace(tensor, ids.size() + 1); + append(inserted.first->second); + if (!inserted.second) { + return; + } + append_string(tensor->name); + append(tensor->type); + append(tensor->op); + append(tensor->flags); + append(tensor->ne); + append(tensor->nb); + append(tensor->op_params); + append(tensor->view_offs); + const auto * base = tensor->view_src ? tensor->view_src : tensor; + const bool weight = base->buffer && base->buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS; + append(weight); + if (weight) { + const size_t buffer_id = ggml_backend_openvino_buffer_get_ctx_id(base->buffer); + has_weight_buffer_id |= buffer_id != 0; + append(buffer_id); + append(tensor->data); + } + visit(tensor->view_src); + for (const auto * src : tensor->src) { + visit(src); + } + }; + append(graph->n_nodes); + for (int i = 0; i < graph->n_nodes; ++i) { + visit(graph->nodes[i]); + } + append(graph->n_leafs); + for (int i = 0; i < graph->n_leafs; ++i) { + visit(graph->leafs[i]); + } + for (const auto & input : decoder.get_model_extra_inputs()) { + append_string(input.first); + append_string(input.second.type.get_type_name()); + append(input.second.shape.size()); + for (auto dim : input.second.shape) { + append(dim); + } + append(input.second.is_parameter); + if (!input.second.is_parameter) { + append(input.second.value); + } + } + // Without an allocation generation, pointer reuse could select stale weights. + // Such graphs still get private requests; they simply do not share compilation. + return has_weight_buffer_id ? key : std::string{}; +} + ov::Tensor create_ov_output_tensor(std::shared_ptr<GgmlOvDecoder> ggml_decoder, std::shared_ptr<ov::InferRequest> infer_request, int output_index, @@ -191,6 +283,26 @@ ov::Tensor create_ov_output_tensor(std::shared_ptr<GgmlOvDecoder> ggml_decoder, return output_tensor; } +// Rewrite ggml's KV rows into a relayout state that keeps the sequence on dim 2. +// ggml stores [seq][n_heads_kv * head_size]; the state wants [1, n_heads_kv, seq, head_size], +// a different element order, so the rows are copied instead of reinterpreted. +static ov::Tensor kv_rows_to_seq_axis_2(const ov::Tensor & kv_tensor, size_t n_heads_kv) { + const size_t rows = kv_tensor.get_shape()[2]; + const size_t head_size = kv_tensor.get_shape()[3] / n_heads_kv; + const size_t elem = kv_tensor.get_element_type().size(); + const size_t head_bytes = head_size * elem; + + ov::Tensor out(kv_tensor.get_element_type(), ov::Shape{1, n_heads_kv, rows, head_size}); + const auto * src = static_cast<const uint8_t *>(kv_tensor.data()); + auto * dst = static_cast<uint8_t *>(out.data()); + for (size_t s = 0; s < rows; s++) { + for (size_t h = 0; h < n_heads_kv; h++) { + memcpy(dst + (h * rows + s) * head_bytes, src + (s * n_heads_kv + h) * head_bytes, head_bytes); + } + } + return out; +} + enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr<ov_runtime_context> r_ctx) { auto & core = ov_singleton_core(); const auto & config = ggml_openvino_get_compile_config(); @@ -216,7 +328,7 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< if (is_naive(cgraph)) { if (!model_is_splitted) { - return naive_compute(cgraph, core, device, config); + return naive_compute(cgraph, core, device, config, *r_ctx->compiled_cache); } } @@ -260,6 +372,7 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< } std::lock_guard<std::mutex> lock(*(entry->mutex)); + cache_hit = cache_hit && entry->ptr && r_ctx->infer_request_cache.count(key) != 0; if (cache_hit) { ggml_decoder = entry->ptr; @@ -297,32 +410,90 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< } else if (r_ctx->stateful_kv_size == static_cast<size_t>(pos_data[0])) { r_ctx->stateful_kv_size += pos_shape[3]; } else { + const size_t pos_begin = static_cast<size_t>(pos_data[0]); + const bool refill = pos_begin > r_ctx->stateful_kv_size; + + // A refill seeds the state from ggml's KV cache, so it needs that cache to be a + // plain prefix: cell i must hold position i. An SWA layer keeps only the last + // n_swa positions, so once a position leaves the window ggml drops it and the + // remaining cells shift - cell i stops holding position i. While every position + // is still inside the window nothing has been dropped and the refill is sound. + if (refill && !ggml_decoder->get_model_params().swa_layers.empty()) { + const int n_swa = ggml_decoder->get_compute_params().swa_window; + if (n_swa < 0 || static_cast<size_t>(n_swa) < pos_begin) { + GGML_LOG_ERROR( + "GGML OpenVINO backend stateful inference failed: cannot resume at position %zu from a " + "state that holds %zu tokens, because the sliding-window layers keep only the last %d " + "positions. Run without GGML_OPENVINO_STATEFUL_EXECUTION.\n", + pos_begin, r_ctx->stateful_kv_size, n_swa); + return GGML_STATUS_FAILED; + } + } + + const bool relayout_enabled = + !ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT"); + auto states = infer_request->query_state(); for (auto state : states) { auto state_tensor = state.get_state(); auto state_tensor_shape = state_tensor.get_shape(); - if (static_cast<uint32_t>(pos_data[0]) > r_ctx->stateful_kv_size) { - std::string state_name; - try { - state_name = r_ctx->kv_state_input_name_map.at(state.get_name()); - } catch (...) { + + std::string state_name; + if (auto it = r_ctx->kv_state_input_name_map.find(state.get_name()); + it != r_ctx->kv_state_input_name_map.end()) { + state_name = it->second; + } + + // Which axis holds the sequence: pass::KVStateSeqAxis moves it from dim 1 + // to dim 2. The head count is still needed below, because only a 1-head + // state stays byte-compatible with ggml's cache buffer. gemma-4 12B mixes + // 1-head full layers with 8-head sliding layers, so it is per state. + int n_heads_kv = ggml_decoder->get_model_params().n_heads_kv; + if (auto layer = extract_layer_from_name(state_name); layer.has_value()) { + n_heads_kv = ggml_decoder->get_n_heads_kv_for_layer(layer.value()); + } + const bool relayout_this_state = relayout_enabled; + const size_t seq_axis = relayout_this_state ? 2 : 1; + const size_t head_axis = seq_axis == 2 ? 1 : 2; + + if (refill) { + if (state_name.empty()) { GGML_LOG_ERROR( "GGML OpenVINO backend stateful inference failed: no input found for the state\n"); return GGML_STATUS_FAILED; } auto kv_tensor = get_ov_input_tensor(ggml_decoder, state_name); - kv_tensor.set_shape({state_tensor_shape[0], kv_tensor.get_shape()[2], state_tensor_shape[2], - state_tensor_shape[3]}); - state_tensor = kv_tensor; + if (relayout_this_state && n_heads_kv != 1) { + // several heads with seq on dim 2: not the same bytes as ggml's + // buffer, so the rows have to be copied into the new order + state_tensor = kv_rows_to_seq_axis_2(kv_tensor, (size_t) n_heads_kv); + } else { + ov::Shape refill_shape(4); + refill_shape[0] = state_tensor_shape[0]; + refill_shape[seq_axis] = kv_tensor.get_shape()[2]; + refill_shape[head_axis] = state_tensor_shape[head_axis]; + refill_shape[3] = state_tensor_shape[3]; + kv_tensor.set_shape(refill_shape); + state_tensor = kv_tensor; + } state_tensor_shape = state_tensor.get_shape(); } + // Only ever shrink to a prefix the source really has. Slicing past it used to + // surface as a bare ov::Exception from the ROI constructor. + if (state_tensor_shape[seq_axis] < pos_begin) { + GGML_LOG_ERROR( + "GGML OpenVINO backend stateful inference failed: state '%s' holds %zu tokens on axis " + "%zu, cannot resume at position %zu\n", + state.get_name().c_str(), state_tensor_shape[seq_axis], seq_axis, pos_begin); + return GGML_STATUS_FAILED; + } ov::Coordinate begin = {0, 0, 0, 0}; - ov::Coordinate end = {state_tensor_shape[0], static_cast<uint32_t>(pos_data[0]), - state_tensor_shape[2], state_tensor_shape[3]}; + ov::Coordinate end(state_tensor_shape.begin(), state_tensor_shape.end()); + end[seq_axis] = pos_begin; ov::Tensor new_state_tensor(state_tensor, begin, end); state.set_state(new_state_tensor); } - r_ctx->stateful_kv_size = pos_data[0] + pos_shape[3]; + r_ctx->stateful_kv_size = pos_begin + pos_shape[3]; } } @@ -330,11 +501,30 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< conversion_end_time = decoder_end_time; compile_end_time = decoder_end_time; } else { + // Compilation can mutate shared weight nodes, so serialize cold paths. + // The lock is released before binding tensors or running inference. + auto shared_cache = r_ctx->compiled_cache; + std::unique_lock<std::mutex> compile_lock(shared_cache->mutex); + auto weight_names = get_weight_names(cgraph); + ggml_decoder = std::make_shared<GgmlOvDecoder>(cgraph, m_params, c_params, weight_names, + is_static, stateful, model_is_splitted); + const std::string shared_key = cache_enabled ? compiled_graph_key(cgraph, *ggml_decoder, device) : ""; + ov::CompiledModel shared_model; + bool imported = false; + auto shared_it = shared_cache->graphs.find(shared_key); + if (!shared_key.empty() && shared_it != shared_cache->graphs.end()) { + shared_model = shared_it->second.decode; + infer_request = std::make_shared<ov::InferRequest>(shared_model.create_infer_request()); + ov_input_names = shared_it->second.input_names; + ov_output_names = shared_it->second.output_names; + imported = true; + GGML_LOG_DEBUG("ggml-openvino: shared compiled model HIT (dynamic)\n"); + } // Fail fast: a cache-miss recompile feeds weight data to compile_model, but // GGML_OPENVINO_RELEASE_WEIGHTS (or GGML_OPENVINO_MEMORY_OPTIMIZE on GPU) // may have already dropped the host weight pages // (they would read as zeros). That mode requires stable graph shapes. - if (ggml_openvino_weight_buffers_released()) { + if (!imported && ggml_openvino_weight_buffers_released()) { GGML_ABORT( "ggml-openvino: a new graph needs to be compiled but host weight buffers were already " "released via GGML_OPENVINO_RELEASE_WEIGHTS/GGML_OPENVINO_MEMORY_OPTIMIZE. This mode requires " @@ -354,7 +544,6 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< const std::string model_cache_dir = ggml_openvino_model_cache_dir(); uint64_t model_fp = 0; std::string blob_path, manifest_path; - bool imported = false; // When the frontend model cache is active it supersedes the plugin-level // ov::cache_dir: a blob exported from a model compiled WITH cache_dir cannot // be re-imported (import returns an uninitialized model). Strip cache_dir / @@ -364,10 +553,10 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< mc_config.erase("CACHE_DIR"); mc_config.erase("CACHE_MODE"); } - if (!model_cache_dir.empty() && !model_is_splitted) { + if (!imported && !model_cache_dir.empty() && !model_is_splitted) { const uint64_t extra_cfg = ggml_openvino_model_cache_extra_cfg(device, stateful); model_fp = ggml_openvino_model_fingerprint(cgraph, device, /*fa=*/true, m_params.rope_params, - 15, extra_cfg); + 16, extra_cfg); blob_path = ggml_openvino_model_cache_blob_path(model_cache_dir, model_fp); manifest_path = ggml_openvino_model_cache_manifest_path(model_cache_dir, model_fp); @@ -393,6 +582,7 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< ggml_decoder = std::make_shared<GgmlOvDecoder>(cgraph, m_params, c_params, weight_names, is_static, stateful, model_is_splitted); infer_request = std::make_shared<ov::InferRequest>(cm.create_infer_request()); + shared_model = cm; entry->ptr = ggml_decoder; // Names must match the decoder's ggml-tensor keys. The non-cached // path keys off Parameter/Result *friendly names* (set by the @@ -486,6 +676,7 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< } infer_request = std::make_shared<ov::InferRequest>(compiled_model.create_infer_request()); + shared_model = compiled_model; entry->ptr = ggml_decoder; for (const auto & ov_param : model->get_parameters()) { @@ -496,6 +687,11 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< } } // end non-imported (compile) path + entry->ptr = ggml_decoder; + if (!shared_key.empty() && shared_it == shared_cache->graphs.end()) { + shared_cache->graphs.emplace(shared_key, ov_compiled_graph{shared_model, {}, ov_input_names, + ov_output_names}); + } if (cache_enabled) { std::lock_guard<std::mutex> map_lock(r_ctx->ctx_mutex); r_ctx->infer_request_cache[key] = infer_request; @@ -506,6 +702,18 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< if (stateful && cache_enabled) { const auto * inp_pos = get_inp_pos_tensor(cgraph); auto pos_shape = ggml_decoder->get_shape(inp_pos); + // A freshly compiled model starts with an empty state, so it can only serve a + // sequence from its beginning. A non-zero start position means the KV history was + // built elsewhere (a restored ggml cache), which the state cannot adopt. + const int32_t pos_begin = ((int32_t *) inp_pos->data)[0]; + if (pos_begin != 0) { + GGML_LOG_ERROR( + "GGML OpenVINO backend stateful inference failed: a new model was compiled for a sequence that " + "starts at position %d, but its state is empty. Run without " + "GGML_OPENVINO_STATEFUL_EXECUTION.\n", + pos_begin); + return GGML_STATUS_FAILED; + } r_ctx->stateful_kv_size = pos_shape[3]; const auto kv_param_res_names = ggml_decoder->get_kv_param_res_names(); for (const auto & pair : kv_param_res_names) { @@ -570,21 +778,31 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr< // GGML_OPENVINO_RELEASE_WEIGHTS (or GGML_OPENVINO_MEMORY_OPTIMIZE on GPU): the plugin holds its own device copy of // every weight after compile, so the host weight buffers can be dropped to reclaim - // RSS. The GPU backend uses a single dynamic-shape model for both prefill and decode, - // so once a graph is compiled it is reused for the whole session — the only thing - // that forces a recompile is clear_caches() on backend teardown. We therefore release - // on the first cache-hit (model compiled, plugin has its copy) and, crucially, pin the - // compiled-model cache so it survives backend teardown (see ggml_backend_openvino_free). - // Without the pin, a later test/context would recompile against the now-dropped pages. - // A genuinely new graph still fails fast at the cache-miss compile branch. - if (cache_hit && ggml_openvino_release_weights_enabled(device) && - !ggml_openvino_weight_buffers_released()) { - ggml_openvino_release_weight_buffers(); + // RSS. Release only while holding the compilation mutex so another context cannot + // be reading host weights during conversion/compilation. Pin the shared compiled + // models across backend teardown; a later context can create its own request without + // reading the dropped pages. A new, uncached graph still fails fast above. + if (cache_hit && ggml_openvino_release_weights_enabled(device)) { + std::lock_guard<std::mutex> compile_lock(r_ctx->compiled_cache->mutex); + if (!ggml_openvino_weight_buffers_released()) { + ggml_openvino_release_weight_buffers(); + } } return GGML_STATUS_SUCCESS; } +static ov::AnyMap without_npuw(const ov::AnyMap & config) { + ov::AnyMap out; + for (const auto & kv : config) { + if (kv.first.rfind("NPUW", 0) == 0 || kv.first == "NPU_USE_NPUW") { + continue; + } + out.insert(kv); + } + return out; +} + enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<ov_runtime_context> r_ctx) { auto & core = ov_singleton_core(); @@ -606,7 +824,7 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o const auto & config = ggml_openvino_get_compile_config(); if (is_naive(cgraph)) { - return naive_compute(cgraph, core, device, config); + return naive_compute(cgraph, core, device, config, *r_ctx->compiled_cache); } auto start_time = ggml_time_us(); @@ -618,7 +836,12 @@ 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(cgraph, inp_pos); + const bool no_kv_cache = m_params.is_cacheless_attn; + const auto is_prefill = no_kv_cache ? true : get_is_prefill(cgraph, inp_pos); + const ov::AnyMap compile_config = no_kv_cache ? without_npuw(config) : config; + if (m_params.n_heads_kv == -1) { + prefill_chunk_size = inp_pos->ne[0]; + } graph_key key(cgraph); static const bool cache_enabled = !ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_CACHE"); bool cache_hit = false; @@ -652,6 +875,8 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o } std::lock_guard<std::mutex> lock(*(entry->mutex)); + cache_hit = cache_hit && entry->ptr && r_ctx->infer_request_cache.count(key) != 0 && + r_ctx->infer_request_cache_prefill.count(key) != 0; if (cache_hit) { ggml_decoder = entry->ptr; @@ -689,88 +914,122 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o r_ctx->infer_request_cache_prefill.erase(key); } - std::shared_ptr<ov::Model> model; - auto model_weights = GgmlOvDecoder::create_weight_nodes(cgraph); + // Static execution shares a compiled prefill/decode pair. Each backend + // creates and retains its own requests for both phases. + auto shared_cache = r_ctx->compiled_cache; + std::unique_lock<std::mutex> compile_lock(shared_cache->mutex); + auto weight_names = get_weight_names(cgraph); + auto local_decoder = std::make_shared<GgmlOvDecoder>( + cgraph, m_params, c_params, weight_names, is_static, stateful, false, is_prefill, prefill_chunk_size); + const std::string shared_key = cache_enabled ? + compiled_graph_key(cgraph, *local_decoder, device, prefill_chunk_size) : ""; + auto shared_it = shared_cache->graphs.find(shared_key); + if (!shared_key.empty() && shared_it != shared_cache->graphs.end()) { + auto & compiled = shared_it->second; + auto prefill_request = std::make_shared<ov::InferRequest>(compiled.prefill.create_infer_request()); + auto decode_request = no_kv_cache ? prefill_request : + std::make_shared<ov::InferRequest>(compiled.decode.create_infer_request()); + ggml_decoder = local_decoder; + entry->ptr = ggml_decoder; + infer_request = is_prefill ? prefill_request : decode_request; + ov_input_names_local = compiled.input_names; + ov_output_names_local = compiled.output_names; + r_ctx->infer_request_cache_prefill[key] = prefill_request; + r_ctx->infer_request_cache[key] = decode_request; + r_ctx->ov_input_names_cache[key] = ov_input_names_local; + r_ctx->ov_output_names_cache[key] = ov_output_names_local; + decoder_end_time = conversion_end_time = compile_end_time = ggml_time_us(); + GGML_LOG_DEBUG("ggml-openvino: shared compiled model HIT (static)\n"); + } else { + std::shared_ptr<ov::Model> model; + auto model_weights = GgmlOvDecoder::create_weight_nodes(cgraph); + + auto ggml_decoder_prefill = std::make_shared<GgmlOvDecoder>( + cgraph, m_params, c_params, model_weights, is_static, stateful, false, true, prefill_chunk_size); + auto ggml_decoder_decode = + no_kv_cache ? ggml_decoder_prefill : + std::make_shared<GgmlOvDecoder>(cgraph, m_params, c_params, model_weights, is_static, + stateful, false, false, prefill_chunk_size); + decoder_end_time = ggml_time_us(); + + 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 build_static_model = [&core, &compile_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 (m_params.n_heads_kv == -1) { - // graph is not a LLM, e.g. context-shift graph - prefill_chunk_size = inp_pos->ne[0]; - } - auto ggml_decoder_prefill = std::make_shared<GgmlOvDecoder>( - cgraph, m_params, c_params, model_weights, is_static, stateful, false, true, prefill_chunk_size); - auto ggml_decoder_decode = std::make_shared<GgmlOvDecoder>(cgraph, m_params, c_params, model_weights, is_static, - stateful, false, false, prefill_chunk_size); - decoder_end_time = ggml_time_us(); + 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); + } - 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 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 (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, compile_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; + 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; + build_static_model(ggml_decoder_prefill, "prefill", model_prefill, compiled_model_prefill, + infer_request_prefill, prefill_conversion_end_time, prefill_compile_end_time); + if (no_kv_cache) { + model_decode = model_prefill; + compiled_model_decode = compiled_model_prefill; + infer_request_decode = infer_request_prefill; + decode_conversion_end_time = prefill_conversion_end_time; + decode_compile_end_time = prefill_compile_end_time; + } else { + build_static_model(ggml_decoder_decode, "decode", model_decode, compiled_model_decode, infer_request_decode, + decode_conversion_end_time, decode_compile_end_time); } + 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); - 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; - 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; - infer_request = is_prefill ? infer_request_prefill : infer_request_decode; - entry->ptr = ggml_decoder; - - for (const auto & ov_param : model->get_parameters()) { - ov_input_names_local.push_back(ov_param->get_friendly_name()); - } - for (const auto & ov_output : model->get_results()) { - ov_output_names_local.push_back(ov_output->get_friendly_name()); - } + model = is_prefill ? model_prefill : model_decode; + ggml_decoder = is_prefill ? ggml_decoder_prefill : ggml_decoder_decode; + infer_request = is_prefill ? infer_request_prefill : infer_request_decode; + entry->ptr = ggml_decoder; - if (cache_enabled) { - std::lock_guard<std::mutex> map_lock(r_ctx->ctx_mutex); - r_ctx->infer_request_cache_prefill[key] = infer_request_prefill; - r_ctx->infer_request_cache[key] = infer_request_decode; - r_ctx->ov_input_names_cache[key] = ov_input_names_local; - r_ctx->ov_output_names_cache[key] = ov_output_names_local; + for (const auto & ov_param : model->get_parameters()) { + ov_input_names_local.push_back(ov_param->get_friendly_name()); + } + for (const auto & ov_output : model->get_results()) { + ov_output_names_local.push_back(ov_output->get_friendly_name()); + } + + if (!shared_key.empty()) { + shared_cache->graphs.emplace(shared_key, ov_compiled_graph{compiled_model_decode, compiled_model_prefill, + ov_input_names_local, ov_output_names_local}); + } + + if (cache_enabled) { + std::lock_guard<std::mutex> map_lock(r_ctx->ctx_mutex); + r_ctx->infer_request_cache_prefill[key] = infer_request_prefill; + r_ctx->infer_request_cache[key] = infer_request_decode; + r_ctx->ov_input_names_cache[key] = ov_input_names_local; + r_ctx->ov_output_names_cache[key] = ov_output_names_local; + } } + } if (is_prefill) { @@ -961,11 +1220,13 @@ bool is_naive(ggml_cgraph * cgraph) { enum ggml_status naive_compute(ggml_cgraph * cgraph, ov::Core & core, const std::string & device, - const ov::AnyMap & config) { + const ov::AnyMap & config, + ov_compiled_model_cache & cache) { if (cgraph->n_nodes == 1 && (cgraph->nodes[0]->op == GGML_OP_NONE || cgraph->nodes[0]->op == GGML_OP_VIEW)) { return GGML_STATUS_SUCCESS; } + std::unique_lock<std::mutex> compile_lock(cache.mutex); bool naive = true; auto model_weights = GgmlOvDecoder::create_weight_nodes(cgraph, naive); auto decoder = std::make_shared<GgmlOvDecoder>(cgraph, model_weights); @@ -977,23 +1238,38 @@ enum ggml_status naive_compute(ggml_cgraph * cgraph, std::shared_ptr<ov::InferRequest> infer_request; auto remote_context = ggml_openvino_get_remote_context(); + ov::AnyMap compile_config = config; if (cgraph->nodes[0]->op == GGML_OP_MUL_MAT) { // TODO ACCURACY hint triggers a bug in GPU plugin/driver on Lunar Lake. Remove once CVS-182166 is resolved - core.set_property(device, ov::hint::execution_mode(ov::hint::ExecutionMode::PERFORMANCE)); + compile_config[ov::hint::execution_mode.name()] = ov::hint::ExecutionMode::PERFORMANCE; } else { - core.set_property(device, ov::hint::execution_mode(ov::hint::ExecutionMode::ACCURACY)); + compile_config[ov::hint::execution_mode.name()] = ov::hint::ExecutionMode::ACCURACY; } if (remote_context.has_value()) { infer_request = std::make_shared<ov::InferRequest>( - core.compile_model(model, remote_context.value(), config).create_infer_request()); + core.compile_model(model, remote_context.value(), compile_config).create_infer_request()); } else { infer_request = - std::make_shared<ov::InferRequest>(core.compile_model(model, device, config).create_infer_request()); - } - - auto ov_params = model->get_parameters(); - for (size_t i = 0; i < ov_params.size(); i++) { - auto param_name = ov_params[i]->get_friendly_name(); + std::make_shared<ov::InferRequest>(core.compile_model(model, device, compile_config).create_infer_request()); + } + std::vector<std::string> input_names; + std::vector<std::string> output_names; + for (const auto & param : model->get_parameters()) { + input_names.push_back(param->get_friendly_name()); + } + for (const auto & result : model->get_results()) { + output_names.push_back(result->get_friendly_name()); + } + // Destroy the frontend graph under the compilation lock as well: it can + // still own edges into the shared weight nodes. + model.reset(); + input_model.reset(); + decoder->clear_model_weights(); + model_weights.clear(); + compile_lock.unlock(); + + for (size_t i = 0; i < input_names.size(); i++) { + const auto & param_name = input_names[i]; auto input_tensor = get_ov_input_tensor(decoder, param_name); infer_request->set_input_tensor(i, input_tensor); } @@ -1003,16 +1279,15 @@ enum ggml_status naive_compute(ggml_cgraph * cgraph, infer_request->infer(); - auto ov_results = model->get_results(); - for (size_t i = 0; i < ov_results.size(); i++) { + for (size_t i = 0; i < output_names.size(); i++) { auto output_tensor = infer_request->get_output_tensor(i); const auto & model_outputs = decoder->get_model_outputs(); - auto model_output_it = model_outputs.find(ov_results[i]->get_friendly_name()); + auto model_output_it = model_outputs.find(output_names[i]); if (model_output_it == model_outputs.end()) { // Debug-only output added via GGML_OPENVINO_DEBUG_NODE; nothing to copy into. if (ggml_openvino_getenv_int("GGML_OPENVINO_DEBUG_OUTPUT") || ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) { - print_output_tensor_info(ov_results[i]->get_friendly_name(), output_tensor, output_tensor.data()); + print_output_tensor_info(output_names[i], output_tensor, output_tensor.data()); } continue; } @@ -1262,6 +1537,20 @@ ov::Tensor get_ov_input_tensor_static_prefill(std::shared_ptr<GgmlOvDecoder> ggm return input_tensor; } + if (GgmlOvDecoder::is_inp_mean(ggml_tensor, op)) { + const size_t n_seqs = ggml_tensor->ne[1]; + const size_t src_stride = ggml_tensor->ne[0]; + const size_t copy_len = std::min<size_t>(chunk_valid_size, src_stride - chunk_index * chunk_size); + ov::Tensor input_tensor(ov::element::f32, ov::Shape{1, 1, n_seqs, chunk_size}); + auto * dst = input_tensor.data<float>(); + std::fill(dst, dst + n_seqs * chunk_size, 0.0f); + const auto * src = static_cast<const float *>(ggml_tensor->data) + chunk_index * chunk_size; + for (size_t s = 0; s < n_seqs; s++) { + std::memcpy(dst + s * chunk_size, src + s * src_stride, copy_len * sizeof(float)); + } + return input_tensor; + } + if (GgmlOvDecoder::is_inp_mask(ggml_tensor, op)) { size_t cols = ggml_tensor->ne[0]; size_t rows = ggml_tensor->ne[1]; diff --git a/ggml/src/ggml-openvino/utils.h b/ggml/src/ggml-openvino/utils.h index 5aa74da38d3b..235b15d7e90a 100644 --- a/ggml/src/ggml-openvino/utils.h +++ b/ggml/src/ggml-openvino/utils.h @@ -2,7 +2,6 @@ #include "ggml-impl.h" #include <algorithm> -#include <atomic> #include <cstddef> #include <functional> #include <memory> @@ -14,6 +13,8 @@ #include <utility> #include <vector> +// Local execution-cache key. A match still needs the ModelParams compatibility +// check; this key alone does not identify weights or a compiled model. struct graph_key { int n_nodes; std::string first_node_name; @@ -26,14 +27,13 @@ struct graph_key { last_node_name = cgraph->nodes[n_nodes - 1]->name; } - auto get_input_key_name = [](const ggml_cgraph * graph, const ggml_tensor * tensor) { - std::string name = tensor->name; - const size_t hash_pos = ggml_hash_find(&graph->visited_hash_set, tensor); - if (((tensor->flags & GGML_TENSOR_FLAG_COMPUTE) || GgmlOvDecoder::is_kvcache(tensor, nullptr)) && - hash_pos != GGML_HASHSET_FULL && ggml_bitset_get(graph->visited_hash_set.used, hash_pos)) { - name += "#" + std::to_string(hash_pos); + std::unordered_map<const ggml_tensor *, std::string> names; + auto get_input_key_name = [&names](const ggml_cgraph * graph, const ggml_tensor * tensor) { + auto it = names.find(tensor); + if (it == names.end()) { + it = names.emplace(tensor, GgmlOvDecoder::get_tensor_name(graph, tensor)).first; } - return name; + return it->second; }; std::vector<std::string> node_names; @@ -90,7 +90,27 @@ struct decoder_runtime_ctx { std::shared_ptr<GgmlOvDecoder> ptr; }; +struct ov_compiled_graph { + ov::CompiledModel decode; + ov::CompiledModel prefill; + std::vector<std::string> input_names; + std::vector<std::string> output_names; +}; + +// Only compilation and cache publication use this mutex. Requests, decoders and +// sequence state belong to individual backend contexts and never enter this cache. +struct ov_compiled_model_cache { + std::mutex mutex; + std::unordered_map<std::string, ov_compiled_graph> graphs; + size_t backend_count = 0; +}; + +// Private to one backend instance. Only compiled_cache is shared with other +// instances; clearing these local caches cannot invalidate their requests. struct ov_runtime_context { + // Serializes calls on this backend only, not inference in other contexts. + std::mutex execution_mutex; + std::shared_ptr<ov_compiled_model_cache> compiled_cache; mutable std::mutex ctx_mutex; std::string device; bool stateful; @@ -99,13 +119,10 @@ struct ov_runtime_context { std::unordered_map<graph_key, std::shared_ptr<ov::InferRequest>, graph_key_hash> infer_request_cache_prefill; std::unordered_map<graph_key, std::vector<std::string>, graph_key_hash> ov_input_names_cache; std::unordered_map<graph_key, std::vector<std::string>, graph_key_hash> ov_output_names_cache; - //TODO: Stateful is only supported for single request at a time. - // Simultanous stateful inference request support to be added. size_t stateful_kv_size; std::map<std::string, std::string> kv_state_input_name_map; - std::atomic<int> backend_count; - ov_runtime_context() : device("CPU"), stateful(false), stateful_kv_size(0), backend_count(0) {} + ov_runtime_context() : device("CPU"), stateful(false), stateful_kv_size(0) {} void clear_caches_locked() { decoder_cache.clear(); @@ -192,4 +209,5 @@ bool is_model_splitted(struct ggml_cgraph * cgraph); enum ggml_status naive_compute(struct ggml_cgraph * cgraph, ov::Core & core, const std::string & device, - const ov::AnyMap & config); + const ov::AnyMap & config, + ov_compiled_model_cache & cache); diff --git a/ggml/src/ggml-rpc/CMakeLists.txt b/ggml/src/ggml-rpc/CMakeLists.txt index af3bd0290f7c..e3d0c9b4c200 100644 --- a/ggml/src/ggml-rpc/CMakeLists.txt +++ b/ggml/src/ggml-rpc/CMakeLists.txt @@ -36,8 +36,10 @@ if (GGML_RPC_RDMA) target_compile_definitions(ggml-rpc PRIVATE GGML_RPC_RDMA) if (APPLE) # librdma.dylib only exists on macOS 26.2 and later. Link it weakly so a build made - # where it exists still loads where it does not; checked at runtime before use. - target_link_options(ggml-rpc PRIVATE "LINKER:-weak_library,${RDMA_LIB}") + # where it exists still loads where it does not; checked at runtime before use + # but with BUILD_SHARED_LIBS=OFF ggml-rpc is a static archive and never links + # so the librdma symbols used by transport-apple.cpp stay undefined. + target_link_options(ggml-rpc PUBLIC "LINKER:-weak_library,${RDMA_LIB}") target_compile_definitions(ggml-rpc PRIVATE GGML_RPC_RDMA_APPLE) target_sources(ggml-rpc PRIVATE transport-apple.cpp) else() diff --git a/ggml/src/ggml-rpc/ggml-rpc.cpp b/ggml/src/ggml-rpc/ggml-rpc.cpp index cc7d7206933f..adb88a2456ef 100644 --- a/ggml/src/ggml-rpc/ggml-rpc.cpp +++ b/ggml/src/ggml-rpc/ggml-rpc.cpp @@ -697,10 +697,31 @@ static void ggml_backend_rpc_buffer_memset_tensor( ctx->dispatcher->send(RPC_CMD_MEMSET_TENSOR, request, sizeof(*request)); } +// input serialization format: | rpc_tensor | cache_flag (1 byte) | offset (8 bytes) | data (size bytes) +static std::shared_ptr<uint8_t> serialize_set_tensor(const rpc_tensor & rpc_tensor, uint8_t cache_flag, uint64_t offset, const void * data, size_t size, size_t & input_size) { + input_size = sizeof(rpc_tensor) + sizeof(cache_flag) + sizeof(offset) + size; + uint8_t * input = new uint8_t[input_size](); + uint8_t * p = input; + memcpy(p, &rpc_tensor, sizeof(rpc_tensor)); p += sizeof(rpc_tensor); + memcpy(p, &cache_flag, sizeof(cache_flag)); p += sizeof(cache_flag); + memcpy(p, &offset, sizeof(offset)); p += sizeof(offset); + memcpy(p, data, size); + return std::shared_ptr<uint8_t>(input, std::default_delete<uint8_t[]>()); +} + +// the hash cache is meant for weights, so that a model reload can skip re-sending them. +// compute-buffer inputs (the activations ggml_backend_sched copies between backends) must not +// take this path, otherwise with `rpc-server -c` every ubatch above the threshold is written +// to the cache directory and later served from there. +static bool rpc_use_hash_cache(const ggml_tensor * tensor, size_t size) { + return size > HASH_THRESHOLD && tensor->buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS; +} + static void ggml_backend_rpc_buffer_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor, const void * data, size_t offset, size_t size) { ggml_backend_rpc_buffer_context * ctx = (ggml_backend_rpc_buffer_context *)buffer->context; rpc_tensor rpc_tensor = serialize_tensor(tensor); - if (size > HASH_THRESHOLD) { + uint8_t cache_flag = 0; + if (rpc_use_hash_cache(tensor, size)) { auto request = std::make_shared<rpc_msg_set_tensor_hash_req>(); request->tensor = rpc_tensor; request->offset = offset; @@ -711,15 +732,12 @@ static void ggml_backend_rpc_buffer_set_tensor(ggml_backend_buffer_t buffer, ggm // the server has the same data, no need to send it return; } + // the server has no cache entry for this tensor - ask it to save one + cache_flag = 1; } - // input serialization format: | rpc_tensor | offset (8 bytes) | data (size bytes) - size_t input_size = sizeof(rpc_tensor) + sizeof(uint64_t) + size; - uint8_t * input = new uint8_t[input_size](); - memcpy(input, &rpc_tensor, sizeof(rpc_tensor)); - memcpy(input + sizeof(rpc_tensor), &offset, sizeof(offset)); - memcpy(input + sizeof(rpc_tensor) + sizeof(offset), data, size); - std::shared_ptr<uint8_t> input_ptr(input, std::default_delete<uint8_t[]>()); - ctx->dispatcher->send(RPC_CMD_SET_TENSOR, input_ptr, input_size); + size_t input_size; + auto input = serialize_set_tensor(rpc_tensor, cache_flag, offset, data, size, input_size); + ctx->dispatcher->send(RPC_CMD_SET_TENSOR, input, input_size); } static void ggml_backend_rpc_buffer_get_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * tensor, void * data, size_t offset, size_t size) { @@ -927,7 +945,8 @@ static void ggml_backend_rpc_free(ggml_backend_t backend) { static void ggml_backend_rpc_set_tensor_async(ggml_backend_t backend, ggml_tensor * tensor, const void * data, size_t offset, size_t size) { ggml_backend_rpc_context * ctx = (ggml_backend_rpc_context *)backend->context; rpc_tensor rpc_tensor = serialize_tensor(tensor); - if (size > HASH_THRESHOLD) { + uint8_t cache_flag = 0; + if (rpc_use_hash_cache(tensor, size)) { auto request = std::make_shared<rpc_msg_set_tensor_hash_req>(); request->tensor = rpc_tensor; request->offset = offset; @@ -939,15 +958,12 @@ static void ggml_backend_rpc_set_tensor_async(ggml_backend_t backend, ggml_tenso // the server has the same data, no need to send it return; } + // the server has no cache entry for this tensor - ask it to save one + cache_flag = 1; } - // input serialization format: | rpc_tensor | offset (8 bytes) | data (size bytes) - size_t input_size = sizeof(rpc_tensor) + sizeof(uint64_t) + size; - uint8_t * input = new uint8_t[input_size](); - memcpy(input, &rpc_tensor, sizeof(rpc_tensor)); - memcpy(input + sizeof(rpc_tensor), &offset, sizeof(offset)); - memcpy(input + sizeof(rpc_tensor) + sizeof(offset), data, size); - std::shared_ptr<uint8_t> input_ptr(input, std::default_delete<uint8_t[]>()); - ctx->dispatcher->send_async(RPC_CMD_SET_TENSOR, input_ptr, input_size); + size_t input_size; + auto input = serialize_set_tensor(rpc_tensor, cache_flag, offset, data, size, input_size); + ctx->dispatcher->send_async(RPC_CMD_SET_TENSOR, input, input_size); } static void ggml_backend_rpc_get_tensor_async(ggml_backend_t backend, const ggml_tensor * tensor, void * data, size_t offset, size_t size) { @@ -1398,14 +1414,17 @@ ggml_tensor * rpc_server::deserialize_tensor(struct ggml_context * ctx, const rp bool rpc_server::set_tensor(const std::vector<uint8_t> & input) { - // serialization format: | rpc_tensor | offset (8 bytes) | data (size bytes) | - if (input.size() < sizeof(rpc_tensor) + sizeof(uint64_t)) { + // serialization format: | rpc_tensor | cache_flag (1 byte) | offset (8 bytes) | data (size bytes) | + uint8_t cache_flag; + uint64_t offset; + const size_t header_size = sizeof(rpc_tensor) + sizeof(cache_flag) + sizeof(offset); + if (input.size() < header_size) { return false; } const rpc_tensor * in_tensor = (const rpc_tensor *)input.data(); - uint64_t offset; - memcpy(&offset, input.data() + sizeof(rpc_tensor), sizeof(offset)); - const size_t size = input.size() - sizeof(rpc_tensor) - sizeof(offset); + memcpy(&cache_flag, input.data() + sizeof(rpc_tensor), sizeof(cache_flag)); + memcpy(&offset, input.data() + sizeof(rpc_tensor) + sizeof(cache_flag), sizeof(offset)); + const size_t size = input.size() - header_size; struct ggml_init_params params { /*.mem_size =*/ ggml_tensor_overhead(), @@ -1434,8 +1453,8 @@ bool rpc_server::set_tensor(const std::vector<uint8_t> & input) { } } - const void * data = input.data() + sizeof(rpc_tensor) + sizeof(offset); - if (cache_dir && size > HASH_THRESHOLD) { + const void * data = input.data() + header_size; + if (cache_dir && cache_flag) { uint64_t hash = fnv_hash((const uint8_t*)data, size); char hash_str[17]; snprintf(hash_str, sizeof(hash_str), "%016" PRIx64, hash); diff --git a/ggml/src/ggml-sycl/CMakeLists.txt b/ggml/src/ggml-sycl/CMakeLists.txt index a8d9c0d804bf..d2196f74d565 100644 --- a/ggml/src/ggml-sycl/CMakeLists.txt +++ b/ggml/src/ggml-sycl/CMakeLists.txt @@ -110,15 +110,21 @@ if (GGML_SYCL_SUPPORT_LEVEL_ZERO_API) # Link against Level Zero loader for direct device memory allocation. # Avoids sycl::malloc_device triggering DMA-buf/TTM system RAM staging # in the xe kernel driver during multi-GPU inference. - find_path(LEVEL_ZERO_INCLUDE_DIR level_zero/ze_api.h HINTS ${ONEAPI_ROOT}/include ${LEVEL_ZERO_V1_SDK_PATH}/include) + find_path(LEVEL_ZERO_DEV_INCLUDE_DIR level_zero/ze_api.h HINTS ${ONEAPI_ROOT}/include ${LEVEL_ZERO_V1_SDK_PATH}/include) find_library(ZE_LOADER_LIB ze_loader HINTS ${ONEAPI_ROOT}/lib ${LEVEL_ZERO_V1_SDK_LIB_PATH} ENV LD_LIBRARY_PATH) - if(ZE_LOADER_LIB AND LEVEL_ZERO_INCLUDE_DIR) + if(ZE_LOADER_LIB AND LEVEL_ZERO_DEV_INCLUDE_DIR) target_link_libraries(ggml-sycl PRIVATE ${ZE_LOADER_LIB}) target_compile_definitions(ggml-sycl PRIVATE GGML_SYCL_SUPPORT_LEVEL_ZERO_API) message(STATUS "Level Zero loader found: ${ZE_LOADER_LIB}") - message(STATUS "Level Zero headers found: ${LEVEL_ZERO_INCLUDE_DIR}") + message(STATUS "Level Zero development headers found: ${LEVEL_ZERO_DEV_INCLUDE_DIR}") else() - message(WARNING "Level Zero loader or headers not found, Level Zero support disabled") + message(WARNING "Level Zero loader or development headers not found, " + "Level Zero API support disabled. " + "Please install the Level Zero SDK/development package " + "to support Level Zero API features. " + "Level Zero API is not mandatory for SYCL backend, " + "but it is required by the special features for better " + "function & performance on Intel GPUs.") endif() endif() diff --git a/ggml/src/ggml-sycl/backend.hpp b/ggml/src/ggml-sycl/backend.hpp index 51ab6f930dca..ab80a2a3b941 100644 --- a/ggml/src/ggml-sycl/backend.hpp +++ b/ggml/src/ggml-sycl/backend.hpp @@ -44,6 +44,7 @@ #include "ssm_conv.hpp" #include "softmax.hpp" #include "topk-moe.hpp" +#include "topk-radix.hpp" #include "tsembd.hpp" #include "upscale.hpp" #include "wkv.hpp" diff --git a/ggml/src/ggml-sycl/base.hpp b/ggml/src/ggml-sycl/base.hpp index 3afd57ccb2db..fe96c4ab855e 100644 --- a/ggml/src/ggml-sycl/base.hpp +++ b/ggml/src/ggml-sycl/base.hpp @@ -17,6 +17,7 @@ #include <cstdio> extern int g_ggml_sycl_debug; +extern int g_ggml_sycl_dev_debug; #if defined(__clang__) && __has_builtin(__builtin_expect) // Hint the optimizer to pipeline the more likely following instruction in branches @@ -33,4 +34,10 @@ extern int g_ggml_sycl_debug; fprintf(stderr, __VA_ARGS__); \ } while (0) +#define GGML_SYCL_DEV_DEBUG(...) \ + do { \ + if (UNLIKELY(g_ggml_sycl_dev_debug)) \ + fprintf(stderr, __VA_ARGS__); \ + } while (0) + #endif // GGML_SYCL_BASE_HPP diff --git a/ggml/src/ggml-sycl/common.hpp b/ggml/src/ggml-sycl/common.hpp index 355dd442b982..dc6cdd3df462 100644 --- a/ggml/src/ggml-sycl/common.hpp +++ b/ggml/src/ggml-sycl/common.hpp @@ -401,29 +401,10 @@ struct ggml_backend_sycl_context { dnnl::stream stream_dnnl() { return stream_dnnl(device, 0); } - dnnl::memory get_scratchpad_mem(const dnnl::memory::desc & scratchpad_md, - const dnnl::engine & eng, const queue_ptr q) { - ggml_sycl_pool_alloc<uint8_t> * pool; - auto it = scratchpad_map.find(q); - if (it == scratchpad_map.end()) { - scratchpad_map[q] = std::make_unique<ggml_sycl_pool_alloc<uint8_t>>(this->pool()); - pool = scratchpad_map[q].get(); - } else { - pool = it->second.get(); - } - - size_t scratchpad_size = scratchpad_md.get_size(); - if (scratchpad_size > pool->actual_size) { - pool->realloc(scratchpad_size); - } - void * mem_ptr = pool->get(); - return dnnl::memory(scratchpad_md, eng, mem_ptr); - } #endif // pool std::unique_ptr<ggml_sycl_pool> pools[GGML_SYCL_MAX_DEVICES]; - std::unordered_map<sycl::queue *, std::unique_ptr<ggml_sycl_pool_alloc<uint8_t>>> scratchpad_map; std::unique_ptr<ggml_sycl_fattn_kv_buffers> fattn_bufs[GGML_SYCL_MAX_DEVICES]; diff --git a/ggml/src/ggml-sycl/gemm.hpp b/ggml/src/ggml-sycl/gemm.hpp index c202da110beb..81bc5c2e6b47 100644 --- a/ggml/src/ggml-sycl/gemm.hpp +++ b/ggml/src/ggml-sycl/gemm.hpp @@ -66,8 +66,10 @@ class DnnlGemmWrapper { auto matmul_pd = dnnl::matmul::primitive_desc(eng, a_in_md, b_in_md, c_md, primitive_attr); auto c_mem = dnnl::memory(matmul_pd.dst_desc(), eng, c); - auto scratchpad_md = matmul_pd.scratchpad_desc(); - auto scratchpad_mem = ctx.get_scratchpad_mem(scratchpad_md, eng, q); + const auto scratchpad_md = matmul_pd.scratchpad_desc(); + ggml_sycl_pool_alloc<uint8_t> scratchpad(ctx.pool()); + void * scratchpad_ptr = scratchpad_md.get_size() > 0 ? scratchpad.alloc(scratchpad_md.get_size()) : nullptr; + auto scratchpad_mem = dnnl::memory(scratchpad_md, eng, scratchpad_ptr); auto matmul_prim = dnnl::matmul(matmul_pd); diff --git a/ggml/src/ggml-sycl/ggml-sycl.cpp b/ggml/src/ggml-sycl/ggml-sycl.cpp index 4091f73a4674..686a4c76e3f3 100644 --- a/ggml/src/ggml-sycl/ggml-sycl.cpp +++ b/ggml/src/ggml-sycl/ggml-sycl.cpp @@ -91,6 +91,7 @@ static bool g_sycl_loaded = false; int g_ggml_sycl_debug = 0; +int g_ggml_sycl_dev_debug = 0; int g_ggml_sycl_enable_optimize = 1; int g_ggml_sycl_enable_graph = 0; int g_ggml_sycl_enable_dnn = 1; @@ -113,8 +114,8 @@ int g_ggml_sycl_enable_host_pinned_mem = 1; int g_ggml_sycl_host_pinned_mem_2g = 0; int g_ggml_sycl_get_mem_api = MEMORY_API_TYPE_LEVEL_ZERO; - static ggml_sycl_device_info ggml_sycl_init() { + GGML_SYCL_DEBUG("[SYCL] call ggml_sycl_init\n"); ggml_sycl_device_info info = {}; // Do not hard crash when there exists no SYCL devices. @@ -205,12 +206,9 @@ static ggml_sycl_device_info ggml_sycl_init() { } #ifdef GGML_SYCL_SUPPORT_LEVEL_ZERO_API - // Large buffers can be allocated before ggml_check_sycl() initializes other - // g_ggml_sycl_enable_* globals, so initialize this one as early as we can. + //update g_ggml_sycl_use_level_zero_api according to the device support g_ggml_sycl_use_level_zero_api = - info.ext_oneapi_level_zero && ggml_sycl_get_env("GGML_SYCL_USE_LEVEL_ZERO_API", 1); -#else - g_ggml_sycl_use_level_zero_api = 0; + info.ext_oneapi_level_zero && g_ggml_sycl_use_level_zero_api; #endif return info; @@ -314,23 +312,40 @@ static const char* dev2dev_int2str(int dev2dev) { * It's the first internal function to be called by them in SYCL backend. * This function is used to do initialize work for the SYCL backend and set the global variables. */ -void initialize_sycl_begining() { #ifdef GGML_SYCL_SUPPORT_LEVEL_ZERO_API - ze_result_t zes_init = zesInit(0); - if (zes_init != ZE_RESULT_SUCCESS) { - std::cerr << "Warning: zesInit failed [ggml_check_sycl] with code " << static_cast<int>(zes_init) - << ". Sysman free-memory query may be unavailable.\n"; +static ze_result_t init_zes() { + ze_result_t res = zesInit(0); + if (res != ZE_RESULT_SUCCESS) { + GGML_SYCL_DEBUG("Warning: [%s] zesInit failed with code %d. Sysman free-memory query be unavailable.\n", + __func__, (int) res); } + return res; +} + +ze_result_t get_zes_init_res() { + static ze_result_t zes_init_res = init_zes(); + GGML_SYCL_DEBUG("[SYCL] call %s: zesInit result: %d\n", __func__, (int) zes_init_res); + return zes_init_res; +} +#endif + +void initialize_sycl_begining() { +#ifdef GGML_SYCL_SUPPORT_LEVEL_ZERO_API + //must be called in initialization stage, before any other Level Zero API calls + GGML_SYCL_DEBUG("[SYCL] call %s\n", __func__); + get_zes_init_res(); #endif } static void ggml_check_sycl() try { + GGML_SYCL_DEBUG("[SYCL] ggml_check_sycl()\n"); static bool initialized = false; if (!initialized) { initialize_sycl_begining(); g_ggml_sycl_debug = ggml_sycl_get_env("GGML_SYCL_DEBUG", 0); + g_ggml_sycl_dev_debug = ggml_sycl_get_env("GGML_SYCL_DEV_DEBUG", 0); g_ggml_sycl_enable_optimize = ggml_sycl_get_env("GGML_SYCL_ENABLE_OPT", 1); g_ggml_sycl_enable_graph = ggml_sycl_get_env("GGML_SYCL_ENABLE_GRAPH", 0); g_ggml_sycl_enable_dnn = ggml_sycl_get_env("GGML_SYCL_ENABLE_DNN", 1); @@ -344,9 +359,13 @@ static void ggml_check_sycl() try { g_ggml_sycl_enable_esimd = ggml_sycl_get_env("GGML_SYCL_ENABLE_ESIMD", 1); g_ggml_sycl_prioritize_dmmv = ggml_sycl_get_env("GGML_SYCL_PRIORITIZE_DMMV", 0); +#ifdef GGML_SYCL_SUPPORT_LEVEL_ZERO_API + g_ggml_sycl_use_level_zero_api = ggml_sycl_get_env("GGML_SYCL_USE_LEVEL_ZERO_API", 1); +#else + g_ggml_sycl_use_level_zero_api = 0; +#endif g_ggml_sycl_dev2dev_memcpy = ggml_sycl_get_env("GGML_SYCL_DEV2DEV_MEMCPY", DEV2DEV_MEMCPY_SYCL); g_ggml_sycl_get_mem_api = ggml_sycl_get_env("GGML_SYCL_GET_MEM_API", MEMORY_API_TYPE_LEVEL_ZERO); - if (g_ggml_sycl_use_level_zero_api == 0) { g_ggml_sycl_dev2dev_memcpy = DEV2DEV_MEMCPY_SYCL; g_ggml_sycl_get_mem_api = MEMORY_API_TYPE_SYCL; @@ -405,6 +424,7 @@ static void ggml_check_sycl() try { GGML_LOG_INFO("Running with Environment Variables:\n"); GGML_LOG_INFO(" GGML_SYCL_DEBUG: %d\n", g_ggml_sycl_debug); + GGML_LOG_INFO(" GGML_SYCL_DEV_DEBUG: %d\n", g_ggml_sycl_dev_debug); #ifdef GGML_SYCL_SUPPORT_LEVEL_ZERO_API GGML_LOG_INFO(" GGML_SYCL_DEV2DEV_MEMCPY: %d (%s)\n", g_ggml_sycl_dev2dev_memcpy, dev2dev_int2str(g_ggml_sycl_dev2dev_memcpy)); @@ -945,6 +965,7 @@ inline void * aligned_malloc_host(size_t alignment, size_t size) { static ggml_backend_buffer_t ggml_backend_sycl_buffer_type_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) try { + GGML_SYCL_DEBUG("[SYCL] call %s: size=%zu\n", __func__, size); ggml_check_sycl(); ggml_backend_sycl_buffer_type_context * buft_ctx = (ggml_backend_sycl_buffer_type_context *)buft->context; @@ -1464,10 +1485,11 @@ static ggml_backend_buffer_type_i ggml_backend_sycl_split_buffer_type_interface }; ggml_backend_buffer_type_t ggml_backend_sycl_split_buffer_type(const float * tensor_split) { + GGML_SYCL_DEBUG("[SYCL] call ggml_backend_sycl_split_buffer_type\n"); + static std::mutex mutex; std::lock_guard<std::mutex> lock(mutex); - GGML_SYCL_DEBUG("[SYCL] call ggml_backend_sycl_split_buffer_type\n"); ggml_check_sycl(); // FIXME: this is not thread safe static std::map<std::array<float, GGML_SYCL_MAX_DEVICES>, struct ggml_backend_buffer_type> buft_map; @@ -1520,6 +1542,7 @@ static const char * ggml_backend_sycl_host_buffer_type_name(ggml_backend_buffer_ //host pinned memory static void * ggml_backend_sycl_host_malloc(size_t size) { + GGML_SYCL_DEBUG("[SYCL] call ggml_backend_sycl_host_malloc\n"); void * ptr = nullptr; try { ggml_check_sycl(); @@ -3098,10 +3121,14 @@ static void ggml_sycl_op_top_k(ggml_backend_sycl_context & ctx, ggml_tensor * ds const int64_t ncols = src0->ne[0]; const int64_t nrows = ggml_nrows(src0); - GGML_ASSERT(k > 0 && k <= 32); + GGML_ASSERT(k > 0); GGML_ASSERT(k <= ncols); - top_k_f32_sycl(ctx, src0_dd, dst_dd, ncols, nrows, k, main_stream); + if (k <= SYCL_TOP_K_SCAN_MERGE_MAX_K) { + top_k_f32_sycl(ctx, src0_dd, dst_dd, ncols, nrows, k, main_stream); + } else { + ggml_sycl_top_k_radix(ctx, src0_dd, dst_dd, ncols, nrows, k, main_stream); + } } inline void ggml_sycl_op_argmax(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { @@ -5341,8 +5368,8 @@ catch (sycl::exception const &exc) { } static bool ggml_sycl_compute_forward(ggml_backend_sycl_context & ctx, struct ggml_tensor * dst) try { + GGML_SYCL_DEBUG("[SYCL] ggml_sycl_compute_forward: dst=%s, op=%s\n", dst->name, ggml_op_name(dst->op)); if (!g_sycl_loaded) return false; - initialize_sycl_begining(); if (dst->src[0] != nullptr && ggml_backend_buffer_is_sycl_split(dst->src[0]->buffer)) { ggml_sycl_set_peer_access(dst->src[1]->ne[1], ctx.device); @@ -5725,11 +5752,27 @@ catch (sycl::exception const &exc) { std::exit(1); } +bool sycl_get_mem_info(int device, size_t * free, size_t * total) { + GGML_SYCL_DEBUG("[SYCL] [%s] g_ggml_sycl_get_mem_api=%d\n", + __func__, g_ggml_sycl_get_mem_api); + + MemoryAPIType mem_api_type = MemoryAPIType::MEMORY_API_TYPE_SYCL; + +#ifdef GGML_SYCL_SUPPORT_LEVEL_ZERO_API + mem_api_type = get_zes_init_res() == ZE_RESULT_SUCCESS ? + (MemoryAPIType) g_ggml_sycl_get_mem_api : MemoryAPIType::MEMORY_API_TYPE_SYCL; +#else + mem_api_type = MemoryAPIType::MEMORY_API_TYPE_SYCL; +#endif + bool res = get_memory_size(dpct::dev_mgr::instance().get_device(device), + *free, *total, mem_api_type); + GGML_SYCL_DEBUG("[SYCL] [%s] total = %zu free = %zu\n", __func__, *total, *free); + return res; +} + void ggml_backend_sycl_get_device_memory(int device, size_t * free, size_t * total) try { GGML_SYCL_DEBUG("[SYCL] call ggml_backend_sycl_get_device_memory\n"); - bool res = get_memory_size(dpct::dev_mgr::instance().get_device(device), *free, *total, - (MemoryAPIType) g_ggml_sycl_get_mem_api); - if (!res) { + if (!sycl_get_mem_info(device, free, total)) { GGML_ABORT("[%s] failed to get device memory size", __func__); } ggml_sycl_memtrace_report_device("device memory query", device, *free, *total); @@ -6177,12 +6220,12 @@ static const char * ggml_backend_sycl_device_get_description(ggml_backend_dev_t } static void ggml_backend_sycl_device_get_memory(ggml_backend_dev_t dev, size_t * free, size_t * total) { + GGML_SYCL_DEBUG("[SYCL] call %s\n", __func__); ggml_backend_sycl_device_context * ctx = (ggml_backend_sycl_device_context *) dev->context; - bool res = get_memory_size(dpct::dev_mgr::instance().get_device(ctx->device), *free, *total, - (MemoryAPIType) g_ggml_sycl_get_mem_api); - if (!res) { + if (!sycl_get_mem_info(ctx->device, free, total)) { GGML_ABORT("[%s] failed to get device memory size", __func__); } + GGML_SYCL_DEBUG("[SYCL] call %s total %zu free %zu\n", __func__, *total, *free); ggml_sycl_memtrace_report_device("device memory query (dev)", ctx->device, *free, *total); } @@ -6326,7 +6369,7 @@ static bool do_ggml_backend_sycl_device_supports_op(ggml_backend_dev_t dev, cons return false; } - if (src0_type == GGML_TYPE_TQ2_0) { + if (src0_type == GGML_TYPE_TQ2_0 || src0_type == GGML_TYPE_TQ1_0) { return false; } @@ -6380,7 +6423,7 @@ static bool do_ggml_backend_sycl_device_supports_op(ggml_backend_dev_t dev, cons case GGML_OP_SET_ROWS: { - if (op->type == GGML_TYPE_TQ2_0) { + if (op->type == GGML_TYPE_TQ2_0 || op->type == GGML_TYPE_TQ1_0) { return false; } auto res = (op->src[0]->type == GGML_TYPE_F32 || op->src[0]->type == GGML_TYPE_F16 || @@ -6502,12 +6545,14 @@ static bool do_ggml_backend_sycl_device_supports_op(ggml_backend_dev_t dev, cons src1_type == GGML_TYPE_IQ3_S || src1_type == GGML_TYPE_IQ1_S || src1_type == GGML_TYPE_IQ1_M || - src1_type == GGML_TYPE_TQ2_0) { + src1_type == GGML_TYPE_TQ2_0 || + src1_type == GGML_TYPE_TQ1_0) { return false; } } - if (src0_type == GGML_TYPE_TQ2_0 || src1_type == GGML_TYPE_TQ2_0) { + if (src0_type == GGML_TYPE_TQ2_0 || src1_type == GGML_TYPE_TQ2_0 || + src0_type == GGML_TYPE_TQ1_0 || src1_type == GGML_TYPE_TQ1_0) { return false; } @@ -6603,7 +6648,7 @@ static bool do_ggml_backend_sycl_device_supports_op(ggml_backend_dev_t dev, cons op->type == GGML_TYPE_I32 && src0->type == GGML_TYPE_F32 && ggml_is_contiguous(src0) && - k > 0 && k <= 32; + k > 0 && k <= src0->ne[0]; } case GGML_OP_POOL_2D: case GGML_OP_POOL_1D: @@ -7059,6 +7104,7 @@ static const ggml_backend_reg_i ggml_backend_sycl_reg_interface = { // backend registry ggml_backend_reg_t ggml_backend_sycl_reg() { + GGML_SYCL_DEBUG("[SYCL] call ggml_backend_sycl_reg\n"); static ggml_backend_reg reg; static bool initialized = false; @@ -7066,7 +7112,7 @@ ggml_backend_reg_t ggml_backend_sycl_reg() { static std::mutex mutex; std::lock_guard<std::mutex> lock(mutex); if (!initialized) { - initialize_sycl_begining(); + ggml_check_sycl(); ggml_backend_sycl_reg_context * ctx = new ggml_backend_sycl_reg_context; const int min_batch_size = getenv("GGML_OP_OFFLOAD_MIN_BATCH") ? atoi(getenv("GGML_OP_OFFLOAD_MIN_BATCH")) : 32; diff --git a/ggml/src/ggml-sycl/mem.cpp b/ggml/src/ggml-sycl/mem.cpp index 5ec466420e02..ad5bfe0ff1eb 100644 --- a/ggml/src/ggml-sycl/mem.cpp +++ b/ggml/src/ggml-sycl/mem.cpp @@ -6,13 +6,13 @@ #include <level_zero/zes_api.h> #endif -#include "base.hpp" -#include "mem.hpp" - #include <cstdint> #include <iostream> #include <vector> +#include "base.hpp" +#include "mem.hpp" + const char * mem_api_int2str(int mem_api) { if (mem_api == MEMORY_API_TYPE_SYCL) { return "SYCL API"; @@ -24,7 +24,12 @@ const char * mem_api_int2str(int mem_api) { } #ifdef GGML_SYCL_SUPPORT_LEVEL_ZERO_API +/* +* Depend on to call zesInit(0) before any other Level Zero API calls, otherwise the Level Zero API calls may fail. +*/ bool query_free_memory_by_ze(sycl::device dev, size_t & free_bytes, size_t & total_bytes) { + GGML_SYCL_DEBUG("[SYCL] call %s: Querying free memory using Level Zero API.\n", __func__); + free_bytes = 0; total_bytes = 0; @@ -37,41 +42,28 @@ bool query_free_memory_by_ze(sycl::device dev, size_t & free_bytes, size_t & tot #endif try { - ze_result_t zes_init = zesInit(0); - if (zes_init != ZE_RESULT_SUCCESS) { - std::cerr << "Warning: zesInit failed with code " << static_cast<int>(zes_init) - << ". Sysman free-memory query may be unavailable.\n"; - } if (dev.get_platform().get_backend() != kL0Backend) { - GGML_SYCL_DEBUG("Device backend is not Level Zero; falling back to SYCL memory query.\n"); - total_bytes = dev.get_info<sycl::info::device::global_mem_size>(); - free_bytes = total_bytes; + GGML_SYCL_DEBUG("Device backend is not Level Zero.\n"); return false; } ze_device_handle_t ze_dev = sycl::get_native<kL0Backend>(dev); if (ze_dev == nullptr) { - GGML_SYCL_DEBUG("Level Zero device handle is null; falling back to SYCL memory query.\n"); - total_bytes = dev.get_info<sycl::info::device::global_mem_size>(); - free_bytes = total_bytes; + GGML_SYCL_DEBUG("Level Zero device handle is null.\n"); return false; } ze_result_t r = zesDeviceEnumMemoryModules(ze_dev, &module_count, nullptr); if (r != ZE_RESULT_SUCCESS || module_count == 0) { - GGML_SYCL_DEBUG("Failed to enumerate Level Zero memory modules. Falling back to SYCL memory query.\n"); - total_bytes = dev.get_info<sycl::info::device::global_mem_size>(); - free_bytes = total_bytes; + GGML_SYCL_DEBUG("Failed to enumerate Level Zero memory modules.\n"); return false; } std::vector<zes_mem_handle_t> modules(module_count); r = zesDeviceEnumMemoryModules(ze_dev, &module_count, modules.data()); if (r != ZE_RESULT_SUCCESS || module_count == 0) { - GGML_SYCL_DEBUG("Failed to enumerate Level Zero memory modules. Falling back to SYCL memory query.\n"); - total_bytes = dev.get_info<sycl::info::device::global_mem_size>(); - free_bytes = total_bytes; + GGML_SYCL_DEBUG("Failed to enumerate Level Zero memory modules.\n"); return false; } @@ -90,73 +82,70 @@ bool query_free_memory_by_ze(sycl::device dev, size_t & free_bytes, size_t & tot } if (total_bytes == 0) { - GGML_SYCL_DEBUG("Level Zero memory query returned zero total bytes. Falling back to SYCL memory query.\n"); - total_bytes = dev.get_info<sycl::info::device::global_mem_size>(); - free_bytes = total_bytes; + GGML_SYCL_DEBUG("Level Zero memory query returned zero total bytes.\n"); return false; } - return true; + return total_bytes >= free_bytes; + } catch (const sycl::exception & e) { GGML_SYCL_DEBUG("Level Zero memory query failed: %s\n", e.what()); - total_bytes = dev.get_info<sycl::info::device::global_mem_size>(); - free_bytes = total_bytes; return false; } } #endif bool get_memory_size_by_sycl_api(sycl::device dev, size_t & free_bytes, size_t & total_bytes) { - GGML_SYCL_DEBUG("[%s]Querying free memory using SYCL API.\n", __func__); + GGML_SYCL_DEBUG("[SYCL] call %s: Querying free memory using SYCL API.\n", __func__); total_bytes = dev.get_info<sycl::info::device::global_mem_size>(); #if (defined(__SYCL_COMPILER_VERSION) && __SYCL_COMPILER_VERSION >= 20221105) if (dev.has(sycl::aspect::ext_intel_free_memory)) { try { - GGML_SYCL_DEBUG("Querying free memory using SYCL aspect::ext_intel_free_memory."); + GGML_SYCL_DEBUG("Querying free memory using SYCL aspect::ext_intel_free_memory.\n"); free_bytes = dev.get_info<sycl::ext::intel::info::device::free_memory>(); return true; } catch (const sycl::exception &) { GGML_SYCL_DEBUG( - "Failed to query free memory using SYCL aspect::ext_intel_free_memory. Using total memory as free " - "memory."); - free_bytes = total_bytes; + "Failed to query free memory using SYCL aspect::ext_intel_free_memory.\n"); return false; } } else { GGML_SYCL_DEBUG( - "Device does not support SYCL aspect::ext_intel_free_memory. Using total memory as free memory."); - free_bytes = total_bytes; + "Device does not support SYCL aspect::ext_intel_free_memory.\n"); } #else - GGML_SYCL_DEBUG("SYCL Compiler version is older than 20221105. Using total memory as free memory."); - free_bytes = total_bytes; + GGML_SYCL_DEBUG("SYCL Compiler version is older than 20221105.\n"); #endif - return true; + return false; } bool get_memory_size(sycl::device dev, size_t & free_bytes, size_t & total_bytes, MemoryAPIType api_type) { - const auto name = dev.get_info<sycl::info::device::name>(); - const auto vendor = dev.get_info<sycl::info::device::vendor>(); - const auto global_mem = dev.get_info<sycl::info::device::global_mem_size>(); - GGML_SYCL_DEBUG("[%s]GPU Name: %s\n", __func__, name.c_str()); - GGML_SYCL_DEBUG("[%s]GPU Vendor: %s\n", __func__, vendor.c_str()); - GGML_SYCL_DEBUG("[%s]GPU Global Memory: %zu bytes\n", __func__, static_cast<size_t>(global_mem)); + GGML_SYCL_DEBUG("[%s]GPU Name: %s\n", __func__, + dev.get_info<sycl::info::device::name>().c_str()); + GGML_SYCL_DEBUG("[%s]GPU Vendor: %s\n", __func__, + dev.get_info<sycl::info::device::vendor>().c_str()); if (api_type == MEMORY_API_TYPE_LEVEL_ZERO) { #ifdef GGML_SYCL_SUPPORT_LEVEL_ZERO_API - GGML_SYCL_DEBUG("[%s]Querying free memory using Level Zero API.\n", __func__); - if (!query_free_memory_by_ze(dev, free_bytes, total_bytes)) { - //fallback to SYCL API if Level Zero API fails - GGML_SYCL_DEBUG("[%s]Falling back to SYCL API for memory query.\n", __func__); - return get_memory_size_by_sycl_api(dev, free_bytes, total_bytes); + GGML_SYCL_DEBUG("[%s] Querying free memory using Level Zero API.\n", __func__); + if (query_free_memory_by_ze(dev, free_bytes, total_bytes)) { + return true; } - return true; -#else - GGML_SYCL_DEBUG("[%s]Level Zero API support is not enabled. Please enable it to use this feature.\n", __func__); - return false; + //fallback to SYCL API if Level Zero API fails + GGML_SYCL_DEBUG("[%s] Falling back to SYCL API for memory query.\n", __func__); #endif - } else { //MEMORY_API_TYPE_SYCL - return get_memory_size_by_sycl_api(dev, free_bytes, total_bytes); } + + //MEMORY_API_TYPE_SYCL + if(get_memory_size_by_sycl_api(dev, free_bytes, total_bytes)){ + return true; + } + + //Todo, fallback to other methods to get free memory size, such as using OS-specific APIs (e.g., /proc/meminfo on Linux, GlobalMemoryStatusEx on Windows, etc.) + GGML_SYCL_DEBUG( + "[%s] Can't get free mem size by Level Zero and SYCL API. Using total memory as free memory.\n", __func__); + free_bytes = total_bytes; + + return true; } diff --git a/ggml/src/ggml-sycl/topk-radix.cpp b/ggml/src/ggml-sycl/topk-radix.cpp new file mode 100644 index 000000000000..8cd0bd2f5fcd --- /dev/null +++ b/ggml/src/ggml-sycl/topk-radix.cpp @@ -0,0 +1,531 @@ +#include "topk-radix.hpp" + +#include "common.hpp" + +#include <algorithm> + +// Large-k top-k by radix select on an order-preserving unsigned key. +// +// The k-th largest key of a row is found by four most-significant-first passes over its +// 8-bit digits: histogram the digit over the candidate set, walk the buckets from the +// top, and recurse into the bucket where the running count reaches what is still +// needed. Everything strictly above that bucket is in the top-k. A final pass emits +// every column whose key beats the pivot, then exactly as many pivot-equal columns as +// are still missing, so duplicate keys yield exactly k distinct indices. +// +// SLM holds only the histogram, so unlike the scan-merge kernels the cost does not grow +// with k. One work-group owns a row and runs every pass, so a top-k is one launch and +// needs no pool scratch. The row is re-read once per pass rather than compacted, which +// keeps the candidate set implicit: (key & mask) == prefix. +// +// The output is the set of winning indices in no particular order, which is what the +// reference op provides (it swaps its first two outputs to say so) and what +// test-backend-ops compares. + +static constexpr int SYCL_TOP_K_RADIX_BITS = 8; +static constexpr int SYCL_TOP_K_RADIX_BUCKETS = 1 << SYCL_TOP_K_RADIX_BITS; +// Private histogram copies, interleaved per bucket so neighbouring lanes hit +// neighbouring banks. Lanes of one instruction spread over the copies, which is what +// bounds the atomic serialisation on tie-heavy rows. +static constexpr int SYCL_TOP_K_RADIX_HIST_COPIES = 8; +static constexpr int SYCL_TOP_K_RADIX_HIST_SIZE = SYCL_TOP_K_RADIX_BUCKETS * SYCL_TOP_K_RADIX_HIST_COPIES; +// Past the histogram: pivot digit, pivot bucket count, remaining need, then the two +// emit counters. +static constexpr int SYCL_TOP_K_RADIX_SLM_WORDS = SYCL_TOP_K_RADIX_HIST_SIZE + 5; + +// Larger float <=> larger key. The reference comparator is a plain float '>', under which +// -0.0 and +0.0 tie, so -0.0 is folded onto +0.0 first. NaN has no defined order in the +// reference (its comparator is not a strict weak order on NaN); here a positive NaN keys +// above +inf and a negative NaN below -inf, which at least makes the result deterministic. +static inline uint32_t top_k_radix_key(float f) { + uint32_t u = sycl::bit_cast<uint32_t>(f); + if (u == 0x80000000u) { + u = 0u; + } + return (u & 0x80000000u) ? ~u : (u | 0x80000000u); +} + +static void top_k_radix_select_f32( + const float * src, + int32_t * dst_idx, + const int ncols, + const int k, + uint32_t * slm, + const sycl::nd_item<1> & item_ct1 +) { + using local_atomic = sycl::atomic_ref<uint32_t, sycl::memory_order::relaxed, + sycl::memory_scope::work_group, + sycl::access::address_space::local_space>; + + const int tid = item_ct1.get_local_id(0); + const int block_size = item_ct1.get_local_range(0); + + uint32_t * hist = slm; + uint32_t * s_digit = slm + SYCL_TOP_K_RADIX_HIST_SIZE; + uint32_t * s_bucket = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 1; + uint32_t * s_need = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 2; + uint32_t * s_cnt_gt = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 3; + uint32_t * s_cnt_eq = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 4; + + if (tid == 0) { + *s_cnt_gt = 0; + *s_cnt_eq = 0; + } + + const int copy = tid & (SYCL_TOP_K_RADIX_HIST_COPIES - 1); + + uint32_t prefix = 0; // digits fixed so far, in place + uint32_t mask = 0; // which bits of prefix are fixed + uint32_t need = (uint32_t) k; + + for (int shift = 32 - SYCL_TOP_K_RADIX_BITS; shift >= 0; shift -= SYCL_TOP_K_RADIX_BITS) { + for (int i = tid; i < SYCL_TOP_K_RADIX_HIST_SIZE; i += block_size) { + hist[i] = 0; + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + for (int col = tid; col < ncols; col += block_size) { + const uint32_t key = top_k_radix_key(src[col]); + if ((key & mask) == prefix) { + const uint32_t bucket = (key >> shift) & (SYCL_TOP_K_RADIX_BUCKETS - 1); + local_atomic(hist[bucket * SYCL_TOP_K_RADIX_HIST_COPIES + copy]).fetch_add(1u); + } + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + // Lane t takes bucket 255 - t, so an inclusive scan over lanes counts from the top + // bucket downward. The pivot is the unique bucket whose cumulative count first + // reaches need; the previous cumulative count is what the higher buckets contribute. + uint32_t cnt = 0; + if (tid < SYCL_TOP_K_RADIX_BUCKETS) { + const uint32_t * h = hist + (SYCL_TOP_K_RADIX_BUCKETS - 1 - tid) * SYCL_TOP_K_RADIX_HIST_COPIES; + for (int c = 0; c < SYCL_TOP_K_RADIX_HIST_COPIES; c++) { + cnt += h[c]; + } + } + const uint32_t incl = sycl::inclusive_scan_over_group(item_ct1.get_group(), cnt, sycl::plus<uint32_t>()); + + if (tid < SYCL_TOP_K_RADIX_BUCKETS && incl >= need && incl - cnt < need) { + *s_digit = (uint32_t) (SYCL_TOP_K_RADIX_BUCKETS - 1 - tid); + *s_bucket = cnt; + *s_need = need - (incl - cnt); + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + const uint32_t digit = *s_digit; + const uint32_t bucket_cnt = *s_bucket; + need = *s_need; + prefix |= digit << shift; + mask |= (uint32_t) (SYCL_TOP_K_RADIX_BUCKETS - 1) << shift; + + // Every candidate in the pivot bucket is wanted: the remaining digits cannot + // change the answer, and the masked emit below is exact as it stands. + if (bucket_cnt == need) { + break; + } + // The next pass rewrites hist and s_*; the reads above must land first. + item_ct1.barrier(sycl::access::fence_space::local_space); + } + + item_ct1.barrier(sycl::access::fence_space::local_space); + + // Exactly k - need columns have (key & mask) > prefix; the first need of the pivot-equal + // columns fill the tail. Both counters live in SLM since the whole row is this group. + const uint32_t base_eq = (uint32_t) k - need; + + for (int col = tid; col < ncols; col += block_size) { + const uint32_t kp = top_k_radix_key(src[col]) & mask; + if (kp > prefix) { + const uint32_t pos = local_atomic(*s_cnt_gt).fetch_add(1u); + dst_idx[pos] = col; + } else if (kp == prefix) { + const uint32_t pos = local_atomic(*s_cnt_eq).fetch_add(1u); + if (pos < need) { + dst_idx[base_eq + pos] = col; + } + } + } +} + +static void top_k_radix_f32_sycl( + ggml_backend_sycl_context & ctx, + const float * src, + int32_t * dst_indices, + const int64_t ncols, + const int64_t nrows, + const int k, + dpct::queue_ptr main_stream +) { + GGML_ASSERT(ncols <= INT32_MAX); + + // One group per row; every pass is a strided sweep of the row, so lanes in flight is the + // only lever, and the device's own limit is the answer -- there is nothing here that + // wants a smaller group. Must still cover the 256 buckets for the scan step. + const int block_size = ggml_sycl_info().max_work_group_sizes[ctx.device]; + GGML_ASSERT(block_size >= SYCL_TOP_K_RADIX_BUCKETS); + + const sycl::range<1> block_dims(block_size); + const sycl::range<1> grid_dims(nrows); + + main_stream->submit([&](sycl::handler &cgh) { + sycl::local_accessor<uint32_t, 1> slm(sycl::range<1>(SYCL_TOP_K_RADIX_SLM_WORDS), cgh); + + cgh.parallel_for( + sycl::nd_range<1>(grid_dims * block_dims, block_dims), + [=](sycl::nd_item<1> item_ct1) { + const int row = item_ct1.get_group(0); + + top_k_radix_select_f32( + src + (int64_t) row * ncols, dst_indices + (int64_t) row * k, + (int) ncols, k, + slm.get_multi_ptr<sycl::access::decorated::no>().get(), + item_ct1); + }); + }); +} + +// One work-group owns a whole row above, which leaves the device idle whenever a graph +// has fewer rows than it has cores -- the common case at batch size 1, where the +// sparse-attention indexer and the backend sampler both top-k a single row. The kernels +// below spread one row over several groups instead. +// +// A digit pass now needs the whole row's histogram before any group can pick the pivot, +// so the per-pass state moves to global memory and the passes become separate launches: +// a work-group barrier no longer spans the row. Each group still accumulates into SLM +// and contributes 256 global atomics at the end, so global traffic is per-group, not +// per-element. The last group to finish a pass (the one whose fetch_add returns G - 1) +// does the scan for the row and clears the histogram for the next pass, which keeps the +// launch count at one per digit rather than two. +// +// Running all four digits unconditionally costs nothing in correctness: once a bucket +// holds exactly the elements still needed, later digits only extend the prefix, and the +// count of columns above that longer prefix grows by exactly as much as `need` shrinks. +// The emit below therefore stays exact whatever pass the answer settled on. + +static constexpr int SYCL_TOP_K_RADIX_ROW_DONE = SYCL_TOP_K_RADIX_BUCKETS + 0; +static constexpr int SYCL_TOP_K_RADIX_ROW_PREFIX = SYCL_TOP_K_RADIX_BUCKETS + 1; +static constexpr int SYCL_TOP_K_RADIX_ROW_MASK = SYCL_TOP_K_RADIX_BUCKETS + 2; +static constexpr int SYCL_TOP_K_RADIX_ROW_NEED = SYCL_TOP_K_RADIX_BUCKETS + 3; +static constexpr int SYCL_TOP_K_RADIX_ROW_CNT_GT = SYCL_TOP_K_RADIX_BUCKETS + 4; +static constexpr int SYCL_TOP_K_RADIX_ROW_CNT_EQ = SYCL_TOP_K_RADIX_BUCKETS + 5; +static constexpr int SYCL_TOP_K_RADIX_ROW_WORDS = SYCL_TOP_K_RADIX_BUCKETS + 6; + +// How wide the split goes is a property of the device, not of the model: enough groups to +// cover the cores, and no more. Past that the extra groups add histogram traffic without +// adding bandwidth (measured on this device: 20 and 40 groups tie, 60 and 160 lose). +// +// nsm is max_compute_units / 16, i.e. it counts an Xe core as 16 EUs. That is a core's +// width on Xe-HPG, but an Xe2 core is 8 XVEs wide, so on Battlemage the field reads half +// the cores actually present (10 for a 20-core B60). The measured curve is flat from one +// group per core to two and only falls off at three, so a factor of two covers the device +// on Xe2 and lands in the flat region on Xe-HPG. It is the one number here that a correct +// core count would remove; it was tuned on Xe2 and has not been measured on Xe-HPG. +static constexpr int SYCL_TOP_K_RADIX_GROUPS_PER_NSM = 2; +// Splitting trades one kernel for five. Below the width at which the single-group kernel +// runs longer than those four extra launches, it wins on its own; measured break-even on +// this device sits just under 64K columns. +static constexpr int SYCL_TOP_K_RADIX_MIN_SPLIT_COLS = 65536; +// A partition thinner than this cannot keep a group's sweep busy. +static constexpr int SYCL_TOP_K_RADIX_MIN_PART_COLS = 4096; + +static int top_k_radix_split_groups(const int device, const int64_t ncols, const int64_t nrows) { + const int64_t target = (int64_t) SYCL_TOP_K_RADIX_GROUPS_PER_NSM * ggml_sycl_info().devices[device].nsm; + + // One group per row already, so a graph with rows enough to cover the device gains + // nothing from splitting and would only pay the extra launches. + if (ncols < SYCL_TOP_K_RADIX_MIN_SPLIT_COLS || nrows >= target) { + return 1; + } + + const int64_t by_rows = target / nrows; // floor: never overshoot a row that is nearly covered + const int64_t by_cols = ncols / SYCL_TOP_K_RADIX_MIN_PART_COLS; + + return (int) std::max<int64_t>(1, std::min(by_rows, by_cols)); +} + +using top_k_radix_gatomic = sycl::atomic_ref<uint32_t, sycl::memory_order::relaxed, + sycl::memory_scope::device, + sycl::access::address_space::global_space>; + +static void top_k_radix_split_pass_f32( + const float * src, + uint32_t * state, + const int ncols, + const int k, + const int shift, + const bool first, + const int part, + const int nparts, + uint32_t * slm, + const sycl::nd_item<1> & item_ct1 +) { + using local_atomic = sycl::atomic_ref<uint32_t, sycl::memory_order::relaxed, + sycl::memory_scope::work_group, + sycl::access::address_space::local_space>; + + const int tid = item_ct1.get_local_id(0); + const int block_size = item_ct1.get_local_range(0); + + uint32_t * hist = slm; + uint32_t * s_last = slm + SYCL_TOP_K_RADIX_HIST_SIZE; + uint32_t * s_row = slm + SYCL_TOP_K_RADIX_HIST_SIZE + 1; // prefix, mask, need + + // The previous launch is the barrier that publishes these, so a plain load is enough. + // One lane reads them and the group takes them from SLM: a device-scope atomic load + // is uncached here, and having every work-item issue three of them off the same + // address costs more than the whole sweep below. + if (tid == 0) { + s_row[0] = first ? 0u : state[SYCL_TOP_K_RADIX_ROW_PREFIX]; + s_row[1] = first ? 0u : state[SYCL_TOP_K_RADIX_ROW_MASK]; + s_row[2] = first ? (uint32_t) k : state[SYCL_TOP_K_RADIX_ROW_NEED]; + } + + for (int i = tid; i < SYCL_TOP_K_RADIX_HIST_SIZE; i += block_size) { + hist[i] = 0; + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + const uint32_t prefix = s_row[0]; + const uint32_t mask = s_row[1]; + const uint32_t need = s_row[2]; + + const int copy = tid & (SYCL_TOP_K_RADIX_HIST_COPIES - 1); + const int chunk = (ncols + nparts - 1) / nparts; + const int col0 = part * chunk; + const int col1 = std::min(ncols, col0 + chunk); + + for (int col = col0 + tid; col < col1; col += block_size) { + const uint32_t key = top_k_radix_key(src[col]); + if ((key & mask) == prefix) { + const uint32_t bucket = (key >> shift) & (SYCL_TOP_K_RADIX_BUCKETS - 1); + local_atomic(hist[bucket * SYCL_TOP_K_RADIX_HIST_COPIES + copy]).fetch_add(1u); + } + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + // One global atomic per bucket per group, not per element. + for (int b = tid; b < SYCL_TOP_K_RADIX_BUCKETS; b += block_size) { + uint32_t sum = 0; + for (int c = 0; c < SYCL_TOP_K_RADIX_HIST_COPIES; c++) { + sum += hist[b * SYCL_TOP_K_RADIX_HIST_COPIES + c]; + } + if (sum) { + top_k_radix_gatomic(state[b]).fetch_add(sum); + } + } + + // Publish this group's bins, then claim the scan if this group is the row's last. + // The group-wide barrier flushes the atomics above; only the claiming lane needs the + // release, so the device-scope fence is paid once per group rather than per work-item. + item_ct1.barrier(sycl::access::fence_space::global_and_local); + if (tid == 0) { + sycl::atomic_fence(sycl::memory_order::release, sycl::memory_scope::device); + sycl::atomic_ref<uint32_t, sycl::memory_order::acq_rel, sycl::memory_scope::device, + sycl::access::address_space::global_space> done(state[SYCL_TOP_K_RADIX_ROW_DONE]); + *s_last = (done.fetch_add(1u) == (uint32_t) (nparts - 1)) ? 1u : 0u; + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + if (*s_last == 0u) { + return; + } + sycl::atomic_fence(sycl::memory_order::acquire, sycl::memory_scope::device); + + // Lane t takes bucket 255 - t, so an inclusive scan counts down from the top bucket. + uint32_t cnt = 0; + if (tid < SYCL_TOP_K_RADIX_BUCKETS) { + cnt = top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_BUCKETS - 1 - tid]).load(); + } + const uint32_t incl = sycl::inclusive_scan_over_group(item_ct1.get_group(), cnt, sycl::plus<uint32_t>()); + + if (tid < SYCL_TOP_K_RADIX_BUCKETS && incl >= need && incl - cnt < need) { + const uint32_t digit = (uint32_t) (SYCL_TOP_K_RADIX_BUCKETS - 1 - tid); + top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_PREFIX]).store(prefix | (digit << shift)); + top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_MASK]).store( + mask | ((uint32_t) (SYCL_TOP_K_RADIX_BUCKETS - 1) << shift)); + top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_NEED]).store(need - (incl - cnt)); + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + // Clear for the next pass; the next launch is the barrier that orders this. + for (int b = tid; b < SYCL_TOP_K_RADIX_BUCKETS; b += block_size) { + top_k_radix_gatomic(state[b]).store(0u); + } + if (tid == 0) { + top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_DONE]).store(0u); + } +} + +static void top_k_radix_split_emit_f32( + const float * src, + int32_t * dst_idx, + uint32_t * state, + const int ncols, + const int k, + const int part, + const int nparts, + uint32_t * slm, + const sycl::nd_item<1> & item_ct1 +) { + using local_atomic = sycl::atomic_ref<uint32_t, sycl::memory_order::relaxed, + sycl::memory_scope::work_group, + sycl::access::address_space::local_space>; + + const int tid = item_ct1.get_local_id(0); + const int block_size = item_ct1.get_local_range(0); + + uint32_t * s_gt = slm; + uint32_t * s_eq = slm + 1; + uint32_t * s_base_gt = slm + 2; + uint32_t * s_base_eq = slm + 3; + + uint32_t * s_row = slm + 4; // prefix, mask, need + + if (tid == 0) { + *s_gt = 0; + *s_eq = 0; + s_row[0] = state[SYCL_TOP_K_RADIX_ROW_PREFIX]; + s_row[1] = state[SYCL_TOP_K_RADIX_ROW_MASK]; + s_row[2] = state[SYCL_TOP_K_RADIX_ROW_NEED]; + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + const uint32_t prefix = s_row[0]; + const uint32_t mask = s_row[1]; + const uint32_t need = s_row[2]; + + // Exactly k - need columns beat the pivot; the first need pivot-equal ones fill the tail. + const uint32_t base_eq = (uint32_t) k - need; + + const int chunk = (ncols + nparts - 1) / nparts; + const int col0 = part * chunk; + const int col1 = std::min(ncols, col0 + chunk); + + // Counting first and reserving one range per group keeps the row's two counters out of + // the inner loop: a per-element global atomic on a single address serialises the whole + // emit, and at k in the thousands that alone outweighs every read the kernel does. + for (int col = col0 + tid; col < col1; col += block_size) { + const uint32_t kp = top_k_radix_key(src[col]) & mask; + if (kp > prefix) { + local_atomic(*s_gt).fetch_add(1u); + } else if (kp == prefix) { + local_atomic(*s_eq).fetch_add(1u); + } + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + if (tid == 0) { + const uint32_t n_gt = *s_gt; + const uint32_t n_eq = *s_eq; + *s_base_gt = n_gt ? top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_CNT_GT]).fetch_add(n_gt) : 0u; + *s_base_eq = n_eq ? top_k_radix_gatomic(state[SYCL_TOP_K_RADIX_ROW_CNT_EQ]).fetch_add(n_eq) : 0u; + *s_gt = 0; + *s_eq = 0; + } + item_ct1.barrier(sycl::access::fence_space::local_space); + + const uint32_t base_gt_g = *s_base_gt; + const uint32_t base_eq_g = *s_base_eq; + + for (int col = col0 + tid; col < col1; col += block_size) { + const uint32_t kp = top_k_radix_key(src[col]) & mask; + if (kp > prefix) { + dst_idx[base_gt_g + local_atomic(*s_gt).fetch_add(1u)] = col; + } else if (kp == prefix) { + const uint32_t pos = base_eq_g + local_atomic(*s_eq).fetch_add(1u); + if (pos < need) { + dst_idx[base_eq + pos] = col; + } + } + } +} + +static void top_k_radix_split_f32_sycl( + ggml_backend_sycl_context & ctx, + const float * src, + int32_t * dst_indices, + const int64_t ncols, + const int64_t nrows, + const int k, + const int nparts, + dpct::queue_ptr main_stream +) { + GGML_ASSERT(ncols <= INT32_MAX); + GGML_ASSERT(nparts > 1); + + const int block_size = ggml_sycl_info().max_work_group_sizes[ctx.device]; + GGML_ASSERT(block_size >= SYCL_TOP_K_RADIX_BUCKETS); + + const size_t state_words = (size_t) nrows * SYCL_TOP_K_RADIX_ROW_WORDS; + ggml_sycl_pool_alloc<uint32_t> state_alloc(ctx.pool(), state_words); + uint32_t * state = state_alloc.get(); + + // Zero histogram, done counter and both emit counters. prefix/mask/need are seeded by + // the first pass, which ignores the stored values. + // The queue is in-order, so the passes below are already ordered after this fill. + SYCL_CHECK(CHECK_TRY_ERROR(main_stream->memset(state, 0, state_words * sizeof(uint32_t)))); + + const sycl::range<1> block_dims(block_size); + const sycl::range<1> grid_dims(nrows * nparts); + + bool first = true; + for (int shift = 32 - SYCL_TOP_K_RADIX_BITS; shift >= 0; shift -= SYCL_TOP_K_RADIX_BITS) { + const bool is_first = first; + first = false; + main_stream->submit([&](sycl::handler &cgh) { + sycl::local_accessor<uint32_t, 1> slm(sycl::range<1>(SYCL_TOP_K_RADIX_HIST_SIZE + 4), cgh); + + cgh.parallel_for( + sycl::nd_range<1>(grid_dims * block_dims, block_dims), + [=](sycl::nd_item<1> item_ct1) { + const int g = item_ct1.get_group(0); + const int row = g / nparts; + const int part = g % nparts; + + top_k_radix_split_pass_f32( + src + (int64_t) row * ncols, + state + (int64_t) row * SYCL_TOP_K_RADIX_ROW_WORDS, + (int) ncols, k, shift, is_first, part, nparts, + slm.get_multi_ptr<sycl::access::decorated::no>().get(), + item_ct1); + }); + }); + } + + main_stream->submit([&](sycl::handler &cgh) { + sycl::local_accessor<uint32_t, 1> slm(sycl::range<1>(8), cgh); + + cgh.parallel_for( + sycl::nd_range<1>(grid_dims * block_dims, block_dims), + [=](sycl::nd_item<1> item_ct1) { + const int g = item_ct1.get_group(0); + const int row = g / nparts; + const int part = g % nparts; + + top_k_radix_split_emit_f32( + src + (int64_t) row * ncols, + dst_indices + (int64_t) row * k, + state + (int64_t) row * SYCL_TOP_K_RADIX_ROW_WORDS, + (int) ncols, k, part, nparts, + slm.get_multi_ptr<sycl::access::decorated::no>().get(), + item_ct1); + }); + }); +} + +void ggml_sycl_top_k_radix( + ggml_backend_sycl_context & ctx, + const float * src, + int32_t * dst_indices, + const int64_t ncols, + const int64_t nrows, + const int k, + dpct::queue_ptr main_stream +) { + const int nparts = top_k_radix_split_groups(ctx.device, ncols, nrows); + if (nparts > 1) { + top_k_radix_split_f32_sycl(ctx, src, dst_indices, ncols, nrows, k, nparts, main_stream); + } else { + top_k_radix_f32_sycl(ctx, src, dst_indices, ncols, nrows, k, main_stream); + } +} diff --git a/ggml/src/ggml-sycl/topk-radix.hpp b/ggml/src/ggml-sycl/topk-radix.hpp new file mode 100644 index 000000000000..db479607e45d --- /dev/null +++ b/ggml/src/ggml-sycl/topk-radix.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "common.hpp" + +// The legacy implementation uses SLM to implement sorting and top_k selection. +// SLM is limited to 128KB on Xe, which limits how much can be sorted to k<32. +// After a k=8, the radix selection becomes beneficial for most cases, because +// scan-merge has (block + 1) * k pairs of (value, index). Given normal sorting of nlog(n), +// radix-select becomes beneficial quite early. This sets it to 8 - however, the other parameters +// (columns and rows) may also be a driving factor. +// We select the legacy implementation for k below this constant because the overhead of radix select +// exceeds the benefit for very small problems +constexpr int SYCL_TOP_K_SCAN_MERGE_MAX_K = 8; + +// Top-k of every row of src, k indices per row into dst_indices, in no particular order. +// Picks between the one-group-per-row and the split-row kernel from the shape and the device. +void ggml_sycl_top_k_radix( + ggml_backend_sycl_context & ctx, + const float * src, + int32_t * dst_indices, + const int64_t ncols, + const int64_t nrows, + const int k, + dpct::queue_ptr main_stream); diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp index 869c916c317b..bafbec6c5cdc 100644 --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp @@ -333,6 +333,7 @@ static void ggml_vk_print_device_lost_info(const vk_device& device); struct vk_queue_handle { vk::Queue queue; vk_device_ref device; + std::mutex * device_submit_mutex = nullptr; virtual void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) = 0; virtual void lock() {} // no-op by default (internally synchronized case) virtual void unlock() {} @@ -342,6 +343,11 @@ struct vk_queue_handle { struct vk_queue_handle_synchronized : vk_queue_handle { std::mutex mutex; void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) override { + // Workaround for NVIDIA driver bug + std::unique_lock<std::mutex> device_guard; + if (device_submit_mutex) { + device_guard = std::unique_lock<std::mutex>(*device_submit_mutex); + } std::lock_guard<std::mutex> guard(mutex); try { queue.submit(submits, fence); @@ -356,9 +362,14 @@ struct vk_queue_handle_synchronized : vk_queue_handle { void unlock() override { mutex.unlock(); } }; +// Driver guarantees internal synchronization via VK_KHR_internally_synchronized_queues struct vk_queue_handle_unsynchronized : vk_queue_handle { void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) override { - // Driver guarantees internal synchronization via VK_KHR_internally_synchronized_queues + // Workaround for NVIDIA driver bug + std::unique_lock<std::mutex> device_guard; + if (device_submit_mutex) { + device_guard = std::unique_lock<std::mutex>(*device_submit_mutex); + } try { queue.submit(submits, fence); } catch (vk::DeviceLostError &) { @@ -835,6 +846,7 @@ static bool ggml_vk_lightning_indexer_k_type_supported(ggml_type type) { struct vk_device_struct { std::recursive_mutex mutex; + std::mutex queue_submit_mutex; mutable std::shared_mutex pinned_memory_mutex; // Guards compile_pending, all_pipelines, and the dynamic pipeline maps @@ -1159,6 +1171,10 @@ struct vk_device_struct { std::map<std::pair<uint32_t, uint32_t>, vk_pipeline> pipeline_fa_mask_opt; + vk_pipeline pipeline_fa_sparse_compact; + vk_pipeline pipeline_fa_sparse_compact_subgroup; + bool fa_sparse_compact_use_subgroups; + vk_pipeline pipeline_flash_attn_split_k_reduce; vk_pipeline pipeline_count_experts; @@ -2184,6 +2200,16 @@ struct vk_op_flash_attn_mask_opt_push_constants { uint32_t nbd3; }; +struct vk_op_flash_attn_sparse_compact_push_constants { + uint32_t KV; + uint32_t nem1; + uint32_t nem2; + uint32_t nbm1; + uint32_t nbm2; + uint32_t nbm3; + uint32_t n_kv_max; +}; + // Allow pre-recording command buffers struct vk_staging_memcpy { vk_staging_memcpy(void * _dst, const void * _src, size_t _n) : dst(_dst), src(_src), n(_n) {} @@ -3520,6 +3546,10 @@ static std::unique_ptr<vk_queue> ggml_vk_create_queue(vk_device& device, uint32_ h->queue = device->device.getQueue2(queue_info2); h->device = device; + // Avoid concurrent submissions on NVIDIA due to driver bug. + if (device->vendor_id == VK_VENDOR_ID_NVIDIA) { + h->device_submit_mutex = &device->queue_submit_mutex; + } q->handle = h; q->cmd_pool.init(device, q.get()); @@ -4103,14 +4133,15 @@ static vk_fa_tuning_params get_fa_tuning_params(const vk_device& device, uint32_ } static vk_fa_pipeline_state get_fa_pipeline_state(const vk_device& device, const vk_fa_tuning_params& params, uint32_t hsk, uint32_t hsv, bool aligned, bool f32acc, - bool use_mask, bool use_mask_opt, bool use_logit_softcap, ggml_type k_type, ggml_type v_type) { + bool use_mask, bool use_mask_opt, bool use_logit_softcap, bool use_sparse, ggml_type k_type, ggml_type v_type) { const bool old_amd_windows = device->vendor_id == VK_VENDOR_ID_AMD && device->driver_id == vk::DriverId::eAmdProprietary && (device->architecture == AMD_GCN || device->architecture == AMD_RDNA1 || device->architecture == AMD_RDNA2); uint32_t flags = (use_mask_opt ? 1 : 0) | (use_mask ? 2 : 0) | (use_logit_softcap ? 4 : 0) | - (old_amd_windows ? 8 : 0); + (old_amd_windows ? 8 : 0) | + (use_sparse ? 16 : 0); const uint32_t subgroup_size = params.disable_subgroups ? 0 : params.subgroup_size; @@ -4770,7 +4801,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { } name = aligned ? "flash_attn_f32_f16_aligned" : "flash_attn_f32_f16"; } - ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 7, + ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 8, sizeof(vk_flash_attn_push_constants), {Br, 1, 1}, get_fa_spec_constants(fa.first), aligned ? Bc : 1, true, !fa_ds, !fa_ds ? fa_sgs : 0); @@ -4806,7 +4837,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { else { spv_data = flash_attn_f32_f16_f16acc_cm1_data; spv_size = flash_attn_f32_f16_f16acc_cm1_len; } name = aligned ? "flash_attn_f32_f16_aligned_cm1" : "flash_attn_f32_f16_cm1"; } - ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 7, + ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 8, sizeof(vk_flash_attn_push_constants), {Br, 1, 1}, get_fa_spec_constants(fa.first), aligned ? Bc : 1, true, !fa_ds, !fa_ds ? fa_sgs : 0); @@ -4843,7 +4874,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { if (f32acc) { spv_data = flash_attn_f32_f16_cm2_data; spv_size = flash_attn_f32_f16_cm2_len; name = "flash_attn_f32_f16_f32acc_cm2"; } else { spv_data = flash_attn_f32_f16_f16acc_cm2_data; spv_size = flash_attn_f32_f16_f16acc_cm2_len; name = "flash_attn_f32_f16_f16acc_cm2"; } } - ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 7, + ggml_vk_create_pipeline(device, fa.second, name, spv_size, spv_data, "main", 8, sizeof(vk_flash_attn_push_constants), {Br, 1, 1}, get_fa_spec_constants(fa.first), aligned ? Bc : 1, true, false, 0); } @@ -5809,6 +5840,22 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) { ggml_vk_create_pipeline(device, it.second, "fa_mask_opt", fa_mask_opt_len, fa_mask_opt_data, "main", 2, sizeof(vk_op_flash_attn_mask_opt_push_constants), {1, 1, 1}, {128, 128 / device->subgroup_size, BrBc.first, BrBc.second}, 1, true, true, device->subgroup_size); } + { + // Large workgroup so the per-row KV scan parallelizes; capped to device limits. + const uint32_t compact_max = std::min({1024u, device->properties.limits.maxComputeWorkGroupInvocations, device->properties.limits.maxComputeWorkGroupSize[0]}); + + // Fast ballot prefix-sum path when the device supports full subgroups; otherwise + // a shared-memory prefix-sum fallback. Both emit a deterministic ascending list. + device->fa_sparse_compact_use_subgroups = device->subgroup_ballot && device->subgroup_require_full_support; + if (device->fa_sparse_compact_use_subgroups) { + const uint32_t compact_wg = std::max(device->subgroup_size, (compact_max / device->subgroup_size) * device->subgroup_size); + const uint32_t compact_num_sg = compact_wg / device->subgroup_size; + ggml_vk_create_pipeline(device, device->pipeline_fa_sparse_compact_subgroup, "fa_sparse_compact_subgroup", fa_sparse_compact_subgroup_len, fa_sparse_compact_subgroup_data, "main", 2, sizeof(vk_op_flash_attn_sparse_compact_push_constants), {1, 1, 1}, {compact_wg, compact_num_sg}, 1, true, true, device->subgroup_size); + } else { + ggml_vk_create_pipeline(device, device->pipeline_fa_sparse_compact, "fa_sparse_compact", fa_sparse_compact_len, fa_sparse_compact_data, "main", 2, sizeof(vk_op_flash_attn_sparse_compact_push_constants), {1, 1, 1}, {compact_max}, 1, true); + } + } + if (device->subgroup_clustered && device->subgroup_require_full_support) { ggml_vk_create_pipeline(device, device->pipeline_quantize_q8_1_x4, "quantize_q8_1_x4", quantize_q8_1_x4_subgroup_len, quantize_q8_1_x4_subgroup_data, "main", 2, sizeof(vk_quantize_q8_1_push_constants), {32 * device->subgroup_size / 8, 1, 1}, { device->subgroup_size }, 1, true, true); } else { @@ -11308,6 +11355,30 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx tuning_params = get_fa_tuning_params(ctx->device, HSK, HSV, N, KV, k_type_eff, v_type_eff, f32acc); + float scale = 1.0f; + float max_bias = 0.0f; + float logit_softcap = 0.0f; + + memcpy(&scale, (const float *) dst->op_params + 0, sizeof(float)); + memcpy(&max_bias, (const float *) dst->op_params + 1, sizeof(float)); + memcpy(&logit_softcap, (const float *) dst->op_params + 2, sizeof(float)); + + if (logit_softcap != 0) { + scale /= logit_softcap; + } + + // Sparse mask hint (op_params[4]): compact the <= n_kv_max finite positions and gather only those. + const int32_t n_kv_max = mask ? ggml_get_op_params_i32(dst, 4) : 0; + static const bool disable_sparse = getenv("GGML_VK_FA_SPARSE_DISABLE") != nullptr; + // cm2 dense is fast, so it needs a larger reduction to win. + const int64_t min_ratio = tuning_params.path == FA_COOPMAT2 ? 4 : 2; + const bool use_sparse = !disable_sparse && n_kv_max > 0 && mask && + max_bias == 0.0f && logit_softcap == 0.0f && + k_type_eff == GGML_TYPE_F16 && v_type_eff == GGML_TYPE_F16 && + nem0 == KV && + (int64_t)KV >= std::max<int64_t>(4096, min_ratio * (int64_t)n_kv_max) && + (gqa_ratio > 1 || (tuning_params.path == FA_SCALAR && N == 1)); + const uint32_t q_stride = (uint32_t)(nbq1 / ggml_type_size(q->type)); uint32_t k_stride = (uint32_t)(nbk1 / ggml_type_size(k->type)); uint32_t v_stride = (uint32_t)(nbv1 / ggml_type_size(v->type)); @@ -11330,7 +11401,6 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx nbv2_eff = (uint32_t)((uint64_t)HSV * KV * sizeof(ggml_fp16_t)); nbv3_eff = (uint32_t)((uint64_t)HSV * KV * nev2 * sizeof(ggml_fp16_t)); } - const uint32_t alignment = tuning_params.block_cols; bool aligned = (KV % alignment) == 0 && // the "aligned" shader variant will forcibly align strides, for performance @@ -11341,23 +11411,11 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx aligned = false; } - float scale = 1.0f; - float max_bias = 0.0f; - float logit_softcap = 0.0f; - - memcpy(&scale, (const float *) dst->op_params + 0, sizeof(float)); - memcpy(&max_bias, (const float *) dst->op_params + 1, sizeof(float)); - memcpy(&logit_softcap, (const float *) dst->op_params + 2, sizeof(float)); - - if (logit_softcap != 0) { - scale /= logit_softcap; - } - // Only use mask opt when the mask is fairly large. This hasn't been tuned extensively. - bool use_mask_opt = mask && nem1 >= 32 && nem0 * nem1 > 32768 && nem0 >= tuning_params.block_cols * 16 + bool use_mask_opt = mask && !use_sparse && nem1 >= 32 && nem0 * nem1 > 32768 && nem0 >= tuning_params.block_cols * 16 && (ctx->device->architecture != vk_device_architecture::AMD_GCN || HSK > 256 || HSV > 256); vk_fa_pipeline_state fa_pipeline_state = get_fa_pipeline_state(ctx->device, tuning_params, HSK, HSV, aligned, f32acc, - mask != nullptr, use_mask_opt, logit_softcap != 0, k_type_eff, v_type_eff); + mask != nullptr, use_mask_opt, logit_softcap != 0, use_sparse, k_type_eff, v_type_eff); vk_pipeline pipeline = nullptr; @@ -11392,7 +11450,19 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx const uint32_t Tr = CEIL_DIV(N, Br); // Try to use split_k when KV is large enough to be worth the overhead. - if (gqa_ratio > 1 && workgroups_x <= Br) { + // Sparse: split_kv carries n_kv_max, split_k partitions its blocks for occupancy. + if (use_sparse) { + split_kv = (uint32_t)n_kv_max; + const uint32_t total_blocks = CEIL_DIV((uint32_t)n_kv_max, Bc); + const uint32_t base_wgs = (gqa_ratio > 1 ? workgroups_x : Tr) * workgroups_y * workgroups_z; + if (base_wgs < shader_core_count * 2) { + split_k = shader_core_count * 2 / base_wgs; + } + split_k = std::max(1u, std::min(split_k, total_blocks)); + // Match the shader's per-split block count so no split is empty. + const uint32_t per_blocks = CEIL_DIV(total_blocks, split_k); + split_k = CEIL_DIV(total_blocks, per_blocks); + } else if (gqa_ratio > 1 && workgroups_x <= Br) { split_k = shader_core_count * 2 / (workgroups_x * workgroups_y * workgroups_z); } else if (gqa_ratio <= 1) { uint32_t total_wgs_no_split = Tr * workgroups_y * workgroups_z; @@ -11401,7 +11471,7 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx } } - if (split_k > 1) { + if (!use_sparse && split_k > 1) { // Try to evenly split KV into split_k chunks, but it needs to be a multiple // of "align", so recompute split_k based on that. split_kv = ROUNDUP_POW2(std::max(1u, KV / split_k), alignment); @@ -11448,6 +11518,24 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx } } + // Sparse index scratch reuses prealloc_y (mutually exclusive with mask opt). + const uint64_t sparse_idx_size = use_sparse + ? sizeof(int32_t) * (uint64_t)n_kv_max * nem1 * nem2 * nem3 + : 0; + vk_pipeline sparse_compact_pipeline = ctx->device->fa_sparse_compact_use_subgroups + ? ctx->device->pipeline_fa_sparse_compact_subgroup + : ctx->device->pipeline_fa_sparse_compact; + if (use_sparse) { + ggml_pipeline_request_descriptor_sets(ctx, sparse_compact_pipeline, 1); + if (ctx->prealloc_size_y < sparse_idx_size) { + ctx->prealloc_size_y = sparse_idx_size; + ggml_vk_preallocate_buffers(ctx, subctx); + } + if (ctx->prealloc_y_need_sync) { + ggml_vk_sync_buffers(ctx, subctx); + } + } + const uint32_t n_head_kv = neq2; const uint32_t n_head_log2 = 1u << (uint32_t) floorf(log2f((float) n_head_kv)); const float m0 = powf(2.0f, -(max_bias ) / n_head_log2); @@ -11460,6 +11548,7 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx vk_subbuffer mask_buf = mask ? ggml_vk_tensor_subbuffer(ctx, mask) : q_buf; vk_subbuffer sinks_buf = sinks ? ggml_vk_tensor_subbuffer(ctx, sinks) : q_buf; vk_subbuffer mask_opt_buf = use_mask_opt ? ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0) : q_buf; + vk_subbuffer sparse_buf = use_sparse ? ggml_vk_subbuffer(ctx, ctx->prealloc_y, 0) : q_buf; if (use_dequant_kv) { const uint64_t fp = sizeof(ggml_fp16_t); @@ -11511,6 +11600,24 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx ggml_vk_sync_buffers(ctx, subctx); } + if (use_sparse) + { + const vk_op_flash_attn_sparse_compact_push_constants sc_pc = { + KV, + nem1, + nem2, + (uint32_t)(mask->nb[1] / sizeof(ggml_fp16_t)), + (uint32_t)(mask->nb[2] / sizeof(ggml_fp16_t)), + (uint32_t)(mask->nb[3] / sizeof(ggml_fp16_t)), + (uint32_t)n_kv_max, + }; + + ggml_vk_dispatch_pipeline(ctx, subctx, sparse_compact_pipeline, + { mask_buf, sparse_buf }, sc_pc, + { nem1, nem2, nem3 }); + ggml_vk_sync_buffers(ctx, subctx); + } + const vk_flash_attn_push_constants pc = { N, KV, (uint32_t)ne1, (uint32_t)ne2, (uint32_t)ne3, (uint32_t)neq2, (uint32_t)neq3, @@ -11543,7 +11650,7 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx vk_subbuffer split_k_buf = ggml_vk_subbuffer(ctx, ctx->prealloc_split_k, 0); ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, - {q_buf, k_buf, v_buf, mask_buf, sinks_buf, split_k_buf, mask_opt_buf}, + {q_buf, k_buf, v_buf, mask_buf, sinks_buf, split_k_buf, mask_opt_buf, sparse_buf}, pc, { dispatch_x, workgroups_y, workgroups_z }); ggml_vk_sync_buffers(ctx, subctx); @@ -11558,13 +11665,16 @@ static void ggml_vk_flash_attn(ggml_backend_vk_context * ctx, vk_context& subctx workgroups_x *= pipeline->wg_denoms[0]; } ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, - {q_buf, k_buf, v_buf, mask_buf, sinks_buf, dst_buf, mask_opt_buf}, + {q_buf, k_buf, v_buf, mask_buf, sinks_buf, dst_buf, mask_opt_buf, sparse_buf}, pc, { workgroups_x, workgroups_y, workgroups_z }); } if (use_dequant_kv) { ctx->prealloc_x_need_sync = true; } + if (use_mask_opt || use_sparse) { + ctx->prealloc_y_need_sync = true; + } } static vk_conv_shapes ggml_vk_conv_select_shape(ggml_backend_vk_context * ctx, uint32_t K, uint32_t NPQ) { diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp index 9a12cdfb8817..107d44aaa801 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn.comp @@ -218,12 +218,14 @@ void main() { uint32_t c = (idx + tid) % Bc; uint32_t r = (idx + tid) / Bc; if (idx + tid < Bc * Br) { - if ((!KV_bounds_check || j * Bc + c < KV) && (!nem1_bounds_check || i * Br + r < p.nem1)) { - FLOAT_TYPE m = FLOAT_TYPE(data_m[m_offset + (i * Br + r) * m_stride + (j * Bc + c)]); + uint32_t kcol; + bool kv_active = fa_kv_index(j * Bc + c, kcol); + if (kv_active && (!nem1_bounds_check || i * Br + r < p.nem1)) { + FLOAT_TYPE m = FLOAT_TYPE(data_m[m_offset + (i * Br + r) * m_stride + kcol]); masksh[c * masksh_stride + r] = m; max_mask = max(max_mask, float(m)); } else { - masksh[c * masksh_stride + r] = FLOAT_TYPE(0); + masksh[c * masksh_stride + r] = USE_SPARSE ? FLOAT_TYPE(NEG_FLT_MAX_OVER_2) : FLOAT_TYPE(0); } } } @@ -258,14 +260,15 @@ void main() { uint32_t c = (idx + tid) / (HSK / 4); if (idx + gl_WorkGroupSize.x <= Bc * HSK / 4 || c < Bc) { FLOAT_TYPEV4 K_Tf = FLOAT_TYPEV4(0); - if (!KV_bounds_check || j * Bc + c < KV) { + uint32_t kcol; + if (fa_kv_index(j * Bc + c, kcol)) { if (USE_DECODE_K) { - uint coord = (j * Bc + c) * k_stride * BLOCK_SIZE_K + 4 * d; + uint coord = kcol * k_stride * BLOCK_SIZE_K + 4 * d; uint ib = coord / BLOCK_SIZE_K; uint iqs = (coord % BLOCK_SIZE_K); K_Tf = dequantize4(ib, iqs, k_offset, BINDING_IDX_K); } else { - K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + (j * Bc + c) * k_stride / 4 + d]); + K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + kcol * k_stride / 4 + d]); } } @@ -305,7 +308,9 @@ void main() { } [[unroll]] for (uint32_t c = 0; c < cols_per_thread; ++c) { - if (KV_bounds_check && j * Bc + c * cols_per_iter + col_tid >= KV) { + uint32_t kcol; + bool kv_active = fa_kv_index(j * Bc + c * cols_per_iter + col_tid, kcol); + if (!kv_active) { continue; } @@ -313,12 +318,12 @@ void main() { if (SHMEM_STAGING != 0) { K_Tf = kvsh[(c * cols_per_iter + col_tid) * kvsh_stride + (d * D_split + d_tid)]; } else if (USE_DECODE_K) { - uint coord = (j * Bc + c * cols_per_iter + col_tid) * k_stride * BLOCK_SIZE_K + 4 * (d * D_split + d_tid); + uint coord = kcol * k_stride * BLOCK_SIZE_K + 4 * (d * D_split + d_tid); uint ib = coord / BLOCK_SIZE_K; uint iqs = (coord % BLOCK_SIZE_K); K_Tf = dequantize4(ib, iqs, k_offset, BINDING_IDX_K); } else { - K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + (j * Bc + c * cols_per_iter + col_tid) * k_stride / 4 + d * D_split + d_tid]); + K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + kcol * k_stride / 4 + d * D_split + d_tid]); } [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { Sf[r][c] = dot_product(Q_cache[r], K_Tf, Sf[r][c]); @@ -327,7 +332,9 @@ void main() { } } else { [[unroll]] for (uint32_t c = 0; c < cols_per_thread; ++c) { - if (KV_bounds_check && j * Bc + c * cols_per_iter + col_tid >= KV) { + uint32_t kcol; + bool kv_active = fa_kv_index(j * Bc + c * cols_per_iter + col_tid, kcol); + if (!kv_active) { continue; } @@ -336,12 +343,12 @@ void main() { if (SHMEM_STAGING != 0) { K_Tf = kvsh[(c * cols_per_iter + col_tid) * kvsh_stride + (d * D_split + d_tid)]; } else if (USE_DECODE_K) { - uint coord = (j * Bc + c * cols_per_iter + col_tid) * k_stride * BLOCK_SIZE_K + 4 * (d * D_split + d_tid); + uint coord = kcol * k_stride * BLOCK_SIZE_K + 4 * (d * D_split + d_tid); uint ib = coord / BLOCK_SIZE_K; uint iqs = (coord % BLOCK_SIZE_K); K_Tf = dequantize4(ib, iqs, k_offset, BINDING_IDX_K); } else { - K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + (j * Bc + c * cols_per_iter + col_tid) * k_stride / 4 + d * D_split + d_tid]); + K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + kcol * k_stride / 4 + d * D_split + d_tid]); } [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { Sf[r][c] = dot_product(Qf[tile_row(r) * qf_stride + d * D_split + d_tid], K_Tf, Sf[r][c]); @@ -489,14 +496,15 @@ void main() { uint32_t c = (idx + tid) / (HSV / 4); if (idx + gl_WorkGroupSize.x <= Bc * HSV / 4 || c < Bc) { FLOAT_TYPEV4 V_Tf = FLOAT_TYPEV4(0); - if (!KV_bounds_check || j * Bc + c < KV) { + uint32_t vcol; + if (fa_kv_index(j * Bc + c, vcol)) { if (USE_DECODE_V) { - uint coord = (j * Bc + c) * v_stride * BLOCK_SIZE_V + 4 * d; + uint coord = vcol * v_stride * BLOCK_SIZE_V + 4 * d; uint ib = coord / BLOCK_SIZE_V; uint iqs = (coord % BLOCK_SIZE_V); V_Tf = dequantize4(ib, iqs, v_offset, BINDING_IDX_V); } else { - V_Tf = FLOAT_TYPEV4(data_vv4[v_offset / 4 + (j * Bc + c) * v_stride / 4 + d]); + V_Tf = FLOAT_TYPEV4(data_vv4[v_offset / 4 + vcol * v_stride / 4 + d]); } } @@ -507,7 +515,9 @@ void main() { } [[unroll]] for (uint32_t c = 0; c < cols_per_thread; ++c) { - if (KV_bounds_check && j * Bc + c * cols_per_iter + col_tid >= KV) { + uint32_t vcol; + bool kv_active = fa_kv_index(j * Bc + c * cols_per_iter + col_tid, vcol); + if (!kv_active) { continue; } @@ -522,12 +532,12 @@ void main() { if (SHMEM_STAGING != 0) { Vf = kvsh[(c * cols_per_iter + col_tid) * kvsh_stride + (d * D_split + d_tid)]; } else if (USE_DECODE_V) { - uint coord = (j * Bc + c * cols_per_iter + col_tid) * v_stride * BLOCK_SIZE_V + 4 * (d * D_split + d_tid); + uint coord = vcol * v_stride * BLOCK_SIZE_V + 4 * (d * D_split + d_tid); uint ib = coord / BLOCK_SIZE_V; uint iqs = (coord % BLOCK_SIZE_V); Vf = dequantize4(ib, iqs, v_offset, BINDING_IDX_V); } else { - Vf = FLOAT_TYPEV4(data_vv4[v_offset / 4 + (j * Bc + c * cols_per_iter + col_tid) * v_stride / 4 + d * D_split + d_tid]); + Vf = FLOAT_TYPEV4(data_vv4[v_offset / 4 + vcol * v_stride / 4 + d * D_split + d_tid]); } [[unroll]] for (uint32_t r = 0; r < rows_per_thread; ++r) { Of[r][d] += FLOAT_TYPEV4(Pf[r] * Vf); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl index a4be1ebf98e3..2e0e23bc11c1 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_base.glsl @@ -24,6 +24,8 @@ const bool USE_MASK_OPT = (Flags & 1) != 0; const bool MASK_ENABLE = (Flags & 2) != 0; const bool LOGIT_SOFTCAP = (Flags & 4) != 0; const bool OLD_AMD_WINDOWS = (Flags & 8) != 0; +// Sparse: gather binding-7 indices instead of scanning [0,KV); p.split_kv = n_kv_max. +const bool USE_SPARSE = (Flags & 16) != 0; // Round up head sizes to a multiple of 16, for coopmat1/coopmat2 paths const uint32_t HSK_pad = (HSK + 15) & ~15; @@ -82,6 +84,8 @@ layout (binding = 5) writeonly buffer OV4 {D_TYPEV4 data_ov4[];}; layout (binding = 6) readonly buffer MO {uint32_t data_mask_opt[];}; +layout (binding = 7) readonly buffer SP {int32_t data_sparse[];}; + #define MASK_OPT_ALL_NEG_INF 1 #define MASK_OPT_ALL_ZERO 2 @@ -144,7 +148,7 @@ ACC_TYPE perElemOpGetSink(const in uint32_t r, const in uint32_t c, const in ACC uint32_t i, N, KV, split_k_index, Tr, start_j, end_j, gqa_iq1, iq2, iq3, rk2, rk3, rv2, rv3, ik2, ik3, iv2, iv3, - q_stride, k_stride, v_stride, m_stride; + q_stride, k_stride, v_stride, m_stride, sparse_base; void init_indices() { @@ -208,6 +212,33 @@ void init_indices() // that prevents the compiler from folding the "&" through the select // and breaking the alignment detection. m_stride = (p.gqa_ratio > 1) ? (p.gqa_ratio >> 16) : KV; + + // Sparse: the tile shares one mask row (gqa heads, or Br==1). split_k + // partitions the n_kv_max blocks. + if (USE_SPARSE) { + uint32_t qrow = (p.gqa_ratio > 1) ? gqa_iq1 : (i * Br); + sparse_base = (((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * p.nem1 + qrow) * p.split_kv; + + uint32_t total_blocks = CEIL_DIV(p.split_kv, Bc); + uint32_t per_blocks = CEIL_DIV(total_blocks, p.k_num); + start_j = min(split_k_index * per_blocks, total_blocks); + end_j = min((split_k_index + 1) * per_blocks, total_blocks); + } +} + +// Resolve a linear KV slot to a real column; false for inactive (sparse padding/-1, or dense OOB). +bool fa_kv_index(uint lin, out uint kv_col) { + if (USE_SPARSE) { + if (lin >= p.split_kv) { + kv_col = 0; + return false; + } + int idx = data_sparse[sparse_base + lin]; + kv_col = idx >= 0 ? uint(idx) : 0; + return idx >= 0; + } + kv_col = lin; + return !KV_bounds_check || lin < KV; } // Bias applied to softmax to stay in fp16 range. diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp index 057ed739aa8d..aa9dd624bef3 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm1.comp @@ -176,9 +176,16 @@ void main() { uint32_t c = (idx + tid) / (Br / 4); uint32_t r = (idx + tid) % (Br / 4); if (idx + tid < Bc * Br / 4 || idx + gl_WorkGroupSize.x <= Bc * Br / 4) { - if ((!KV_bounds_check || j * Bc + c < KV)) { + uint32_t kcol; + bool kv_active = fa_kv_index(j * Bc + c, kcol); + if (kv_active) { f16vec4 m; - if (!nem1_bounds_check || i * Br + r * 4 + 3 < p.nem1) { + if (USE_SPARSE) { + // sparse is gqa-gated (m_stride == 0): all four rows share the value + FLOAT_TYPE mv = FLOAT_TYPE(data_m[m_offset + kcol]); + m = f16vec4(mv); + max_mask = max(max_mask, float(mv)); + } else if (!nem1_bounds_check || i * Br + r * 4 + 3 < p.nem1) { m = f16vec4(data_m[m_offset + (i * Br + r * 4 ) * m_stride + (j * Bc + c)], data_m[m_offset + (i * Br + r * 4 + 1) * m_stride + (j * Bc + c)], data_m[m_offset + (i * Br + r * 4 + 2) * m_stride + (j * Bc + c)], @@ -206,6 +213,8 @@ void main() { m = f16vec4(0.0); } mask_cache[idx / WorkGroupSize] = m; + } else if (USE_SPARSE) { + mask_cache[idx / WorkGroupSize] = f16vec4(NEG_FLT_MAX_OVER_2); } } } @@ -231,17 +240,19 @@ void main() { uint32_t c = (idx + tid) / (HSK_pad / 4); if (idx + gl_WorkGroupSize.x <= Bc * HSK_pad / 4 || c < Bc) { FLOAT_TYPEV4 K_Tf = FLOAT_TYPEV4(0); - if ((!KV_bounds_check || j * Bc + c < KV) && (HSK == HSK_pad || d < HSK / 4)) { + uint32_t kcol; + bool kv_active = fa_kv_index(j * Bc + c, kcol); + if (kv_active && (HSK == HSK_pad || d < HSK / 4)) { #if !defined(BFLOAT16) if (USE_DECODE_K) { - uint coord = (j * Bc + c) * k_stride * BLOCK_SIZE_K + 4 * d; + uint coord = kcol * k_stride * BLOCK_SIZE_K + 4 * d; uint ib = coord / BLOCK_SIZE_K; uint iqs = (coord % BLOCK_SIZE_K); K_Tf = dequantize4(ib, iqs, k_offset, BINDING_IDX_K); } else #endif { - K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + (j * Bc + c) * k_stride / 4 + d]); + K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + kcol * k_stride / 4 + d]); } } @@ -266,7 +277,7 @@ void main() { if (SHMEM_STAGING == 0) { // For quants we always need to dequant into kvsh; for f16/bf16 we can load // directly from global memory when alignment / bounds allow it. - const bool stage_k = USE_DECODE_K || KV_bounds_check || d * 16 + 16 > HSK; + const bool stage_k = USE_DECODE_K || KV_bounds_check || USE_SPARSE || d * 16 + 16 > HSK; if (stage_k) { barrier(); [[unroll]] for (uint32_t idx = 0; idx < Bc * MatBr / 4; idx += gl_WorkGroupSize.x) { @@ -274,17 +285,19 @@ void main() { uint32_t row = (idx + tid) / (MatBr / 4); if (idx + tid < Bc * MatBr / 4) { FLOAT_TYPEV4 K_Tf = FLOAT_TYPEV4(0); - if ((!KV_bounds_check || j * Bc + row < KV) && (HSK == HSK_pad || d * 16 + col_vec * 4 < HSK)) { + uint32_t kcol; + bool kv_active = fa_kv_index(j * Bc + row, kcol); + if (kv_active && (HSK == HSK_pad || d * 16 + col_vec * 4 < HSK)) { #if !defined(BFLOAT16) if (USE_DECODE_K) { - uint coord = (j * Bc + row) * k_stride * BLOCK_SIZE_K + d * 16 + col_vec * 4; + uint coord = kcol * k_stride * BLOCK_SIZE_K + d * 16 + col_vec * 4; uint ib = coord / BLOCK_SIZE_K; uint iqs = (coord % BLOCK_SIZE_K); K_Tf = dequantize4(ib, iqs, k_offset, BINDING_IDX_K); } else #endif { - K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + (j * Bc + row) * k_stride / 4 + d * 16 / 4 + col_vec]); + K_Tf = FLOAT_TYPEV4(data_kv4[k_offset / 4 + kcol * k_stride / 4 + d * 16 / 4 + col_vec]); } } @@ -401,17 +414,19 @@ void main() { uint32_t c = (idx + tid) / (HSV_pad / 4); if (idx + gl_WorkGroupSize.x <= Bc * HSV_pad / 4 || c < Bc) { FLOAT_TYPEV4 V_Tf = FLOAT_TYPEV4(0); - if ((!KV_bounds_check || j * Bc + c < KV) && (HSV == HSV_pad || d < HSV / 4)) { + uint32_t v_row; + bool kv_active = fa_kv_index(j * Bc + c, v_row); + if (kv_active && (HSV == HSV_pad || d < HSV / 4)) { #if !defined(BFLOAT16) if (USE_DECODE_V) { - uint coord = (j * Bc + c) * v_stride * BLOCK_SIZE_V + 4 * d; + uint coord = v_row * v_stride * BLOCK_SIZE_V + 4 * d; uint ib = coord / BLOCK_SIZE_V; uint iqs = (coord % BLOCK_SIZE_V); V_Tf = dequantize4(ib, iqs, v_offset, BINDING_IDX_V); } else #endif { - V_Tf = FLOAT_TYPEV4(data_vv4[v_offset / 4 + (j * Bc + c) * v_stride / 4 + d]); + V_Tf = FLOAT_TYPEV4(data_vv4[v_offset / 4 + v_row * v_stride / 4 + d]); } } @@ -441,21 +456,22 @@ void main() { if (SHMEM_STAGING == 0) { // For quants we always preload via kvsh. For f16/bf16 we only preload when // alignment / bounds force it (otherwise we coopMatLoad direct from data_vv4). - const bool stage_v = USE_DECODE_V || KV_bounds_check; + const bool stage_v = USE_DECODE_V || KV_bounds_check || USE_SPARSE; if (stage_v) { [[unroll]] for (uint32_t i = 0; i < v_loads_per_thread; ++i) { const uint idx = i * gl_WorkGroupSize.x + tid; const uint row = idx / v_cols; const uint col = idx % v_cols; - const uint v_row = j * Bc + row; + uint32_t v_row; + bool kv_active = fa_kv_index(j * Bc + row, v_row); const uint v_col = hsv_tile * MatBc * row_split + col * 4; const uint coord = v_row * v_stride * BLOCK_SIZE_V + v_col; const uint ib = coord / BLOCK_SIZE_V; const uint iqs = coord % BLOCK_SIZE_V; - if (!KV_bounds_check || (v_row < KV && v_col < HSV)) { + if (USE_SPARSE ? (kv_active && v_col < HSV) : (!KV_bounds_check || (v_row < KV && v_col < HSV))) { #if !defined(BFLOAT16) if (USE_DECODE_V) { kvsh[row * vsh_stride + col] = dequantize4(ib, iqs, v_offset, BINDING_IDX_V); @@ -479,7 +495,7 @@ void main() { coopMatLoad(KMat, Psh, bc_chunk * MatBc * psh_stride, psh_stride, gl_CooperativeMatrixLayoutColumnMajor); if (SHMEM_STAGING == 0) { - if (!USE_DECODE_V && !KV_bounds_check) { + if (!USE_DECODE_V && !KV_bounds_check && !USE_SPARSE) { // F16/BF16 values can be loaded directly from global memory const uint v_tile_row = j * Bc + bc_chunk * MatBc; const uint v_tile_offset = v_offset / 4 + v_tile_row * v_stride / 4 + hsv_offset / 4; diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm2.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm2.comp index 5a9abe2265fa..c6ed63dd42a6 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm2.comp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_cm2.comp @@ -29,6 +29,12 @@ #include "dequant_funcs_cm2.glsl" #endif +#ifdef GL_NV_cooperative_matrix_decode_vector +#define FA_GATHER_BS 4u +#else +#define FA_GATHER_BS 1u +#endif + // buffer_reference stride = sizeof(struct) = FaBlockBytesK/V. layout(buffer_reference, std430, buffer_reference_align = 1) buffer decodeBufFA_K { uint8_t raw[FaBlockBytesK]; @@ -107,6 +113,67 @@ layout (binding = 1) readonly buffer K {uint8_t data_k[];}; layout (binding = 2) readonly buffer V {uint8_t data_v[];}; layout (binding = 3) readonly buffer M {uint8_t data_m[];}; +// f16 aliases for the sparse gather callbacks. +layout (binding = 1) readonly buffer KF16 {float16_t data_kf16[];}; +layout (binding = 2) readonly buffer VF16 {float16_t data_vf16[];}; +layout (binding = 3) readonly buffer MF16 {float16_t data_mf16[];}; +#ifdef GL_NV_cooperative_matrix_decode_vector +layout (binding = 1) readonly buffer KF16V4 {f16vec4 data_kf16v4[];}; +layout (binding = 2) readonly buffer VF16V4 {f16vec4 data_vf16v4[];}; +#endif + +// K/V/mask f16-element offsets for the current head/batch, set in main(). +uint32_t g_k_off_elem, g_v_off_elem, g_m_off_elem; + +#if !defined(BFLOAT16) +// blockCoords are in block units: KV slot = blockCoords[0], +// head dim = blockCoords[1]*FA_GATHER_BS + coordInBlock[1]. +float16_t faGatherK(const decodeBufFA_K unused, const uint32_t blockCoords[2], const uint32_t coordInBlock[2]) { + if (blockCoords[0] >= p.split_kv) { return float16_t(0); } + const int r = data_sparse[sparse_base + blockCoords[0]]; + return r < 0 ? float16_t(0) : data_kf16[g_k_off_elem + uint(r) * k_stride + blockCoords[1] * FA_GATHER_BS + coordInBlock[1]]; +} + +float16_t faGatherV(const decodeBufFA_V unused, const uint32_t blockCoords[2], const uint32_t coordInBlock[2]) { + if (blockCoords[0] >= p.split_kv) { return float16_t(0); } + const int r = data_sparse[sparse_base + blockCoords[0]]; + return r < 0 ? float16_t(0) : data_vf16[g_v_off_elem + uint(r) * v_stride + blockCoords[1] * FA_GATHER_BS + coordInBlock[1]]; +} + +#ifdef GL_NV_cooperative_matrix_decode_vector +f16vec4 faGatherKVector(const decodeBufFA_K unused, const uint32_t blockCoords[2], const uint32_t coordInBlock[2]) { + if (blockCoords[0] >= p.split_kv) { return f16vec4(0); } + const int r = data_sparse[sparse_base + blockCoords[0]]; + if (r < 0) { return f16vec4(0); } + const uint32_t o = g_k_off_elem + uint(r) * k_stride + blockCoords[1] * FA_GATHER_BS + coordInBlock[1]; + return data_kf16v4[o / 4]; +} + +f16vec4 faGatherVVector(const decodeBufFA_V unused, const uint32_t blockCoords[2], const uint32_t coordInBlock[2]) { + if (blockCoords[0] >= p.split_kv) { return f16vec4(0); } + const int r = data_sparse[sparse_base + blockCoords[0]]; + if (r < 0) { return f16vec4(0); } + const uint32_t o = g_v_off_elem + uint(r) * v_stride + blockCoords[1] * FA_GATHER_BS + coordInBlock[1]; + return data_vf16v4[o / 4]; +} + +#define FAGATHERK , faGatherK, faGatherKVector +#define FAGATHERV , faGatherV, faGatherVVector +#else +#define FAGATHERK , faGatherK +#define FAGATHERV , faGatherV +#endif +#endif + +// Add gathered mask to S (slope==1 since sparse requires max_bias==0). col = slot in block jblk. +ACC_TYPE faAddSparseMask(const uint32_t row, const uint32_t col, const ACC_TYPE elem, const uint32_t jblk) { + const float NEG = uintBitsToFloat(0xFEFFFFFF); + const uint32_t kvslot = jblk * Bc + col; + if (kvslot >= p.split_kv) { return ACC_TYPE(NEG); } + const int r = data_sparse[sparse_base + kvslot]; + return r < 0 ? ACC_TYPE(NEG) : elem + ACC_TYPE(data_mf16[g_m_off_elem + row * m_stride + uint(r)]); +} + ACC_TYPE maxReduce(const in ACC_TYPE x, const in ACC_TYPE y) { return max(x, y); } @@ -185,14 +252,16 @@ void main() { tensorViewNV<2, false, 1, 0> tensorViewTranspose = createTensorViewNV(2, false, 1, 0); - const uint bs_k = fa_block_elems(FaTypeK); - const uint bs_v = fa_block_elems(FaTypeV); + const uint bs_k = USE_SPARSE ? FA_GATHER_BS : fa_block_elems(FaTypeK); + const uint bs_v = USE_SPARSE ? FA_GATHER_BS : fa_block_elems(FaTypeV); tensorLayoutK = setTensorLayoutBlockSizeNV(tensorLayoutK, 1, bs_k); tensorLayoutV = setTensorLayoutBlockSizeNV(tensorLayoutV, 1, bs_v); + // Sparse iterates n_kv_max (in split_kv); the decode callbacks remap each slot. + const uint32_t KV_iter = USE_SPARSE ? p.split_kv : KV; tensorLayoutQ = setTensorLayoutDimensionNV(tensorLayoutQ, N, HSK); - tensorLayoutK = setTensorLayoutDimensionNV(tensorLayoutK, KV, HSK); - tensorLayoutV = setTensorLayoutDimensionNV(tensorLayoutV, KV, HSV); + tensorLayoutK = setTensorLayoutDimensionNV(tensorLayoutK, KV_iter, HSK); + tensorLayoutV = setTensorLayoutDimensionNV(tensorLayoutV, KV_iter, HSV); // hint to the compiler that strides are aligned for the aligned variant of the shader if (Clamp != gl_CooperativeMatrixClampModeConstantNV) @@ -250,6 +319,10 @@ void main() { mo_offset += ((iq3 % p.nem3) * p.nem2 + (iq2 % p.nem2)) * CEIL_DIV(p.nem1, Br) * mo_stride; } + g_k_off_elem = (ik2*p.nb12 + ik3*p.nb13) / 2; + g_v_off_elem = (iv2*p.nb22 + iv3*p.nb23) / 2; + g_m_off_elem = m_offset / 2; + uint32_t mask_opt = 0; uint32_t mask_opt_idx = ~0; @@ -257,7 +330,7 @@ void main() { for (uint32_t j = start_j; j < end_j; ++j) { coopmat<float16_t, gl_ScopeWorkgroup, Br, Bc, gl_MatrixUseAccumulator> mv = coopmat<float16_t, gl_ScopeWorkgroup, Br, Bc, gl_MatrixUseAccumulator>(0); - if (MASK_ENABLE) { + if (MASK_ENABLE && !USE_SPARSE) { if (USE_MASK_OPT && mask_opt_idx != j / 16) { mask_opt_idx = j / 16; @@ -315,7 +388,9 @@ void main() { coopMatLoadTensorNV(K_T, data_k, k_offset, sliceTensorLayoutNV(tensorLayoutK, j * Bc, Bc, 0, HSK_pad), tensorViewTranspose); #else const bool k_use_decode = (bs_k > 1u); - if (k_use_decode) { + if (USE_SPARSE) { + coopMatLoadTensorNV(K_T, data_k, k_offset, sliceTensorLayoutNV(tensorLayoutK, j * Bc, Bc, 0, HSK_pad), tensorViewTranspose FAGATHERK); + } else if (k_use_decode) { coopMatLoadTensorNV(K_T, data_k, k_offset, sliceTensorLayoutNV(tensorLayoutK, j * Bc, Bc, 0, HSK_pad), tensorViewTranspose FADECODEK); } else { coopMatLoadTensorNV(K_T, data_k, k_offset, sliceTensorLayoutNV(tensorLayoutK, j * Bc, Bc, 0, HSK_pad), tensorViewTranspose); @@ -330,7 +405,9 @@ void main() { } } - if (MASK_ENABLE) { + if (MASK_ENABLE && USE_SPARSE) { + coopMatPerElementNV(S, S, faAddSparseMask, j); + } else if (MASK_ENABLE) { S += slopeMat*coopmat<ACC_TYPE, gl_ScopeWorkgroup, Br, Bc, gl_MatrixUseAccumulator>(mv); } @@ -385,7 +462,9 @@ void main() { coopMatLoadTensorNV(V, data_v, v_offset, sliceTensorLayoutNV(tensorLayoutV, j * Bc, Bc, 0, HSV_pad)); #else const bool v_use_decode = (bs_v > 1u); - if (v_use_decode) { + if (USE_SPARSE) { + coopMatLoadTensorNV(V, data_v, v_offset, sliceTensorLayoutNV(tensorLayoutV, j * Bc, Bc, 0, HSV_pad) FAGATHERV); + } else if (v_use_decode) { coopMatLoadTensorNV(V, data_v, v_offset, sliceTensorLayoutNV(tensorLayoutV, j * Bc, Bc, 0, HSV_pad) FADECODEV); } else { coopMatLoadTensorNV(V, data_v, v_offset, sliceTensorLayoutNV(tensorLayoutV, j * Bc, Bc, 0, HSV_pad)); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_sparse_compact.comp b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_sparse_compact.comp new file mode 100644 index 000000000000..3d31362661b3 --- /dev/null +++ b/ggml/src/ggml-vulkan/vulkan-shaders/flash_attn_sparse_compact.comp @@ -0,0 +1,102 @@ +#version 450 + +#extension GL_EXT_control_flow_attributes : enable +#extension GL_EXT_shader_16bit_storage : require +#extension GL_EXT_shader_explicit_arithmetic_types_int32 : require +#ifdef USE_SUBGROUPS +#extension GL_KHR_shader_subgroup_basic : require +#extension GL_KHR_shader_subgroup_ballot : require +#endif + +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; +layout(constant_id = 0) const uint BLOCK_SIZE = 128; +layout(constant_id = 1) const uint NUM_SUBGROUPS = 1; + +layout (binding = 0) readonly buffer M {float16_t data_m[];}; +layout (binding = 1) writeonly buffer I {int32_t data_i[];}; + +layout (push_constant) uniform parameter { + uint KV; + uint nem1; + uint nem2; + uint nbm1; + uint nbm2; + uint nbm3; + uint n_kv_max; +} p; + +#ifdef USE_SUBGROUPS +shared uvec4 ballots_sh[NUM_SUBGROUPS]; +#else +shared uint scan[BLOCK_SIZE]; +#endif + +// One workgroup per mask row: compact the finite-mask KV positions into a +// per-row index list of length n_kv_max, -1 padded. Emitted in ascending KV +// order so the downstream attention accumulation is deterministic. +void main() { + const uint i1 = gl_WorkGroupID.x; + const uint i2 = gl_WorkGroupID.y; + const uint i3 = gl_WorkGroupID.z; + const uint tid = gl_LocalInvocationIndex; + + const uint m_base = i3 * p.nbm3 + i2 * p.nbm2 + i1 * p.nbm1; + const uint out_base = ((i3 * p.nem2 + i2) * p.nem1 + i1) * p.n_kv_max; + + uint base = 0; + for (uint chunk = 0; chunk < p.KV; chunk += BLOCK_SIZE) { + const uint k = chunk + tid; + bool selected = false; + if (k < p.KV) { + const float v = float(data_m[m_base + k]); + selected = !isinf(v) && !isnan(v); + } + +#ifdef USE_SUBGROUPS + const uvec4 ballot = subgroupBallot(selected); + if (subgroupElect()) { + ballots_sh[gl_SubgroupID] = ballot; + } + barrier(); + + uint subgroup_base = 0; + uint total = 0; + [[unroll]] for (uint s = 0; s < gl_NumSubgroups; ++s) { + if (s == gl_SubgroupID) { + subgroup_base = total; + } + total += subgroupBallotBitCount(ballots_sh[s]); + } + barrier(); + + const uint slot = base + subgroup_base + subgroupBallotExclusiveBitCount(ballot); +#else + // Hillis-Steele inclusive prefix sum over the workgroup. + scan[tid] = selected ? 1u : 0u; + barrier(); + for (uint off = 1; off < BLOCK_SIZE; off <<= 1) { + uint add = 0; + if (tid >= off) { + add = scan[tid - off]; + } + barrier(); + scan[tid] += add; + barrier(); + } + + const uint inclusive = scan[tid]; + const uint total = scan[BLOCK_SIZE - 1]; + const uint slot = base + inclusive - 1u; +#endif + + if (selected && slot < p.n_kv_max) { + data_i[out_base + slot] = int32_t(k); + } + base += total; + barrier(); + } + + for (uint s = min(base, p.n_kv_max) + tid; s < p.n_kv_max; s += BLOCK_SIZE) { + data_i[out_base + s] = int32_t(-1); + } +} diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 30fe0884e547..d3f425968df2 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -922,6 +922,8 @@ void process_shaders() { string_to_spv("fa_split_k_reduce", "flash_attn_split_k_reduce.comp", {}); string_to_spv("fa_mask_opt", "flash_attn_mask_opt.comp", {}); + string_to_spv("fa_sparse_compact", "flash_attn_sparse_compact.comp", {}); + string_to_spv("fa_sparse_compact_subgroup", "flash_attn_sparse_compact.comp", {{"USE_SUBGROUPS", "1"}}); string_to_spv("quantize_q8_1", "quantize_q8_1.comp", {}); string_to_spv("quantize_q8_1_subgroup", "quantize_q8_1.comp", {{"USE_SUBGROUPS", "1"}}); diff --git a/ggml/src/ggml-webgpu/CMakeLists.txt b/ggml/src/ggml-webgpu/CMakeLists.txt index 1503a1ef8ba6..2eacca7f2b3b 100644 --- a/ggml/src/ggml-webgpu/CMakeLists.txt +++ b/ggml/src/ggml-webgpu/CMakeLists.txt @@ -39,6 +39,12 @@ ggml_add_backend_library(ggml-webgpu add_dependencies(ggml-webgpu generate_shaders) +# Dawn needs C++20 (https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/quickstart-cmake.md#prerequisites) +target_compile_features(ggml-webgpu PRIVATE cxx_std_20) + +# Disable C++20 module scanning since emscan-deps fails to find webgpu_cpp.h +set_target_properties(ggml-webgpu PROPERTIES CXX_SCAN_FOR_MODULES OFF) + if(EMSCRIPTEN) set(EMDAWNWEBGPU_DIR "" CACHE PATH "Path to emdawnwebgpu_pkg") diff --git a/ggml/src/ggml-webgpu/ggml-webgpu.cpp b/ggml/src/ggml-webgpu/ggml-webgpu.cpp index f06a9c872db9..8b060c41a1c6 100644 --- a/ggml/src/ggml-webgpu/ggml-webgpu.cpp +++ b/ggml/src/ggml-webgpu/ggml-webgpu.cpp @@ -374,20 +374,28 @@ static wgpu::Buffer ggml_webgpu_tensor_buf(const ggml_tensor * tensor) { return ctx->buffer; } +// Binding offset for a tensor: the largest aligned offset at or before the tensor whose +// distance to the tensor is a whole number of type blocks, so shaders can index the +// misalignment in elements even for block quantized types. +static size_t ggml_webgpu_tensor_align_offset(const ggml_tensor * t, size_t alignment) { + const size_t offset = ggml_webgpu_tensor_offset(t); + const size_t type_size = ggml_type_size(t->type); + size_t aligned = offset & ~(alignment - 1); + while ((offset - aligned) % type_size != 0) { + GGML_ASSERT(aligned >= alignment); + aligned -= alignment; + } + return aligned; +} + static size_t ggml_webgpu_tensor_misalignment(const ggml_tensor * t, size_t alignment) { - size_t offset = ggml_webgpu_tensor_offset(t); - return offset & (alignment - 1); + return ggml_webgpu_tensor_offset(t) - ggml_webgpu_tensor_align_offset(t, alignment); } static size_t ggml_webgpu_tensor_misalignment(webgpu_context & ctx, const ggml_tensor * t) { return ggml_webgpu_tensor_misalignment(t, ctx->global_ctx->capabilities.limits.minStorageBufferOffsetAlignment); } -static size_t ggml_webgpu_tensor_align_offset(const ggml_tensor * t, size_t alignment) { - size_t offset = ggml_webgpu_tensor_offset(t); - return offset & ~(alignment - 1); -} - static size_t ggml_webgpu_tensor_align_offset(webgpu_context & ctx, const ggml_tensor * t) { return ggml_webgpu_tensor_align_offset(t, ctx->global_ctx->capabilities.limits.minStorageBufferOffsetAlignment); } @@ -4006,16 +4014,17 @@ static void ggml_backend_webgpu_request_adapter(wgpu::Instance & instance, wgpu: options.nextInChain = &adapterTogglesDesc; #endif - instance.WaitAny(instance.RequestAdapter( - &options, wgpu::CallbackMode::AllowSpontaneous, - [&adapter](wgpu::RequestAdapterStatus status, wgpu::Adapter _adapter, const char * message) { - if (status != wgpu::RequestAdapterStatus::Success) { - GGML_LOG_ERROR("ggml_webgpu: Failed to get an adapter: %s\n", message); - return; - } - adapter = std::move(_adapter); - }), - UINT64_MAX); + instance.WaitAny( + instance.RequestAdapter( + &options, wgpu::CallbackMode::AllowSpontaneous, + [&adapter](wgpu::RequestAdapterStatus status, wgpu::Adapter _adapter, wgpu::StringView message) { + if (status != wgpu::RequestAdapterStatus::Success) { + GGML_LOG_ERROR("ggml_webgpu: Failed to get an adapter: %s\n", std::string(message).c_str()); + return; + } + adapter = std::move(_adapter); + }), + UINT64_MAX); } static void create_webgpu_device(ggml_backend_webgpu_reg_context * ctx) { diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index aa666ff0212a..78f56315d5f5 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -541,6 +541,7 @@ class MODEL_ARCH(IntEnum): ARWKV7 = auto() MAMBA = auto() MAMBA2 = auto() + MAPLE = auto() JAMBA = auto() XVERSE = auto() COMMAND_R = auto() @@ -1295,6 +1296,7 @@ class MODEL_TENSOR(IntEnum): MODEL_ARCH.ARWKV7: "arwkv7", MODEL_ARCH.MAMBA: "mamba", MODEL_ARCH.MAMBA2: "mamba2", + MODEL_ARCH.MAPLE: "maple", MODEL_ARCH.JAMBA: "jamba", MODEL_ARCH.XVERSE: "xverse", MODEL_ARCH.COMMAND_R: "command-r", @@ -3490,6 +3492,23 @@ class MODEL_TENSOR(IntEnum): MODEL_TENSOR.SSM_NORM, MODEL_TENSOR.SSM_OUT, ], + MODEL_ARCH.MAPLE: [ + MODEL_TENSOR.TOKEN_EMBD, + MODEL_TENSOR.OUTPUT_NORM, + MODEL_TENSOR.OUTPUT, + MODEL_TENSOR.ATTN_NORM, + 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.FFN_NORM, + MODEL_TENSOR.FFN_GATE_INP, + MODEL_TENSOR.FFN_GATE_EXP, + MODEL_TENSOR.FFN_DOWN_EXP, + MODEL_TENSOR.FFN_UP_EXP, + ], MODEL_ARCH.JAMBA: [ MODEL_TENSOR.TOKEN_EMBD, MODEL_TENSOR.OUTPUT_NORM, diff --git a/grammars/README.md b/grammars/README.md index 9478b3e1b5aa..f005fc2522b8 100644 --- a/grammars/README.md +++ b/grammars/README.md @@ -146,8 +146,6 @@ You can use GBNF grammars: - For any completion endpoints, passed as the `json_schema` body field - For the `/chat/completions` endpoint, passed inside the `response_format` body field (e.g. `{"type", "json_object", "schema": {"items": {}}}` or `{ type: "json_schema", json_schema: {"schema": ...} }`) - In [llama-cli](../tools/cli) and [llama-completion](../tools/completion), passed as the `--json` / `-j` flag -- To convert to a grammar ahead of time: - - in CLI, with [examples/json_schema_to_grammar.py](../examples/json_schema_to_grammar.py) > [!NOTE] > The JSON schema is only used to constrain the model output and is not injected into the prompt. The model has no visibility into the schema, so if you want it to understand the expected structure, describe it explicitly in your prompt. This does not apply to tool calling, where schemas are injected into the prompt. @@ -187,11 +185,7 @@ llama-cli \ <summary>Show grammar</summary> -You can convert any schema in command-line with: - -```bash -examples/json_schema_to_grammar.py name-age-schema.json -``` +The schema above converts to: ``` char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) diff --git a/scripts/check-apiabi-compat.sh b/scripts/check-apiabi-compat.sh new file mode 100755 index 000000000000..078abb864f65 --- /dev/null +++ b/scripts/check-apiabi-compat.sh @@ -0,0 +1,272 @@ +#!/bin/sh +# Check for backwards-incompatible API and ABI changes between two builds +# +# Backwards-incompatible API changes, such as removing a value from an enum, +# are checked by abi-compliance-checker. Such changes can break compilation of +# existing programs. +# +# Backwards-incompatible ABI changes, such as the removal of a public function, +# are checked by libigail-tools. Such changes could break run-time dynamic +# linking of existing binaries. (We don't use a-c-c for ABI checks because it +# needs a debug build, whereas abigail does not.) +# +# Commands: +# --generate <build-dir>: Creates API/ABI dumps in <build-dir> +# <build-dir> is expected to be a CMake build result +# --check <dir1> <dir2>: Compares dumps in <dir1> and <dir2> +# Comparison exit codes +# 0: all good +# 1: backwards-incompatible changes found +# +# Options: +# --include-path <dir>: a-c-c calls gcc on headers; use this option to add +# directories to gcc's search path +# +# +# This script would typically be used before cutting a release: +# +# 1. Generate API/ABI dump for the old version +# +# $ check-apiabi-compat.sh --generate <build-dir-old> libfoo [ libbar ...] +# +# 2. <update source> +# +# 3. Generate API/ABI dump for the new version +# +# $ check-apiabi-compat.sh --generate <build-dir-new> libfoo [ libbar ...] +# +# 4. Compare the two dumps +# +# $ check-apiabi-compat.sh --check <old-build-dir> <new-build-dir> +# +# If the check exits 0, all is fine. Otherwise, backwards-incompatible +# changes were found, and the librar(ies) need a SOVER bump. +set -eu + +# Preconditions +if ! command -v abi-compliance-checker >/dev/null 2>&1; then + echo "abi-compliance-checker is not installed." >&2 + exit 1 +elif ! command -v abidw >/dev/null 2>&1; then + echo "abigail-tools are not installed." >&2 + exit 1 +fi + +# Some generic functions +usage() { + echo "Usage: $0 [ --include-path <dir> ] --generate <build-dir> libXXX [ libYYY ... ]" >&2 + echo " $0 --check <old-build-dir> <new-build-dir>" >&2 +} + +get_cmake_project_name() { + sed -nr 's/^project\("(.*)".*$/\1/p' CMakeLists.txt +} + +get_cmake_version() { + major="$(sed -nr 's/^set\([A-Z]+_VERSION_MAJOR ([0-9]+)\)$/\1/p' CMakeLists.txt)" + minor="$(sed -nr 's/^set\([A-Z]+_VERSION_MINOR ([0-9]+)\)$/\1/p' CMakeLists.txt)" + patch="$(sed -nr 's/^set\([A-Z]+_VERSION_PATCH ([0-9]+)\)$/\1/p' CMakeLists.txt)" + echo "$major.$minor.$patch" +} + +# Option parsing and validation +DO_GEN=0 +DO_CHECK=0 +BUILD_DIR= +BUILD_DIR_NEW= +INCLUDE_PATHS= +while [ "$#" -gt 0 ]; do + case "$1" in + --generate=*) + DO_GEN=1 + BUILD_DIR="${1#*=}" + shift + ;; + --generate) + DO_GEN=1 + if [ -z "${2:-}" ]; then + usage + exit 1 + fi + BUILD_DIR="$2" + shift 2 + ;; + --check) + DO_CHECK=1 + if [ -z "${2:-}" ] || [ -z "${3:-}" ]; then + usage + exit 1 + elif ! [ -d "$2" ]; then + echo "$2 is not a directory." >&2 + exit 1 + elif ! [ -d "$3" ]; then + echo "$3 is not a directory." >&2 + exit 1 + fi + BUILD_DIR="$2" + BUILD_DIR_NEW="$3" + shift 3 + ;; + --include-path=*) + INCLUDE_PATHS="$INCLUDE_PATHS ${1#*=}" + shift + ;; + --include-path) + if [ -z "${2:-}" ]; then + usage + exit 1 + fi + INCLUDE_PATHS="$INCLUDE_PATHS $2" + shift 2 + ;; + -h | --help) + usage + exit 1 + ;; + -?*) + usage + exit 1 + ;; + *) + break + ;; + esac +done +if [ $((DO_GEN + DO_CHECK)) -gt 1 ]; then + echo "Can only use one --command." >&2 + exit +fi +PROJECT_NAME="$(get_cmake_project_name)" +PROJECT_VERSION="$(get_cmake_version)" +LIB_NAMES="" +while [ "$#" -gt 0 ]; do + if [ "${1#lib}" = "$1" ]; then + echo "Library to check must start with libXXX." >&2 + exit 1 + fi + LIB_NAMES="$LIB_NAMES $1" + shift +done + +dump_current_api() { + echo "Dumping API..." + + DESCRIPTOR="$BUILD_DIR/apiabi/acc-descriptor.xml" + mkdir -p "$BUILD_DIR/apiabi" + cat >"$DESCRIPTOR" <<EOF +<version>$PROJECT_VERSION</version> +<headers>include</headers> +<add_include_paths>$INCLUDE_PATHS</add_include_paths> +EOF + + # This addresses a bug between a-c-c and universal-ctags, manifested when + # a name is use both for a tag and a function name + mkdir -p "$BUILD_DIR/apiabi/.ctags.d" + echo "--fields=-t" >"$BUILD_DIR/apiabi/.ctags.d/acc.ctags" + + # Change HOME so that .ctags.d gets picked up by universal-ctags, if used + HOME="$BUILD_DIR/apiabi" abi-compliance-checker \ + -headers-only \ + -lib "$PROJECT_NAME" \ + -dump "$DESCRIPTOR" \ + -log-path "$BUILD_DIR/apiabi/acc.log" \ + -dump-path "$BUILD_DIR/apiabi/api.dump" + # acc generates this file with an ancient timestamp, which confuses gzip + touch "$BUILD_DIR/apiabi/api.dump" +} + +dump_current_abi() { + echo "Dumping ABIs ..." + mkdir -p "$BUILD_DIR/apiabi" + # The suppressions are needed to avoid including all the internal C++ + # symbols, and system types + cat >"$BUILD_DIR/apiabi/abidw.suppress" <<EOF +[suppress_function] + +label = suppress internal C++ mangled functions +symbol_name_regexp = ^_Z +drop = yes + +[suppress_variable] +label = suppress internal C++ mangled variables +symbol_name_regexp = ^_Z +drop = yes + +[suppress_type] +label = Suppress types outside of our own source +source_location_not_regexp = ^include/ +drop = yes +EOF + + # In abidw 2.5, handling of undefined stuff was changed a bit + abidw_version="$(abidw --version | sed -r 's/^abidw: ([0-9]+\.[0-9]+).*$/\1/')" + abidw_major="${abidw_version%.*}" + abidw_minor="${abidw_version#*.}" + if [ "$abidw_major" -gt 2 ] || [ "$abidw_minor" -gt 4 ]; then + abidw_undefined_syms_options="--no-load-undefined-interfaces" + else + abidw_undefined_syms_options="--drop-undefined-syms" + fi + + for lib_name in $LIB_NAMES; do + # Depending on where add_library resides, the libraries can end up in + # build/src or build/bin + lib_path="$BUILD_DIR/src/$lib_name.so" + if ! [ -f "$lib_path" ]; then + lib_path="$BUILD_DIR/bin/$lib_name.so" + if ! [ -f "$lib_path" ]; then + echo "Cannot find library $lib_name.so" >&2 + exit 1 + fi + fi + abidw \ + --headers-dir include \ + "$abidw_undefined_syms_options" \ + --suppressions "$BUILD_DIR/apiabi/abidw.suppress" \ + --out-file "${BUILD_DIR}/apiabi/$lib_name.abi.xml" \ + "$lib_path" + done +} + +# Run the actual commands +if [ "$DO_GEN" -eq 1 ]; then + dump_current_api + dump_current_abi + exit 0 +elif [ "$DO_CHECK" -eq 1 ]; then + # From here on, we don't want to exit on first error + set +e + + abi-compliance-checker \ + -strict \ + -source \ + -library "$PROJECT_NAME" \ + -old "$BUILD_DIR/apiabi/api.dump" \ + -new "$BUILD_DIR_NEW/apiabi/api.dump" \ + -src-report-path "$BUILD_DIR_NEW/apiabi/api_compat_report.html" + API_RESULT=$? + + ABI_RESULT=0 + for xml_file in "$BUILD_DIR/apiabi/"lib*.abi.xml; do + xml_file_new="$BUILD_DIR_NEW/apiabi/$(basename "$xml_file")" + + if ! [ -f "$xml_file_new" ]; then + echo "Cannot compare, missing file: $xml_file_new" >&2 + exit 1 + fi + + abidiff "$xml_file" "$xml_file_new" + res=$? + [ "$((res & 8))" -ne 0 ] && ABI_RESULT=1 + done + + if [ "$API_RESULT" -gt 0 ]; then + echo "ERROR: API changed with possible backwards-compatibility problems." >&2 + fi + if [ "$ABI_RESULT" -gt 0 ]; then + echo "ERROR: ABI changed with possible backwards-compatibility problems." >&2 + fi + if [ "$((API_RESULT + ABI_RESULT))" -gt 0 ]; then + exit 1 + fi +fi diff --git a/scripts/hip/gcn-cdna-vgpr-check.py b/scripts/hip/gcn-cdna-vgpr-check.py index 40fb789417c3..f660735f401f 100644 --- a/scripts/hip/gcn-cdna-vgpr-check.py +++ b/scripts/hip/gcn-cdna-vgpr-check.py @@ -64,6 +64,7 @@ def main(): '_ZL12rwkv_wkv_f32ILi128EEviiiiPKfS1_S1_S1_S1_S1_Pf', '_ZL9mul_mat_qIL9ggml_type10ELi64ELb1EEvPKcPKiS4_S4_PfS5_PKf15HIP_vector_typeIjLj3EEiiiiiS9_S9_iiiS9_S9_iiiS9_', '_ZL9mul_mat_qIL9ggml_type42ELi128ELb1EEvPKcPKiS4_S4_PfS5_PKf15HIP_vector_typeIjLj3EEiiiiiS9_S9_iiiS9_S9_iiiS9_', + '_ZL18flash_attn_ext_f16ILi576ELi512ELi2ELi32ELb0ELb1ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil' } functions = parse_log_file(log_file) diff --git a/scripts/snapdragon/ggml-hexagon-align-macros.py b/scripts/snapdragon/ggml-hexagon-align-macros.py new file mode 100755 index 000000000000..b64db3e654a8 --- /dev/null +++ b/scripts/snapdragon/ggml-hexagon-align-macros.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +""" +align-macros.py - Inspect and align trailing backslashes in multiline C/C++ macros. + +Usage: + align-macros.py [paths...] # Check and report misaligned macros + align-macros.py --diff [paths...] # Show unified diff of fixes + align-macros.py --fix [paths...] # Fix misaligned macros in-place + align-macros.py --fix --mode majority ... # Align to the dominant column + align-macros.py --fix --pad 2 ... # Align to (max_content_len + pad) + +Safety rules: + - Macros that are ALREADY aligned are NEVER touched (unless --all is given). + - Whitespace after trailing backslashes is flagged and cleaned. +""" + +import argparse +import difflib +import logging +import os +import re +import sys +from collections import Counter +from typing import List, Optional, Tuple, NamedTuple + +logger = logging.getLogger("ggml-hexagon-align-macros") + + +class MacroLine(NamedTuple): + line_num: int # 1-indexed + raw: str # Original line including newline + content: str # Line content before trailing backslash (stripped of trailing whitespace) + bs_col: Optional[int] # 1-indexed column of backslash, or None if last line has no backslash + trailing_ws: bool # True if whitespace existed after the backslash + + +class MacroDef(NamedTuple): + name: str + filepath: str + start_line: int + end_line: int + lines: List[MacroLine] + + +def parse_macros(filepath: str) -> List[MacroDef]: + """Extract all multiline macros from a C/C++ source file.""" + try: + with open(filepath, "r", encoding="utf-8", errors="replace") as f: + lines = f.readlines() + except Exception as e: + logger.error(f"Error reading {filepath}: {e}") + return [] + + macros: List[MacroDef] = [] + i = 0 + n = len(lines) + + while i < n: + line = lines[i] + m = re.match(r"^\s*#\s*define\s+([A-Za-z_][A-Za-z0-9_]*)", line) + if m: + macro_name = m.group(1) + macro_start = i + 1 + macro_lines: List[MacroLine] = [] + cur = i + + while cur < n: + l_raw = lines[cur] + l_rstrip = l_raw.rstrip("\r\n") + + # Check if line has a trailing backslash + # Note: handle possible accidental spaces after backslash + match_bs = re.search(r"\\([ \t]*)$", l_rstrip) + if match_bs: + has_trailing_ws = len(match_bs.group(1)) > 0 + bs_index = match_bs.start() + content = l_rstrip[:bs_index].rstrip() + # 1-indexed column of the backslash + bs_col = bs_index + 1 + macro_lines.append(MacroLine( + line_num=cur + 1, + raw=l_raw, + content=content, + bs_col=bs_col, + trailing_ws=has_trailing_ws + )) + cur += 1 + else: + # Line does not end with backslash + if cur == i: + # Single-line macro, not multiline + break + else: + # Final line of a multiline macro + macro_lines.append(MacroLine( + line_num=cur + 1, + raw=l_raw, + content=l_rstrip.rstrip(), + bs_col=None, + trailing_ws=False + )) + break + + # Only record if it is a multiline macro (has at least one continuation line) + continuation_lines = [ml for ml in macro_lines if ml.bs_col is not None] + if continuation_lines: + macro_end = macro_lines[-1].line_num + macros.append(MacroDef( + name=macro_name, + filepath=filepath, + start_line=macro_start, + end_line=macro_end, + lines=macro_lines + )) + i = cur + i += 1 + + return macros + + +def is_macro_aligned(macro: MacroDef) -> bool: + """A macro is aligned if all continuation lines have backslashes at the same column.""" + bs_cols = [ml.bs_col for ml in macro.lines if ml.bs_col is not None] + if not bs_cols: + return True + has_trailing_ws = any(ml.trailing_ws for ml in macro.lines) + return len(set(bs_cols)) == 1 and not has_trailing_ws + + +def compute_target_column(macro: MacroDef, mode: str, pad: int, target_col: Optional[int]) -> int: + """Determine the column where backslashes should be aligned.""" + max_content_len = max(len(ml.content) for ml in macro.lines) + min_needed = max_content_len + pad + + if target_col is not None: + return max(target_col, min_needed) + + bs_cols = [ml.bs_col for ml in macro.lines if ml.bs_col is not None] + if not bs_cols: + return min_needed + + if mode == "min": + return min_needed + elif mode == "max": + return max(max(bs_cols), min_needed) + elif mode == "majority": + counts = Counter(bs_cols) + # Sort by frequency descending, then by column descending + majority_col = sorted(counts.items(), key=lambda x: (-x[1], -x[0]))[0][0] + return max(majority_col, min_needed) + else: + return min_needed + + +def realign_macro_lines(macro: MacroDef, target_col: int) -> List[str]: + """Format macro lines with backslashes aligned at target_col.""" + new_lines: List[str] = [] + for ml in macro.lines: + nl = "\r\n" if ml.raw.endswith("\r\n") else "\n" + if ml.bs_col is None: + # Last line without backslash + new_lines.append(ml.raw) + else: + if not ml.content: + spaces = " " * (target_col - 1) + new_lines.append(f"{spaces}\\{nl}") + else: + spaces_needed = max(1, target_col - len(ml.content) - 1) + new_lines.append(f"{ml.content}{' ' * spaces_needed}\\{nl}") + return new_lines + + +def process_file(filepath: str, args: argparse.Namespace) -> Tuple[int, int, Optional[str]]: + macros = parse_macros(filepath) + if not macros: + return 0, 0, None + + with open(filepath, "r", encoding="utf-8", errors="replace") as f: + file_lines = f.readlines() + + misaligned_count = 0 + modified = False + new_file_lines = list(file_lines) + + for macro in macros: + aligned = is_macro_aligned(macro) + if not aligned or args.all: + if not aligned: + misaligned_count += 1 + + bs_cols = [ml.bs_col for ml in macro.lines if ml.bs_col is not None] + max_content = max(len(ml.content) for ml in macro.lines) + col_counts = Counter(bs_cols) + + if not args.quiet: + logger.info(f"{filepath}:{macro.start_line}-{macro.end_line} [{macro.name}]") + logger.info(f" Max content width: {max_content}, Min needed column (+{args.pad}): {max_content + args.pad}") + logger.info(f" Current backslash columns: {dict(sorted(col_counts.items()))}") + trailing_ws_lines = [ml.line_num for ml in macro.lines if ml.trailing_ws] + if trailing_ws_lines: + logger.warning(f" Warning: Trailing whitespace after backslash on line(s): {trailing_ws_lines}") + + target_col = compute_target_column(macro, args.mode, args.pad, args.target_col) + if not args.quiet: + logger.info(f" -> Target alignment column: {target_col}") + + realigned = realign_macro_lines(macro, target_col) + + start_idx = macro.start_line - 1 + end_idx = start_idx + len(macro.lines) + if new_file_lines[start_idx:end_idx] != realigned: + new_file_lines[start_idx:end_idx] = realigned + modified = True + + diff_text = None + if modified: + diff = difflib.unified_diff( + file_lines, + new_file_lines, + fromfile=f"a/{filepath}", + tofile=f"b/{filepath}", + lineterm="" + ) + diff_text = "\n".join(diff) + + if args.fix: + with open(filepath, "w", encoding="utf-8") as f: + f.writelines(new_file_lines) + if not args.quiet: + logger.info(f" [FIXED] Updated {filepath}") + + return len(macros), misaligned_count, diff_text + + +def find_source_files(paths: List[str]) -> List[str]: + extensions = {".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".inl"} + result: List[str] = [] + for p in paths: + if os.path.isfile(p): + result.append(p) + elif os.path.isdir(p): + for root, _, files in os.walk(p): + for file in sorted(files): + _, ext = os.path.splitext(file) + if ext.lower() in extensions: + result.append(os.path.join(root, file)) + return sorted(result) + + +def main(): + logging.basicConfig(level=logging.INFO, format="%(message)s") + parser = argparse.ArgumentParser( + description="Inspect and align backslashes in multiline C/C++ macros." + ) + parser.add_argument("paths", nargs="*", default=["."], help="Files or directories to scan (default: current dir)") + parser.add_argument("--fix", action="store_true", help="Fix misaligned macros in-place") + parser.add_argument("--diff", action="store_true", help="Display unified diff of suggested fixes") + parser.add_argument("--check", action="store_true", help="Exit with code 1 if misaligned macros exist") + parser.add_argument("--mode", choices=["min", "max", "majority"], default="min", + help="Alignment mode: 'min' (max_len + pad), 'max' (max existing col), 'majority' (dominant col)") + parser.add_argument("--pad", type=int, default=2, help="Spaces between longest line and backslash (default: 2)") + parser.add_argument("--target-col", type=int, default=None, help="Force alignment to an exact column") + parser.add_argument("--all", action="store_true", help="Realign all macros even if already aligned (default: only misaligned)") + parser.add_argument("-q", "--quiet", action="store_true", help="Only output errors and diffs/summary") + + args = parser.parse_args() + + files = find_source_files(args.paths) + if not files: + logger.error("No C/C++ source files found.") + sys.exit(0) + + total_macros = 0 + total_misaligned = 0 + diffs: List[str] = [] + + for filepath in files: + num_macros, num_misaligned, diff_text = process_file(filepath, args) + total_macros += num_macros + total_misaligned += num_misaligned + if diff_text: + diffs.append(diff_text) + + if args.diff and diffs: + logger.info("\n--- Proposed Changes ---\n") + for d in diffs: + logger.info(d) + + logger.info(f"\nSummary: scanned {len(files)} files, {total_macros} multiline macros, {total_misaligned} misaligned.") + + if args.check and total_misaligned > 0: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/snapdragon/run.py b/scripts/snapdragon/run.py index 81eecd2e0ceb..dc71d4a3219d 100755 --- a/scripts/snapdragon/run.py +++ b/scripts/snapdragon/run.py @@ -14,6 +14,42 @@ logger = logging.getLogger("run") +MANAGED_ENV_NAMES = ( + "GGML_HEXAGON_DEVICES", + "GGML_HEXAGON_VERBOSE", + "GGML_HEXAGON_PROFILE", + "GGML_HEXAGON_NHVX", + "GGML_HEXAGON_NHMX", + "GGML_HEXAGON_HOSTBUF", + "GGML_HEXAGON_OPBATCH", + "GGML_HEXAGON_OPQUEUE", + "GGML_HEXAGON_OPPOLL", + "GGML_HEXAGON_OPFILTER", + "GGML_HEXAGON_OPFUSION", + "GGML_HEXAGON_VMEM", + "GGML_HEXAGON_MBUF", + "GGML_HEXAGON_MM_SELECT", + "GGML_HEXAGON_FA_SELECT", + "GGML_HEXAGON_AR_SELECT", + "GGML_HEXAGON_ETM", + "GGML_HEXAGON_ARCH", + "GGML_HEXAGON_OPTRACE", + "GGML_OPENCL_PLATFORM", + "GGML_OPENCL_DEVICE", + "GGML_OPENCL_OPFILTER", + "GGML_OPENCL_KERNEL_CACHE_DIR", + "GGML_OPENCL_KERNEL_CACHE_DEBUG", + "GGML_OPENCL_FA_TUNE", + "GGML_OPENCL_DISABLE_FUSION", + "GGML_OPENCL_ADRENO_XMEM_GEMM", + "GGML_OPENCL_ADRENO_USE_LARGE_BUFFER", + "GGML_SCHED_DEBUG", + "MTMD_BACKEND_DEVICE", + "D", + "DEVICE", +) + + def parse_target(target_str): if not target_str: return None, None @@ -38,6 +74,57 @@ def shlex_join(args_list): return " ".join(pipes.quote(x) for x in args_list) +def split_device_list(devices): + parts = [] + curr = [] + bracket_depth = 0 + + for ch in devices: + if ch == '[': + bracket_depth += 1 + curr.append(ch) + elif ch == ']': + if bracket_depth > 0: + bracket_depth -= 1 + curr.append(ch) + elif ch == ',' and bracket_depth == 0: + part = "".join(curr).strip() + if part: + parts.append(part) + curr = [] + else: + curr.append(ch) + + part = "".join(curr).strip() + if part: + parts.append(part) + + return parts + + +def device_arg_from_devices(devices): + if devices.isdigit(): + n = int(devices) + return ",".join(f"HTP{i}" for i in range(n)) + + names = [] + for part in split_device_list(devices): + if "[" in part: + part = part.split("[", 1)[0].strip() + if part: + names.append(part) + + return ",".join(names) + + +def normalize_cmd_device_args(cmd_args): + for i, arg in enumerate(cmd_args): + if arg == "--device" and i + 1 < len(cmd_args): + cmd_args[i + 1] = device_arg_from_devices(cmd_args[i + 1]) + elif arg.startswith("--device="): + cmd_args[i] = "--device=" + device_arg_from_devices(arg.split("=", 1)[1]) + + def main(): logging.basicConfig(level=logging.INFO, format='%(message)s') # Split arguments at '--' @@ -142,8 +229,6 @@ def main(): def set_env(env_name, opt_val): if opt_val is not None: env_vars[env_name] = str(opt_val) - elif env_name in os.environ: - env_vars[env_name] = os.environ[env_name] # Resolve and filter devices (HTP vs OpenCL) device_in_cmd = None @@ -166,7 +251,7 @@ def set_env(env_name, opt_val): hex_devices = devices_val cl_device = "" else: - parts = [p.strip() for p in devices_val.split(",")] + parts = split_device_list(devices_val) # Any device containing "htp" is Hexagon, rest is OpenCL hex_parts = [p for p in parts if "htp" in p.lower()] cl_parts = [ @@ -181,15 +266,13 @@ def set_env(env_name, opt_val): # Set Hexagon devices if hex_devices: env_vars["GGML_HEXAGON_DEVICES"] = hex_devices - elif "GGML_HEXAGON_DEVICES" in os.environ: - env_vars["GGML_HEXAGON_DEVICES"] = os.environ["GGML_HEXAGON_DEVICES"] + + normalize_cmd_device_args(cmd_args) # Set OpenCL device (unless overridden by --cl-device) final_cl_device = args.cl_device if args.cl_device is not None else cl_device if final_cl_device: env_vars["GGML_OPENCL_DEVICE"] = final_cl_device - elif "GGML_OPENCL_DEVICE" in os.environ: - env_vars["GGML_OPENCL_DEVICE"] = os.environ["GGML_OPENCL_DEVICE"] # Map shared & backend-specific parameters with correct overrides @@ -206,8 +289,6 @@ def set_env(env_name, opt_val): if args.cl_fa_tune or args.profile is not None: env_vars["GGML_OPENCL_FA_TUNE"] = "1" - elif "GGML_OPENCL_FA_TUNE" in os.environ: - env_vars["GGML_OPENCL_FA_TUNE"] = os.environ["GGML_OPENCL_FA_TUNE"] # Other Hexagon environment variables set_env("GGML_HEXAGON_NHVX", args.hex_nhvx) @@ -235,18 +316,12 @@ def set_env(env_name, opt_val): if args.cl_disable_fusion: env_vars["GGML_OPENCL_DISABLE_FUSION"] = "1" - elif "GGML_OPENCL_DISABLE_FUSION" in os.environ: - env_vars["GGML_OPENCL_DISABLE_FUSION"] = os.environ["GGML_OPENCL_DISABLE_FUSION"] if args.cl_adreno_xmem: env_vars["GGML_OPENCL_ADRENO_XMEM_GEMM"] = "1" - elif "GGML_OPENCL_ADRENO_XMEM_GEMM" in os.environ: - env_vars["GGML_OPENCL_ADRENO_XMEM_GEMM"] = os.environ["GGML_OPENCL_ADRENO_XMEM_GEMM"] if args.cl_adreno_large_buffer: env_vars["GGML_OPENCL_ADRENO_USE_LARGE_BUFFER"] = "1" - elif "GGML_OPENCL_ADRENO_USE_LARGE_BUFFER" in os.environ: - env_vars["GGML_OPENCL_ADRENO_USE_LARGE_BUFFER"] = os.environ["GGML_OPENCL_ADRENO_USE_LARGE_BUFFER"] if args.sched_debug: env_vars["GGML_SCHED_DEBUG"] = "2" @@ -288,15 +363,7 @@ def set_env(env_name, opt_val): has_b = any(arg == "-b" for arg in cmd_args) if not has_b: if args.devices: - if args.devices.isdigit(): - n = int(args.devices) - device_val = ",".join(f"HTP{i}" for i in range(n)) - else: - device_val = args.devices - elif "D" in os.environ: - device_val = os.environ["D"] - elif "DEVICE" in os.environ: - device_val = os.environ["DEVICE"] + device_val = device_arg_from_devices(args.devices) else: device_val = "HTP0" if device_val: @@ -305,17 +372,10 @@ def set_env(env_name, opt_val): has_device = any(arg.startswith("--device") for arg in cmd_args) if not has_device: if args.devices: - if args.devices.isdigit(): - n = int(args.devices) - device_val = ",".join(f"HTP{i}" for i in range(n)) - else: - device_val = args.devices - elif "D" in os.environ: - device_val = os.environ["D"] - elif "DEVICE" in os.environ: - device_val = os.environ["DEVICE"] + device_val = device_arg_from_devices(args.devices) else: device_val = "HTP0" + if device_val: cmd_args += ["--device", device_val] @@ -415,6 +475,8 @@ def set_env(env_name, opt_val): else: local_env["LD_LIBRARY_PATH"] = lib_dir + os.path.pathsep + local_env.get("LD_LIBRARY_PATH", "") + for k in MANAGED_ENV_NAMES: + local_env.pop(k, None) for k, v in env_vars.items(): local_env[k] = v diff --git a/scripts/sync-ggml.last b/scripts/sync-ggml.last index 7b44a311abd0..d2a1bd951795 100644 --- a/scripts/sync-ggml.last +++ b/scripts/sync-ggml.last @@ -1 +1 @@ -e91ded11bdcd78c42f9c8d3978ff6686eb4c1226 +456172ec733a135778adcd32d00e576a58232e45 diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py index 0170b5b168f4..a73ae1193e8c 100755 --- a/scripts/sync_vendor.py +++ b/scripts/sync_vendor.py @@ -5,7 +5,7 @@ import sys import subprocess -HTTPLIB_VERSION = "refs/tags/v0.54.1" +HTTPLIB_VERSION = "refs/tags/v0.56.0" # used by examples/gguf-hash, these repos have no release tag, so we pin a commit XXHASH_COMMIT = "9f465f1ea932d6ad9a26cd77496311ffa544cd68" diff --git a/scripts/ui-assets.cmake b/scripts/ui-assets.cmake index 402f95bd4f34..dec34ebda96c 100644 --- a/scripts/ui-assets.cmake +++ b/scripts/ui-assets.cmake @@ -21,6 +21,7 @@ set(DIST_DIR "${UI_BINARY_DIR}/dist") set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist") set(WORK_DIR "${UI_BINARY_DIR}/ui-src") set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp") +set(EMBED_STAMP "${UI_BINARY_DIR}/.ui-embed.sha256") set(UI_CPP "${UI_BINARY_DIR}/ui.cpp") set(UI_H "${UI_BINARY_DIR}/ui.h") @@ -141,9 +142,59 @@ function(ui_validate_assets files in_dir) endfunction() # Generate ui.cpp/ui.h embedding every file of ${dist_dir} (empty table when -# it has no index.html). When LLAMA_UI_GZIP is enabled, assets are compressed -# first and served pre-gzipped (llama_ui_use_gzip()). +# it has no index.html), gzip-compressed when LLAMA_UI_GZIP is enabled. function(emit_files dist_dir) + set(UI_TEMPLATE_DIR "${LLAMA_SOURCE_DIR}/tools/ui") + + # Collect the asset list once and reuse it for the fingerprint, + # validation, compression and embedding. + set(assets "") + if(EXISTS "${dist_dir}/index.html") + file(GLOB_RECURSE assets + LIST_DIRECTORIES false + RELATIVE "${dist_dir}" + "${dist_dir}/*") + list(FILTER assets EXCLUDE REGEX "^_gzip/") + list(SORT assets) + endif() + + if(LLAMA_UI_GZIP AND NOT DEFINED ENV{SOURCE_DATE_EPOCH}) + # Zero the gzip header timestamp so identical inputs give identical + # bytes (and therefore stable ETags) on every machine. + set(ENV{SOURCE_DATE_EPOCH} 0) + endif() + + # Fingerprint of every input that determines ui.cpp/ui.h: compression + # settings, the asset tree (names + SHA-256) and this script + templates. + set(fp "${LLAMA_UI_GZIP}|$ENV{SOURCE_DATE_EPOCH}|${CMAKE_VERSION}\n") + foreach(f ${assets}) + file(SHA256 "${dist_dir}/${f}" h) + string(APPEND fp "${f} ${h}\n") + endforeach() + foreach(g + "${CMAKE_CURRENT_FUNCTION_LIST_FILE}" + "${UI_TEMPLATE_DIR}/ui.h.in" + "${UI_TEMPLATE_DIR}/ui.cpp.in") + file(SHA256 "${g}" h) + string(APPEND fp "gen ${h}\n") + endforeach() + string(SHA256 fingerprint "${fp}") + + if(EXISTS "${EMBED_STAMP}" AND EXISTS "${UI_CPP}" AND EXISTS "${UI_H}") + file(READ "${EMBED_STAMP}" fp_saved) + string(STRIP "${fp_saved}" fp_saved) + if(fp_saved STREQUAL "${fingerprint}") + message(STATUS "UI: assets unchanged, skipping embedding") + return() + endif() + endif() + + # Drop the old stamp up front so a crash mid-generation cannot leave + # outputs and stamp out of sync. + file(REMOVE "${EMBED_STAMP}") + + ui_validate_assets("${assets}" "${dist_dir}") + set(embed_dir "${dist_dir}") set(use_gzip FALSE) @@ -156,21 +207,11 @@ function(emit_files dist_dir) endif() if(LLAMA_UI_GZIP) # Compress every asset into a parallel _gzip/ tree under the build - # directory (never write into the source or dist tree); the - # structure stays the same: /abc/def --> /_gzip/abc/def. - # FORMAT raw produces a bare gzip stream (no archive container) - # that can be served with Content-Encoding: gzip. SOURCE_DATE_EPOCH - # zeroes the header timestamp so identical inputs give identical - # bytes (and therefore stable ETags) on every machine. - if(NOT DEFINED ENV{SOURCE_DATE_EPOCH}) - set(ENV{SOURCE_DATE_EPOCH} 0) - endif() + # directory, served with Content-Encoding: gzip. set(gzip_root "${UI_BINARY_DIR}/ui-gzip") set(gzip_dir "${gzip_root}/_gzip") file(REMOVE_RECURSE "${gzip_root}") - file(GLOB_RECURSE all_files RELATIVE "${dist_dir}" "${dist_dir}/*") - list(FILTER all_files EXCLUDE REGEX "^_gzip/") - foreach(f ${all_files}) + foreach(f IN LISTS assets) get_filename_component(asset_path "${dist_dir}/${f}" REALPATH) get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY) file(MAKE_DIRECTORY "${dst_dir}") @@ -187,21 +228,10 @@ function(emit_files dist_dir) endif() endif() - set(assets "") - if(EXISTS "${embed_dir}/index.html") - file(GLOB_RECURSE assets RELATIVE "${embed_dir}" "${embed_dir}/*") - list(FILTER assets EXCLUDE REGEX "^_gzip/") - list(SORT assets) - ui_validate_assets("${assets}" "${embed_dir}") - endif() - list(LENGTH assets n_assets) - # Only the per-asset data arrays and table rows are built here; all - # static C++ lives in the ui.h.in / ui.cpp.in templates. configure_file - # rewrites an output only when its contents change, so the library is - # not recompiled needlessly. @ONLY keeps ${...} in the content literal; - # mime types come from a fixed list. + # Per-asset arrays and table rows go into the ui.h.in / ui.cpp.in templates; + # configure_file only rewrites on content change, avoiding needless recompiles. set(ASSET_ARRAYS "") set(ASSET_TABLE "") set(idx 0) @@ -235,9 +265,11 @@ function(emit_files dist_dir) set(USE_GZIP true) endif() - set(UI_TEMPLATE_DIR "${LLAMA_SOURCE_DIR}/tools/ui") configure_file("${UI_TEMPLATE_DIR}/ui.h.in" "${UI_H}" @ONLY) configure_file("${UI_TEMPLATE_DIR}/ui.cpp.in" "${UI_CPP}" @ONLY) + + # Write the embed stamp last, after both generated files succeeded. + file(WRITE "${EMBED_STAMP}" "${fingerprint}") message(STATUS "UI: embedded ${n_assets} assets") endfunction() @@ -419,16 +451,8 @@ function(hf_download version out_var out_resolved) message(STATUS "UI: downloading from ${resolved}: ${base}/dist.tar.gz") - file(DOWNLOAD "${base}/dist.tar.gz?download=true" "${archive}" - STATUS status TIMEOUT 300 ${auth_headers} - ) - list(GET status 0 rc) - if(NOT rc EQUAL 0) - list(GET status 1 errmsg) - message(STATUS "UI: download dist.tar.gz from ${resolved} failed: ${errmsg}") - continue() - endif() - + # Fetch the checksum first: when the archive we already have matches + # it, the expensive download is skipped and only extraction repeats. file(DOWNLOAD "${base}/dist.tar.gz.sha256?download=true" "${archive}.sha256" STATUS status TIMEOUT 30 ${auth_headers} ) @@ -439,17 +463,44 @@ function(hf_download version out_var out_resolved) continue() endif() - # Validate sha256 checkums + # Validate the sha256 checksum: reject anything that is not a full + # 64-hex-digit digest before touching the archive. file(READ "${archive}.sha256" expected) string(REGEX MATCH "^[0-9a-fA-F]+" expected "${expected}") string(TOLOWER "${expected}" expected) - file(SHA256 "${archive}" actual) - if("${expected}" STREQUAL "" OR NOT "${actual}" STREQUAL "${expected}") - message(STATUS "UI: checksum mismatch for dist.tar.gz from ${resolved}") + string(LENGTH "${expected}" expected_len) + if(NOT expected_len EQUAL 64) + message(STATUS "UI: invalid checksum from ${resolved}") continue() endif() - # Clear DIST_DIR to remove stale files first + set(actual "") + if(EXISTS "${archive}") + file(SHA256 "${archive}" actual) + endif() + + if("${actual}" STREQUAL "${expected}") + message(STATUS "UI: local dist.tar.gz matches checksum from ${resolved}, skipping download") + else() + file(DOWNLOAD "${base}/dist.tar.gz?download=true" "${archive}" + STATUS status TIMEOUT 300 ${auth_headers} + ) + list(GET status 0 rc) + if(NOT rc EQUAL 0) + list(GET status 1 errmsg) + message(STATUS "UI: download dist.tar.gz from ${resolved} failed: ${errmsg}") + continue() + endif() + + file(SHA256 "${archive}" actual) + if(NOT "${actual}" STREQUAL "${expected}") + message(STATUS "UI: checksum mismatch for dist.tar.gz from ${resolved}") + continue() + endif() + endif() + + # Remove the stamp with the dist tree it describes, together. + file(REMOVE "${STAMP_FILE}") file(REMOVE_RECURSE "${DIST_DIR}") file(ARCHIVE_EXTRACT INPUT "${archive}" DESTINATION "${DIST_DIR}") @@ -495,27 +546,27 @@ endif() if(NOT provisioned AND HF_ENABLED) resolve_version(VERSION) + # Stamp a successful HF download: records bucket + requested version and + # lets later steps distinguish downloaded assets from locally built ones. + set(stamp_key "${HF_BUCKET}|${VERSION}") + set(stamp_ok FALSE) - if(EXISTS "${STAMP_FILE}" AND NOT "${VERSION}" STREQUAL "") + if(EXISTS "${STAMP_FILE}" AND EXISTS "${DIST_DIR}/index.html" AND NOT "${VERSION}" STREQUAL "") file(READ "${STAMP_FILE}" stamped) string(STRIP "${stamped}" stamped) - if("${stamped}" STREQUAL "${VERSION}") + if(stamped STREQUAL "${stamp_key}") set(stamp_ok TRUE) endif() endif() - set(have_assets FALSE) - if(EXISTS "${DIST_DIR}/index.html") - set(have_assets TRUE) - endif() - if(stamp_ok AND have_assets) - message(STATUS "UI: HF stamp '${stamped}' matches version, skipping HF fetch") + if(stamp_ok) + message(STATUS "UI: HF stamp matches '${stamp_key}', skipping HF fetch") set(provisioned TRUE) else() hf_download("${VERSION}" HF_OK HF_RESOLVED) if(HF_OK) - file(WRITE "${STAMP_FILE}" "${HF_RESOLVED}") - message(STATUS "UI: HF download succeeded, stamp updated (${HF_RESOLVED})") + file(WRITE "${STAMP_FILE}" "${stamp_key}") + message(STATUS "UI: HF download succeeded, stamp updated (${stamp_key}, resolved: ${HF_RESOLVED})") set(provisioned TRUE) else() message(STATUS "UI: HF download failed") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b3d17330ca54..8dbaee3902c1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -68,7 +68,6 @@ configure_file(llama-version.h.in ${CMAKE_CURRENT_BINARY_DIR}/llama-version.h @O target_include_directories(llama PRIVATE . ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(llama PUBLIC ../include) target_compile_features (llama PRIVATE cxx_std_17) # don't bump -target_precompile_headers (llama PRIVATE models/models.h) target_link_libraries(llama PUBLIC ggml) diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp index b5efb7206565..0fac27efc05e 100644 --- a/src/llama-arch.cpp +++ b/src/llama-arch.cpp @@ -62,6 +62,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = { { LLM_ARCH_STARCODER2, "starcoder2" }, { LLM_ARCH_MAMBA, "mamba" }, { LLM_ARCH_MAMBA2, "mamba2" }, + { LLM_ARCH_MAPLE, "maple" }, { LLM_ARCH_JAMBA, "jamba" }, { LLM_ARCH_FALCON_H1, "falcon-h1" }, { LLM_ARCH_XVERSE, "xverse" }, diff --git a/src/llama-arch.h b/src/llama-arch.h index f1d173a57556..6e67f5d6599d 100644 --- a/src/llama-arch.h +++ b/src/llama-arch.h @@ -67,6 +67,7 @@ enum llm_arch { LLM_ARCH_STARCODER2, LLM_ARCH_MAMBA, LLM_ARCH_MAMBA2, + LLM_ARCH_MAPLE, LLM_ARCH_JAMBA, LLM_ARCH_FALCON_H1, LLM_ARCH_XVERSE, diff --git a/src/llama-grammar.cpp b/src/llama-grammar.cpp index 6aa03c7666a5..deffec8c0741 100644 --- a/src/llama-grammar.cpp +++ b/src/llama-grammar.cpp @@ -871,17 +871,18 @@ static void llama_grammar_advance_stack( std::set<llama_grammar_stack, decltype(stack_cmp)> seen(stack_cmp); while (!todo.empty()) { - llama_grammar_stack curr_stack = std::move(todo.back()); + llama_grammar_stack curr_stack_candidate = std::move(todo.back()); todo.pop_back(); - if (seen.find( curr_stack) != seen.end()) { + auto [curr_stack_it, inserted] = seen.insert(std::move(curr_stack_candidate)); + if (!inserted) { continue; } - seen.insert(curr_stack); + const llama_grammar_stack & curr_stack = *curr_stack_it; if (curr_stack.empty()) { if (std::find(new_stacks.begin(), new_stacks.end(), curr_stack) == new_stacks.end()) { - new_stacks.emplace_back(std::move(curr_stack)); + new_stacks.emplace_back(curr_stack); } continue; } @@ -924,7 +925,7 @@ static void llama_grammar_advance_stack( case LLAMA_GRETYPE_TOKEN_NOT: if (std::find(new_stacks.begin(), new_stacks.end(), curr_stack) == new_stacks.end()) { // only add the stack if it's not a duplicate of one we already have - new_stacks.emplace_back(std::move(curr_stack)); + new_stacks.emplace_back(curr_stack); } break; default: diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index 46a5d0464bed..657ea80f7f6e 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -2226,7 +2226,7 @@ ggml_tensor * llm_graph_context::build_moe_ffn( const float limit = hparams.swiglu_clamp_exp[il]; constexpr float eps = 1e-6f; if (limit > eps) { - if (arch == LLM_ARCH_DEEPSEEK4 || (arch == LLM_ARCH_DFLASH && hparams.dsv4_hc_mult > 0) || arch == LLM_ARCH_HY_V4) { + if (arch == LLM_ARCH_MAPLE || arch == LLM_ARCH_DEEPSEEK4 || (arch == LLM_ARCH_DFLASH && hparams.dsv4_hc_mult > 0) || arch == LLM_ARCH_HY_V4) { cur = ggml_swiglu_clamp(ctx0, cur, up, limit); } else { up = ggml_clamp(ctx0, up, -limit, limit); diff --git a/src/llama-model-saver.cpp b/src/llama-model-saver.cpp index 66f8bdec3796..59a8ff84f84e 100644 --- a/src/llama-model-saver.cpp +++ b/src/llama-model-saver.cpp @@ -33,6 +33,7 @@ bool llama_model_saver_supports_arch(llm_arch arch) { case LLM_ARCH_LAGUNA: case LLM_ARCH_GRANITE_SWA: case LLM_ARCH_DOTS3NOTE: // TODO: need to handle SWA pattern and MLA+SWA config + case LLM_ARCH_MAPLE: return false; default: return true; diff --git a/src/llama-model.cpp b/src/llama-model.cpp index 6b0557a7b437..6aab7355fde9 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -162,6 +162,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params return new llama_model_mamba(params); case LLM_ARCH_MAMBA2: return new llama_model_mamba2(params); + case LLM_ARCH_MAPLE: + return new llama_model_maple(params); case LLM_ARCH_JAMBA: return new llama_model_jamba(params); case LLM_ARCH_XVERSE: @@ -3025,6 +3027,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) { case LLM_ARCH_SPARK2_5: case LLM_ARCH_TALKIE: case LLM_ARCH_MELLUM: + case LLM_ARCH_MAPLE: return LLAMA_ROPE_TYPE_NEOX; case LLM_ARCH_DFLASH: diff --git a/src/models/dflash.cpp b/src/models/dflash.cpp index da84f30b638a..ed5366d8088d 100644 --- a/src/models/dflash.cpp +++ b/src/models/dflash.cpp @@ -240,21 +240,6 @@ void llama_model_dflash::load_arch_tensors(llama_model_loader &) { } } -std::unique_ptr<llm_graph_context> llama_model_dflash::build_arch_graph(const llm_graph_params & params) const { - switch (params.gtype) { - case LLM_GRAPH_TYPE_ENCODER: - return std::make_unique<graph<true>>(*this, params); - case LLM_GRAPH_TYPE_DEFAULT: - case LLM_GRAPH_TYPE_DECODER: - if (hparams.dsv4_hc_mult > 0) { - return std::make_unique<graph_dsv4>(*this, params); - } - return std::make_unique<graph<false>>(*this, params); - default: - GGML_ABORT("invalid graph type"); - }; -} - template <> ggml_tensor * llama_model_dflash::graph<true>::build_inp_embd_enc() const { const int64_t n_embd_inp = hparams.n_embd_inp_enc(); @@ -999,3 +984,18 @@ llama_model_dflash::graph_dsv4::graph_dsv4(const llama_model & model, const llm_ build_dspark_markov_head(*this, model, inp_tokens); } } + +std::unique_ptr<llm_graph_context> llama_model_dflash::build_arch_graph(const llm_graph_params & params) const { + switch (params.gtype) { + case LLM_GRAPH_TYPE_ENCODER: + return std::make_unique<graph<true>>(*this, params); + case LLM_GRAPH_TYPE_DEFAULT: + case LLM_GRAPH_TYPE_DECODER: + if (hparams.dsv4_hc_mult > 0) { + return std::make_unique<graph_dsv4>(*this, params); + } + return std::make_unique<graph<false>>(*this, params); + default: + GGML_ABORT("invalid graph type"); + }; +} diff --git a/src/models/eagle3.cpp b/src/models/eagle3.cpp index be466056df69..bfde35e43bd1 100644 --- a/src/models/eagle3.cpp +++ b/src/models/eagle3.cpp @@ -100,18 +100,6 @@ void llama_model_eagle3::load_arch_tensors(llama_model_loader &) { } } -std::unique_ptr<llm_graph_context> llama_model_eagle3::build_arch_graph(const llm_graph_params & params) const { - switch (params.gtype) { - case LLM_GRAPH_TYPE_ENCODER: - return std::make_unique<graph<true>>(*this, params); - case LLM_GRAPH_TYPE_DEFAULT: - case LLM_GRAPH_TYPE_DECODER: - return std::make_unique<graph<false>>(*this, params); - default: - GGML_ABORT("invalid graph type"); - }; -} - template <> ggml_tensor * llama_model_eagle3::graph<true>::build_inp_embd_enc() const { ggml_tensor * cur = nullptr; @@ -336,3 +324,15 @@ llama_model_eagle3::graph<false>::graph(const llama_model & model, const llm_gra ggml_build_forward_expand(gf, cur); } + +std::unique_ptr<llm_graph_context> llama_model_eagle3::build_arch_graph(const llm_graph_params & params) const { + switch (params.gtype) { + case LLM_GRAPH_TYPE_ENCODER: + return std::make_unique<graph<true>>(*this, params); + case LLM_GRAPH_TYPE_DEFAULT: + case LLM_GRAPH_TYPE_DECODER: + return std::make_unique<graph<false>>(*this, params); + default: + GGML_ABORT("invalid graph type"); + }; +} diff --git a/src/models/gemma4-assistant.cpp b/src/models/gemma4-assistant.cpp index 8431ec2a1fc7..74d06151e35a 100644 --- a/src/models/gemma4-assistant.cpp +++ b/src/models/gemma4-assistant.cpp @@ -4,7 +4,7 @@ void llama_model_gemma4_assistant::load_arch_hparams(llama_model_loader & ml) { hparams.n_embd_inp_impl = hparams.n_embd_out(); hparams.swa_type = LLAMA_SWA_TYPE_STANDARD; - ml.get_key_or_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl, hparams.n_layer_all); + ml.get_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl); uint32_t n_kv_shared_layers = 0; ml.get_key(LLM_KV_ATTENTION_SHARED_KV_LAYERS, n_kv_shared_layers, false); diff --git a/src/models/gemma4.cpp b/src/models/gemma4.cpp index 39e899aa6e9f..67de74c5471b 100644 --- a/src/models/gemma4.cpp +++ b/src/models/gemma4.cpp @@ -2,7 +2,7 @@ void llama_model_gemma4::load_arch_hparams(llama_model_loader & ml) { hparams.swa_type = LLAMA_SWA_TYPE_STANDARD; - ml.get_key_or_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl, hparams.n_layer()); + ml.get_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl); uint32_t n_kv_shared_layers = 0; ml.get_key(LLM_KV_ATTENTION_SHARED_KV_LAYERS, n_kv_shared_layers, false); diff --git a/src/models/maple.cpp b/src/models/maple.cpp new file mode 100644 index 000000000000..7604b7dfee45 --- /dev/null +++ b/src/models/maple.cpp @@ -0,0 +1,150 @@ +#include "models.h" + +void llama_model_maple::load_arch_hparams(llama_model_loader & ml) { + hparams.swa_type = LLAMA_SWA_TYPE_STANDARD; + + ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); + ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa); + ml.get_key_or_arr(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp_arr, hparams.n_layer_all); + + ml.get_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl); + + hparams.rope_freq_base_train_swa = hparams.rope_freq_base_train; + hparams.rope_freq_scale_train_swa = hparams.rope_freq_scale_train; + ml.get_key(LLM_KV_ROPE_FREQ_BASE_SWA, hparams.rope_freq_base_train_swa, false); + + ml.get_key_or_arr(LLM_KV_SWIGLU_CLAMP_EXP, hparams.swiglu_clamp_exp, hparams.n_layer_all); + + switch (hparams.n_layer()) { + case 24: type = LLM_TYPE_20B; break; + default: type = LLM_TYPE_UNKNOWN; + } +} + +void llama_model_maple::load_arch_tensors(llama_model_loader &) { + LLAMA_LOAD_LOCALS; + + const int64_t n_ff_exp = hparams.n_ff_exp(); + const int64_t head_dim = hparams.n_embd_head_k(); + + tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0); + + output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0); + output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, 0); + + if (n_expert == 0) { + throw std::runtime_error("n_expert must be > 0 for Maple"); + } + if (n_expert_used == 0) { + throw std::runtime_error("n_expert_used must be > 0 for Maple"); + } + + for (int i = 0; i < n_layer; ++i) { + auto & layer = layers[i]; + + layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0); + + create_tensor_qkv(layer, i, n_embd, n_head * head_dim, n_head_kv * head_dim, n_head_kv * head_dim, 0); + layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_head * head_dim, n_embd}, 0); + + layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {head_dim}, 0); + layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {head_dim}, 0); + layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0); + + layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, 0); + layer.ffn_gate_exps = create_tensor(tn(LLM_TENSOR_FFN_GATE_EXPS, "weight", i), {n_embd, n_ff_exp, n_expert}, 0); + layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, 0); + layer.ffn_up_exps = create_tensor(tn(LLM_TENSOR_FFN_UP_EXPS, "weight", i), {n_embd, n_ff_exp, n_expert}, 0); + } +} + +std::unique_ptr<llm_graph_context> llama_model_maple::build_arch_graph(const llm_graph_params & params) const { + return std::make_unique<graph>(*this, params); +} + +llama_model_maple::graph::graph(const llama_model & model, const llm_graph_params & params) : + llm_graph_context(params) { + const int64_t n_embd_head = hparams.n_embd_head_k(); + + GGML_ASSERT(n_embd_head == hparams.n_embd_head_v()); + + ggml_tensor * inpL = build_inp_embd(model.tok_embd); + ggml_tensor * inp_pos = build_inp_pos(); + auto * inp_attn = build_attn_inp_kv_iswa(); + ggml_tensor * inp_out_ids = build_inp_out_ids(); + + for (int il = 0; il < n_layer; ++il) { + ggml_tensor * inpSA = inpL; + + ggml_tensor * cur = build_norm(inpL, model.layers[il].attn_norm, nullptr, LLM_NORM_RMS, il); + cb(cur, "attn_norm", il); + + { + auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur, n_embd_head, n_head, n_head_kv, il); + + Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, nullptr, LLM_NORM_RMS, il); + Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, nullptr, LLM_NORM_RMS, il); + cb(Qcur, "Qcur_normed", il); + cb(Kcur, "Kcur_normed", il); + + if (hparams.is_swa(il)) { + const int64_t n_rot_l = hparams.n_rot(il); + const float freq_base_l = model.get_rope_freq_base(cparams, il); + const float freq_scale_l = model.get_rope_freq_scale(cparams, il); + + Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr, n_rot_l, rope_type, n_ctx_orig, freq_base_l, + freq_scale_l, ext_factor, attn_factor, beta_fast, beta_slow); + Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr, n_rot_l, rope_type, n_ctx_orig, freq_base_l, + freq_scale_l, ext_factor, attn_factor, beta_fast, beta_slow); + } + cb(Qcur, "Qcur", il); + cb(Kcur, "Kcur", il); + cb(Vcur, "Vcur", il); + + cur = build_attn(inp_attn, + model.layers[il].wo, nullptr, model.layers[il].wo_s, + Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, 1.0f / sqrtf(float(n_embd_head)), il); + cb(cur, "attn_out", il); + } + + if (il == n_layer - 1 && inp_out_ids) { + cur = ggml_get_rows(ctx0, cur, inp_out_ids); + inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids); + } + + ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA); + cb(ffn_inp, "ffn_inp", il); + + cur = build_norm(ffn_inp, model.layers[il].ffn_norm, nullptr, LLM_NORM_RMS, il); + cb(cur, "ffn_norm", il); + + cur = build_moe_ffn(cur, + model.layers[il].ffn_gate_inp, + model.layers[il].ffn_up_exps, + model.layers[il].ffn_gate_exps, + model.layers[il].ffn_down_exps, + nullptr, + n_expert, n_expert_used, + LLM_FFN_SILU, true, + 1.0f, + LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX, + il); + cb(cur, "ffn_moe_out", il); + + cur = ggml_add(ctx0, cur, ffn_inp); + cur = build_cvec(cur, il); + cb(cur, "l_out", il); + + inpL = cur; + } + + ggml_tensor * cur = build_norm(inpL, model.output_norm, nullptr, LLM_NORM_RMS, -1); + cb(cur, "result_norm", -1); + res->t_embd = cur; + + cur = build_lora_mm(model.output, cur, model.output_s); + cb(cur, "result_output", -1); + res->t_logits = cur; + + ggml_build_forward_expand(gf, cur); +} diff --git a/src/models/mimo2.cpp b/src/models/mimo2.cpp index 8772319f4611..a466984af01c 100644 --- a/src/models/mimo2.cpp +++ b/src/models/mimo2.cpp @@ -9,7 +9,7 @@ void llama_model_mimo2::load_arch_hparams(llama_model_loader & ml) { ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa); ml.get_key(LLM_KV_ROPE_FREQ_BASE_SWA, hparams.rope_freq_base_train_swa, false); - ml.get_key_or_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl, hparams.n_layer()); + ml.get_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl); float value_scale = 0.0f; if (ml.get_key(LLM_KV_ATTENTION_VALUE_SCALE, value_scale, false) && value_scale != 1.0f) { diff --git a/src/models/models.h b/src/models/models.h index 9ab189f16fd8..bc4238926a6f 100644 --- a/src/models/models.h +++ b/src/models/models.h @@ -945,6 +945,19 @@ struct llama_model_mamba2 : public llama_model_base { }; +struct llama_model_maple : public llama_model_base { + llama_model_maple(const struct llama_model_params & params) : llama_model_base(params) {} + void load_arch_hparams(llama_model_loader & ml) override; + void load_arch_tensors(llama_model_loader & ml) override; + + struct graph : public llm_graph_context { + graph(const llama_model & model, const llm_graph_params & params); + }; + + std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override; +}; + + struct llama_model_jamba : public llama_model_base { llama_model_jamba(const struct llama_model_params & params) : llama_model_base(params) {} void load_arch_hparams(llama_model_loader & ml) override; diff --git a/src/models/nemotron-h.cpp b/src/models/nemotron-h.cpp index d2c48f125ee2..ff8784d1828a 100644 --- a/src/models/nemotron-h.cpp +++ b/src/models/nemotron-h.cpp @@ -145,8 +145,14 @@ void llama_model_nemotron_h::load_arch_tensors(llama_model_loader & ml) { const int64_t n_head_i = hparams.n_head(i); const int64_t n_embd_k_gqa_i = hparams.n_embd_k_gqa(i); const int64_t n_embd_v_gqa_i = hparams.n_embd_v_gqa(i); - const int64_t n_ff_exp = hparams.n_ff_exp(i) ? (int64_t)hparams.n_ff_exp(i) : n_ff / (int64_t)hparams.n_expert_used(i); - const int64_t n_ff_shexp = hparams.n_ff_shexp; + const int64_t n_expert_used_i = hparams.n_expert_used(i); + const int64_t n_ff_exp_i = hparams.n_ff_exp(i); + if (n_ff_exp_i == 0 && n_expert_used_i == 0) { + throw std::runtime_error(format("%s: layer %d declares neither expert_feed_forward_length nor expert_used_count, " + "cannot determine the expert FFN size", __func__, i)); + } + const int64_t n_ff_exp = n_ff_exp_i ? n_ff_exp_i : n_ff / n_expert_used_i; + const int64_t n_ff_shexp = hparams.n_ff_shexp; // NextN input-fusion tensors layer.nextn.enorm = create_tensor(tn(LLM_TENSOR_NEXTN_ENORM, "weight", i), {n_embd}, mtp_flags); diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 632cd46adbb8..0a378ca14fbb 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -160,7 +160,8 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), { n_embd, n_vocab }, 0); // there is no output_norm: the final hyper-connection mixer carries it - hc_head_norm = create_tensor(tn(LLM_TENSOR_HC_HEAD_NORM, "weight"), { hc_dim }, 0); + // the gammas load as [n_embd, hc] so the grouped norm multiplies them without a graph reshape + hc_head_norm = create_tensor(tn(LLM_TENSOR_HC_HEAD_NORM, "weight"), { n_embd, hc }, TENSOR_ALLOW_RESHAPE); hc_head_down = create_tensor(tn(LLM_TENSOR_HC_HEAD_DOWN, "weight"), { hc_dim, hc_lr }, 0); hc_head_up = create_tensor(tn(LLM_TENSOR_HC_HEAD_UP, "weight"), { hc_lr, hc_dim }, 0); @@ -229,11 +230,11 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { const int64_t conv_dim = key_dim * 2 + value_dim; // two HC modules per layer: before the token mixer, before the MoE - layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { hc_dim }, trunk_flags); + layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE | trunk_flags); layer.hc_attn_down = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN, "weight", il), { hc_dim, hc_lr }, trunk_flags); layer.hc_attn_up = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP, "weight", il), { hc_lr, hc_dim }, trunk_flags); layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, trunk_flags); - layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { hc_dim }, trunk_flags); + layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE | trunk_flags); layer.hc_ffn_down = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN, "weight", il), { hc_dim, hc_lr }, trunk_flags); layer.hc_ffn_up = create_tensor(tn(LLM_TENSOR_HC_FFN_UP, "weight", il), { hc_lr, hc_dim }, trunk_flags); layer.hc_ffn_inject = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT, "weight", il), { hc_dim, hc }, trunk_flags); @@ -266,9 +267,9 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { if (hparams.is_ple(il)) { layer.ple_key = create_tensor(tn(LLM_TENSOR_PLE_KEY, "weight", il), { n_embd, hc_dim }, trunk_flags); layer.ple_value = create_tensor(tn(LLM_TENSOR_PLE_VALUE, "weight", il), { n_embd, n_embd }, trunk_flags); - layer.ple_norm_key = create_tensor(tn(LLM_TENSOR_PLE_NORM_KEY, "weight", il), { hc_dim }, trunk_flags); - layer.ple_norm_query = create_tensor(tn(LLM_TENSOR_PLE_NORM_QUERY, "weight", il), { hc_dim }, trunk_flags); - layer.ple_norm_conv = create_tensor(tn(LLM_TENSOR_PLE_NORM_CONV, "weight", il), { hc_dim }, trunk_flags); + layer.ple_norm_key = create_tensor(tn(LLM_TENSOR_PLE_NORM_KEY, "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE | trunk_flags); + layer.ple_norm_query = create_tensor(tn(LLM_TENSOR_PLE_NORM_QUERY, "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE | trunk_flags); + layer.ple_norm_conv = create_tensor(tn(LLM_TENSOR_PLE_NORM_CONV, "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE | trunk_flags); layer.ple_conv1d = create_tensor(tn(LLM_TENSOR_PLE_CONV1D, "weight", il), { hparams.ple_conv_kernel, hc_dim }, trunk_flags); } @@ -292,11 +293,11 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { const int64_t n_ff_shexp = hparams.n_ff_shexp ? hparams.n_ff_shexp : n_ff; const int64_t idx_dim = hparams.indexer_head_size; - layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { hc_dim }, flags); + layer.hc_attn_norm = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM, "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE | flags); layer.hc_attn_down = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN, "weight", il), { hc_dim, hc_lr }, flags); layer.hc_attn_up = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP, "weight", il), { hc_lr, hc_dim }, flags); layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, flags); - layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { hc_dim }, flags); + layer.hc_ffn_norm = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM, "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE | flags); layer.hc_ffn_down = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN, "weight", il), { hc_dim, hc_lr }, flags); layer.hc_ffn_up = create_tensor(tn(LLM_TENSOR_HC_FFN_UP, "weight", il), { hc_lr, hc_dim }, flags); layer.hc_ffn_inject = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT, "weight", il), { hc_dim, hc }, flags); @@ -460,11 +461,10 @@ ggml_tensor * llama_model_qwen4exp::graph::build_hc_mix( const int64_t hc_dim = hc * n_embd; const int64_t nt = x->ne[2]; - // grouped RMSNorm: reduce over one stream, then scale all streams with the [hc_dim] gamma + // grouped RMSNorm: reduce over one stream, then scale all streams with the [n_embd, hc] gamma // the converter folded each gamma to (1 + w) - ggml_tensor * xn = ggml_rms_norm(ctx0, x, hparams.f_norm_rms_eps); + ggml_tensor * xn = ggml_mul(ctx0, ggml_rms_norm(ctx0, x, hparams.f_norm_rms_eps), w_norm); xn = ggml_reshape_2d(ctx0, xn, hc_dim, nt); - xn = ggml_mul(ctx0, xn, w_norm); cb(xn, "hc_norm", il); ggml_tensor * lo = build_lora_mm(w_down, xn); @@ -1453,13 +1453,10 @@ ggml_tensor * llama_model_qwen4exp::graph::build_ple( ggml_tensor * key = build_lora_mm(model.layers[il].ple_key, emb); ggml_tensor * value = build_lora_mm(model.layers[il].ple_value, emb); - // both norms group over one hc stream, with a weight over the whole hc*n_embd layout + // both norms group over one hc stream, with a [n_embd, hc] weight auto grouped_norm = [&](ggml_tensor * x, ggml_tensor * w) { ggml_tensor * t = ggml_reshape_3d(ctx0, x, n_embd, hc, n_tokens); - t = ggml_rms_norm(ctx0, t, hparams.f_norm_rms_eps); - t = ggml_reshape_2d(ctx0, t, hc_dim, n_tokens); - t = ggml_mul(ctx0, t, w); - return ggml_reshape_3d(ctx0, t, n_embd, hc, n_tokens); + return ggml_mul(ctx0, ggml_rms_norm(ctx0, t, hparams.f_norm_rms_eps), w); }; key = grouped_norm(key, model.layers[il].ple_norm_key); diff --git a/src/models/step35.cpp b/src/models/step35.cpp index 946a3696000f..ca68855d8dab 100644 --- a/src/models/step35.cpp +++ b/src/models/step35.cpp @@ -23,7 +23,7 @@ void llama_model_step35::load_arch_hparams(llama_model_loader & ml) { ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa); ml.get_key(LLM_KV_ROPE_FREQ_BASE_SWA, hparams.rope_freq_base_train_swa, false); - ml.get_key_or_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl, hparams.n_layer_all); + ml.get_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl); ml.get_key_or_arr(LLM_KV_SWIGLU_CLAMP_EXP, hparams.swiglu_clamp_exp, hparams.n_layer_all, false); ml.get_key_or_arr(LLM_KV_SWIGLU_CLAMP_SHEXP, hparams.swiglu_clamp_shexp, hparams.n_layer_all, false); diff --git a/src/models/t5.cpp b/src/models/t5.cpp index b0e3f062572f..e2bf12b6b151 100644 --- a/src/models/t5.cpp +++ b/src/models/t5.cpp @@ -106,18 +106,6 @@ void llama_model_t5::load_arch_tensors(llama_model_loader &) { } } -std::unique_ptr<llm_graph_context> llama_model_t5::build_arch_graph(const llm_graph_params & params) const { - switch (params.gtype) { - case LLM_GRAPH_TYPE_ENCODER: - return std::make_unique<graph<true>>(*this, params); - case LLM_GRAPH_TYPE_DEFAULT: - case LLM_GRAPH_TYPE_DECODER: - return std::make_unique<graph<false>>(*this, params); - default: - GGML_ABORT("invalid graph type"); - }; -} - template <> llama_model_t5::graph<false>::graph(const llama_model & model, const llm_graph_params & params) : llm_graph_context(params) { const int64_t n_embd_head = hparams.n_embd_head_v(); @@ -368,3 +356,15 @@ llama_model_t5::graph<true>::graph(const llama_model & model, const llm_graph_pa ggml_build_forward_expand(gf, cur); } + +std::unique_ptr<llm_graph_context> llama_model_t5::build_arch_graph(const llm_graph_params & params) const { + switch (params.gtype) { + case LLM_GRAPH_TYPE_ENCODER: + return std::make_unique<graph<true>>(*this, params); + case LLM_GRAPH_TYPE_DEFAULT: + case LLM_GRAPH_TYPE_DECODER: + return std::make_unique<graph<false>>(*this, params); + default: + GGML_ABORT("invalid graph type"); + }; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fde48492b9d9..99885d4274bb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -163,11 +163,7 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS) llama_build_and_test(test-chat.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) target_include_directories(test-chat PRIVATE ${PROJECT_SOURCE_DIR}/tools/server) target_link_libraries(test-chat PRIVATE server-context) - # TODO: disabled on loongarch64 because the ggml-ci node lacks Python 3.8 - if (NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "loongarch64") - llama_build_and_test(test-json-schema-to-grammar.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) - target_include_directories(test-json-schema-to-grammar PRIVATE ${PROJECT_SOURCE_DIR}/tools/server) - endif() + llama_build_and_test(test-json-schema-to-grammar.cpp) if (NOT GGML_BACKEND_DL) llama_build(test-quantize-stats.cpp) @@ -262,6 +258,7 @@ endif() llama_build_and_test(test-chat-peg-parser.cpp peg-parser/simple-tokenize.cpp) llama_build_and_test(test-jinja.cpp) llama_test(test-jinja NAME test-jinja-py ARGS -py LABEL python) +llama_build_and_test(test-json-schema.cpp) llama_build_and_test(test-chat-auto-parser.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) llama_build_and_test(test-chat-template.cpp) # debug tool for chat template differential analysis (not registered as a test, run it manually) @@ -278,8 +275,6 @@ llama_build_and_test( peg-parser/test-unicode.cpp peg-parser/tests.h ) -target_precompile_headers(test-peg-parser PRIVATE peg-parser/tests.h) - if (NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "s390x") set(MODEL_NAME "tinyllamas/stories15M-q4_0.gguf") @@ -294,7 +289,7 @@ add_test(NAME test-download-model COMMAND ${CMAKE_COMMAND} -DDEST=${MODEL_DEST} -DNAME=${MODEL_NAME} -DHASH=${MODEL_HASH} - -P ${CMAKE_SOURCE_DIR}/cmake/download-models.cmake + -P ${PROJECT_SOURCE_DIR}/cmake/download-models.cmake ) set_tests_properties(test-download-model PROPERTIES FIXTURES_SETUP test-download-model) diff --git a/tests/fusion/README.md b/tests/fusion/README.md new file mode 100644 index 000000000000..3ac02b061e9a --- /dev/null +++ b/tests/fusion/README.md @@ -0,0 +1,26 @@ +# Fusion baselines + +Per-device baselines for `test-fusion`, one CSV per backend (e.g. `MTL.csv`). Rows are +`arch,moe,mode,label,count`. Regenerate a CSV whenever fusion patterns change. + +## Update a baseline + +```sh +cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_METAL=ON # enable the target backend +cmake --build build --config Release --target test-llama-archs --target test-fusion -j + +rm -rf build-ci-models && mkdir -p build-ci-models +./build/bin/test-llama-archs -o build-ci-models + +./build/bin/test-fusion --models build-ci-models --device MTL0 --record MTL.csv +``` + +## Validate + +```sh +./build/bin/test-fusion --models build-ci-models --device MTL0 --check MTL.csv +``` + +Non-zero exit means a row differs from the baseline. Use `--model FILE` to run a single +architecture. Note `--check` only sees present rows — a fusion that stops matching is not +reported, so diff the recorded CSV to catch removed patterns. diff --git a/tests/snapshots/nemotron-nano-3-30b-a3b.schema b/tests/snapshots/nvidia-nemotron-3-nano-30b-a3b.schema similarity index 100% rename from tests/snapshots/nemotron-nano-3-30b-a3b.schema rename to tests/snapshots/nvidia-nemotron-3-nano-30b-a3b.schema diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 1472efdd519e..27adea031cfe 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -7218,20 +7218,32 @@ struct test_sum_rows : public test_case { struct test_mean : public test_case { const ggml_type type; const std::array<int64_t, 4> ne; + const bool permute; + const bool slice; std::string vars() override { - return VARS_TO_STR2(type, ne); + return VARS_TO_STR4(type, ne, permute, slice); } test_mean(ggml_type type = GGML_TYPE_F32, - std::array<int64_t, 4> ne = {10, 5, 4, 3}) - : type(type), ne(ne) {} + std::array<int64_t, 4> ne = {10, 5, 4, 3}, + bool permute = false, bool slice = false) + : type(type), ne(ne), permute(permute), slice(slice) {} ggml_tensor * build_graph(ggml_context * ctx) override { ggml_tensor * a = ggml_new_tensor(ctx, type, 4, ne.data()); ggml_set_param(a); ggml_set_name(a, "a"); + if (slice) { + a = ggml_view_4d(ctx, a, + ne[0], ne[1], ne[2] / 2, ne[3] - 1, + a->nb[1], a->nb[2] * 2, a->nb[3], /*offset=*/a->nb[3]); + } + if (permute) { + a = ggml_permute(ctx, a, 0, 2, 3, 1); + } + ggml_tensor * out = ggml_mean(ctx, a); ggml_set_name(out, "out"); @@ -10579,6 +10591,9 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_mean(GGML_TYPE_F32, { 32, 1, 1, 1 })); test_cases.emplace_back(new test_mean(GGML_TYPE_F32, { 32, 256, 1, 1 })); test_cases.emplace_back(new test_mean(GGML_TYPE_F32, { 32768, 1, 1, 1 })); + test_cases.emplace_back(new test_mean(GGML_TYPE_F32, { 11, 5, 6, 3 }, true, false)); + test_cases.emplace_back(new test_mean(GGML_TYPE_F32, { 11, 5, 6, 3 }, false, true)); + test_cases.emplace_back(new test_mean(GGML_TYPE_F32, { 11, 5, 6, 3 }, true, true)); test_cases.emplace_back(new test_sum(GGML_TYPE_F32, { 33, 1, 1, 1 })); test_cases.emplace_back(new test_sum(GGML_TYPE_F32, { 33, 1024, 1, 1 })); test_cases.emplace_back(new test_sum(GGML_TYPE_F32, { 33, 256, 1, 1 })); @@ -10711,7 +10726,8 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { for (int hsk : { 40, 64, 72, 80, 96, 128, 192, 256, 320, 512, 576 }) { for (int hsv : { 40, 64, 72, 80, 96, 128, 192, 256, 512 }) { - if (hsk != 192 && hsk != 320 && hsk != 576 && hsk != hsv) continue; + if (hsk != 96 && hsk != 192 && hsk != 320 && hsk != 576 && hsk != hsv) continue; + if (hsk == 96 && (hsv != 64 && hsv != 96)) continue; // MiniCPM3 if (hsk == 192 && (hsv != 128 && hsv != 192)) continue; if (hsk == 576 && hsv != 512) continue; // DeepSeek MLA if (hsk == 320 && hsv != 256) continue; // Mistral4 MLA @@ -10787,9 +10803,9 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext(256, 256, 2, {16, 1}, 16384, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0)); // MLA shape: the V cache is a sub-view of the K cache, with quantized KV - test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {20, 1}, 113, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true)); - test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {20, 1}, 1024, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true)); - test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {20, 1}, 1024, 64, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true)); + test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {8, 1}, 113, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true)); + test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {8, 1}, 1024, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true)); + test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {8, 1}, 1024, 64, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true)); // Sparse mask hint: supported decode/prefill layouts and dense fallbacks. test_cases.emplace_back(new test_flash_attn_ext(512, 512, 1, { 8, 1}, 4096, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 1, 2, 3}, true, false, 512)); @@ -10815,6 +10831,9 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_flash_attn_ext(128, 128, 1, { 8, 1}, 4096, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, false, 512)); test_cases.emplace_back(new test_flash_attn_ext(128, 128, 1, { 8, 1}, 4096, 64, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, false, 512)); + // Qwen QSA: 256/256, gqa 12, budget 2048. + test_cases.emplace_back(new test_flash_attn_ext(256, 256, 2, {12, 1}, 8192, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 1, 2, 3}, true, false, 2048)); + // more V-is-sub-view-of-K cases: other head shapes, and full views with equal head sizes test_cases.emplace_back(new test_flash_attn_ext(320, 256, 1, {32, 1}, 512, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 1, 2, 3}, true, true)); test_cases.emplace_back(new test_flash_attn_ext(192, 128, 4, {8, 1}, 512, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 1, 2, 3}, true, true)); @@ -11282,6 +11301,14 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() { // Qwen3-VL-8B https://github.com/ggml-org/llama.cpp/issues/17012 test_cases.emplace_back(new test_flash_attn_ext(72, 72, 16, {1, 1}, 5776, 5776, false, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); + // Sparse flash attention (n_kv_max hint) decode across KV depths. + // Shapes: 576/512 DeepSeek MLA, 512/512 DeepSeek-V4/GLM-5.2, 256/256 gqa12 Qwen QSA. + for (int64_t kv : {4096, 16384, 32768}) { + test_cases.emplace_back(new test_flash_attn_ext(512, 512, 1, { 8, 1}, kv, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 1, 2, 3}, true, false, 512)); + test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {16, 1}, kv, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 1, 2, 3}, true, true, 512)); + test_cases.emplace_back(new test_flash_attn_ext(256, 256, 2, {12, 1}, kv, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 1, 2, 3}, true, false, 2048)); + } + test_cases.emplace_back(new test_flash_attn_ext(64, 64, 8, {8, 1}, 7680, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); test_cases.emplace_back(new test_flash_attn_ext(64, 64, 8, {8, 1}, 7680, 4, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16)); test_cases.emplace_back(new test_flash_attn_ext(64, 64, 8, {8, 1}, 7680, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q4_0, GGML_TYPE_Q4_0)); @@ -11416,6 +11443,30 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() { } } + // qwen4exp sparse-attention indexer: nrows = n_tokens/n_stream, so tg gives nrows==1. + // Sweep nrows to expose how much of the device a single row leaves idle. + for (auto cols : {8192, 32768, 131072}) { + for (auto nrows : {1, 2, 4, 8, 16, 32}) { + test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, nrows, 1, 1}, 2048)); + } + } + // backend sampler: one row of the vocab (llama-sampler.cpp top_k) + for (auto k : {20, 40}) { + test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {151936, 1, 1, 1}, k)); + } + + // short rows, many of them: MoE routing and group selection. The opposite corner from + // the indexer, and the one where a work-group per row is the wasteful choice. + for (auto cols : {2, 16, 128, 1024}) { + for (auto nrows : {1024, 8192}) { + for (auto k : {1, 2, 8, 16, 32}) { + if (k <= cols) { + test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, nrows, 1, 1}, k)); + } + } + } + } + for (auto nrows : {1, 4, 8, 16}) { for (auto cols : {128, 1024, 4096, 8192, 16384, 32768, 65536, 131072, 200000, 2000000}) { test_cases.emplace_back(new test_cumsum(GGML_TYPE_F32, {cols, nrows, 1, 1})); diff --git a/tests/test-chat-peg-parser.cpp b/tests/test-chat-peg-parser.cpp index 793891394ce6..9d15796f7aef 100644 --- a/tests/test-chat-peg-parser.cpp +++ b/tests/test-chat-peg-parser.cpp @@ -358,11 +358,6 @@ static void test_example_native(testing & t) { auto parser = build_parser(tc); auto lazy = !tc.tools.empty() && tc.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED; auto grammar = build_grammar([&](const common_grammar_builder & builder) { - for (const auto & def : tc.tools) { - auto function = def.at("function"); - auto parameters = function.at("parameters"); - builder.resolve_refs(parameters); - }; parser.build_grammar(builder, lazy); }); @@ -440,11 +435,6 @@ static void test_example_qwen3_coder(testing & t) { }); auto grammar = build_grammar([&](const common_grammar_builder & builder) { - for (const auto & def : tools) { - auto function = def.at("function"); - auto parameters = function.at("parameters"); - builder.resolve_refs(parameters); - }; parser.build_grammar(builder); }); @@ -513,11 +503,6 @@ static void test_example_qwen3_non_coder(testing & t) { }); auto grammar = build_grammar([&](const common_grammar_builder & builder) { - for (const auto & def : tools) { - auto function = def.at("function"); - auto parameters = function.at("parameters"); - builder.resolve_refs(parameters); - }; parser.build_grammar(builder); }); diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index c3106e480acb..f7932ee26f6d 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -472,6 +472,12 @@ static common_chat_tool empty_args_tool_no_properties{ })", }; +static common_chat_tool empty_args_tool_no_schema{ + /* .name = */ "empty_args_no_schema", + /* .description = */ "A tool that takes no arguments and has no parameters schema", + /* .parameters = */ "{}", +}; + static common_chat_tool python_tool{ /* .name = */ "python", /* .description = */ "an ipython interpreter", @@ -840,6 +846,25 @@ static common_chat_tool nullable_int_tool{ })", }; +static common_chat_tool string_union_tool{ + /* .name = */ "set_union", + /* .description = */ "Set values whose types are unions with string", + /* .parameters = */ R"({ + "type": "object", + "properties": { + "value": { + "type": ["string", "object"], + "description": "A string or object value" + }, + "amount": { + "type": ["string", "integer"], + "description": "A string or integer value" + } + }, + "required": ["value", "amount"] + })", +}; + static common_chat_tool enum_no_type_tool{ /* .name = */ "set_unit", /* .description = */ "Set a temperature unit", @@ -3799,6 +3824,46 @@ static void test_template_output_peg_parsers(bool detailed_debug) { }) .run(); + // nullable string given null - parses as JSON null, not the string "null" + tst.test( + "<tool_call>\n" + "<function=set_nullable_str>\n" + "<parameter=name>\nnull\n</parameter>\n" + "</function>\n" + "</tool_call>") + .tools({ nullable_string_tool }) + .expect_tool_calls({ + { "set_nullable_str", R"({"name": null})", {} }, + }) + .run(); + + // unions with string - JSON values of the other types are typed, everything else is a string + tst.test( + "<tool_call>\n" + "<function=set_union>\n" + "<parameter=value>\n{\"a\": 1}\n</parameter>\n" + "<parameter=amount>\n2 dollars\n</parameter>\n" + "</function>\n" + "</tool_call>") + .tools({ string_union_tool }) + .expect_tool_calls({ + { "set_union", R"({"value": {"a": 1}, "amount": "2 dollars"})", {} }, + }) + .run(); + + tst.test( + "<tool_call>\n" + "<function=set_union>\n" + "<parameter=value>\n{not valid json\n</parameter>\n" + "<parameter=amount>\n42\n</parameter>\n" + "</function>\n" + "</tool_call>") + .tools({ string_union_tool }) + .expect_tool_calls({ + { "set_union", R"({"value": "{not valid json", "amount": 42})", {} }, + }) + .run(); + // enum without explicit type key - should infer string from enum values tst.test( "<tool_call>\n" @@ -5071,6 +5136,13 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .expect(simple_assist_msg("", "", "empty_args", "{}")) .run(); + // Tool call with no parameters schema, {} means no arguments + tst.test("<tool_call>\n{\"name\": \"empty_args_no_schema\", \"arguments\": {}}</tool_call>") + .enable_thinking(false) + .tools({ empty_args_tool_no_schema }) + .expect(simple_assist_msg("", "", "empty_args_no_schema", "{}")) + .run(); + // fake tool call marker in reasoning tst.test( "Let me think about <tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 2}}</tool_call> hmm\n</think>\n\n" diff --git a/tests/test-grammar-integration.cpp b/tests/test-grammar-integration.cpp index eb4b7c78f50f..2af96f8b4797 100644 --- a/tests/test-grammar-integration.cpp +++ b/tests/test-grammar-integration.cpp @@ -918,7 +918,7 @@ static void test_json_schema() { // Otherwise, this test structure is the same. test_schema( - "empty schema (object)", + "empty schema (any value)", // Schema R"""( {} @@ -927,14 +927,16 @@ static void test_json_schema() { { R"""({})""", R"""({"foo": "bar"})""", - }, - // Failing strings - { - "", "[]", "null", R"""("")""", "true", + }, + // Failing strings + { + "", + R"""({"foo"})""", + "foo", } ); diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index ab551d7b38fd..00de91ddf93e 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -398,6 +398,18 @@ static void test_expressions(testing & t) { "Bob" ); + test_template(t, "dot notation (integer property)", + "{{ {10: 'Bob'}.10 }}", + json::object(), + "Bob" + ); + + test_template(t, "dot notation (array index)", + "{{ user.10 }}", + {{"user", json::array({"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"})}}, + "k" + ); + test_template(t, "negative float (not dot notation)", "{{ -1.0 }}", json::object(), diff --git a/tests/test-json-schema-to-grammar.cpp b/tests/test-json-schema-to-grammar.cpp index 2a1b6348c951..4c4206c6e690 100755 --- a/tests/test-json-schema-to-grammar.cpp +++ b/tests/test-json-schema-to-grammar.cpp @@ -9,8 +9,6 @@ #include "json.h" #include <cassert> -#include <fstream> -#include <sstream> #include <regex> static std::string trim(const std::string & source) { @@ -64,21 +62,8 @@ struct TestCase { } }; -static void write(const std::string & file, const std::string & content) { - std::ofstream f; - f.open(file.c_str()); - f << content.c_str(); - f.close(); -} - -static std::string read(const std::string & file) { - std::ostringstream actuals; - actuals << std::ifstream(file.c_str()).rdbuf(); - return actuals.str(); -} - -static void test_all(const std::string & lang, std::function<void(const TestCase &)> runner) { - fprintf(stderr, "#\n# Testing JSON schema conversion (%s)\n#\n", lang.c_str()); +static void test_all(const std::string & title, std::function<void(const TestCase &)> runner) { + fprintf(stderr, "#\n# %s\n#\n", title.c_str()); auto test = [&](const TestCase & tc) { fprintf(stderr, "- %s%s\n", tc.name.c_str(), tc.expected_status == FAILURE ? " (failure expected)" : ""); runner(tc); @@ -330,7 +315,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase test({ SUCCESS, - "empty schema (object)", + "empty schema (any value)", "{}", R"""( array ::= "[" space ( value ("," space value)* )? space "]" @@ -341,7 +326,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" - root ::= object + root ::= value space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null @@ -569,6 +554,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase )""" }); + // items {} constrains nothing, the same as no items at all test({ SUCCESS, "array with empty items", @@ -582,11 +568,10 @@ static void test_all(const std::string & lang, std::function<void(const TestCase char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} - item ::= object null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" - root ::= "[" space (item ("," space item)*)? space "]" + root ::= "[" space ( value ("," space value)* )? space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null @@ -607,11 +592,10 @@ static void test_all(const std::string & lang, std::function<void(const TestCase char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} - item ::= object null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" - root ::= "[" space (item ("," space item)*)? space "]" + root ::= "[" space ( value ("," space value)* )? space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null @@ -1434,88 +1418,100 @@ static void test_all(const std::string & lang, std::function<void(const TestCase space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); -} -static void test_resolves_to_string() { - fprintf(stderr, "#\n# Testing resolves_to_string\n#\n"); - - auto test = [](const std::string & name, const std::string & schema_str, bool expected) { - fprintf(stderr, "- %s\n", name.c_str()); - common_schema_info info; - auto schema = common_json::parse(schema_str); - info.resolve_refs(schema); - bool result = info.resolves_to_string(schema); - if (result != expected) { - fprintf(stderr, "#\n# Test '%s' failed.\n#\n", name.c_str()); - fprintf(stderr, "Schema: %s\n", schema_str.c_str()); - fprintf(stderr, "Expected: %s, Got: %s\n", expected ? "true" : "false", result ? "true" : "false"); - assert(false); - } - }; + test({ + SUCCESS, + "regexp with non-capturing group", + R"""({ + "type": "string", + "pattern": "^(?:foo|bar)baz$" + })""", + R"""( + root ::= "\"" (("foo" | "bar") "baz") "\"" + space ::= | " " | "\n"{1,2} [ \t]{0,20} + )""" + }); - // Basic type checks - test("type string", R"({"type": "string"})", true); - test("type integer", R"({"type": "integer"})", false); - test("type number", R"({"type": "number"})", false); - test("type boolean", R"({"type": "boolean"})", false); - test("type object", R"({"type": "object"})", false); - test("type array", R"({"type": "array"})", false); - - // Type array (nullable string) - test("type array with string", R"({"type": ["string", "null"]})", true); - test("type array without string", R"({"type": ["integer", "null"]})", false); - - // String-specific keywords - test("minLength implies string", R"({"minLength": 1})", true); - test("maxLength implies string", R"({"maxLength": 10})", true); - test("pattern implies string", R"({"pattern": "^[a-z]+$"})", true); - - // Format - test("format date", R"({"format": "date"})", true); - test("format uuid", R"({"format": "uuid"})", true); - test("format email", R"({"format": "email"})", true); - - // Const - test("const string", R"({"const": "hello"})", true); - test("const number", R"({"const": 123})", false); - - // Enum - test("enum with strings", R"({"enum": ["a", "b", "c"]})", true); - test("enum with numbers", R"({"enum": [1, 2, 3]})", false); - test("enum mixed with string", R"({"enum": [1, "a", null]})", true); - - // anyOf - test("anyOf with string", R"({"anyOf": [{"type": "string"}, {"type": "integer"}]})", true); - test("anyOf without string", R"({"anyOf": [{"type": "integer"}, {"type": "boolean"}]})", false); - - // oneOf - test("oneOf with string", R"({"oneOf": [{"type": "string"}, {"type": "number"}]})", true); - test("oneOf without string", R"({"oneOf": [{"type": "object"}, {"type": "array"}]})", false); - - // allOf - all must be strings - test("allOf all strings", R"({"allOf": [{"type": "string"}, {"minLength": 1}]})", true); - test("allOf mixed types", R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", false); - - // $ref - test("$ref to string", - R"({"$ref": "#/$defs/str", "$defs": {"str": {"type": "string"}}})", true); - test("$ref to integer", - R"({"$ref": "#/$defs/num", "$defs": {"num": {"type": "integer"}}})", false); - - // Nested - test("nested anyOf with string", - R"({"anyOf": [{"anyOf": [{"type": "integer"}, {"type": "string"}]}, {"type": "boolean"}]})", true); - - fprintf(stderr, "All resolves_to_string tests passed!\n"); -} + test({ + SUCCESS, + "regexp with nested non-capturing groups", + R"""({ + "type": "string", + "pattern": "^(?:(?:ab)+c)?d$" + })""", + R"""( + root ::= "\"" ((("ab")+ "c")? "d") "\"" + space ::= | " " | "\n"{1,2} [ \t]{0,20} + )""" + }); -int main() { - fprintf(stderr, "LLAMA_NODE_AVAILABLE = %s\n", getenv("LLAMA_NODE_AVAILABLE") ? "true" : "false"); - fprintf(stderr, "LLAMA_PYTHON_AVAILABLE = %s\n", getenv("LLAMA_PYTHON_AVAILABLE") ? "true" : "false"); + test({ + SUCCESS, + "unanchored regexp", + R"""({ + "type": "string", + "pattern": "[0-9]+" + })""", + R"""( + char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) + root ::= string + space ::= | " " | "\n"{1,2} [ \t]{0,20} + string ::= "\"" char* "\"" + )""" + }); + + // the rules of the partial conversion (here "root-0") must not leak into the grammar + test({ + SUCCESS, + "regexp with unsupported shorthand", + R"""({ + "type": "string", + "pattern": "^[0-9]{3}\\w$" + })""", + R"""( + char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) + root ::= string + space ::= | " " | "\n"{1,2} [ \t]{0,20} + string ::= "\"" char* "\"" + )""" + }); - test_resolves_to_string(); + // a regexp that is invalid under any flavor is still an error + test({ + FAILURE, + "regexp with unbalanced parentheses", + R"""({ + "type": "string", + "pattern": "^(a$" + })""", + "" + }); - test_all("C++", [](const TestCase & tc) { + // only the property with the bad pattern degrades + test({ + SUCCESS, + "unsupported regexp in a property", + R"""({ + "type": "object", + "properties": { + "a": { "type": "string", "pattern": "^[a-z\\-]+$" } + }, + "required": ["a"], + "additionalProperties": false + })""", + R"""( + a ::= string + a-kv ::= "\"a\"" space ":" space a + char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) + root ::= "{" space a-kv space "}" + space ::= | " " | "\n"{1,2} [ \t]{0,20} + string ::= "\"" char* "\"" + )""" + }); +} + +int main() { + test_all("JSON schema conversion", [](const TestCase & tc) { try { tc.verify(json_schema_to_grammar(common_json::parse(tc.schema), true)); tc.verify_status(SUCCESS); @@ -1525,127 +1521,58 @@ int main() { } }); - // C++ only tests (features not yet supported in JS/Python implementations) + // a document parsed up front gives the same grammar as the JSON, recursion included { - fprintf(stderr, "#\n# Testing C++ only features\n#\n"); - auto run = [](const TestCase & tc) { - fprintf(stderr, "- %s\n", tc.name.c_str()); - try { - tc.verify(json_schema_to_grammar(common_json::parse(tc.schema), true)); - tc.verify_status(SUCCESS); - } catch (const std::invalid_argument & ex) { - fprintf(stderr, "Error: %s\n", ex.what()); - tc.verify_status(FAILURE); + fprintf(stderr, "- parsed document\n"); + auto schema = common_json::parse(R"""({ + "$ref": "#/$defs/node", + "$defs": { + "node": { + "type": "object", + "properties": {"next": {"$ref": "#/$defs/node"}, "leaf": {}}, + "additionalProperties": false + } } - }; - - run({ - SUCCESS, - "regexp with non-capturing group", - R"""({ - "type": "string", - "pattern": "^(?:foo|bar)baz$" - })""", - R"""( - root ::= "\"" (("foo" | "bar") "baz") "\"" - space ::= | " " | "\n"{1,2} [ \t]{0,20} - )""", - }); - - run({ - SUCCESS, - "regexp with nested non-capturing groups", - R"""({ - "type": "string", - "pattern": "^(?:(?:ab)+c)?d$" - })""", - R"""( - root ::= "\"" ((("ab")+ "c")? "d") "\"" - space ::= | " " | "\n"{1,2} [ \t]{0,20} - )""", - }); - - run({ - SUCCESS, - "unanchored regexp", - R"""({ - "type": "string", - "pattern": "[0-9]+" - })""", - R"""( - char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) - root ::= string - space ::= | " " | "\n"{1,2} [ \t]{0,20} - string ::= "\"" char* "\"" - )""", - }); + })"""); + assert(json_schema_to_grammar(common_chat_schema_from_json(schema)) == json_schema_to_grammar(schema, true)); + } - // the rules of the partial conversion (here "root-0") must not leak into the grammar - run({ - SUCCESS, - "regexp with unsupported shorthand", - R"""({ - "type": "string", - "pattern": "^[0-9]{3}\\w$" - })""", - R"""( - char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) - root ::= string - space ::= | " " | "\n"{1,2} [ \t]{0,20} - string ::= "\"" char* "\"" - )""", - }); - - // a regexp that is invalid under any flavor is still an error - run({ - FAILURE, - "regexp with unbalanced parentheses", - R"""({ - "type": "string", - "pattern": "^(a$" - })""", - "" - }); - - // only the property with the bad pattern degrades - run({ + // a property node carries its $ref target, so its grammar names the ref rule + { + fprintf(stderr, "- sub-schema $ref\n"); + auto parameters = common_json::parse(R"""({ + "type": "object", + "properties": {"item": {"$ref": "#/$defs/item"}}, + "$defs": { + "item": { + "type": "object", + "properties": {"a": {"type": "string"}}, + "required": ["a"], + "additionalProperties": false + } + } + })"""); + TestCase tc { SUCCESS, - "unsupported regexp in a property", - R"""({ - "type": "object", - "properties": { - "a": { "type": "string", "pattern": "^[a-z\\-]+$" } - }, - "required": ["a"], - "additionalProperties": false - })""", + "sub-schema $ref", + "", R"""( - a ::= string - a-kv ::= "\"a\"" space ":" space a char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) - root ::= "{" space a-kv space "}" + ref-defs-item ::= "{" space ref-defs-item-a-kv space "}" + ref-defs-item-a-kv ::= "\"a\"" space ":" space string + root ::= ref-defs-item space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""", - }); - } - - if (getenv("LLAMA_SKIP_TESTS_SLOW_ON_EMULATOR")) { - fprintf(stderr, "\033[33mWARNING: Skipping slow tests on emulator.\n\033[0m"); - } else { - if (getenv("LLAMA_PYTHON_AVAILABLE") || (std::system("python -c \"import sys; exit(1) if sys.version_info < (3, 8) else print('Python version is sufficient')\"") == 0)) { - test_all("Python", [](const TestCase & tc) { - write("test-json-schema-input.tmp", tc.schema); - tc.verify_status(std::system( - "python ./examples/json_schema_to_grammar.py test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE); - tc.verify(read("test-grammar-output.tmp")); - }); - } else { - fprintf(stderr, "\033[33mWARNING: Python not found (min version required is 3.8), skipping Python JSON schema -> grammar tests.\n\033[0m"); - } + }; + auto doc = common_chat_schema_from_json(parameters); + tc.verify(build_grammar([&](const common_grammar_builder & builder) { + const auto & item = static_cast<const common_chat_schema_object &>(*doc.root).properties.at(0); + builder.add_schema("root", *item.schema); + })); } - test_all("Check Expectations Validity", [](const TestCase & tc) { + test_all("Check the expectations parse", [](const TestCase & tc) { if (tc.expected_status == SUCCESS) { tc.verify_expectation_parseable(); } diff --git a/tests/test-json-schema.cpp b/tests/test-json-schema.cpp new file mode 100644 index 000000000000..fb8cee18b9fb --- /dev/null +++ b/tests/test-json-schema.cpp @@ -0,0 +1,513 @@ +#include "json-schema.h" +#include "json.h" +#include "testing.h" + +#include <cstdlib> +#include <iostream> +#include <stdexcept> +#include <string> +#include <utility> + +static common_chat_schema_document parse(const std::string & schema) { + return common_chat_schema_from_json(common_json::parse(schema)); +} + +// the node as T, aborting the current test when it is some other kind +template <typename T> +static const T & as(testing & t, const common_chat_schema * node, const char * what) { + const T * typed = dynamic_cast<const T *>(node); + if (!t.assert_true(std::string(what) + " has the expected kind", typed != nullptr)) { + throw std::runtime_error(std::string(what) + " has the wrong kind"); + } + return *typed; +} + +template <typename T> +static const T & root(testing & t, const common_chat_schema_document & doc) { + return as<T>(t, doc.root.get(), "root"); +} + +static void assert_error(testing & t, const std::string & schema, const std::string & needle) { + try { + parse(schema); + t.assert_true(schema + " is rejected", false); + } catch (const std::runtime_error & e) { + std::string what = e.what(); + t.assert_true(schema + " -> " + what, what.find(needle) != std::string::npos); + } +} + +static void test_any(testing & t) { + t.test("empty schema", [](testing & t) { + auto doc = parse("{}"); + root<common_chat_schema_any>(t, doc); + t.assert_true("no refs", doc.refs.empty()); + }); + + t.test("keywords that do not imply a type", [](testing & t) { + auto doc = parse(R"({"description": "x", "format": "email", "additionalProperties": true})"); + root<common_chat_schema_any>(t, doc); + }); +} + +static void test_primitives(testing & t) { + t.test("null, boolean, number", [](testing & t) { + auto doc_null = parse(R"({"type": "null"})"); + root<common_chat_schema_null>(t, doc_null); + auto doc_bool = parse(R"({"type": "boolean"})"); + root<common_chat_schema_boolean>(t, doc_bool); + auto doc_num = parse(R"({"type": "number", "minimum": 1, "maximum": 2})"); + root<common_chat_schema_number>(t, doc_num); + }); +} + +static void test_integer(testing & t) { + t.test("unbounded", [](testing & t) { + auto doc = parse(R"({"type": "integer"})"); + const auto & i = root<common_chat_schema_integer>(t, doc); + t.assert_equal("minimum", INT64_MIN, i.minimum); + t.assert_equal("maximum", INT64_MAX, i.maximum); + }); + + t.test("inclusive bounds", [](testing & t) { + auto doc = parse(R"({"type": "integer", "minimum": -5, "maximum": 10})"); + const auto & i = root<common_chat_schema_integer>(t, doc); + t.assert_equal("minimum", -5, i.minimum); + t.assert_equal("maximum", 10, i.maximum); + }); + + t.test("exclusive bounds are folded", [](testing & t) { + auto doc = parse(R"({"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 10})"); + const auto & i = root<common_chat_schema_integer>(t, doc); + t.assert_equal("minimum", 1, i.minimum); + t.assert_equal("maximum", 9, i.maximum); + }); + + t.test("fractional bounds round inwards", [](testing & t) { + auto doc = parse(R"({"type": "integer", "minimum": 1.5, "exclusiveMaximum": 9.5})"); + const auto & i = root<common_chat_schema_integer>(t, doc); + t.assert_equal("minimum", 2, i.minimum); + t.assert_equal("maximum", 9, i.maximum); + }); +} + +static void test_string(testing & t) { + t.test("defaults", [](testing & t) { + auto doc = parse(R"({"type": "string"})"); + const auto & s = root<common_chat_schema_string>(t, doc); + t.assert_equal("pattern", "", s.pattern); + t.assert_equal("format", common_chat_schema::FORMAT_NONE, s.format); + t.assert_equal("min_length", 0, s.min_length); + t.assert_equal("max_length", -1, s.max_length); + }); + + t.test("all keywords are kept", [](testing & t) { + auto doc = parse(R"({"type": "string", "pattern": "^[a-z]+$", "format": "date", "minLength": 2, "maxLength": 8})"); + const auto & s = root<common_chat_schema_string>(t, doc); + t.assert_equal("pattern", "^[a-z]+$", s.pattern); + t.assert_equal("format", common_chat_schema::FORMAT_DATE, s.format); + t.assert_equal("min_length", 2, s.min_length); + t.assert_equal("max_length", 8, s.max_length); + }); + + t.test("formats", [](testing & t) { + auto expect = [&](const char * format, common_chat_schema::string_format expected) { + auto doc = parse(std::string(R"({"type": "string", "format": ")") + format + "\"}"); + t.assert_equal(format, expected, root<common_chat_schema_string>(t, doc).format); + }; + expect("time", common_chat_schema::FORMAT_TIME); + expect("date-time", common_chat_schema::FORMAT_DATE_TIME); + expect("uuid", common_chat_schema::FORMAT_UUID); + expect("uuid5", common_chat_schema::FORMAT_UUID); + expect("email", common_chat_schema::FORMAT_NONE); + }); + + t.test("pattern, length and known format imply a string", [](testing & t) { + auto doc_pattern = parse(R"({"pattern": "^a$"})"); + t.assert_equal("pattern", "^a$", root<common_chat_schema_string>(t, doc_pattern).pattern); + auto doc_length = parse(R"({"minLength": 1, "maxLength": 3})"); + t.assert_equal("min_length", 1, root<common_chat_schema_string>(t, doc_length).min_length); + t.assert_equal("max_length", 3, root<common_chat_schema_string>(t, doc_length).max_length); + auto doc_format = parse(R"({"format": "uuid"})"); + t.assert_equal("format", common_chat_schema::FORMAT_UUID, root<common_chat_schema_string>(t, doc_format).format); + }); +} + +static void test_array(testing & t) { + t.test("items with bounds", [](testing & t) { + auto doc = parse(R"({"type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 3})"); + const auto & a = root<common_chat_schema_array>(t, doc); + as<common_chat_schema_integer>(t, a.items.get(), "items"); + t.assert_equal("min_items", 1, a.min_items); + t.assert_equal("max_items", 3, a.max_items); + }); + + t.test("no items", [](testing & t) { + auto doc = parse(R"({"type": "array"})"); + const auto & a = root<common_chat_schema_array>(t, doc); + as<common_chat_schema_any>(t, a.items.get(), "items"); + t.assert_equal("min_items", 0, a.min_items); + t.assert_equal("max_items", -1, a.max_items); + }); + + t.test("items imply an array", [](testing & t) { + auto doc = parse(R"({"items": {"type": "string"}})"); + const auto & a = root<common_chat_schema_array>(t, doc); + as<common_chat_schema_string>(t, a.items.get(), "items"); + }); +} + +static void test_tuple(testing & t) { + t.test("prefixItems", [](testing & t) { + auto doc = parse(R"({"prefixItems": [{"type": "string"}, {"type": "number"}]})"); + const auto & tup = root<common_chat_schema_tuple>(t, doc); + t.assert_equal("size", (size_t) 2, tup.items.size()); + as<common_chat_schema_string>(t, tup.items[0].get(), "items[0]"); + as<common_chat_schema_number>(t, tup.items[1].get(), "items[1]"); + }); + + t.test("items as an array", [](testing & t) { + auto doc = parse(R"({"type": "array", "items": [{"type": "boolean"}]})"); + const auto & tup = root<common_chat_schema_tuple>(t, doc); + t.assert_equal("size", (size_t) 1, tup.items.size()); + as<common_chat_schema_boolean>(t, tup.items[0].get(), "items[0]"); + }); +} + +static void test_object(testing & t) { + t.test("type alone accepts any object", [](testing & t) { + auto doc = parse(R"({"type": "object"})"); + const auto & o = root<common_chat_schema_object>(t, doc); + t.assert_true("no properties", o.properties.empty()); + as<common_chat_schema_any>(t, o.additional_properties.get(), "additional_properties"); + }); + + t.test("properties", [](testing & t) { + auto doc = parse(R"({ + "type": "object", + "properties": { + "b": {"type": "string"}, + "a": {"type": "integer"}, + "c": {"type": "boolean"} + }, + "required": ["a", "c"] + })"); + const auto & o = root<common_chat_schema_object>(t, doc); + t.assert_equal("size", (size_t) 3, o.properties.size()); + t.assert_equal("order", "b", o.properties[0].name); + t.assert_equal("order", "a", o.properties[1].name); + t.assert_equal("order", "c", o.properties[2].name); + t.assert_true("b optional", !o.properties[0].required); + t.assert_true("a required", o.properties[1].required); + t.assert_true("c required", o.properties[2].required); + as<common_chat_schema_string>(t, o.properties[0].schema.get(), "b"); + as<common_chat_schema_integer>(t, o.properties[1].schema.get(), "a"); + as<common_chat_schema_boolean>(t, o.properties[2].schema.get(), "c"); + t.assert_true("closed", o.additional_properties == nullptr); + }); + + t.test("unknown required entries are ignored", [](testing & t) { + auto doc = parse(R"({"properties": {"a": {}}, "required": ["a", "zzz", 1]})"); + const auto & o = root<common_chat_schema_object>(t, doc); + t.assert_equal("size", (size_t) 1, o.properties.size()); + t.assert_true("a required", o.properties[0].required); + }); + + t.test("additionalProperties false implies an object", [](testing & t) { + auto doc = parse(R"({"additionalProperties": false})"); + const auto & o = root<common_chat_schema_object>(t, doc); + t.assert_true("no properties", o.properties.empty()); + t.assert_true("closed", o.additional_properties == nullptr); + }); + + t.test("additionalProperties schema", [](testing & t) { + auto doc = parse(R"({"properties": {"a": {}}, "additionalProperties": {"type": "integer", "minimum": 0}})"); + const auto & o = root<common_chat_schema_object>(t, doc); + t.assert_equal("size", (size_t) 1, o.properties.size()); + const auto & v = as<common_chat_schema_integer>(t, o.additional_properties.get(), "additional_properties"); + t.assert_equal("minimum", 0, v.minimum); + }); + + t.test("nested", [](testing & t) { + auto doc = parse(R"({"properties": {"inner": {"properties": {"leaf": {"type": "null"}}, "required": ["leaf"]}}})"); + const auto & o = root<common_chat_schema_object>(t, doc); + const auto & inner = as<common_chat_schema_object>(t, o.properties[0].schema.get(), "inner"); + t.assert_equal("leaf name", "leaf", inner.properties[0].name); + t.assert_true("leaf required", inner.properties[0].required); + as<common_chat_schema_null>(t, inner.properties[0].schema.get(), "leaf"); + }); +} + +static void test_const_enum(testing & t) { + t.test("const", [](testing & t) { + auto doc = parse(R"({"const": {"a": [1, null]}})"); + t.assert_equal("value", R"({"a":[1,null]})", root<common_chat_schema_const>(t, doc).value.dump()); + }); + + t.test("enum", [](testing & t) { + auto doc = parse(R"({"enum": ["a", 1, null, true]})"); + const auto & e = root<common_chat_schema_enum>(t, doc); + t.assert_equal("size", (size_t) 4, e.values.size()); + t.assert_equal("values[0]", "\"a\"", e.values[0].dump()); + t.assert_equal("values[1]", "1", e.values[1].dump()); + t.assert_equal("values[2]", "null", e.values[2].dump()); + t.assert_equal("values[3]", "true", e.values[3].dump()); + }); + + t.test("const wins over enum, enum wins over type", [](testing & t) { + auto doc_enum = parse(R"({"type": "integer", "enum": [1, 2]})"); + root<common_chat_schema_enum>(t, doc_enum); + auto doc_const = parse(R"({"type": "string", "const": "x", "enum": ["y"]})"); + t.assert_equal("value", "\"x\"", root<common_chat_schema_const>(t, doc_const).value.dump()); + }); +} + +static void test_any_of(testing & t) { + t.test("anyOf and oneOf", [](testing & t) { + auto doc_any = parse(R"({"anyOf": [{"type": "string"}, {"type": "number"}]})"); + const auto & u = root<common_chat_schema_any_of>(t, doc_any); + t.assert_equal("size", (size_t) 2, u.children.size()); + as<common_chat_schema_string>(t, u.children[0].get(), "children[0]"); + as<common_chat_schema_number>(t, u.children[1].get(), "children[1]"); + + auto doc_one = parse(R"({"oneOf": [{"type": "null"}]})"); + const auto & o = root<common_chat_schema_any_of>(t, doc_one); + t.assert_equal("size", (size_t) 1, o.children.size()); + as<common_chat_schema_null>(t, o.children[0].get(), "children[0]"); + }); + + t.test("oneOf wins over anyOf and type", [](testing & t) { + auto doc = parse(R"({"type": "string", "oneOf": [{"type": "null"}], "anyOf": [{"type": "number"}, {"type": "boolean"}]})"); + const auto & u = root<common_chat_schema_any_of>(t, doc); + t.assert_equal("size", (size_t) 1, u.children.size()); + as<common_chat_schema_null>(t, u.children[0].get(), "children[0]"); + }); + + t.test("type array expands with sibling keywords", [](testing & t) { + auto doc = parse(R"({"type": ["string", "null", "integer"], "minLength": 2, "minimum": 5})"); + const auto & u = root<common_chat_schema_any_of>(t, doc); + t.assert_equal("size", (size_t) 3, u.children.size()); + t.assert_equal("min_length", 2, as<common_chat_schema_string>(t, u.children[0].get(), "children[0]").min_length); + as<common_chat_schema_null>(t, u.children[1].get(), "children[1]"); + t.assert_equal("minimum", 5, as<common_chat_schema_integer>(t, u.children[2].get(), "children[2]").minimum); + }); +} + +static void test_all_of(testing & t) { + t.test("components", [](testing & t) { + auto doc = parse(R"({"allOf": [{"properties": {"a": {}}}, {"anyOf": [{"properties": {"b": {}}}, {"type": "null"}]}]})"); + const auto & all = root<common_chat_schema_all_of>(t, doc); + t.assert_equal("size", (size_t) 2, all.children.size()); + as<common_chat_schema_object>(t, all.children[0].get(), "children[0]"); + as<common_chat_schema_any_of>(t, all.children[1].get(), "children[1]"); + + auto doc_typed = parse(R"({"type": "object", "allOf": [{"properties": {"a": {}}}]})"); + root<common_chat_schema_all_of>(t, doc_typed); + }); + + t.test("properties win over allOf", [](testing & t) { + auto doc = parse(R"({"type": "object", "properties": {"a": {}}, "allOf": [{"properties": {"b": {}}}]})"); + t.assert_equal("size", (size_t) 1, root<common_chat_schema_object>(t, doc).properties.size()); + }); + + t.test("other types ignore allOf", [](testing & t) { + auto doc = parse(R"({"type": "integer", "allOf": [{"minimum": 1}]})"); + root<common_chat_schema_integer>(t, doc); + }); +} + +static void test_ref(testing & t) { + t.test("target is owned by the document", [](testing & t) { + auto doc = parse(R"({"$ref": "#/$defs/t", "type": "string", "$defs": {"t": {"type": "boolean"}}})"); + const auto & r = root<common_chat_schema_ref>(t, doc); + t.assert_equal("ref", "#/$defs/t", r.ref); + t.assert_equal("refs", (size_t) 1, doc.refs.size()); + t.assert_true("target", r.target != nullptr && r.target == doc.refs.at("#/$defs/t").get()); + as<common_chat_schema_boolean>(t, r.target, "target"); + }); + + t.test("definitions", [](testing & t) { + auto doc = parse(R"({"properties": {"a": {"$ref": "#/definitions/t"}}, "definitions": {"t": {"type": "number"}}})"); + const auto & o = root<common_chat_schema_object>(t, doc); + const auto & r = as<common_chat_schema_ref>(t, o.properties[0].schema.get(), "a"); + as<common_chat_schema_number>(t, r.target, "target"); + }); + + t.test("recursive", [](testing & t) { + auto doc = parse(R"({ + "$ref": "#/$defs/node", + "$defs": { + "node": { + "type": "object", + "properties": { + "value": {"type": "number"}, + "next": {"$ref": "#/$defs/node"} + }, + "required": ["value"] + } + } + })"); + const auto & r = root<common_chat_schema_ref>(t, doc); + const auto & node = as<common_chat_schema_object>(t, r.target, "node"); + t.assert_equal("properties", (size_t) 2, node.properties.size()); + const auto & next = as<common_chat_schema_ref>(t, node.properties[1].schema.get(), "next"); + t.assert_true("cycle", next.target == r.target); + t.assert_equal("refs", (size_t) 1, doc.refs.size()); + }); + + t.test("pointer through an array", [](testing & t) { + auto doc = parse(R"({"oneOf": [{"type": "null"}, {"$ref": "#/oneOf/0"}]})"); + const auto & u = root<common_chat_schema_any_of>(t, doc); + const auto & r = as<common_chat_schema_ref>(t, u.children[1].get(), "children[1]"); + as<common_chat_schema_null>(t, r.target, "target"); + }); + + t.test("targets survive moving the document", [](testing & t) { + auto parsed = parse(R"({"items": {"$ref": "#/$defs/t"}, "$defs": {"t": {"type": "null"}}})"); + common_chat_schema_document doc = std::move(parsed); + const auto & a = root<common_chat_schema_array>(t, doc); + const auto & r = as<common_chat_schema_ref>(t, a.items.get(), "items"); + t.assert_true("target", r.target == doc.refs.at("#/$defs/t").get()); + as<common_chat_schema_null>(t, r.target, "target"); + }); +} + +static void test_may_be_string(testing & t) { + auto check = [](testing & t, const std::string & schema, bool expected) { + t.assert_equal(schema, expected, parse(schema).root->may_be_string()); + }; + + t.test("leaves", [&](testing & t) { + check(t, R"({"type": "string"})", true); + check(t, R"({"type": "integer"})", false); + check(t, R"({"minLength": 1})", true); + check(t, R"({"pattern": "^[a-z]+$"})", true); + check(t, R"({"const": "hello"})", true); + check(t, R"({"const": 123})", false); + check(t, R"({"enum": [1, "a", null]})", true); + check(t, R"({"enum": [1, 2, 3]})", false); + }); + + t.test("composites", [&](testing & t) { + check(t, R"({"type": ["integer", "string"]})", true); + check(t, R"({"anyOf": [{"type": "integer"}, {"type": "boolean"}]})", false); + check(t, R"({"allOf": [{"type": "string"}, {"minLength": 1}]})", true); + check(t, R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", false); + check(t, R"({"allOf": [{"minLength": 1}, {"maxLength": 2}]})", true); + }); + + t.test("ref", [&](testing & t) { + check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"anyOf": [{"$ref": "#/$defs/n"}, {"type": "string"}]}}})", true); + check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"$ref": "#/$defs/n"}}})", false); + check(t, R"({"anyOf": [{"$ref": "#/$defs/a"}, {"$ref": "#/$defs/b"}], "$defs": {"a": {"allOf": [{"$ref": "#/$defs/b"}, {"type": "integer"}]}, "b": {"type": "string"}}})", true); + }); +} + +// e.g. {number, integer}, in type order +static std::string dump(const common_chat_schema::type_set & types) { + static const common_chat_schema::value_type order[] = { common_chat_schema::TYPE_NULL, common_chat_schema::TYPE_BOOLEAN, common_chat_schema::TYPE_NUMBER, + common_chat_schema::TYPE_INTEGER, common_chat_schema::TYPE_STRING, common_chat_schema::TYPE_ARRAY, + common_chat_schema::TYPE_OBJECT }; + std::string out; + for (auto type : order) { + if (types.has(type)) { + out += (out.empty() ? "" : ", ") + std::string(common_chat_schema::type_name(type)); + } + } + return "{" + out + "}"; +} + +static void test_value_types(testing & t) { + auto check = [](testing & t, const std::string & schema, const common_chat_schema::type_set & expected) { + t.assert_equal(schema, dump(expected), dump(parse(schema).root->value_types())); + }; + + t.test("leaves", [&](testing & t) { + check(t, R"({"type": "string"})", { common_chat_schema::TYPE_STRING }); + check(t, R"({"type": "number"})", { common_chat_schema::TYPE_NUMBER, common_chat_schema::TYPE_INTEGER }); + check(t, R"({"description": "x"})", common_chat_schema::type_set::all()); + check(t, R"({"properties": {"a": {"type": "string"}}})", { common_chat_schema::TYPE_OBJECT }); + check(t, R"({"items": {"type": "string"}})", { common_chat_schema::TYPE_ARRAY }); + check(t, R"({"const": 1.5})", { common_chat_schema::TYPE_NUMBER }); + check(t, R"({"enum": [1, "a", null]})", { common_chat_schema::TYPE_INTEGER, common_chat_schema::TYPE_STRING, common_chat_schema::TYPE_NULL }); + }); + + t.test("any_of is the union, all_of is the intersection", [&](testing & t) { + check(t, R"({"type": ["string", "null"]})", { common_chat_schema::TYPE_STRING, common_chat_schema::TYPE_NULL }); + check(t, R"({"allOf": [{"type": ["string", "number"]}, {"type": ["number", "object"]}]})", { common_chat_schema::TYPE_NUMBER, common_chat_schema::TYPE_INTEGER }); + check(t, R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", {}); + }); + + t.test("ref", [&](testing & t) { + check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"anyOf": [{"$ref": "#/$defs/n"}, {"type": "string"}]}}})", + { common_chat_schema::TYPE_STRING }); + }); +} + +static void test_errors(testing & t) { + t.test("not a schema", [](testing & t) { + assert_error(t, R"([])", "#: schema must be an object"); + }); + + t.test("type", [](testing & t) { + assert_error(t, R"({"type": 5})", "#: type must be a string or an array of strings"); + assert_error(t, R"({"type": []})", "#: type must not be empty"); + assert_error(t, R"({"type": ["string", "bad"]})", "#/type/1: unrecognized type bad"); + }); + + t.test("ref", [](testing & t) { + assert_error(t, R"({"$ref": 5})", "#: $ref must be a string"); + assert_error(t, R"({"$ref": "https://example.com/x.json"})", "#: unsupported $ref https://example.com/x.json"); + assert_error(t, R"({"$ref": ""})", "#: unsupported $ref ,"); + assert_error(t, R"({"$ref": "#"})", "#: unsupported $ref #,"); + assert_error(t, R"({"$defs": {}, "$ref": "#/$defs/missing"})", "#: cannot resolve $ref #/$defs/missing, missing not found"); + assert_error(t, R"({"oneOf": [{}], "$ref": "#/oneOf/1"})", "#: cannot resolve $ref #/oneOf/1, 1 is out of range"); + assert_error(t, R"({"$defs": {"a": {"$ref": "#/$defs/a/nope"}}, "$ref": "#/$defs/a"})", "#/$defs/a: cannot resolve $ref #/$defs/a/nope, nope not found"); + }); + + t.test("alternatives", [](testing & t) { + assert_error(t, R"({"oneOf": []})", "#/oneOf: must not be empty"); + assert_error(t, R"({"anyOf": {}})", "#/anyOf: must be an array of schemas"); + assert_error(t, R"({"anyOf": [{"type": "string"}, {"items": {"type": "x"}}]})", "#/anyOf/1/items: unrecognized type x"); + }); + + t.test("keywords", [](testing & t) { + assert_error(t, R"({"enum": []})", "#: enum must be a non-empty array"); + assert_error(t, R"({"type": "string", "pattern": 5})", "#: pattern must be a string"); + assert_error(t, R"({"type": "string", "minLength": -1})", "#: minLength must be a non-negative integer"); + assert_error(t, R"({"type": "integer", "minimum": "1"})", "#: minimum must be a number"); + assert_error(t, R"({"type": "array", "maxItems": 1.5})", "#: maxItems must be a non-negative integer"); + assert_error(t, R"({"properties": []})", "#: properties must be an object"); + assert_error(t, R"({"properties": {"a": {"type": "nope"}}})", "#/properties/a: unrecognized type nope"); + assert_error(t, R"({"additionalProperties": null})", "#: additionalProperties must be a boolean or a schema"); + }); +} + +int main(int argc, char * argv[]) { + testing t(std::cout); + if (argc >= 2) { + t.set_filter(argv[1]); + } + + const char * verbose = getenv("LLAMA_TEST_VERBOSE"); + if (verbose) { + t.verbose = std::string(verbose) == "1"; + } + + t.test("any", test_any); + t.test("primitives", test_primitives); + t.test("integer", test_integer); + t.test("string", test_string); + t.test("array", test_array); + t.test("tuple", test_tuple); + t.test("object", test_object); + t.test("const and enum", test_const_enum); + t.test("any_of", test_any_of); + t.test("all_of", test_all_of); + t.test("ref", test_ref); + t.test("may_be_string", test_may_be_string); + t.test("value_types", test_value_types); + t.test("errors", test_errors); + + return t.summary(); +} diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp index 3496f72e4949..90a6a7162331 100644 --- a/tests/test-llama-archs.cpp +++ b/tests/test-llama-archs.cpp @@ -239,7 +239,8 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { // SWA pattern: every 5th layer is full attention (matches E2B layer_types) ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, uint32_t(5)); } else if (arch == LLM_ARCH_COHERE2MOE || arch == LLM_ARCH_MIMO2 || arch == LLM_ARCH_STEP35 || arch == LLM_ARCH_SPARK2_5 || - arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_GRANITE_SWA || arch == LLM_ARCH_DOTS3NOTE) { + arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_GRANITE_SWA || arch == LLM_ARCH_DOTS3NOTE || + arch == LLM_ARCH_MAPLE) { std::vector<uint32_t> pattern; pattern.reserve(n_layer); for (uint32_t il = 0; il < n_layer; il++) { @@ -323,6 +324,11 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) { ms.add_kv(LLM_KV_EXPERT_WEIGHTS_SCALE, 1.0f); ms.add_kv(LLM_KV_EXPERT_WEIGHTS_NORM, true); } + + if (arch == LLM_ARCH_MAPLE) { + ms.add_kv(LLM_KV_SWIGLU_CLAMP_EXP, 7.0f); + } + ms.add_kv(LLM_KV_TOKENIZER_MODEL, "no_vocab"); // ms.add_kv(LLM_KV_DENSE_2_FEAT_OUT, n_embd); // ms.add_kv(LLM_KV_DENSE_3_FEAT_IN, n_embd); @@ -505,6 +511,7 @@ static bool moe_mandatory(const llm_arch arch) { case LLM_ARCH_MISTRAL4: case LLM_ARCH_MELLUM: case LLM_ARCH_LAGUNA: + case LLM_ARCH_MAPLE: return true; default: return false; @@ -563,7 +570,8 @@ 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 || arch == LLM_ARCH_QWEN4EXP) { + if (arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA || arch == LLM_ARCH_DOTS3NOTE || arch == LLM_ARCH_QWEN4EXP || + arch == LLM_ARCH_HY_V4) { return false; } #endif // GGML_USE_WEBGPU diff --git a/tests/test-quant-type-selection.cpp b/tests/test-quant-type-selection.cpp index 9a5f5e53e119..1696ec164c07 100644 --- a/tests/test-quant-type-selection.cpp +++ b/tests/test-quant-type-selection.cpp @@ -221,7 +221,7 @@ static const remote_model_spec model_specs[] = { { "ggml-org/Step-3.5-Flash-GGUF", "Q4_K" }, { "ggml-org/Qwen3-Coder-Next-GGUF", "Q8_0" }, { "ggml-org/Qwen3-14B-GGUF", "Q8_0" }, - { "ggml-org/NVIDIA-Nemotron-Nano-3-30B-A3B-GGUF", "Q8_0" }, + { "ggml-org/NVIDIA-Nemotron-3-Nano-30B-A3B-GGUF", "Q8_0" }, { "ggml-org/gpt-oss-120b-GGUF", "mxfp4" }, { "ggml-org/gemma-3-4b-it-GGUF", "Q8_0" }, { "bartowski/Meta-Llama-3.1-70B-Instruct-GGUF", "Q4_K_M" }, diff --git a/tools/cli/README.md b/tools/cli/README.md index 77a5e6fe3259..ea1f7aaf8bd2 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -133,8 +133,8 @@ | `-l, --logit-bias TOKEN_ID(+/-)BIAS` | modifies the likelihood of token appearing in the completion,<br/>i.e. `--logit-bias 15043+1` to increase likelihood of token ' Hello',<br/>or `--logit-bias 15043-1` to decrease likelihood of token ' Hello' | | `--grammar GRAMMAR` | BNF-like grammar to constrain generations (see samples in grammars/ dir) | | `--grammar-file FNAME` | file to read grammar from | -| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead | -| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead | +| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object | +| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object | | `-bs, --backend-sampling` | enable backend sampling (experimental) (default: disabled)<br/>(env: LLAMA_ARG_BACKEND_SAMPLING) | diff --git a/tools/completion/README.md b/tools/completion/README.md index 08485a95f593..c9a4cccfc271 100644 --- a/tools/completion/README.md +++ b/tools/completion/README.md @@ -216,8 +216,8 @@ llama-completion.exe -m models\gemma-1.1-7b-it.Q4_K_M.gguf --ignore-eos -n -1 | `-l, --logit-bias TOKEN_ID(+/-)BIAS` | modifies the likelihood of token appearing in the completion,<br/>i.e. `--logit-bias 15043+1` to increase likelihood of token ' Hello',<br/>or `--logit-bias 15043-1` to decrease likelihood of token ' Hello' | | `--grammar GRAMMAR` | BNF-like grammar to constrain generations (see samples in grammars/ dir) | | `--grammar-file FNAME` | file to read grammar from | -| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead | -| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead | +| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object | +| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object | | `-bs, --backend-sampling` | enable backend sampling (experimental) (default: disabled)<br/>(env: LLAMA_ARG_BACKEND_SAMPLING) | @@ -556,7 +556,7 @@ These options help improve the performance and memory usage of the LLaMA models. - `--grammar GRAMMAR`, `--grammar-file FILE`: Specify a grammar (defined inline or in a file) to constrain model output to a specific format. For example, you could force the model to output JSON or to speak only in emojis. See the [GBNF guide](../../grammars/README.md) for details on the syntax. -- `--json-schema SCHEMA`: Specify a [JSON schema](https://json-schema.org/) to constrain model output to (e.g. `{}` for any JSON object, or `{"items": {"type": "string", "minLength": 10, "maxLength": 100}, "minItems": 10}` for a JSON array of strings with size constraints). If a schema uses external `$ref`s, you should use `--grammar "$( python examples/json_schema_to_grammar.py myschema.json )"` instead. +- `--json-schema SCHEMA`: Specify a [JSON schema](https://json-schema.org/) to constrain model output to (e.g. `{"type": "object"}` for any JSON object, or `{"items": {"type": "string", "minLength": 10, "maxLength": 100}, "minItems": 10}` for a JSON array of strings with size constraints). ### Quantization diff --git a/tools/mtmd/CMakeLists.txt b/tools/mtmd/CMakeLists.txt index 176eb1505740..907468e87ec7 100644 --- a/tools/mtmd/CMakeLists.txt +++ b/tools/mtmd/CMakeLists.txt @@ -84,13 +84,6 @@ target_link_libraries (mtmd PUBLIC ggml llama) target_link_libraries (mtmd PRIVATE Threads::Threads vendor::hash vendor::miniaudio vendor::stb vendor::sheredom) target_include_directories(mtmd PUBLIC .) target_compile_features (mtmd PRIVATE cxx_std_17) -target_precompile_headers (mtmd PRIVATE models/models.h) - -set_source_files_properties( - mtmd-helper.cpp - mtmd-helper-gen.cpp - PROPERTIES SKIP_PRECOMPILE_HEADERS ON -) if (MTMD_VIDEO) target_compile_definitions(mtmd PRIVATE MTMD_VIDEO) diff --git a/tools/server/CMakeLists.txt b/tools/server/CMakeLists.txt index 02607c838ca8..4adaaceefd23 100644 --- a/tools/server/CMakeLists.txt +++ b/tools/server/CMakeLists.txt @@ -1,13 +1,5 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) -# MSVC emits a PCH bookkeeping symbol that WINDOWS_EXPORT_ALL_SYMBOLS exports as an ambiguous "__" - -set(LLAMA_SERVER_PCH ON) - -if (BUILD_SHARED_LIBS AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(LLAMA_SERVER_PCH OFF) -endif() - # server-context containing the core server logic, used by llama-server and CLI set(TARGET server-context) @@ -38,13 +30,9 @@ if (BUILD_SHARED_LIBS) endif() target_include_directories(${TARGET} PRIVATE ../mtmd) -target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}) +target_include_directories(${TARGET} PRIVATE ${PROJECT_SOURCE_DIR}) target_link_libraries(${TARGET} PUBLIC llama-common mtmd ${CMAKE_THREAD_LIBS_INIT}) -if (LLAMA_SERVER_PCH) - target_precompile_headers(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/common/common.h) -endif() - # llama-server-impl: server logic, reusable by app set(TARGET llama-server-impl) @@ -59,13 +47,9 @@ add_library(${TARGET} set_target_properties(${TARGET} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) target_include_directories(${TARGET} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_include_directories(${TARGET} PRIVATE ../mtmd ${CMAKE_SOURCE_DIR}) +target_include_directories(${TARGET} PRIVATE ../mtmd ${PROJECT_SOURCE_DIR}) target_link_libraries(${TARGET} PUBLIC server-context llama-ui cpp-httplib ${CMAKE_THREAD_LIBS_INIT}) -if (LLAMA_SERVER_PCH) - target_precompile_headers(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/common/common.h) -endif() - add_dependencies(${TARGET} llama-ui-assets) if(LLAMA_TOOLS_INSTALL) diff --git a/tools/server/README.md b/tools/server/README.md index 86d41df73fe0..494f40b6089e 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -150,8 +150,8 @@ For the full list of features, please refer to [server's changelog](https://gith | `-l, --logit-bias TOKEN_ID(+/-)BIAS` | modifies the likelihood of token appearing in the completion,<br/>i.e. `--logit-bias 15043+1` to increase likelihood of token ' Hello',<br/>or `--logit-bias 15043-1` to decrease likelihood of token ' Hello' | | `--grammar GRAMMAR` | BNF-like grammar to constrain generations (see samples in grammars/ dir) | | `--grammar-file FNAME` | file to read grammar from | -| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead | -| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead | +| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object | +| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object | | `-bs, --backend-sampling` | enable backend sampling (experimental) (default: disabled)<br/>(env: LLAMA_ARG_BACKEND_SAMPLING) | diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp index a71de2333212..b5dc4e2dceb9 100644 --- a/tools/server/server-common.cpp +++ b/tools/server/server-common.cpp @@ -15,6 +15,23 @@ #include <limits> #include <cstring> #include <type_traits> +#include <chrono> +#include <thread> + +#ifdef _WIN32 +// windows.h defines min and max as macros, which breaks std::min and std::max +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +# define NOMINMAX +#endif +#include <windows.h> +#include <io.h> +#else +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <unistd.h> +#endif json format_error_response(const std::string & message, const enum error_type type) { std::string type_str; @@ -1181,6 +1198,11 @@ json oaicompat_chat_params_parse( } } + // an absent or empty schema means any object + if (json_schema.is_object() && json_schema.empty()) { + json_schema["type"] = "object"; + } + // get input files if (!body.contains("messages")) { throw std::invalid_argument("'messages' is required"); @@ -1832,3 +1854,133 @@ server_tokens format_prompt_rerank( return result; } + +// +// server_subproc +// + +bool server_subproc::has_output() { + if (out_handle >= 0) { + return true; + } + FILE * f = sproc.stdout_file(); // combined stdout/stderr + if (!f) { + return false; + } +#ifdef _WIN32 + HANDLE h = (HANDLE) _get_osfhandle(_fileno(f)); + if (h != INVALID_HANDLE_VALUE) { + out_handle = (intptr_t) h; + } +#else + int fd = fileno(f); + if (fd >= 0) { + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + out_handle = fd; + } +#endif + return out_handle >= 0; +} + +int server_subproc::read_output(char * buf, size_t len) { + if (!has_output()) { + return -1; + } +#ifdef _WIN32 + HANDLE h = (HANDLE) out_handle; + DWORD avail = 0; + if (!PeekNamedPipe(h, NULL, 0, NULL, &avail, NULL)) { + return -1; // pipe broken, child gone + } + if (avail == 0) { + return 0; + } + DWORD to_read = avail < (DWORD) len ? avail : (DWORD) len; + DWORD got = 0; + if (!ReadFile(h, buf, to_read, &got, NULL) || got == 0) { + return -1; + } + return (int) got; +#else + while (true) { + ssize_t r = read((int) out_handle, buf, len); + if (r > 0) { + return (int) r; + } + if (r == 0) { + return -1; // EOF + } + if (errno == EINTR) { + continue; + } + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return -1; + } +#endif +} + +server_subproc::waiter::waiter() { +#ifndef _WIN32 + int fds[2]; + GGML_ASSERT(pipe(fds) == 0); + for (int fd : fds) { + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + } + wake_fd[0] = fds[0]; + wake_fd[1] = fds[1]; +#endif +} + +server_subproc::waiter::~waiter() { +#ifndef _WIN32 + close((int) wake_fd[0]); + close((int) wake_fd[1]); +#endif +} + +void server_subproc::waiter::wake() { +#ifndef _WIN32 + char c = 1; + (void) !write((int) wake_fd[1], &c, 1); +#endif +} + +void server_subproc::waiter::wait(const std::vector<server_subproc *> & procs, std::vector<bool> & ready, int64_t timeout_ms) { + ready.assign(procs.size(), false); +#ifdef _WIN32 + // no waitable wait exists for anonymous pipes, so poll them in 50 ms steps + bool any = false; + for (size_t i = 0; i < procs.size(); i++) { + DWORD avail = 0; + if (!procs[i]->has_output() || !PeekNamedPipe((HANDLE) procs[i]->out_handle, NULL, 0, NULL, &avail, NULL) || avail > 0) { + ready[i] = true; // data or broken pipe, read_output() tells which + any = true; + } + } + if (!any) { + int64_t step = timeout_ms < 0 ? 50 : std::min<int64_t>(timeout_ms, 50); + std::this_thread::sleep_for(std::chrono::milliseconds(step)); + } +#else + std::vector<pollfd> pfds; + pfds.reserve(procs.size() + 1); + pfds.push_back({ (int) wake_fd[0], POLLIN, 0 }); + for (auto * p : procs) { + pfds.push_back({ p->has_output() ? (int) p->out_handle : -1, POLLIN, 0 }); // poll() skips negative fds + } + int timeout = timeout_ms < 0 ? -1 : (int) std::min<int64_t>(timeout_ms, std::numeric_limits<int>::max()); + int r = poll(pfds.data(), pfds.size(), timeout); + if (r < 0 && errno != EINTR) { + LOG_ERR("%s: poll() failed: %s\n", __func__, strerror(errno)); + } + if (pfds[0].revents) { + char buf[64]; + while (read((int) wake_fd[0], buf, sizeof(buf)) > 0) {} + } + for (size_t i = 0; i < procs.size(); i++) { + ready[i] = pfds[i + 1].fd < 0 || pfds[i + 1].revents != 0; + } +#endif +} diff --git a/tools/server/server-common.h b/tools/server/server-common.h index 3e37a6a4c0dd..eaba876d9a03 100644 --- a/tools/server/server-common.h +++ b/tools/server/server-common.h @@ -6,6 +6,7 @@ #include "chat.h" #include "mtmd.h" #include "mtmd-helper.h" +#include "subproc.h" #include "json.h" @@ -13,6 +14,7 @@ #include <chrono> #include <condition_variable> #include <cinttypes> +#include <cstdio> #include <functional> #include <mutex> #include <queue> @@ -613,3 +615,39 @@ struct server_pipe { return true; } }; + +// wrapper around common_subproc to manage a child server process +// mainly used by router mode +struct server_subproc { + common_subproc sproc; + std::atomic<bool> stopped{false}; // set by the monitor once the process exited and was reaped + + bool is_alive() { return sproc.alive(); } + void terminate() { sproc.terminate(); } + int join() { return sproc.join(); } + + // true if the child's combined stdout/stderr pipe is available (call after create()) + bool has_output(); + + // non-blocking read + // returns the number of bytes read, 0 when nothing is available, -1 when the pipe is closed or broken + int read_output(char * buf, size_t len); + + // wait until one of a set of children has output, wake() is called, or a timeout passes + struct waiter { + waiter(); + ~waiter(); + + // thread-safe; on Windows this is a no-op, wait() returns within 50 ms anyway + void wake(); + + // timeout_ms < 0 waits until data or wake(); ready[i] is set for each child with data (or a broken pipe) + void wait(const std::vector<server_subproc *> & procs, std::vector<bool> & ready, int64_t timeout_ms); + + private: + intptr_t wake_fd[2] = { -1, -1 }; // POSIX self-pipe + }; + +private: + intptr_t out_handle = -1; // fd on POSIX, HANDLE on Windows; taken lazily from sproc +}; diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp index e17d3c30ab1f..d3219428e4b0 100644 --- a/tools/server/server-models.cpp +++ b/tools/server/server-models.cpp @@ -44,30 +44,215 @@ extern char **environ; #define CMD_ROUTER_TO_CHILD_EXIT "cmd_router_to_child:exit" #define CMD_CHILD_TO_ROUTER_STATE "cmd_child_to_router:state:" // followed by json string +// note: SIGPIPE is ignored by the server +static void request_child_exit(server_subproc & proc) { + FILE * stdin_file = proc.sproc.stdin_file(); + if (stdin_file) { + fprintf(stdin_file, "%s\n", CMD_ROUTER_TO_CHILD_EXIT); + fflush(stdin_file); + } +} + // address for child process, this is needed because router may run on 0.0.0.0 // ref: https://github.com/ggml-org/llama.cpp/issues/17862 #define CHILD_ADDR "127.0.0.1" -struct server_subproc { - common_subproc sproc; // not yet spawned while in DOWNLOADING state - std::atomic<bool> stopped{false}; // set to cancel a download or signal child process exit +// single-threaded, watching all child processes at once +struct server_monitor { + server_monitor(server_models & models) : models(models) { + th = std::thread([this]() { run(); }); + } + + ~server_monitor() { + push({ cmd_t::QUIT, {}, "", 0, false }); + th.join(); + } + + // thread-safe + void watch(const std::string & name, std::shared_ptr<server_subproc> proc, server_child_mode mode, int port) { + child_t c; + c.name = name; + c.proc = std::move(proc); + c.mode = mode; + c.port = port; + if (!c.proc->has_output()) { + SRV_ERR("failed to get stdout/stderr of child process for name=%s\n", name.c_str()); + c.eof = true; + } + push({ cmd_t::WATCH, std::move(c), "", 0, false }); + } + + // thread-safe + void stop(const std::string & name, int stop_timeout, bool send_exit) { + push({ cmd_t::STOP, {}, name, stop_timeout, send_exit }); + } + +private: + struct child_t { + std::string name; + std::shared_ptr<server_subproc> proc; + server_child_mode mode = SERVER_CHILD_MODE_NORMAL; + int port = 0; + std::string buf; // partial line + bool eof = false; // output closed, waiting for the process to be reaped + int64_t deadline = 0; // force-kill time in ms, 0 when no stop is pending + }; + + struct cmd_t { + enum { WATCH, STOP, QUIT } type; + child_t child; + std::string name; + int stop_timeout; + bool send_exit; + }; + + void push(cmd_t && cmd) { + { + std::lock_guard<std::mutex> lk(mu); + cmds.push_back(std::move(cmd)); + } + waiter.wake(); + } + + // returns true if the loop should exit + bool handle_commands() { + std::deque<cmd_t> batch; + { + std::lock_guard<std::mutex> lk(mu); + batch.swap(cmds); + } + for (auto & cmd : batch) { + switch (cmd.type) { + case cmd_t::WATCH: + children.push_back(std::move(cmd.child)); + break; + case cmd_t::STOP: + // the newest child with this name is the one the registry knows + for (auto it = children.rbegin(); it != children.rend(); ++it) { + if (it->name != cmd.name) { + continue; + } + if (cmd.send_exit && !it->eof) { + request_child_exit(*it->proc); + } + it->deadline = ggml_time_ms() + (int64_t) cmd.stop_timeout * 1000; + break; + } + break; + case cmd_t::QUIT: + return true; + } + } + return false; + } - bool is_alive() { - return sproc.alive(); + // read what the child wrote, forward complete lines + void read_output(child_t & c) { + char chunk[4096]; + while (!c.eof) { + int n = c.proc->read_output(chunk, sizeof(chunk)); + if (n < 0) { + c.eof = true; + break; + } + if (n == 0) { + break; + } + c.buf.append(chunk, (size_t) n); + size_t start = 0; + while (true) { + size_t nl = c.buf.find('\n', start); + if (nl == std::string::npos) { + break; + } + std::string line = c.buf.substr(start, nl + 1 - start); + start = nl + 1; + on_line(c, line); + } + c.buf.erase(0, start); + if (c.buf.size() > max_line) { + c.buf.clear(); // a child that never writes a newline must not grow this without bound + } + } + if (c.eof && !c.buf.empty()) { + on_line(c, c.buf); + c.buf.clear(); + } } - void request_exit() { - FILE * stdin_file = sproc.stdin_file(); - if (stdin_file) { - fprintf(stdin_file, "%s\n", CMD_ROUTER_TO_CHILD_EXIT); - fflush(stdin_file); + void on_line(child_t & c, const std::string & line) { + if (string_starts_with(line, CMD_CHILD_TO_ROUTER_STATE)) { + LOG_DBG("[%5d] %s", c.port, line.c_str()); // prevent spamming the log + models.handle_child_state(c.name, line); + } else { + LOG("[%5d] %s", c.port, line.c_str()); // forward log } - stopped.store(true, std::memory_order_relaxed); } - void terminate() { - sproc.terminate(); + void run() { + while (true) { + if (handle_commands()) { + return; + } + + // wait for output, a wakeup, or the next deadline; + // a child whose output closed is polled for its exit every 50 ms + int64_t now = ggml_time_ms(); + int64_t timeout = -1; + for (const auto & c : children) { + if (c.eof) { + timeout = timeout < 0 ? 50 : std::min<int64_t>(timeout, 50); + } + if (c.deadline) { + int64_t d = std::max<int64_t>(0, c.deadline - now); + timeout = timeout < 0 ? d : std::min(timeout, d); + } + } + std::vector<server_subproc *> procs; + std::vector<child_t *> owners; + for (auto & c : children) { + if (!c.eof) { + procs.push_back(c.proc.get()); + owners.push_back(&c); + } + } + std::vector<bool> ready; + waiter.wait(procs, ready, timeout); + for (size_t i = 0; i < owners.size(); i++) { + if (ready[i]) { + read_output(*owners[i]); + } + } + + // deadlines and exits + now = ggml_time_ms(); + for (auto it = children.begin(); it != children.end();) { + if (it->deadline && now >= it->deadline && !it->proc->stopped.load(std::memory_order_acquire)) { + SRV_WRN("force-killing model instance name=%s after timeout\n", it->name.c_str()); + it->proc->terminate(); + it->deadline = 0; + } + if (it->eof && !it->proc->is_alive()) { + int exit_code = it->proc->join(); + it->proc->stopped.store(true, std::memory_order_release); + models.on_child_exit(it->name, it->proc, it->mode, exit_code); + SRV_INF("instance name=%s exited with status %d\n", it->name.c_str(), exit_code); + it = children.erase(it); + } else { + ++it; + } + } + } } + + static constexpr size_t max_line = 1024 * 1024; + + server_models & models; + std::mutex mu; + std::deque<cmd_t> cmds; + std::vector<child_t> children; // monitor thread only + server_subproc::waiter waiter; + std::thread th; }; struct server_lru_sched { @@ -395,7 +580,8 @@ server_models::server_models( base_params(params), base_env(get_environment()), base_preset(ctx_preset.load_from_args(argc, argv)), - sched(std::make_unique<server_lru_sched>(*this)) { + sched(std::make_unique<server_lru_sched>(*this)), + monitor(std::make_unique<server_monitor>(*this)) { // clean up base preset unset_reserved_args(base_preset, true); // set binary path @@ -412,6 +598,10 @@ server_models::server_models( server_models::~server_models() = default; +void server_models::instance_t::request_exit() const { + request_child_exit(*subproc); +} + void server_models::add_model(server_model_meta && meta) { if (mapping.find(meta.name) != mapping.end()) { throw std::runtime_error(string_format("model '%s' appears multiple times", meta.name.c_str())); @@ -466,7 +656,6 @@ void server_models::add_model(server_model_meta && meta) { std::string name = meta.name; mapping[name] = instance_t{ /* subproc */ std::make_shared<server_subproc>(), - /* th */ std::thread(), /* meta */ std::move(meta) }; } @@ -624,9 +813,7 @@ void server_models::load_models() { }; // Phase 2: acquire the lock once for all mapping mutations. - // We temporarily release it only when calling functions that acquire it internally - // (unload, load) or when joining threads (the monitoring thread calls update_status - // which locks the mutex, so joining while holding it would deadlock). + // We temporarily release it only when calling functions that acquire it internally (unload) std::unique_lock<std::mutex> lk(mutex); need_reload = false; @@ -711,49 +898,15 @@ void server_models::load_models() { return true; }); - // collect all threads to join in one pass while the lock is held: - // - monitoring threads from just-unloaded models (to_unload) - // - threads of finished downloads (DOWNLOADED), they acquire the mutex on exit - // - threads of already-UNLOADED models that are being removed from source - std::vector<std::thread> threads_to_join; - for (const auto & name : to_unload) { - auto it = mapping.find(name); - if (it != mapping.end() && it->second.th.joinable()) { - threads_to_join.push_back(std::move(it->second.th)); - } - } - for (auto & [name, inst] : mapping) { - if (inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) { - continue; // downloading models are not from config sources, leave them alone - } - if (inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADED) { - // joining this thread under the lock deadlocks: it locks the mutex on its way out - if (inst.th.joinable()) { - threads_to_join.push_back(std::move(inst.th)); - } - continue; - } - if (final_presets.find(name) == final_presets.end() && !inst.meta.is_running() && inst.th.joinable()) { - threads_to_join.push_back(std::move(inst.th)); - } - } - - // join outside the lock - monitoring thread calls update_status (needs lock) - lk.unlock(); - for (auto & th : threads_to_join) th.join(); - lk.lock(); - // erase models no longer in any source for (auto it = mapping.begin(); it != mapping.end(); ) { if (it->second.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) { ++it; // download thread is still busy, skip } else if (it->second.meta.status == SERVER_MODEL_STATUS_DOWNLOADED) { - // download finished, thread is joined above, safe to erase - GGML_ASSERT(!it->second.th.joinable()); + // download finished, safe to erase it = mapping.erase(it); } else if (final_presets.find(it->first) == final_presets.end()) { SRV_INF("(reload) removing model name=%s (no longer in source)\n", it->first.c_str()); - GGML_ASSERT(!it->second.th.joinable()); // must have been joined above it = mapping.erase(it); } else { ++it; @@ -979,7 +1132,8 @@ void server_models::load(const std::string & name, const load_options & opts) { // exceeding models_max. Without this, the window between unload_lru() // releasing its lock and this lock_guard acquiring allows multiple // threads to each observe capacity and all proceed to load. - if (base_params.models_max > 0) { + // Download workers do not use models_max slots. + if (opts.mode == SERVER_CHILD_MODE_NORMAL && base_params.models_max > 0) { size_t count_active = 0; for (const auto & m : mapping) { if (m.second.meta.is_running()) { @@ -1033,117 +1187,12 @@ void server_models::load(const std::string & name, const load_options & opts) { } } - // start a thread to manage the child process - // captured variables are guaranteed to be destroyed only after the thread is joined - inst.th = std::thread([ - this, name, - child_proc = inst.subproc, - port = inst.meta.port, - stop_timeout = inst.meta.stop_timeout, - child_mode = opts.mode - ]() { - FILE * stdin_file = child_proc->sproc.stdin_file(); - FILE * stdout_file = child_proc->sproc.stdout_file(); // combined stdout/stderr - - std::thread log_thread([&]() { - // read stdout/stderr and forward to main server log - // also handle status report from child process - std::vector<char> vec_buf(128 * 1024); // large buffer for storing info - char * buffer = vec_buf.data(); - if (stdout_file) { - while (fgets(buffer, vec_buf.size(), stdout_file) != nullptr) { - std::string str(buffer); - if (string_starts_with(buffer, CMD_CHILD_TO_ROUTER_STATE)) { - LOG_DBG("[%5d] %s", port, buffer); // prevent spamming the log - this->handle_child_state(name, str); - } else { - // forward log - LOG("[%5d] %s", port, buffer); - } - } - } else { - SRV_ERR("failed to get stdout/stderr of child process for name=%s\n", name.c_str()); - } - }); - - std::thread stopping_thread([&]() { - // thread to monitor explicit stop requests; child crash is signalled via child_proc->stopped - auto is_stopping = [this, &name]() { - return this->stopping_models.find(name) != this->stopping_models.end(); - }; - { - std::unique_lock<std::mutex> lk(this->mutex); - this->cv_stop.wait(lk, [&]() { - return is_stopping() || child_proc->stopped.load(std::memory_order_acquire); - }); - } - // child crashed or finished on its own, skip graceful shutdown sequence - if (child_proc->stopped.load(std::memory_order_acquire)) { - return; - } - SRV_INF("stopping model instance name=%s\n", name.c_str()); - fprintf(stdin_file, "%s\n", CMD_ROUTER_TO_CHILD_EXIT); - fflush(stdin_file); - int64_t start_time = ggml_time_ms(); - while (true) { - std::unique_lock<std::mutex> lk(this->mutex); - if (!is_stopping() || child_proc->stopped.load(std::memory_order_acquire)) { - return; - } - int64_t elapsed = ggml_time_ms() - start_time; - if (elapsed >= stop_timeout * 1000) { - lk.unlock(); - SRV_WRN("force-killing model instance name=%s after %d seconds timeout\n", name.c_str(), stop_timeout); - child_proc->terminate(); - return; - } - this->cv_stop.wait_for(lk, std::chrono::seconds(1), [&]() { - return !is_stopping() || child_proc->stopped.load(std::memory_order_acquire); - }); - } - }); - - // we reach here when the child process exits (stdout EOF) - // note: we cannot join() prior to this point because it will close stdin_file - if (log_thread.joinable()) { - log_thread.join(); - } - - child_proc->stopped.store(true, std::memory_order_release); - { - std::lock_guard<std::mutex> lk(this->mutex); - stopping_models.erase(name); - cv_stop.notify_all(); - } - if (stopping_thread.joinable()) { - stopping_thread.join(); - } - - // get the exit code - int exit_code = child_proc->sproc.join(); - - // update status and exit code - if (child_mode == SERVER_CHILD_MODE_DOWNLOAD) { - // instance will be cleaned up on next load_models() call - } else { - this->update_status(name, { - SERVER_MODEL_STATUS_UNLOADED, - exit_code - }); - } - SRV_INF("instance name=%s exited with status %d\n", name.c_str(), exit_code); - }); - - // clean up old process/thread if exists + // old process should have exited already, but just in case, we clean it up here { - auto & old_instance = mapping[name]; - // old process should have exited already, but just in case, we clean it up here - if (old_instance.subproc && old_instance.subproc->is_alive()) { + auto it = mapping.find(name); + if (it != mapping.end() && it->second.subproc && it->second.subproc->is_alive()) { SRV_WRN("old process for model name=%s is still alive, this is unexpected\n", name.c_str()); - old_instance.subproc->terminate(); // force kill - } - if (old_instance.th.joinable()) { - old_instance.th.join(); + it->second.subproc->terminate(); // force kill } } @@ -1151,13 +1200,41 @@ void server_models::load(const std::string & name, const load_options & opts) { {"status", server_model_status_to_string(inst.meta.status)}, }); + auto proc = inst.subproc; + int port = inst.meta.port; mapping[name] = std::move(inst); + monitor->watch(name, proc, opts.mode, port); cv.notify_all(); } -void server_models::request_stop(const std::string & name) { +void server_models::request_stop(const std::string & name, bool send_exit) { + auto it = mapping.find(name); + if (it == mapping.end() || stopping_models.count(name)) { + return; + } stopping_models.insert(name); - cv_stop.notify_all(); + monitor->stop(name, it->second.meta.stop_timeout, send_exit); +} + +void server_models::on_child_exit(const std::string & name, const std::shared_ptr<server_subproc> & proc, server_child_mode mode, int exit_code) { + { + std::lock_guard<std::mutex> lk(mutex); + stopping_models.erase(name); + auto it = mapping.find(name); + if (it == mapping.end() || it->second.subproc != proc) { + return; // entry erased, or a newer instance took the name + } + } + if (mode == SERVER_CHILD_MODE_DOWNLOAD) { + // instance will be cleaned up on next load_models() call + std::lock_guard<std::mutex> lk(mutex); + cv.notify_all(); + } else { + update_status(name, { + SERVER_MODEL_STATUS_UNLOADED, + exit_code + }); + } } void server_models::unload(const std::string & name) { @@ -1166,20 +1243,21 @@ void server_models::unload(const std::string & name) { if (it != mapping.end()) { if (it->second.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) { SRV_INF("cancelling download for model name=%s\n", name.c_str()); - it->second.subproc->request_exit(); + it->second.request_exit(); // for convenience, we wait the status change here wait(lk, name, [](const server_model_meta & new_meta) { return new_meta.status != SERVER_MODEL_STATUS_DOWNLOADING; }); } else if (it->second.meta.is_running()) { SRV_INF("stopping model instance name=%s\n", name.c_str()); - if (it->second.meta.status == SERVER_MODEL_STATUS_LOADING) { + bool loading = it->second.meta.status == SERVER_MODEL_STATUS_LOADING; + if (loading) { // special case: if model is in loading state, unloading means force-killing it SRV_WRN("model name=%s is still loading, force-killing\n", name.c_str()); it->second.subproc->terminate(); } - request_stop(name); - // status change will be handled by the managing thread + request_stop(name, !loading); + // status change will be handled by the monitor } else { SRV_WRN("model instance name=%s is not running\n", name.c_str()); } @@ -1187,27 +1265,29 @@ void server_models::unload(const std::string & name) { } void server_models::unload_all() { - std::vector<std::thread> to_join; - { - std::lock_guard<std::mutex> lk(mutex); - for (auto & [name, inst] : mapping) { - if (inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) { - SRV_INF("cancelling download for model name=%s\n", name.c_str()); - inst.subproc->stopped.store(true, std::memory_order_relaxed); - } else if (inst.meta.is_running()) { - SRV_INF("stopping model instance name=%s\n", name.c_str()); - request_stop(name); - // status change will be handled by the managing thread + std::unique_lock<std::mutex> lk(mutex); + for (auto & [name, inst] : mapping) { + if (inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) { + SRV_INF("cancelling download for model name=%s\n", name.c_str()); + inst.request_exit(); + } else if (inst.meta.is_running()) { + SRV_INF("stopping model instance name=%s\n", name.c_str()); + bool loading = inst.meta.status == SERVER_MODEL_STATUS_LOADING; + if (loading) { + inst.subproc->terminate(); } - // moving the thread to join list to avoid deadlock - to_join.push_back(std::move(inst.th)); + request_stop(name, !loading); } } - for (auto & th : to_join) { - if (th.joinable()) { - th.join(); + // wait for every child to exit, the monitor force-kills the ones that ignore the exit command + cv.wait(lk, [this]() { + for (const auto & [name, inst] : mapping) { + if (inst.meta.is_running() || inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) { + return false; + } } - } + return true; + }); } void server_models::update_status(const std::string & name, const update_status_args & args) { @@ -1294,18 +1374,18 @@ bool server_models::remove(const std::string & name) { if (it->second.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) { // cancel in-flight download SRV_INF("cancelling download for model name=%s\n", name.c_str()); - it->second.subproc->request_exit(); + it->second.request_exit(); } else if (it->second.meta.is_running()) { // stop running instance SRV_INF("stopping model instance name=%s\n", name.c_str()); - stopping_models.insert(name); - if (it->second.meta.status == SERVER_MODEL_STATUS_LOADING) { + bool loading = it->second.meta.status == SERVER_MODEL_STATUS_LOADING; + if (loading) { it->second.subproc->terminate(); } - cv_stop.notify_all(); + request_stop(name, !loading); } - // wait until the monitoring thread finishes + // wait until the child is gone wait(lk, name, [](const server_model_meta & meta) { return meta.status == SERVER_MODEL_STATUS_UNLOADED || meta.status == SERVER_MODEL_STATUS_DOWNLOADED; @@ -1314,8 +1394,7 @@ bool server_models::remove(const std::string & name) { // re-find after wait - load_models() may have erased the entry during the wait it = mapping.find(name); if (it == mapping.end()) { - // load_models() already joined the thread and erased the entry; - // we just need to clean up the cached files on disk + // load_models() already erased the entry; we just need to clean up the cached files on disk lk.unlock(); bool ok = common_download_remove(name); SRV_INF("removing model name=%s from cache (%s)\n", name.c_str(), ok ? "succeeded" : "partial"); @@ -1323,11 +1402,6 @@ bool server_models::remove(const std::string & name) { return true; } - // join before erasing - thread no longer acquires this mutex - if (it->second.th.joinable()) { - it->second.th.join(); - } - // remove from disk (best-effort: cancelled downloads may have no cached files) bool ok = common_download_remove(name); mapping.erase(name); @@ -1542,7 +1616,7 @@ void server_models::handle_child_state(const std::string & name, const std::stri std::lock_guard<std::mutex> lk(mutex); auto it = mapping.find(name); if (it != mapping.end()) { - return it->second.subproc->request_exit(); + return it->second.request_exit(); } }; if (result == "download_finished") { @@ -1716,7 +1790,10 @@ void server_child::notify_to_router(const std::string & state, const json & payl std::lock_guard<std::mutex> lk(mtx_stdout); common_log_pause(common_log_main()); fflush(stdout); - fprintf(stdout, "%s%s\n", CMD_CHILD_TO_ROUTER_STATE, safe_json_to_str(data).c_str()); + // the router matches the command on a line prefix, so the leading newline + // closes whatever the logger left open on the shared pipe, down to the + // trailing color reset that carries no newline of its own + fprintf(stdout, "\n%s%s\n", CMD_CHILD_TO_ROUTER_STATE, safe_json_to_str(data).c_str()); fflush(stdout); common_log_resume(common_log_main()); } diff --git a/tools/server/server-models.h b/tools/server/server-models.h index 7f6c26b358b4..90161bf34ad6 100644 --- a/tools/server/server-models.h +++ b/tools/server/server-models.h @@ -9,6 +9,7 @@ #include <mutex> #include <condition_variable> +#include <thread> #include <functional> #include <memory> #include <optional> @@ -107,27 +108,29 @@ struct server_model_meta { }; struct server_models_routes; -struct server_subproc; // defined in server-models.cpp struct server_lru_sched; // defined in server-models.cpp +struct server_monitor; // defined in server-models.cpp struct server_models { friend struct server_models_routes; friend struct server_lru_sched; + friend struct server_monitor; private: struct instance_t { - std::shared_ptr<server_subproc> subproc; // shared between main thread and monitoring thread - std::thread th; + std::shared_ptr<server_subproc> subproc; // shared with the monitor thread server_model_meta meta; int req_count = 0; // number of active proxy requests + + // ask the child to exit (it handles the command on its stdin, see server_child::setup) + void request_exit() const; }; std::mutex mutex; std::condition_variable cv; std::map<std::string, instance_t> mapping; - // for stopping models - std::condition_variable cv_stop; + // models asked to stop, still counted as running until the monitor records their exit std::set<std::string> stopping_models; // set to true while load_models() is executing a reload; load() will wait until clear @@ -216,9 +219,12 @@ struct server_models { // not thread-safe, caller must hold mutex void add_model(server_model_meta && meta); - // ask the monitoring thread to stop a running instance + // ask the monitor to stop a running instance; send_exit is false for a child that was already force-killed // not thread-safe, caller must hold mutex - void request_stop(const std::string & name); + void request_stop(const std::string & name, bool send_exit = true); + + // called by the monitor once a child exited and was reaped + void on_child_exit(const std::string & name, const std::shared_ptr<server_subproc> & proc, server_child_mode mode, int exit_code); // notify SSE clients void notify_sse(const std::string & event, const std::string & model_id, const json & data = nullptr); @@ -297,12 +303,16 @@ struct server_models { // handle message sent from server_child::notify_to_router() // raw input must starts with CMD_CHILD_TO_ROUTER_STATE, followed by a JSON string - // this function is not thread-safe, must be called from instance's monitoring thread + // called from the monitor thread // payload per state: // state = loading -> payload = {} (TODO: add progress info) // state = ready -> payload = model_info (json), or {} if wakeup from sleeping // state = sleeping -> payload = {} void handle_child_state(const std::string & name, const std::string & raw_input); + +private: + // one thread watching every child; keep last, the destructor joins the thread + std::unique_ptr<server_monitor> monitor; }; struct server_child { diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 64b9251295ce..27ecafb7a595 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -257,6 +257,10 @@ std::vector<std::unique_ptr<field>> make_llama_cmpl_schema(const common_params & if (data.contains("json_schema") && !data.contains("grammar")) { try { auto schema = json_value(data, "json_schema", json::object()); + if (schema.is_object() && schema.empty()) { + // an empty schema means any object + schema["type"] = "object"; + } SRV_DBG("JSON schema: %s\n", schema.dump(2).c_str()); std::string grammar_str = json_schema_to_grammar(schema); SRV_DBG("Converted grammar: %s\n", grammar_str.c_str()); diff --git a/tools/server/tests/unit/test_router.py b/tools/server/tests/unit/test_router.py index e4b7f9fe4826..bae156517749 100644 --- a/tools/server/tests/unit/test_router.py +++ b/tools/server/tests/unit/test_router.py @@ -540,13 +540,17 @@ def _wait_for_sse_event(collected: list, event_type: str, model: str, timeout: i def test_router_download_model(): - """Case 1: download a model, verify SSE events and GET /models.""" + """Case 1: download a model at the model limit, verify SSE events and GET /models.""" global server + server.models_max = 1 server.start() # Ensure the model is not present before we start server.make_request("DELETE", f"/models?model={MODEL_DOWNLOAD_ID}") + # A download worker must not consume or evict a model slot + _load_model_and_wait(MODEL_B, timeout=120) + sse_events: list = [] stop = threading.Event() sse_ready = threading.Event() @@ -580,6 +584,7 @@ def test_router_download_model(): # Model should now appear in GET /models ids = _get_model_ids(is_reload=False) assert MODEL_DOWNLOAD_ID in ids, f"{MODEL_DOWNLOAD_ID} not found in /models after download" + assert _get_model_status(MODEL_B) == "loaded" def test_router_delete_model(): diff --git a/tools/tuning/CMakeLists.txt b/tools/tuning/CMakeLists.txt index 39ff0018026c..f07983882621 100644 --- a/tools/tuning/CMakeLists.txt +++ b/tools/tuning/CMakeLists.txt @@ -3,7 +3,7 @@ set(TARGET ggml-metal-tuning) add_executable(${TARGET} main.cpp bench.cpp fa-vec.cpp) target_link_libraries(${TARGET} PRIVATE ggml ${CMAKE_THREAD_LIBS_INIT}) target_compile_features(${TARGET} PRIVATE cxx_std_17) -target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/ggml/src/ggml-metal) +target_include_directories(${TARGET} PRIVATE ${PROJECT_SOURCE_DIR}/ggml/src/ggml-metal) if(LLAMA_TOOLS_INSTALL) install(TARGETS ${TARGET} RUNTIME) diff --git a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts index 21deed32d307..cb361aad8a4e 100644 --- a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts +++ b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts @@ -47,7 +47,7 @@ export function useToolsPanel(): UseToolsPanelReturn { if (toolsStore.toolGroups.length > 0) return null; - // Tools endpoint is unreachable (404) — server started without --tools + // Tools endpoint unreachable (403) — server started without tools if (toolsStore.isToolsEndpointUnreachable) { return `To enable Server Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`; } diff --git a/tools/ui/src/lib/stores/agentic/index.svelte.ts b/tools/ui/src/lib/stores/agentic/index.svelte.ts index 50db1be0cc30..121ee7739cdb 100644 --- a/tools/ui/src/lib/stores/agentic/index.svelte.ts +++ b/tools/ui/src/lib/stores/agentic/index.svelte.ts @@ -315,8 +315,14 @@ class AgenticStore { // Clear any pending permissions/continue requests for this conversation when starting a new flow this.gates.clear(conversationId); - // Ensure server tools are fetched before checking if agentic is enabled - if (toolsStore.serverTools.length === 0 && !toolsStore.loading) { + // Ensure server tools are fetched before checking if agentic is enabled. + // A disabled /tools endpoint stays disabled for the life of the server, + // so the tools panel is the only place that probes it again. + if ( + toolsStore.serverTools.length === 0 && + !toolsStore.loading && + !toolsStore.isToolsEndpointUnreachable + ) { await toolsStore.fetchServerTools(); } diff --git a/tools/ui/src/lib/stores/tools.svelte.ts b/tools/ui/src/lib/stores/tools.svelte.ts index db05e3cd55b1..1d4133408b54 100644 --- a/tools/ui/src/lib/stores/tools.svelte.ts +++ b/tools/ui/src/lib/stores/tools.svelte.ts @@ -32,7 +32,7 @@ import { mcpStore } from '$lib/stores/mcp/index.svelte'; import { modelsStore } from '$lib/stores/models/index.svelte'; import { settingsStore } from '$lib/stores/settings/index.svelte'; import type { OpenAIToolDefinition, ToolEntry, ToolGroup } from '$lib/types'; -import { buildSandboxToolDefinition } from '$lib/utils'; +import { ApiError, buildSandboxToolDefinition } from '$lib/utils'; import { SvelteMap, SvelteSet } from 'svelte/reactivity'; /** Stable selection identity for a tool, shared by the disabled set and the permission store */ @@ -246,13 +246,10 @@ class ToolsStore { toolInfos.filter((info) => info.uses_cwd).map((info) => info.tool) ); } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - - this._error = errorMessage; + this._error = err instanceof Error ? err.message : String(err); // 403 from /tools means the server was started without --tools - // TODO: check status code instead of relying on message - if (errorMessage.includes('this feature is disabled')) { + if (err instanceof ApiError && err.status === 403) { this._toolsEndpointUnreachable = true; console.info('[ToolsStore] Server tools are disabled on the server'); } else { diff --git a/vendor/cpp-httplib/httplib.cpp b/vendor/cpp-httplib/httplib.cpp index 7fd10b3939bc..c82ff1e71de8 100644 --- a/vendor/cpp-httplib/httplib.cpp +++ b/vendor/cpp-httplib/httplib.cpp @@ -912,17 +912,42 @@ bool write_websocket_frame(Stream &strm, ws::Opcode opcode, namespace ws { namespace impl { -bool read_websocket_frame(Stream &strm, Opcode &opcode, - std::string &payload, bool &fin, - bool expect_masked, size_t max_len) { - // Read first 2 bytes +// Read exactly `size` bytes. Stream::read may return less than asked for -- it +// hands back whatever its buffer already holds -- so every multi-byte field has +// to loop. Reading a 2-byte header with a single read() fails whenever the +// header straddles the read buffer's boundary. +// +// Timeout is reported only when nothing at all was consumed. Once a byte has +// been taken the stream sits mid-field and cannot be resumed, so a timeout +// there is a failure like any other. (When read() fails it always records why, +// so the error belongs to this call and not to an earlier one.) +FrameRead read_exact(Stream &strm, void *buf, size_t size) { + auto p = static_cast<char *>(buf); + size_t total = 0; + while (total < size) { + auto n = strm.read(p + total, size - total); + if (n <= 0) { + auto timed_out = total == 0 && strm.get_error() == Error::Timeout; + return timed_out ? FrameRead::Timeout : FrameRead::Fail; + } + total += static_cast<size_t>(n); + } + return FrameRead::Ok; +} + +FrameRead read_websocket_frame(Stream &strm, Opcode &opcode, + std::string &payload, bool &fin, + bool expect_masked, size_t max_len) { + // Read first 2 bytes. This is the only read that may report a timeout: it + // sits on a frame boundary, where nothing has been consumed yet. uint8_t header[2]; - if (strm.read(reinterpret_cast<char *>(header), 2) != 2) { return false; } + FrameRead first = read_exact(strm, header, 2); + if (first != FrameRead::Ok) { return first; } fin = (header[0] & 0x80) != 0; // RSV1, RSV2, RSV3 must be 0 when no extension is negotiated - if (header[0] & 0x70) { return false; } + if (header[0] & 0x70) { return FrameRead::Fail; } opcode = static_cast<Opcode>(header[0] & 0x0F); bool masked = (header[1] & 0x80) != 0; @@ -932,46 +957,44 @@ bool read_websocket_frame(Stream &strm, Opcode &opcode, // MUST have a payload length of 125 bytes or less bool is_control = (static_cast<uint8_t>(opcode) & 0x08) != 0; if (is_control) { - if (!fin) { return false; } - if (payload_len > 125) { return false; } + if (!fin) { return FrameRead::Fail; } + if (payload_len > 125) { return FrameRead::Fail; } } - if (masked != expect_masked) { return false; } + if (masked != expect_masked) { return FrameRead::Fail; } // Extended payload length if (payload_len == 126) { uint8_t ext[2]; - if (strm.read(reinterpret_cast<char *>(ext), 2) != 2) { return false; } + if (read_exact(strm, ext, 2) != FrameRead::Ok) { return FrameRead::Fail; } payload_len = (static_cast<uint64_t>(ext[0]) << 8) | ext[1]; } else if (payload_len == 127) { uint8_t ext[8]; - if (strm.read(reinterpret_cast<char *>(ext), 8) != 8) { return false; } + if (read_exact(strm, ext, 8) != FrameRead::Ok) { return FrameRead::Fail; } // RFC 6455 Section 5.2: the most significant bit MUST be 0 - if (ext[0] & 0x80) { return false; } + if (ext[0] & 0x80) { return FrameRead::Fail; } payload_len = 0; for (int i = 0; i < 8; i++) { payload_len = (payload_len << 8) | ext[i]; } } - if (payload_len > max_len) { return false; } + if (payload_len > max_len) { return FrameRead::Fail; } // Read mask key if present uint8_t mask_key[4] = {0}; if (masked) { - if (strm.read(reinterpret_cast<char *>(mask_key), 4) != 4) { return false; } + if (read_exact(strm, mask_key, 4) != FrameRead::Ok) { + return FrameRead::Fail; + } } // Read payload payload.resize(static_cast<size_t>(payload_len)); - if (payload_len > 0) { - size_t total_read = 0; - while (total_read < payload_len) { - auto n = strm.read(&payload[total_read], - static_cast<size_t>(payload_len - total_read)); - if (n <= 0) { return false; } - total_read += static_cast<size_t>(n); - } + if (payload_len > 0 && + read_exact(strm, &payload[0], static_cast<size_t>(payload_len)) != + FrameRead::Ok) { + return FrameRead::Fail; } // Unmask if needed @@ -981,7 +1004,7 @@ bool read_websocket_frame(Stream &strm, Opcode &opcode, } } - return true; + return FrameRead::Ok; } } // namespace impl @@ -1728,7 +1751,9 @@ ssize_t select_impl(socket_t sock, short events, time_t sec, pfd.events = events; pfd.revents = 0; - auto timeout = static_cast<int>(sec * 1000 + usec / 1000); + // A negative timeout waits forever, poll's own convention. 0 keeps meaning + // "return immediately", which callers here rely on to probe a socket. + auto timeout = sec < 0 ? -1 : static_cast<int>(sec * 1000 + usec / 1000); return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); }); } @@ -1810,8 +1835,11 @@ class SocketStream final : public Stream { bool ensure_readable(); socket_t sock_; - time_t read_timeout_sec_; - time_t read_timeout_usec_; + // Atomic because ws::WebSocket::set_read_timeout() reaches this from another + // thread while a read is in flight -- that is the point of it, for a caller + // holding one connection and wanting control back to send on it. + std::atomic<time_t> read_timeout_sec_; + std::atomic<time_t> read_timeout_usec_; time_t write_timeout_sec_; time_t write_timeout_usec_; time_t max_timeout_msec_; @@ -2204,12 +2232,10 @@ int getaddrinfo_with_timeout(const char *node, const char *service, // actually finish before letting the stack frame go. The trade-off is that // a wedged DNS server can hold this thread for the system resolver timeout // (~30s by default) past the caller's connection timeout. - struct gaicb request {}; + struct gaicb request{}; struct gaicb *requests[1] = {&request}; - struct sigevent sevp {}; - struct timespec timeout { - timeout_sec, 0 - }; + struct sigevent sevp{}; + struct timespec timeout{timeout_sec, 0}; request.ar_name = node; request.ar_service = service; @@ -2948,8 +2974,21 @@ EncodingType encoding_type(const Request &req, return best; } +// `content_type` is taken separately because a file-backed response has not +// been given one yet when its coding has to be decided. +EncodingType encoding_type(const Request &req, const Response &res, + const std::string &content_type) { + // The response already names a content coding of its own: a handler serving + // a body it encoded itself (pre-compressed static assets, say), or a mount + // point whose headers name the coding its files are stored in. Applying one + // on top of that would double-encode the body and append a second + // `Content-Encoding` field line. + if (res.has_header("Content-Encoding")) { return EncodingType::None; } + return encoding_type(req, content_type); +} + EncodingType encoding_type(const Request &req, const Response &res) { - return encoding_type(req, res.get_header_value("Content-Type")); + return encoding_type(req, res, res.get_header_value("Content-Type")); } std::unique_ptr<compressor> make_compressor(EncodingType type) { @@ -3677,6 +3716,17 @@ bool is_chunked_transfer_encoding(const Headers &headers) { return case_ignore::equal(last_coding, "chunked"); } +bool has_conflicting_content_length(const Headers &headers) { + // RFC 9112 §6.3: a message carrying both Transfer-Encoding and a non-zero + // Content-Length is framed ambiguously. The body readers here delimit it by + // the transfer coding and drop Content-Length, while an intermediary may do + // the reverse, so the two disagree on where the body ends and a reused + // connection is desynchronised (request/response smuggling). Content-Length: + // 0 is tolerated for compatibility with existing peers. + return has_header(headers, "Transfer-Encoding") && + get_header_value_u64(headers, "Content-Length", 0, 0) > 0; +} + template <typename T, typename U> bool prepare_content_receiver(T &x, int &status, ContentReceiverWithProgress receiver, @@ -4035,7 +4085,7 @@ void set_file_content_provider(Response &res, return true; }); - res.file_content_encoding_ = encoding; + res.content_coding_ = encoding; } template <typename T, typename U> @@ -4361,13 +4411,20 @@ bool parse_range_header(const std::string &s, Ranges &ranges) try { ssize_t first = -1; if (!lhs.empty()) { - ssize_t v; - auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v); - if (res.ec == std::errc{}) { first = v; } + // Reject an overflowing first-byte-pos; treating it as absent (-1) + // would turn the range into a suffix range. + auto res = + detail::from_chars(lhs.data(), lhs.data() + lhs.size(), first); + if (res.ec != std::errc{}) { + all_valid_ranges = false; + return; + } } ssize_t last = -1; if (!rhs.empty()) { + // An overflowing last-byte-pos is past any content length, so keeping + // -1 ("remainder", RFC 9110 14.1.2) is correct here. ssize_t v; auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v); if (res.ec == std::errc{}) { last = v; } @@ -6902,7 +6959,7 @@ void Response::set_content(const char *s, size_t n, auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); - file_content_encoding_ = detail::EncodingType::None; + content_coding_ = detail::EncodingType::None; } void Response::set_content(const std::string &s, @@ -6917,7 +6974,7 @@ void Response::set_content(std::string &&s, auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); - file_content_encoding_ = detail::EncodingType::None; + content_coding_ = detail::EncodingType::None; } void Response::set_content_provider( @@ -6928,7 +6985,7 @@ void Response::set_content_provider( if (in_length > 0) { content_provider_ = std::move(provider); } content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = false; - file_content_encoding_ = detail::EncodingType::None; + content_coding_ = detail::EncodingType::None; } void Response::set_content_provider( @@ -6939,7 +6996,7 @@ void Response::set_content_provider( content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = false; - file_content_encoding_ = detail::EncodingType::None; + content_coding_ = detail::EncodingType::None; } void Response::set_chunked_content_provider( @@ -6950,7 +7007,7 @@ void Response::set_chunked_content_provider( content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = true; - file_content_encoding_ = detail::EncodingType::None; + content_coding_ = detail::EncodingType::None; } void Response::set_file_content(const std::string &path, @@ -7991,12 +8048,19 @@ ssize_t WebSocketSSLStream::read(char *ptr, size_t size) { needs_readable || (err.code == tls::ErrorCode::SyscallError && WSAGetLastError() == WSAETIMEDOUT); #endif - if (!needs_readable && err.code != tls::ErrorCode::WantWrite) { return -1; } + if (!needs_readable && err.code != tls::ErrorCode::WantWrite) { + error_ = Error::Read; + return -1; + } if (!(needs_readable ? wait_readable() : wait_writable())) { error_ = Error::Timeout; return -1; } } + // Out of retries. Recording a reason matters: a caller that reads get_error() + // to tell a timeout from a close would otherwise see whatever the previous + // failure left behind (error_ is never cleared on success). + error_ = Error::Read; return -1; } @@ -8653,9 +8717,10 @@ Server::write_content_with_provider(Stream &strm, const Request &req, } } else { if (res.is_chunked_content_provider_) { - auto type = detail::encoding_type(req, res); - - auto compressor = detail::make_compressor(type); + // Use the coding `apply_ranges()` chose when it wrote the headers; + // re-negotiating here would disagree with them, e.g. once a handler's + // own Content-Encoding header suppresses the negotiation. + auto compressor = detail::make_compressor(res.content_coding_); if (!compressor) { compressor = detail::make_unique<detail::nocompressor>(); } @@ -8881,7 +8946,8 @@ bool Server::handle_file_request(Request &req, Response &res) { auto encoding = detail::EncodingType::None; if (static_file_compression_) { content_type = content_type_of(); - encoding = static_file_encoding(req, content_type, stat.size()); + encoding = + static_file_encoding(req, res, content_type, stat.size()); } // The ETag names the representation actually sent, so a client that @@ -9296,8 +9362,10 @@ bool Server::dispatch_request(Request &req, Response &res, // the ETag, which has to name the representation actually sent, and // `apply_static_file_compression()` go through this, so the two cannot drift // apart. -detail::EncodingType Server::static_file_encoding( - const Request &req, const std::string &content_type, size_t length) const { +detail::EncodingType +Server::static_file_encoding(const Request &req, const Response &res, + const std::string &content_type, + size_t length) const { if (!static_file_compression_) { return detail::EncodingType::None; } // Nothing to compress, and an empty file already answers with @@ -9322,14 +9390,14 @@ detail::EncodingType Server::static_file_encoding( return detail::EncodingType::None; } - return detail::encoding_type(req, content_type); + return detail::encoding_type(req, res, content_type); } // Compresses a file-backed content provider into `res.body` and takes over the // framing headers. Returns false when the response is left untouched. bool Server::apply_static_file_compression(const Request &req, Response &res) const { - auto type = res.file_content_encoding_; + auto type = res.content_coding_; if (type == detail::EncodingType::None || !res.content_provider_) { return false; } @@ -9353,7 +9421,7 @@ bool Server::apply_static_file_compression(const Request &req, res.content_provider_success_ = true; res.content_provider_ = nullptr; res.content_length_ = 0; - res.file_content_encoding_ = detail::EncodingType::None; + res.content_coding_ = detail::EncodingType::None; res.set_header("Content-Encoding", detail::encoding_name(type)); res.set_header("Vary", "Accept-Encoding"); @@ -9412,6 +9480,7 @@ void Server::apply_ranges(const Request &req, Response &res, if (res.content_provider_) { if (res.is_chunked_content_provider_) { res.set_header("Transfer-Encoding", "chunked"); + res.content_coding_ = type; if (type != detail::EncodingType::None) { res.set_header("Content-Encoding", detail::encoding_name(type)); res.set_header("Vary", "Accept-Encoding"); @@ -9568,8 +9637,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr, // coding is not chunked, which leaves the body length undeterminable. The // latter must not fall through to the "no body" path, or the body bytes are // parsed as the next request on a persistent connection. - if (req.has_header("Transfer-Encoding") && - (req.get_header_value_u64("Content-Length") > 0 || + if (detail::has_conflicting_content_length(req.headers) || + (req.has_header("Transfer-Encoding") && !detail::is_chunked_transfer_encoding(req.headers))) { connection_closed = true; res.status = StatusCode::BadRequest_400; @@ -9734,7 +9803,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, auto ws_strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream( strm.socket(), const_cast<tls::session_t>(req.ssl), - CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0, + CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0, write_timeout_sec_, write_timeout_usec_)); ws::WebSocket ws(std::move(ws_strm), req, true, websocket_ping_interval_sec_, @@ -9744,7 +9813,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr, } #endif // Use WebSocket-specific read timeout instead of HTTP timeout - strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0); + strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, + 0); ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_, websocket_max_missed_pongs_); entry.handler(req, ws); @@ -9808,7 +9878,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, detail::set_file_content_provider( res, mm, content_type, - static_file_encoding(req, content_type, mm->size())); + static_file_encoding(req, res, content_type, mm->size())); } } @@ -10228,8 +10298,12 @@ Result ClientImpl::send_(Request &&req) { void ClientImpl::prepare_default_headers(Request &r, bool for_stream, const std::string &ct) { (void)for_stream; - for (const auto &header : default_headers_) { - if (!r.has_header(header.first)) { r.headers.insert(header); } + // Default headers are meant for the origin and may carry its credentials, so + // keep them off the CONNECT request the proxy reads. + if (r.method != "CONNECT") { + for (const auto &header : default_headers_) { + if (!r.has_header(header.first)) { r.headers.insert(header); } + } } // RFC 9110 5.3 recommends sending control data such as Host first, so @@ -10379,6 +10453,17 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, return handle; } + // Same framing check as ClientImpl::process_request(). A HEAD or bodyless + // (204/304) response legitimately carries framing headers with no body. + if (method != "HEAD" && + handle.response->status != StatusCode::NoContent_204 && + handle.response->status != StatusCode::NotModified_304 && + detail::has_conflicting_content_length(handle.response->headers)) { + handle.error = Error::Read; + handle.response.reset(); + return handle; + } + handle.body_reader_.stream = handle.stream_; handle.body_reader_.payload_max_length = payload_max_length_; @@ -10910,24 +10995,24 @@ bool ClientImpl::write_request(Stream &strm, Request &req, } } - if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) { - if (!req.has_header("Authorization")) { + // A CONNECT request is read by the proxy; everything sent through the tunnel + // it opens is read by the origin. Each credential goes only to its own hop. + auto is_connect = req.method == "CONNECT"; + + if (!is_connect && !req.has_header("Authorization")) { + if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) { req.headers.insert(make_basic_authentication_header( basic_auth_username_, basic_auth_password_, false)); - } - } - - if (!bearer_token_auth_token_.empty()) { - if (!req.has_header("Authorization")) { + } else if (!bearer_token_auth_token_.empty()) { req.headers.insert(make_bearer_token_authentication_header( bearer_token_auth_token_, false)); } } - // Proxy-Authorization is only sent when the proxy is actually used for - // this target — otherwise NO_PROXY-matched requests would leak proxy - // credentials directly to the destination server. - if (is_proxy_enabled_for_host(host_)) { + // Proxy-Authorization is only sent when the proxy reads this message — + // otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel, + // would leak proxy credentials to the destination server. + if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) { if (!proxy_basic_auth_username_.empty() && !proxy_basic_auth_password_.empty() && !req.has_header("Proxy-Authorization")) { @@ -11323,6 +11408,17 @@ bool ClientImpl::process_request(Stream &strm, Request &req, // Body if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" && req.method != "CONNECT") { + // Reject ambiguous framing (RFC 9112 §6.3). Unlike a request, a response + // whose final transfer coding is not chunked is not ambiguous: its body + // runs until the server closes the connection, so it is not rejected. + // HEAD/204 are excluded above and a 304 carries no body. + if (res.status != StatusCode::NotModified_304 && + detail::has_conflicting_content_length(res.headers)) { + error = Error::Read; + output_error_log(error, &req); + return false; + } + auto redirect = 300 < res.status && res.status < 400 && res.status != StatusCode::NotModified_304 && follow_location_; @@ -17562,8 +17658,16 @@ ReadResult WebSocket::read(std::string &msg) { std::string payload; bool fin; - if (!impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_, - CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) { + impl::FrameRead r = + impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_, + CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH); + // A timeout landed on a frame boundary: the connection is untouched and + // still usable, so hand control back without closing it. That is only + // useful to a caller who asked for the timeout; the compile-time default + // is a backstop against a peer gone quiet, and elapsing it closes the + // connection so a plain `while (ws.read(msg))` loop ends. + if (r == impl::FrameRead::Timeout && read_timeout_set_) { return Timeout; } + if (r != impl::FrameRead::Ok) { closed_ = true; return Fail; } @@ -17600,9 +17704,14 @@ ReadResult WebSocket::read(std::string &msg) { Opcode cont_opcode; std::string cont_payload; bool cont_fin; - if (!impl::read_websocket_frame( + // A timeout is not reportable here: half of a fragmented message is + // already in `msg` and read() has no way to resume it, so it is a + // failure like any other. Timeouts are only ever seen on a message + // boundary. + if (impl::read_websocket_frame( strm_, cont_opcode, cont_payload, cont_fin, is_server_, - CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) { + CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH) != + impl::FrameRead::Ok) { closed_ = true; return Fail; } @@ -17696,7 +17805,8 @@ void WebSocket::close(CloseStatus status, const std::string &reason) { Opcode op; std::string resp; bool fin; - while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125)) { + while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125) == + impl::FrameRead::Ok) { if (op == Opcode::Close) { break; } } } @@ -17741,6 +17851,15 @@ const Request &WebSocket::request() const { return req_; } bool WebSocket::is_open() const { return !closed_; } +void WebSocket::set_read_timeout(time_t sec, time_t usec) { + // 0 waits forever here, as it does for SO_RCVTIMEO. The stream waits with + // poll(), where 0 would instead mean "return immediately", so hand it the + // negative poll uses for an unbounded wait. + if (sec == 0 && usec == 0) { sec = -1; } + strm_.set_read_timeout(sec, usec); + read_timeout_set_ = true; +} + // WebSocketClient implementation WebSocketClient::WebSocketClient( const std::string &scheme_host_port_path, const Headers &headers) @@ -17843,6 +17962,16 @@ void WebSocketClient::shutdown_and_close() { bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm, Error &error, int &ssl_error, uint64_t &ssl_backend_error) { + // A read timeout of 0 means "wait forever", the way SO_RCVTIMEO reads it. + // The streams wait with poll(), where 0 instead means "return immediately", + // so they are given the negative poll uses for an unbounded wait. + auto unbounded = read_timeout_sec_ == 0 && read_timeout_usec_ == 0; + time_t strm_read_sec = unbounded ? -1 : read_timeout_sec_; + time_t strm_read_usec = unbounded ? 0 : read_timeout_usec_; + // The handshake belongs to establishing the connection, so an unset read + // timeout leaves it bounded by the connection timeout instead of forever. + time_t hs_sec = unbounded ? connection_timeout_sec_ : read_timeout_sec_; + time_t hs_usec = unbounded ? connection_timeout_usec_ : read_timeout_usec_; #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl_) { // A plain flag rather than SSLClient::load_certs()'s call_once: connect() @@ -17862,8 +17991,8 @@ bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm, detail::ClientTlsSessionError tls_error; if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_, server_certificate_verification_, - read_timeout_sec_, read_timeout_usec_, - &tls_error, options)) { + hs_sec, hs_usec, &tls_error, + options)) { error = tls_error.error; ssl_error = tls_error.ssl_error; ssl_backend_error = tls_error.backend_error; @@ -17871,17 +18000,19 @@ bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm, } strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream( - sock_, tls_session_, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_)); + sock_, tls_session_, strm_read_sec, strm_read_usec, write_timeout_sec_, + write_timeout_usec_)); return true; } #else (void)error; (void)ssl_error; (void)ssl_backend_error; + (void)hs_sec; + (void)hs_usec; #endif strm = std::unique_ptr<Stream>( - new detail::SocketStream(sock_, read_timeout_sec_, read_timeout_usec_, + new detail::SocketStream(sock_, strm_read_sec, strm_read_usec, write_timeout_sec_, write_timeout_usec_)); return true; } @@ -17951,6 +18082,9 @@ Result WebSocketClient::connect() { ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false, websocket_ping_interval_sec_, websocket_max_missed_pongs_)); + // The stream was created with the timeout already; tell the WebSocket + // whether it came from the caller, so read() knows to report it as Timeout. + ws_->read_timeout_set_ = read_timeout_set_; return Result{Error::Success, upgrade.status, std::move(upgrade.headers)}; } @@ -17983,6 +18117,10 @@ const std::string &WebSocketClient::subprotocol() const { void WebSocketClient::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; + read_timeout_set_ = true; + // The members above only seed the next connect(); read() consults the + // stream, so an already-open connection has to be told directly. + if (ws_) { ws_->set_read_timeout(sec, usec); } } void WebSocketClient::set_write_timeout(time_t sec, time_t usec) { diff --git a/vendor/cpp-httplib/httplib.h b/vendor/cpp-httplib/httplib.h index ca7c96a41a38..a3a2ff45afc4 100644 --- a/vendor/cpp-httplib/httplib.h +++ b/vendor/cpp-httplib/httplib.h @@ -8,8 +8,8 @@ #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_VERSION "0.54.1" -#define CPPHTTPLIB_VERSION_NUM "0x003601" +#define CPPHTTPLIB_VERSION "0.56.0" +#define CPPHTTPLIB_VERSION_NUM "0x003800" #ifdef _WIN32 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 @@ -215,8 +215,36 @@ #define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216 #endif -#ifndef CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND -#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 300 +// One macro used to set the read timeout for both sides. They want different +// defaults: a client's read timeout is the caller's own tool (it waits forever +// until asked not to), while a server keeps a ceiling that reclaims a worker +// from a peer that has gone quiet. The old name still works and sets both. +#ifdef CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND +#pragma message( \ + "CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND is deprecated; define " \ + "CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND and/or " \ + "CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND instead") +#ifndef CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND +#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND \ + CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND +#endif +#ifndef CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND +#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND \ + CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND +#endif +#endif + +// 0 waits forever. A read timeout is how a caller gets control back to send on +// the same connection; it is not a liveness check (that is ping/pong). Only a +// timeout set at runtime through set_read_timeout() is reported as +// ws::Timeout; when one of these compile-time defaults elapses, read() returns +// ws::Fail and closes the connection. +#ifndef CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND +#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND 0 +#endif + +#ifndef CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND +#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND 300 #endif #ifndef CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND @@ -1817,10 +1845,12 @@ struct Response { std::string file_content_path_; std::string file_content_content_type_; - // Content coding chosen for a file-backed content provider, decided once - // where the file is opened so that the ETag and the body cannot disagree. - // `EncodingType::None` for every other kind of response. - detail::EncodingType file_content_encoding_ = detail::EncodingType::None; + // Content coding chosen for the response body, decided once so that the + // headers and the body cannot disagree: where the file is opened for a + // file-backed content provider (keeping the ETag honest), and in + // `apply_ranges()` for a chunked content provider. `EncodingType::None` + // for every other kind of response. + detail::EncodingType content_coding_ = detail::EncodingType::None; }; enum class Error { @@ -2359,6 +2389,7 @@ class Server { bool parse_request_line(const char *s, Request &req) const; detail::EncodingType static_file_encoding(const Request &req, + const Response &res, const std::string &content_type, size_t length) const; bool apply_static_file_compression(const Request &req, Response &res) const; @@ -3663,6 +3694,9 @@ ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags); EncodingType encoding_type(const Request &req, const std::string &content_type); +EncodingType encoding_type(const Request &req, const Response &res, + const std::string &content_type); + EncodingType encoding_type(const Request &req, const Response &res); class BufferStream final : public Stream { @@ -4345,7 +4379,11 @@ enum class CloseStatus : uint16_t { InternalError = 1011, }; -enum ReadResult : int { Fail = 0, Text = 1, Binary = 2 }; +// Timeout is returned only when a read timeout was set and it elapsed before +// any byte of a frame arrived: nothing was consumed and the connection is +// still open, so the caller can send on it and read again. `msg` is left +// untouched, so a `while (ws.read(msg))` loop must not treat it as a message. +enum ReadResult : int { Fail = 0, Text = 1, Binary = 2, Timeout = 3 }; // Result of WebSocketClient::connect(). Truthy only when the WebSocket // upgrade handshake fully succeeded. On failure error() identifies the @@ -4405,6 +4443,18 @@ class WebSocket { const Request &request() const; bool is_open() const; + // Bound how long read() waits before returning Timeout. 0 waits forever. + // A server handler owns its connection's timeout this way; a client sets it + // through WebSocketClient. Safe to call while another thread is in read(). + // + // Only a timeout set here is reported as Timeout. The compile-time default + // (CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND) is a backstop rather + // than a request for control, so when it elapses read() returns Fail and + // closes the connection, and `while (ws.read(msg))` ends as it always has. + void set_read_timeout(time_t sec, time_t usec = 0); + template <class Rep, class Period> + void set_read_timeout(const std::chrono::duration<Rep, Period> &duration); + private: friend class httplib::Server; friend class WebSocketClient; @@ -4440,6 +4490,10 @@ class WebSocket { int max_missed_pongs_; int unacked_pings_ = 0; std::atomic<bool> closed_{false}; + // Set once the caller has bounded read() through set_read_timeout(). Until + // then the timeout in effect is the compile-time default, and elapsing it + // is a failure that closes the connection, not a Timeout. + std::atomic<bool> read_timeout_set_{false}; std::mutex write_mutex_; // Owned by whichever thread is parsing frames off strm_. Only one thread // may do so: read_websocket_frame() reads a payload until it has the whole @@ -4527,8 +4581,9 @@ class WebSocketClient { bool is_valid_ = false; socket_t sock_ = INVALID_SOCKET; std::unique_ptr<WebSocket> ws_; - time_t read_timeout_sec_ = CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND; + time_t read_timeout_sec_ = CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND; time_t read_timeout_usec_ = 0; + bool read_timeout_set_ = false; // see WebSocket::read_timeout_set_ time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND; time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND; time_t websocket_ping_interval_sec_ = @@ -4560,6 +4615,13 @@ class WebSocketClient { #endif }; +template <class Rep, class Period> +inline void WebSocket::set_read_timeout( + const std::chrono::duration<Rep, Period> &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); +} + template <class Rep, class Period> inline void WebSocketClient::set_read_timeout( const std::chrono::duration<Rep, Period> &duration) { @@ -4586,8 +4648,14 @@ namespace impl { bool is_valid_utf8(const std::string &s); -bool read_websocket_frame(Stream &strm, Opcode &opcode, std::string &payload, - bool &fin, bool expect_masked, size_t max_len); +// Three states, because a failure that consumed bytes and one that consumed +// none are not the same thing: the first has left the stream in the middle of +// a frame and the connection cannot be reused, the second can just be retried. +enum class FrameRead { Ok, Fail, Timeout }; + +FrameRead read_websocket_frame(Stream &strm, Opcode &opcode, + std::string &payload, bool &fin, + bool expect_masked, size_t max_len); } // namespace impl From 77024dd9575eb59e112f8d01a425acb153d6c46c Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Tue, 15 Sep 2026 21:34:53 +0100 Subject: [PATCH 163/212] --ngram-on-disk and lazymode merge (#29) --- README.md | 1 - common/arg.cpp | 28 +-- common/common.cpp | 5 - common/common.h | 5 - docs/speculative-prefill.md | 2 - include/llama.h | 7 - src/CMakeLists.txt | 1 - src/llama-model.cpp | 8 +- src/llama-ple-disk.cpp | 335 ------------------------------------ src/llama-ple-disk.h | 38 ---- src/models/models.h | 6 - src/models/qwen4exp.cpp | 73 +------- tests/test-arg-parser.cpp | 7 + 13 files changed, 18 insertions(+), 498 deletions(-) delete mode 100644 src/llama-ple-disk.cpp delete mode 100644 src/llama-ple-disk.h diff --git a/README.md b/README.md index cff3f3bdc62d..3d61cafa38d1 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,6 @@ Everything else is upstream `llama.cpp`. The additions currently carried here: | Change | Flag / switch | What it does | | --- | --- | --- | | Speculative prefill | `--spec-prefill` | A small draft model scores prompt tokens by attention importance so the target model only prefills the ones that matter, cutting time-to-first-token on long prompts | -| N-gram table on disk | `--ngram-on-disk`, `--ngram-cache`, `--ngram-io-threads` | Keeps a model's n-gram hash-embedding table (28.8 GB on Qwen3.8-Flash-Next) off the memory budget entirely, reading only the rows each batch actually gathers | | Adaptive speculative draft length | `--spec-draft-adaptive` | Sizes each draft from a measured per-sequence acceptance EMA rather than always drafting `--spec-draft-n-max`; speeds up MTP and DFlash | | Vulkan fixes and tuning for RDNA 3.5 | | Driver-gated coopmat LDS stride padding, UMA bulk readback gated on host-cached mappings, IQ3_S mat-vec at batch sizes > 4, and a radix top-k kernel for large k | | Hidden server presets | `hidden` in the models `.ini` | Keep a model loadable by name while omitting it from `GET /models` | diff --git a/common/arg.cpp b/common/arg.cpp index f5bce417e7a4..481f85499115 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -2773,35 +2773,11 @@ common_params_context common_params_parser_init(common_params & params, llama_ex ).set_env("LLAMA_ARG_OVERRIDE_TENSOR")); add_opt(common_arg( {"--ngram-on-disk"}, - "keep the model's n-gram hash-embedding table (per_layer_token_embd, 28.8 GB on Qwen3.8-Flash-Next)\n" - "on disk: it is never mapped or loaded, each batch reads just the rows it gathers from the GGUF.\n" - "Only qwen4exp has such a table; on any other model this is a no-op", + "deprecated alias for --lazy-mode on", [](common_params & params) { - params.ple_on_disk = true; + params.lazy_mode = LLAMA_LAZY_MODE_ON; } ).set_env("LLAMA_ARG_NGRAM_ON_DISK")); - add_opt(common_arg( - {"--ngram-io-threads"}, "N", - string_format("threads reading n-gram rows for --ngram-on-disk (default: %d)", params.ple_io_threads), - [](common_params & params, int value) { - params.ple_io_threads = value; - } - ).set_env("LLAMA_ARG_NGRAM_IO_THREADS")); - add_opt(common_arg( - {"--ngram-cache"}, "MiB", - string_format("in-memory cache of recently read n-gram rows for --ngram-on-disk, 0 disables (default: %d)", params.ple_cache_mb), - [](common_params & params, int value) { - params.ple_cache_mb = value; - } - ).set_env("LLAMA_ARG_NGRAM_CACHE")); - add_opt(common_arg( - {"--ngram-direct-io"}, - {"--no-ngram-direct-io"}, - string_format("read n-gram rows with O_DIRECT so they bypass the page cache (default: %s)", params.ple_direct_io ? "enabled" : "disabled"), - [](common_params & params, bool value) { - params.ple_direct_io = value; - } - ).set_env("LLAMA_ARG_NGRAM_DIRECT_IO")); add_opt(common_arg( {"-cmoe", "--cpu-moe"}, "keep all Mixture of Experts (MoE) weights in the CPU", diff --git a/common/common.cpp b/common/common.cpp index ef1ccc4d6a7e..bc9f1a7d2692 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -1692,11 +1692,6 @@ struct llama_model_params common_model_params_to_llama(common_params & params) { mparams.check_tensors = params.check_tensors; mparams.use_extra_bufts = !params.no_extra_bufts; mparams.no_host = params.no_host; - mparams.ple_on_disk = params.ple_on_disk; - mparams.ple_direct_io = params.ple_direct_io; - mparams.ple_io_threads = params.ple_io_threads; - mparams.ple_cache_mb = params.ple_cache_mb; - if (params.kv_overrides.empty()) { mparams.kv_overrides = NULL; } else { diff --git a/common/common.h b/common/common.h index 127844681968..abbe46d62c13 100644 --- a/common/common.h +++ b/common/common.h @@ -603,11 +603,6 @@ struct common_params { bool no_op_offload = false; // globally disable offload host tensor operations to device bool no_extra_bufts = false; // disable extra buffer types (used for weight repacking) bool no_host = false; // bypass host buffer allowing extra buffers to be used - bool ple_on_disk = false; // keep the n-gram hash-embedding table on disk (qwen4exp) - bool ple_direct_io = true; // ... read with O_DIRECT - int32_t ple_io_threads = 64; // ... parallel readers (random 4 KiB reads: this NVMe gives 62k IOPS at 16, 130k at 64, ~160k at 128+) - int32_t ple_cache_mb = 256; // ... row cache, 0 disables - bool single_turn = false; // single turn chat conversation ggml_type cache_type_k = GGML_TYPE_F16; // KV cache data type for the K diff --git a/docs/speculative-prefill.md b/docs/speculative-prefill.md index e7eb9d571c15..952d374a1634 100644 --- a/docs/speculative-prefill.md +++ b/docs/speculative-prefill.md @@ -79,8 +79,6 @@ Flash-Next gains less than the dense 27B because it is hybrid: most layers are l so baseline prefill is already closer to linear in N. MTP acceptance and decode rate hold across keep ratios. -The IQ4_XS build reaches 4,718 -> 1,599 ms over the same sweep when run with `--ngram-on-disk`. - ## Caveats - Greedy (temperature 0) output is not reproducible on this box even with no speculation and no diff --git a/include/llama.h b/include/llama.h index 0f6fd55a95a0..2bafdde0c986 100644 --- a/include/llama.h +++ b/include/llama.h @@ -326,10 +326,6 @@ extern "C" { // the GPU that is used for the entire model when split_mode is LLAMA_SPLIT_MODE_NONE int32_t main_gpu; - // n-gram hash-embedding table kept on disk (see ple_on_disk below) - int32_t ple_io_threads; // parallel pread workers - int32_t ple_cache_mb; // in-memory cache of recently read rows, 0 disables - // proportion of the model (layers or rows) to offload to each GPU, size: llama_max_devices() const float * tensor_split; @@ -351,9 +347,6 @@ extern "C" { bool no_host; // bypass host buffer allowing extra buffers to be used bool no_alloc; // only load metadata and simulate memory allocations bool load_mtp; // whether to load MTP layers - bool ple_on_disk; // keep the n-gram hash-embedding table (per_layer_token_embd) on disk: never - // mapped or loaded, the rows a batch needs are read from the file (qwen4exp) - bool ple_direct_io; // read those rows with O_DIRECT, bypassing the page cache }; struct llama_sampler_seq_config { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8dbaee3902c1..afdaddc79de8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,7 +33,6 @@ set(LLAMA_CORE_SOURCES llama-memory-hybrid-idx.cpp llama-memory-recurrent.cpp llama-mmap.cpp - llama-ple-disk.cpp llama-model-loader.cpp llama-model-saver.cpp llama-model.cpp diff --git a/src/llama-model.cpp b/src/llama-model.cpp index 6aab7355fde9..a7a163e27cf9 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -1706,9 +1706,7 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) { } } - // With the n-gram table left on disk, a populated mapping would pull the table's - // third of the file resident for nothing; readahead alone carries the sequential load. - ml.init_mappings(!params.ple_on_disk, use_mlock ? &pimpl->mlock_mmaps : nullptr); + ml.init_mappings(true, use_mlock ? &pimpl->mlock_mmaps : nullptr); pimpl->mappings.reserve(ml.mappings.size()); // create the backend buffers @@ -2770,8 +2768,6 @@ llama_model_params llama_model_default_params() { /*.load_mode =*/ LLAMA_LOAD_MODE_AUTO, /*.lazy_mode =*/ LLAMA_LAZY_MODE_AUTO, /*.main_gpu =*/ 0, - /*.ple_io_threads =*/ 64, - /*.ple_cache_mb =*/ 256, /*.tensor_split =*/ nullptr, /*.progress_callback =*/ nullptr, /*.progress_callback_user_data =*/ nullptr, @@ -2782,8 +2778,6 @@ llama_model_params llama_model_default_params() { /*.no_host =*/ false, /*.no_alloc =*/ false, /*.load_mtp =*/ false, - /*.ple_on_disk =*/ false, - /*.ple_direct_io =*/ true, }; return result; diff --git a/src/llama-ple-disk.cpp b/src/llama-ple-disk.cpp deleted file mode 100644 index 08263746b863..000000000000 --- a/src/llama-ple-disk.cpp +++ /dev/null @@ -1,335 +0,0 @@ -#include "llama-ple-disk.h" - -#include "llama-impl.h" - -#include <algorithm> -#include <atomic> -#include <chrono> -#include <condition_variable> -#include <cstring> -#include <mutex> -#include <stdexcept> -#include <thread> -#include <vector> - -#if !defined(_WIN32) -#include <cerrno> -#include <fcntl.h> -#include <unistd.h> -#endif - -struct llama_ple_disk::impl { - std::string fname; - int fd = -1; - bool direct = false; - size_t offs = 0; - ggml_type type = GGML_TYPE_COUNT; - int64_t ne0 = 0; - int64_t nrows = 0; - size_t rs = 0; // bytes per row - size_t block = 4096; // O_DIRECT alignment for offset, length and buffer - ggml_to_float_t to_float = nullptr; - - // direct-mapped cache of raw rows: slot = row & mask, tag = row - size_t n_slots = 0; - size_t mask = 0; - std::vector<int64_t> tags; - std::vector<uint8_t> slab; - - // per-gather scratch, reused - std::vector<int32_t> uniq; - std::vector<uint8_t> raw; // [uniq.size(), rs] - std::vector<std::pair<int32_t, uint32_t>> misses; // (row, index into uniq) - uint8_t * bounce0 = nullptr; // main-thread bounce buffer - - std::mutex mtx; // one gather at a time; a model is shared by every context built on it - - // reader pool, started on first use - int32_t n_threads = 1; - std::vector<std::thread> workers; - std::mutex pm; - std::condition_variable cv_work; - std::condition_variable cv_done; - uint64_t gen = 0; - size_t pending = 0; - std::atomic<size_t> next{0}; - bool stop = false; - - uint64_t st_calls = 0, st_rows = 0, st_uniq = 0, st_hits = 0, st_reads = 0, st_bytes = 0; - double st_ms = 0; - - impl(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p) - : fname(fname), offs(offs), type(type), ne0(ne0), nrows(nrows) { -#if defined(_WIN32) - throw std::runtime_error("llama_ple_disk: not supported on Windows"); -#else - if (ggml_is_quantized(type) && ne0 % ggml_blck_size(type) != 0) { - throw std::runtime_error(format("llama_ple_disk: row of %lld %s elements is not a whole number of blocks", - (long long) ne0, ggml_type_name(type))); - } - rs = ggml_row_size(type, ne0); - if (type != GGML_TYPE_F32) { - to_float = ggml_get_type_traits(type)->to_float; - if (to_float == nullptr) { - throw std::runtime_error(format("llama_ple_disk: no dequantizer for %s", ggml_type_name(type))); - } - } - - direct = p.direct_io; - if (direct) { - // only Linux has O_DIRECT; Darwin turns the page cache off per descriptor - // with F_NOCACHE after the open -#if defined(O_DIRECT) - fd = open(fname.c_str(), O_RDONLY | O_DIRECT | O_CLOEXEC); -#else - fd = open(fname.c_str(), O_RDONLY | O_CLOEXEC); -#if defined(F_NOCACHE) - if (fd >= 0 && fcntl(fd, F_NOCACHE, 1) < 0) { - LLAMA_LOG_WARN("%s: F_NOCACHE on %s failed (%s); reads go through the page cache\n", - __func__, fname.c_str(), strerror(errno)); - direct = false; - } -#else - direct = false; -#endif -#endif - if (fd < 0) { - LLAMA_LOG_WARN("%s: direct open of %s failed (%s); falling back to buffered reads\n", - __func__, fname.c_str(), strerror(errno)); - direct = false; - } - } - if (fd < 0) { - fd = open(fname.c_str(), O_RDONLY | O_CLOEXEC); - if (fd < 0) { - throw std::runtime_error(format("llama_ple_disk: failed to open %s: %s", fname.c_str(), strerror(errno))); - } - } - - n_threads = std::max<int32_t>(1, p.n_threads); - - if (p.cache_bytes >= rs) { - size_t n = p.cache_bytes / rs; - n_slots = 1; - while (n_slots * 2 <= n) { - n_slots *= 2; - } - mask = n_slots - 1; - tags.assign(n_slots, -1); - slab.resize(n_slots * rs); - } -#endif - } - - ~impl() { -#if !defined(_WIN32) - { - std::lock_guard<std::mutex> lk(pm); - stop = true; - } - cv_work.notify_all(); - for (auto & w : workers) { - w.join(); - } - free(bounce0); - if (fd >= 0) { - close(fd); - } -#endif - } - -#if !defined(_WIN32) - size_t bounce_size() const { - return ((rs + block - 1) / block) * block + 2 * block; - } - - uint8_t * alloc_bounce() const { - void * ptr = nullptr; - if (posix_memalign(&ptr, block, bounce_size()) != 0) { - throw std::runtime_error("llama_ple_disk: posix_memalign failed"); - } - return (uint8_t *) ptr; - } - - // read `len` bytes at `off` into `dst`; a short read is only tolerated past `need` - void pread_full(uint8_t * dst, size_t len, off_t off, size_t need) const { - size_t got = 0; - while (got < len) { - const ssize_t r = pread(fd, dst + got, len - got, off + (off_t) got); - if (r < 0) { - if (errno == EINTR) { - continue; - } - GGML_ABORT("llama_ple_disk: pread(%s, %zu @ %lld) failed: %s", - fname.c_str(), len, (long long) off, strerror(errno)); - } - if (r == 0) { - break; // EOF - } - got += (size_t) r; - } - if (got < need) { - GGML_ABORT("llama_ple_disk: short read in %s: %zu of %zu bytes at %lld", - fname.c_str(), got, need, (long long) off); - } - } - - void read_row(int64_t row, uint8_t * dst, uint8_t * bounce) const { - const off_t off = (off_t) offs + (off_t) row * (off_t) rs; - if (!direct) { - pread_full(dst, rs, off, rs); - return; - } - const off_t a0 = off & ~(off_t) (block - 1); - const size_t need = (size_t) (off - a0) + rs; - const size_t len = ((need + block - 1) / block) * block; - pread_full(bounce, len, a0, need); - memcpy(dst, bounce + (off - a0), rs); - } - - void worker() { - uint8_t * bounce = direct ? alloc_bounce() : nullptr; - uint64_t seen = 0; - for (;;) { - { - std::unique_lock<std::mutex> lk(pm); - cv_work.wait(lk, [&] { return stop || gen != seen; }); - if (stop) { - break; - } - seen = gen; - } - for (;;) { - const size_t i = next.fetch_add(1); - if (i >= misses.size()) { - break; - } - read_row(misses[i].first, raw.data() + (size_t) misses[i].second * rs, bounce); - } - { - std::lock_guard<std::mutex> lk(pm); - if (--pending == 0) { - cv_done.notify_one(); - } - } - } - free(bounce); - } - - void run_misses() { - if (n_threads <= 1 || misses.size() <= 2) { - if (direct && bounce0 == nullptr) { - bounce0 = alloc_bounce(); - } - for (const auto & m : misses) { - read_row(m.first, raw.data() + (size_t) m.second * rs, bounce0); - } - return; - } - if (workers.empty()) { - workers.reserve(n_threads); - for (int32_t i = 0; i < n_threads; ++i) { - workers.emplace_back([this] { worker(); }); - } - } - { - std::lock_guard<std::mutex> lk(pm); - next = 0; - pending = workers.size(); - ++gen; - } - cv_work.notify_all(); - std::unique_lock<std::mutex> lk(pm); - cv_done.wait(lk, [&] { return pending == 0; }); - } - - void gather(const int32_t * idx, size_t n, float * dst) { - std::lock_guard<std::mutex> lk(mtx); - const auto t0 = std::chrono::steady_clock::now(); - - uniq.assign(idx, idx + n); - std::sort(uniq.begin(), uniq.end()); - uniq.erase(std::unique(uniq.begin(), uniq.end()), uniq.end()); - if (!uniq.empty() && (uniq.front() < 0 || (int64_t) uniq.back() >= nrows)) { - GGML_ABORT("llama_ple_disk: row index out of range (%d..%d of %lld rows)", - uniq.front(), uniq.back(), (long long) nrows); - } - - raw.resize(uniq.size() * rs); - misses.clear(); - for (size_t i = 0; i < uniq.size(); ++i) { - if (n_slots) { - const size_t slot = (size_t) uniq[i] & mask; - if (tags[slot] == uniq[i]) { - memcpy(raw.data() + i * rs, slab.data() + slot * rs, rs); - continue; - } - } - misses.emplace_back(uniq[i], (uint32_t) i); - } - - if (!misses.empty()) { - run_misses(); - if (n_slots) { - for (const auto & m : misses) { - const size_t slot = (size_t) m.first & mask; - tags[slot] = m.first; - memcpy(slab.data() + slot * rs, raw.data() + (size_t) m.second * rs, rs); - } - } - } - - for (size_t k = 0; k < n; ++k) { - const size_t i = (size_t) (std::lower_bound(uniq.begin(), uniq.end(), idx[k]) - uniq.begin()); - const uint8_t * src = raw.data() + i * rs; - float * out = dst + k * (size_t) ne0; - if (to_float) { - to_float(src, out, ne0); - } else { - memcpy(out, src, rs); - } - } - - st_calls += 1; - st_rows += n; - st_uniq += uniq.size(); - st_hits += uniq.size() - misses.size(); - st_reads += misses.size(); - st_bytes += misses.size() * (direct ? block : rs); - st_ms += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count(); - } -#endif -}; - -llama_ple_disk::llama_ple_disk(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p) - : pimpl(std::make_unique<impl>(fname, offs, type, ne0, nrows, p)) {} - -llama_ple_disk::~llama_ple_disk() { - if (pimpl->st_calls) { - LLAMA_LOG_INFO("%s: %llu batches, %llu rows gathered (%llu unique): %llu cache hits, %llu disk reads (%.1f MiB), %.1f ms total\n", - __func__, (unsigned long long) pimpl->st_calls, (unsigned long long) pimpl->st_rows, - (unsigned long long) pimpl->st_uniq, (unsigned long long) pimpl->st_hits, - (unsigned long long) pimpl->st_reads, pimpl->st_bytes / (1024.0 * 1024.0), pimpl->st_ms); - } -} - -void llama_ple_disk::gather(const int32_t * idx, size_t n, float * dst) { -#if defined(_WIN32) - GGML_UNUSED(idx); GGML_UNUSED(n); GGML_UNUSED(dst); - GGML_ABORT("llama_ple_disk: not supported on Windows"); -#else - pimpl->gather(idx, n, dst); -#endif -} - -int64_t llama_ple_disk::n_rows() const { return pimpl->nrows; } -int64_t llama_ple_disk::ne0() const { return pimpl->ne0; } -size_t llama_ple_disk::row_size() const { return pimpl->rs; } - -std::string llama_ple_disk::describe() const { - return format("%s @ %zu: %lld rows x %lld %s (%zu B/row, %.2f GiB), %s I/O, %d threads, row cache %zu MiB", - pimpl->fname.c_str(), pimpl->offs, (long long) pimpl->nrows, (long long) pimpl->ne0, - ggml_type_name(pimpl->type), pimpl->rs, (double) pimpl->nrows * pimpl->rs / (1024.0 * 1024.0 * 1024.0), - pimpl->direct ? "direct" : "buffered", pimpl->n_threads, - pimpl->n_slots * pimpl->rs / (1024 * 1024)); -} diff --git a/src/llama-ple-disk.h b/src/llama-ple-disk.h deleted file mode 100644 index e8af7c58ded3..000000000000 --- a/src/llama-ple-disk.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "ggml.h" - -#include <cstdint> -#include <memory> -#include <string> - -// A weight table that stays in the GGUF file. Nothing is mapped or loaded for it at -// model load; each batch reads exactly the rows it gathers, with pread, and hands -// them over dequantized. -// -// Built for the qwen4exp n-gram hash embedding (per_layer_token_embd): 320 M rows of -// 90 bytes, a third of the model's bytes, of which one token touches 16 at unrelated -// hashed offsets. The working set is a few thousand rows per batch, so the table has -// no business being resident -- on a unified-memory machine it competes with the -// weights and the KV cache for the same RAM. -struct llama_ple_disk { - struct params { - int32_t n_threads = 64; // parallel readers: random reads on NVMe need queue depth - size_t cache_bytes = 256u << 20; // direct-mapped cache of raw rows; 0 disables - bool direct_io = true; // O_DIRECT, so the rows never enter the page cache either - }; - - llama_ple_disk(const std::string & fname, size_t offs, ggml_type type, int64_t ne0, int64_t nrows, const params & p); - ~llama_ple_disk(); - - // dequantize rows idx[0..n) into dst, laid out [ne0, n]; aborts on an out-of-range index - void gather(const int32_t * idx, size_t n, float * dst); - - int64_t n_rows() const; - int64_t ne0() const; - size_t row_size() const; - std::string describe() const; - - struct impl; - std::unique_ptr<impl> pimpl; -}; diff --git a/src/models/models.h b/src/models/models.h index bc4238926a6f..822d2089bd7a 100644 --- a/src/models/models.h +++ b/src/models/models.h @@ -2358,17 +2358,11 @@ struct llama_model_qwen35 : public llama_model_base { }; -struct llama_ple_disk; - 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; - // Set when the n-gram table is left on disk (--ngram-on-disk): per_layer_tok_embd is - // counted as created but never allocated, mapped or read, and build_ple takes its - // rows from this reader instead of ggml_get_rows. - std::shared_ptr<llama_ple_disk> ple_disk; void load_arch_hparams(llama_model_loader & ml) override; void load_arch_tensors(llama_model_loader & ml) override; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 0a378ca14fbb..4c831eeebb50 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -2,7 +2,6 @@ #include "llama-impl.h" #include "llama-memory-hybrid-idx.h" #include "llama-memory-recurrent.h" -#include "llama-ple-disk.h" #include <algorithm> #include <cinttypes> @@ -189,25 +188,8 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { ple_rows = ple_w->tensor->ne[1]; } - // on disk the tensor is counted as created but never allocated, mapped or read: each - // ubatch preads exactly the rows it gathers (llm_graph_input_ple::set_input). A model - // with no tensor to read from stays on the lazy path. - if (params.ple_on_disk && ple_w) { - llama_ple_disk::params dp; - dp.n_threads = params.ple_io_threads; - dp.cache_bytes = params.ple_cache_mb > 0 ? (size_t) params.ple_cache_mb << 20 : 0; - dp.direct_io = params.ple_direct_io; - - ple_disk = std::make_shared<llama_ple_disk>(ml.fnames.at(ple_w->idx), ple_w->offs, - ple_w->tensor->type, ple_w->tensor->ne[0], ple_rows, dp); - LLAMA_LOG_INFO("%s: PLE n-gram table stays on disk: %s\n", __func__, ple_disk->describe().c_str()); - - per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), - { hparams.ple_head_dim, ple_rows }, TENSOR_SKIP); - } else { - per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), - { hparams.ple_head_dim, ple_rows }, TENSOR_READ_LAZY); - } + per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), + { hparams.ple_head_dim, ple_rows }, TENSOR_READ_LAZY); } // An MTP-only file carries just the draft block. Keep walking the trunk so the @@ -1249,23 +1231,10 @@ class llm_graph_input_ple : public llm_graph_input_i { const int64_t n_want = (int64_t) pmodel.hparams.ple_n_heads * params.ubatch.n_tokens; - // exactly one of the two is built: `rows` when the table is a tensor and the - // graph does the get_rows, `embd` when --ngram-on-disk gathered it in set_input. - // Dereferencing `rows` unconditionally segfaults on every on-disk decode. - if (rows) { - return rows->ne[0] == n_want; // I32 [n_heads*n_tokens] - } - if (embd) { - return embd->ne[1] == n_want; // F32 [ple_head_dim, n_heads*n_tokens] - } - return false; + return rows->ne[0] == n_want; } ggml_tensor * rows = nullptr; // I32 [ple_n_heads * n_tokens] - ggml_tensor * embd = nullptr; // F32 [ple_head_dim, ple_n_heads * n_tokens]: the gathered rows, when the table is on disk - - std::vector<float> embd_buf; - std::vector<int32_t> rows_global; const llama_model_qwen4exp & pmodel; @@ -1336,19 +1305,6 @@ void llm_graph_input_ple::set_input(const llama_ubatch * ubatch) { } } - if (pmodel.ple_disk) { - // idx is already exactly what the reader wants: one global row per (token, head), - // token-major, with ple_head_offsets folded in by the loop above. The previous - // version read it head-major and added the offset a second time, which is only - // correct for a table split per head -- on a joined per_layer_token_embd it walks - // past the end (rows beyond ple_rows) and gather() aborts. - GGML_ASSERT(embd != nullptr && rows == nullptr); - embd_buf.resize(idx.size() * (size_t) hp.ple_head_dim); - pmodel.ple_disk->gather(idx.data(), idx.size(), embd_buf.data()); - ggml_backend_tensor_set(embd, embd_buf.data(), 0, embd_buf.size() * sizeof(float)); - return; - } - ggml_backend_tensor_set(rows, idx.data(), 0, idx.size()*ggml_element_size(rows)); } @@ -1416,26 +1372,13 @@ ggml_tensor * llama_model_qwen4exp::graph::build_inp_ple( auto ple_inp = std::make_unique<llm_graph_input_ple>( static_cast<const llama_model_qwen4exp &>(model), mctx_hyb->get_attn()); - const auto & qmodel = static_cast<const llama_model_qwen4exp &>(model); - ggml_tensor * rows = nullptr; - ggml_tensor * emb = nullptr; - if (qmodel.ple_disk) { - // table on disk: the gather happened in set_input and arrives as an F32 input, - // token-major, which reshapes to what the per-head gathers + concat produce - ple_inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, hparams.ple_head_dim, n_heads * n_tokens); - ggml_set_input(ple_inp->embd); - emb = ple_inp->embd; - } else { - ple_inp->rows = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_heads * n_tokens); - ggml_set_input(ple_inp->rows); - rows = ple_inp->rows; - } + ggml_tensor * rows = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_heads * n_tokens); + ple_inp->rows = rows; + ggml_set_input(rows); res->add_input(std::move(ple_inp)); - if (rows != nullptr) { - // gather then flatten the heads: get_rows lays the head dimension out slowest, as the reference does - emb = ggml_get_rows(ctx0, model.per_layer_tok_embd, rows); - } + // gather then flatten the heads: get_rows lays the head dimension out slowest, as the reference does + ggml_tensor * emb = ggml_get_rows(ctx0, model.per_layer_tok_embd, rows); emb = ggml_reshape_2d(ctx0, emb, hparams.ple_head_dim * n_heads, n_tokens); cb(emb, "ple_embd", -1); diff --git a/tests/test-arg-parser.cpp b/tests/test-arg-parser.cpp index e0907631abd8..fc71e2ab8cc9 100644 --- a/tests/test-arg-parser.cpp +++ b/tests/test-arg-parser.cpp @@ -294,6 +294,13 @@ static void test(void) { assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); assert(params.load_mode == LLAMA_LOAD_MODE_DIRECT_IO); + { + common_params lazy_params; + argv = {"binary_name", "-m", "model_file.gguf", "--ngram-on-disk"}; + assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), lazy_params, LLAMA_EXAMPLE_COMMON)); + assert(lazy_params.lazy_mode == LLAMA_LAZY_MODE_ON); + } + // multi-value args (CSV) argv = {"binary_name", "--lora", "file1.gguf,\"file2,2.gguf\",\"file3\"\"3\"\".gguf\",file4\".gguf"}; assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); From 8a400d0d043fb4645c800763cba0e736bacf21e1 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Tue, 15 Sep 2026 22:01:52 +0100 Subject: [PATCH 164/212] ggml: recover two upstream commits dropped during the #28 sync (#30) Co-authored-by: Hongqiang Wang <wangh@qti.qualcomm.com> Co-authored-by: Li He <lih@qti.qualcomm.com> Co-authored-by: Sandro Steeger <78495486+Stastez@users.noreply.github.com> --- ggml/src/ggml-cuda/allreduce.cu | 58 ++++++++++--------- ggml/src/ggml-cuda/allreduce.cuh | 2 +- ggml/src/ggml-cuda/vendors/hip.h | 4 ++ ggml/src/ggml-opencl/ggml-opencl.cpp | 45 ++++++++------ ggml/src/ggml-opencl/kernels/moe_reorder_b.cl | 12 ++-- 5 files changed, 72 insertions(+), 49 deletions(-) diff --git a/ggml/src/ggml-cuda/allreduce.cu b/ggml/src/ggml-cuda/allreduce.cu index d56129a227e5..39b23bed75d3 100644 --- a/ggml/src/ggml-cuda/allreduce.cu +++ b/ggml/src/ggml-cuda/allreduce.cu @@ -1,6 +1,6 @@ #include "allreduce.cuh" -#if !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA) +#if !defined(GGML_USE_MUSA) #include "convert.cuh" #include "ggml-impl.h" @@ -11,11 +11,12 @@ #include <limits> // --------------------------------------------------------------------------- -// CUDA AllReduce for tensor-parallel inference across two GPUs. +// AllReduce for tensor-parallel inference across two GPUs (CUDA or +// ROCm/HIP). // -// Provides an in-place sum reduction over matching tensors on two CUDA -// devices in the same process. Used by the tensor-split path alongside -// NCCL; targets setups without NVLink, where data is exchanged between the +// Provides an in-place sum reduction over matching tensors on two GPUs +// in the same process. Used by the tensor-split path alongside NCCL; +// targets setups without NVLink/xGMI, where data is exchanged between the // GPUs by staging it through pinned host memory over PCIe. // // Two reduction strategies are selected per call by tensor size: @@ -161,11 +162,14 @@ static __global__ void ggml_cuda_ar_kernel( __threadfence_system(); // make our signal visible system-wide while (ggml_cuda_ar_signal_get(other_slot) != token) { -#if __CUDA_ARCH__ >= GGML_CUDA_CC_VOLTA +#ifdef GGML_USE_HIP + // Equals ~100ns at 2500 MHz (sleeps for n * [1,64] clock cycles) + __builtin_amdgcn_s_sleep(4); +#elif __CUDA_ARCH__ >= GGML_CUDA_CC_VOLTA __nanosleep(100); #else NO_DEVICE_CODE; -#endif // __CUDA_ARCH__ >= GGML_CUDA_CC_VOLTA +#endif // GGML_USE_HIP } } @@ -280,7 +284,7 @@ struct ggml_cuda_ar_host_mapping { } rc = cudaHostGetDevicePointer(reinterpret_cast<void **>(&dev), host, 0); if (rc != cudaSuccess) { - cudaFreeHost(host); + CUDA_CHECK(cudaFreeHost(host)); host = nullptr; dev = nullptr; } @@ -289,7 +293,7 @@ struct ggml_cuda_ar_host_mapping { void free() { if (host) { - cudaFreeHost(host); + CUDA_CHECK(cudaFreeHost(host)); host = nullptr; dev = nullptr; } @@ -401,7 +405,8 @@ ggml_cuda_ar_pipeline * ggml_cuda_ar_pipeline_init(const int * devices, size_t n return nullptr; } - // The chunked kernel uses __nanosleep, which is sm70+ (Volta+). + // The chunked kernel uses __nanosleep (NVIDIA, sm70+) or + // __builtin_amdgcn_s_sleep (AMD). for (size_t i = 0; i < n_devices; ++i) { const int cc = ggml_cuda_info().devices[devices[i]].cc; if (cc < GGML_CUDA_CC_VOLTA) { @@ -543,7 +548,7 @@ void ggml_cuda_ar_pipeline_free(ggml_cuda_ar_pipeline * p) { for (int i = 0; i < p->n_devices; ++i) { if (p->streams[i]) { ggml_cuda_set_device(p->devices[i]); - cudaStreamSynchronize(p->streams[i]); + CUDA_CHECK(cudaStreamSynchronize(p->streams[i])); } } @@ -552,28 +557,28 @@ void ggml_cuda_ar_pipeline_free(ggml_cuda_ar_pipeline * p) { p->host_large[i].free(); if (p->dev_tmp[i]) { ggml_cuda_set_device(p->devices[i]); - cudaFree(p->dev_tmp[i]); + CUDA_CHECK(cudaFree(p->dev_tmp[i])); } ggml_cuda_set_device(p->devices[i]); for (int s = 0; s < GGML_CUDA_AR_POOL_SIZE; ++s) { - if (p->ev_pool[i][s].app) { cudaEventDestroy(p->ev_pool[i][s].app); } + if (p->ev_pool[i][s].app) { CUDA_CHECK(cudaEventDestroy(p->ev_pool[i][s].app)); } for (int c = 0; c < GGML_CUDA_AR_COPY_MAX_CHUNKS; ++c) { - if (p->ev_pool[i][s].cpy[c]) { cudaEventDestroy(p->ev_pool[i][s].cpy[c]); } + if (p->ev_pool[i][s].cpy[c]) { CUDA_CHECK(cudaEventDestroy(p->ev_pool[i][s].cpy[c])); } } - if (p->ev_pool[i][s].h2d) { cudaEventDestroy(p->ev_pool[i][s].h2d); } - if (p->ev_pool[i][s].ker) { cudaEventDestroy(p->ev_pool[i][s].ker); } + if (p->ev_pool[i][s].h2d) { CUDA_CHECK(cudaEventDestroy(p->ev_pool[i][s].h2d)); } + if (p->ev_pool[i][s].ker) { CUDA_CHECK(cudaEventDestroy(p->ev_pool[i][s].ker)); } } if (p->host_large_read_done[i]) { ggml_cuda_set_device(p->devices[i]); - cudaEventDestroy(p->host_large_read_done[i]); + CUDA_CHECK(cudaEventDestroy(p->host_large_read_done[i])); } if (p->dev_tmp_kernel_done[i]) { ggml_cuda_set_device(p->devices[i]); - cudaEventDestroy(p->dev_tmp_kernel_done[i]); + CUDA_CHECK(cudaEventDestroy(p->dev_tmp_kernel_done[i])); } if (p->streams[i]) { ggml_cuda_set_device(p->devices[i]); - cudaStreamDestroy(p->streams[i]); + CUDA_CHECK(cudaStreamDestroy(p->streams[i])); } } p->arrival.free(); @@ -952,13 +957,14 @@ bool ggml_cuda_ar_allreduce( return ok; } -#else // defined(GGML_USE_HIP) || defined(GGML_USE_MUSA) +#else // defined(GGML_USE_MUSA) -// HIP and MUSA lack the host-mapped pinned-memory APIs (cudaHostAllocPortable -// / cudaHostAllocMapped / cudaHostGetDevicePointer) and __nanosleep that this -// implementation relies on, so the internal AllReduce is a CUDA-only feature. -// The dispatcher in ggml-cuda.cu treats a nullptr pipeline as "init failed" -// and silently falls back to the meta backend's generic AllReduce. +// MUSA lacks the host-mapped pinned-memory APIs (cudaHostAllocPortable +// / cudaHostAllocMapped / cudaHostGetDevicePointer) and a device-side +// sleep intrinsic that this implementation relies on, so the internal +// AllReduce is unavailable there. The dispatcher in ggml-cuda.cu treats +// a nullptr pipeline as "init failed" and silently falls back to the meta +// backend's generic AllReduce. ggml_cuda_ar_pipeline * ggml_cuda_ar_pipeline_init(const int *, size_t) { return nullptr; } @@ -968,4 +974,4 @@ bool ggml_cuda_ar_allreduce(ggml_cuda_ar_pipeline *, ggml_backend_t *, ggml_tens return false; } -#endif // !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA) +#endif // !defined(GGML_USE_MUSA) diff --git a/ggml/src/ggml-cuda/allreduce.cuh b/ggml/src/ggml-cuda/allreduce.cuh index 0f2c9518d5d8..76205d323aa0 100644 --- a/ggml/src/ggml-cuda/allreduce.cuh +++ b/ggml/src/ggml-cuda/allreduce.cuh @@ -9,7 +9,7 @@ struct ggml_cuda_ar_pipeline; // Allocate a pipeline for n_devices GPUs. -// devices[] holds the CUDA device IDs in rank order. +// devices[] holds the GPU device IDs in rank order. // Returns nullptr on allocation failure. ggml_cuda_ar_pipeline * ggml_cuda_ar_pipeline_init( const int * devices, size_t n_devices); diff --git a/ggml/src/ggml-cuda/vendors/hip.h b/ggml/src/ggml-cuda/vendors/hip.h index 2fc0fe9fdbb7..48d4eb2ce3e2 100644 --- a/ggml/src/ggml-cuda/vendors/hip.h +++ b/ggml/src/ggml-cuda/vendors/hip.h @@ -73,6 +73,10 @@ #define cudaGetDeviceProperties hipGetDeviceProperties #define cudaGetErrorString hipGetErrorString #define cudaGetLastError hipGetLastError +#define cudaHostAlloc hipHostMalloc +#define cudaHostAllocPortable hipHostMallocPortable +#define cudaHostAllocMapped hipHostMallocMapped +#define cudaHostGetDevicePointer hipHostGetDevicePointer #define cudaHostRegister hipHostRegister #define cudaHostRegisterPortable hipHostRegisterPortable #define cudaHostRegisterReadOnly hipHostRegisterReadOnly diff --git a/ggml/src/ggml-opencl/ggml-opencl.cpp b/ggml/src/ggml-opencl/ggml-opencl.cpp index 5b99f5d00536..bd5af9e3781c 100644 --- a/ggml/src/ggml-opencl/ggml-opencl.cpp +++ b/ggml/src/ggml-opencl/ggml-opencl.cpp @@ -24566,10 +24566,33 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0, CL_CHECK(clReleaseMemObject(buf_src2)); } else { // for gemm - kernel = backend_ctx->kernel_gemm_moe_q4_0_f32_ns; - if (backend_ctx->kernel_gemm_moe_q4_0_f32_ns_bin) { - kernel = backend_ctx->kernel_gemm_moe_q4_0_f32_ns_bin; - } + // dp4a (int8) prefill GEMM variant + static const char * q4_0_moe_dp4a_env = getenv("GGML_OPENCL_Q4_0_MOE_DP4A"); + + // It turns out that the prebuilt kernel only outperforms the dp4a variant (on X2-90) + // at very large routing counts, so we gate its use accordingly using moe_bin_min, + // which can be overridden via the GGML_OPENCL_MOE_BIN_MIN_ROUTINGS environment variable. + // The routing count is ne20 * ne21 (n_expert_used * n_tokens). + static const char * moe_bin_min_env = getenv("GGML_OPENCL_MOE_BIN_MIN_ROUTINGS"); + const int moe_bin_min = moe_bin_min_env ? atoi(moe_bin_min_env) : 4096; + + // whether bin kernels are available + const bool bin_available = backend_ctx->kernel_gemm_moe_q4_0_f32_ns_bin != nullptr; + const bool dp4a_bin_available = backend_ctx->kernel_gemm_moe_q4_0_q8_1_dp4a_bin != nullptr; + + bool use_moe_dp4a = q4_0_moe_dp4a_env + ? (atoi(q4_0_moe_dp4a_env) != 0) + : (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E + && (dp4a_bin_available || !bin_available + || (int)(ne20 * ne21) < moe_bin_min)); + // dot prod has to be available + use_moe_dp4a = backend_ctx->has_integer_dot && use_moe_dp4a; + + const bool use_bin_kernel = bin_available && !use_moe_dp4a; + + kernel = use_bin_kernel + ? backend_ctx->kernel_gemm_moe_q4_0_f32_ns_bin + : backend_ctx->kernel_gemm_moe_q4_0_f32_ns; // Reorder router if called from test-backend-ops or when new router is generated. // Otherwise reuse the reordered result from previous mul_mat_id call. @@ -24582,18 +24605,6 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0, cl_mem buf_src1_reordered = nullptr, image_src1_reordered = nullptr; cl_mem buf_src2, buf_src2_emap; - // dp4a (int8) prefill GEMM variant - static const char * q4_0_moe_dp4a_env = getenv("GGML_OPENCL_Q4_0_MOE_DP4A"); - bool use_moe_dp4a = q4_0_moe_dp4a_env - ? (atoi(q4_0_moe_dp4a_env) != 0) - : (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 - 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; region.size = sizeof(int) * max_post_router_tile * n_tile_size; @@ -24632,7 +24643,7 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0, cl_image_desc image_desc_buf_src1; image_format_buf_src1 = {CL_RGBA, CL_FLOAT}; image_desc_buf_src1 = {CL_MEM_OBJECT_IMAGE1D_BUFFER, static_cast<size_t>(ne00 * max_post_router_tile * n_tile_size / 4), 0,0,0,0,0,0,0, {buf_src1_reordered}}; - if (backend_ctx->kernel_gemm_moe_q4_0_f32_ns_bin) { + if (use_bin_kernel) { // bin kernel uses slightly different image format image_format_buf_src1 = {CL_R, CL_FLOAT}; image_desc_buf_src1.image_width = static_cast<size_t>(ne00 * max_post_router_tile * n_tile_size); diff --git a/ggml/src/ggml-opencl/kernels/moe_reorder_b.cl b/ggml/src/ggml-opencl/kernels/moe_reorder_b.cl index e6295c81648e..2f5c110bf7e7 100644 --- a/ggml/src/ggml-opencl/kernels/moe_reorder_b.cl +++ b/ggml/src/ggml-opencl/kernels/moe_reorder_b.cl @@ -20,11 +20,13 @@ kernel void kernel_moe_reorder_b( uint router_idx = router[post_router_idx]; - float4 out = (float4)(0); - if (router_idx != 0xFFFFFFFF) { - ushort activation_idx = router_idx / map_ratio; - out = src[activation_idx * K / 4 + k_4]; + // Padded slots need not be written at all. The MoE GEMMs accumulate per output + // column and scatter only the real columns, so whatever sits in a padded slot + // never reaches dst + if (router_idx == 0xFFFFFFFF) { + return; } - dst[post_router_idx * K / 4 + k_4] = out; + ushort activation_idx = router_idx / map_ratio; + dst[post_router_idx * K / 4 + k_4] = src[activation_idx * K / 4 + k_4]; } From 4bc01d0e59fbeb11090ac061b332c764a4b4a4ad Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Tue, 15 Sep 2026 22:36:39 +0100 Subject: [PATCH 165/212] docs: add upstream sync procedure (#31) --- SYNC.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 SYNC.md diff --git a/SYNC.md b/SYNC.md new file mode 100644 index 000000000000..7f6bf7712ed9 --- /dev/null +++ b/SYNC.md @@ -0,0 +1,71 @@ +# Syncing halo-box/llama.cpp with upstream + +## Why GitHub says "N commits behind" after a sync + +GitHub computes that number as `git rev-list --count HEAD..upstream/master` — commits +reachable from upstream but not from us. It is a **graph reachability** question and has +nothing to do with whether our files match upstream's. + +A squash merge copies content without copying ancestry: the resulting commit's only parent +is our previous tip, so upstream's commits never become our ancestors. The counter stays +pinned forever, and worse, `git merge-base` stays pinned too — so the *next* sync replays +every already-applied commit as a conflict. + +Squash is the right tool for collapsing our own PRs. It is the wrong tool for ingesting +upstream. + +## Preferred: real merge + +```sh +git fetch upstream +git merge upstream/master +``` + +Merge commits are noisy. They are also correct, and they keep merge-base moving. Resolve +conflicts once, in the merge, and the ancestry is recorded for free. + +## When a PR + squash was unavoidable + +If divergence forced the sync through a PR (conflict resolution needs review), the content +lands but the ancestry does not. Two things must then happen. + +### 1. Audit for dropped commits + +Conflict resolution silently drops upstream hunks. Find them before recording the merge: + +```sh +MB=$(git merge-base HEAD upstream/master) + +# Files upstream touched that we never touched => upstream change dropped entirely +git diff --name-only $MB upstream/master | while read -r f; do + git diff --quiet HEAD upstream/master -- "$f" && continue + n=$(git diff --numstat $MB HEAD -- "$f" | awk '{print $1+$2}') + [ -z "$n" ] && echo "DROPPED: $f" +done +``` + +Recover each one with `git cherry-pick -x <sha>`. Re-run until the list is empty. + +A file that differs from upstream *and* that we modified is normal fork divergence — review +it by hand, the script cannot judge intent. + +### 2. Record the ancestry + +Only once the audit is clean: + +```sh +git merge -s ours upstream/master -m "Record upstream sync (content applied via #<PR>)" +``` + +`-s ours` creates a two-parent merge commit and keeps our tree **byte-identical** — it +cannot change a single file, so it cannot break the build. Its only danger is burying an +upstream change we failed to apply, which is exactly what step 1 rules out. + +Never run `-s ours` before the audit. + +## Verifying a sync + +```sh +git rev-list --count HEAD..upstream/master # expect 0 +git log -1 --format='%P' HEAD # expect two parents after -s ours +``` From 0636c9aee4a2ab1d38bb7e8c038227c6634c3857 Mon Sep 17 00:00:00 2001 From: "Piotr Wilkin (ilintar)" <piotr.wilkin@syndatis.com> Date: Wed, 16 Sep 2026 19:14:10 +0200 Subject: [PATCH 166/212] pwilkin/strix-halo official merge (#63) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> --- common/arg.cpp | 2 + common/speculative.cpp | 6 +- conversion/base.py | 1 + conversion/qwen.py | 3 +- conversion/qwen4exp.py | 9 +- convert_hf_to_gguf.py | 10 + ggml/src/ggml-cuda/conv-tail.cuh | 105 ++ ggml/src/ggml-cuda/dequantize.cuh | 165 ++- ggml/src/ggml-cuda/expand.cu | 63 + ggml/src/ggml-cuda/expand.cuh | 14 + ggml/src/ggml-cuda/fattn.cu | 27 + ggml/src/ggml-cuda/gdn-conv.cu | 163 +++ ggml/src/ggml-cuda/gdn-conv.cuh | 18 + ggml/src/ggml-cuda/getrows.cu | 106 ++ ggml/src/ggml-cuda/getrows.cuh | 4 + ggml/src/ggml-cuda/ggml-cuda.cu | 1000 ++++++++++++++-- ggml/src/ggml-cuda/hyperconn.cu | 176 ++- ggml/src/ggml-cuda/hyperconn.cuh | 6 + ggml/src/ggml-cuda/idx-relu-sum.cu | 25 + ggml/src/ggml-cuda/idx-relu-sum.cuh | 8 + ggml/src/ggml-cuda/mmb-quant.cuh | 336 ++++++ ggml/src/ggml-cuda/mmb.cu | 964 +++++++++++++++ ggml/src/ggml-cuda/mmb.cuh | 27 + ggml/src/ggml-cuda/mmid.cu | 119 ++ ggml/src/ggml-cuda/mmid.cuh | 6 + ggml/src/ggml-cuda/mmq.cu | 14 +- ggml/src/ggml-cuda/moe-weighted-reduction.cu | 149 ++- ggml/src/ggml-cuda/moe-weighted-reduction.cuh | 3 +- ggml/src/ggml-cuda/norm-gated.cu | 134 +++ ggml/src/ggml-cuda/norm-gated.cuh | 14 + ggml/src/ggml-cuda/ple-conv.cu | 299 +++++ ggml/src/ggml-cuda/ple-conv.cuh | 19 + ggml/src/ggml-cuda/qsa-decode-wmma.cuh | 121 ++ ggml/src/ggml-cuda/qsa-decode.cu | 151 +++ ggml/src/ggml-cuda/qsa-decode.cuh | 4 + ggml/src/ggml-cuda/qsa-prefill.cu | 495 ++++++++ ggml/src/ggml-cuda/qsa-prefill.cuh | 4 + ggml/src/ggml-cuda/top-k.cu | 83 +- gguf-py/gguf/constants.py | 14 + gguf-py/gguf/tensor_mapping.py | 9 + include/llama.h | 1 + src/llama-arch.cpp | 6 + src/llama-arch.h | 3 + src/llama-context.cpp | 11 +- src/llama-graph.cpp | 51 +- src/llama-memory-hybrid-idx.cpp | 434 ++++++- src/llama-memory-hybrid-idx.h | 52 + src/llama-memory-recurrent.cpp | 14 +- src/llama-model-loader.cpp | 2 +- src/llama-model.cpp | 39 +- src/llama-model.h | 4 + src/llama-ple-disk.cpp | 158 ++- src/llama-ple-disk.h | 7 + src/models/qwen4exp.cpp | 592 ++++++++-- src/prefix.h | 34 + src/qsa-prefix-state.h | 53 + tests/CMakeLists.txt | 2 + tests/test-backend-ops.cpp | 1030 ++++++++++++++++- tests/test-ple-disk.cpp | 173 +++ tests/test-qsa-prefix.cpp | 41 + tools/llama-bench/llama-bench.cpp | 6 +- tools/server/server-context.cpp | 8 + 62 files changed, 7221 insertions(+), 376 deletions(-) create mode 100644 ggml/src/ggml-cuda/conv-tail.cuh create mode 100644 ggml/src/ggml-cuda/expand.cu create mode 100644 ggml/src/ggml-cuda/expand.cuh create mode 100644 ggml/src/ggml-cuda/gdn-conv.cu create mode 100644 ggml/src/ggml-cuda/gdn-conv.cuh create mode 100644 ggml/src/ggml-cuda/idx-relu-sum.cu create mode 100644 ggml/src/ggml-cuda/idx-relu-sum.cuh create mode 100644 ggml/src/ggml-cuda/mmb-quant.cuh create mode 100644 ggml/src/ggml-cuda/mmb.cu create mode 100644 ggml/src/ggml-cuda/mmb.cuh create mode 100644 ggml/src/ggml-cuda/norm-gated.cu create mode 100644 ggml/src/ggml-cuda/norm-gated.cuh create mode 100644 ggml/src/ggml-cuda/ple-conv.cu create mode 100644 ggml/src/ggml-cuda/ple-conv.cuh create mode 100644 ggml/src/ggml-cuda/qsa-decode-wmma.cuh create mode 100644 ggml/src/ggml-cuda/qsa-decode.cu create mode 100644 ggml/src/ggml-cuda/qsa-decode.cuh create mode 100644 ggml/src/ggml-cuda/qsa-prefill.cu create mode 100644 ggml/src/ggml-cuda/qsa-prefill.cuh create mode 100644 src/prefix.h create mode 100644 src/qsa-prefix-state.h create mode 100644 tests/test-ple-disk.cpp create mode 100644 tests/test-qsa-prefix.cpp diff --git a/common/arg.cpp b/common/arg.cpp index be5f8ebe34bd..3ac2a09c2990 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -2729,10 +2729,12 @@ common_params_context common_params_parser_init(common_params & params, llama_ex {"-lzm", "--lazy-mode"}, "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" + "- on-direct: like on, but the arch reads the rows with explicit pread()s instead of demand paging the mmap (currently only PLE tables; same reader as --ngram-on-disk, page-cached)\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.lazy_mode = LLAMA_LAZY_MODE_ON; } + else if (value == "on-direct") { params.lazy_mode = LLAMA_LAZY_MODE_DIRECT; } else if (value == "auto") { params.lazy_mode = LLAMA_LAZY_MODE_AUTO; } else if (value == "off") { params.lazy_mode = LLAMA_LAZY_MODE_OFF; } else { throw std::invalid_argument("invalid value"); } diff --git a/common/speculative.cpp b/common/speculative.cpp index 05fc1aed7921..507f5f08f286 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -1543,7 +1543,11 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { llama_set_embeddings_nextn(ctx_tgt, true, /*masked*/ false); llama_set_embeddings_nextn(ctx_dft, true, /*masked*/ true); - is_mem_shared = llama_get_ctx_other(ctx_dft) == ctx_tgt; + // only the gemma4 assistant shares the target's memory; other drafts (a qwen4exp shared sidecar) set ctx_other + // merely to borrow the target's embeddings and LM head + char arch[64] = {0}; + llama_model_meta_val_str(llama_get_model(ctx_dft), "general.architecture", arch, sizeof(arch)); + is_mem_shared = llama_get_ctx_other(ctx_dft) == ctx_tgt && std::strcmp(arch, "gemma4-assistant") == 0; chain_heads = n_mtp_layers > 1 && !is_mem_shared; if (chain_heads) { diff --git a/conversion/base.py b/conversion/base.py index d2d80be3688b..4badf3cfb06a 100644 --- a/conversion/base.py +++ b/conversion/base.py @@ -120,6 +120,7 @@ class ModelBase: supports_mtp_export: bool = False mtp_only: bool = False no_mtp: bool = False + mtp_shared_embd: bool = False def __init__(self, dir_model: Path, ftype: gguf.LlamaFileType, fname_out: Path, *, is_big_endian: bool = False, use_temp_file: bool = False, eager: bool = False, diff --git a/conversion/qwen.py b/conversion/qwen.py index c7e0809f38c4..d50a2295e278 100644 --- a/conversion/qwen.py +++ b/conversion/qwen.py @@ -290,6 +290,7 @@ class _QwenMtpMixin: tensor_map: gguf.TensorNameMap no_mtp: bool mtp_only: bool + mtp_shared_embd: bool _original_block_count: int | None = None opt_num_mtp_layers: int = 0 @@ -338,7 +339,7 @@ def filter_tensors(cls, item): elif len(parts) == 3 and parts[1] in remapper: name = f"model.layers.{cls._original_block_count}.{remapper[parts[1]]}.{parts[2]}" elif cls.mtp_only: - keep = name in ( + keep = not cls.mtp_shared_embd and name in ( "model.embed_tokens.weight", "model.norm.weight", "lm_head.weight", "embed_tokens.weight", "norm.weight", ) diff --git a/conversion/qwen4exp.py b/conversion/qwen4exp.py index e23cf56fa706..0d2a57d9dba7 100644 --- a/conversion/qwen4exp.py +++ b/conversion/qwen4exp.py @@ -72,7 +72,7 @@ def set_gguf_parameters(self): # 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: + if not ple_layers or self.mtp_only: return self.gguf_writer.add_ple_layers(ple_layers) self.gguf_writer.add_ple_ngram_size(hp["ngram_size"]) @@ -114,9 +114,12 @@ def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Ca # no trunk, so that one becomes the file's mixer; a full model already has the # trunk's, and graph_mtp reads it, so this copy is dropped. if name.startswith("mtp.hyper_connection_mixer."): - if cls.no_mtp or not cls.mtp_only: + if cls.no_mtp: return None - return (name.replace("mtp.", "model.", 1), item[1]) + # the draft's mixer is its own (it differs from the trunk's), so it stays + # with the draft block as blk.N.nextn.hc_head_* in both file layouts + assert cls._original_block_count is not None + return (f"model.layers.{cls._original_block_count}.{name[len('mtp.'):]}", item[1]) return super().filter_tensors(item) def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]: diff --git a/convert_hf_to_gguf.py b/convert_hf_to_gguf.py index e09616b190cf..51614f941ccd 100755 --- a/convert_hf_to_gguf.py +++ b/convert_hf_to_gguf.py @@ -125,6 +125,10 @@ def parse_args() -> argparse.Namespace: "--no-nextn", "--no-mtp", dest="no_mtp", action="store_true", help="Exclude NextN speculative draft tensors from the converted GGUF. Pair with --mtp or --dspark on a second run to publish target and draft as two files.", ) + parser.add_argument( + "--mtp-shared-embd", action="store_true", + help="With --mtp, leave the token embeddings, output norm and LM head out of the draft and take them from the target model at load time. Much smaller draft, but it needs a llama.cpp new enough to read it.", + ) parser.add_argument( "--dspark", action="store_true", help="Export only the DeepSeek-V4 DSpark draft tensors as a separate GGUF.", @@ -282,6 +286,12 @@ def main() -> None: if args.mtp: model_class.mtp_only = True + if args.mtp_shared_embd: + if not args.mtp: + logger.error("--mtp-shared-embd only applies together with --mtp") + sys.exit(1) + model_class.mtp_shared_embd = True + model_instance = model_class(dir_model, output_type, fname_out, is_big_endian=args.bigendian, use_temp_file=args.use_temp_file, eager=args.no_lazy, diff --git a/ggml/src/ggml-cuda/conv-tail.cuh b/ggml/src/ggml-cuda/conv-tail.cuh new file mode 100644 index 000000000000..c943211d7c8e --- /dev/null +++ b/ggml/src/ggml-cuda/conv-tail.cuh @@ -0,0 +1,105 @@ +#pragma once +#include "common.cuh" + +// Readers of a conv-history concat that a tail-only materialization can serve: the rollback-slot views of the +// last H columns, their CONTs, and copies of either. Anything else reading the concat needs the whole tensor. +static inline int64_t ggml_cuda_conv_tail_view_col(const ggml_tensor * t, const ggml_tensor * cc, int64_t H) { + if (t->op != GGML_OP_VIEW || t->view_src != cc || t->type != GGML_TYPE_F32 || + t->ne[0] != H || t->ne[1] != cc->ne[1] || t->ne[2] != cc->ne[2] || t->ne[3] != 1 || + t->nb[1] != cc->nb[1] || t->nb[2] != cc->nb[2] || t->view_offs % sizeof(float) != 0) { + return -1; + } + const int64_t col = (int64_t) (t->view_offs / sizeof(float)); + return (col >= 0 && col + H <= cc->ne[0]) ? col : -1; +} + +static inline bool ggml_cuda_conv_tail_reader(const ggml_tensor * t, const ggml_tensor * cc, int64_t H, int64_t & tail_from) { + if (t->op == GGML_OP_VIEW) { + const int64_t col = ggml_cuda_conv_tail_view_col(t, cc, H); + if (col < 0) { + return false; + } + tail_from = std::min(tail_from, col); + return true; + } + if (t->op == GGML_OP_CONT) { + return t->src[0] && ggml_cuda_conv_tail_view_col(t->src[0], cc, H) >= 0; + } + if (t->op == GGML_OP_CPY) { + const ggml_tensor * s = t->src[0]; + if (!s) { + return false; + } + if (s->op == GGML_OP_CONT) { + s = s->src[0]; + } + return s && ggml_cuda_conv_tail_view_col(s, cc, H) >= 0; + } + return false; +} + +static inline bool ggml_cuda_reads(const ggml_tensor * t, const ggml_tensor * root) { + for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) { + if (t->src[s] == root || t->src[s]->view_src == root) { + return true; + } + } + return false; +} + +// the concat match is dispatched as "this node plus the next": the next node must be a no-op the loop skips anyway +static inline bool ggml_cuda_conv_next_is_noop(const ggml_cgraph * cgraph, int i) { + if (i + 1 >= cgraph->n_nodes) { + return false; + } + const ggml_tensor * t = cgraph->nodes[i + 1]; + return ggml_is_empty(t) || t->op == GGML_OP_RESHAPE || t->op == GGML_OP_TRANSPOSE || + t->op == GGML_OP_VIEW || t->op == GGML_OP_PERMUTE || t->op == GGML_OP_NONE; +} + +// GGML_CONV_FUSE_DEBUG=1 prints why a concat did not match, with the node index the decision was made at +static inline bool ggml_cuda_conv_reject(const char * which, const char * why, int node) { + static const bool dbg = getenv("GGML_CONV_FUSE_DEBUG") != nullptr; + if (dbg) { + fprintf(stderr, "%s: no fusion, %s (node %d)\n", which, why, node); + } + return false; +} + +// nodes after `from` (inclusive) that read t, other than t itself and `except`: a scan, so it works on graph +// copies and views whose use-count tables are not populated +static inline int ggml_cuda_conv_readers(const ggml_cgraph * cgraph, int from, const ggml_tensor * t, const ggml_tensor * except) { + int n = 0; + for (int k = from; k < cgraph->n_nodes; ++k) { + const ggml_tensor * r = cgraph->nodes[k]; + if (r == t || r == except || r->op == GGML_OP_RESHAPE || r->op == GGML_OP_VIEW || r->op == GGML_OP_TRANSPOSE || r->op == GGML_OP_PERMUTE) { + continue; // a view of t is not a consumer: whoever reads the view is counted through view_src + } + if (ggml_cuda_reads(r, t)) { + ++n; + } + } + return n; +} + +// the whole concat, either directly or through a view that covers all of it +static inline bool ggml_cuda_conv_whole(const ggml_tensor * s, const ggml_tensor * cc) { + return s == cc || (s && s->view_src == cc && s->view_offs == 0 && ggml_nelements(s) == ggml_nelements(cc) && ggml_is_contiguous(s)); +} + +// concat src[1] is TRANSPOSE(view of x): x must be the whole contiguous storage of a [C, T] activation, whatever +// shape its producer gave it (qwen4exp's grouped norm leaves it [n_embd, hc, T]); C and T come from the transpose +static inline const ggml_tensor * ggml_cuda_conv_activation(const ggml_tensor * tr, int64_t & C, int64_t & T) { + if (!tr || tr->op != GGML_OP_TRANSPOSE || tr->type != GGML_TYPE_F32 || !tr->view_src || tr->view_offs != 0 || + tr->ne[2] != 1 || tr->ne[3] != 1) { + return nullptr; + } + T = tr->ne[0]; + C = tr->ne[1]; + const ggml_tensor * x = tr->view_src; + if (x->type != GGML_TYPE_F32 || !ggml_is_contiguous(x) || ggml_nelements(x) != C * T || + tr->nb[1] != sizeof(float) || tr->nb[0] != (size_t) C * sizeof(float)) { + return nullptr; + } + return x; +} diff --git a/ggml/src/ggml-cuda/dequantize.cuh b/ggml/src/ggml-cuda/dequantize.cuh index 651524be7b3e..fd9b6d6e01bf 100644 --- a/ggml/src/ggml-cuda/dequantize.cuh +++ b/ggml/src/ggml-cuda/dequantize.cuh @@ -124,8 +124,8 @@ static __device__ __forceinline__ void dequantize_q8_0(const void * vx, const in // Each call dequantizes one super-block of QK_K values into y using the // thread layout of the caller: 32 threads for q4_K, 64 threads otherwise. -template<typename dst_t> -static __device__ __forceinline__ void dequantize_q2_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_q2_K(const void * vx, const int64_t ib, dst_ptr_t yy, const int tid) { const block_q2_K * x = (const block_q2_K *) vx; const int64_t n = tid/32; @@ -133,7 +133,7 @@ static __device__ __forceinline__ void dequantize_q2_K(const void * vx, const in const int64_t is = 8*n + l/16; const uint8_t q = x[ib].qs[32*n + l]; - dst_t * y = yy + 128*n; + auto y = yy + 128*n; float dall = __low2half(x[ib].dm); float dmin = __high2half(x[ib].dm); @@ -143,8 +143,8 @@ static __device__ __forceinline__ void dequantize_q2_K(const void * vx, const in y[l+96] = ggml_cuda_cast<dst_t>(dall * (x[ib].scales[is+6] & 0xF) * ((q >> 6) & 3) - dmin * (x[ib].scales[is+6] >> 4)); } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_q3_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_q3_K(const void * vx, const int64_t ib, dst_ptr_t yy, const int tid) { const block_q3_K * x = (const block_q3_K *) vx; const int64_t r = tid/4; @@ -165,7 +165,7 @@ static __device__ __forceinline__ void dequantize_q3_K(const void * vx, const in float d_all = x[ib].d; float dl = d_all * (us - 32); - dst_t * y = yy + 128*n + 32*j; + auto y = yy + 128*n + 32*j; const uint8_t * q = x[ib].qs + 32*n; const uint8_t * hm = x[ib].hmask; @@ -183,8 +183,8 @@ static inline __device__ void get_scale_min_k4(int j, const uint8_t * q, uint8_t } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_q4_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_q4_K(const void * vx, const int64_t ib, dst_ptr_t yy, const int tid) { const block_q4_K * x = (const block_q4_K *) vx; // assume 32 threads @@ -193,7 +193,7 @@ static __device__ __forceinline__ void dequantize_q4_K(const void * vx, const in const int64_t is = 2*il; const int64_t n = 4; - dst_t * y = yy + 64*il + n*ir; + auto y = yy + 64*il + n*ir; const float dall = __low2half(x[ib].dm); const float dmin = __high2half(x[ib].dm); @@ -211,8 +211,8 @@ static __device__ __forceinline__ void dequantize_q4_K(const void * vx, const in } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_q5_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_q5_K(const void * vx, const int64_t ib, dst_ptr_t yy, const int tid) { const block_q5_K * x = (const block_q5_K *) vx; // assume 64 threads - this is very slightly better than the one below @@ -220,7 +220,7 @@ static __device__ __forceinline__ void dequantize_q5_K(const void * vx, const in const int64_t ir = tid%16; // ir is in 0...15 const int64_t is = 2*il; // is is in 0...6 - dst_t * y = yy + 64*il + 2*ir; + auto y = yy + 64*il + 2*ir; const float dall = __low2half(x[ib].dm); const float dmin = __high2half(x[ib].dm); @@ -242,8 +242,8 @@ static __device__ __forceinline__ void dequantize_q5_K(const void * vx, const in y[33] = ggml_cuda_cast<dst_t>(d2 * ((ql[ 1] >> 4) + (qh[ 1] & hm ? 16 : 0)) - m2); } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_q6_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_q6_K(const void * vx, const int64_t ib, dst_ptr_t yy, const int tid) { const block_q6_K * x = (const block_q6_K *) vx; // assume 64 threads - this is very slightly better than the one below @@ -251,7 +251,7 @@ static __device__ __forceinline__ void dequantize_q6_K(const void * vx, const in const int64_t il = tid - 32*ip; // 0...32 const int64_t is = 8*ip + il/16; - dst_t * y = yy + 128*ip + il; + auto y = yy + 128*ip + il; const float d = x[ib].d; @@ -270,14 +270,14 @@ static __device__ __forceinline__ void dequantize_q6_K(const void * vx, const in // Each call dequantizes one super-block of QK_K values into y with 32 // threads; iq4_nl packs QK_K/QK4_NL sub-blocks per super-block. -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq2_xxs(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq2_xxs(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq2_xxs * x = (const block_iq2_xxs *) vx; const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 8*il; + auto y = yy + 32*ib + 8*il; const uint16_t * q2 = x[ibs].qs + 4*ib; const uint8_t * aux8 = (const uint8_t *)q2; const uint8_t * grid = (const uint8_t *)(iq2xxs_grid + aux8[il]); @@ -289,14 +289,14 @@ static __device__ __forceinline__ void dequantize_iq2_xxs(const void * vx, const } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq2_xs(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq2_xs(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq2_xs * x = (const block_iq2_xs *) vx; const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 8*il; + auto y = yy + 32*ib + 8*il; const uint16_t * q2 = x[ibs].qs + 4*ib; const uint8_t * grid = (const uint8_t *)(iq2xs_grid + (q2[il] & 511)); const float d = (float)x[ibs].d * (0.5f + ((x[ibs].scales[ib] >> 4*(il/2)) & 0xf)) * 0.25f; @@ -306,14 +306,14 @@ static __device__ __forceinline__ void dequantize_iq2_xs(const void * vx, const } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq2_s(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq2_s(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq2_s * x = (const block_iq2_s *) vx; const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 8*il; + auto y = yy + 32*ib + 8*il; const uint8_t * grid = (const uint8_t *)(iq2s_grid + (x[ibs].qs[4*ib+il] | ((x[ibs].qh[ib] << (8-2*il)) & 0x300))); const float d = (float)x[ibs].d * (0.5f + ((x[ibs].scales[ib] >> 4*(il/2)) & 0xf)) * 0.25f; const uint8_t signs = x[ibs].qs[QK_K/8+4*ib+il]; @@ -322,14 +322,14 @@ static __device__ __forceinline__ void dequantize_iq2_s(const void * vx, const i } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq3_xxs(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq3_xxs(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq3_xxs * x = (const block_iq3_xxs *) vx; const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 8*il; + auto y = yy + 32*ib + 8*il; const uint8_t * q3 = x[ibs].qs + 8*ib; const uint16_t * gas = (const uint16_t *)(x[ibs].qs + QK_K/4) + 2*ib; const uint8_t * grid1 = (const uint8_t *)(iq3xxs_grid + q3[2*il+0]); @@ -343,14 +343,14 @@ static __device__ __forceinline__ void dequantize_iq3_xxs(const void * vx, const } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq3_s(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq3_s(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq3_s * x = (const block_iq3_s *) vx; const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 8*il; + auto y = yy + 32*ib + 8*il; const uint8_t * qs = x[ibs].qs + 8*ib; const uint8_t * grid1 = (const uint8_t *)(iq3s_grid + (qs[2*il+0] | ((x[ibs].qh[ib] << (8-2*il)) & 256))); const uint8_t * grid2 = (const uint8_t *)(iq3s_grid + (qs[2*il+1] | ((x[ibs].qh[ib] << (7-2*il)) & 256))); @@ -362,14 +362,14 @@ static __device__ __forceinline__ void dequantize_iq3_s(const void * vx, const i } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq1_s(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq1_s(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq1_s * x = (const block_iq1_s *) vx; const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 8*il; + auto y = yy + 32*ib + 8*il; const float delta = x[ibs].qh[ib] & 0x8000 ? -1 - IQ1S_DELTA : -1 + IQ1S_DELTA; const float d = (float)x[ibs].d * (2*((x[ibs].qh[ib] >> 12) & 7) + 1); uint32_t grid32[2]; const int8_t * q = (const int8_t *)grid32; @@ -381,14 +381,14 @@ static __device__ __forceinline__ void dequantize_iq1_s(const void * vx, const i } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq1_m(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq1_m(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq1_m * x = (const block_iq1_m *) vx; const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 8*il; + auto y = yy + 32*ib + 8*il; const uint16_t * sc = (const uint16_t *)x[ibs].scales; iq1m_scale_t scale; scale.u16 = (sc[0] >> 12) | ((sc[1] >> 8) & 0x00f0) | ((sc[2] >> 4) & 0x0f00) | (sc[3] & 0xf000); @@ -404,14 +404,14 @@ static __device__ __forceinline__ void dequantize_iq1_m(const void * vx, const i } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq4_nl(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq4_nl(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq4_nl * x = (const block_iq4_nl *) vx + ibs*(QK_K/QK4_NL); const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 4*il; + auto y = yy + 32*ib + 4*il; const uint8_t * q4 = x[ib].qs + 4*il; const float d = (float)x[ib].d; for (int j = 0; j < 4; ++j) { @@ -420,13 +420,13 @@ static __device__ __forceinline__ void dequantize_iq4_nl(const void * vx, const } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_iq4_xs(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_iq4_xs(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_iq4_xs * x = (const block_iq4_xs *)vx; const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 4*il; + auto y = yy + 32*ib + 4*il; const uint8_t * q4 = x[ibs].qs + 16*ib + 4*il; const float d = (float)x[ibs].d * ((((x[ibs].scales_l[ib/2] >> 4*(ib%2)) & 0xf) | (((x[ibs].scales_h >> 2*ib) & 3) << 4)) - 32); for (int j = 0; j < 4; ++j) { @@ -435,14 +435,14 @@ static __device__ __forceinline__ void dequantize_iq4_xs(const void * vx, const } } -template<typename dst_t> -static __device__ __forceinline__ void dequantize_mxfp4(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { +template<typename dst_t, typename dst_ptr_t> +static __device__ __forceinline__ void dequantize_mxfp4(const void * vx, const int64_t ibs, dst_ptr_t yy, const int tid) { const block_mxfp4 * x = (const block_mxfp4 *) vx + ibs*(QK_K/QK_MXFP4); const int64_t il = tid/8; // 0...3 const int64_t ib = tid%8; // 0...7 - dst_t * y = yy + 32*ib + 4*il; + auto y = yy + 32*ib + 4*il; const uint8_t * q4 = x[ib].qs + 4*il; const float d = ggml_cuda_e8m0_to_fp32(x[ib].e); for (int j = 0; j < 4; ++j) { @@ -450,3 +450,78 @@ static __device__ __forceinline__ void dequantize_mxfp4(const void * vx, const i y[j+16] = ggml_cuda_cast<dst_t>(d * kvalues_mxfp4[q4[j] >> 4]*0.5f); } } + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_q2_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_q2_K<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_q3_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_q3_K<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_q4_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_q4_K<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_q5_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_q5_K<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_q6_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_q6_K<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq2_xxs(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq2_xxs<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq2_xs(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq2_xs<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq2_s(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq2_s<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq3_xxs(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq3_xxs<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq3_s(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq3_s<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq1_s(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq1_s<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq1_m(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq1_m<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq4_nl(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq4_nl<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_iq4_xs(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_iq4_xs<dst_t, dst_t *>(vx, ib, yy, tid); +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_mxfp4(const void * vx, const int64_t ib, dst_t * yy, const int tid) { + dequantize_mxfp4<dst_t, dst_t *>(vx, ib, yy, tid); +} diff --git a/ggml/src/ggml-cuda/expand.cu b/ggml/src/ggml-cuda/expand.cu new file mode 100644 index 000000000000..ba008ca1f59f --- /dev/null +++ b/ggml/src/ggml-cuda/expand.cu @@ -0,0 +1,63 @@ +#include "common.cuh" +#include "expand.cuh" +#include <cstdio> +#include <cstdlib> +#include <string> + +struct expand_layout { + size_t block_row, score_row, cell_row, tail_row, output_row, cast_block_row, cast_cell_row, expanded_row; + int n_blocks; +}; + +__global__ __launch_bounds__(256) void qsa_expand_complete_blocks_512( + const char * blocks, const char * scores, const char * cells, const char * tail, + char * output, char * cast_blocks, char * cast_cells, char * expanded, expand_layout layout) { + const int query = blockIdx.x; + const int lane = threadIdx.x; + __shared__ int ordered[512]; + const int * selected = reinterpret_cast<const int *>(blocks + query*layout.block_row); + for (int i = lane; i < 512; i += 256) { + ordered[i] = selected[i]; + reinterpret_cast<float *>(cast_blocks + query*layout.cast_block_row)[i] = float(selected[i]); + } + __syncthreads(); + for (int size = 2; size <= 512; size *= 2) { + for (int distance = size/2; distance; distance /= 2) { + for (int i = lane; i < 512; i += 256) { + const int other = i ^ distance; + if (other > i) { + const int a = ordered[i], b = ordered[other]; + if ((i & size) ? a < b : a > b) { ordered[i] = b; ordered[other] = a; } + } + } + __syncthreads(); + } + } + const float * score = reinterpret_cast<const float *>(scores + query*layout.score_row); + const int * remainder = reinterpret_cast<const int *>(tail + query*layout.tail_row); + int * out = reinterpret_cast<int *>(output + query*layout.output_row); + for (int i = lane; i < 2051; i += 256) { + if (i >= 2048) { out[i] = remainder[i-2048]; continue; } + const int block = ordered[i/4]; + if (block < 0 || block >= layout.n_blocks) { out[i] = -1; continue; } + const int cell = reinterpret_cast<const int *>(cells + block*layout.cell_row)[i%4]; + const float valid = __fadd_rn(score[block], 1.0f) > 0.0f ? 1.0f : 0.0f; + const float value = float(cell); + reinterpret_cast<float *>(cast_cells + query*layout.cast_cell_row)[i] = value; + const float shifted = __fadd_rn(value, 1.0f); + const int result = int(__fadd_rn(__fmul_rn(shifted, valid), -1.0f)); + reinterpret_cast<int *>(expanded + query*layout.expanded_row)[i] = result; + out[i] = result; + } +} + +void ggml_cuda_op_qsa_expand(ggml_backend_cuda_context & ctx, const ggml_cuda_qsa_expand_args & a) { + expand_layout layout{a.blocks->nb[1], a.scores->nb[1], a.cells->nb[1], a.tail->nb[1], a.output->nb[1], a.cast_blocks->nb[1], a.cast_cells->nb[2], a.expanded->nb[2], int(a.scores->ne[0])}; + const ggml_cuda_kernel_launch_params launch(dim3(a.output->ne[1]), dim3(256), 0, ctx.stream()); + ggml_cuda_kernel_launch(qsa_expand_complete_blocks_512, launch, + (const char *)a.blocks->data, (const char *)a.scores->data, (const char *)a.cells->data, + (const char *)a.tail->data, (char *)a.output->data, (char *)a.cast_blocks->data, + (char *)a.cast_cells->data, (char *)a.expanded->data, layout); + CUDA_CHECK(cudaGetLastError()); +} + diff --git a/ggml/src/ggml-cuda/expand.cuh b/ggml/src/ggml-cuda/expand.cuh new file mode 100644 index 000000000000..37a1ef7e78c9 --- /dev/null +++ b/ggml/src/ggml-cuda/expand.cuh @@ -0,0 +1,14 @@ +#pragma once +#include "ggml.h" +struct ggml_backend_cuda_context; +struct ggml_cuda_qsa_expand_args { + const ggml_tensor * blocks; + const ggml_tensor * scores; + const ggml_tensor * cells; + const ggml_tensor * tail; + ggml_tensor * cast_blocks; + ggml_tensor * cast_cells; + ggml_tensor * expanded; + ggml_tensor * output; +}; +void ggml_cuda_op_qsa_expand(ggml_backend_cuda_context &, const ggml_cuda_qsa_expand_args &); diff --git a/ggml/src/ggml-cuda/fattn.cu b/ggml/src/ggml-cuda/fattn.cu index a38566111f48..e88684810f21 100644 --- a/ggml/src/ggml-cuda/fattn.cu +++ b/ggml/src/ggml-cuda/fattn.cu @@ -4,6 +4,10 @@ #include "fattn-tile.cuh" #include "fattn-vec.cuh" #include "fattn.cuh" +#if defined(GGML_USE_HIP) +#include "qsa-prefill.cuh" +#include "qsa-decode.cuh" +#endif #if !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA) __launch_bounds__(256, 1) @@ -757,6 +761,29 @@ size_t ggml_cuda_flash_attn_ext_get_alloc_size(int device, const ggml_tensor * d void ggml_cuda_flash_attn_ext(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { ggml_cuda_set_device(ctx.device); +#if defined(GGML_USE_HIP) + if (ggml_cuda_flash_attn_ext_qsa_decode_supported(ctx, dst)) { + ggml_cuda_flash_attn_ext_qsa_decode(ctx, dst); + return; + } + if (ggml_cuda_flash_attn_ext_qsa_prefill_supported(ctx, dst)) { + ggml_cuda_flash_attn_ext_qsa_prefill(ctx, dst); + return; + } +#endif + // a selected-key op without a mask carries its visibility only in src[5], which the dense kernels ignore: + // reaching them would attend to the whole padded cache + if (dst->src[5] && !dst->src[3]) { + const ggml_tensor * q = dst->src[0], * k = dst->src[1], * v = dst->src[2], * ids = dst->src[5]; + GGML_ABORT("flash_attn_ext: maskless selected-key attention was not taken by a QSA kernel (%s): q [%lld,%lld,%lld,%lld] %s nb %zu/%zu, " + "k [%lld,%lld,%lld,%lld] %s nb %zu/%zu/%zu, v %s nb %zu/%zu, ids [%lld,%lld,%lld,%lld] nb %zu, src6 %d src7 %d p4 %d dst contiguous %d", + dst->name, (long long) q->ne[0], (long long) q->ne[1], (long long) q->ne[2], (long long) q->ne[3], ggml_type_name(q->type), q->nb[1], q->nb[2], + (long long) k->ne[0], (long long) k->ne[1], (long long) k->ne[2], (long long) k->ne[3], ggml_type_name(k->type), k->nb[0], k->nb[1], k->nb[2], + ggml_type_name(v->type), v->nb[1], v->nb[2], (long long) ids->ne[0], (long long) ids->ne[1], (long long) ids->ne[2], (long long) ids->ne[3], ids->nb[1], + dst->src[6] != nullptr, dst->src[7] != nullptr, ggml_get_op_params_i32(dst, 4), ggml_is_contiguous(dst)); + } + + switch (ggml_cuda_get_best_fattn_kernel(ggml_cuda_get_device(), dst)) { case BEST_FATTN_KERNEL_NONE: GGML_ABORT("fatal error"); diff --git a/ggml/src/ggml-cuda/gdn-conv.cu b/ggml/src/ggml-cuda/gdn-conv.cu new file mode 100644 index 000000000000..57a80fd290e2 --- /dev/null +++ b/ggml/src/ggml-cuda/gdn-conv.cu @@ -0,0 +1,163 @@ +#include "gdn-conv.cuh" +#include "conv-tail.cuh" +#include "unary.cuh" + +// columns [tail_from, T+3) of every channel row of the concat: what the history copies read +static __global__ void gdn_concat_tail(const float * __restrict__ state, const float * __restrict__ x, float * __restrict__ out, + const int C, const int T, const int tail_from, const int row_stride) { + const int ncols = T + 3 - tail_from; + const int64_t idx = (int64_t) blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= (int64_t) C * ncols) { + return; + } + const int c = (int) (idx / ncols), j = tail_from + (int) (idx % ncols); + out[(size_t) c * row_stride + j] = (j < 3) ? state[c * 3 + j] : x[(size_t) (j - 3) * C + c]; +} + +// the same accumulation as ssm_conv_f32 with a zero bias, then SiLU +template <int TT> +static __global__ void __launch_bounds__(256) gdn_conv_direct_kernel(const float * __restrict__ state, const float * __restrict__ x, + const float * __restrict__ w, float * __restrict__ y, const int C, const int T) { + const int c = blockIdx.x * 256 + threadIdx.x; + const int t0 = blockIdx.y * TT; + if (c >= C) { + return; + } + float wr[4]; +#pragma unroll + for (int j = 0; j < 4; ++j) { + wr[j] = w[c * 4 + j]; + } + float xw[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) { + const int jp = t0 + k; + xw[k] = (jp < 3) ? state[c * 3 + jp] : x[(size_t) (jp - 3) * C + c]; + } + const float b = 0.0f; + const int tend = min(T, t0 + TT); + for (int t = t0; t < tend; ++t) { + float sumf = 0.0f; +#pragma unroll + for (int j = 0; j < 4; ++j) { + sumf += xw[j] * wr[j]; + } + sumf += b; + y[(size_t) t * C + c] = ggml_cuda_op_silu_single(sumf); + xw[0] = xw[1]; xw[1] = xw[2]; xw[2] = xw[3]; + const int jp = t + 4; + xw[3] = (jp < 3) ? state[c * 3 + jp] : ((jp - 3) < T ? x[(size_t) (jp - 3) * C + c] : 0.0f); + } +} + +static bool gdn_conv_check(const ggml_cgraph * cgraph, int i, ggml_cuda_gdn_conv_match & m) { + if (i < 0 || i + 2 >= cgraph->n_nodes) { + return ggml_cuda_conv_reject("gdn_conv", "check 1", i); + } + const ggml_tensor * cc = cgraph->nodes[i]; + if (cc->op != GGML_OP_CONCAT || cc->type != GGML_TYPE_F32 || ggml_get_op_params_i32(cc, 0) != 0 || + (cc->flags & GGML_TENSOR_FLAG_OUTPUT) || !ggml_cuda_conv_next_is_noop(cgraph, i)) { + return ggml_cuda_conv_reject("gdn_conv", "check 2", i); + } + const ggml_tensor * st = cc->src[0]; + const ggml_tensor * tr = cc->src[1]; + if (!st || !tr || st->type != GGML_TYPE_F32 || tr->type != GGML_TYPE_F32 || tr->op != GGML_OP_TRANSPOSE || !tr->view_src) { + return ggml_cuda_conv_reject("gdn_conv", "check 3", i); + } + int64_t C = 0, T = 0; + const ggml_tensor * x = ggml_cuda_conv_activation(tr, C, T); + if (!x) { + return ggml_cuda_conv_reject("gdn_conv", "check 4", i); + } + if (st->ne[0] != 3 || st->ne[1] != C || st->ne[2] != 1 || st->ne[3] != 1 || !ggml_is_contiguous(st)) { + return ggml_cuda_conv_reject("gdn_conv", "check 5", i); + } + if (tr->ne[0] != T || tr->ne[1] != C || tr->ne[2] != 1 || tr->view_offs != 0) { + return ggml_cuda_conv_reject("gdn_conv", "check 6", i); + } + if (cc->ne[0] != T + 3 || cc->ne[1] != C || cc->ne[2] != 1 || cc->ne[3] != 1 || !ggml_is_contiguous(cc) || T < 256 || C % 256 != 0) { + return ggml_cuda_conv_reject("gdn_conv", "check 7", i); + } + + // the conv is the only reader of the whole concat; everything else must be a tail reader + int64_t tail_from = T + 3; + int conv = -1; + for (int n = i + 1; n < cgraph->n_nodes; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + if (!ggml_cuda_reads(t, cc)) { + continue; + } + if (t->op == GGML_OP_SSM_CONV && ggml_cuda_conv_whole(t->src[0], cc)) { + if (conv >= 0) { + return ggml_cuda_conv_reject("gdn_conv", "check 8", n); + } + conv = n; + continue; + } + if (ggml_cuda_conv_whole(t, cc) && (t->op == GGML_OP_RESHAPE || t->op == GGML_OP_VIEW)) { + continue; // shape plumbing into the conv; its readers are checked as readers of the concat + } + if (!ggml_cuda_conv_tail_reader(t, cc, 3, tail_from)) { + return ggml_cuda_conv_reject("gdn_conv", "check 9", n); + } + } + if (conv < 0 || conv + 1 >= cgraph->n_nodes) { + return ggml_cuda_conv_reject("gdn_conv", "check 10", i); + } + const ggml_tensor * cv = cgraph->nodes[conv]; + const ggml_tensor * w = cv->src[1]; + if (cv->type != GGML_TYPE_F32 || cv->src[2] != nullptr || !w || w->type != GGML_TYPE_F32 || w->ne[0] != 4 || w->ne[1] != C || + !ggml_is_contiguous(w) || cv->ne[0] != C || cv->ne[1] != T || cv->ne[2] != 1 || cv->ne[3] != 1 || !ggml_is_contiguous(cv) || + (cv->flags & GGML_TENSOR_FLAG_OUTPUT)) { + return ggml_cuda_conv_reject("gdn_conv", "check 11", i); + } + const ggml_tensor * su = cgraph->nodes[conv + 1]; + if (su->op != GGML_OP_UNARY || ggml_get_unary_op(su) != GGML_UNARY_OP_SILU || su->src[0] != cv || su->type != GGML_TYPE_F32 || + !ggml_is_contiguous(su) || ggml_cuda_conv_readers(cgraph, conv + 1, cv, nullptr) != 1) { + return ggml_cuda_conv_reject("gdn_conv", "check 12", i); + } + m.concat_idx = i; m.conv_idx = conv; m.x = x; m.state = st; m.concat = cc; m.w = w; m.out = cgraph->nodes[conv + 1]; + m.C = C; m.T = T; m.tail_from = std::max<int64_t>(0, tail_from); + return true; +} + +bool ggml_cuda_gdn_conv_match_at_concat(const ggml_cgraph * cgraph, int i, ggml_cuda_gdn_conv_match & m) { + return gdn_conv_check(cgraph, i, m); +} + +bool ggml_cuda_gdn_conv_match_at_conv(const ggml_cgraph * cgraph, int j, ggml_cuda_gdn_conv_match & m) { + if (j < 1 || cgraph->nodes[j]->op != GGML_OP_SSM_CONV) { + return false; + } + // the conv reads the concat directly or through a whole-tensor view; both entry points must find the same node + const ggml_tensor * src = cgraph->nodes[j]->src[0]; + const ggml_tensor * cc = src ? (src->op == GGML_OP_CONCAT ? src : src->view_src) : nullptr; + if (!cc || cc->op != GGML_OP_CONCAT || !ggml_cuda_conv_whole(src, cc)) { + return false; + } + for (int i = j - 1; i >= 0; --i) { + if (cgraph->nodes[i] == cc) { + return gdn_conv_check(cgraph, i, m) && m.conv_idx == j; + } + } + return false; +} + +void ggml_cuda_gdn_conv_write_tail(ggml_backend_cuda_context & ctx, const ggml_cuda_gdn_conv_match & m) { + const int ncols = (int) (m.T + 3 - m.tail_from); + if (ncols <= 0) { + return; + } + const int64_t n = m.C * ncols; + gdn_concat_tail<<<(unsigned) ((n + 255) / 256), 256, 0, ctx.stream()>>>((const float *) m.state->data, (const float *) m.x->data, + (float *) m.concat->data, (int) m.C, (int) m.T, (int) m.tail_from, (int) (m.concat->nb[1] / sizeof(float))); + CUDA_CHECK(cudaGetLastError()); +} + +void ggml_cuda_gdn_conv_direct(ggml_backend_cuda_context & ctx, const ggml_cuda_gdn_conv_match & m) { + constexpr int TT = 128; + dim3 grid((unsigned) (m.C / 256), (unsigned) ((m.T + TT - 1) / TT)); + gdn_conv_direct_kernel<TT><<<grid, 256, 0, ctx.stream()>>>((const float *) m.state->data, (const float *) m.x->data, + (const float *) m.w->data, (float *) m.out->data, (int) m.C, (int) m.T); + CUDA_CHECK(cudaGetLastError()); +} diff --git a/ggml/src/ggml-cuda/gdn-conv.cuh b/ggml/src/ggml-cuda/gdn-conv.cuh new file mode 100644 index 000000000000..e5e3b6a62810 --- /dev/null +++ b/ggml/src/ggml-cuda/gdn-conv.cuh @@ -0,0 +1,18 @@ +#pragma once +#include "common.cuh" + +// Gated DeltaNet prefill convolution: concat(history, x^T) -> ssm_conv -> SiLU. On a match the concat writes only +// the columns its tail copies read and one kernel computes the SiLU output straight from the history and x. +struct ggml_cuda_gdn_conv_match { + int concat_idx = -1, conv_idx = -1; + const ggml_tensor * x = nullptr; // [C, T] F32 contiguous + const ggml_tensor * state = nullptr; // [3, C] F32 contiguous + const ggml_tensor * concat = nullptr; // [T+3, C] F32 + const ggml_tensor * w = nullptr; // [4, C] F32 + ggml_tensor * out = nullptr; // SiLU output [C, T] + int64_t C = 0, T = 0, tail_from = 0; +}; +bool ggml_cuda_gdn_conv_match_at_concat(const ggml_cgraph * cgraph, int i, ggml_cuda_gdn_conv_match & m); +bool ggml_cuda_gdn_conv_match_at_conv(const ggml_cgraph * cgraph, int j, ggml_cuda_gdn_conv_match & m); +void ggml_cuda_gdn_conv_write_tail(ggml_backend_cuda_context & ctx, const ggml_cuda_gdn_conv_match & m); +void ggml_cuda_gdn_conv_direct(ggml_backend_cuda_context & ctx, const ggml_cuda_gdn_conv_match & m); diff --git a/ggml/src/ggml-cuda/getrows.cu b/ggml/src/ggml-cuda/getrows.cu index 6b36b8fbc18c..ffcb51f00912 100644 --- a/ggml/src/ggml-cuda/getrows.cu +++ b/ggml/src/ggml-cuda/getrows.cu @@ -101,6 +101,38 @@ static __global__ void k_get_rows_float( } } +template<typename src0_t, typename dst_t, int cols, int values_per_lane = 1> +static __global__ void k_get_rows_float_packed( + const src0_t * src0, const int32_t * src1, dst_t * dst, + const int64_t ne00, const int64_t ne10, const int64_t ne11, const uint3 ne12_fdv, + const size_t s1, const size_t s2, const size_t s3, + const size_t nb01, const size_t nb02, const size_t nb03, + const size_t s10, const size_t s11, const size_t s12) { + constexpr int rows_per_block = CUDA_GET_ROWS_BLOCK_SIZE / cols; + const int64_t row = int64_t(blockIdx.x) * rows_per_block + threadIdx.x / cols; + const int col = threadIdx.x % cols; + + ggml_cuda_pdl_lc(); + ggml_cuda_pdl_sync(); + if (row >= ne10 || col >= ne00) { + return; + } + + for (int64_t z = blockIdx.y; z < ne11 * int64_t(ne12_fdv.z); z += gridDim.y) { + const uint2 dm = fast_div_modulo(uint32_t(z), ne12_fdv); + const int32_t index = src1[row * s10 + dm.x * s11 + dm.y * s12]; + const src0_t * src_row = (const src0_t *) ((const char *) src0 + index * nb01 + dm.x * nb02 + dm.y * nb03); + dst_t * dst_row = dst + row * s1 + dm.x * s2 + dm.y * s3; +#pragma unroll + for (int v = 0; v < values_per_lane; ++v) { + const int column = col + v * cols; + if (column < ne00) { + dst_row[column] = ggml_cuda_cast<dst_t>(src_row[column]); + } + } + } +} + template<typename dst_t> static __global__ void k_get_rows_float_vec( const dst_t * src0_ptr, const int32_t * src1_ptr, dst_t * dst_ptr, @@ -254,6 +286,40 @@ static void get_rows_cuda_float( GGML_ASSERT(ne11 <= std::numeric_limits<uint32_t>::max() / ne12); const uint3 ne12_fdv = init_fastdiv_values(ne12); + if constexpr (std::is_same<dst_t, float>::value && + (std::is_same<src0_t, float>::value || std::is_same<src0_t, half>::value)) { + if (ne00 > 0 && ne00 <= 128 && ne10 >= 128 && + GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[ggml_cuda_get_device()].cc)) { + auto launch = [&](auto width, auto elements) { + constexpr int cols = decltype(width)::value; + constexpr int values = decltype(elements)::value; + constexpr int rows_per_block = CUDA_GET_ROWS_BLOCK_SIZE / cols; + const dim3 grid((ne10 + rows_per_block - 1) / rows_per_block, MIN(ne11 * ne12, UINT16_MAX), 1); + const ggml_cuda_kernel_launch_params params{grid, block_dims, 0, stream}; + ggml_cuda_kernel_launch(k_get_rows_float_packed<src0_t, dst_t, cols, values>, params, + src0_d, src1_d, dst_d, ne00, ne10, ne11, ne12_fdv, + s1, s2, s3, nb01, nb02, nb03, s10, s11, s12); + }; + constexpr auto one = std::integral_constant<int, 1>{}; + if constexpr (std::is_same<src0_t, half>::value) { + if (ne00 > 32) { + if (ne00 <= 64) launch(std::integral_constant<int, 32>{}, std::integral_constant<int, 2>{}); + else launch(std::integral_constant<int, 32>{}, std::integral_constant<int, 4>{}); + return; + } + } + if (ne00 <= 1) launch(std::integral_constant<int, 1>{}, one); + else if (ne00 <= 2) launch(std::integral_constant<int, 2>{}, one); + else if (ne00 <= 4) launch(std::integral_constant<int, 4>{}, one); + else if (ne00 <= 8) launch(std::integral_constant<int, 8>{}, one); + else if (ne00 <= 16) launch(std::integral_constant<int, 16>{}, one); + else if (ne00 <= 32) launch(std::integral_constant<int, 32>{}, one); + else if (ne00 <= 64) launch(std::integral_constant<int, 64>{}, one); + else launch(std::integral_constant<int, 128>{}, one); + return; + } + } + if constexpr (std::is_same<src0_t, dst_t>::value) { constexpr int VEC = 16 / sizeof(dst_t); const int64_t ne00v = ne00 / VEC; @@ -488,3 +554,43 @@ void ggml_cuda_op_get_rows_back(ggml_backend_cuda_context & ctx, ggml_tensor * d k_get_rows_back_float<<<block_nums, block_dims, 0, stream>>>(src0_d, src1_d, dst_d, ne00, ne10, ne1); } + +// QSA indexer key pooling: mean of the 4 member rows of every block, 128-wide rows, one thread per column. +// Fuses get_rows -> reshape -> 4 x (view, cont, add) -> scale on the same summation order as the graph. +template<typename T> +static __global__ void get_rows_mean4_128( + const char * src, const char * ids, float * dst, + size_t src_row_stride, size_t src_stream_stride, size_t ids_stream_stride, + float scale, float bias) { + ggml_cuda_pdl_lc(); + ggml_cuda_pdl_sync(); + const int64_t group = blockIdx.x; + const int64_t stream = blockIdx.y; + const int col = threadIdx.x; + const int32_t * selected = (const int32_t *) (ids + stream*ids_stream_stride) + 4*group; + const char * input = src + stream*src_stream_stride; + float sum = (float) ((const T *) (input + selected[0]*src_row_stride))[col]; +#pragma unroll + for (int i = 1; i < 4; ++i) { + sum += (float) ((const T *) (input + selected[i]*src_row_stride))[col]; + } + dst[(stream*gridDim.x + group)*128 + col] = scale*sum + bias; +} + +void ggml_cuda_op_get_rows_mean4( + ggml_backend_cuda_context & ctx, const ggml_tensor * src, const ggml_tensor * ids, ggml_tensor * dst) { + const dim3 grid(dst->ne[1], dst->ne[2], 1); + const ggml_cuda_kernel_launch_params params(grid, dim3(128, 1, 1), 0, ctx.stream()); + const float scale = ggml_get_op_params_f32(dst, 0); + const float bias = ggml_get_op_params_f32(dst, 1); + if (src->type == GGML_TYPE_F16) { + ggml_cuda_kernel_launch(get_rows_mean4_128<half>, params, + (const char *) src->data, (const char *) ids->data, (float *) dst->data, + src->nb[1], src->nb[2], ids->nb[1], scale, bias); + } else { + GGML_ASSERT(src->type == GGML_TYPE_F32); + ggml_cuda_kernel_launch(get_rows_mean4_128<float>, params, + (const char *) src->data, (const char *) ids->data, (float *) dst->data, + src->nb[1], src->nb[2], ids->nb[1], scale, bias); + } +} diff --git a/ggml/src/ggml-cuda/getrows.cuh b/ggml/src/ggml-cuda/getrows.cuh index 3c5bea5f48c1..e8b9d7bd67eb 100644 --- a/ggml/src/ggml-cuda/getrows.cuh +++ b/ggml/src/ggml-cuda/getrows.cuh @@ -13,3 +13,7 @@ void get_rows_cuda( void ggml_cuda_op_get_rows(ggml_backend_cuda_context & ctx, ggml_tensor * dst); void ggml_cuda_op_get_rows_back(ggml_backend_cuda_context & ctx, ggml_tensor * dst); + +// fused QSA indexer key pooling (get_rows of 4 member rows per block, summed in graph order, scaled) +void ggml_cuda_op_get_rows_mean4( + ggml_backend_cuda_context & ctx, const ggml_tensor * src, const ggml_tensor * ids, ggml_tensor * dst); diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 5af45fd950ad..931fe4828da2 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -61,6 +61,12 @@ #include "ggml-cuda/gla.cuh" #include "ggml-cuda/gated_delta_net.cuh" #include "ggml-cuda/hyperconn.cuh" +#include "ggml-cuda/norm-gated.cuh" +#include "ggml-cuda/idx-relu-sum.cuh" +#include "ggml-cuda/ple-conv.cuh" +#include "ggml-cuda/gdn-conv.cuh" +#include "ggml-cuda/mmb.cuh" +#include "ggml-cuda/expand.cuh" #include "ggml-cuda/dsv4-hc.cuh" #include "ggml-cuda/set.cuh" #include "ggml-cuda/set-rows.cuh" @@ -1863,6 +1869,10 @@ static void ggml_cuda_mul_mat(ggml_backend_cuda_context & ctx, const ggml_tensor ggml_cuda_mul_mat_vec_q(ctx, src0, src1, nullptr, dst); return; } + if (ggml_cuda_mmb_supported_mm(src0, src1, dst)) { + ggml_cuda_mul_mat_mmb(ctx, src0, src1, dst); + return; + } if (ggml_cuda_should_use_mmq(src0->type, cc, ne11, /*n_experts =*/ 0)) { ggml_cuda_mul_mat_q(ctx, src0, src1, nullptr, dst); return; @@ -1931,6 +1941,10 @@ static void ggml_cuda_mul_mat_id(ggml_backend_cuda_context & ctx, ggml_tensor * } } + if (ggml_cuda_mmb_supported_mmid(src0, src1, ids, dst)) { + ggml_cuda_mul_mat_id_mmb(ctx, src0, src1, ids, dst); + return; + } if (ggml_cuda_should_use_mmq(src0->type, cc, ne12, /*n_experts=*/ne02)) { ggml_cuda_mul_mat_q(ctx, src0, src1, ids, dst); return; @@ -2059,6 +2073,16 @@ static void ggml_cuda_mul_mat_id(ggml_backend_cuda_context & ctx, ggml_tensor * } static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct ggml_tensor * dst) { + // a tensor marked BF16-only was never written as F32: any reader outside the fused paths is a bug, not a fallback + if (ggml_cuda_mmb_marks_count() > 0 && dst->op != GGML_OP_MUL_MAT && dst->op != GGML_OP_MUL_MAT_ID && dst->op != GGML_OP_VIEW && + dst->op != GGML_OP_RESHAPE && dst->op != GGML_OP_PERMUTE && dst->op != GGML_OP_TRANSPOSE && dst->op != GGML_OP_NONE) { + for (int s = 0; s < GGML_MAX_SRC && dst->src[s]; ++s) { + const ggml_tensor * src = dst->src[s]; + if (ggml_cuda_mmb_is_bf16_only(src) || (src->view_src && ggml_cuda_mmb_is_bf16_only(src->view_src))) { + GGML_ABORT("MMB: %s(%s) reads BF16-only tensor %s through the unfused path", ggml_op_name(dst->op), dst->name, src->name); + } + } + } switch (dst->op) { case GGML_OP_ARGMAX: ggml_cuda_argmax(ctx, dst); @@ -2430,6 +2454,7 @@ static const char * ggml_backend_cuda_get_name(ggml_backend_t backend) { } static void ggml_backend_cuda_free(ggml_backend_t backend) { + ggml_cuda_mmb_release_all(); // release cached BF16 conversions before the pool is destroyed (pool leak assert) ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *)backend->context; delete cuda_ctx; @@ -3765,6 +3790,360 @@ static int ggml_cuda_match_hc_mix(const ggml_cgraph * cgraph, const int i, ggml_ return n_ops; } +static int ggml_cuda_match_idx_relu_sum(const ggml_cgraph * g, int i, ggml_cuda_idx_relu_sum_args & a) { + if (i + 4 >= g->n_nodes) return 0; + const ggml_tensor * relu = g->nodes[i]; + if (relu->op != GGML_OP_UNARY || ggml_get_unary_op(relu) != GGML_UNARY_OP_RELU) return 0; + if (relu->type != GGML_TYPE_F32 || !ggml_is_contiguous(relu)) return 0; + const ggml_tensor * src = relu->src[0]; + if (!src || src->type != GGML_TYPE_F32 || !ggml_is_contiguous(src) || !ggml_are_same_shape(src, relu)) return 0; + const int64_t nb = relu->ne[0], H = relu->ne[1], nt = relu->ne[2], ns = relu->ne[3]; + if (H < 2 || H > 32 || nb < 64 || nt * ns < 64 || nt * ns > UINT16_MAX) return 0; + auto is_slice = [&](const ggml_tensor * v, int64_t h) { + return v->op == GGML_OP_VIEW && v->view_src == relu && v->type == GGML_TYPE_F32 && + v->ne[0] == nb && v->ne[1] == nt && v->ne[2] == ns && v->ne[3] == 1 && + v->nb[0] == sizeof(float) && v->nb[1] == relu->nb[2] && v->nb[2] == relu->nb[3] && + v->view_offs == (size_t) h * relu->nb[1]; + }; + const ggml_tensor * cont = g->nodes[i + 2]; + if (!is_slice(g->nodes[i + 1], 0) || cont->op != GGML_OP_CONT || cont->src[0] != g->nodes[i + 1]) return 0; + const ggml_tensor * prev = cont; + int k = i + 3; + for (int64_t h = 1; h < H; ++h) { + if (k + 1 >= g->n_nodes) return 0; + const ggml_tensor * v = g->nodes[k]; const ggml_tensor * add = g->nodes[k + 1]; + if (!is_slice(v, h) || add->op != GGML_OP_ADD || add->type != GGML_TYPE_F32) return 0; + if (add->src[0] != prev || add->src[1] != v || !ggml_are_same_shape(add, cont) || !ggml_is_contiguous(add)) return 0; + prev = add; k += 2; + } + const int count = k - i; + if (count > 32) return 0; + int indices[32]; ggml_op ops[32]; + for (int j = 0; j < count; ++j) { indices[j] = i + j; ops[j] = g->nodes[i + j]->op; } + const int output = i + count - 1; + if (!ggml_can_fuse_subgraph_ext(g, indices, count, ops, &output, 1)) return 0; + if (src->data && prev->data) { + const uintptr_t av = (uintptr_t) src->data, bv = (uintptr_t) prev->data; + const bool overlap = av <= bv ? bv - av < ggml_nbytes(src) : av - bv < ggml_nbytes(prev); + if (overlap) { return 0; } + } + a.score = src; a.dst = (ggml_tensor *) prev; a.heads = (int) H; + return count; +} + +// hc_mix match plus the fusion-closure check, as one call +static int ggml_cuda_hc_mix_closed(const ggml_cgraph * graph, int index, ggml_cuda_hc_mix_args & args) { + ggml_op ops[36]; + const int count = ggml_cuda_match_hc_mix(graph, index, args, ops); + if (count == 0) { return 0; } + int indices[36]; + for (int j = 0; j < count; ++j) { indices[j] = index + j; } + const int output = index + count - 1; + return ggml_can_fuse_subgraph_ext(graph, indices, count, ops, &output, 1) ? count : 0; +} + +static int ggml_cuda_match_hc_combine_norm(ggml_cgraph * cgraph, int i, + ggml_cuda_hc_combine_norm_args & args, int warp_size, bool check_alias) { + ggml_tensor * node = cgraph->nodes[i]; + args.single_block = false; + if (node->op == GGML_OP_SCALE && node->type == GGML_TYPE_F32 && i + 8 < cgraph->n_nodes && + cgraph->nodes[i + 1]->op == GGML_OP_UNARY && ggml_get_unary_op(cgraph->nodes[i + 1]) == GGML_UNARY_OP_SIGMOID && + cgraph->nodes[i + 1]->src[0] == node && + cgraph->nodes[i + 2]->op == GGML_OP_SCALE && cgraph->nodes[i + 2]->src[0] == cgraph->nodes[i + 1]) { + const ggml_tensor * scale1 = node; + const ggml_tensor * scale2 = cgraph->nodes[i + 2]; + const ggml_tensor * inject = scale1->src[0]; + + int j = i + 3; + while (j < cgraph->n_nodes && j < i + 7 && ggml_cuda_is_view_or_noop(cgraph->nodes[j]) && !ggml_is_empty(cgraph->nodes[j])) { + j++; + } + + if (j + 4 < cgraph->n_nodes && + cgraph->nodes[j]->op == GGML_OP_REPEAT && cgraph->nodes[j + 1]->op == GGML_OP_MUL && cgraph->nodes[j + 2]->op == GGML_OP_ADD && + cgraph->nodes[j + 3]->op == GGML_OP_RMS_NORM && cgraph->nodes[j + 4]->op == GGML_OP_MUL) { + const ggml_tensor * repeat = cgraph->nodes[j]; + const ggml_tensor * mul = cgraph->nodes[j + 1]; + ggml_tensor * add = cgraph->nodes[j + 2]; + const ggml_tensor * rms = cgraph->nodes[j + 3]; + ggml_tensor * mulg = cgraph->nodes[j + 4]; + const ggml_tensor * b = repeat->src[0]; + const ggml_tensor * w = mul->src[0] == repeat ? mul->src[1] : mul->src[0]; + const ggml_tensor * res = add->src[0] == mul ? add->src[1] : add->src[0]; + const ggml_tensor * gamma = mulg->src[0] == rms ? mulg->src[1] : mulg->src[0]; + + const int64_t n_embd = add->ne[0]; + const int64_t hc = add->ne[1]; + const int64_t n_tok = add->ne[2]; + + const bool w_ok = (w == scale2 || (ggml_cuda_is_view_or_noop(w) && w->view_src == scale2)) && + w->type == GGML_TYPE_F32 && ggml_is_contiguous(w) && ggml_nelements(w) == hc * n_tok && w->ne[0] == 1 && w->ne[1] == hc; + const bool b_ok = b->type == GGML_TYPE_F32 && ggml_is_contiguous(b) && ggml_nelements(b) == n_embd * n_tok && + b->ne[0] == n_embd && b->ne[1] == 1; + const bool ok = add->ne[3] == 1 && n_tok <= 65535 && + inject->type == GGML_TYPE_F32 && ggml_is_contiguous(inject) && ggml_nelements(inject) == hc * n_tok && + ggml_nrows(inject) == n_tok && + ggml_are_same_shape(scale1, inject) && ggml_are_same_shape(scale2, inject) && + (mul->src[0] == repeat || mul->src[1] == repeat) && w != repeat && + (add->src[0] == mul || add->src[1] == mul) && res != mul && + rms->src[0] == add && (mulg->src[0] == rms || mulg->src[1] == rms) && gamma != rms && + scale2->type == GGML_TYPE_F32 && repeat->type == GGML_TYPE_F32 && + mul->type == GGML_TYPE_F32 && add->type == GGML_TYPE_F32 && res->type == GGML_TYPE_F32 && + rms->type == GGML_TYPE_F32 && mulg->type == GGML_TYPE_F32 && gamma->type == GGML_TYPE_F32 && + ggml_are_same_shape(repeat, add) && ggml_are_same_shape(mul, add) && ggml_are_same_shape(res, add) && + ggml_are_same_shape(rms, add) && ggml_are_same_shape(mulg, add) && + ggml_is_contiguous(res) && ggml_is_contiguous(add) && ggml_is_contiguous(mulg) && ggml_is_contiguous(gamma) && + ggml_nelements(gamma) == n_embd * hc && gamma->ne[0] == n_embd && gamma->ne[1] == hc && w_ok && b_ok; + + if (ok) { + args.inject = inject; + args.residual = res; + args.block_out = b; + args.gamma = gamma; + args.out_res = add; + args.out_xn = mulg; + args.s1 = ggml_get_op_params_f32(scale1, 0); + args.b1 = ggml_get_op_params_f32(scale1, 1); + args.s2 = ggml_get_op_params_f32(scale2, 0); + args.b2 = ggml_get_op_params_f32(scale2, 1); + memcpy(&args.eps, rms->op_params, sizeof(float)); + + bool alias_ok = true; + if (check_alias) { + // every block reads block_out and inject and writes its own stream of both outputs: the outputs + // must not overlap those, and the residual may only alias the first output in place + auto overlap = [](const ggml_tensor * p, const ggml_tensor * q) { + const uintptr_t p0 = (uintptr_t) p->data, p1 = p0 + ggml_backend_buft_get_alloc_size(p->buffer->buft, p); + const uintptr_t q0 = (uintptr_t) q->data, q1 = q0 + ggml_backend_buft_get_alloc_size(q->buffer->buft, q); + return p0 < q1 && q0 < p1; + }; + if (overlap(add, gamma) || overlap(mulg, gamma)) { + return 0; + } + const bool res_inplace = res->data == add->data && ggml_are_same_layout(res, add); + alias_ok = !overlap(add, b) && !overlap(add, inject) && (res_inplace || !overlap(add, res)) && + !overlap(mulg, b) && !overlap(mulg, inject) && !overlap(mulg, res) && !overlap(mulg, add); + // The single-block kernel reads aliased inputs before writing either output. + if (!alias_ok && n_tok == 1 && hc <= 4) { + args.single_block = true; + alias_ok = true; + } + } + + int idxs[16]; + enum ggml_op ops[16]; + int count = 0; + for (int k = i; k <= j + 4; ++k) { + const ggml_tensor * n = cgraph->nodes[k]; + if (ggml_cuda_is_view_or_noop(n) && n->view_src != scale2) { + continue; + } + idxs[count] = k; + ops[count] = n->op; + count++; + } + const int out_nodes[] = { j + 2, j + 4 }; + if (alias_ok && ggml_cuda_hc_combine_norm_supported(args, warp_size) && + ggml_can_fuse_subgraph_ext(cgraph, idxs, count, ops, out_nodes, 2)) { + return j + 4 - i; + } + } + } + } + + return 0; +} + +// QSA indexer key pooling: get_rows(k, member_rows) -> reshape [128, 4, n_blocks, n_stream] -> four contiguous +// stream slices added in order -> scale 1/4. One kernel reads the four member rows and writes the mean. +struct ggml_cuda_get_rows_mean4_match { + const ggml_tensor * src = nullptr; + const ggml_tensor * ids = nullptr; + ggml_tensor * dst = nullptr; + int node_count = 14; +}; + +static bool ggml_cuda_match_get_rows_mean4( + const ggml_cgraph * graph, int i, ggml_cuda_get_rows_mean4_match & match) { + if (i < 0 || i + match.node_count > graph->n_nodes || graph->nodes[i]->op != GGML_OP_GET_ROWS) { + return false; + } + if (!ggml_can_fuse_subgraph(graph, i, + { GGML_OP_GET_ROWS, GGML_OP_RESHAPE, + GGML_OP_VIEW, GGML_OP_CONT, GGML_OP_VIEW, GGML_OP_CONT, GGML_OP_ADD, + GGML_OP_VIEW, GGML_OP_CONT, GGML_OP_ADD, GGML_OP_VIEW, GGML_OP_CONT, GGML_OP_ADD, + GGML_OP_SCALE }, { i + 13 })) { + return false; + } + const ggml_tensor * rows = graph->nodes[i]; + const ggml_tensor * src = rows->src[0]; + const ggml_tensor * ids = rows->src[1]; + const ggml_tensor * members = graph->nodes[i+1]; + ggml_tensor * dst = graph->nodes[i+13]; + if ((src->type != GGML_TYPE_F16 && src->type != GGML_TYPE_F32) || ids->type != GGML_TYPE_I32 || + src->ne[0] != 128 || src->ne[1] <= 0 || src->ne[2] <= 0 || src->nb[0] != ggml_type_size(src->type) || src->ne[3] != 1 || + ids->nb[0] != sizeof(int32_t) || ids->ne[0] <= 0 || ids->ne[0] % 4 != 0 || ids->ne[2] != 1 || ids->ne[3] != 1 || + ids->ne[1] != src->ne[2] || ids->ne[1] > 65535 || ids->ne[0]/4 > INT32_MAX) { + return false; + } + if (members->src[0] != rows || members->ne[0] != 128 || members->ne[1] != 4 || + members->ne[2] != ids->ne[0]/4 || members->ne[3] != ids->ne[1] || + !ggml_is_contiguous(members)) { + return false; + } + for (int n = 0; n < match.node_count; ++n) { + if (graph->nodes[i+n]->type != GGML_TYPE_F32) { + return false; + } + } + const int offsets[4] = { 2, 4, 7, 10 }; + const ggml_tensor * sum = nullptr; + for (int n = 0; n < 4; ++n) { + const ggml_tensor * view = graph->nodes[i+offsets[n]]; + const ggml_tensor * copy = graph->nodes[i+offsets[n]+1]; + if (view->src[0] != members || view->ne[0] != 128 || view->ne[1] != members->ne[2] || + view->ne[2] != members->ne[3] || view->ne[3] != 1 || view->nb[0] != sizeof(float) || + view->nb[1] != members->nb[2] || view->nb[2] != members->nb[3] || + view->view_offs != members->view_offs + n*members->nb[1] || + copy->src[0] != view || !ggml_are_same_shape(copy, view) || !ggml_is_contiguous(copy)) { + return false; + } + if (n == 0) { + sum = copy; + } else { + const ggml_tensor * add = graph->nodes[i+offsets[n]+2]; + if (add->src[0] != sum || add->src[1] != copy || !ggml_are_same_shape(add, copy)) { + return false; + } + sum = add; + } + } + if (dst->src[0] != sum || !ggml_are_same_shape(dst, sum) || !ggml_is_contiguous(dst) || + ggml_get_op_params_f32(dst, 0) != 0.25f || ggml_get_op_params_f32(dst, 1) != 0.0f) { + return false; + } + match.src = src; + match.ids = ids; + match.dst = dst; + return true; +} + +static bool ggml_cuda_mean4_ranges_overlap(const ggml_tensor * a, const ggml_tensor * b) { + const uintptr_t av = (uintptr_t) a->data; + const uintptr_t bv = (uintptr_t) b->data; + return av <= bv ? bv-av < ggml_nbytes(a) : av-bv < ggml_nbytes(b); +} + +// QSA complete-block selection (qwen4exp_select_complete_blocks): top-512 blocks -> ascending order -> cells with +// validity -> concat tail, as one kernel. Matches the 20/21-node subgraph the builder emits for 512 blocks / 2051 cells. +static int ggml_cuda_match_qsa_expand(const ggml_cgraph * graph, int first, ggml_cuda_qsa_expand_args & args) { + if (first + 19 >= graph->n_nodes) return 0; + if (graph->nodes[first]->op != GGML_OP_CONT || !graph->nodes[first]->src[0] || graph->nodes[first]->src[0]->op != GGML_OP_TOP_K) return 0; + static const bool debug = getenv("GGML_QSA_EXPAND_DEBUG") != nullptr; + static int debug_left = 3; + auto reject = [&](int line) { + if (debug && debug_left-- > 0) { + fprintf(stderr, "qsa_expand: reject at line %d, node %d (%s)\n", line, first, graph->nodes[first]->name); + for (int j = 0; j < 22 && first + j < graph->n_nodes; ++j) { + const ggml_tensor * t = graph->nodes[first + j]; + fprintf(stderr, " [%2d] %-10s %-6s [%lld,%lld,%lld,%lld] %s\n", j, ggml_op_name(t->op), ggml_type_name(t->type), + (long long) t->ne[0], (long long) t->ne[1], (long long) t->ne[2], (long long) t->ne[3], t->name); + } + } + return 0; + }; + const ggml_op expected[] = {GGML_OP_CONT, GGML_OP_VIEW, GGML_OP_CPY, GGML_OP_ARGSORT, + GGML_OP_GET_ROWS, GGML_OP_RESHAPE, GGML_OP_RESHAPE, GGML_OP_GET_ROWS, GGML_OP_RESHAPE, + GGML_OP_CPY, GGML_OP_SCALE, GGML_OP_VIEW, GGML_OP_GET_ROWS, GGML_OP_SCALE, GGML_OP_UNARY, + GGML_OP_MUL, GGML_OP_SCALE, GGML_OP_CPY, GGML_OP_RESHAPE, GGML_OP_VIEW, GGML_OP_CONCAT}; + const ggml_tensor * n[21]; + for (int j = 0; j < 19; ++j) { + n[j] = graph->nodes[first+j]; + if (n[j]->op != expected[j]) return reject(__LINE__); + } + // the tail may arrive through a view (a strip of the tail input) or a reshape of the whole input + const bool tail_view = graph->nodes[first+19]->op == GGML_OP_VIEW || graph->nodes[first+19]->op == GGML_OP_RESHAPE; + const int node_count = tail_view ? 21 : 20; + if (first + node_count > graph->n_nodes) return reject(__LINE__); + n[20] = graph->nodes[first+node_count-1]; + if (n[20]->op != GGML_OP_CONCAT) return reject(__LINE__); + // the tail the concat reads: the scheduler may substitute an input copy for the graph's own view node (the tail + // indices are a host input), so take the concat's actual source rather than nodes[first+19] + n[19] = n[20]->src[1]; + if (!n[19]) return reject(__LINE__); + auto source = [&](int node, int src, int producer) { return n[node]->src[src] == n[producer]; }; + for (const auto & edge : {std::array<int,3>{1,0,0}, {2,0,0}, {3,0,2}, {4,0,1}, {4,1,3}, + {5,0,4}, {6,0,5}, {7,1,6}, {8,0,7}, {9,0,8}, {10,0,9}, {12,0,11}, {12,1,5}, + {13,0,12}, {14,0,13}, {15,0,10}, {15,1,14}, {16,0,15}, {17,0,16}, {18,0,17}, + {20,0,18}}) { + if (!source(edge[0], edge[1], edge[2])) { if (debug) fprintf(stderr, "qsa_expand: edge n[%d]->src[%d] != n[%d] (is %s)\n", edge[0], edge[1], edge[2], n[edge[0]]->src[edge[1]] ? n[edge[0]]->src[edge[1]]->name : "null"); return reject(__LINE__); } + } + const auto * blocks = n[0]->src[0]; + const auto * scores = n[11]->src[0]; + const auto * cells = n[7]->src[0]; + const auto * tail = n[19]; + if (!blocks || !scores || !cells || blocks->op != GGML_OP_TOP_K || blocks->src[0] != scores || + blocks->type != GGML_TYPE_I32 || scores->type != GGML_TYPE_F32 || cells->type != GGML_TYPE_I32 || + tail->type != GGML_TYPE_I32 || n[20]->type != GGML_TYPE_I32 || + ggml_get_op_params_i32(n[3],0) != GGML_SORT_ORDER_ASC || + ggml_get_unary_op(n[14]) != GGML_UNARY_OP_STEP || ggml_get_op_params_i32(n[20],0) != 0) return reject(__LINE__); + auto shape = [](const ggml_tensor * t, int64_t a, int64_t b, int64_t c=1, int64_t d=1) { + return t->ne[0]==a && t->ne[1]==b && t->ne[2]==c && t->ne[3]==d; + }; + const int64_t nq = blocks->ne[1], nb = scores->ne[0]; + if (nq < 1 || nq > INT_MAX || nb < 512 || nb > 16777216 || + !shape(blocks,512,nq) || !shape(scores,nb,nq) || !shape(cells,4,nb) || !shape(tail,3,nq) || + !shape(n[0],512,nq) || !shape(n[1],1,512,nq) || !shape(n[2],512,nq) || + !shape(n[3],512,nq) || !shape(n[4],1,512,nq) || !shape(n[5],512,nq) || + !shape(n[6],512*nq,1) || !shape(n[7],4,512*nq) || !shape(n[8],4,512,nq) || + !shape(n[9],4,512,nq) || !shape(n[10],4,512,nq) || !shape(n[11],1,nb,nq) || + !shape(n[12],1,512,nq) || !shape(n[13],1,512,nq) || !shape(n[14],1,512,nq) || + !shape(n[15],4,512,nq) || !shape(n[16],4,512,nq) || !shape(n[17],4,512,nq) || + !shape(n[18],2048,nq) || !shape(n[20],2051,nq)) return reject(__LINE__); + for (int j : {2,9,10,11,12,13,14,15,16}) if (n[j]->type != GGML_TYPE_F32) return reject(__LINE__); + for (int j : {0,1,3,4,5,6,7,8,17,18,19,20}) if (n[j]->type != GGML_TYPE_I32) return reject(__LINE__); + for (int j : {10,13,16}) { + if (ggml_get_op_params_f32(n[j],0) != 1.0f || ggml_get_op_params_f32(n[j],1) != (j==16 ? -1.0f : 1.0f)) return reject(__LINE__); + } + for (const auto * t : {blocks,scores,cells,tail,n[20]}) { + if (!ggml_is_contiguous(t) || t->nb[0] != 4) return reject(__LINE__); + } + if (n[1]->view_offs != 0 || n[11]->view_offs != 0 || + n[1]->nb[1] != 4 || n[1]->nb[2] != size_t(512)*4 || + n[11]->nb[1] != 4 || n[11]->nb[2] != size_t(nb)*4) return reject(__LINE__); + int indices[21], count=0; ggml_op ops[21]; + for (int j=0;j<node_count;++j) { + if (j==11 || (tail_view && j==19)) continue; + indices[count]=first+j;ops[count++]=graph->nodes[first+j]->op; + } + const int outputs[] = {first+2,first+9,first+17,first+18,first+node_count-1}; + if (!ggml_can_fuse_subgraph_ext(graph, indices, count, ops, outputs, 5)) return reject(__LINE__); + args = {blocks,scores,cells,tail,graph->nodes[first+2],graph->nodes[first+9],graph->nodes[first+17],graph->nodes[first+node_count-1]}; + return node_count-1; +} + +static bool ggml_cuda_qsa_expand_alias_ok(const ggml_cuda_qsa_expand_args & args) { + const ggml_tensor * outputs[] = {args.cast_blocks,args.cast_cells,args.expanded,args.output}; + for (int i=0;i<4;++i) { + for (const auto * input : {args.blocks,args.scores,args.cells,args.tail}) { + if (ggml_cuda_mean4_ranges_overlap(input,outputs[i])) return false; + } + for (int j=0;j<i;++j) if (ggml_cuda_mean4_ranges_overlap(outputs[i],outputs[j])) return false; + } + return true; +} + +// true when the BF16 WMMA GEMM would take this MUL_MAT / MUL_MAT_ID, so MMQ-based fusions leave it alone +static bool ggml_cuda_mmb_claims(const ggml_tensor * t) { + if (!t || !t->src[0] || !t->src[1]) { + return false; + } + if (t->op == GGML_OP_MUL_MAT_ID) { + return t->src[2] && ggml_cuda_mmb_supported_mmid(t->src[0], t->src[1], t->src[2], t); + } + return t->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t); +} + static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph * cgraph, int i) { static bool disable_fusion = getenv("GGML_CUDA_DISABLE_FUSION") != nullptr && std::atoi(getenv("GGML_CUDA_DISABLE_FUSION")); @@ -3781,6 +4160,59 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph ggml_tensor * node = cgraph->nodes[i]; + // routed gate/up + SwiGLU on the BF16 WMMA path from 512 tokens: ahead of the MMQ-based expert fusions below, + // which yield to MMB whenever it supports the GEMM + if (node->op == GGML_OP_MUL_MAT_ID && i + 2 < cgraph->n_nodes && cgraph->nodes[i + 1]->op == GGML_OP_MUL_MAT_ID && + cgraph->nodes[i + 2]->op == GGML_OP_GLU) { + ggml_tensor * glu = cgraph->nodes[i + 2]; + ggml_tensor * gate = glu->src[0]; + ggml_tensor * up = glu->src[1]; + const bool paired = (gate == node && up == cgraph->nodes[i + 1]) || (gate == cgraph->nodes[i + 1] && up == node); + if (paired && ggml_cuda_mmb_supported_glu(gate->src[0], up->src[0], up->src[1], up->src[2], glu) && + ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_MUL_MAT_ID, GGML_OP_MUL_MAT_ID, GGML_OP_GLU }, { i + 2 })) { + const int out_nodes[] = { i + 2 }; + if (ggml_cuda_check_fusion_memory_ranges(cgraph, i, 3, out_nodes, 1)) { + ggml_cuda_mul_mat_id_mmb_glu(*cuda_ctx, gate->src[0], up->src[0], up->src[1], up->src[2], glu); + return 2; + } + } + } + + // qwen4exp prefill convolutions: the concat writes only the columns its history copies read, and one kernel + // replaces the tap chain (PLE) or the ssm_conv + SiLU (Gated DeltaNet). Both decisions come from the same matcher. + if (node->op == GGML_OP_CONCAT || node->op == GGML_OP_CONT) { + ggml_cuda_ple_conv_match pm; + if (node->op == GGML_OP_CONCAT && ggml_cuda_ple_conv_match_at_concat(cgraph, i, pm)) { + ggml_cuda_ple_conv_write_tail(*cuda_ctx, pm); + return 1; // the concat and the no-op view after it + } + if (node->op == GGML_OP_CONT && ggml_cuda_ple_conv_match_at_tap(cgraph, i, pm)) { + ggml_cuda_ple_conv_direct(*cuda_ctx, pm); + return pm.silu_idx - i; + } + } + if (node->op == GGML_OP_CONCAT || node->op == GGML_OP_SSM_CONV) { + ggml_cuda_gdn_conv_match gm; + if (node->op == GGML_OP_CONCAT && ggml_cuda_gdn_conv_match_at_concat(cgraph, i, gm)) { + ggml_cuda_gdn_conv_write_tail(*cuda_ctx, gm); + return 1; // the concat and the no-op view after it + } + if (node->op == GGML_OP_SSM_CONV && ggml_cuda_gdn_conv_match_at_conv(cgraph, i, gm)) { + ggml_cuda_gdn_conv_direct(*cuda_ctx, gm); + return 1; + } + } + if (node->op == GGML_OP_UNARY && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + ggml_cuda_idx_relu_sum_args args; + const int count = ggml_cuda_match_idx_relu_sum(cgraph, i, args); + if (count > 0) { + ggml_cuda_op_idx_relu_sum(*cuda_ctx, args); + return count - 1; + } + } + + + // RDNA3.5 decode: consecutive single-column MUL_MATs that read the same activation vector (plain Q8_0/F32 // matvecs, or a [mul_mat, mul_mat, glu] gate/up pair) are launched as one grouped kernel. The segments are // mutually independent, so the launch position of the first one is valid for all of them. @@ -3925,11 +4357,26 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph if (node->op == GGML_OP_MUL) { ggml_cuda_moe_weighted_reduction_match match; if (ggml_cuda_match_moe_weighted_reduction(cgraph, i, match)) { - const int output_idx = i + match.node_count - 1; - if (ggml_cuda_check_fusion_memory_ranges(cgraph, i, match.node_count, &output_idx, 1)) { + int count = match.node_count; + const ggml_tensor * merge = nullptr; + { // shared-expert merge: ADD(reduction, ffn_shexp_gated) right after, with the reduction used only there + const int nadd = i + match.node_count; + if (ggml_cuda_mmb_blk16() && nadd < cgraph->n_nodes && ggml_node_has_n_uses(cgraph, i + match.node_count - 1, 1)) { + const ggml_tensor * add = cgraph->nodes[nadd]; + if (add->op == GGML_OP_ADD && add->type == GGML_TYPE_F32 && ggml_is_contiguous(add) && + ggml_are_same_shape(add, match.dst) && ggml_cuda_mmb_is_bf16_only(add)) { + const ggml_tensor * o = add->src[0] == match.dst ? add->src[1] : (add->src[1] == match.dst ? add->src[0] : nullptr); + if (o && o->type == GGML_TYPE_F32 && ggml_is_contiguous(o) && ggml_are_same_shape(o, match.dst)) { + merge = o; match.dst = const_cast<ggml_tensor *>(add); count = match.node_count + 1; + } + } + } + } + const int output_idx = i + count - 1; + if (ggml_cuda_check_fusion_memory_ranges(cgraph, i, count, &output_idx, 1)) { ggml_cuda_op_moe_weighted_reduction( - *cuda_ctx, match.experts, match.expert_scale, match.weights, match.dst); - return match.node_count - 1; + *cuda_ctx, match.experts, match.expert_scale, match.weights, match.dst, merge); + return count - 1; } } } @@ -3967,7 +4414,8 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } } - if (node->op == GGML_OP_GLU && i + 1 < cgraph->n_nodes && ggml_get_glu_op(node) == GGML_GLU_OP_SWIGLU && node->src[1]) { + if (node->op == GGML_OP_GLU && i + 1 < cgraph->n_nodes && ggml_get_glu_op(node) == GGML_GLU_OP_SWIGLU && node->src[1] && + !ggml_cuda_mmb_is_bf16_only(node) && !ggml_cuda_mmb_claims(cgraph->nodes[i + 1])) { ggml_tensor * next = cgraph->nodes[i + 1]; const bool has_ids = next->op == GGML_OP_MUL_MAT_ID; if (has_ids || next->op == GGML_OP_MUL_MAT) { @@ -4232,7 +4680,8 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph // experts for one output row in a wave and apply their routing weights immediately, avoiding the // [n_embd, n_used] intermediate and its separate reduction launch. if (getenv("GGML_CUDA_DISABLE_WEIGHTED_DOWN") == nullptr && - node->op == GGML_OP_MUL_MAT_ID && i + 20 < cgraph->n_nodes && cgraph->nodes[i + 1]->op == GGML_OP_MUL) { + node->op == GGML_OP_MUL_MAT_ID && i + 20 < cgraph->n_nodes && cgraph->nodes[i + 1]->op == GGML_OP_MUL && + !ggml_cuda_mmb_claims(node)) { constexpr int n_used = 10; constexpr int n_ops = 2 + n_used + (n_used - 1); ggml_tensor * mul = cgraph->nodes[i + 1]; @@ -4371,7 +4820,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph const bool use_mmq = shared_inputs && ggml_cuda_should_use_mmq(src0->type, cc, mmq_cols, n_experts) && ggml_cuda_should_use_mmq(src0_next->type, cc, mmq_cols, n_experts); - const bool compatible = use_mmq && node->src[1]->type == GGML_TYPE_F32 && + const bool compatible = use_mmq && !ggml_cuda_mmb_claims(node) && node->src[1]->type == GGML_TYPE_F32 && node->type == GGML_TYPE_F32 && next->type == GGML_TYPE_F32 && ggml_are_same_shape(src0, src0_next) && ggml_are_same_shape(node, next) && mmq_get_q8_1_ds_layout(src0->type) == mmq_get_q8_1_ds_layout(src0_next->type) && @@ -4705,6 +5154,13 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph const ggml_tensor * src1 = up->src[1]; const ggml_tensor * ids = up->src[2]; + if (op == GGML_OP_MUL_MAT_ID && ggml_cuda_mmb_supported_glu(gate->src[0], up->src[0], src1, ids, glu)) { + ggml_cuda_mul_mat_id_mmb_glu(*cuda_ctx, gate->src[0], up->src[0], src1, ids, glu); + fused_mul_mat_vec = true; + fused_node_count = 3; + break; + } + if (ggml_cuda_should_fuse_mul_mat_vec_f(up)) { ggml_cuda_mm_fusion_args_host fusion_data{}; fusion_data.gate = gate->src[0]; @@ -4902,6 +5358,28 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph return 2; } + if (node->op == GGML_OP_RMS_NORM && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + ggml_cuda_norm_gated_match match; + int skip = ggml_cuda_norm_gated_match_at(cgraph, i, match); + if (skip == 0) skip = ggml_cuda_norm_rows_match_at(cgraph, i, match); + if (skip > 0) { + if (match.staged) { + for (int j = i; j <= i + skip; ++j) { + if (ggml_cuda_is_view_or_noop(cgraph->nodes[j])) continue; + if (!ggml_cuda_compute_forward(*cuda_ctx, cgraph->nodes[j])) { + GGML_ABORT("norm-gated: staged dispatch failed"); + } + } + } else { + if (match.pre >= 0 && !ggml_cuda_compute_forward(*cuda_ctx, cgraph->nodes[match.pre])) { + GGML_ABORT("norm-gated: gate MUL_MAT dispatch failed"); + } + ggml_cuda_op_norm_gated(*cuda_ctx, match); + } + return skip; + } + } + if (ggml_cuda_can_fuse(cgraph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL }, {})) { ggml_cuda_op_rms_norm_fused(*cuda_ctx, node, cgraph->nodes[i + 1]); return 1; @@ -4917,7 +5395,41 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph return 1; } + // qwen4exp complete-block selection: sort the chosen blocks, gather their cells, validity, tail, in one kernel + if (node->op == GGML_OP_CONT && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + ggml_cuda_qsa_expand_args args; + const int skip = ggml_cuda_match_qsa_expand(cgraph, i, args); + if (skip && ggml_cuda_qsa_expand_alias_ok(args)) { + ggml_cuda_op_qsa_expand(*cuda_ctx, args); + return skip; + } + } + + // qwen4exp indexer key pooling: get_rows of the four member rows per block, summed and scaled in one kernel + if (node->op == GGML_OP_GET_ROWS && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + ggml_cuda_get_rows_mean4_match match; + if (ggml_cuda_match_get_rows_mean4(cgraph, i, match)) { + const int output_idx = i + match.node_count - 1; + if (ggml_cuda_check_fusion_memory_ranges(cgraph, i, match.node_count, &output_idx, 1) && + !ggml_cuda_mean4_ranges_overlap(match.src, match.dst) && + !ggml_cuda_mean4_ranges_overlap(match.ids, match.dst)) { + ggml_cuda_op_get_rows_mean4(*cuda_ctx, match.src, match.ids, match.dst); + return match.node_count - 1; + } + } + } + // qwen4exp hyper-connection stream mix: + // HC gate GEMM [320 -> 10240] whose only consumer is the fused stream mix: GEMM + sigmoid + mix in one kernel + if (node->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_gatemix() && i + 1 < cgraph->n_nodes && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + const ggml_tensor * w = node->src[0], * lo = node->src[1]; + if (ggml_is_quantized(w->type) && ggml_node_has_n_uses(cgraph, i, 1) && ggml_cuda_mmb_supported_mm(w, lo, node)) { + ggml_cuda_hc_mix_args ma; + const int count = ggml_cuda_hc_mix_closed(cgraph, i + 1, ma); + if (count > 0 && ma.gate == node && ggml_cuda_hc_gate_mix(*cuda_ctx, w, lo, ma.xn, ma.dst, ma.hc, ma.scale, ma.bias)) return count; + } + } + // sigmoid(gate) -> mul(xn, .) -> reshape -> view(stream 0) -> cont -> (view(stream c) -> add) x (hc-1) -> scale // one kernel reads xn/gate once and writes the [n_embd, T] mean of the gated streams { @@ -4940,109 +5452,49 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } } - // qwen4exp hyper-connection combine + next grouped norm: - // scale(inject) -> sigmoid -> scale -> [views] -> repeat(block_out) -> mul -> add(residual, .) -> rms_norm -> mul(gamma) - // one kernel: block per (stream, token), residual update + norm - if (node->op == GGML_OP_SCALE && node->type == GGML_TYPE_F32 && i + 8 < cgraph->n_nodes && - cgraph->nodes[i + 1]->op == GGML_OP_UNARY && ggml_get_unary_op(cgraph->nodes[i + 1]) == GGML_UNARY_OP_SIGMOID && - cgraph->nodes[i + 1]->src[0] == node && - cgraph->nodes[i + 2]->op == GGML_OP_SCALE && cgraph->nodes[i + 2]->src[0] == cgraph->nodes[i + 1]) { - const ggml_tensor * scale1 = node; - const ggml_tensor * scale2 = cgraph->nodes[i + 2]; - const ggml_tensor * inject = scale1->src[0]; - - int j = i + 3; - while (j < cgraph->n_nodes && j < i + 7 && ggml_cuda_is_view_or_noop(cgraph->nodes[j]) && !ggml_is_empty(cgraph->nodes[j])) { - j++; - } - - if (j + 4 < cgraph->n_nodes && - cgraph->nodes[j]->op == GGML_OP_REPEAT && cgraph->nodes[j + 1]->op == GGML_OP_MUL && cgraph->nodes[j + 2]->op == GGML_OP_ADD && - cgraph->nodes[j + 3]->op == GGML_OP_RMS_NORM && cgraph->nodes[j + 4]->op == GGML_OP_MUL) { - const ggml_tensor * repeat = cgraph->nodes[j]; - const ggml_tensor * mul = cgraph->nodes[j + 1]; - ggml_tensor * add = cgraph->nodes[j + 2]; - const ggml_tensor * rms = cgraph->nodes[j + 3]; - ggml_tensor * mulg = cgraph->nodes[j + 4]; - const ggml_tensor * b = repeat->src[0]; - const ggml_tensor * w = mul->src[0] == repeat ? mul->src[1] : mul->src[0]; - const ggml_tensor * res = add->src[0] == mul ? add->src[1] : add->src[0]; - const ggml_tensor * gamma = mulg->src[0] == rms ? mulg->src[1] : mulg->src[0]; - - const int64_t n_embd = add->ne[0]; - const int64_t hc = add->ne[1]; - const int64_t n_tok = add->ne[2]; - - const bool w_ok = (w == scale2 || (ggml_cuda_is_view_or_noop(w) && w->view_src == scale2)) && - w->type == GGML_TYPE_F32 && ggml_is_contiguous(w) && ggml_nelements(w) == hc * n_tok && w->ne[0] == 1 && w->ne[1] == hc; - const bool b_ok = b->type == GGML_TYPE_F32 && ggml_is_contiguous(b) && ggml_nelements(b) == n_embd * n_tok && - b->ne[0] == n_embd && b->ne[1] == 1; - const bool ok = add->ne[3] == 1 && n_tok <= 65535 && - inject->type == GGML_TYPE_F32 && ggml_is_contiguous(inject) && ggml_nelements(inject) == hc * n_tok && - ggml_nrows(inject) == n_tok && - ggml_are_same_shape(scale1, inject) && ggml_are_same_shape(scale2, inject) && - (mul->src[0] == repeat || mul->src[1] == repeat) && w != repeat && - (add->src[0] == mul || add->src[1] == mul) && res != mul && - rms->src[0] == add && (mulg->src[0] == rms || mulg->src[1] == rms) && gamma != rms && - scale2->type == GGML_TYPE_F32 && repeat->type == GGML_TYPE_F32 && - mul->type == GGML_TYPE_F32 && add->type == GGML_TYPE_F32 && res->type == GGML_TYPE_F32 && - rms->type == GGML_TYPE_F32 && mulg->type == GGML_TYPE_F32 && gamma->type == GGML_TYPE_F32 && - ggml_are_same_shape(repeat, add) && ggml_are_same_shape(mul, add) && ggml_are_same_shape(res, add) && - ggml_are_same_shape(rms, add) && ggml_are_same_shape(mulg, add) && - ggml_is_contiguous(res) && ggml_is_contiguous(add) && ggml_is_contiguous(mulg) && ggml_is_contiguous(gamma) && - ggml_nelements(gamma) == n_embd * hc && gamma->ne[0] == n_embd && gamma->ne[1] == hc && w_ok && b_ok; - - if (ok) { - ggml_cuda_hc_combine_norm_args args; - args.inject = inject; - args.residual = res; - args.block_out = b; - args.gamma = gamma; - args.out_res = add; - args.out_xn = mulg; - args.s1 = ggml_get_op_params_f32(scale1, 0); - args.b1 = ggml_get_op_params_f32(scale1, 1); - args.s2 = ggml_get_op_params_f32(scale2, 0); - args.b2 = ggml_get_op_params_f32(scale2, 1); - memcpy(&args.eps, rms->op_params, sizeof(float)); - - // every block reads block_out and inject and writes its own stream of both outputs: the outputs - // must not overlap those, and the residual may only alias the first output in place - auto overlap = [](const ggml_tensor * p, const ggml_tensor * q) { - const uintptr_t p0 = (uintptr_t) p->data, p1 = p0 + ggml_backend_buft_get_alloc_size(p->buffer->buft, p); - const uintptr_t q0 = (uintptr_t) q->data, q1 = q0 + ggml_backend_buft_get_alloc_size(q->buffer->buft, q); - return p0 < q1 && q0 < p1; - }; - const bool res_inplace = res->data == add->data && ggml_are_same_layout(res, add); - bool alias_ok = !overlap(add, b) && !overlap(add, inject) && (res_inplace || !overlap(add, res)) && - !overlap(mulg, b) && !overlap(mulg, inject) && !overlap(mulg, res) && !overlap(mulg, add); - // outputs overlapping the inputs (typically xn reusing the block_out buffer, which dies at the - // combine): single-block variant that reads everything before it writes - if (!alias_ok && n_tok == 1 && hc <= 4) { - args.single_block = true; - alias_ok = true; - } - - int idxs[16]; - enum ggml_op ops[16]; - int count = 0; - for (int k = i; k <= j + 4; ++k) { - const ggml_tensor * n = cgraph->nodes[k]; - if (ggml_cuda_is_view_or_noop(n) && n->view_src != scale2) { - continue; - } - idxs[count] = k; - ops[count] = n->op; - count++; + { + ggml_cuda_hc_combine_norm_args args; + const int skip = ggml_cuda_match_hc_combine_norm(cgraph, i, args, + ggml_cuda_info().devices[cuda_ctx->device].warp_size, true); + if (skip > 0 && !args.single_block) { + // HC16: a BF16 copy of xn for its consumers, and BF16 residual / block_out streams where every reader agreed + args.out_xn_bf16 = ggml_cuda_mmb_cache_reserve(*cuda_ctx, args.out_xn, (size_t) ggml_nelements(args.out_xn)); + args.store_xn_f32 = !(args.out_xn_bf16 && ggml_cuda_mmb_is_bf16_only(args.out_xn)); + if (ggml_cuda_mmb_res16()) { + const ggml_tensor * rin = args.residual->view_src ? args.residual->view_src : args.residual; + const ggml_tensor * rout = args.out_res->view_src ? args.out_res->view_src : args.out_res; + const bool in16 = ggml_cuda_mmb_is_bf16_only(rin) && args.residual->view_offs == 0; + const bool out16 = ggml_cuda_mmb_is_bf16_only(rout) && args.out_res->view_offs == 0; + if (args.out_res->data == args.residual->data && in16 != out16) { + GGML_ABORT("hc_combine_norm: in-place residual with mismatched BF16 marks (%s); the graph_optimize " + "allocation dependency should have separated them", args.out_res->name); } - const int out_nodes[] = { j + 2, j + 4 }; - if (alias_ok && ggml_cuda_hc_combine_norm_supported(args, ggml_cuda_info().devices[cuda_ctx->device].warp_size) && - ggml_can_fuse_subgraph_ext(cgraph, idxs, count, ops, out_nodes, 2)) { - ggml_cuda_op_hc_combine_norm(*cuda_ctx, args); - return j + 4 - i; + const ggml_tensor * rblk = args.block_out->view_src ? args.block_out->view_src : args.block_out; + args.blk_in_bf16 = (ggml_cuda_mmb_blk16() && ggml_cuda_mmb_is_bf16_only(rblk) && args.block_out->view_offs == 0) + ? (const uint16_t *) args.block_out->data : nullptr; + args.res_in_bf16 = in16 ? (const uint16_t *) args.residual->data : nullptr; + args.res_out_bf16 = out16 ? (uint16_t *) args.out_res->data : nullptr; + } + } else { + // HC16: a combine whose streams were marked BF16-only in graph_optimize has no unfused fallback (the marked + // tensors were never written as F32), so a refusal here (buffer aliasing, or the single-block kernel) is a bug + ggml_cuda_hc_combine_norm_args sa; + const int structural = skip > 0 ? skip : ggml_cuda_match_hc_combine_norm(cgraph, i, sa, + ggml_cuda_info().devices[cuda_ctx->device].warp_size, false); + if (structural > 0) { + const ggml_cuda_hc_combine_norm_args & a = skip > 0 ? args : sa; + auto marked = [](const ggml_tensor * t) { return ggml_cuda_mmb_is_bf16_only(t) || (t->view_src && ggml_cuda_mmb_is_bf16_only(t->view_src)); }; + if (marked(a.out_xn) || marked(a.residual) || marked(a.out_res) || marked(a.block_out)) { + GGML_ABORT("hc_combine_norm: %s has BF16-only streams but the fused kernel was refused (%s); the graph_optimize " + "allocation dependencies should have prevented the aliasing", a.out_res->name, + skip > 0 ? "single-block only" : "buffer aliasing"); } } } + if (skip > 0) { + ggml_cuda_op_hc_combine_norm(*cuda_ctx, args); + return skip; + } } // qwen4exp hyper-connection combine: @@ -5428,7 +5880,40 @@ static bool ggml_cuda_graph_set_enabled(ggml_backend_cuda_context * cuda_ctx, co } #endif // USE_CUDA_GRAPH +// tracks when a new split sequence starts, so the mmb marks are cleared once per sequence +static bool g_gt_after_compute = true; +static const void * g_gt_first_split = nullptr; + +// the marks are keyed by tensor pointer and belong to the graphs graph_optimize last saw. A graph computed without +// that optimize (test-backend-ops, any direct ggml_backend_graph_compute caller) can recycle those addresses, so a +// producer would store BF16 into a tensor of another graph and a plain reader would abort. Fingerprint each optimized +// split and drop the marks when a graph arrives that none of them match; a reused (identical) graph keeps them. +static std::vector<uint64_t> g_gt_graph_sigs; + +static uint64_t ggml_cuda_graph_sig(const ggml_cgraph * cgraph) { + // no data pointers: the scheduler allocates after graph_optimize, so they differ between optimize and compute + uint64_t h = 1469598103934665603ull ^ (uint64_t) cgraph->n_nodes; + for (int i = 0; i < cgraph->n_nodes; ++i) { + const ggml_tensor * t = cgraph->nodes[i]; + h = (h ^ (uint64_t) (uintptr_t) t) * 1099511628211ull; + h = (h ^ (uint64_t) t->op ^ ((uint64_t) t->ne[0] << 8) ^ ((uint64_t) t->ne[1] << 24)) * 1099511628211ull; + for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) { + h = (h ^ (uint64_t) (uintptr_t) t->src[s]) * 1099511628211ull; + } + } + return h; +} + static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) { + if (ggml_cuda_mmb_marks_count() > 0) { + const uint64_t sig = ggml_cuda_graph_sig(cgraph); + if (std::find(g_gt_graph_sigs.begin(), g_gt_graph_sigs.end(), sig) == g_gt_graph_sigs.end()) { + ggml_cuda_mmb_marks_clear(); + g_gt_graph_sigs.clear(); + g_gt_first_split = nullptr; + } + } + ggml_cuda_mmb_begin_graph(); ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context; ggml_cuda_set_device(cuda_ctx->device); @@ -5484,6 +5969,7 @@ static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cuda_graph_evaluate_and_capture(cuda_ctx, cgraph, use_cuda_graph, cuda_graph_update_required, graph_key); + g_gt_after_compute = true; return GGML_STATUS_SUCCESS; } @@ -5512,11 +5998,310 @@ static void ggml_backend_cuda_event_wait(ggml_backend_t backend, ggml_backend_ev } } + +// true when every use of x, and of the views of x present in this split, is a node of this split graph. The scheduler +// optimizes each split separately and the split graph view shares the full graph's use counts, so a reader that lives +// in a later split (llama-bench's zero-output prefill ubatches place the final get_rows elsewhere) shows up as a use +// count the local scan cannot account for; such a tensor must keep its F32 store. +static bool ggml_cuda_marks_is_view(const ggml_tensor * t) { + return t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE || t->op == GGML_OP_PERMUTE || t->op == GGML_OP_TRANSPOSE; +} + +static bool ggml_cuda_marks_readers_local(const ggml_cgraph * g, const ggml_tensor * x) { + auto uses = [g](const ggml_tensor * t) -> int { + const size_t h = ggml_hash_find(&g->visited_hash_set, t); + return h == GGML_HASHSET_FULL || !ggml_bitset_get(g->visited_hash_set.used, h) ? -1 : g->use_counts[h]; + }; + int expected = uses(x), found = 0; + if (expected < 0) return false; + for (int i = 0; i < g->n_nodes; ++i) { + const ggml_tensor * t = g->nodes[i]; + if (t != x && ggml_cuda_marks_is_view(t) && t->view_src == x) { + const int u = uses(t); + if (u < 0) return false; + expected += u; + } + for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) { + const ggml_tensor * src = t->src[s]; + if (src == x || (ggml_cuda_marks_is_view(src) && src->view_src == x)) ++found; + } + } + return expected == found; +} + static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph * cgraph, ggml_backend_graph_optimize_params * params) { ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context; + { // marks live for the whole scheduled graph (all splits): clear at the first optimize after a compute, or when the first split repeats + const void * key = cgraph->n_nodes ? cgraph->nodes[0] : nullptr; + if (g_gt_after_compute || g_gt_first_split == nullptr || key == g_gt_first_split) { ggml_cuda_mmb_marks_clear(); g_gt_graph_sigs.clear(); g_gt_first_split = key; g_gt_after_compute = false; } + } + { // HC16: mark tensors whose readers all take the BF16 copy, so their producers skip the F32 store + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + const int ws = ggml_cuda_info().devices[cuda_ctx->device].warp_size; + auto reads = [](const ggml_tensor * t, const ggml_tensor * x) { for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) if (t->src[s] == x || t->src[s]->view_src == x) return true; return false; }; + // normalized stream xn: read only by MMB GEMMs (which take the BF16 copy) and the fused stream mix + for (int i = 0; i < cgraph->n_nodes; ++i) { + ggml_cuda_hc_combine_norm_args ca; + if (ggml_cuda_match_hc_combine_norm(cgraph, i, ca, ws, false) <= 0) continue; // structure only: no data pointers yet + const ggml_tensor * xn = ca.out_xn; + if (ggml_nrows(xn) < 512) continue; + bool ok = true; int nread = 0; + for (int n = i + 1; n < cgraph->n_nodes && ok; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + if (!reads(t, xn)) continue; + ++nread; + if (t->op == GGML_OP_MUL_MAT && t->src[0]->type != GGML_TYPE_F32 && + ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) continue; + if (t->op == GGML_OP_MUL && n >= 1) { ggml_cuda_hc_mix_args ma; if (ggml_cuda_hc_mix_closed(cgraph, n - 1, ma) > 0 && (ma.xn == t->src[0] || ma.xn == t->src[1]) && (ma.xn == xn || ma.xn->view_src == xn)) continue; } + if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE) continue; + ok = false; + } + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, xn)) ggml_cuda_mmb_mark_bf16_only(xn); + } + if (ggml_cuda_mmb_blk16()) { // block_out (attention out-proj / MoE merge) is read only by the fused combine + std::vector<const ggml_tensor *> blks; + for (int i = 0; i < cgraph->n_nodes; ++i) { + ggml_cuda_hc_combine_norm_args ca; + if (ggml_cuda_match_hc_combine_norm(cgraph, i, ca, ws, false) > 0) + blks.push_back(ca.block_out->view_src ? ca.block_out->view_src : ca.block_out); + } + for (const ggml_tensor * b : blks) { + if (ggml_nrows(b) < 512 || b->ne[0] % 8 != 0) continue; + bool producer_ok = false; // only producers that honour a BF16 mark + if (b->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_supported_mm(b->src[0], b->src[1], b)) producer_ok = true; + if (!producer_ok) { + for (int k = 0; k < cgraph->n_nodes && !producer_ok; ++k) { + ggml_cuda_moe_weighted_reduction_match mm; + if (cgraph->nodes[k]->op != GGML_OP_MUL) continue; + if (!ggml_cuda_match_moe_weighted_reduction(cgraph, k, mm)) continue; + if (mm.dst == b) producer_ok = true; + else if (k + mm.node_count < cgraph->n_nodes && + cgraph->nodes[k + mm.node_count] == b && b->op == GGML_OP_ADD && + ggml_node_has_n_uses(cgraph, k + mm.node_count - 1, 1) && + (b->src[0] == mm.dst || b->src[1] == mm.dst)) producer_ok = true; + } + } + if (!producer_ok) continue; + int bi = -1; + for (int k = 0; k < cgraph->n_nodes; ++k) if (cgraph->nodes[k] == b) { bi = k; break; } + if (bi < 0) continue; + bool ok = true; int nread = 0; + for (int n = bi + 1; n < cgraph->n_nodes && ok; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + if (!reads(t, b)) continue; + ++nread; + if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE || t->op == GGML_OP_REPEAT) continue; + if (t->op == GGML_OP_MUL || t->op == GGML_OP_ADD) continue; + ok = false; + } + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, b)) ggml_cuda_mmb_mark_bf16_only(b); + } + } + if (ggml_cuda_mmb_res16()) { // residual stream: the only readers are the fused norm and the next combine + std::vector<std::pair<const ggml_tensor *, const ggml_tensor *>> comb; + for (int i = 0; i < cgraph->n_nodes; ++i) { + ggml_cuda_hc_combine_norm_args ca; + if (ggml_cuda_match_hc_combine_norm(cgraph, i, ca, ws, false) > 0) comb.emplace_back(ca.residual, ca.out_res); + } + for (const auto & c : comb) { + const ggml_tensor * r = c.second; + if (ggml_nrows(r) < 512) continue; + int ri = -1; + for (int k = 0; k < cgraph->n_nodes; ++k) if (cgraph->nodes[k] == r) { ri = k; break; } + if (ri < 0) continue; + bool ok = true; int nread = 0; + for (int n = ri + 1; n < cgraph->n_nodes && ok; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + if (!reads(t, r)) continue; + ++nread; + if (t->op == GGML_OP_RMS_NORM) continue; + if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE) continue; + bool next_combine = false; + for (const auto & d : comb) { + const ggml_tensor * dr = d.first->view_src ? d.first->view_src : d.first; + if (dr == r && d.second == t) { next_combine = true; break; } + } + if (next_combine) continue; + ok = false; + } + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, r)) ggml_cuda_mmb_mark_bf16_only(r); + } + } + for (int i = 0; i < cgraph->n_nodes; ++i) { // fused stream mix output read only by MMB GEMMs + ggml_cuda_hc_mix_args ma; + if (ggml_cuda_hc_mix_closed(cgraph, i, ma) <= 0 || !ma.dst) continue; + const ggml_tensor * d = ma.dst; + if (ggml_nrows(d) < 512) continue; + bool ok = true; int nread = 0; + for (int n = i + 1; n < cgraph->n_nodes && ok; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + if (!reads(t, d)) continue; + ++nread; + if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE) continue; + if (t->op == GGML_OP_MUL_MAT && t->src[0]->type != GGML_TYPE_F32 && + ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) continue; + ok = false; + } + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, d)) ggml_cuda_mmb_mark_bf16_only(d); + } + for (int i = 0; i < cgraph->n_nodes; ++i) { // the HC gate GEMM feeding only the fused mix keeps its BF16 epilogue + const ggml_tensor * t = cgraph->nodes[i]; + if (t->op != GGML_OP_MUL_MAT || t->src[0]->ne[0] != 320 || t->src[0]->ne[1] != 10240 || !ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) continue; + bool ok = true; int nread = 0; + for (int n = i + 1; n < cgraph->n_nodes && ok; ++n) { + const ggml_tensor * u = cgraph->nodes[n]; + if (!reads(u, t)) continue; + ++nread; + if (u->op == GGML_OP_UNARY && ggml_get_unary_op(u) == GGML_UNARY_OP_SIGMOID) { ggml_cuda_hc_mix_args ma; if (ggml_cuda_hc_mix_closed(cgraph, n, ma) > 0 && ma.gate == t) continue; } + ok = false; + } + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, t)) ggml_cuda_mmb_mark_bf16_only(t); + } + if (ggml_cuda_mmb_down16()) { // IQ4_NL routed down GEMM read only by the weighted reduction keeps BF16 outputs + for (int i = 0; i < cgraph->n_nodes; ++i) { + ggml_cuda_moe_weighted_reduction_match match; + if (!ggml_cuda_match_moe_weighted_reduction(cgraph, i, match)) continue; + const ggml_tensor * ex = match.experts; + int xi = -1; + for (int k = 0; k < i; ++k) { if (cgraph->nodes[k] == ex) { xi = k; break; } } + if (xi < 0 || ex->op != GGML_OP_MUL_MAT_ID || ex->type != GGML_TYPE_F32) continue; + if (!ggml_cuda_mmb_supported_mmid(ex->src[0], ex->src[1], ex->src[2], const_cast<ggml_tensor *>(ex))) continue; + if (ex->src[0]->type != GGML_TYPE_IQ4_NL) continue; // other formats keep F32 expert outputs + if (!ggml_node_has_n_uses(cgraph, xi, 1)) continue; + if (ex->ne[0] % 8 != 0 || ggml_nrows(ex) < 512) continue; + if (ggml_cuda_marks_readers_local(cgraph, ex)) ggml_cuda_mmb_mark_bf16_only(ex); + } + } + if (ggml_cuda_mmb_res16()) { // 16-bit residual stream: producer and consumer on different buffers + for (int i = 0; i < cgraph->n_nodes; ++i) { + ggml_cuda_hc_combine_norm_args ca; + if (ggml_cuda_match_hc_combine_norm(cgraph, i, ca, ws, false) <= 0) continue; + ggml_tensor * rr = const_cast<ggml_tensor *>(ca.residual->view_src ? ca.residual->view_src : ca.residual); + params->add_alloc_dep(params->user_data, rr, ca.out_res); + params->add_alloc_dep(params->user_data, rr, ca.out_xn); + } + } + } + } + { // BF16 shadow copies of eligible dense weights, and the fused gate/up GEMM: its inputs stay allocated through the + // GLU output, and a GLU read only by a supported routed GEMM keeps the BF16 epilogue copy instead of the F32 output + auto reads = [](const ggml_tensor * t, const ggml_tensor * x) { for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) if (t->src[s] == x || t->src[s]->view_src == x) return true; return false; }; + for (int i = 0; i < cgraph->n_nodes; ++i) { + const ggml_tensor * t = cgraph->nodes[i]; + if (t->op == GGML_OP_MUL_MAT && (t->src[0]->type == GGML_TYPE_IQ4_NL || t->src[0]->type == GGML_TYPE_Q6_K) && ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) ggml_cuda_mmb_shadow_prepare(*cuda_ctx, t->src[0]); + } + for (int i = 0; i < cgraph->n_nodes; ++i) { + const ggml_tensor * glu = cgraph->nodes[i]; + if (glu->op != GGML_OP_GLU || !glu->src[0] || !glu->src[1] || glu->src[0]->op != GGML_OP_MUL_MAT_ID) continue; + if (!ggml_cuda_mmb_supported_glu(glu->src[0]->src[0], glu->src[1]->src[0], glu->src[0]->src[1], glu->src[0]->src[2], glu)) continue; + for (const auto * input : {glu->src[0]->src[0], glu->src[1]->src[0], glu->src[0]->src[1], glu->src[0]->src[2], glu->src[0], glu->src[1]}) { + auto * root = const_cast<ggml_tensor *>(input->view_src ? input->view_src : input); + params->add_alloc_dep(params->user_data, root, const_cast<ggml_tensor *>(glu)); + } + bool ok = true; int nread = 0; + for (int n = i + 1; n < cgraph->n_nodes && ok; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + if (!reads(t, glu)) continue; + ++nread; + if (t->op == GGML_OP_MUL_MAT_ID && t->src[1] == glu && ggml_cuda_mmb_supported_mmid(t->src[0], t->src[1], t->src[2], t)) continue; + ok = false; + } + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, glu)) ggml_cuda_mmb_mark_bf16_only(glu); + } + } static const bool disable_fusion = getenv("GGML_CUDA_DISABLE_FUSION") != nullptr && std::atoi(getenv("GGML_CUDA_DISABLE_FUSION")); if (!disable_fusion) { + for (int i = 0; i < cgraph->n_nodes; ++i) { + ggml_cuda_hc_combine_norm_args args; + const int skip = ggml_cuda_match_hc_combine_norm(cgraph, i, args, + ggml_cuda_info().devices[cuda_ctx->device].warp_size, false); + if (skip > 0) { + const ggml_tensor * inputs[] = {args.block_out, args.inject}; + for (const auto * input : inputs) { + auto * root = const_cast<ggml_tensor *>(input->view_src ? input->view_src : input); + params->add_alloc_dep(params->user_data, root, args.out_xn); + } + // the combined stream must not share a buffer with the normalized output either: the alias check refuses + // that overlap, and a refused combine whose streams are BF16-only has no fallback (last layer: out_res is + // otherwise dead after its own norm, so the allocator would hand its buffer to out_xn) + params->add_alloc_dep(params->user_data, args.out_res, args.out_xn); + i += skip; + } + } + // fused block selection: every input and intermediate output stays allocated through the final output + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + for (int i = 0; i < cgraph->n_nodes; ++i) { + ggml_cuda_qsa_expand_args args; + const int skip = ggml_cuda_match_qsa_expand(cgraph, i, args); + if (skip) { + for (const auto * input : {args.blocks, args.scores, args.cells, args.tail, + (const ggml_tensor *) args.cast_blocks, (const ggml_tensor *) args.cast_cells, (const ggml_tensor *) args.expanded}) { + auto * root = const_cast<ggml_tensor *>(input->view_src ? input->view_src : input); + params->add_alloc_dep(params->user_data, root, args.output); + } + i += skip; + } + } + } + // fused indexer key pooling: the key cache view and the member rows stay allocated through the pooled output + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + for (int i = 0; i < cgraph->n_nodes; ++i) { + ggml_cuda_get_rows_mean4_match pool; + if (ggml_cuda_match_get_rows_mean4(cgraph, i, pool)) { + params->add_alloc_dep(params->user_data, const_cast<ggml_tensor *>(pool.src->view_src ? pool.src->view_src : pool.src), pool.dst); + params->add_alloc_dep(params->user_data, const_cast<ggml_tensor *>(pool.ids->view_src ? pool.ids->view_src : pool.ids), pool.dst); + i += pool.node_count - 1; + } + } + } + + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + for (int i = 0; i < cgraph->n_nodes; ++i) { + if (cgraph->nodes[i]->op != GGML_OP_UNARY) continue; + ggml_cuda_idx_relu_sum_args args; + if (ggml_cuda_match_idx_relu_sum(cgraph, i, args) > 0) { + auto * root = const_cast<ggml_tensor *>(args.score->view_src ? args.score->view_src : args.score); + params->add_alloc_dep(params->user_data, root, args.dst); + } + } + } + + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + for (int i = 0; i < cgraph->n_nodes; ++i) { + if (cgraph->nodes[i]->op != GGML_OP_RMS_NORM) continue; + ggml_cuda_norm_gated_match match; + if (ggml_cuda_norm_gated_match_at(cgraph, i, match) > 0 && match.pre >= 0) { + for (const auto * input : {match.x, match.w, match.z}) { + auto * root = const_cast<ggml_tensor *>(input->view_src ? input->view_src : input); + params->add_alloc_dep(params->user_data, root, match.dst); + } + } + } + } + + // the direct convolution kernels read the history and x while writing the SiLU output + for (int i = 0; i < cgraph->n_nodes; ++i) { + if (cgraph->nodes[i]->op != GGML_OP_CONCAT) { + continue; + } + ggml_cuda_ple_conv_match pm; + ggml_cuda_gdn_conv_match gm; + const ggml_tensor * inputs[2] = { nullptr, nullptr }; + ggml_tensor * out = nullptr; + if (ggml_cuda_ple_conv_match_at_concat(cgraph, i, pm)) { + inputs[0] = pm.x; inputs[1] = pm.state; out = pm.out; + } else if (ggml_cuda_gdn_conv_match_at_concat(cgraph, i, gm)) { + inputs[0] = gm.x; inputs[1] = gm.state; out = gm.out; + } + for (const ggml_tensor * input : inputs) { + if (input) { + auto * root = const_cast<ggml_tensor *>(input->view_src ? input->view_src : input); + params->add_alloc_dep(params->user_data, root, out); + } + } + } + for (int i = 0; i < cgraph->n_nodes; ++i) { if (cgraph->nodes[i]->op != GGML_OP_MUL) { continue; @@ -5546,6 +6331,9 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph GGML_UNUSED(cgraph); #endif + // the marks for this split are complete here; the fingerprint must be recorded before the early returns below + g_gt_graph_sigs.push_back(ggml_cuda_graph_sig(cgraph)); + static bool enable_graph_optimization = [] { const char * env = getenv("GGML_CUDA_GRAPH_OPT"); return env != nullptr && atoi(env) == 1; diff --git a/ggml/src/ggml-cuda/hyperconn.cu b/ggml/src/ggml-cuda/hyperconn.cu index b57bc966a915..46b75160aa8e 100644 --- a/ggml/src/ggml-cuda/hyperconn.cu +++ b/ggml/src/ggml-cuda/hyperconn.cu @@ -1,4 +1,5 @@ #include "hyperconn.cuh" +#include "mmb.cuh" #include "ggml-backend-impl.h" // Explicitly rounded mul/add: keeps the fused kernels from being contracted into FMAs so that @@ -79,6 +80,48 @@ static __global__ void hc_combine_f32( dst[index] = hc_add_rn(residual[index], m); } + +static __global__ void hc_mix_reduce_f32_hc4_parallel( + const float * __restrict__ xn, const float * __restrict__ gate, float * __restrict__ dst, + const int64_t n_embd, const int64_t n_tokens, const float scale, const float bias) { + const int lane = threadIdx.x % 32; + const int stream = threadIdx.x / 32; + const int64_t index = int64_t(blockIdx.x) * 32 + lane; + const int64_t count = n_embd * n_tokens; + __shared__ float products[4][32]; + float value = 0.0f; + if (index < count) { + const int64_t token = index / n_embd; + const int64_t embd = index - token * n_embd; + const int64_t offset = token * (4 * n_embd) + stream * n_embd + embd; + value = hc_mul_rn(xn[offset], hc_sigmoid(gate[offset])); + } + products[stream][lane] = value; + __syncthreads(); + if (stream == 0 && index < count) { + float sum = products[0][lane]; + sum = hc_add_rn(sum, products[1][lane]); + sum = hc_add_rn(sum, products[2][lane]); + sum = hc_add_rn(sum, products[3][lane]); + dst[index] = scale * sum + bias; + } +} + +__device__ __forceinline__ float hc_bf2f32(const uint16_t h) { return __uint_as_float(((uint32_t) h) << 16); } +__device__ __forceinline__ uint16_t hc_f2bf32(const float f) { uint32_t u = __float_as_uint(f); u += 0x7fffu + ((u >> 16) & 1u); return (uint16_t)(u >> 16); } + +// same reduction as hc_mix_reduce_f32, reading BF16 copies of xn and gate (16-bit HC intermediates) +static __global__ void hc_mix_reduce_bf16( + const uint16_t * __restrict__ xn, const uint16_t * __restrict__ gate, float * __restrict__ dst, + const int64_t n_embd, const int64_t n_tokens, const int hc, const float scale, const float bias) { + const int64_t index = int64_t(blockIdx.x) * blockDim.x + threadIdx.x; + if (index >= n_embd * n_tokens) return; + const int64_t t = index / n_embd, e = index - t * n_embd, base = t * (int64_t) n_embd * hc + e; + float acc = hc_mul_rn(hc_bf2f32(xn[base]), hc_sigmoid(hc_bf2f32(gate[base]))); + for (int c = 1; c < hc; ++c) { const int64_t i = base + int64_t(c) * n_embd; acc = hc_add_rn(acc, hc_mul_rn(hc_bf2f32(xn[i]), hc_sigmoid(hc_bf2f32(gate[i])))); } + dst[index] = scale * acc + bias; +} + static bool hc_ranges_overlap(const ggml_tensor * a, const ggml_tensor * b) { const char * a0 = (const char *) a->data; const char * a1 = a0 + ggml_backend_buft_get_alloc_size(a->buffer->buft, a); @@ -123,11 +166,25 @@ void ggml_cuda_op_hc_mix_reduce(ggml_backend_cuda_context & ctx, const ggml_cuda float * out = stage ? staged.alloc(n_embd * n_tokens) : (float *) dst->data; const int64_t n_items = n_embd * n_tokens; - const int threads = 256; - const int blocks = (int) ((n_items + threads - 1) / threads); - const ggml_cuda_kernel_launch_params launch_params(blocks, threads, 0, ctx.stream()); - ggml_cuda_kernel_launch(hc_mix_reduce_f32, launch_params, - (const float *) xn->data, (const float *) gate->data, out, n_embd, n_tokens, args.hc, args.scale, args.bias); + const uint16_t * xn16 = ggml_cuda_mmb_cache_lookup(xn); + const uint16_t * g16 = ggml_cuda_mmb_cache_lookup(gate); + if (xn16 && g16) { + const int threads = 256; const int blocks = (int) ((n_items + threads - 1) / threads); + const ggml_cuda_kernel_launch_params launch_params(blocks, threads, 0, ctx.stream()); + ggml_cuda_kernel_launch(hc_mix_reduce_bf16, launch_params, xn16, g16, out, n_embd, n_tokens, args.hc, args.scale, args.bias); + } else if (args.hc == 4 && n_tokens <= 32) { + // one wave per stream, the same in-order sum: bit-identical to the serial kernel with more blocks in flight for decode + const int blocks = (int) ((n_items + 31) / 32); + const ggml_cuda_kernel_launch_params launch_params(blocks, 128, 0, ctx.stream()); + ggml_cuda_kernel_launch(hc_mix_reduce_f32_hc4_parallel, launch_params, + (const float *) xn->data, (const float *) gate->data, out, n_embd, n_tokens, args.scale, args.bias); + } else { + const int threads = 256; + const int blocks = (int) ((n_items + threads - 1) / threads); + const ggml_cuda_kernel_launch_params launch_params(blocks, threads, 0, ctx.stream()); + ggml_cuda_kernel_launch(hc_mix_reduce_f32, launch_params, + (const float *) xn->data, (const float *) gate->data, out, n_embd, n_tokens, args.hc, args.scale, args.bias); + } if (stage) { CUDA_CHECK(cudaMemcpyAsync(dst->data, out, ggml_nbytes(dst), cudaMemcpyDeviceToDevice, ctx.stream())); @@ -235,10 +292,8 @@ static __global__ void __launch_bounds__(HC_CN_BLOCK, 1) hc_combine_norm_f32( } } -// Same computation for one token with all streams in a single block, streams processed one after the other -// with exactly the per-stream expressions and block reduction above. Every input element is loaded into -// registers before the first store, so the outputs may alias any of the inputs (the graph allocator reuses -// the block_out buffer for xn when block_out dies at the combine). +// Read all residual, block and injection values before writing aliased outputs. +// Gamma must remain disjoint from both outputs. #define HC_CN_SINGLE_MAX_HC 4 static __global__ void __launch_bounds__(HC_CN_BLOCK, 1) hc_combine_norm_single_f32( @@ -307,21 +362,33 @@ static __global__ void __launch_bounds__(HC_CN_BLOCK, 1) hc_combine_norm_single_ tmp[c] = warp_reduce_sum(tmp[c]); } - // 3. stores + // Finish residual writes before an overlapping normalized output replaces them. +#pragma unroll + for (int c = 0; c < HC_CN_SINGLE_MAX_HC; ++c) { + if (c < hc) { +#pragma unroll + for (int k = 0; k < 3; ++k) { + const int col = tid + k * HC_CN_BLOCK; + if (col < n_embd) { + out_res[((int64_t) t * hc + c) * n_embd + col] = xs[c][k]; + } + } + } + } + __syncthreads(); + #pragma unroll for (int c = 0; c < HC_CN_SINGLE_MAX_HC; ++c) { if (c < hc) { const float mean = tmp[c] / n_embd; const float scale = rsqrtf(mean + eps); const int64_t row = (int64_t) t * hc + c; - float * dst = out_res + row * n_embd; float * xn = out_xn + row * n_embd; const float * g = gamma + (int64_t) c * n_embd; #pragma unroll for (int k = 0; k < 3; ++k) { const int col = tid + k * HC_CN_BLOCK; if (col < n_embd) { - dst[col] = xs[c][k]; xn[col] = scale * xs[c][k] * g[col]; } } @@ -329,6 +396,82 @@ static __global__ void __launch_bounds__(HC_CN_BLOCK, 1) hc_combine_norm_single_ } } +#define HC_CN_BLOCK2 256 +__device__ __forceinline__ float hc_lo(const uint32_t u) { return __uint_as_float(u << 16); } +__device__ __forceinline__ float hc_hi(const uint32_t u) { return __uint_as_float(u & 0xffff0000u); } +__device__ __forceinline__ uint32_t hc_pack2(const float a, const float b) { + return (uint32_t) hc_f2bf32(a) | ((uint32_t) hc_f2bf32(b) << 16); +} + +// two elements per thread per iteration, packed 32-bit accesses; optional BF16 residual / block_out in and BF16 outputs +static __global__ void __launch_bounds__(HC_CN_BLOCK2, 4) hc_combine_norm_f32_b256( + const float * inject, const float * residual, + const float * block_out, const float * gamma, + float * out_res, float * out_xn, uint16_t * out_xn_bf16, const bool store_xn_f32, + const uint16_t * res_in_bf16, uint16_t * res_out_bf16, const uint16_t * blk_in_bf16, + const int n_embd, const float s1, const float b1, const float s2, const float b2, const float eps) { + __shared__ float s_sum[32]; + const int c = blockIdx.x, t = blockIdx.y, hc = gridDim.x, tid = threadIdx.x; + const float x1 = s1 * inject[(int64_t) t * hc + c] + b1; + const float x2 = hc_sigmoid(x1); + const float w = s2 * x2 + b2; + const int64_t row = (int64_t) t * hc + c; + const float * res = residual + row * n_embd; + const uint16_t * res16 = res_in_bf16 ? res_in_bf16 + row * n_embd : nullptr; + float * dst = out_res + row * n_embd; + uint16_t * dst16 = res_out_bf16 ? res_out_bf16 + row * n_embd : nullptr; + const float * blk = block_out + (int64_t) t * n_embd; + const uint16_t * blk16 = blk_in_bf16 ? blk_in_bf16 + (int64_t) t * n_embd : nullptr; + constexpr int KP = (HC_CN_MAX_EMB / 2 + HC_CN_BLOCK2 - 1) / HC_CN_BLOCK2; + float xs[2 * KP]; + float tmp = 0.0f; +#pragma unroll + for (int k = 0; k < KP; ++k) { + const int col = (tid + k * HC_CN_BLOCK2) * 2; + xs[2 * k] = 0.0f; xs[2 * k + 1] = 0.0f; + if (col + 1 < n_embd) { + float r0, r1, m0, m1; + if (res16) { const uint32_t u = *(const uint32_t *)(res16 + col); r0 = hc_lo(u); r1 = hc_hi(u); } + else { const float2 v = *(const float2 *)(res + col); r0 = v.x; r1 = v.y; } + if (blk16) { const uint32_t u = *(const uint32_t *)(blk16 + col); m0 = hc_lo(u); m1 = hc_hi(u); } + else { const float2 v = *(const float2 *)(blk + col); m0 = v.x; m1 = v.y; } + const float a0 = hc_add_rn(r0, hc_mul_rn(m0, w)); + const float a1 = hc_add_rn(r1, hc_mul_rn(m1, w)); + if (dst16) *(uint32_t *)(dst16 + col) = hc_pack2(a0, a1); + else *(float2 *)(dst + col) = make_float2(a0, a1); + xs[2 * k] = a0; xs[2 * k + 1] = a1; + tmp += a0 * a0 + a1 * a1; + } else if (col < n_embd) { + const float r0 = res16 ? hc_bf2f32(res16[col]) : res[col]; + const float m0 = blk16 ? hc_bf2f32(blk16[col]) : blk[col]; + const float a0 = hc_add_rn(r0, hc_mul_rn(m0, w)); + if (dst16) dst16[col] = hc_f2bf32(a0); else dst[col] = a0; + xs[2 * k] = a0; + tmp += a0 * a0; + } + } + tmp = block_reduce<block_reduce_method::SUM, HC_CN_BLOCK2>(tmp, s_sum); + const float mean = tmp / n_embd; + const float scale = rsqrtf(mean + eps); + const float * g = gamma + (int64_t) c * n_embd; + float * xn = out_xn + row * n_embd; + uint16_t * xh = out_xn_bf16 ? out_xn_bf16 + row * n_embd : nullptr; +#pragma unroll + for (int k = 0; k < KP; ++k) { + const int col = (tid + k * HC_CN_BLOCK2) * 2; + if (col + 1 < n_embd) { + const float2 gv = *(const float2 *)(g + col); + const float v0 = scale * xs[2 * k] * gv.x, v1 = scale * xs[2 * k + 1] * gv.y; + if (store_xn_f32) *(float2 *)(xn + col) = make_float2(v0, v1); + if (xh) *(uint32_t *)(xh + col) = hc_pack2(v0, v1); + } else if (col < n_embd) { + const float v0 = scale * xs[2 * k] * g[col]; + if (store_xn_f32) xn[col] = v0; + if (xh) xh[col] = hc_f2bf32(v0); + } + } +} + bool ggml_cuda_hc_combine_norm_supported(const ggml_cuda_hc_combine_norm_args & a, const int warp_size) { const int64_t n_embd = a.out_res->ne[0]; const int64_t hc = a.out_res->ne[1]; @@ -348,6 +491,7 @@ void ggml_cuda_op_hc_combine_norm(ggml_backend_cuda_context & ctx, const ggml_cu if (a.single_block) { GGML_ASSERT(hc <= HC_CN_SINGLE_MAX_HC); + GGML_ASSERT(a.store_xn_f32 && !a.out_xn_bf16 && !a.res_in_bf16 && !a.res_out_bf16 && !a.blk_in_bf16); const ggml_cuda_kernel_launch_params launch_params(dim3((int) n_tokens, 1, 1), HC_CN_BLOCK, 0, ctx.stream()); ggml_cuda_kernel_launch(hc_combine_norm_single_f32, launch_params, (const float *) a.inject->data, (const float *) a.residual->data, @@ -357,10 +501,12 @@ void ggml_cuda_op_hc_combine_norm(ggml_backend_cuda_context & ctx, const ggml_cu return; } - const ggml_cuda_kernel_launch_params launch_params(dim3((int) hc, (int) n_tokens, 1), HC_CN_BLOCK, 0, ctx.stream()); - ggml_cuda_kernel_launch(hc_combine_norm_f32, launch_params, + // 256 threads x 2 packed elements per iteration (the shipped layout); the BF16 options ride on this kernel + const ggml_cuda_kernel_launch_params lp2(dim3((int) hc, (int) n_tokens, 1), HC_CN_BLOCK2, 0, ctx.stream()); + ggml_cuda_kernel_launch(hc_combine_norm_f32_b256, lp2, (const float *) a.inject->data, (const float *) a.residual->data, (const float *) a.block_out->data, (const float *) a.gamma->data, - (float *) a.out_res->data, (float *) a.out_xn->data, + (float *) a.out_res->data, (float *) a.out_xn->data, a.out_xn_bf16, a.store_xn_f32, + a.res_in_bf16, a.res_out_bf16, a.blk_in_bf16, (int) n_embd, a.s1, a.b1, a.s2, a.b2, a.eps); } diff --git a/ggml/src/ggml-cuda/hyperconn.cuh b/ggml/src/ggml-cuda/hyperconn.cuh index b510c2d632ed..a2de7aab80c7 100644 --- a/ggml/src/ggml-cuda/hyperconn.cuh +++ b/ggml/src/ggml-cuda/hyperconn.cuh @@ -49,6 +49,12 @@ struct ggml_cuda_hc_combine_norm_args { float s1, b1, s2, b2; float eps; bool single_block = false; // alias-proof variant: one block per token, all inputs read before any store (hc <= 4) + // BF16 stream options (HC16): a BF16 copy of xn for consumers that read it, and BF16 in-place residual / block_out + uint16_t * out_xn_bf16 = nullptr; + bool store_xn_f32 = true; // false: every consumer reads the BF16 copy + const uint16_t * res_in_bf16 = nullptr; // `residual` is BF16 in place (marked bf16-only) + const uint16_t * blk_in_bf16 = nullptr; + uint16_t * res_out_bf16 = nullptr; }; bool ggml_cuda_hc_combine_norm_supported(const ggml_cuda_hc_combine_norm_args & args, int warp_size); diff --git a/ggml/src/ggml-cuda/idx-relu-sum.cu b/ggml/src/ggml-cuda/idx-relu-sum.cu new file mode 100644 index 000000000000..6cd833bcbfc3 --- /dev/null +++ b/ggml/src/ggml-cuda/idx-relu-sum.cu @@ -0,0 +1,25 @@ +#include "idx-relu-sum.cuh" + +// Preserve the head addition order of the RELU/CONT/ADD graph. +static __global__ void idx_relu_sum_f32(const float * __restrict__ src, float * __restrict__ dst, + const int n_blocks, const int heads) { + const int b = blockIdx.x * blockDim.x + threadIdx.x; + if (b >= n_blocks) return; + const int64_t tt = blockIdx.y; // token within stream, streams stacked + const float * p = src + tt * (int64_t) heads * n_blocks + b; + float acc = fmaxf(p[0], 0.0f); + for (int h = 1; h < heads; ++h) { + acc = acc + fmaxf(p[(int64_t) h * n_blocks], 0.0f); + } + dst[tt * (int64_t) n_blocks + b] = acc; +} + +void ggml_cuda_op_idx_relu_sum(ggml_backend_cuda_context & ctx, const ggml_cuda_idx_relu_sum_args & args) { + const ggml_tensor * s = args.score; + const int n_blocks = (int) s->ne[0]; + const int64_t rows = s->ne[2] * s->ne[3]; + constexpr int threads = 256; + const dim3 grid((n_blocks + threads - 1) / threads, (unsigned) rows, 1); + idx_relu_sum_f32<<<grid, threads, 0, ctx.stream()>>>((const float *) s->data, (float *) args.dst->data, n_blocks, args.heads); + CUDA_CHECK(cudaGetLastError()); +} diff --git a/ggml/src/ggml-cuda/idx-relu-sum.cuh b/ggml/src/ggml-cuda/idx-relu-sum.cuh new file mode 100644 index 000000000000..04f902bac221 --- /dev/null +++ b/ggml/src/ggml-cuda/idx-relu-sum.cuh @@ -0,0 +1,8 @@ +#include "common.cuh" + +struct ggml_cuda_idx_relu_sum_args { + const ggml_tensor * score = nullptr; + ggml_tensor * dst = nullptr; // [n_blocks, n_tps, n_stream] + int heads = 0; +}; +void ggml_cuda_op_idx_relu_sum(ggml_backend_cuda_context & ctx, const ggml_cuda_idx_relu_sum_args & args); diff --git a/ggml/src/ggml-cuda/mmb-quant.cuh b/ggml/src/ggml-cuda/mmb-quant.cuh new file mode 100644 index 000000000000..6bb8812630d4 --- /dev/null +++ b/ggml/src/ggml-cuda/mmb-quant.cuh @@ -0,0 +1,336 @@ +#pragma once +#include "dequantize.cuh" +#include <type_traits> + +struct mmb_quant_slice { + uint16_t * dst; + int begin; + int offset = 0; + struct element { + uint16_t * dst; + int index; + __device__ void operator=(float value) const { + if (index >= 0 && index < 64) dst[index] = mmb_f2bf(value); + } + }; + __device__ mmb_quant_slice operator+(int64_t n) const { return {dst, begin, offset + (int) n}; } + __device__ element operator[](int64_t n) const { return {dst, offset + (int) n - begin}; } +}; + +template <ggml_type TYPE> +__device__ __forceinline__ void mmb_decode_slice(const void * row, const int k0, uint16_t * dst, const int lane) { + constexpr int QK = ggml_cuda_type_traits<TYPE>::qk; + if constexpr (TYPE == GGML_TYPE_Q1_0) { + constexpr int QR = ggml_cuda_type_traits<TYPE>::qr; +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, qs = (pos % QK) / QR; + float2 v; + dequantize_q1_0(row, ib, qs, v); + const int o = ib * QK + qs - k0; + dst[o] = mmb_f2bf(v.x); + dst[o + (QR == 1 ? 1 : QK / 2)] = mmb_f2bf(v.y); + } + } + else if constexpr (TYPE == GGML_TYPE_Q2_0) { + constexpr int QR = ggml_cuda_type_traits<TYPE>::qr; +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, qs = (pos % QK) / QR; + float2 v; + dequantize_q2_0(row, ib, qs, v); + const int o = ib * QK + qs - k0; + dst[o] = mmb_f2bf(v.x); + dst[o + (QR == 1 ? 1 : QK / 2)] = mmb_f2bf(v.y); + } + } + else if constexpr (TYPE == GGML_TYPE_Q4_0) { + constexpr int QR = ggml_cuda_type_traits<TYPE>::qr; +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, qs = (pos % QK) / QR; + float2 v; + dequantize_q4_0(row, ib, qs, v); + const int o = ib * QK + qs - k0; + dst[o] = mmb_f2bf(v.x); + dst[o + (QR == 1 ? 1 : QK / 2)] = mmb_f2bf(v.y); + } + } + else if constexpr (TYPE == GGML_TYPE_Q4_1) { + constexpr int QR = ggml_cuda_type_traits<TYPE>::qr; +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, qs = (pos % QK) / QR; + float2 v; + dequantize_q4_1(row, ib, qs, v); + const int o = ib * QK + qs - k0; + dst[o] = mmb_f2bf(v.x); + dst[o + (QR == 1 ? 1 : QK / 2)] = mmb_f2bf(v.y); + } + } + else if constexpr (TYPE == GGML_TYPE_Q5_0) { + constexpr int QR = ggml_cuda_type_traits<TYPE>::qr; +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, qs = (pos % QK) / QR; + float2 v; + dequantize_q5_0(row, ib, qs, v); + const int o = ib * QK + qs - k0; + dst[o] = mmb_f2bf(v.x); + dst[o + (QR == 1 ? 1 : QK / 2)] = mmb_f2bf(v.y); + } + } + else if constexpr (TYPE == GGML_TYPE_Q5_1) { + constexpr int QR = ggml_cuda_type_traits<TYPE>::qr; +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, qs = (pos % QK) / QR; + float2 v; + dequantize_q5_1(row, ib, qs, v); + const int o = ib * QK + qs - k0; + dst[o] = mmb_f2bf(v.x); + dst[o + (QR == 1 ? 1 : QK / 2)] = mmb_f2bf(v.y); + } + } + else if constexpr (TYPE == GGML_TYPE_Q8_0) { + constexpr int QR = ggml_cuda_type_traits<TYPE>::qr; +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, qs = (pos % QK) / QR; + float2 v; + dequantize_q8_0(row, ib, qs, v); + const int o = ib * QK + qs - k0; + dst[o] = mmb_f2bf(v.x); + dst[o + (QR == 1 ? 1 : QK / 2)] = mmb_f2bf(v.y); + } + } + else if constexpr (TYPE == GGML_TYPE_Q2_K) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 64; tid += 8) dequantize_q2_K<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_Q3_K) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 64; tid += 8) dequantize_q3_K<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_Q4_K) { + const mmb_quant_slice out{dst, k0 % QK}; + const int tid = (k0 % QK) / 64 * 8 + lane; + dequantize_q4_K<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_Q5_K) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 64; tid += 8) dequantize_q5_K<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_Q6_K) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 64; tid += 8) dequantize_q6_K<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ1_S) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 32; tid += 8) dequantize_iq1_s<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ1_M) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 32; tid += 8) dequantize_iq1_m<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ2_XXS) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 32; tid += 8) dequantize_iq2_xxs<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ2_XS) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 32; tid += 8) dequantize_iq2_xs<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ2_S) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 32; tid += 8) dequantize_iq2_s<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ3_XXS) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 32; tid += 8) dequantize_iq3_xxs<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ3_S) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 32; tid += 8) dequantize_iq3_s<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ4_XS) { + const mmb_quant_slice out{dst, k0 % QK}; +#pragma unroll + for (int tid = lane; tid < 32; tid += 8) dequantize_iq4_xs<float>(row, k0 / QK, out, tid); + } + else if constexpr (TYPE == GGML_TYPE_IQ4_NL) { +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / 32, q = (pos % 32) / 2; + const block_iq4_nl & b = ((const block_iq4_nl *) row)[ib]; + const int o = ib * 32 + q - k0; + dst[o] = mmb_f2bf((float)b.d * kvalues_iq4nl[b.qs[q] & 15]); + dst[o + 16] = mmb_f2bf((float)b.d * kvalues_iq4nl[b.qs[q] >> 4]); + } + } + else if constexpr (TYPE == GGML_TYPE_MXFP4) { +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, q = (pos % QK) / 2; + const block_mxfp4 & b = ((const block_mxfp4 *) row)[ib]; + const float d = ggml_cuda_e8m0_to_fp32(b.e); + const int o = ib * QK + q - k0; + dst[o] = mmb_f2bf(d * kvalues_mxfp4[b.qs[q] & 15] * 0.5f); + dst[o + QK / 2] = mmb_f2bf(d * kvalues_mxfp4[b.qs[q] >> 4] * 0.5f); + } + } + else if constexpr (TYPE == GGML_TYPE_NVFP4) { +#pragma unroll + for (int p = lane; p < 32; p += 8) { + const int pos = k0 + 2 * p, ib = pos / QK, in = pos % QK; + const int sub = in / QK_NVFP4_SUB, j = (in % QK_NVFP4_SUB) / 2; + const block_nvfp4 & b = ((const block_nvfp4 *) row)[ib]; + const float d = ggml_cuda_ue4m3_to_fp32(b.d[sub]); + const uint8_t q = b.qs[sub * (QK_NVFP4_SUB / 2) + j]; + const int o = ib * QK + sub * QK_NVFP4_SUB + j - k0; + dst[o] = mmb_f2bf(d * kvalues_mxfp4[q & 15]); + dst[o + QK_NVFP4_SUB / 2] = mmb_f2bf(d * kvalues_mxfp4[q >> 4]); + } + } +} + +template <int WTYPE> +__host__ __device__ constexpr size_t mmb_row_bytes(int k) { + if constexpr (WTYPE == 0) return (size_t)(k / 32) * 18; + else if constexpr (WTYPE == 1) return (size_t)(k / 32) * 34; + else if constexpr (WTYPE == 2) return (size_t)k * 2; + else { + using traits = ggml_cuda_type_traits<(ggml_type)(WTYPE - 32)>; + return (size_t)(k / traits::qk) * traits::bs; + } +} + +template <int WTYPE, int BM, int STRIDE> +__device__ __forceinline__ void mmb_load_quant_tile(const uint8_t * weights, size_t row_bytes, int rows, int ks, uint16_t * tile) { + for (int row = threadIdx.x / 8; row < BM; row += blockDim.x / 8) { + uint16_t * dst = tile + row * STRIDE; + const int lane = threadIdx.x % 8; + if (row < rows) mmb_decode_slice<(ggml_type)(WTYPE - 32)>(weights + row * row_bytes, ks * 64, dst, lane); + else { +#pragma unroll + for (int k = lane; k < 64; k += 8) dst[k] = 0; + } + } +} + +static bool mmb_quant_type(ggml_type type) { + switch (type) { + case GGML_TYPE_Q1_0: + case GGML_TYPE_Q2_0: + case GGML_TYPE_Q4_0: + case GGML_TYPE_Q4_1: + case GGML_TYPE_Q5_0: + case GGML_TYPE_Q5_1: + case GGML_TYPE_Q8_0: + case GGML_TYPE_Q2_K: + case GGML_TYPE_Q3_K: + case GGML_TYPE_Q4_K: + case GGML_TYPE_Q5_K: + case GGML_TYPE_Q6_K: + case GGML_TYPE_IQ1_S: + case GGML_TYPE_IQ1_M: + case GGML_TYPE_IQ2_XXS: + case GGML_TYPE_IQ2_XS: + case GGML_TYPE_IQ2_S: + case GGML_TYPE_IQ3_XXS: + case GGML_TYPE_IQ3_S: + case GGML_TYPE_IQ4_XS: + case GGML_TYPE_IQ4_NL: + case GGML_TYPE_MXFP4: + case GGML_TYPE_NVFP4: + return true; + default: return false; + } +} + +template <typename Fn> +static void mmb_dispatch_quant(ggml_type type, Fn fn) { + switch (type) { + case GGML_TYPE_Q1_0: fn(std::integral_constant<int, 32 + GGML_TYPE_Q1_0>{}); break; + case GGML_TYPE_Q2_0: fn(std::integral_constant<int, 32 + GGML_TYPE_Q2_0>{}); break; + case GGML_TYPE_Q4_0: fn(std::integral_constant<int, 32 + GGML_TYPE_Q4_0>{}); break; + case GGML_TYPE_Q4_1: fn(std::integral_constant<int, 32 + GGML_TYPE_Q4_1>{}); break; + case GGML_TYPE_Q5_0: fn(std::integral_constant<int, 32 + GGML_TYPE_Q5_0>{}); break; + case GGML_TYPE_Q5_1: fn(std::integral_constant<int, 32 + GGML_TYPE_Q5_1>{}); break; + case GGML_TYPE_Q8_0: fn(std::integral_constant<int, 1>{}); break; + case GGML_TYPE_Q2_K: fn(std::integral_constant<int, 32 + GGML_TYPE_Q2_K>{}); break; + case GGML_TYPE_Q3_K: fn(std::integral_constant<int, 32 + GGML_TYPE_Q3_K>{}); break; + case GGML_TYPE_Q4_K: fn(std::integral_constant<int, 32 + GGML_TYPE_Q4_K>{}); break; + case GGML_TYPE_Q5_K: fn(std::integral_constant<int, 32 + GGML_TYPE_Q5_K>{}); break; + case GGML_TYPE_Q6_K: fn(std::integral_constant<int, 32 + GGML_TYPE_Q6_K>{}); break; + case GGML_TYPE_IQ1_S: fn(std::integral_constant<int, 32 + GGML_TYPE_IQ1_S>{}); break; + case GGML_TYPE_IQ1_M: fn(std::integral_constant<int, 32 + GGML_TYPE_IQ1_M>{}); break; + case GGML_TYPE_IQ2_XXS: fn(std::integral_constant<int, 32 + GGML_TYPE_IQ2_XXS>{}); break; + case GGML_TYPE_IQ2_XS: fn(std::integral_constant<int, 32 + GGML_TYPE_IQ2_XS>{}); break; + case GGML_TYPE_IQ2_S: fn(std::integral_constant<int, 32 + GGML_TYPE_IQ2_S>{}); break; + case GGML_TYPE_IQ3_XXS: fn(std::integral_constant<int, 32 + GGML_TYPE_IQ3_XXS>{}); break; + case GGML_TYPE_IQ3_S: fn(std::integral_constant<int, 32 + GGML_TYPE_IQ3_S>{}); break; + case GGML_TYPE_IQ4_XS: fn(std::integral_constant<int, 32 + GGML_TYPE_IQ4_XS>{}); break; + case GGML_TYPE_IQ4_NL: fn(std::integral_constant<int, 0>{}); break; + case GGML_TYPE_MXFP4: fn(std::integral_constant<int, 32 + GGML_TYPE_MXFP4>{}); break; + case GGML_TYPE_NVFP4: fn(std::integral_constant<int, 32 + GGML_TYPE_NVFP4>{}); break; + default: GGML_ABORT("unsupported MMB quant type"); + } +} + +__device__ __forceinline__ void mmb_dq_q4k_slice(uint4 q0, uint4 q1, uint4 meta, int slice, uint32_t * out) { + const float d = mmb_h2f((uint16_t) meta.x), dm = mmb_h2f((uint16_t)(meta.x >> 16)); + auto byte = [&](int i) { const uint32_t word = i < 4 ? meta.y : i < 8 ? meta.z : meta.w; return (word >> (8 * (i & 3))) & 255; }; + auto scale_min = [&](int i, float & ds, float & ms) { + const uint32_t sc = i < 4 ? byte(i) & 63 : (byte(i + 4) & 15) | ((byte(i - 4) >> 6) << 4); + const uint32_t mn = i < 4 ? byte(i + 4) & 63 : (byte(i + 4) >> 4) | ((byte(i) >> 6) << 4); + ds = d * sc; ms = dm * mn; + }; + float d0, d1, m0, m1; + scale_min(2 * slice, d0, m0); scale_min(2 * slice + 1, d1, m1); + const uint32_t words[8] = {q0.x, q0.y, q0.z, q0.w, q1.x, q1.y, q1.z, q1.w}; +#pragma unroll + for (int j = 0; j < 8; ++j) { + const uint32_t q = words[j]; + out[2*j] = mmb_pack2(d0 * (q & 15) - m0, d0 * ((q >> 8) & 15) - m0); + out[2*j + 1] = mmb_pack2(d0 * ((q >> 16) & 15) - m0, d0 * ((q >> 24) & 15) - m0); + out[16 + 2*j] = mmb_pack2(d1 * ((q >> 4) & 15) - m1, d1 * ((q >> 12) & 15) - m1); + out[16 + 2*j + 1] = mmb_pack2(d1 * ((q >> 20) & 15) - m1, d1 * (q >> 28) - m1); + } +} + + +__device__ __forceinline__ void mmb_dq_q51_pair(uint4 a, uint4 b, uint4 c, uint32_t * out) { + const uint32_t words[12] = {a.x,a.y,a.z,a.w,b.x,b.y,b.z,b.w,c.x,c.y,c.z,c.w}; +#pragma unroll + for (int block = 0; block < 2; ++block) { + const uint32_t dm = words[block * 6], high = words[block * 6 + 1]; + const float d = mmb_h2f((uint16_t)dm), m = mmb_h2f((uint16_t)(dm >> 16)); +#pragma unroll + for (int j = 0; j < 4; ++j) { + const uint32_t q = words[block * 6 + 2 + j]; + float lo[4], hi[4]; +#pragma unroll + for (int lane = 0; lane < 4; ++lane) { + const int low = ((q >> (8 * lane)) & 15) | (((high >> (4 * j + lane)) & 1) << 4); + const int upper = ((q >> (8 * lane + 4)) & 15) | (((high >> (16 + 4 * j + lane)) & 1) << 4); + lo[lane] = d * low + m; hi[lane] = d * upper + m; + } + out[block * 16 + 2*j] = mmb_pack2(lo[0],lo[1]); + out[block * 16 + 2*j + 1] = mmb_pack2(lo[2],lo[3]); + out[block * 16 + 8 + 2*j] = mmb_pack2(hi[0],hi[1]); + out[block * 16 + 8 + 2*j + 1] = mmb_pack2(hi[2],hi[3]); + } + } +} diff --git a/ggml/src/ggml-cuda/mmb.cu b/ggml/src/ggml-cuda/mmb.cu new file mode 100644 index 000000000000..4fc7e7440971 --- /dev/null +++ b/ggml/src/ggml-cuda/mmb.cu @@ -0,0 +1,964 @@ +#include "mmb.cuh" +#include "unary.cuh" +#include <unordered_map> +#include <map> +#include <utility> +#include "mmid.cuh" +#include <cstdlib> +#include <vector> +#include <unordered_set> + +namespace { + +typedef short v16s __attribute__((ext_vector_type(16))); +typedef float v8f __attribute__((ext_vector_type(8))); +constexpr int MMB_BK = 64, MMB_NT = 256, MMB_LDS_STRIDE = MMB_BK + 8; + +__device__ __forceinline__ uint16_t mmb_f2bf(float f) { uint32_t u = __float_as_uint(f); u += 0x7fffu + ((u >> 16) & 1u); return (uint16_t)(u >> 16); } +__device__ __forceinline__ uint32_t mmb_pack2(float a, float b) { return (uint32_t)mmb_f2bf(a) | ((uint32_t)mmb_f2bf(b) << 16); } +__constant__ int8_t mmb_kv_iq4nl[16] = {-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113}; +__device__ __forceinline__ float mmb_h2f(uint16_t h) { return (float) __builtin_bit_cast(_Float16, h); } + +__global__ void mmb_cvt_f32_bf16(const float * __restrict__ x, uint16_t * __restrict__ y, const size_t n) { + size_t i = ((size_t)blockIdx.x * blockDim.x + threadIdx.x) * 8; + if (i + 8 <= n) { + const float4 a = *(const float4 *)(x + i), b = *(const float4 *)(x + i + 4); + uint4 o; o.x = mmb_pack2(a.x, a.y); o.y = mmb_pack2(a.z, a.w); o.z = mmb_pack2(b.x, b.y); o.w = mmb_pack2(b.z, b.w); + *(uint4 *)(y + i) = o; + } else { + for (; i < n; ++i) y[i] = mmb_f2bf(x[i]); + } +} + +} +#include "mmb-quant.cuh" +namespace { + +// dequantize one weight row's two consecutive IQ4_NL blocks (36 bytes) into 64 bf16 in LDS. +// LUT held in registers as (kv + 128) bytes and applied with v_perm_b32 (4 nibbles per op pair) instead of a per-lane +// indexed constant array (which lowers to one scalar-byte memory load per element). The value kv*d is produced as +// fma(kv+128, d, -128*d): -128*d is exact, so the single rounding equals RN(kv*d) -> bitwise the same BF16 as before. +__device__ __forceinline__ void mmb_dq_row36(const uint4 w0, const uint4 w1, const uint32_t w2, uint32_t * arow) { + const uint32_t ws[9] = {w0.x, w0.y, w0.z, w0.w, w1.x, w1.y, w1.z, w1.w, w2}; + const float d0 = mmb_h2f((uint16_t)(ws[0] & 0xffff)), d1 = mmb_h2f((uint16_t)(ws[4] >> 16)); + const uint32_t q0[4] = { (ws[0] >> 16) | (ws[1] << 16), (ws[1] >> 16) | (ws[2] << 16), (ws[2] >> 16) | (ws[3] << 16), (ws[3] >> 16) | (ws[4] << 16) }; + const uint32_t q1[4] = { ws[5], ws[6], ws[7], ws[8] }; + // kv + 128 = {1,24,45,63,79,93,106,118,129,141,153,166,181,197,217,241} packed little-endian, 4 per dword + const uint32_t L0 = 0x3f2d1801u, L1 = 0x766a5d4fu, L2 = 0xa6998d81u, L3 = 0xf1d9c5b5u; +#pragma unroll + for (int blk = 0; blk < 2; ++blk) { + const float d = blk ? d1 : d0; const float md = -128.0f * d; const uint32_t * q = blk ? q1 : q0; uint32_t * out = arow + blk * 16; +#pragma unroll + for (int w = 0; w < 4; ++w) { + const uint32_t v = q[w]; + const uint32_t nib[2] = { v & 0x0F0F0F0Fu, (v >> 4) & 0x0F0F0F0Fu }; + float x[2][4]; +#pragma unroll + for (int h = 0; h < 2; ++h) { + const uint32_t n = nib[h]; + const uint32_t sel = n & 0x07070707u; + const uint32_t pA = __builtin_amdgcn_perm(L1, L0, sel); // entries 0..7 + const uint32_t pB = __builtin_amdgcn_perm(L3, L2, sel); // entries 8..15 + const uint32_t m = ((n >> 3) & 0x01010101u) * 0xFFu; // 0xFF where the nibble >= 8 + const uint32_t u = (pA & ~m) | (pB & m); + x[h][0] = fmaf((float)(u & 0xFFu), d, md); + x[h][1] = fmaf((float)((u >> 8) & 0xFFu), d, md); + x[h][2] = fmaf((float)((u >> 16) & 0xFFu), d, md); + x[h][3] = fmaf((float)(u >> 24), d, md); + } + out[2*w] = mmb_pack2(x[0][0], x[0][1]); out[2*w + 1] = mmb_pack2(x[0][2], x[0][3]); + out[8 + 2*w] = mmb_pack2(x[1][0], x[1][1]); out[8 + 2*w + 1] = mmb_pack2(x[1][2], x[1][3]); + } + } +} + +// dequantize one weight row's two consecutive Q8_0 blocks (68 bytes: d0 qs0[32] d1 qs1[32]) into 64 bf16 in LDS +__device__ __forceinline__ void mmb_dq_row68(const uint4 w0, const uint4 w1, const uint4 w2, const uint4 w3, const uint32_t w4, uint32_t * arow) { + const uint32_t ws[17] = {w0.x,w0.y,w0.z,w0.w, w1.x,w1.y,w1.z,w1.w, w2.x,w2.y,w2.z,w2.w, w3.x,w3.y,w3.z,w3.w, w4}; + const float d0 = mmb_h2f((uint16_t)(ws[0] & 0xffff)), d1 = mmb_h2f((uint16_t)(ws[8] >> 16)); +#pragma unroll + for (int blk = 0; blk < 2; ++blk) { + const float d = blk ? d1 : d0; uint32_t * out = arow + blk * 16; +#pragma unroll + for (int w = 0; w < 8; ++w) { + const uint32_t v = blk ? ws[9 + w] : ((ws[w] >> 16) | (ws[w + 1] << 16)); + const float e0 = d * (float)(int8_t)(v ), e1 = d * (float)(int8_t)(v >> 8); + const float e2 = d * (float)(int8_t)(v >> 16), e3 = d * (float)(int8_t)(v >> 24); + out[2*w] = mmb_pack2(e0, e1); out[2*w + 1] = mmb_pack2(e2, e3); + } + } +} + +template <typename DRowFn> +__device__ __forceinline__ void mmb_store_tile(const v8f & acc, float * __restrict__ stg, float * __restrict__ D, uint16_t * __restrict__ Dh, + const bool store_f32, const int M, DRowFn drow, const int n_base, const int m_base, const int lane) { + const int cm = lane & 15, cn = lane >> 4; +#pragma unroll + for (int e = 0; e < 8; ++e) { stg[(2 * e + cn) * 16 + cm] = acc[e]; } + __syncthreads(); + const int n = lane >> 1, half = lane & 1; + const int dr = drow(n_base + n); + if (dr >= 0) { + const float4 v0 = *(const float4 *)(stg + n * 16 + half * 8), v1 = *(const float4 *)(stg + n * 16 + half * 8 + 4); + const size_t base = (size_t)dr * M + m_base + half * 8; + const bool full = m_base + 16 <= M; + if (store_f32) { + if (full && (M & 3) == 0) { *(float4 *)(D + base) = v0; *(float4 *)(D + base + 4) = v1; } + else { const float vv[8] = {v0.x, v0.y, v0.z, v0.w, v1.x, v1.y, v1.z, v1.w}; +#pragma unroll + for (int k = 0; k < 8; ++k) { if (m_base + half * 8 + k < M) D[base + k] = vv[k]; } } + } + if (Dh) { + if (full && (M & 7) == 0) { *(uint4 *)(Dh + base) = make_uint4(mmb_pack2(v0.x, v0.y), mmb_pack2(v0.z, v0.w), mmb_pack2(v1.x, v1.y), mmb_pack2(v1.z, v1.w)); } + else { const float vv[8] = {v0.x, v0.y, v0.z, v0.w, v1.x, v1.y, v1.z, v1.w}; +#pragma unroll + for (int k = 0; k < 8; ++k) { if (m_base + half * 8 + k < M) Dh[base + k] = mmb_f2bf(vv[k]); } } + } + } + __syncthreads(); +} +template <int BM, int BN, int WTM, int WTN, int WTYPE, bool TAIL, typename XRowFn, typename DRowFn> +__device__ __forceinline__ void mmb_tile_gemm(const uint8_t * __restrict__ Wbase, const size_t wrow_bytes, const int a_rows, + const uint16_t * __restrict__ Xh, const int K, XRowFn xrow, float * __restrict__ D, uint16_t * __restrict__ Dh, const bool store_f32, const int M, DRowFn drow, const int m0, + const int n_cols, uint16_t * As, uint16_t * Bs) { + constexpr int WAVES_M = BM / WTM, TM = WTM / 16, TN = WTN / 16; + constexpr int A_ITEMS = (BM + MMB_NT - 1) / MMB_NT; + constexpr int B_ITEMS = (BN * 8) / MMB_NT; + const int tid = threadIdx.x, lane = tid & 31, wave = tid >> 5; + const int wm = wave % WAVES_M, wn = wave / WAVES_M; + uint4 a0[A_ITEMS], a1[A_ITEMS], a3[A_ITEMS], a4[A_ITEMS], a5[A_ITEMS], a6[A_ITEMS], a7[A_ITEMS], a8[A_ITEMS]; uint32_t a2[A_ITEMS]; + uint4 bst[B_ITEMS]; + int brow[B_ITEMS]; +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; brow[i] = xrow(c >> 3); } + + int weight_ks = 0; + auto load_regs = [&](const int ks) { + weight_ks = ks; +#pragma unroll + for (int i = 0; i < A_ITEMS; ++i) { + const int row = tid + i * MMB_NT; + if (row < BM && row < a_rows) { + if constexpr (WTYPE == 0) { const uint8_t * p = Wbase + (size_t)row * wrow_bytes + (size_t)ks * 36; + a0[i] = *(const uint4 *)(p); a1[i] = *(const uint4 *)(p + 16); a2[i] = *(const uint32_t *)(p + 32); } + else if constexpr (WTYPE == 1) { const uint8_t * p = Wbase + (size_t)row * wrow_bytes + (size_t)ks * 68; + a0[i] = *(const uint4 *)(p); a1[i] = *(const uint4 *)(p + 16); a3[i] = *(const uint4 *)(p + 32); a4[i] = *(const uint4 *)(p + 48); a2[i] = *(const uint32_t *)(p + 64); } + else if constexpr (WTYPE == 32 + GGML_TYPE_Q5_1) { + const uint4 * p = (const uint4 *)(Wbase + (size_t)row * wrow_bytes + (size_t)ks * 48); + a0[i] = p[0]; a1[i] = p[1]; a3[i] = p[2]; + } + else if constexpr (WTYPE == 2) { const uint4 * p = (const uint4 *)(Wbase + (size_t)row * wrow_bytes + (size_t)ks * 128); + a0[i] = p[0]; a1[i] = p[1]; a3[i] = p[2]; a4[i] = p[3]; a5[i] = p[4]; a6[i] = p[5]; a7[i] = p[6]; a8[i] = p[7]; } + } else { a0[i] = make_uint4(0,0,0,0); a1[i] = make_uint4(0,0,0,0); a3[i] = make_uint4(0,0,0,0); a4[i] = make_uint4(0,0,0,0); a5[i] = a6[i] = a7[i] = a8[i] = make_uint4(0,0,0,0); a2[i] = 0; } + } +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { + const int c = tid + i * MMB_NT; const int off = (c & 7) * 8; + bst[i] = (brow[i] >= 0) ? *(const uint4 *)(Xh + (size_t)brow[i] * K + ks * MMB_BK + off) : make_uint4(0,0,0,0); + } + }; + auto store_lds = [&]() { + if constexpr (WTYPE >= 32 && WTYPE != 32 + GGML_TYPE_Q5_1) mmb_load_quant_tile<WTYPE, BM, MMB_LDS_STRIDE>(Wbase, wrow_bytes, a_rows, weight_ks, As); +#pragma unroll + for (int i = 0; i < A_ITEMS; ++i) { const int row = tid + i * MMB_NT; if (row < BM) { + if constexpr (WTYPE == 0) mmb_dq_row36(a0[i], a1[i], a2[i], (uint32_t *)(As + row * MMB_LDS_STRIDE)); + else if constexpr (WTYPE == 1) mmb_dq_row68(a0[i], a1[i], a3[i], a4[i], a2[i], (uint32_t *)(As + row * MMB_LDS_STRIDE)); + else if constexpr (WTYPE == 32 + GGML_TYPE_Q5_1) mmb_dq_q51_pair(a0[i], a1[i], a3[i], (uint32_t *)(As + row * MMB_LDS_STRIDE)); + else if constexpr (WTYPE == 2) { uint4 * d = (uint4 *)(As + row * MMB_LDS_STRIDE); d[0] = a0[i]; d[1] = a1[i]; d[2] = a3[i]; d[3] = a4[i]; d[4] = a5[i]; d[5] = a6[i]; d[6] = a7[i]; d[7] = a8[i]; } } } +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; *(uint4 *)(Bs + (c >> 3) * MMB_LDS_STRIDE + (c & 7) * 8) = bst[i]; } + }; + + v8f acc[TM][TN]; +#pragma unroll + for (int i = 0; i < TM; ++i) +#pragma unroll + for (int j = 0; j < TN; ++j) +#pragma unroll + for (int e = 0; e < 8; ++e) acc[i][j][e] = 0.f; + + bool jact[TN]; +#pragma unroll + for (int j = 0; j < TN; ++j) jact[j] = !TAIL || (wn * WTN + j * 16) < n_cols; // whole fragments past the valid columns are never stored + const int nks = K / MMB_BK; + load_regs(0); store_lds(); __syncthreads(); + for (int ks = 0; ks < nks; ++ks) { + if (ks + 1 < nks) load_regs(ks + 1); +#pragma unroll + for (int kk = 0; kk < MMB_BK; kk += 16) { + v16s a[TM], b[TN]; const int r = lane & 15; +#pragma unroll + for (int i = 0; i < TM; ++i) { const uint16_t * p = As + (wm * WTM + i * 16 + r) * MMB_LDS_STRIDE + kk; + const uint4 v0 = *(const uint4 *)p, v1 = *(const uint4 *)(p + 8); a[i] = __builtin_bit_cast(v16s, (uint4[2]){v0, v1}); } +#pragma unroll + for (int j = 0; j < TN; ++j) { if constexpr (TAIL) { if (!jact[j]) continue; } const uint16_t * p = Bs + (wn * WTN + j * 16 + r) * MMB_LDS_STRIDE + kk; + const uint4 v0 = *(const uint4 *)p, v1 = *(const uint4 *)(p + 8); b[j] = __builtin_bit_cast(v16s, (uint4[2]){v0, v1}); } +#pragma unroll + for (int i = 0; i < TM; ++i) +#pragma unroll + for (int j = 0; j < TN; ++j) { if constexpr (TAIL) { if (!jact[j]) continue; } acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_bf16_w32(b[j], a[i], acc[i][j]); } + } + __syncthreads(); + if (ks + 1 < nks) store_lds(); + __syncthreads(); + } + // epilogue through LDS (per-wave 1 KB stage in the now-free A/B tile area); tiles are 16 rows, a_rows is a + // multiple of 32 in this model, so whole tiles are either valid or beyond a_rows + float * stg = (float *)((BN * MMB_LDS_STRIDE * 2 >= MMB_NT / 32 * 1024) ? Bs : As) + wave * 256; +#pragma unroll + for (int i = 0; i < TM; ++i) { + const int ml = wm * WTM + i * 16; const bool ok = ml < a_rows; +#pragma unroll + for (int j = 0; j < TN; ++j) { + if (ok && jact[j]) mmb_store_tile(acc[i][j], stg, D, Dh, store_f32, M, drow, wn * WTN + j * 16, m0 + ml, lane); + else { __syncthreads(); __syncthreads(); } + } + } +} + +template <int BM, int BN, int WTM, int WTN, int WTYPE> +__global__ void __launch_bounds__(MMB_NT, 2) +mmb_dense_kernel(const uint8_t * __restrict__ W, const uint16_t * __restrict__ Xh, float * __restrict__ D, uint16_t * __restrict__ Dh, const bool store_f32, const int M, const int K, const int T) { +#if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) + NO_DEVICE_CODE; // WMMA kernels are RDNA3-only; the host gate keeps other devices off this path +#else + __shared__ __align__(16) uint16_t As[BM * MMB_LDS_STRIDE]; + __shared__ __align__(16) uint16_t Bs[BN * MMB_LDS_STRIDE]; + const int m0 = blockIdx.x * BM, t0 = blockIdx.y * BN; + const size_t wrow_bytes = mmb_row_bytes<WTYPE>(K); + mmb_tile_gemm<BM, BN, WTM, WTN, WTYPE, false>(W + (size_t)m0 * wrow_bytes, wrow_bytes, M - m0, Xh, K, + [&](int i) { return (t0 + i < T) ? t0 + i : -1; }, D, Dh, store_f32, M, [&](int i) { return (t0 + i < T) ? t0 + i : -1; }, m0, T - t0, As, Bs); +#endif +} + +#if defined(__HIP_PLATFORM_AMD__) +__device__ __forceinline__ float gm_mul_rn(const float a, const float b) { float r; asm("v_mul_f32_e32 %0, %1, %2" : "=v"(r) : "v"(a), "v"(b)); return r; } +__device__ __forceinline__ float gm_add_rn(const float a, const float b) { float r; asm("v_add_f32_e32 %0, %1, %2" : "=v"(r) : "v"(a), "v"(b)); return r; } +#else +__device__ __forceinline__ float gm_mul_rn(const float a, const float b) { return __fmul_rn(a, b); } +__device__ __forceinline__ float gm_add_rn(const float a, const float b) { return __fadd_rn(a, b); } +#endif +__device__ __forceinline__ float gm_sigmoid(const float x) { return 1.0f / (1.0f + expf(-x)); } +__device__ __forceinline__ float gm_bf2f(const uint16_t h) { return __uint_as_float(((uint32_t) h) << 16); } + +template <int HC, int WTYPE = 0> +__global__ void __launch_bounds__(MMB_NT, 2) +hc_gate_mix_kernel(const uint8_t * __restrict__ W, const uint16_t * __restrict__ Lo, const uint16_t * __restrict__ Xn, float * __restrict__ Out, + uint16_t * __restrict__ OutH, const bool store_f32, + const int E, const int K, const int T, const float scale, const float bias) { +#if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) + // WMMA (wave32, 16x16x16 bf16/f16 into f32) exists only on RDNA3; the host gate keeps other devices off this path + NO_DEVICE_CODE; +#else + constexpr int CH = 32, BN = 128, BM = HC * CH; + static_assert(BM <= MMB_NT, "one A row per thread"); + __shared__ __align__(16) uint16_t As[BM * MMB_LDS_STRIDE]; + __shared__ __align__(16) uint16_t Bs[BN * MMB_LDS_STRIDE]; + const int tid = threadIdx.x, lane = tid & 31, wave = tid >> 5; + const int wm = wave & 1, wn = wave >> 1; // wave: 16 channels (all HC streams) x 32 tokens + const int e0 = blockIdx.x * CH, t0 = blockIdx.y * BN; + const size_t wrow_bytes = mmb_row_bytes<WTYPE>(K); + constexpr int B_ITEMS = (BN * 8) / MMB_NT; + uint4 a0 = make_uint4(0,0,0,0), a1 = make_uint4(0,0,0,0); uint32_t a2 = 0; uint4 bst[B_ITEMS]; int brow[B_ITEMS]; + const uint8_t * arow = W; + if (tid < BM) { const int c = tid / CH, i = tid - c * CH; arow = W + (size_t)(c * E + e0 + i) * wrow_bytes; } +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; const int t = t0 + (c >> 3); brow[i] = t < T ? t : -1; } + int weight_ks = 0; + auto load_regs = [&](const int ks) { + weight_ks = ks; + if constexpr (WTYPE == 0) if (tid < BM) { const uint8_t * p = arow + (size_t)ks * 36; a0 = *(const uint4 *)(p); a1 = *(const uint4 *)(p + 16); a2 = *(const uint32_t *)(p + 32); } +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; const int off = (c & 7) * 8; + bst[i] = (brow[i] >= 0) ? *(const uint4 *)(Lo + (size_t)brow[i] * K + ks * MMB_BK + off) : make_uint4(0,0,0,0); } + }; + auto store_lds = [&]() { + if constexpr (WTYPE == 0) { if (tid < BM) mmb_dq_row36(a0, a1, a2, (uint32_t *)(As + tid * MMB_LDS_STRIDE)); } + else { + constexpr int TYPE = WTYPE == 1 ? GGML_TYPE_Q8_0 : WTYPE - 32; + for (int row = tid / 8; row < BM; row += MMB_NT / 8) { + const int c = row / CH, ch = row % CH; + mmb_decode_slice<(ggml_type)TYPE>(W + (size_t)(c * E + e0 + ch) * wrow_bytes, weight_ks * 64, As + row * MMB_LDS_STRIDE, tid % 8); + } + } +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; *(uint4 *)(Bs + (c >> 3) * MMB_LDS_STRIDE + (c & 7) * 8) = bst[i]; } + }; + v8f acc[HC][2]; +#pragma unroll + for (int c = 0; c < HC; ++c) +#pragma unroll + for (int j = 0; j < 2; ++j) +#pragma unroll + for (int e = 0; e < 8; ++e) acc[c][j][e] = 0.f; + const int nks = K / MMB_BK; + load_regs(0); store_lds(); __syncthreads(); + for (int ks = 0; ks < nks; ++ks) { + if (ks + 1 < nks) load_regs(ks + 1); +#pragma unroll + for (int kk = 0; kk < MMB_BK; kk += 16) { + v16s a[HC], b[2]; const int r = lane & 15; +#pragma unroll + for (int c = 0; c < HC; ++c) { const uint16_t * p = As + (c * CH + wm * 16 + r) * MMB_LDS_STRIDE + kk; + const uint4 v0 = *(const uint4 *)p, v1 = *(const uint4 *)(p + 8); a[c] = __builtin_bit_cast(v16s, (uint4[2]){v0, v1}); } +#pragma unroll + for (int j = 0; j < 2; ++j) { const uint16_t * p = Bs + (wn * 32 + j * 16 + r) * MMB_LDS_STRIDE + kk; + const uint4 v0 = *(const uint4 *)p, v1 = *(const uint4 *)(p + 8); b[j] = __builtin_bit_cast(v16s, (uint4[2]){v0, v1}); } +#pragma unroll + for (int c = 0; c < HC; ++c) +#pragma unroll + for (int j = 0; j < 2; ++j) acc[c][j] = __builtin_amdgcn_wmma_f32_16x16x16_bf16_w32(b[j], a[c], acc[c][j]); + } + __syncthreads(); + if (ks + 1 < nks) store_lds(); + __syncthreads(); + } + // epilogue: lane holds channel (lane & 15) of the wave's 16 and tokens 2e + (lane >> 4) of each 16-token fragment + const int cm = lane & 15, cn = lane >> 4; const int ch = e0 + wm * 16 + cm; +#pragma unroll + for (int j = 0; j < 2; ++j) { +#pragma unroll + for (int e = 0; e < 8; ++e) { + const int t = t0 + wn * 32 + j * 16 + 2 * e + cn; + if (t >= T) continue; + const uint16_t * xr = Xn + (size_t)t * ((size_t)HC * E) + ch; + float s = 0.f; +#pragma unroll + for (int c = 0; c < HC; ++c) { + const float g = __uint_as_float(((uint32_t) mmb_f2bf(acc[c][j][e])) << 16); // the gate GEMM's BF16 epilogue rounding + const float term = gm_mul_rn(gm_bf2f(xr[(size_t)c * E]), gm_sigmoid(g)); + s = (c == 0) ? term : gm_add_rn(s, term); + } + const float o = scale * s + bias; + if (store_f32) Out[(size_t)t * E + ch] = o; + if (OutH) OutH[(size_t)t * E + ch] = mmb_f2bf(o); + } + } +#endif +} + +template <int BM, int BN, int WTM, int WTN, int WTYPE = 0> +__global__ void __launch_bounds__(MMB_NT, 2) +mmb_routed_kernel(const uint8_t * __restrict__ W, const size_t expert_bytes, const uint16_t * __restrict__ Xh, float * __restrict__ D, + uint16_t * __restrict__ Dh, const bool store_f32, + const int32_t * __restrict__ ids_src, const int32_t * __restrict__ ids_dst, const int32_t * __restrict__ bounds, + const uint32_t * __restrict__ desc, const int M, const int K) { +#if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) + NO_DEVICE_CODE; // WMMA kernels are RDNA3-only; the host gate keeps other devices off this path +#else + __shared__ __align__(16) uint16_t As[BM * MMB_LDS_STRIDE]; + __shared__ __align__(16) uint16_t Bs[BN * MMB_LDS_STRIDE]; + const uint32_t dsc = desc[blockIdx.y]; + if (dsc == UINT32_MAX) return; // uniform across the block, before any barrier + const int e = dsc & 0xffff, jt = dsc >> 16; + const int r0 = bounds[e] + jt * BN, cnt = bounds[e + 1] - r0; + const int m0 = blockIdx.x * BM; + const size_t wrow_bytes = mmb_row_bytes<WTYPE>(K); + mmb_tile_gemm<BM, BN, WTM, WTN, WTYPE, true>(W + (size_t)e * expert_bytes + (size_t)m0 * wrow_bytes, wrow_bytes, M - m0, Xh, K, + [&](int i) { return (i < cnt) ? ids_src[r0 + i] : -1; }, D, Dh, store_f32, M, [&](int i) { return (i < cnt) ? ids_dst[r0 + i] : -1; }, m0, cnt, As, Bs); +#endif +} + +template <int BM, int BN, int WTM, int WTN, int WTYPE, bool TAIL, typename XRowFn, typename DRowFn> +__device__ __forceinline__ void mmb_tile_gemm_glu(const uint8_t * __restrict__ Wg, const uint8_t * __restrict__ Wu, const size_t wrow_bytes, const int a_rows, + const uint16_t * __restrict__ Xh, const int K, XRowFn xrow, float * __restrict__ D, uint16_t * __restrict__ Dh, const bool store_f32, const int M, DRowFn drow, const int m0, + const int n_cols, uint16_t * Ag, uint16_t * Au, uint16_t * Bs) { + constexpr int WAVES_M = BM / WTM, TM = WTM / 16, TN = WTN / 16; + constexpr int A_ITEMS = (BM + MMB_NT - 1) / MMB_NT; + constexpr int B_ITEMS = (BN * 8) / MMB_NT; + const int tid = threadIdx.x, lane = tid & 31, wave = tid >> 5; + const int wm = wave % WAVES_M, wn = wave / WAVES_M; + uint4 g0[A_ITEMS], g1[A_ITEMS], u0[A_ITEMS], u1[A_ITEMS]; uint32_t g2[A_ITEMS], u2[A_ITEMS]; + uint4 gm[A_ITEMS], um[A_ITEMS]; + uint4 bst[B_ITEMS]; + int brow[B_ITEMS]; +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; brow[i] = xrow(c >> 3); } + int weight_ks = 0; + auto load_regs = [&](const int ks) { + weight_ks = ks; +#pragma unroll + for (int i = 0; i < A_ITEMS; ++i) { + const int row = tid + i * MMB_NT; + if constexpr (WTYPE == 0) { + if (row < BM && row < a_rows) { + const uint8_t * pg = Wg + (size_t)row * wrow_bytes + (size_t)ks * 36; + const uint8_t * pu = Wu + (size_t)row * wrow_bytes + (size_t)ks * 36; + g0[i] = *(const uint4 *)(pg); g1[i] = *(const uint4 *)(pg + 16); g2[i] = *(const uint32_t *)(pg + 32); + u0[i] = *(const uint4 *)(pu); u1[i] = *(const uint4 *)(pu + 16); u2[i] = *(const uint32_t *)(pu + 32); + } else { g0[i] = g1[i] = u0[i] = u1[i] = make_uint4(0,0,0,0); g2[i] = u2[i] = 0; } + } else if constexpr (WTYPE == 32 + GGML_TYPE_Q4_K) { + if (row < BM && row < a_rows) { + const uint8_t * pg = Wg + (size_t)row * wrow_bytes + (size_t)(ks / 4) * sizeof(block_q4_K); + const uint8_t * pu = Wu + (size_t)row * wrow_bytes + (size_t)(ks / 4) * sizeof(block_q4_K); + gm[i] = *(const uint4 *)pg; um[i] = *(const uint4 *)pu; + const int offset = 16 + (ks & 3) * 32; + g0[i] = *(const uint4 *)(pg + offset); g1[i] = *(const uint4 *)(pg + offset + 16); + u0[i] = *(const uint4 *)(pu + offset); u1[i] = *(const uint4 *)(pu + offset + 16); + } else { gm[i] = um[i] = g0[i] = g1[i] = u0[i] = u1[i] = make_uint4(0,0,0,0); } + } + } +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { + const int c = tid + i * MMB_NT; const int off = (c & 7) * 8; + bst[i] = (brow[i] >= 0) ? *(const uint4 *)(Xh + (size_t)brow[i] * K + ks * MMB_BK + off) : make_uint4(0,0,0,0); + } + }; + auto store_lds = [&]() { + if constexpr (WTYPE != 0 && WTYPE != 32 + GGML_TYPE_Q4_K) { + constexpr int LOAD_TYPE = WTYPE == 1 ? 32 + GGML_TYPE_Q8_0 : WTYPE; + mmb_load_quant_tile<LOAD_TYPE, BM, MMB_LDS_STRIDE>(Wg, wrow_bytes, a_rows, weight_ks, Ag); + mmb_load_quant_tile<LOAD_TYPE, BM, MMB_LDS_STRIDE>(Wu, wrow_bytes, a_rows, weight_ks, Au); + } + +#pragma unroll + for (int i = 0; i < A_ITEMS; ++i) { + const int row = tid + i * MMB_NT; + if (row < BM) { + if constexpr (WTYPE == 0) { + mmb_dq_row36(g0[i], g1[i], g2[i], (uint32_t *)(Ag + row * MMB_LDS_STRIDE)); + mmb_dq_row36(u0[i], u1[i], u2[i], (uint32_t *)(Au + row * MMB_LDS_STRIDE)); + } else if constexpr (WTYPE == 32 + GGML_TYPE_Q4_K) { + mmb_dq_q4k_slice(g0[i], g1[i], gm[i], weight_ks & 3, (uint32_t *)(Ag + row * MMB_LDS_STRIDE)); + mmb_dq_q4k_slice(u0[i], u1[i], um[i], weight_ks & 3, (uint32_t *)(Au + row * MMB_LDS_STRIDE)); + } + } + } +#pragma unroll + for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; *(uint4 *)(Bs + (c >> 3) * MMB_LDS_STRIDE + (c & 7) * 8) = bst[i]; } + }; + v8f accg[TM][TN], accu[TM][TN]; +#pragma unroll + for (int i = 0; i < TM; ++i) +#pragma unroll + for (int j = 0; j < TN; ++j) +#pragma unroll + for (int e = 0; e < 8; ++e) { accg[i][j][e] = 0.f; accu[i][j][e] = 0.f; } + bool jact[TN]; +#pragma unroll + for (int j = 0; j < TN; ++j) jact[j] = !TAIL || (wn * WTN + j * 16) < n_cols; + const int nks = K / MMB_BK; + load_regs(0); store_lds(); __syncthreads(); + for (int ks = 0; ks < nks; ++ks) { + if (ks + 1 < nks) load_regs(ks + 1); +#pragma unroll + for (int kk = 0; kk < MMB_BK; kk += 16) { + v16s ag[TM], au[TM], b[TN]; const int r = lane & 15; +#pragma unroll + for (int i = 0; i < TM; ++i) { const int off = (wm * WTM + i * 16 + r) * MMB_LDS_STRIDE + kk; + ag[i] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Ag + off), *(const uint4 *)(Ag + off + 8)}); + au[i] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Au + off), *(const uint4 *)(Au + off + 8)}); } +#pragma unroll + for (int j = 0; j < TN; ++j) { if constexpr (TAIL) { if (!jact[j]) continue; } const uint16_t * p = Bs + (wn * WTN + j * 16 + r) * MMB_LDS_STRIDE + kk; + b[j] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)p, *(const uint4 *)(p + 8)}); } +#pragma unroll + for (int i = 0; i < TM; ++i) +#pragma unroll + for (int j = 0; j < TN; ++j) { + if constexpr (TAIL) { if (!jact[j]) continue; } + accg[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_bf16_w32(b[j], ag[i], accg[i][j]); + accu[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_bf16_w32(b[j], au[i], accu[i][j]); + } + } + __syncthreads(); + if (ks + 1 < nks) store_lds(); + __syncthreads(); + } + float * stg = (float *)((BN * MMB_LDS_STRIDE * 2 >= MMB_NT / 32 * 1024) ? Bs : Ag) + wave * 256; +#pragma unroll + for (int i = 0; i < TM; ++i) { + const int ml = wm * WTM + i * 16; const bool ok = ml < a_rows; +#pragma unroll + for (int j = 0; j < TN; ++j) { + if (ok && jact[j]) { + v8f v; +#pragma unroll + for (int e = 0; e < 8; ++e) { v[e] = ggml_cuda_op_silu_single(accg[i][j][e]) * accu[i][j][e]; } + mmb_store_tile(v, stg, D, Dh, store_f32, M, drow, wn * WTN + j * 16, m0 + ml, lane); + } else { __syncthreads(); __syncthreads(); } + } + } +} + +template <int BM, int BN, int WTM, int WTN, int WTYPE = 0> +__global__ void __launch_bounds__(MMB_NT, 2) +mmb_routed_glu_kernel(const uint8_t * __restrict__ Wg, const uint8_t * __restrict__ Wu, const size_t expert_bytes, const uint16_t * __restrict__ Xh, + float * __restrict__ D, uint16_t * __restrict__ Dh, const bool store_f32, + const int32_t * __restrict__ ids_src, const int32_t * __restrict__ ids_dst, const int32_t * __restrict__ bounds, + const uint32_t * __restrict__ desc, const int M, const int K) { +#if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) + NO_DEVICE_CODE; // WMMA kernels are RDNA3-only; the host gate keeps other devices off this path +#else + __shared__ __align__(16) uint16_t Ag[BM * MMB_LDS_STRIDE]; + __shared__ __align__(16) uint16_t Au[BM * MMB_LDS_STRIDE]; + __shared__ __align__(16) uint16_t Bs[BN * MMB_LDS_STRIDE]; + const uint32_t dsc = desc[blockIdx.y]; + if (dsc == UINT32_MAX) return; + const int e = dsc & 0xffff, jt = dsc >> 16; + const int r0 = bounds[e] + jt * BN, cnt = bounds[e + 1] - r0; + const int m0 = blockIdx.x * BM; + const size_t wrow_bytes = mmb_row_bytes<WTYPE>(K); + mmb_tile_gemm_glu<BM, BN, WTM, WTN, WTYPE, true>(Wg + (size_t)e * expert_bytes + (size_t)m0 * wrow_bytes, Wu + (size_t)e * expert_bytes + (size_t)m0 * wrow_bytes, wrow_bytes, M - m0, Xh, K, + [&](int i) { return (i < cnt) ? ids_src[r0 + i] : -1; }, D, Dh, store_f32, M, [&](int i) { return (i < cnt) ? ids_dst[r0 + i] : -1; }, m0, cnt, Ag, Au, Bs); +#endif +} + +// F32 x F32 -> F32 GEMM with F32-equivalent precision on WMMA: each operand is split into F16 hi + F16 lo at tile load and +// the product is accumulated as hi*hi + hi*lo + lo*hi in F32 (lo*lo ~2^-22 relative, dropped). Used for the F32 MoE router. +__device__ __forceinline__ void mmb_split2(float x, uint16_t & hi, uint16_t & lo) { + hi = __builtin_bit_cast(uint16_t, (_Float16) x); lo = __builtin_bit_cast(uint16_t, (_Float16) (x - (float) __builtin_bit_cast(_Float16, hi))); +} +template <int BM, int BN, int WTM, int WTN, bool TWO> +__global__ void __launch_bounds__(MMB_NT, 2) +mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, float * __restrict__ D, const int M, const int K, const int T) { +#if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) + // WMMA (wave32, 16x16x16 bf16/f16 into f32) exists only on RDNA3; the host gate keeps other devices off this path + NO_DEVICE_CODE; +#else + constexpr int BKs = 32, LS = BKs + 8, WAVES_M = BM / WTM, TM = WTM / 16, TN = WTN / 16; + __shared__ __align__(16) uint16_t Ah[BM * LS], Al[BM * LS], Bh[BN * LS], Bl[BN * LS]; + const int tid = threadIdx.x, lane = tid & 31, wave = tid >> 5, wm = wave % WAVES_M, wn = wave / WAVES_M; + const int m0 = blockIdx.x * BM, t0 = blockIdx.y * BN; + v8f acc[TM][TN]; +#pragma unroll + for (int i = 0; i < TM; ++i) +#pragma unroll + for (int j = 0; j < TN; ++j) +#pragma unroll + for (int e = 0; e < 8; ++e) acc[i][j][e] = 0.f; + constexpr int A_CH = BM * BKs / 4, B_CH = BN * BKs / 4; + for (int k0 = 0; k0 < K; k0 += BKs) { + for (int idx = tid; idx < A_CH; idx += MMB_NT) { const int row = idx >> 3, c4 = (idx & 7) * 4; const int m = m0 + row; + float4 v = make_float4(0.f,0.f,0.f,0.f); if (m < M) v = *(const float4 *)(W + (size_t) m * K + k0 + c4); + uint16_t h[4], l[4]; mmb_split2(v.x,h[0],l[0]); mmb_split2(v.y,h[1],l[1]); mmb_split2(v.z,h[2],l[2]); mmb_split2(v.w,h[3],l[3]); + *(uint2 *)(Ah + row * LS + c4) = make_uint2((uint32_t)h[0] | ((uint32_t)h[1] << 16), (uint32_t)h[2] | ((uint32_t)h[3] << 16)); + *(uint2 *)(Al + row * LS + c4) = make_uint2((uint32_t)l[0] | ((uint32_t)l[1] << 16), (uint32_t)l[2] | ((uint32_t)l[3] << 16)); } + for (int idx = tid; idx < B_CH; idx += MMB_NT) { const int row = idx >> 3, c4 = (idx & 7) * 4; const int t = t0 + row; + float4 v = make_float4(0.f,0.f,0.f,0.f); if (t < T) v = *(const float4 *)(X + (size_t) t * K + k0 + c4); + uint16_t h[4], l[4]; mmb_split2(v.x,h[0],l[0]); mmb_split2(v.y,h[1],l[1]); mmb_split2(v.z,h[2],l[2]); mmb_split2(v.w,h[3],l[3]); + *(uint2 *)(Bh + row * LS + c4) = make_uint2((uint32_t)h[0] | ((uint32_t)h[1] << 16), (uint32_t)h[2] | ((uint32_t)h[3] << 16)); + *(uint2 *)(Bl + row * LS + c4) = make_uint2((uint32_t)l[0] | ((uint32_t)l[1] << 16), (uint32_t)l[2] | ((uint32_t)l[3] << 16)); } + __syncthreads(); + const int r = lane & 15; +#pragma unroll + for (int kk = 0; kk < BKs; kk += 16) { + v16s ah[TM], al[TM], bh[TN], bl[TN]; +#pragma unroll + for (int i = 0; i < TM; ++i) { const int off = (wm * WTM + i * 16 + r) * LS + kk; + ah[i] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Ah + off), *(const uint4 *)(Ah + off + 8)}); + al[i] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Al + off), *(const uint4 *)(Al + off + 8)}); } +#pragma unroll + for (int j = 0; j < TN; ++j) { const int off = (wn * WTN + j * 16 + r) * LS + kk; + bh[j] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Bh + off), *(const uint4 *)(Bh + off + 8)}); + bl[j] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Bl + off), *(const uint4 *)(Bl + off + 8)}); } +#pragma unroll + for (int i = 0; i < TM; ++i) +#pragma unroll + for (int j = 0; j < TN; ++j) { + acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(bh[j], ah[i], acc[i][j]); + acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(bl[j], ah[i], acc[i][j]); + if constexpr (!TWO) acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(bh[j], al[i], acc[i][j]); + } + } + __syncthreads(); + } + const int cm = lane & 15, cn = lane >> 4; +#pragma unroll + for (int i = 0; i < TM; ++i) { const int m = m0 + wm * WTM + i * 16 + cm; if (m >= M) continue; +#pragma unroll + for (int j = 0; j < TN; ++j) +#pragma unroll + for (int e = 0; e < 8; ++e) { const int t = t0 + wn * WTN + j * 16 + 2 * e + cn; if (t < T) D[(size_t) t * M + m] = acc[i][j][e]; } } +#endif +} + +// two tile classes: experts with >= thresh rows get BN_BIG-row tiles, the rest BN_SMALL-row tiles (fewer wasted rows on tiny experts) +__global__ void mmb_build_desc2(const int32_t * __restrict__ bounds, uint32_t * __restrict__ desc_big, uint32_t * __restrict__ desc_small, + const int E, const int nbig_max, const int nsmall_max, const int BN_BIG, const int BN_SMALL, const int thresh) { + __shared__ int sb[1024], ss[1024]; + const int e = threadIdx.x; + for (int i = e; i < nbig_max; i += blockDim.x) desc_big[i] = UINT32_MAX; + for (int i = e; i < nsmall_max; i += blockDim.x) desc_small[i] = UINT32_MAX; + int cnt = (e < E) ? bounds[e + 1] - bounds[e] : 0; + const bool big = cnt >= thresh; + const int tb = big ? (cnt + BN_BIG - 1) / BN_BIG : 0; + const int ts = big ? 0 : (cnt + BN_SMALL - 1) / BN_SMALL; + sb[e] = tb; ss[e] = ts; + __syncthreads(); + for (int off = 1; off < 1024; off <<= 1) { + const int vb = (e >= off) ? sb[e - off] : 0, vs = (e >= off) ? ss[e - off] : 0; + __syncthreads(); + sb[e] += vb; ss[e] += vs; + __syncthreads(); + } + const int bb = sb[e] - tb, bs = ss[e] - ts; + for (int jt = 0; jt < tb; ++jt) { const int idx = bb + jt; if (idx < nbig_max) desc_big[idx] = (uint32_t)e | ((uint32_t)jt << 16); } + for (int jt = 0; jt < ts; ++jt) { const int idx = bs + jt; if (idx < nsmall_max) desc_small[idx] = (uint32_t)e | ((uint32_t)jt << 16); } +} + +struct mmb_cache_entry { const ggml_tensor * root; const void * data; size_t n; ggml_cuda_pool_alloc<uint16_t> * buf; }; +static std::vector<mmb_cache_entry> g_mmb_cache; +static mmb_cache_entry g_mmb_slots[4] = {{nullptr,nullptr,0,nullptr},{nullptr,nullptr,0,nullptr},{nullptr,nullptr,0,nullptr},{nullptr,nullptr,0,nullptr}}; +static std::unordered_set<const ggml_tensor *> g_mmb_bf16_only; + +static size_t mmb_cache_max() { return 4; } +static const ggml_tensor * mmb_root(const ggml_tensor * t) { return t->view_src ? t->view_src : t; } +static uint16_t * mmb_cache_insert(ggml_backend_cuda_context & ctx, const ggml_tensor * t, const size_t n) { + if (g_mmb_cache.size() >= mmb_cache_max()) { delete g_mmb_cache.front().buf; g_mmb_cache.erase(g_mmb_cache.begin()); } + auto * buf = new ggml_cuda_pool_alloc<uint16_t>(ctx.pool(), n); + g_mmb_cache.push_back({mmb_root(t), t->data, n, buf}); + return buf->get(); +} +static const uint16_t * mmb_bf16_activation(ggml_backend_cuda_context & ctx, const ggml_tensor * src1, const size_t n, cudaStream_t stream) { + const ggml_tensor * root = mmb_root(src1); + for (auto & e : g_mmb_slots) if (e.buf && e.root == root && e.data == src1->data && e.n == n) return e.buf->get(); + for (auto & e : g_mmb_cache) if (e.root == root && e.data == src1->data && e.n == n) return e.buf->get(); + uint16_t * buf = mmb_cache_insert(ctx, src1, n); + mmb_cvt_f32_bf16<<<(unsigned)((n / 8 + 255) / 256), 256, 0, stream>>>((const float *) src1->data, buf, n); + return buf; +} + +// Shadow BF16 copies of IQ4_NL dense weights: dequantised once (same LUT*scale -> BF16 RNE as mmb_dq_row36, so the +// WMMA inputs are bitwise identical) so the dense GEMM runs the dequant-free WTYPE=2 path. +__global__ void mmb_dq_q6k_bf16_kernel(const uint8_t * __restrict__ W, uint16_t * __restrict__ out, const size_t nblocks) { + const size_t b = (size_t) blockIdx.x * blockDim.x + threadIdx.x; + if (b >= nblocks) return; + const uint8_t * p = W + b * 210; + const uint8_t * ql = p, * qh = p + 128; + const int8_t * sc = (const int8_t *) (p + 192); + const float d = mmb_h2f(*(const uint16_t *) (p + 208)); + uint16_t * o = out + b * 256; + for (int n = 0; n < 2; ++n) { + const uint8_t * QL = ql + 64 * n; const uint8_t * QH = qh + 32 * n; const int8_t * S = sc + 8 * n; uint16_t * Y = o + 128 * n; + for (int l = 0; l < 32; ++l) { + const int is = l / 16; + const int8_t q1 = (int8_t)((QL[l + 0] & 0xF) | (((QH[l] >> 0) & 3) << 4)) - 32; + const int8_t q2 = (int8_t)((QL[l + 32] & 0xF) | (((QH[l] >> 2) & 3) << 4)) - 32; + const int8_t q3 = (int8_t)((QL[l + 0] >> 4) | (((QH[l] >> 4) & 3) << 4)) - 32; + const int8_t q4 = (int8_t)((QL[l + 32] >> 4) | (((QH[l] >> 6) & 3) << 4)) - 32; + Y[l + 0] = mmb_f2bf(d * S[is + 0] * q1); + Y[l + 32] = mmb_f2bf(d * S[is + 2] * q2); + Y[l + 64] = mmb_f2bf(d * S[is + 4] * q3); + Y[l + 96] = mmb_f2bf(d * S[is + 6] * q4); + } + } +} + +__global__ void mmb_dq_iq4nl_bf16_kernel(const uint8_t * __restrict__ W, uint16_t * __restrict__ out, const size_t nblocks) { + const size_t b = (size_t) blockIdx.x * blockDim.x + threadIdx.x; + if (b >= nblocks) return; + const uint8_t * p = W + b * 18; + const float d = mmb_h2f(*(const uint16_t *) p); + const uint32_t * q = (const uint32_t *) (p + 2); + uint32_t * o = (uint32_t *) (out + b * 32); +#pragma unroll + for (int w = 0; w < 4; ++w) { + const uint32_t v = q[w]; + const float l0 = d * mmb_kv_iq4nl[(v ) & 0xF], h0 = d * mmb_kv_iq4nl[(v >> 4) & 0xF]; + const float l1 = d * mmb_kv_iq4nl[(v >> 8) & 0xF], h1 = d * mmb_kv_iq4nl[(v >> 12) & 0xF]; + const float l2 = d * mmb_kv_iq4nl[(v >> 16) & 0xF], h2 = d * mmb_kv_iq4nl[(v >> 20) & 0xF]; + const float l3 = d * mmb_kv_iq4nl[(v >> 24) & 0xF], h3 = d * mmb_kv_iq4nl[(v >> 28) & 0xF]; + o[2*w] = mmb_pack2(l0, l1); o[2*w + 1] = mmb_pack2(l2, l3); o[8 + 2*w] = mmb_pack2(h0, h1); o[8 + 2*w + 1] = mmb_pack2(h2, h3); + } +} +static std::unordered_map<const void *, uint16_t *> g_mmb_shadow; +static std::map<std::pair<const void *, const void *>, uint16_t *> g_mmb_shadow_pair; // concat(w0, w1) along rows -> BF16 copy +static size_t g_mmb_shadow_bytes = 0; +int mmb_shadow_mode(){ return 2; } +bool mmb_shadow() { return mmb_shadow_mode() != 0; } +bool mmb_shadow_q6k(){ return mmb_shadow_mode() >= 1; } +size_t mmb_shadow_cap(){ return (size_t) 6144 << 20; } +static bool mmb_is_resident_q6k(const ggml_tensor * w) { return w && w->type == GGML_TYPE_Q6_K && w->op == GGML_OP_NONE && w->data && w->buffer && w->ne[2] == 1 && w->ne[3] == 1 && ggml_is_contiguous(w) && w->ne[0] % 256 == 0 && w->ne[1] <= 32768; } +static bool mmb_is_resident_iq4(const ggml_tensor * w) { return w && w->type == GGML_TYPE_IQ4_NL && w->op == GGML_OP_NONE && w->data && w->buffer && w->ne[2] == 1 && w->ne[3] == 1 && ggml_is_contiguous(w); } +static bool mmb_is_row_concat(const ggml_tensor * w) { + return w && w->op == GGML_OP_CONCAT && w->type == GGML_TYPE_IQ4_NL && ggml_get_op_params_i32(w, 0) == 1 && mmb_is_resident_iq4(w->src[0]) && mmb_is_resident_iq4(w->src[1]) && + w->src[0]->ne[0] == w->src[1]->ne[0] && w->ne[0] == w->src[0]->ne[0] && w->ne[1] == w->src[0]->ne[1] + w->src[1]->ne[1]; +} +static const uint16_t * mmb_shadow_lookup(const ggml_tensor * w) { + if (w->op == GGML_OP_CONCAT) { auto it = g_mmb_shadow_pair.find({w->src[0]->data, w->src[1]->data}); return it == g_mmb_shadow_pair.end() ? nullptr : it->second; } + auto it = g_mmb_shadow.find(w->data); return it == g_mmb_shadow.end() ? nullptr : it->second; +} + +// the BF16 WMMA kernels are RDNA3.5 (gfx1151) work: other devices keep the MMQ/MMVQ paths +bool mmb_enabled() { + const int id = ggml_cuda_get_device(); + return GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[id].cc); +} +int mmb_min_t() { return 512; } +int mmb_f32split_mode(){ return 2; } +bool mmb_f32split() { return true; } +bool mmb_bf16w() { return true; } +bool mmb_hc16() { return true; } +int mmb_tall_mode(){ return 2; } +bool mmb_tall() { return mmb_tall_mode() != 0; } +bool mmb_gatemix_flag() { return true; } +bool mmb_down16_flag() { return true; } +bool mmb_glu() { return true; } + +} // namespace + +const uint16_t * ggml_cuda_mmb_cache_lookup(const ggml_tensor * t) { + const ggml_tensor * root = mmb_root(t); + for (auto & e : g_mmb_slots) if (e.buf && e.root == root && e.data == t->data) return e.buf->get(); + for (auto & e : g_mmb_cache) if (e.root == root && e.data == t->data) return e.buf->get(); + return nullptr; +} +static size_t g_mmb_slot_cap[4] = {0, 0, 0}; +uint16_t * ggml_cuda_mmb_slot_reserve(ggml_backend_cuda_context & ctx, int slot, const ggml_tensor * t, size_t n) { + mmb_cache_entry & e = g_mmb_slots[slot]; + if (e.buf && g_mmb_slot_cap[slot] < n) { delete e.buf; e.buf = nullptr; } + if (!e.buf) { e.buf = new ggml_cuda_pool_alloc<uint16_t>(ctx.pool(), n); g_mmb_slot_cap[slot] = n; } + e.root = mmb_root(t); e.data = t->data; e.n = n; + return e.buf->get(); +} +void ggml_cuda_mmb_marks_clear() { g_mmb_bf16_only.clear(); } +size_t ggml_cuda_mmb_marks_count() { return g_mmb_bf16_only.size(); } +void ggml_cuda_mmb_mark_bf16_only(const ggml_tensor * t) { g_mmb_bf16_only.insert(t); } +bool ggml_cuda_mmb_is_bf16_only(const ggml_tensor * t) { return g_mmb_bf16_only.count(t) > 0; } +void ggml_cuda_mmb_begin_graph() { for (auto & e : g_mmb_cache) delete e.buf; g_mmb_cache.clear(); for (auto & e : g_mmb_slots) { e.root = nullptr; e.data = nullptr; e.n = 0; } } +void ggml_cuda_mmb_release_all() { + ggml_cuda_mmb_begin_graph(); + for (int i = 0; i < 4; ++i) { if (g_mmb_slots[i].buf) delete g_mmb_slots[i].buf; g_mmb_slots[i].buf = nullptr; g_mmb_slot_cap[i] = 0; } + // the shadow weights are raw cudaMalloc, keyed by data pointer and held for the life of the + // process. A model has finitely many weights so this never mattered, but a long-lived process + // that sees many distinct tensors (test-backend-ops) keeps every one of them. + for (auto & e : g_mmb_shadow) { if (e.second) CUDA_CHECK(cudaFree(e.second)); } + for (auto & e : g_mmb_shadow_pair) { if (e.second) CUDA_CHECK(cudaFree(e.second)); } + g_mmb_shadow.clear(); + g_mmb_shadow_pair.clear(); + g_mmb_shadow_bytes = 0; +} +uint16_t * ggml_cuda_mmb_cache_reserve(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n) { + if (!mmb_enabled() || ggml_nrows(t) < mmb_min_t()) return nullptr; + return ggml_cuda_mmb_slot_reserve(ctx, 0, t, n); +} + +bool ggml_cuda_mmb_supported_mm(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst) { + if (!mmb_enabled()) return false; + const bool quant = mmb_quant_type(src0->type); + const bool bf16w = src0->type == GGML_TYPE_BF16 && mmb_bf16w(); + const bool f32w = src0->type == GGML_TYPE_F32 && mmb_f32split(); + if (quant && src0->ne[0] % ggml_blck_size(src0->type) != 0) return false; + if ((!quant && !bf16w && !f32w) || src1->type != GGML_TYPE_F32 || dst->type != GGML_TYPE_F32) return false; + if (src0->ne[2] != 1 || src0->ne[3] != 1) return false; + if (!ggml_is_contiguous(src0) || !ggml_is_contiguous(src1) || !ggml_is_contiguous(dst)) return false; + const int64_t K = src0->ne[0], M = src0->ne[1]; + if ((f32w ? K % 32 : K % 64) != 0 || src1->ne[0] != K || dst->ne[0] != M) return false; + const int64_t T = src1->ne[1] * src1->ne[2] * src1->ne[3]; + if (T < mmb_min_t() || T > INT32_MAX / 4) return false; + return ggml_nrows(dst) == T; +} + +bool ggml_cuda_mmb_supported_mmid(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst) { + if (!mmb_enabled()) return false; + if (!mmb_quant_type(src0->type) || src1->type != GGML_TYPE_F32 || dst->type != GGML_TYPE_F32 || ids->type != GGML_TYPE_I32) return false; + if (!ggml_is_contiguous(src0) || !ggml_is_contiguous(src1) || !ggml_is_contiguous(dst)) return false; + const int64_t K = src0->ne[0], M = src0->ne[1], E = src0->ne[2]; + if (src0->ne[3] != 1 || K % 64 != 0 || E < 1 || E > 1024) return false; + if (K % ggml_blck_size(src0->type) != 0) return false; + const int64_t n_used = ids->ne[0], T = ids->ne[1]; + if (src1->ne[0] != K || src1->ne[3] != 1 || src1->ne[2] != T) return false; + if (src1->ne[1] != 1 && src1->ne[1] != n_used) return false; + if (dst->ne[0] != M || dst->ne[1] != n_used || dst->ne[2] != T || dst->ne[3] != 1) return false; + if (ids->nb[0] != sizeof(int32_t) || ids->ne[2] != 1 || ids->ne[3] != 1) return false; + if (T < mmb_min_t() || n_used > 64 || (T * n_used) >> 16 >= 1024) return false; // tile index must fit in 16 bits per expert + return true; +} + +void ggml_cuda_mul_mat_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { + cudaStream_t stream = ctx.stream(); + const int K = (int) src0->ne[0], M = (int) src0->ne[1]; + const int T = (int) (src1->ne[1] * src1->ne[2] * src1->ne[3]); + if (src0->type == GGML_TYPE_F32) { + dim3 grid((M + 127) / 128, (T + 127) / 128); + static const bool two = true; + if (two) mmb_f32split_kernel<128, 128, 32, 64, true ><<<grid, MMB_NT, 0, stream>>>((const float *) src0->data, (const float *) src1->data, (float *) dst->data, M, K, T); + else mmb_f32split_kernel<128, 128, 32, 64, false><<<grid, MMB_NT, 0, stream>>>((const float *) src0->data, (const float *) src1->data, (float *) dst->data, M, K, T); + CUDA_CHECK(cudaGetLastError()); return; + } + const uint16_t * xhp = mmb_bf16_activation(ctx, src1, (size_t) T * K, stream); + const uint8_t * W = (const uint8_t *) src0->data; float * D = (float *) dst->data; + if (mmb_tall() && src0->type == GGML_TYPE_IQ4_NL && M <= 384 && K >= 4096 && T >= 2048) { // tall-M tile: HC down|inject [10240 -> 324], activations read once + static const int wide = mmb_tall_mode() >= 2; + if (wide) { dim3 grid(1, (T + 63) / 64); mmb_dense_kernel<384, 64, 96, 32, 0><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, (uint16_t *) nullptr, true, M, K, T); } + else { dim3 grid(1, (T + 31) / 32); mmb_dense_kernel<384, 32, 96, 16, 0><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, (uint16_t *) nullptr, true, M, K, T); } + CUDA_CHECK(cudaGetLastError()); + return; + } + const uint16_t * shadow_pre = ((src0->type == GGML_TYPE_IQ4_NL && mmb_shadow()) || src0->type == GGML_TYPE_Q6_K) ? mmb_shadow_lookup(src0) : nullptr; + const bool big = (M >= 6144 && K >= 2560) || (shadow_pre && K >= 2560 && T >= 4096); + uint16_t * Dh = (mmb_hc16() && K == 320 && M == 10240) ? ggml_cuda_mmb_slot_reserve(ctx, 1, dst, (size_t) T * M) : nullptr; + bool store_f32 = !(Dh && ggml_cuda_mmb_is_bf16_only(dst)); + if (ggml_cuda_mmb_blk16() && !Dh && ggml_cuda_mmb_is_bf16_only(dst) && (M & 7) == 0) { + Dh = (uint16_t *) dst->data; store_f32 = false; + } + dim3 grid((M + 127) / 128, big ? (T + 255) / 256 : (T + 127) / 128); + const uint16_t * shadow = shadow_pre; + + if (shadow) { + if (big) mmb_dense_kernel<128, 256, 64, 64, 2><<<grid, MMB_NT, 0, stream>>>((const uint8_t *) shadow, xhp, D, Dh, store_f32, M, K, T); + else mmb_dense_kernel<128, 128, 32, 64, 2><<<grid, MMB_NT, 0, stream>>>((const uint8_t *) shadow, xhp, D, Dh, store_f32, M, K, T); + } else if (src0->type == GGML_TYPE_IQ4_NL) { + if (big) mmb_dense_kernel<128, 256, 64, 64, 0><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + else mmb_dense_kernel<128, 128, 32, 64, 0><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + } else if (src0->type == GGML_TYPE_Q8_0) { + if (big) mmb_dense_kernel<128, 256, 64, 64, 1><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + else mmb_dense_kernel<128, 128, 32, 64, 1><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + } else if (mmb_quant_type(src0->type)) { + mmb_dispatch_quant(src0->type, [&](auto tag) { + constexpr int WT = decltype(tag)::value; + if (big) mmb_dense_kernel<128, 256, 64, 64, WT><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + else mmb_dense_kernel<128, 128, 32, 64, WT><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + }); + } else { + if (big) mmb_dense_kernel<128, 256, 64, 64, 2><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + else mmb_dense_kernel<128, 128, 32, 64, 2><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + } + CUDA_CHECK(cudaGetLastError()); +} + +bool ggml_cuda_mmb_gatemix() { return mmb_gatemix_flag(); } +bool ggml_cuda_mmb_down16() { return mmb_down16_flag(); } +bool ggml_cuda_mmb_blk16() { return true; } +bool ggml_cuda_mmb_res16() { return true; } +bool ggml_cuda_hc_gate_mix(ggml_backend_cuda_context & ctx, const ggml_tensor * w, const ggml_tensor * lo, const ggml_tensor * xn, ggml_tensor * dst, + const int hc, const float scale, const float bias) { + if (!mmb_gatemix_flag() || hc != 4 || !mmb_quant_type(w->type) || lo->type != GGML_TYPE_F32 || !ggml_is_contiguous(lo) || !ggml_is_contiguous(dst)) return false; + const int K = (int) w->ne[0], M = (int) w->ne[1], E = (int) dst->ne[0]; const int T = (int) ggml_nrows(dst); + if (K % ggml_blck_size(w->type) != 0) return false; + if (K % MMB_BK != 0 || M != hc * E || E % 32 != 0 || lo->ne[0] != K || ggml_nrows(lo) != T || xn->ne[0] != M || ggml_nrows(xn) != T || T < mmb_min_t()) return false; + const uint16_t * xn16 = ggml_cuda_mmb_cache_lookup(xn); + if (!xn16) return false; + cudaStream_t stream = ctx.stream(); + const uint16_t * lo16 = mmb_bf16_activation(ctx, lo, (size_t) T * K, stream); + uint16_t * outh = ggml_cuda_mmb_slot_reserve(ctx, 3, dst, (size_t) T * E); + const bool store_f32 = !(outh && ggml_cuda_mmb_is_bf16_only(dst)); + dim3 grid(E / 32, (T + 127) / 128); + mmb_dispatch_quant(w->type, [&](auto tag) { + constexpr int WT = decltype(tag)::value; + hc_gate_mix_kernel<4, WT><<<grid, MMB_NT, 0, stream>>>((const uint8_t *) w->data, lo16, xn16, (float *) dst->data, outh, store_f32, E, K, T, scale, bias); + }); + CUDA_CHECK(cudaGetLastError()); + return true; +} + +void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst) { + cudaStream_t stream = ctx.stream(); + const int K = (int) src0->ne[0], M = (int) src0->ne[1], E = (int) src0->ne[2]; + const int ne11 = (int) src1->ne[1], T = (int) src1->ne[2], n_used = (int) ids->ne[0]; + const int n_rows_x = ne11 * T, n_rows = n_used * T; + constexpr int BN = 128; + + const uint16_t * xhp = mmb_bf16_activation(ctx, src1, (size_t) n_rows_x * K, stream); + + ggml_cuda_pool_alloc<int32_t> ids_src1(ctx.pool(), n_rows); + ggml_cuda_pool_alloc<int32_t> ids_dst(ctx.pool(), n_rows); + ggml_cuda_pool_alloc<int32_t> bounds(ctx.pool(), E + 1); + const int si1 = (int) (ids->nb[1] / sizeof(int32_t)); + const int sis1 = (int) (src1->nb[2] / src1->nb[1]); + if (!ggml_cuda_launch_mm_ids_bounded(ctx, (const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), bounds.get(), + E, T, n_used, ne11, si1, sis1, /*inverse=*/false, stream)) { + ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), bounds.get(), + E, T, n_used, ne11, si1, sis1, /*write_inverse=*/false, stream); + } + constexpr int BN_SMALL = 32, THRESH = 128; + const int nbig_max = n_rows / BN + E + 1; + const int nsmall_max = E * ((THRESH + BN_SMALL - 1) / BN_SMALL) + 1; + ggml_cuda_pool_alloc<uint32_t> desc_big(ctx.pool(), nbig_max); + ggml_cuda_pool_alloc<uint32_t> desc_small(ctx.pool(), nsmall_max); + mmb_build_desc2<<<1, 1024, 0, stream>>>(bounds.get(), desc_big.get(), desc_small.get(), E, nbig_max, nsmall_max, BN, BN_SMALL, THRESH); + + const uint8_t * W = (const uint8_t *) src0->data; float * D = (float *) dst->data; const size_t eb = (size_t) src0->nb[2]; + uint16_t * Dh = (mmb_down16_flag() && ggml_cuda_mmb_is_bf16_only(dst)) ? (uint16_t *) dst->data : nullptr; + const bool store_f32 = Dh == nullptr; + dim3 gbig((M + 127) / 128, nbig_max), gsmall((M + 127) / 128, nsmall_max); + mmb_dispatch_quant(src0->type, [&](auto tag) { + constexpr int WT = decltype(tag)::value; + mmb_routed_kernel<128, BN, 32, 64, WT><<<gbig, MMB_NT, 0, stream>>>(W, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); + mmb_routed_kernel<128, BN_SMALL, 32, 16, WT><<<gsmall, MMB_NT, 0, stream>>>(W, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + }); + CUDA_CHECK(cudaGetLastError()); +} + +bool ggml_cuda_mmb_supported_glu(const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * glu) { + if (!mmb_enabled() || !mmb_glu() || !gw || !uw || !src1 || !ids || !glu) return false; + if (!mmb_quant_type(gw->type) || uw->type != gw->type) return false; + if (!ggml_are_same_shape(gw, uw) || gw->nb[1] != uw->nb[1] || gw->nb[2] != uw->nb[2]) return false; + if (glu->op != GGML_OP_GLU || ggml_get_glu_op(glu) != GGML_GLU_OP_SWIGLU || ggml_get_op_params_i32(glu, 1) != 0) return false; + if (glu->type != GGML_TYPE_F32 || !ggml_is_contiguous(glu) || !glu->src[0] || !glu->src[1]) return false; + if (glu->src[0]->op != GGML_OP_MUL_MAT_ID || glu->src[1]->op != GGML_OP_MUL_MAT_ID) return false; + if (glu->src[0]->src[0] != gw || glu->src[1]->src[0] != uw || glu->src[0]->src[1] != src1 || glu->src[1]->src[1] != src1 || glu->src[0]->src[2] != ids || glu->src[1]->src[2] != ids) return false; + if (ggml_nelements(glu) != ggml_nelements(glu->src[0]) || glu->ne[0] != gw->ne[1]) return false; + return ggml_cuda_mmb_supported_mmid(gw, src1, ids, glu->src[0]) && ggml_cuda_mmb_supported_mmid(uw, src1, ids, glu->src[1]); +} + +void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * glu) { + cudaStream_t stream = ctx.stream(); + const int K = (int) gw->ne[0], M = (int) gw->ne[1], E = (int) gw->ne[2]; + const int ne11 = (int) src1->ne[1], T = (int) src1->ne[2], n_used = (int) ids->ne[0]; + const int n_rows_x = ne11 * T, n_rows = n_used * T; + constexpr int BN = 128; + const uint16_t * xhp = mmb_bf16_activation(ctx, src1, (size_t) n_rows_x * K, stream); + ggml_cuda_pool_alloc<int32_t> ids_src1(ctx.pool(), n_rows); + ggml_cuda_pool_alloc<int32_t> ids_dst(ctx.pool(), n_rows); + ggml_cuda_pool_alloc<int32_t> bounds(ctx.pool(), E + 1); + const int si1 = (int) (ids->nb[1] / sizeof(int32_t)); + const int sis1 = (int) (src1->nb[2] / src1->nb[1]); + if (!ggml_cuda_launch_mm_ids_bounded(ctx, (const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), bounds.get(), + E, T, n_used, ne11, si1, sis1, /*inverse=*/false, stream)) { + ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), bounds.get(), + E, T, n_used, ne11, si1, sis1, /*write_inverse=*/false, stream); + } + constexpr int BN_SMALL = 32, THRESH = 128; + const int nbig_max = n_rows / BN + E + 1; + const int nsmall_max = E * ((THRESH + BN_SMALL - 1) / BN_SMALL) + 1; + ggml_cuda_pool_alloc<uint32_t> desc_big(ctx.pool(), nbig_max); + ggml_cuda_pool_alloc<uint32_t> desc_small(ctx.pool(), nsmall_max); + mmb_build_desc2<<<1, 1024, 0, stream>>>(bounds.get(), desc_big.get(), desc_small.get(), E, nbig_max, nsmall_max, BN, BN_SMALL, THRESH); + uint16_t * Dh = ggml_cuda_mmb_slot_reserve(ctx, 2, glu, (size_t) n_rows * M); + const bool store_f32 = !ggml_cuda_mmb_is_bf16_only(glu); + const uint8_t * Wg = (const uint8_t *) gw->data, * Wu = (const uint8_t *) uw->data; float * D = (float *) glu->data; const size_t eb = (size_t) gw->nb[2]; + dim3 gbig((M + 63) / 64, nbig_max), gsmall((M + 63) / 64, nsmall_max); + mmb_dispatch_quant(gw->type, [&](auto tag) { + constexpr int WT = decltype(tag)::value; + mmb_routed_glu_kernel<64, BN, 32, 32, WT><<<gbig, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); + mmb_routed_glu_kernel<64, BN_SMALL, 16, 16, WT><<<gsmall, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + }); + CUDA_CHECK(cudaGetLastError()); +} + +// Called from graph_optimize (outside stream capture): create the shadow for an eligible IQ4_NL dense weight. +void ggml_cuda_mmb_shadow_prepare(ggml_backend_cuda_context & ctx, const ggml_tensor * w) { + if (!w) return; + if (mmb_is_resident_q6k(w)) { + if (!mmb_shadow_q6k() || g_mmb_shadow.count(w->data) > 0) return; + const size_t n = (size_t) w->ne[0] * w->ne[1], bytes = n * 2; + if (g_mmb_shadow_bytes + bytes > mmb_shadow_cap()) { GGML_LOG_INFO("MMB_SHADOW cap reached; %s stays Q6_K\n", w->name); return; } + uint16_t * buf = nullptr; + if (cudaMalloc((void **) &buf, bytes) != cudaSuccess) { GGML_LOG_WARN("MMB_SHADOW alloc failed (%zu bytes)\n", bytes); return; } + mmb_dq_q6k_bf16_kernel<<<(unsigned) ((n / 256 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->data, buf, n / 256); + CUDA_CHECK(cudaGetLastError()); + g_mmb_shadow[w->data] = buf; g_mmb_shadow_bytes += bytes; + return; + } + if (mmb_shadow_mode() != 1) return; // mode 2: Q6_K only + const bool concat = mmb_is_row_concat(w); + if (!concat && !mmb_is_resident_iq4(w)) return; + if (concat ? g_mmb_shadow_pair.count({w->src[0]->data, w->src[1]->data}) > 0 : g_mmb_shadow.count(w->data) > 0) return; + const size_t n = (size_t) w->ne[0] * w->ne[1]; + const size_t bytes = n * 2; + if (g_mmb_shadow_bytes + bytes > mmb_shadow_cap()) { static bool warned = false; if (!warned) { GGML_LOG_INFO("MMB_SHADOW cap reached at %.1f MB; further weights stay IQ4_NL\n", g_mmb_shadow_bytes / 1048576.0); warned = true; } return; } + uint16_t * buf = nullptr; + if (cudaMalloc((void **) &buf, bytes) != cudaSuccess) { GGML_LOG_WARN("MMB_SHADOW alloc failed (%zu bytes)\n", bytes); return; } + if (concat) { + const size_t n0 = (size_t) w->src[0]->ne[0] * w->src[0]->ne[1], n1 = (size_t) w->src[1]->ne[0] * w->src[1]->ne[1]; + mmb_dq_iq4nl_bf16_kernel<<<(unsigned) ((n0 / 32 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->src[0]->data, buf, n0 / 32); + mmb_dq_iq4nl_bf16_kernel<<<(unsigned) ((n1 / 32 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->src[1]->data, buf + n0, n1 / 32); + g_mmb_shadow_pair[{w->src[0]->data, w->src[1]->data}] = buf; + } else { + mmb_dq_iq4nl_bf16_kernel<<<(unsigned) ((n / 32 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->data, buf, n / 32); + g_mmb_shadow[w->data] = buf; + } + CUDA_CHECK(cudaGetLastError()); + g_mmb_shadow_bytes += bytes; +} diff --git a/ggml/src/ggml-cuda/mmb.cuh b/ggml/src/ggml-cuda/mmb.cuh new file mode 100644 index 000000000000..431785828cea --- /dev/null +++ b/ggml/src/ggml-cuda/mmb.cuh @@ -0,0 +1,27 @@ +#pragma once +#include "common.cuh" +// Quantized-weight BF16 WMMA GEMM on gfx1151, from 512 tokens up. +bool ggml_cuda_mmb_supported_mm (const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst); +bool ggml_cuda_mmb_supported_mmid(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst); +void ggml_cuda_mul_mat_mmb (ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst); +void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst); +void ggml_cuda_mmb_begin_graph(); +// producers that can emit a BF16 copy of an F32 output register it here; returns the BF16 buffer to fill (n elements) +uint16_t * ggml_cuda_mmb_cache_reserve(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n); +// BF16 copy of tensor t if one is cached for the current graph (consumers may read it instead of the F32 data) +const uint16_t * ggml_cuda_mmb_cache_lookup(const ggml_tensor * t); +// producer slots (pinned until the next producer of the same kind): 0 = HC normalized stream xn, 1 = HC gate +uint16_t * ggml_cuda_mmb_slot_reserve(ggml_backend_cuda_context & ctx, int slot, const ggml_tensor * t, size_t n); +void ggml_cuda_mmb_marks_clear(); +size_t ggml_cuda_mmb_marks_count(); +void ggml_cuda_mmb_mark_bf16_only(const ggml_tensor * t); +bool ggml_cuda_mmb_is_bf16_only(const ggml_tensor * t); +bool ggml_cuda_mmb_gatemix(); +bool ggml_cuda_mmb_down16(); +bool ggml_cuda_mmb_res16(); +bool ggml_cuda_mmb_blk16(); +bool ggml_cuda_hc_gate_mix(ggml_backend_cuda_context & ctx, const ggml_tensor * w, const ggml_tensor * lo, const ggml_tensor * xn, ggml_tensor * dst, int hc, float scale, float bias); +bool ggml_cuda_mmb_supported_glu(const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * glu); +void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * glu); +void ggml_cuda_mmb_shadow_prepare(ggml_backend_cuda_context & ctx, const ggml_tensor * w); +void ggml_cuda_mmb_release_all(); diff --git a/ggml/src/ggml-cuda/mmid.cu b/ggml/src/ggml-cuda/mmid.cu index dc2c070976df..f61f4dff6497 100644 --- a/ggml/src/ggml-cuda/mmid.cu +++ b/ggml/src/ggml-cuda/mmid.cu @@ -236,3 +236,122 @@ void ggml_cuda_launch_mm_ids_helper( break; } } + +static constexpr int MM_IDS_ROUTE_TILE = 1024; +static constexpr int MM_IDS_ROUTE_BITS = 10; +static constexpr int MM_IDS_ROUTE_EXPERTS = 512; +static constexpr int MM_IDS_ROUTE_USED = 10; + +static __global__ void mm_ids_route_sort( + const int32_t * ids, uint32_t * sorted, int32_t * offsets, int32_t * starts, + int routes, int chunks, int ids_stride) { + __shared__ uint32_t keys[MM_IDS_ROUTE_TILE]; + __shared__ int first[MM_IDS_ROUTE_EXPERTS]; + __shared__ int counts[MM_IDS_ROUTE_EXPERTS]; + const int chunk=blockIdx.x; + for (int i=threadIdx.x;i<MM_IDS_ROUTE_EXPERTS;i+=blockDim.x) { + first[i]=0;counts[i]=0; + } + for (int i=threadIdx.x;i<MM_IDS_ROUTE_TILE;i+=blockDim.x) { + const int route=chunk*MM_IDS_ROUTE_TILE+i; + int expert=-1; + if (route<routes) expert=ids[(route/MM_IDS_ROUTE_USED)*ids_stride+route%MM_IDS_ROUTE_USED]; + keys[i]=expert>=0 && expert<MM_IDS_ROUTE_EXPERTS ? + (uint32_t(expert)<<MM_IDS_ROUTE_BITS)|uint32_t(i) : UINT32_MAX; + } + __syncthreads(); + for (int span=2;span<=MM_IDS_ROUTE_TILE;span*=2) { + for (int distance=span/2;distance>0;distance/=2) { + for (int i=threadIdx.x;i<MM_IDS_ROUTE_TILE;i+=blockDim.x) { + const int partner=i^distance; + if (partner>i) { + const uint32_t a=keys[i],b=keys[partner]; + if (((i&span)==0 && a>b) || ((i&span)!=0 && a<b)) { + keys[i]=b;keys[partner]=a; + } + } + } + __syncthreads(); + } + } + for (int i=threadIdx.x;i<MM_IDS_ROUTE_TILE;i+=blockDim.x) { + if (keys[i]!=UINT32_MAX) { + const int expert=keys[i]>>MM_IDS_ROUTE_BITS; + if (i==0 || (keys[i-1]>>MM_IDS_ROUTE_BITS)!=uint32_t(expert)) first[expert]=i; + } + sorted[chunk*MM_IDS_ROUTE_TILE+i]=keys[i]; + } + __syncthreads(); + for (int i=threadIdx.x;i<MM_IDS_ROUTE_TILE;i+=blockDim.x) { + if (keys[i]!=UINT32_MAX) { + const int expert=keys[i]>>MM_IDS_ROUTE_BITS; + if (i+1==MM_IDS_ROUTE_TILE || (keys[i+1]>>MM_IDS_ROUTE_BITS)!=uint32_t(expert)) { + counts[expert]=i+1-first[expert]; + } + } + } + __syncthreads(); + for (int i=threadIdx.x;i<MM_IDS_ROUTE_EXPERTS;i+=blockDim.x) { + offsets[chunk*MM_IDS_ROUTE_EXPERTS+i]=counts[i]; + starts[chunk*MM_IDS_ROUTE_EXPERTS+i]=first[i]; + } + (void)chunks; +} + +static __global__ void mm_ids_route_prefix(int32_t * offsets, int32_t * bounds, int chunks) { + __shared__ int totals[MM_IDS_ROUTE_EXPERTS]; + const int expert=threadIdx.x; + int total=0; + for (int chunk=0;chunk<chunks;++chunk) total+=offsets[chunk*MM_IDS_ROUTE_EXPERTS+expert]; + totals[expert]=total; + __syncthreads(); + int prefix=0; + for (int previous=0;previous<expert;++previous) prefix+=totals[previous]; + bounds[expert]=prefix; + if (expert==MM_IDS_ROUTE_EXPERTS-1) bounds[MM_IDS_ROUTE_EXPERTS]=prefix+total; + for (int chunk=0;chunk<chunks;++chunk) { + const int index=chunk*MM_IDS_ROUTE_EXPERTS+expert; + const int count=offsets[index]; + offsets[index]=prefix; + prefix+=count; + } +} + +static __global__ void mm_ids_route_scatter( + const uint32_t * sorted, const int32_t * offsets, const int32_t * starts, + int32_t * src_map, int32_t * dst_map, int chunks, int channels, int token_stride, bool inverse) { + const int index=blockIdx.x*blockDim.x+threadIdx.x; + if (index>=chunks*MM_IDS_ROUTE_TILE) return; + const uint32_t key=sorted[index]; + if (key==UINT32_MAX) return; + const int chunk=index/MM_IDS_ROUTE_TILE; + const int expert=key>>MM_IDS_ROUTE_BITS; + const int local=index%MM_IDS_ROUTE_TILE; + const int route=chunk*MM_IDS_ROUTE_TILE+(key&(MM_IDS_ROUTE_TILE-1)); + const int target=offsets[chunk*MM_IDS_ROUTE_EXPERTS+expert]+local-starts[chunk*MM_IDS_ROUTE_EXPERTS+expert]; + dst_map[target]=route; + if (inverse) src_map[route]=target; + else src_map[target]=(route/MM_IDS_ROUTE_USED)*token_stride+(route%MM_IDS_ROUTE_USED)%channels; +} + +bool ggml_cuda_launch_mm_ids_bounded( + ggml_backend_cuda_context & ctx, const int32_t * ids, int32_t * src_map, + int32_t * dst_map, int32_t * bounds, int experts, int tokens, int used, + int channels, int ids_stride, int token_stride, bool inverse, cudaStream_t stream) { + const int device=ctx.device; + if (!GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[device].cc) || + experts!=MM_IDS_ROUTE_EXPERTS || used!=MM_IDS_ROUTE_USED || + tokens<=4096 || tokens>32768 || (channels!=1 && channels!=used) || + ids_stride<used || token_stride<channels) return false; + const int routes=tokens*used; + const int chunks=(routes+MM_IDS_ROUTE_TILE-1)/MM_IDS_ROUTE_TILE; + ggml_cuda_pool_alloc<uint32_t> sorted(ctx.pool(device),size_t(chunks)*MM_IDS_ROUTE_TILE); + ggml_cuda_pool_alloc<int32_t> offsets(ctx.pool(device),size_t(chunks)*experts); + ggml_cuda_pool_alloc<int32_t> starts(ctx.pool(device),size_t(chunks)*experts); + mm_ids_route_sort<<<chunks,256,0,stream>>>(ids,sorted.get(),offsets.get(),starts.get(),routes,chunks,ids_stride); + mm_ids_route_prefix<<<1,MM_IDS_ROUTE_EXPERTS,0,stream>>>(offsets.get(),bounds,chunks); + mm_ids_route_scatter<<<(chunks*MM_IDS_ROUTE_TILE+255)/256,256,0,stream>>> + (sorted.get(),offsets.get(),starts.get(),src_map,dst_map,chunks,channels,token_stride,inverse); + CUDA_CHECK(cudaGetLastError()); + return true; +} diff --git a/ggml/src/ggml-cuda/mmid.cuh b/ggml/src/ggml-cuda/mmid.cuh index 74c2db43385e..96de66b48d07 100644 --- a/ggml/src/ggml-cuda/mmid.cuh +++ b/ggml/src/ggml-cuda/mmid.cuh @@ -3,3 +3,9 @@ void ggml_cuda_launch_mm_ids_helper( const int32_t * ids, int32_t * ids_src1, int32_t * ids_dst, int32_t * expert_bounds, int n_experts, int n_tokens, int n_expert_used, int nchannels_y, int si1, int sis1, bool write_inverse, cudaStream_t stream); + +struct ggml_backend_cuda_context; +bool ggml_cuda_launch_mm_ids_bounded(ggml_backend_cuda_context & ctx, + const int32_t * ids, int32_t * src_map, int32_t * dst_map, int32_t * bounds, + int experts, int tokens, int used, int channels, int ids_stride, int token_stride, + bool inverse, cudaStream_t stream); diff --git a/ggml/src/ggml-cuda/mmq.cu b/ggml/src/ggml-cuda/mmq.cu index 35b0206d91c6..dc7d2fdbc3a1 100644 --- a/ggml/src/ggml-cuda/mmq.cu +++ b/ggml/src/ggml-cuda/mmq.cu @@ -183,8 +183,11 @@ void ggml_cuda_mul_mat_q_pair(ggml_backend_cuda_context & ctx, ggml_tensor * dst const int si1 = ids->nb[1] / ggml_element_size(ids); const int sis1 = src1->nb[2] / src1->nb[1]; - ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), expert_bounds.get(), - src0->ne[2], n_tokens, n_expert_used, src1->ne[1], si1, sis1, /*write_inverse =*/ dedup_bcast, stream); + if (!ggml_cuda_launch_mm_ids_bounded(ctx, (const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), expert_bounds.get(), + src0->ne[2], n_tokens, n_expert_used, src1->ne[1], si1, sis1, dedup_bcast, stream)) { + ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), expert_bounds.get(), + src0->ne[2], n_tokens, n_expert_used, src1->ne[1], si1, sis1, /*write_inverse =*/ dedup_bcast, stream); + } CUDA_CHECK(cudaGetLastError()); size_t nbytes_src1_q8_1 = n_tokens*n_expert_used*ne10_padded * sizeof(block_q8_1_mmq)/QK8_1_MMQ; @@ -372,8 +375,11 @@ static void ggml_cuda_mul_mat_q_impl( const int si1 = ids->nb[1] / ggml_element_size(ids); const int sis1 = nb12 / nb11; - ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), expert_bounds.get(), - ne02, ne12, n_expert_used, ne11, si1, sis1, /*write_inverse =*/ dedup_bcast, stream); + if (!ggml_cuda_launch_mm_ids_bounded(ctx, (const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), expert_bounds.get(), + ne02, ne12, n_expert_used, ne11, si1, sis1, dedup_bcast, stream)) { + ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), expert_bounds.get(), + ne02, ne12, n_expert_used, ne11, si1, sis1, /*write_inverse =*/ dedup_bcast, stream); + } CUDA_CHECK(cudaGetLastError()); } diff --git a/ggml/src/ggml-cuda/moe-weighted-reduction.cu b/ggml/src/ggml-cuda/moe-weighted-reduction.cu index 11ec58497f1e..017d7688527d 100644 --- a/ggml/src/ggml-cuda/moe-weighted-reduction.cu +++ b/ggml/src/ggml-cuda/moe-weighted-reduction.cu @@ -1,4 +1,9 @@ #include "moe-weighted-reduction.cuh" +#include "mmb.cuh" +#include <cstdlib> + +__device__ __forceinline__ float moe_bf2f(const uint16_t h) { return __uint_as_float(((uint32_t) h) << 16); } + static __global__ void moe_weighted_reduction_f32(const float * __restrict__ experts, const float * __restrict__ expert_scale, @@ -24,6 +29,115 @@ static __global__ void moe_weighted_reduction_f32(const float * __restrict__ exp dst[token * n_embd + col] = sum; } + +// float4 variant: 4 consecutive columns per thread, same per-element accumulation order as the scalar kernel (bit-identical) +static __global__ void moe_weighted_reduction_f32_v4(const float * __restrict__ experts, + const float * __restrict__ expert_scale, + const float * __restrict__ weights, + float * __restrict__ dst, + const int64_t n_embd, + const int n_expert_used) { + const int64_t token = blockIdx.x; + const int64_t col4 = ((int64_t) blockIdx.y * blockDim.x + threadIdx.x) * 4; + if (col4 >= n_embd) return; + const uint64_t first_row = (uint64_t) token * n_expert_used; + const float first_scale = expert_scale != nullptr ? expert_scale[first_row] : 1.0f; + const float w0 = weights[first_row]; + const float4 e0 = *(const float4 *)(experts + first_row * n_embd + col4); + float4 sum; sum.x = (e0.x * first_scale) * w0; sum.y = (e0.y * first_scale) * w0; sum.z = (e0.z * first_scale) * w0; sum.w = (e0.w * first_scale) * w0; + for (int expert = 1; expert < n_expert_used; ++expert) { + const uint64_t row = first_row + expert; + const float scale = expert_scale != nullptr ? expert_scale[row] : 1.0f; + const float w = weights[row]; + const float4 e = *(const float4 *)(experts + row * n_embd + col4); + sum.x += (e.x * scale) * w; sum.y += (e.y * scale) * w; sum.z += (e.z * scale) * w; sum.w += (e.w * scale) * w; + } + *(float4 *)(dst + token * n_embd + col4) = sum; +} + +// BF16 expert outputs: identical arithmetic, the inputs are the BF16-rounded down-GEMM results +static __global__ void moe_weighted_reduction_bf16_v4(const uint16_t * __restrict__ experts, + const float * __restrict__ expert_scale, + const float * __restrict__ weights, + float * __restrict__ dst, + const int64_t n_embd, + const int n_expert_used) { + const int64_t token = blockIdx.x; + const int64_t col4 = ((int64_t) blockIdx.y * blockDim.x + threadIdx.x) * 4; + if (col4 >= n_embd) return; + const uint64_t first_row = (uint64_t) token * n_expert_used; + const float first_scale = expert_scale != nullptr ? expert_scale[first_row] : 1.0f; + const float w0 = weights[first_row]; + const ushort4 h0 = *(const ushort4 *)(experts + first_row * n_embd + col4); + float4 sum; sum.x = (moe_bf2f(h0.x) * first_scale) * w0; sum.y = (moe_bf2f(h0.y) * first_scale) * w0; + sum.z = (moe_bf2f(h0.z) * first_scale) * w0; sum.w = (moe_bf2f(h0.w) * first_scale) * w0; + for (int expert = 1; expert < n_expert_used; ++expert) { + const uint64_t row = first_row + expert; + const float scale = expert_scale != nullptr ? expert_scale[row] : 1.0f; + const float w = weights[row]; + const ushort4 h = *(const ushort4 *)(experts + row * n_embd + col4); + sum.x += (moe_bf2f(h.x) * scale) * w; sum.y += (moe_bf2f(h.y) * scale) * w; + sum.z += (moe_bf2f(h.z) * scale) * w; sum.w += (moe_bf2f(h.w) * scale) * w; + } + *(float4 *)(dst + token * n_embd + col4) = sum; +} + +__device__ __forceinline__ uint16_t moe_f2bf(const float f) { uint32_t u = __float_as_uint(f); u += 0x7fffu + ((u >> 16) & 1u); return (uint16_t)(u >> 16); } + +// BF16 in, BF16 out: the only consumer is the fused HC combine, which reads block_out as BF16 +static __global__ void moe_weighted_reduction_bf16_v4_out(const uint16_t * __restrict__ experts, + const float * __restrict__ expert_scale, + const float * __restrict__ weights, + uint16_t * __restrict__ dst, + const float * __restrict__ merge, + const int64_t n_embd, const int n_expert_used) { + const int64_t token = blockIdx.x; + const int64_t col4 = ((int64_t) blockIdx.y * blockDim.x + threadIdx.x) * 4; + if (col4 >= n_embd) return; + const uint64_t first_row = (uint64_t) token * n_expert_used; + const float first_scale = expert_scale != nullptr ? expert_scale[first_row] : 1.0f; + const float w0 = weights[first_row]; + const ushort4 h0 = *(const ushort4 *)(experts + first_row * n_embd + col4); + float4 sum; sum.x = (moe_bf2f(h0.x) * first_scale) * w0; sum.y = (moe_bf2f(h0.y) * first_scale) * w0; + sum.z = (moe_bf2f(h0.z) * first_scale) * w0; sum.w = (moe_bf2f(h0.w) * first_scale) * w0; + for (int expert = 1; expert < n_expert_used; ++expert) { + const uint64_t row = first_row + expert; + const float scale = expert_scale != nullptr ? expert_scale[row] : 1.0f; + const float w = weights[row]; + const ushort4 h = *(const ushort4 *)(experts + row * n_embd + col4); + sum.x += (moe_bf2f(h.x) * scale) * w; sum.y += (moe_bf2f(h.y) * scale) * w; + sum.z += (moe_bf2f(h.z) * scale) * w; sum.w += (moe_bf2f(h.w) * scale) * w; + } + if (merge) { const float4 m = *(const float4 *)(merge + token * n_embd + col4); sum.x += m.x; sum.y += m.y; sum.z += m.z; sum.w += m.w; } + *(ushort4 *)(dst + token * n_embd + col4) = make_ushort4(moe_f2bf(sum.x), moe_f2bf(sum.y), moe_f2bf(sum.z), moe_f2bf(sum.w)); +} + +static __global__ void moe_weighted_reduction_f32in_bf16out_v4(const float * __restrict__ experts, + const float * __restrict__ expert_scale, + const float * __restrict__ weights, + uint16_t * __restrict__ dst, + const float * __restrict__ merge, + const int64_t n_embd, const int n_expert_used) { + const int64_t token = blockIdx.x; + const int64_t col4 = ((int64_t) blockIdx.y * blockDim.x + threadIdx.x) * 4; + if (col4 >= n_embd) return; + const uint64_t first_row = (uint64_t) token * n_expert_used; + const float first_scale = expert_scale != nullptr ? expert_scale[first_row] : 1.0f; + const float w0 = weights[first_row]; + const float4 e0 = *(const float4 *)(experts + first_row * n_embd + col4); + float4 sum; sum.x = (e0.x * first_scale) * w0; sum.y = (e0.y * first_scale) * w0; + sum.z = (e0.z * first_scale) * w0; sum.w = (e0.w * first_scale) * w0; + for (int expert = 1; expert < n_expert_used; ++expert) { + const uint64_t row = first_row + expert; + const float scale = expert_scale != nullptr ? expert_scale[row] : 1.0f; + const float w = weights[row]; + const float4 e = *(const float4 *)(experts + row * n_embd + col4); + sum.x += (e.x * scale) * w; sum.y += (e.y * scale) * w; sum.z += (e.z * scale) * w; sum.w += (e.w * scale) * w; + } + if (merge) { const float4 m = *(const float4 *)(merge + token * n_embd + col4); sum.x += m.x; sum.y += m.y; sum.z += m.z; sum.w += m.w; } + *(ushort4 *)(dst + token * n_embd + col4) = make_ushort4(moe_f2bf(sum.x), moe_f2bf(sum.y), moe_f2bf(sum.z), moe_f2bf(sum.w)); +} + static void launch_moe_weighted_reduction(const float * experts, const float * expert_scale, const float * weights, @@ -33,6 +147,12 @@ static void launch_moe_weighted_reduction(const float * experts, int n_expert_used, cudaStream_t stream) { constexpr int threads = 256; + static const bool use_v4 = true; + if (use_v4 && n_embd % 4 == 0 && ((uintptr_t) experts % 16) == 0 && ((uintptr_t) dst % 16) == 0) { + const dim3 blocks(n_tokens, (n_embd / 4 + threads - 1) / threads, 1); + moe_weighted_reduction_f32_v4<<<blocks, threads, 0, stream>>>(experts, expert_scale, weights, dst, n_embd, n_expert_used); + return; + } const dim3 blocks(n_tokens, (n_embd + threads - 1) / threads, 1); moe_weighted_reduction_f32 <<<blocks, threads, 0, stream>>>(experts, expert_scale, weights, dst, n_embd, n_expert_used); @@ -42,7 +162,8 @@ void ggml_cuda_op_moe_weighted_reduction(ggml_backend_cuda_context & ctx, const ggml_tensor * experts, const ggml_tensor * expert_scale, const ggml_tensor * weights, - ggml_tensor * dst) { + ggml_tensor * dst, + const ggml_tensor * merge) { GGML_ASSERT(experts->type == GGML_TYPE_F32); GGML_ASSERT(weights->type == GGML_TYPE_F32); GGML_ASSERT(expert_scale == nullptr || expert_scale->type == GGML_TYPE_F32); @@ -57,6 +178,32 @@ void ggml_cuda_op_moe_weighted_reduction(ggml_backend_cuda_context & ctx, const int64_t n_tokens = experts->ne[2] * experts->ne[3]; cudaStream_t stream = ctx.stream(); + const float * mrg = merge ? (const float *) merge->data : nullptr; + if (ggml_cuda_mmb_blk16() && ggml_cuda_mmb_is_bf16_only(dst)) { + GGML_ASSERT(n_embd % 4 == 0); + const bool ein = ggml_cuda_mmb_is_bf16_only(experts); + constexpr int threads = 256; + const dim3 blocks(n_tokens, (n_embd / 4 + threads - 1) / threads, 1); + if (ein) moe_weighted_reduction_bf16_v4_out<<<blocks, threads, 0, stream>>>((const uint16_t *) experts->data, + expert_scale ? (const float *) expert_scale->data : nullptr, (const float *) weights->data, + (uint16_t *) dst->data, mrg, n_embd, (int) n_expert_used); + else moe_weighted_reduction_f32in_bf16out_v4<<<blocks, threads, 0, stream>>>((const float *) experts->data, + expert_scale ? (const float *) expert_scale->data : nullptr, (const float *) weights->data, + (uint16_t *) dst->data, mrg, n_embd, (int) n_expert_used); + CUDA_CHECK(cudaGetLastError()); + return; + } + GGML_ASSERT(merge == nullptr && "shared-expert merge is only fused on the BF16 output path"); + if (ggml_cuda_mmb_down16() && ggml_cuda_mmb_is_bf16_only(experts)) { + GGML_ASSERT(n_embd % 4 == 0 && ((uintptr_t) experts->data % 16) == 0 && ((uintptr_t) dst->data % 16) == 0); + constexpr int threads = 256; + const dim3 blocks(n_tokens, (n_embd / 4 + threads - 1) / threads, 1); + moe_weighted_reduction_bf16_v4<<<blocks, threads, 0, stream>>>((const uint16_t *) experts->data, + expert_scale ? (const float *) expert_scale->data : nullptr, (const float *) weights->data, + (float *) dst->data, n_embd, (int) n_expert_used); + CUDA_CHECK(cudaGetLastError()); + return; + } launch_moe_weighted_reduction((const float *) experts->data, expert_scale ? (const float *) expert_scale->data : nullptr, (const float *) weights->data, diff --git a/ggml/src/ggml-cuda/moe-weighted-reduction.cuh b/ggml/src/ggml-cuda/moe-weighted-reduction.cuh index b72f947ab398..894c4b1a35ed 100644 --- a/ggml/src/ggml-cuda/moe-weighted-reduction.cuh +++ b/ggml/src/ggml-cuda/moe-weighted-reduction.cuh @@ -4,4 +4,5 @@ void ggml_cuda_op_moe_weighted_reduction(ggml_backend_cuda_context & ctx, const ggml_tensor * experts, const ggml_tensor * expert_scale, const ggml_tensor * weights, - ggml_tensor * dst); + ggml_tensor * dst, + const ggml_tensor * merge = nullptr); diff --git a/ggml/src/ggml-cuda/norm-gated.cu b/ggml/src/ggml-cuda/norm-gated.cu new file mode 100644 index 000000000000..ff509550e1b0 --- /dev/null +++ b/ggml/src/ggml-cuda/norm-gated.cu @@ -0,0 +1,134 @@ +#include "norm-gated.cuh" +#include "ggml-impl.h" + +// One wave per row, with the same reduction tree as the block-per-row norm. +static __device__ __forceinline__ float xor_tree(float v) { +#pragma unroll + for (int off = 16; off > 0; off >>= 1) { v += __shfl_xor_sync(0xffffffff, v, off, 32); } + return v; +} +template <bool GATE> +static __global__ void __launch_bounds__(256) rms_rows_f32(const float * x, const float * w, const float * z, float * dst, + const int ncols, const uint3 row_div, const uint3 channel_div, const int64_t total_rows, + const int64_t stride_row, const int64_t stride_channel, const int64_t stride_sample, const float eps) { + const int lane = threadIdx.x & 31; + const int64_t g = (int64_t) blockIdx.x * 8 + (threadIdx.x >> 5); + if (g >= total_rows) return; + uint32_t row = uint32_t(g), channel = 0, sample = 0; + if (uint64_t(row_div.z) != uint64_t(total_rows)) { + const uint2 rc = fast_div_modulo(row, row_div); + const uint2 cs = fast_div_modulo(rc.x, channel_div); + row = rc.y; channel = cs.y; sample = cs.x; + } + x += sample * stride_sample + channel * stride_channel + row * stride_row; + dst += g * ncols; if (GATE) z += g * ncols; + float part[8]; +#pragma unroll + for (int wv = 0; wv < 8; ++wv) { + const int col = 32 * wv + lane; + const float xi = col < ncols ? x[col] : 0.0f; + part[wv] = xor_tree(xi * xi); + } + float v = 0.0f; +#pragma unroll + for (int wv = 0; wv < 8; ++wv) { v = lane == wv ? part[wv] : v; } + const float tmp = xor_tree(v); + const float mean = tmp / ncols; + const float scale = rsqrtf(mean + eps); +#pragma unroll + for (int wv = 0; wv < 8; ++wv) { + const int col = 32 * wv + lane; + if (col < ncols) { + const float t = scale * x[col] * w[col]; + if (GATE) { const float s = 1.0f / (1.0f + expf(-z[col])); dst[col] = t * s; } else { dst[col] = t; } + } + } +} + +static bool rows_shape_ok(const ggml_tensor * x, const ggml_tensor * w, const ggml_tensor * out) { + if (x->type != GGML_TYPE_F32 || w->type != GGML_TYPE_F32 || out->type != GGML_TYPE_F32) return false; + if (x->ne[0] > 256 || x->ne[0] % 32 != 0 || x->nb[0] != 4 || !ggml_is_contiguous(w) || !ggml_is_contiguous(out)) return false; + if (w->ne[0] != x->ne[0] || w->ne[1] != 1 || w->ne[2] != 1 || w->ne[3] != 1) return false; + const int64_t rows = x->ne[1] * x->ne[2] * x->ne[3]; + if (rows < 4096 || uint64_t(rows) > UINT32_MAX) return false; + return ggml_nelements(out) == ggml_nelements(x); +} + +static bool norm_overlaps(const ggml_tensor * a, const ggml_tensor * b) { + if (!a || !b || !a->data || !b->data) return false; + const uintptr_t av = uintptr_t(a->data), bv = uintptr_t(b->data); + return av <= bv ? bv - av < ggml_nbytes(a) : av - bv < ggml_nbytes(b); +} + +static bool norm_elementwise_alias(const ggml_tensor * input, const ggml_tensor * out) { + return !norm_overlaps(input, out) || (input->data == out->data && + ggml_is_contiguous(input) && ggml_are_same_shape(input, out)); +} + +static bool norm_needs_staging(const ggml_cuda_norm_gated_match & m) { + if (!norm_elementwise_alias(m.x, m.dst) || norm_overlaps(m.w, m.dst) || + (m.z && !norm_elementwise_alias(m.z, m.dst))) return true; + if (m.pre >= 0) { + const auto * projected = m.z->view_src ? m.z->view_src : m.z; + if (norm_overlaps(m.x, projected) || norm_overlaps(m.w, projected)) return true; + } + return false; +} + +int ggml_cuda_norm_gated_match_at(const ggml_cgraph * cgraph, int i, ggml_cuda_norm_gated_match & m) { + if (i + 3 >= cgraph->n_nodes) return 0; + const ggml_tensor * rms = cgraph->nodes[i], * mul = cgraph->nodes[i+1]; + if (rms->op != GGML_OP_RMS_NORM || mul->op != GGML_OP_MUL) return 0; + int k = i + 2; m.pre = -1; + while (k + 1 < cgraph->n_nodes && k < i + 6) { + const ggml_op op = cgraph->nodes[k]->op; + if (op == GGML_OP_VIEW || op == GGML_OP_RESHAPE || op == GGML_OP_PERMUTE || op == GGML_OP_TRANSPOSE) { ++k; continue; } + if (op == GGML_OP_MUL_MAT && m.pre < 0) { m.pre = k; ++k; continue; } + break; + } + if (k + 1 >= cgraph->n_nodes) return 0; + const ggml_tensor * sig = cgraph->nodes[k]; ggml_tensor * out = cgraph->nodes[k+1]; + if (sig->op != GGML_OP_UNARY || out->op != GGML_OP_MUL) return 0; + if (ggml_get_unary_op(sig) != GGML_UNARY_OP_SIGMOID) return 0; + const ggml_tensor * x = rms->src[0]; + const ggml_tensor * w = mul->src[0] == rms ? mul->src[1] : (mul->src[1] == rms ? mul->src[0] : nullptr); + if (!w) return 0; + const ggml_tensor * z = sig->src[0]; + if (m.pre >= 0) { const ggml_tensor * zr = z->view_src ? z->view_src : z; if (zr != cgraph->nodes[m.pre]) return 0; } + if (!((out->src[0] == mul && out->src[1] == sig) || (out->src[0] == sig && out->src[1] == mul))) return 0; + if (!ggml_node_has_n_uses(cgraph, i, 1) || !ggml_node_has_n_uses(cgraph, i+1, 1) || !ggml_node_has_n_uses(cgraph, k, 1)) return 0; + if (!rows_shape_ok(x, w, out) || z->type != GGML_TYPE_F32 || !ggml_is_contiguous(z) || !ggml_is_contiguous(mul)) return 0; + if (!ggml_are_same_shape(z, out) || !ggml_are_same_shape(mul, out) || !ggml_are_same_shape(rms, out)) return 0; + float eps; memcpy(&eps, rms->op_params, sizeof(float)); if (eps < 0.0f) return 0; + m.x = x; m.w = w; m.z = z; m.dst = out; m.eps = eps; + m.staged = norm_needs_staging(m); + return (k + 1) - i; +} + +// plain RMS_NORM -> MUL(w) on narrow rows +int ggml_cuda_norm_rows_match_at(const ggml_cgraph * cgraph, int i, ggml_cuda_norm_gated_match & m) { + if (i + 1 >= cgraph->n_nodes) return 0; + const ggml_tensor * rms = cgraph->nodes[i]; ggml_tensor * mul = cgraph->nodes[i+1]; + if (rms->op != GGML_OP_RMS_NORM || mul->op != GGML_OP_MUL) return 0; + const ggml_tensor * x = rms->src[0]; + const ggml_tensor * w = mul->src[0] == rms ? mul->src[1] : (mul->src[1] == rms ? mul->src[0] : nullptr); + if (!w || !ggml_node_has_n_uses(cgraph, i, 1)) return 0; + if (!rows_shape_ok(x, w, mul) || !ggml_are_same_shape(rms, mul)) return 0; + float eps; memcpy(&eps, rms->op_params, sizeof(float)); if (eps < 0.0f) return 0; + m.x = x; m.w = w; m.z = nullptr; m.dst = mul; m.eps = eps; m.pre = -1; + m.staged = norm_needs_staging(m); + return 1; +} + +void ggml_cuda_op_norm_gated(ggml_backend_cuda_context & ctx, const ggml_cuda_norm_gated_match & m) { + const ggml_tensor * x = m.x; + const int64_t total = x->ne[1] * x->ne[2] * x->ne[3]; + const uint3 row_div = init_fastdiv_values(uint32_t(x->ne[1])); + const uint3 channel_div = init_fastdiv_values(uint32_t(x->ne[2])); + const dim3 grid((unsigned) ((total + 7) / 8)), block(256); + const ggml_cuda_kernel_launch_params lp(grid, block, 0, ctx.stream()); + if (m.z) ggml_cuda_kernel_launch(rms_rows_f32<true>, lp, (const float *) x->data, (const float *) m.w->data, (const float *) m.z->data, (float *) m.dst->data, + (int) x->ne[0], row_div, channel_div, total, (int64_t) (x->nb[1] / 4), (int64_t) (x->nb[2] / 4), (int64_t) (x->nb[3] / 4), m.eps); + else ggml_cuda_kernel_launch(rms_rows_f32<false>, lp, (const float *) x->data, (const float *) m.w->data, (const float *) nullptr, (float *) m.dst->data, + (int) x->ne[0], row_div, channel_div, total, (int64_t) (x->nb[1] / 4), (int64_t) (x->nb[2] / 4), (int64_t) (x->nb[3] / 4), m.eps); +} diff --git a/ggml/src/ggml-cuda/norm-gated.cuh b/ggml/src/ggml-cuda/norm-gated.cuh new file mode 100644 index 000000000000..5280a9c5581e --- /dev/null +++ b/ggml/src/ggml-cuda/norm-gated.cuh @@ -0,0 +1,14 @@ +#pragma once +#include "common.cuh" +struct ggml_cuda_norm_gated_match { + const ggml_tensor * x; + const ggml_tensor * w; + const ggml_tensor * z; + ggml_tensor * dst; + float eps; + int pre = -1; + bool staged = false; +}; +int ggml_cuda_norm_gated_match_at(const ggml_cgraph * cgraph, int i, ggml_cuda_norm_gated_match & m); +void ggml_cuda_op_norm_gated(ggml_backend_cuda_context & ctx, const ggml_cuda_norm_gated_match & m); +int ggml_cuda_norm_rows_match_at(const ggml_cgraph * cgraph, int i, ggml_cuda_norm_gated_match & m); diff --git a/ggml/src/ggml-cuda/ple-conv.cu b/ggml/src/ggml-cuda/ple-conv.cu new file mode 100644 index 000000000000..e0c5015ea17b --- /dev/null +++ b/ggml/src/ggml-cuda/ple-conv.cu @@ -0,0 +1,299 @@ +#include "ple-conv.cuh" +#include "conv-tail.cuh" +#include "unary.cuh" + +#if defined(__HIP_PLATFORM_AMD__) +// the unfused graph multiplies in one kernel and adds in another: keep the two roundings apart +static __device__ __forceinline__ float ple_mul_rn(const float a, const float b) { float r; asm("v_mul_f32_e32 %0, %1, %2" : "=v"(r) : "v"(a), "v"(b)); return r; } +static __device__ __forceinline__ float ple_add_rn(const float a, const float b) { float r; asm("v_add_f32_e32 %0, %1, %2" : "=v"(r) : "v"(a), "v"(b)); return r; } +#else +static __device__ __forceinline__ float ple_mul_rn(const float a, const float b) { return __fmul_rn(a, b); } +static __device__ __forceinline__ float ple_add_rn(const float a, const float b) { return __fadd_rn(a, b); } +#endif + +// columns [tail_from, T+H) of every channel row of the concat: what the history copies read +static __global__ void ple_concat_tail(const float * __restrict__ state, const float * __restrict__ x, float * __restrict__ out, + const int C, const int T, const int H, const int tail_from, const int row_stride) { + const int ncols = T + H - tail_from; + const int64_t idx = (int64_t) blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= (int64_t) C * ncols) { + return; + } + const int c = (int) (idx / ncols), j = tail_from + (int) (idx % ncols); + out[(size_t) c * row_stride + j] = (j < H) ? state[c * H + j] : x[(size_t) (j - H) * C + c]; +} + +template <int K, int DIL, int TT> +static __global__ void __launch_bounds__(256) ple_conv_kernel(const float * __restrict__ state, const float * __restrict__ x, + const half * __restrict__ w, float * __restrict__ y, const int C, const int T) { + constexpr int H = (K - 1) * DIL, WIN = H + 1; + const int c = blockIdx.x * 256 + threadIdx.x, t0 = blockIdx.y * TT; + if (c >= C) { + return; + } + float wr[K]; +#pragma unroll + for (int k = 0; k < K; ++k) { + wr[k] = __half2float(w[c * K + k]); + } + float win[WIN]; + auto ld = [&](int jp) -> float { + return (jp < H) ? state[c * H + jp] : ((jp - H) < T ? x[(size_t) (jp - H) * C + c] : 0.0f); + }; +#pragma unroll + for (int k = 0; k < WIN; ++k) { + win[k] = ld(t0 + k); + } + const int tend = min(T, t0 + TT); + for (int t = t0; t < tend; ++t) { + float s = ple_mul_rn(win[0], wr[0]); +#pragma unroll + for (int k = 1; k < K; ++k) { + s = ple_add_rn(s, ple_mul_rn(win[k * DIL], wr[k])); + } + y[(size_t) t * C + c] = ggml_cuda_op_silu_single(s); +#pragma unroll + for (int k = 0; k < WIN - 1; ++k) { + win[k] = win[k + 1]; + } + win[WIN - 1] = ld(t + WIN); + } +} + +// tap k: CONT(TRANSPOSE(VIEW of the concat at column k*dil, [T, C])) +static int ple_tap_index(const ggml_tensor * t, const ggml_tensor * cc, int64_t C, int64_t T, int64_t dil, int K) { + if (t->op != GGML_OP_CONT || !t->src[0] || t->src[0]->op != GGML_OP_TRANSPOSE || t->type != GGML_TYPE_F32 || + t->ne[0] != C || t->ne[1] != T || t->ne[2] != 1 || t->ne[3] != 1 || !ggml_is_contiguous(t)) { + return -1; + } + const ggml_tensor * v = t->src[0]->src[0]; + if (!v || v->op != GGML_OP_VIEW || v->view_src != cc || v->ne[0] != T || v->ne[1] != C || v->ne[2] != 1 || + v->nb[1] != cc->nb[1] || v->view_offs % (dil * sizeof(float)) != 0) { + return -1; + } + const int64_t k = (int64_t) (v->view_offs / (dil * sizeof(float))); + return (k >= 0 && k < K) ? (int) k : -1; +} + +// the weight operand of a tap MUL: cast(F32) <- reshape <- CONT <- VIEW of column k of the F16 [K, C] weight +static const ggml_tensor * ple_weight_root(const ggml_tensor * wk, int64_t C, int k) { + if (wk->type != GGML_TYPE_F32 || ggml_nelements(wk) != C || wk->op != GGML_OP_CPY) { + return nullptr; + } + const ggml_tensor * src = wk->src[0]; + while (src && (src->op == GGML_OP_RESHAPE || src->op == GGML_OP_VIEW)) { + src = src->src[0]; + } + if (!src || src->op != GGML_OP_CONT) { + return nullptr; + } + const ggml_tensor * wv = src->src[0]; + if (!wv || wv->op != GGML_OP_VIEW || !wv->view_src) { + return nullptr; + } + const ggml_tensor * W = wv->view_src; + if (W->type != GGML_TYPE_F16 || W->ne[1] != C || W->ne[2] != 1 || W->ne[3] != 1 || !ggml_is_contiguous(W) || + wv->ne[0] != 1 || wv->ne[1] != C || wv->nb[1] != W->nb[1] || wv->view_offs != (size_t) k * sizeof(ggml_fp16_t)) { + return nullptr; + } + return W; +} + +static bool ple_conv_check(const ggml_cgraph * cgraph, int i, ggml_cuda_ple_conv_match & m) { + if (i < 0 || i + 2 >= cgraph->n_nodes) { + return ggml_cuda_conv_reject("ple_conv", "check 1", i); + } + const ggml_tensor * cc = cgraph->nodes[i]; + if (cc->op != GGML_OP_CONCAT || cc->type != GGML_TYPE_F32 || ggml_get_op_params_i32(cc, 0) != 0 || + (cc->flags & GGML_TENSOR_FLAG_OUTPUT) || !ggml_cuda_conv_next_is_noop(cgraph, i)) { + return ggml_cuda_conv_reject("ple_conv", "check 2", i); + } + const ggml_tensor * st = cc->src[0]; + const ggml_tensor * tr = cc->src[1]; + if (!st || !tr || st->type != GGML_TYPE_F32 || tr->type != GGML_TYPE_F32 || tr->op != GGML_OP_TRANSPOSE || !tr->view_src) { + return ggml_cuda_conv_reject("ple_conv", "check 3", i); + } + int64_t C = 0, T = 0; + const ggml_tensor * x = ggml_cuda_conv_activation(tr, C, T); + if (!x) { + return ggml_cuda_conv_reject("ple_conv", "check 4", i); + } + constexpr int K = 4, DIL = 3, H = (K - 1) * DIL; + if (st->ne[0] != H || st->ne[1] != C || st->ne[2] != 1 || st->ne[3] != 1 || !ggml_is_contiguous(st)) { + return ggml_cuda_conv_reject("ple_conv", "check 5", i); + } + if (tr->ne[0] != T || tr->ne[1] != C || tr->ne[2] != 1 || tr->view_offs != 0) { + return ggml_cuda_conv_reject("ple_conv", "check 6", i); + } + if (cc->ne[0] != T + H || cc->ne[1] != C || cc->ne[2] != 1 || cc->ne[3] != 1 || !ggml_is_contiguous(cc) || T < 256 || C % 256 != 0) { + return ggml_cuda_conv_reject("ple_conv", "check 7", i); + } + + // Every later reader of the concat must be a tail reader or one of the four taps. Between the first tap and the + // SiLU every node must belong to the chain, because the direct kernel replaces all of them. + int64_t tail_from = T + H; + int first_tap = -1, silu = -1; + int n_taps = 0; + const ggml_tensor * tap_out[K] = { nullptr, nullptr, nullptr, nullptr }; // latest value of tap k: CONT, then its MUL + const ggml_tensor * W = nullptr; + const ggml_tensor * chain = nullptr; // running ADD chain (starts at tap 0's MUL) + for (int n = i + 1; n < cgraph->n_nodes; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + const bool reads_cc = ggml_cuda_reads(t, cc); + if (silu >= 0) { + if (reads_cc && !ggml_cuda_conv_tail_reader(t, cc, H, tail_from)) { + return ggml_cuda_conv_reject("ple_conv", "check 8", n); + } + continue; + } + if (reads_cc) { + const int k = ple_tap_index(t, cc, C, T, DIL, K); + if (k >= 0) { + if (tap_out[k] != nullptr) { + return ggml_cuda_conv_reject("ple_conv", "check 9", n); + } + if (first_tap < 0) { + first_tap = n; + } + tap_out[k] = t; + ++n_taps; + continue; + } + if (t->op == GGML_OP_VIEW || t->op == GGML_OP_TRANSPOSE) { + // a tap view or its transpose (no-ops), or a tail view + if (t->op == GGML_OP_VIEW && ggml_cuda_conv_tail_view_col(t, cc, H) >= 0) { + tail_from = std::min(tail_from, ggml_cuda_conv_tail_view_col(t, cc, H)); + continue; + } + if (t->op == GGML_OP_VIEW && t->ne[0] == T && t->ne[1] == C && t->nb[1] == cc->nb[1] && t->view_offs % (DIL * sizeof(float)) == 0 && (int64_t) (t->view_offs / (DIL * sizeof(float))) < K) { + continue; + } + if (t->op == GGML_OP_TRANSPOSE && t->src[0] && t->src[0]->op == GGML_OP_VIEW && t->src[0]->view_src == cc && t->src[0]->ne[0] == T) { + continue; + } + return ggml_cuda_conv_reject("ple_conv", "check 10", n); + } + if (first_tap >= 0) { + return false; // a tail CONT/CPY inside the chain window would be skipped with it + } + if (!ggml_cuda_conv_tail_reader(t, cc, H, tail_from)) { + return ggml_cuda_conv_reject("ple_conv", "check 11", n); + } + continue; + } + if (first_tap < 0) { + continue; // unrelated work before the chain is computed normally + } + // inside the chain window: only the chain's own nodes + if (t->op == GGML_OP_MUL) { + const ggml_tensor * a = t->src[0]; + const ggml_tensor * b = t->src[1]; + int k = -1; + for (int q = 0; q < K; ++q) { + if (tap_out[q] && tap_out[q]->op == GGML_OP_CONT && (tap_out[q] == a || tap_out[q] == b)) { + k = q; + } + } + if (k < 0 || t->type != GGML_TYPE_F32 || !ggml_is_contiguous(t) || t->ne[0] != C || t->ne[1] != T || t->ne[2] != 1) { + return ggml_cuda_conv_reject("ple_conv", "check 12", n); + } + const ggml_tensor * Wk = ple_weight_root(tap_out[k] == a ? b : a, C, k); + if (!Wk || (W && W != Wk)) { + return ggml_cuda_conv_reject("ple_conv", "check 13", n); + } + W = Wk; + tap_out[k] = t; + if (k == 0) { + chain = t; + } + continue; + } + if (t->op == GGML_OP_ADD) { + if (!chain || t->src[0] != chain || t->type != GGML_TYPE_F32 || !ggml_is_contiguous(t)) { + return ggml_cuda_conv_reject("ple_conv", "check 14", n); + } + int k = -1; + for (int q = 1; q < K; ++q) { + if (tap_out[q] && tap_out[q]->op == GGML_OP_MUL && tap_out[q] == t->src[1]) { + k = q; + } + } + if (k < 0) { + return ggml_cuda_conv_reject("ple_conv", "check 15", n); + } + tap_out[k] = t; // consumed + chain = t; + continue; + } + if (t->op == GGML_OP_UNARY && ggml_get_unary_op(t) == GGML_UNARY_OP_SILU && chain && t->src[0] == chain) { + if (n_taps != K || !W || t->type != GGML_TYPE_F32 || !ggml_is_contiguous(t) || t->ne[0] != C || t->ne[1] != T) { + return ggml_cuda_conv_reject("ple_conv", "check 16", n); + } + silu = n; + continue; + } + if (t->op == GGML_OP_CONT || t->op == GGML_OP_RESHAPE || t->op == GGML_OP_VIEW || t->op == GGML_OP_CPY) { + // weight column plumbing: must not read the concat (checked above) and must lead to a tap MUL + continue; + } + return ggml_cuda_conv_reject("ple_conv", "check 17", n); + } + if (silu < 0 || first_tap < 0 || n_taps != K || !W) { + return ggml_cuda_conv_reject("ple_conv", "check 18", i); + } + // chain intermediates feed only the chain: the direct kernel never produces them + for (int n = first_tap; n < silu; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + if (t->op == GGML_OP_CONT || t->op == GGML_OP_MUL || t->op == GGML_OP_ADD || t->op == GGML_OP_CPY) { + if ((t->flags & GGML_TENSOR_FLAG_OUTPUT) || ggml_cuda_conv_readers(cgraph, n + 1, t, nullptr) != 1) { + return ggml_cuda_conv_reject("ple_conv", "chain intermediate has another reader", n); + } + } + } + m.concat_idx = i; m.first_tap_idx = first_tap; m.silu_idx = silu; + m.x = x; m.state = st; m.concat = cc; m.w = W; m.out = cgraph->nodes[silu]; + m.C = C; m.T = T; m.H = H; m.K = K; m.dil = DIL; m.tail_from = std::max<int64_t>(0, tail_from); + return true; +} + +bool ggml_cuda_ple_conv_match_at_concat(const ggml_cgraph * cgraph, int i, ggml_cuda_ple_conv_match & m) { + return ple_conv_check(cgraph, i, m); +} + +bool ggml_cuda_ple_conv_match_at_tap(const ggml_cgraph * cgraph, int i, ggml_cuda_ple_conv_match & m) { + const ggml_tensor * t = cgraph->nodes[i]; + if (t->op != GGML_OP_CONT || !t->src[0] || t->src[0]->op != GGML_OP_TRANSPOSE || !t->src[0]->src[0] || + t->src[0]->src[0]->op != GGML_OP_VIEW || !t->src[0]->src[0]->view_src) { + return false; + } + const ggml_tensor * cc = t->src[0]->src[0]->view_src; + if (cc->op != GGML_OP_CONCAT) { + return false; + } + for (int c = i - 1; c >= 0; --c) { + if (cgraph->nodes[c] == cc) { + return ple_conv_check(cgraph, c, m) && m.first_tap_idx == i; + } + } + return false; +} + +void ggml_cuda_ple_conv_write_tail(ggml_backend_cuda_context & ctx, const ggml_cuda_ple_conv_match & m) { + const int ncols = (int) (m.T + m.H - m.tail_from); + if (ncols <= 0) { + return; + } + const int64_t n = m.C * ncols; + ple_concat_tail<<<(unsigned) ((n + 255) / 256), 256, 0, ctx.stream()>>>((const float *) m.state->data, (const float *) m.x->data, + (float *) m.concat->data, (int) m.C, (int) m.T, (int) m.H, (int) m.tail_from, (int) (m.concat->nb[1] / sizeof(float))); + CUDA_CHECK(cudaGetLastError()); +} + +void ggml_cuda_ple_conv_direct(ggml_backend_cuda_context & ctx, const ggml_cuda_ple_conv_match & m) { + constexpr int TT = 128; + GGML_ASSERT(m.K == 4 && m.dil == 3); + dim3 grid((unsigned) (m.C / 256), (unsigned) ((m.T + TT - 1) / TT)); + ple_conv_kernel<4, 3, TT><<<grid, 256, 0, ctx.stream()>>>((const float *) m.state->data, (const float *) m.x->data, + (const half *) m.w->data, (float *) m.out->data, (int) m.C, (int) m.T); + CUDA_CHECK(cudaGetLastError()); +} diff --git a/ggml/src/ggml-cuda/ple-conv.cuh b/ggml/src/ggml-cuda/ple-conv.cuh new file mode 100644 index 000000000000..26af69b165fe --- /dev/null +++ b/ggml/src/ggml-cuda/ple-conv.cuh @@ -0,0 +1,19 @@ +#pragma once +#include "common.cuh" + +// qwen4exp PLE convolution: concat(history, x^T) -> four dilated taps (CONT of transposed views) x F16 weight +// columns -> ADD chain -> SiLU. On a match the concat writes only the columns its tail copies read and one +// kernel computes the SiLU output straight from the history and x. +struct ggml_cuda_ple_conv_match { + int concat_idx = -1, first_tap_idx = -1, silu_idx = -1; + const ggml_tensor * x = nullptr; // [C, T] F32 contiguous + const ggml_tensor * state = nullptr; // [H, C] F32 contiguous, H = (K-1)*dil + const ggml_tensor * concat = nullptr; // [T+H, C] + const ggml_tensor * w = nullptr; // [K, C] F16 + ggml_tensor * out = nullptr; // SiLU output [C, T] + int64_t C = 0, T = 0, H = 0, K = 0, dil = 0, tail_from = 0; +}; +bool ggml_cuda_ple_conv_match_at_concat(const ggml_cgraph * cgraph, int i, ggml_cuda_ple_conv_match & m); +bool ggml_cuda_ple_conv_match_at_tap(const ggml_cgraph * cgraph, int i, ggml_cuda_ple_conv_match & m); +void ggml_cuda_ple_conv_write_tail(ggml_backend_cuda_context & ctx, const ggml_cuda_ple_conv_match & m); +void ggml_cuda_ple_conv_direct(ggml_backend_cuda_context & ctx, const ggml_cuda_ple_conv_match & m); diff --git a/ggml/src/ggml-cuda/qsa-decode-wmma.cuh b/ggml/src/ggml-cuda/qsa-decode-wmma.cuh new file mode 100644 index 000000000000..fd0fc859197a --- /dev/null +++ b/ggml/src/ggml-cuda/qsa-decode-wmma.cuh @@ -0,0 +1,121 @@ +#pragma once + +typedef short qd_half16 __attribute__((ext_vector_type(16))); +typedef float qd_float8 __attribute__((ext_vector_type(8))); + +static __global__ __launch_bounds__(256) void qsa_decode_wmma_partial( + const char * q, const char * k, const char * v, const char * mask, const char * ids, + size_t q1, size_t q2, size_t k1, size_t k2, size_t v1, size_t v2, size_t m1, size_t i1, + int nk, int ns, int nh, int splits, float scale, float * partial) { +#if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) + NO_DEVICE_CODE; +#else + const int lane = threadIdx.x & 31, w = threadIdx.x >> 5, r = lane & 15, hi = lane >> 4; + const int kvh = blockIdx.x, query = blockIdx.y, split = blockIdx.z; + const int * ir = (const int *) (ids + query*i1); + const half * mr = mask ? (const half *) (mask + query*m1) : nullptr; + __shared__ union { + uint16_t query_stage[8][16][32]; + float4 scores[8][64]; + } shared; + __shared__ __align__(16) uint16_t probabilities[16][16]; + __shared__ float alpha_s[16]; + qd_half16 qf[2]; + const _Float16 hs = (_Float16) scale; + for (int j = lane; j < 16*32; j += 32) { + const int head = j/32, d = j%32; + const float value = head < 12 ? *(const float *) (q + query*q1 + (kvh*12+head)*q2 + (w*32+d)*4) : 0.0f; + shared.query_stage[w][head][d] = __builtin_bit_cast(uint16_t, (_Float16)value*hs); + } + __syncthreads(); +#pragma unroll + for (int t = 0; t < 2; ++t) { + const uint16_t * src = &shared.query_stage[w][r][t*16]; + qf[t] = __builtin_bit_cast(qd_half16, (uint4[2]){*(const uint4 *)src, *(const uint4 *)(src+8)}); + } + __syncthreads(); + qd_float8 output[2] = {}; + float maximum = -INFINITY, normalizer = 0.0f; + const int end = min(ns, split*64+64); + for (int start = split*64; start < end; start += 16) { + int keys[16]; +#pragma unroll + for (int j = 0; j < 16; ++j) { + int key = start+j < end ? ir[start+j] : -1; + if (key < 0 || key >= nk || (mr && __half2float(mr[key]) == -INFINITY)) { key = -1; } + keys[j] = key; + } + qd_float8 scores = {}; +#pragma unroll + for (int t = 0; t < 2; ++t) { + qd_half16 kf = {}; + const int key = keys[r]; + if (key >= 0 && key < nk) { + const uint16_t * src = (const uint16_t *) (k + key*k1 + kvh*k2) + 32*w+16*t; + kf = __builtin_bit_cast(qd_half16, (uint4[2]){*(const uint4 *)src, *(const uint4 *)(src+8)}); + } + scores = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(kf, qf[t], scores); + } + shared.scores[w][lane*2] = make_float4(scores[0],scores[1],scores[2],scores[3]); + shared.scores[w][lane*2+1] = make_float4(scores[4],scores[5],scores[6],scores[7]); + __syncthreads(); + if (w == 0) { + float score[8] = {}; +#pragma unroll + for (int sw = 0; sw < 8; ++sw) { + const float4 lo = shared.scores[sw][lane*2], high = shared.scores[sw][lane*2+1]; + score[0]+=lo.x;score[1]+=lo.y;score[2]+=lo.z;score[3]+=lo.w; + score[4]+=high.x;score[5]+=high.y;score[6]+=high.z;score[7]+=high.w; + } + float local = -INFINITY; +#pragma unroll + for (int e = 0; e < 8; ++e) { + const int key = keys[2*e+hi]; + if (key < 0 || key >= nk) { score[e] = -INFINITY; } + else if (mr) { score[e] += __half2float(mr[key]); } + local = fmaxf(local,score[e]); + } + local = fmaxf(local,__shfl_xor(local,16)); + const float next = fmaxf(maximum,local); + const float alpha = next == -INFINITY ? 1.0f : exp2f((maximum-next)*1.4426950408889634f); + float sum = 0.0f; +#pragma unroll + for (int e = 0; e < 8; ++e) { + const float p = next == -INFINITY ? 0.0f : exp2f((score[e]-next)*1.4426950408889634f); + sum += p; + probabilities[r][2*e+hi] = __builtin_bit_cast(uint16_t,(_Float16)p); + } + sum += __shfl_xor(sum,16); + normalizer = normalizer*alpha+sum; + maximum = next; + if (hi == 0) { alpha_s[r] = alpha; } + } + __syncthreads(); + const float alpha = alpha_s[r]; + const qd_half16 pf = __builtin_bit_cast(qd_half16, (uint4[2]){ + *(const uint4 *)&probabilities[r][0], *(const uint4 *)&probabilities[r][8]}); +#pragma unroll + for (int t = 0; t < 2; ++t) { + uint16_t values[16]; +#pragma unroll + for (int j = 0; j < 16; ++j) { + const int key = keys[j]; + values[j] = key >= 0 && key < nk ? *((const uint16_t *)(v+key*v1+kvh*v2)+32*w+16*t+r) : 0; + } + const qd_half16 vf = __builtin_bit_cast(qd_half16,values); +#pragma unroll + for (int e = 0; e < 8; ++e) { output[t][e] *= alpha; } + output[t] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(vf,pf,output[t]); + } + __syncthreads(); + } + if (r < 12) { + float * dst = partial + ((size_t(query)*nh+kvh*12+r)*splits+split)*258; +#pragma unroll + for (int t = 0; t < 2; ++t) +#pragma unroll + for (int e = 0; e < 8; ++e) { dst[32*w+16*t+2*e+hi] = output[t][e]; } + if (w == 0 && hi == 0) { dst[256] = maximum; dst[257] = normalizer; } + } +#endif +} diff --git a/ggml/src/ggml-cuda/qsa-decode.cu b/ggml/src/ggml-cuda/qsa-decode.cu new file mode 100644 index 000000000000..e0a7f7f62420 --- /dev/null +++ b/ggml/src/ggml-cuda/qsa-decode.cu @@ -0,0 +1,151 @@ +#include "qsa-decode.cuh" +#if defined(GGML_USE_HIP) +#include "qsa-decode-wmma.cuh" + +static __global__ __launch_bounds__(128) void qsa_decode_partial( + const char * q, const char * k, const char * v, const char * mask, const char * ids, + size_t q1, size_t q2, size_t k1, size_t k2, size_t v1, size_t v2, size_t m1, size_t i1, + int nk, int ns, int nh, int ratio, int splits, float scale, float * partial) { + const int lane = threadIdx.x % 32, warp = threadIdx.x / 32; + const int head = blockIdx.x, query = blockIdx.y, split = blockIdx.z; + const int kv_head = head / ratio; + const float * qr = (const float *) (q + query*q1 + head*q2); + const int * ir = (const int *) (ids + query*i1); + const half * mr = mask ? (const half *) (mask + query*m1) : nullptr; + float qv[8], acc[8] = {}; +#pragma unroll + for (int d = 0; d < 8; ++d) { qv[d] = qr[lane + d*32]; } + float mx = -INFINITY, sum = 0.0f; + const int begin = split*128, end = min(ns, begin + 128); + for (int j = begin + warp; j < end; j += 4) { + const int key = ir[j]; + if (key < 0 || key >= nk) { continue; } + const float bias = mr ? __half2float(mr[key]) : 0.0f; + if (bias == -INFINITY) { continue; } + const half * kr = (const half *) (k + key*k1 + kv_head*k2); + const half * vr = (const half *) (v + key*v1 + kv_head*v2); + float dot = 0.0f; +#pragma unroll + for (int d = 0; d < 8; ++d) { dot = fmaf(qv[d], __half2float(kr[lane + d*32]), dot); } +#pragma unroll + for (int delta = 16; delta > 0; delta >>= 1) { dot += __shfl_xor_sync(0xffffffff, dot, delta, 32); } + const float score = dot*scale + bias; + const float next = fmaxf(mx, score); + const float alpha = expf(mx - next), weight = expf(score - next); +#pragma unroll + for (int d = 0; d < 8; ++d) { acc[d] = acc[d]*alpha + weight*__half2float(vr[lane + d*32]); } + sum = sum*alpha + weight; + mx = next; + } + __shared__ float values[4][256]; + __shared__ float maxima[4], sums[4]; +#pragma unroll + for (int d = 0; d < 8; ++d) { values[warp][lane + d*32] = acc[d]; } + if (lane == 0) { maxima[warp] = mx; sums[warp] = sum; } + __syncthreads(); + float maximum = -INFINITY; +#pragma unroll + for (int w = 0; w < 4; ++w) { maximum = fmaxf(maximum, maxima[w]); } + float factors[4], total = 0.0f; +#pragma unroll + for (int w = 0; w < 4; ++w) { + factors[w] = sums[w] > 0.0f ? expf(maxima[w] - maximum) : 0.0f; + total += factors[w]*sums[w]; + } + float * dst = partial + ((size_t(query)*nh + head)*splits + split)*258; + for (int d = threadIdx.x; d < 256; d += 128) { + float value = 0.0f; +#pragma unroll + for (int w = 0; w < 4; ++w) { value += values[w][d]*factors[w]; } + dst[d] = value; + } + if (threadIdx.x == 0) { dst[256] = maximum; dst[257] = total; } +} + +static __global__ __launch_bounds__(256) void qsa_decode_merge( + const float * partial, float * out, int splits) { + const int row = blockIdx.x, d = threadIdx.x; + const float * src = partial + size_t(row)*splits*258; + float maximum = -INFINITY; + for (int s = 0; s < splits; ++s) { maximum = fmaxf(maximum, src[s*258 + 256]); } + float sum = 0.0f, value = 0.0f; + for (int s = 0; s < splits; ++s) { + const float count = src[s*258 + 257]; + const float factor = count > 0.0f ? expf(src[s*258 + 256] - maximum) : 0.0f; + value += src[s*258 + d]*factor; + sum += count*factor; + } + out[size_t(row)*256 + d] = sum > 0.0f ? value/sum : 0.0f; +} + +bool ggml_cuda_flash_attn_ext_qsa_decode_supported(ggml_backend_cuda_context & ctx, const ggml_tensor * dst) { + if (!GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[ctx.device].cc)) { return false; } + const auto * q = dst->src[0], * k = dst->src[1], * v = dst->src[2], * m = dst->src[3], * ids = dst->src[5]; + // the mask is optional: the launcher and both kernels take a null mask (visibility from the index rows alone) + if (!q || !k || !v || !ids || dst->src[4] || dst->src[6] || dst->src[7] || ggml_get_op_params_i32(dst, 4) != 0) { return false; } + float bias, cap; + memcpy(&bias, (const char *) dst->op_params + 4, 4); + memcpy(&cap, (const char *) dst->op_params + 8, 4); + if (bias != 0.0f || cap != 0.0f || q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || + v->type != GGML_TYPE_F16 || dst->type != GGML_TYPE_F32 || ids->type != GGML_TYPE_I32 || + q->ne[0] != 256 || k->ne[0] != 256 || v->ne[0] != 256 || q->ne[1] < 1 || q->ne[1] > 8 || + k->ne[1] < 1 || k->ne[1] > 262144 || v->ne[1] != k->ne[1] || + k->ne[2] < 1 || q->ne[2] % k->ne[2] || v->ne[2] != k->ne[2] || + q->ne[3] != 1 || k->ne[3] != 1 || v->ne[3] != 1 || + q->nb[0] != 4 || k->nb[0] != 2 || v->nb[0] != 2 || ids->nb[0] != 4 || + ids->ne[0] < 1 || ids->ne[0] > 2560 || ids->ne[1] < q->ne[1] || ids->ne[2] != 1 || ids->ne[3] != 1 || + !ggml_is_contiguous(dst)) { return false; } + return !m || (m->type == GGML_TYPE_F16 && m->nb[0] == 2 && m->ne[0] >= k->ne[1] && + m->ne[1] >= q->ne[1] && m->ne[2] == 1 && m->ne[3] == 1); +} + +static __global__ void qsa_decode_unique_ids(const char * input, size_t stride, int * output, int count, int keys) { + __shared__ int seen[8192]; + const int tid=threadIdx.x, row=blockIdx.x; + for (int i=tid;i<8192;i+=blockDim.x) seen[i]=-1; + __syncthreads(); + const auto * src=reinterpret_cast<const int *>(input+size_t(row)*stride); + int * dst=output+size_t(row)*count; + for (int j=tid;j<count;j+=blockDim.x) { + const int key=src[j]; + dst[j]=-1; + if (key<0 || key>=keys) continue; + unsigned slot=(unsigned(key)*2654435761u)&8191u; + while (true) { + const int old=atomicCAS(seen+slot,-1,key); + if (old==-1) { dst[j]=key; break; } + if (old==key) break; + slot=(slot+1)&8191u; + } + } +} + +void ggml_cuda_flash_attn_ext_qsa_decode(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + const auto * q = dst->src[0], * k = dst->src[1], * v = dst->src[2], * m = dst->src[3], * ids = dst->src[5]; + ggml_cuda_pool_alloc<int> unique_ids(ctx.pool(), size_t(ids->ne[0])*q->ne[1]); + qsa_decode_unique_ids<<<q->ne[1],256,0,ctx.stream()>>>((const char *)ids->data,ids->nb[1],unique_ids.get(),ids->ne[0],k->ne[1]); + const bool wmma = q->ne[2] == 12*k->ne[2] && k->nb[1]%16 == 0 && + k->nb[2]%16 == 0 && uintptr_t(k->data)%16 == 0; + const int ns = ids->ne[0], nq = q->ne[1], nh = q->ne[2], step = wmma ? 64 : 128; + const int splits = (ns + step - 1)/step; + float scale; + memcpy(&scale, dst->op_params, sizeof(scale)); + ggml_cuda_pool_alloc<float> partial(ctx.pool(), size_t(nq)*nh*splits*258); + if (wmma) { + qsa_decode_wmma_partial<<<dim3(k->ne[2], nq, splits), 256, 0, ctx.stream()>>>( + (const char *) q->data, (const char *) k->data, (const char *) v->data, + m ? (const char *) m->data : nullptr, (const char *) unique_ids.get(), + q->nb[1], q->nb[2], k->nb[1], k->nb[2], v->nb[1], v->nb[2], m ? m->nb[1] : 0, size_t(ns)*sizeof(int), + k->ne[1], ns, nh, splits, scale, partial.get()); + } else { + qsa_decode_partial<<<dim3(nh, nq, splits), 128, 0, ctx.stream()>>>( + (const char *) q->data, (const char *) k->data, (const char *) v->data, + m ? (const char *) m->data : nullptr, (const char *) unique_ids.get(), + q->nb[1], q->nb[2], k->nb[1], k->nb[2], v->nb[1], v->nb[2], m ? m->nb[1] : 0, size_t(ns)*sizeof(int), + k->ne[1], ns, nh, nh/k->ne[2], splits, scale, partial.get()); + } + qsa_decode_merge<<<nq*nh, 256, 0, ctx.stream()>>>(partial.get(), (float *) dst->data, splits); + CUDA_CHECK(cudaGetLastError()); +} + +#endif diff --git a/ggml/src/ggml-cuda/qsa-decode.cuh b/ggml/src/ggml-cuda/qsa-decode.cuh new file mode 100644 index 000000000000..f032adb52cc3 --- /dev/null +++ b/ggml/src/ggml-cuda/qsa-decode.cuh @@ -0,0 +1,4 @@ +#pragma once +#include "common.cuh" +bool ggml_cuda_flash_attn_ext_qsa_decode_supported(ggml_backend_cuda_context & ctx, const ggml_tensor * dst); +void ggml_cuda_flash_attn_ext_qsa_decode(ggml_backend_cuda_context & ctx, ggml_tensor * dst); diff --git a/ggml/src/ggml-cuda/qsa-prefill.cu b/ggml/src/ggml-cuda/qsa-prefill.cu new file mode 100644 index 000000000000..860878949f37 --- /dev/null +++ b/ggml/src/ggml-cuda/qsa-prefill.cu @@ -0,0 +1,495 @@ +#include "qsa-prefill.cuh" +#if defined(GGML_USE_HIP) +#include "common.cuh" +#include "qsa-prefill.cuh" +#include <cstdio> +#include <cstdlib> +#include <cstring> + +typedef short v16s __attribute__((ext_vector_type(16))); +typedef float v8f __attribute__((ext_vector_type(8))); + +#define QSA3_L2E 1.4426950408889634f +#define QSA3_G 4 + +static __device__ __forceinline__ uint16_t qsa3_f2h(const float f) { return __builtin_bit_cast(uint16_t, (_Float16) f); } +static __device__ __forceinline__ float qsa3_h2f(const uint16_t h) { return (float) __builtin_bit_cast(_Float16, h); } + +// union builder, kernel A: per query row, check that the (validity-transformed) row is non-decreasing; if not, rank-sort it into srow[q] and set sflag[q]. Sentinel 0x7FFFFFFF for invalid (-1 / >= nk) entries. +#define QSA3_SENT 0x7FFFFFFF +__global__ __launch_bounds__(256) void qsa3_rows_kernel( + const int * __restrict__ ids, const size_t i1, const int ns, const int nk, int * __restrict__ srow, int * __restrict__ sflag, + const char * mask, size_t m1) { + extern __shared__ int ent[]; + __shared__ int unsorted; + const int q = blockIdx.x, tid = threadIdx.x; + const int * row = reinterpret_cast<const int *>(reinterpret_cast<const char *>(ids) + (size_t) q * i1); + if (tid == 0) { unsorted = 0; } + __syncthreads(); + // without a mask the row itself is the visibility record: the graph only lists cells the query may see + // and writes -1 elsewhere (complete-block selection with tails) + const auto * mq = mask ? reinterpret_cast<const uint16_t *>(mask + (size_t) q * m1) : nullptr; + for (int j = tid; j < ns; j += 256) { + const int key = row[j]; + const bool valid = key >= 0 && key < nk; + const bool visible = valid && (!mq || mq[key] != 0xfc00u); + ent[j] = visible ? key : QSA3_SENT; + if (valid && !visible) { atomicOr(&unsorted, 1); } + } + __syncthreads(); + for (int j = tid; j + 1 < ns; j += 256) { + if (ent[j] > ent[j+1]) { atomicOr(&unsorted, 1); } + } + __syncthreads(); + if (unsorted) { + int * dst = srow + (size_t) q * ns; + for (int j = tid; j < ns; j += 256) { + const int e = ent[j]; + int rank = 0; + for (int k = 0; k < ns; ++k) { const int f = ent[k]; rank += (f < e) || (f == e && k < j); } + dst[rank] = e; + } + } + if (tid == 0) { sflag[q] = unsorted; } +} + +// kernel B: one workgroup (128 lanes) per group of 4 queries. Lane = key sub-range; the range boundaries are the quantiles of the first non-empty row (block aligned), so the work per lane is balanced for the real (sorted, similar) rows. Each lane merges the 4 sorted rows inside its range: pass 1 counts, block-wide prefix sum, pass 2 writes. +#define QSA3_MERGE_LANES 128 +static __device__ __forceinline__ int qsa3_get(const int * __restrict__ p, const int j, const int nk, const bool raw) { + const int key = p[j]; + return raw ? ((key >= 0 && key < nk) ? key : QSA3_SENT) : key; +} +__global__ __launch_bounds__(QSA3_MERGE_LANES) void qsa3_merge_kernel( + const int * __restrict__ ids, const size_t i1, const int n_q, const int ns, const int nk, + const int * __restrict__ srow, const int * __restrict__ sflag, + uint16_t * __restrict__ ublk, uint16_t * __restrict__ umask, int * __restrict__ ucount, const int cap) { + extern __shared__ int rows_s[]; + __shared__ int scan_s[QSA3_MERGE_LANES]; + const int lane = threadIdx.x; + const int g = blockIdx.x; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { + const int q = QSA3_G * g + qi; + int * dst = rows_s + qi * ns; + if (q < n_q) { + const bool rw = sflag[q] == 0; + const int * src = rw ? reinterpret_cast<const int *>(reinterpret_cast<const char *>(ids) + (size_t) q * i1) : srow + (size_t) q * ns; + for (int j = lane; j < ns; j += QSA3_MERGE_LANES) { dst[j] = qsa3_get(src, j, nk, rw); } + } else { + for (int j = lane; j < ns; j += QSA3_MERGE_LANES) { dst[j] = QSA3_SENT; } + } + } + __syncthreads(); + const int * rp[QSA3_G]; bool raw[QSA3_G]; int cnt[QSA3_G]; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { + raw[qi] = false; rp[qi] = rows_s + qi * ns; + int a = 0, b = ns; + while (a < b) { const int mid = (a + b) >> 1; if (rp[qi][mid] < QSA3_SENT) { a = mid + 1; } else { b = mid; } } + cnt[qi] = a; + } + int kmax = -1, ref = -1; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { + if (cnt[qi] > 0) { kmax = max(kmax, qsa3_get(rp[qi], cnt[qi] - 1, nk, raw[qi])); if (ref < 0) { ref = qi; } } + } + uint16_t * ob = ublk + (size_t) g * cap, * om = umask + (size_t) g * cap; + if (kmax < 0) { if (lane == 0) { ucount[g] = 0; } return; } + const int rc = cnt[ref]; + const int lo = lane == 0 ? 0 : (qsa3_get(rp[ref], (int) (((long long) rc * lane) / QSA3_MERGE_LANES), nk, raw[ref]) & ~3); + const int hi = lane == QSA3_MERGE_LANES - 1 ? ((kmax + 4) & ~3) + : (qsa3_get(rp[ref], (int) (((long long) rc * (lane + 1)) / QSA3_MERGE_LANES), nk, raw[ref]) & ~3); + int p[QSA3_G], pe[QSA3_G]; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { + int a = 0, b = cnt[qi]; + while (a < b) { const int mid = (a + b) >> 1; if (qsa3_get(rp[qi], mid, nk, raw[qi]) < lo) { a = mid + 1; } else { b = mid; } } + p[qi] = a; b = cnt[qi]; + while (a < b) { const int mid = (a + b) >> 1; if (qsa3_get(rp[qi], mid, nk, raw[qi]) < hi) { a = mid + 1; } else { b = mid; } } + pe[qi] = a; + } + int n = 0; + { + int pp[QSA3_G]; int head[QSA3_G]; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { pp[qi] = p[qi]; head[qi] = pp[qi] < pe[qi] ? qsa3_get(rp[qi], pp[qi], nk, raw[qi]) : QSA3_SENT; } + while (true) { + int b = QSA3_SENT; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { b = min(b, head[qi]); } + if (b == QSA3_SENT) { break; } + b >>= 2; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { + if (head[qi] != QSA3_SENT && (head[qi] >> 2) == b) { + const int * rr = rp[qi]; + int pn = pp[qi]; + if ((head[qi] & 3) == 0 && pn + 3 < pe[qi] && rr[pn+3] == head[qi] + 3 && rr[pn+1] == head[qi] + 1 && rr[pn+2] == head[qi] + 2) { pn += 4; } + else { do { ++pn; } while (pn < pe[qi] && (rr[pn] >> 2) == b); } + pp[qi] = pn; head[qi] = pn < pe[qi] ? rr[pn] : QSA3_SENT; + } + } + ++n; + } + } + scan_s[lane] = n; + __syncthreads(); + for (int off = 1; off < QSA3_MERGE_LANES; off <<= 1) { + const int v = lane >= off ? scan_s[lane - off] : 0; + __syncthreads(); + scan_s[lane] += v; + __syncthreads(); + } + const int total = scan_s[QSA3_MERGE_LANES - 1]; + int o = scan_s[lane] - n; + { + int head[QSA3_G]; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { head[qi] = p[qi] < pe[qi] ? qsa3_get(rp[qi], p[qi], nk, raw[qi]) : QSA3_SENT; } + while (true) { + int b = QSA3_SENT; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { b = min(b, head[qi]); } + if (b == QSA3_SENT) { break; } + b >>= 2; + uint32_t mk = 0; +#pragma unroll + for (int qi = 0; qi < QSA3_G; ++qi) { + if (head[qi] != QSA3_SENT && (head[qi] >> 2) == b) { + const int * rr = rp[qi]; + int pn = p[qi]; + if ((head[qi] & 3) == 0 && pn + 3 < pe[qi] && rr[pn+3] == head[qi] + 3 && rr[pn+1] == head[qi] + 1 && rr[pn+2] == head[qi] + 2) { mk |= 0xFu << (4*qi); pn += 4; } + else { do { mk |= 1u << (4*qi + (rr[pn] & 3)); ++pn; } while (pn < pe[qi] && (rr[pn] >> 2) == b); } + p[qi] = pn; head[qi] = pn < pe[qi] ? rr[pn] : QSA3_SENT; + } + } + ob[o] = (uint16_t) b; om[o] = (uint16_t) mk; ++o; + } + } + if (lane == QSA3_MERGE_LANES - 1) { + int t = total; + while (t & 3) { ob[t] = 0xFFFFu; om[t] = 0; ++t; } + ucount[g] = t; + } +} + +struct qsa3_layout { + size_t q1, q2, o1, o2, m1; + int nk, n_q, gqa, cap; + float scale; +}; + +__global__ __launch_bounds__(256) void qsa3_attn_kernel( + const float * __restrict__ q, const uint16_t * __restrict__ pk, const uint16_t * __restrict__ pv, + const uint16_t * __restrict__ mask, const uint16_t * __restrict__ ublk, const uint16_t * __restrict__ umask, + const int * __restrict__ ucount, float * __restrict__ out, const qsa3_layout s) { +#if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) + NO_DEVICE_CODE; +#else + const int tid = threadIdx.x, lane = tid & 31, w = tid >> 5, r = lane & 15, hi = lane >> 4; + const int g = blockIdx.x, kvh = blockIdx.y; + __shared__ float4 part[3][8][64]; + __shared__ uint4 ptile[3][32]; + __shared__ float alpha_s[48]; + __shared__ float l_s[48]; + + const size_t nblk = (size_t) s.nk / 4; + const uint16_t * pkg = pk + (size_t) kvh * nblk * 1024; + const uint16_t * pvg = pv + (size_t) kvh * nblk * 1024; + const uint16_t * gblk = ublk + (size_t) g * s.cap; + const uint16_t * gmsk = umask + (size_t) g * s.cap; + const int nchunks = ucount[g] >> 2; + + v16s qf[3][2]; + { + uint16_t * stage = reinterpret_cast<uint16_t *>(&part[0][0][0]) + w * (48 * 32); + const _Float16 hs = (_Float16) s.scale; +#pragma unroll + for (int c = 0; c < 12; ++c) { + const int row = 4*c + (lane >> 3), dq = 4 * (lane & 7); + const int qi = row / 12, h = row - 12*qi; + const int query = QSA3_G * g + qi; + float4 v = make_float4(0.f, 0.f, 0.f, 0.f); + if (query < s.n_q) { + v = *reinterpret_cast<const float4 *>(reinterpret_cast<const char *>(q) + (size_t) query * s.q1 + (size_t) (kvh * s.gqa + h) * s.q2 + (size_t) (32*w + dq) * 4); + } + const uint16_t h0 = __builtin_bit_cast(uint16_t, (_Float16) v.x * hs), h1 = __builtin_bit_cast(uint16_t, (_Float16) v.y * hs); + const uint16_t h2 = __builtin_bit_cast(uint16_t, (_Float16) v.z * hs), h3 = __builtin_bit_cast(uint16_t, (_Float16) v.w * hs); + *reinterpret_cast<uint2 *>(stage + row * 32 + dq) = make_uint2((uint32_t) h0 | ((uint32_t) h1 << 16), (uint32_t) h2 | ((uint32_t) h3 << 16)); + } + __syncthreads(); +#pragma unroll + for (int i = 0; i < 3; ++i) +#pragma unroll + for (int t = 0; t < 2; ++t) { + const uint16_t * src = stage + (16*i + r) * 32 + 16*t; + qf[i][t] = __builtin_bit_cast(v16s, (uint4[2]){*reinterpret_cast<const uint4 *>(src), *reinterpret_cast<const uint4 *>(src + 8)}); + } + __syncthreads(); + } + v8f O[3][2]; +#pragma unroll + for (int i = 0; i < 3; ++i) +#pragma unroll + for (int t = 0; t < 2; ++t) +#pragma unroll + for (int e = 0; e < 8; ++e) { O[i][t][e] = 0.f; } + float m = -INFINITY, l = 0.f; + const int own_row = 16*w + r, own_qi = own_row / 12; + const int own_query = QSA3_G * g + own_qi; + const uint16_t * maskq = (mask && w < 3 && own_query < s.n_q) + ? reinterpret_cast<const uint16_t *>(reinterpret_cast<const char *>(mask) + (size_t) own_query * s.m1) : nullptr; + + auto load_desc = [&](const int c, uint32_t & b01, uint32_t & b23, uint32_t & m01, uint32_t & m23) { + const uint2 bb = *reinterpret_cast<const uint2 *>(gblk + 4*c); + const uint2 mm = *reinterpret_cast<const uint2 *>(gmsk + 4*c); + b01 = bb.x; b23 = bb.y; m01 = mm.x; m23 = mm.y; + }; + auto blk_of = [&](const uint32_t b01, const uint32_t b23, const int j) -> int { + const uint32_t v = (j < 2 ? b01 : b23) >> (16 * (j & 1)) & 0xFFFFu; + return v == 0xFFFFu ? 0 : (int) v; + }; + auto load_k = [&](const uint32_t b01, const uint32_t b23, const uint32_t m01, const uint32_t m23, v16s * kf) { + const int j = r >> 2; + const uint32_t mk = ((j < 2 ? m01 : m23) >> (16 * (j & 1))) & 0xffffu; + if (((mk >> (r & 3)) & 0x1111u) == 0) { + kf[0] = v16s{}; + kf[1] = v16s{}; + return; + } + const int kb = blk_of(b01, b23, j); + const uint16_t * krow = pkg + (size_t) kb * 1024 + (r & 3) * 16 + w * 128; +#pragma unroll + for (int t = 0; t < 2; ++t) { + const uint4 lo = *reinterpret_cast<const uint4 *>(krow + t*64); + const uint4 hi4 = *reinterpret_cast<const uint4 *>(krow + t*64 + 8); + kf[t] = __builtin_bit_cast(v16s, (uint4[2]){lo, hi4}); + } + }; + + auto mask_values = [&](uint2 v, uint32_t mk) { + uint32_t lo = 0, hi = 0; + if (mk & 0x1111u) lo |= 0x0000ffffu; + if (mk & 0x2222u) lo |= 0xffff0000u; + if (mk & 0x4444u) hi |= 0x0000ffffu; + if (mk & 0x8888u) hi |= 0xffff0000u; + return make_uint2(v.x & lo, v.y & hi); + }; + + uint32_t b01 = 0, b23 = 0, m01 = 0, m23 = 0; + v16s kf[2]; + if (nchunks > 0) { load_desc(0, b01, b23, m01, m23); load_k(b01, b23, m01, m23, kf); } + + for (int c = 0; c < nchunks; ++c) { + v8f sc[3]; +#pragma unroll + for (int i = 0; i < 3; ++i) { +#pragma unroll + for (int e = 0; e < 8; ++e) { sc[i][e] = 0.f; } +#pragma unroll + for (int t = 0; t < 2; ++t) { sc[i] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(kf[t], qf[i][t], sc[i]); } + } + v16s vf[2]; + { + const int kb0 = blk_of(b01, b23, 0), kb1 = blk_of(b01, b23, 1), kb2 = blk_of(b01, b23, 2), kb3 = blk_of(b01, b23, 3); +#pragma unroll + for (int t = 0; t < 2; ++t) { + const int d = 32*w + 16*t + r; + const uint2 v0 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb0 * 1024 + d * 4), m01 & 0xffffu); + const uint2 v1 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb1 * 1024 + d * 4), m01 >> 16); + const uint2 v2 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb2 * 1024 + d * 4), m23 & 0xffffu); + const uint2 v3 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb3 * 1024 + d * 4), m23 >> 16); + vf[t] = __builtin_bit_cast(v16s, (uint2[4]){v0, v1, v2, v3}); + } + } + uint32_t nb01 = 0, nb23 = 0, nm01 = 0, nm23 = 0; + v16s kfn[2]; + if (c + 1 < nchunks) { load_desc(c + 1, nb01, nb23, nm01, nm23); load_k(nb01, nb23, nm01, nm23, kfn); } + else { kfn[0] = kf[0]; kfn[1] = kf[1]; } +#pragma unroll + for (int i = 0; i < 3; ++i) { + if (i != w) { + part[i][w][lane*2+0] = make_float4(sc[i][0], sc[i][1], sc[i][2], sc[i][3]); + part[i][w][lane*2+1] = make_float4(sc[i][4], sc[i][5], sc[i][6], sc[i][7]); + } + } + __syncthreads(); + if (w < 3) { + float scf[8]; +#pragma unroll + for (int e = 0; e < 8; ++e) { scf[e] = 0.f; } +#pragma unroll + for (int w2 = 0; w2 < 8; ++w2) { + if (w2 == w) { +#pragma unroll + for (int e = 0; e < 8; ++e) { scf[e] += sc[w][e]; } + } else { + const float4 a = part[w][w2][lane*2+0], b = part[w][w2][lane*2+1]; + scf[0] += a.x; scf[1] += a.y; scf[2] += a.z; scf[3] += a.w; scf[4] += b.x; scf[5] += b.y; scf[6] += b.z; scf[7] += b.w; + } + } +#pragma unroll + for (int e = 0; e < 8; ++e) { + const int j = e >> 1, cc = 2*(e & 1) + hi; + const uint32_t mk = ((j < 2 ? m01 : m23) >> (16 * (j & 1))) & 0xFFFFu; + const bool kv = (mk >> (own_qi * 4 + cc)) & 1u; + if (maskq && kv) { scf[e] += qsa3_h2f(maskq[4 * blk_of(b01, b23, j) + cc]); } + if (!kv) { scf[e] = -INFINITY; } + } + float mloc = scf[0]; +#pragma unroll + for (int e = 1; e < 8; ++e) { mloc = fmaxf(mloc, scf[e]); } + mloc = fmaxf(mloc, __shfl_xor(mloc, 16)); + const float mnew = fmaxf(m, mloc); + const float alpha = mnew == -INFINITY ? 1.f : exp2f((m - mnew) * QSA3_L2E); + float p[8], ls = 0.f; +#pragma unroll + for (int e = 0; e < 8; ++e) { p[e] = mnew == -INFINITY ? 0.f : exp2f((scf[e] - mnew) * QSA3_L2E); ls += p[e]; } + ls += __shfl_xor(ls, 16); + l = l * alpha + ls; + m = mnew; + uint32_t pp[4], po[4]; +#pragma unroll + for (int e2 = 0; e2 < 4; ++e2) { + pp[e2] = (uint32_t) qsa3_f2h(p[2*e2]) | ((uint32_t) qsa3_f2h(p[2*e2+1]) << 16); + po[e2] = (uint32_t) __shfl_xor((int) pp[e2], 16); + } + uint16_t ph[16]; +#pragma unroll + for (int e = 0; e < 8; ++e) { + const uint16_t mine = (e & 1) ? (uint16_t) (pp[e>>1] >> 16) : (uint16_t) (pp[e>>1] & 0xffffu); + const uint16_t other = (e & 1) ? (uint16_t) (po[e>>1] >> 16) : (uint16_t) (po[e>>1] & 0xffffu); + ph[2*e] = hi ? other : mine; + ph[2*e+1] = hi ? mine : other; + } + if (hi == 0) { + ptile[w][r*2+0] = make_uint4((uint32_t) ph[0] | ((uint32_t) ph[1] << 16), (uint32_t) ph[2] | ((uint32_t) ph[3] << 16), + (uint32_t) ph[4] | ((uint32_t) ph[5] << 16), (uint32_t) ph[6] | ((uint32_t) ph[7] << 16)); + ptile[w][r*2+1] = make_uint4((uint32_t) ph[8] | ((uint32_t) ph[9] << 16), (uint32_t) ph[10] | ((uint32_t) ph[11] << 16), + (uint32_t) ph[12] | ((uint32_t) ph[13] << 16), (uint32_t) ph[14] | ((uint32_t) ph[15] << 16)); + alpha_s[own_row] = alpha; + } + } + __syncthreads(); + float al[3]; +#pragma unroll + for (int i = 0; i < 3; ++i) { al[i] = alpha_s[16*i + r]; } + if (__ballot(al[0] != 1.f || al[1] != 1.f || al[2] != 1.f)) { +#pragma unroll + for (int i = 0; i < 3; ++i) +#pragma unroll + for (int t = 0; t < 2; ++t) +#pragma unroll + for (int e = 0; e < 8; ++e) { O[i][t][e] *= al[i]; } + } +#pragma unroll + for (int i = 0; i < 3; ++i) { + const v16s pf = __builtin_bit_cast(v16s, (uint4[2]){ptile[i][r*2+0], ptile[i][r*2+1]}); +#pragma unroll + for (int t = 0; t < 2; ++t) { O[i][t] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(vf[t], pf, O[i][t]); } + } + kf[0] = kfn[0]; kf[1] = kfn[1]; + b01 = nb01; b23 = nb23; m01 = nm01; m23 = nm23; + } + + if (w < 3 && hi == 0) { l_s[own_row] = l; } + __syncthreads(); + float * ostage = reinterpret_cast<float *>(&part[0][0][0]) + w * (16 * 32); +#pragma unroll + for (int i = 0; i < 3; ++i) { + const float li = l_s[16*i + r]; +#pragma unroll + for (int t = 0; t < 2; ++t) +#pragma unroll + for (int e = 0; e < 8; ++e) { ostage[r * 32 + 16*t + 2*e + hi] = li > 0.f ? O[i][t][e] / li : 0.f; } + __syncthreads(); +#pragma unroll + for (int c = 0; c < 4; ++c) { + const int rr = 4*c + (lane >> 3), dq = 4 * (lane & 7); + const int row = 16*i + rr, qi = row / 12, h = row - 12*qi; + const int query = QSA3_G * g + qi; + if (query < s.n_q) { + float * dst = reinterpret_cast<float *>(reinterpret_cast<char *>(out) + (size_t) query * s.o2 + (size_t) (kvh * s.gqa + h) * s.o1) + 32*w + dq; + *reinterpret_cast<float4 *>(dst) = *reinterpret_cast<const float4 *>(ostage + rr * 32 + dq); + } + } + __syncthreads(); + } +#endif +} + +bool ggml_cuda_flash_attn_ext_qsa_prefill_supported(ggml_backend_cuda_context & ctx, const ggml_tensor * dst) { + const auto * q=dst->src[0], * k=dst->src[1], * v=dst->src[2], * m=dst->src[3], * ids=dst->src[5]; + if (!q || !k || !v || !ids || dst->src[4] || ggml_get_op_params_i32(dst, 4) != 0 || + !GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[ctx.device].cc)) return false; + float bias, softcap; memcpy(&bias, (const char *) dst->op_params+4, 4); memcpy(&softcap, (const char *) dst->op_params+8, 4); + // the mask is optional: a maskless op relies on the index rows naming only visible cells (-1 elsewhere) + if (m && (m->type!=GGML_TYPE_F16 || m->nb[0]!=2 || m->ne[0]<k->ne[1] || m->ne[1]<q->ne[1] || m->ne[2]!=1 || m->ne[3]!=1)) return false; + return bias==0 && softcap==0 && q->type==GGML_TYPE_F32 && k->type==GGML_TYPE_F16 && v->type==GGML_TYPE_F16 && + dst->type==GGML_TYPE_F32 && ids->type==GGML_TYPE_I32 && + q->ne[0]==256 && k->ne[0]==256 && v->ne[0]==256 && q->ne[1]>=128 && q->ne[1]<=INT_MAX && + k->ne[1]>0 && k->ne[1]<=262140 && k->ne[1]%4==0 && v->ne[1]==k->ne[1] && + k->ne[2]>0 && k->ne[2]<=65535 && q->ne[2]==12*k->ne[2] && v->ne[2]==k->ne[2] && + q->ne[3]==1 && k->ne[3]==1 && v->ne[3]==1 && ggml_nelements(k)/4<=INT_MAX && + q->nb[0]==4 && k->nb[0]==2 && v->nb[0]==2 && ids->nb[0]==4 && + q->nb[1]%16==0 && q->nb[2]%16==0 && uintptr_t(q->data)%16==0 && + k->nb[1]%16==0 && k->nb[2]%16==0 && uintptr_t(k->data)%16==0 && + ids->nb[1]%4==0 && uintptr_t(ids->data)%4==0 && ids->ne[0]>0 && ids->ne[0]<=2560 && + ids->ne[1]>=q->ne[1] && ids->ne[2]==1 && ids->ne[3]==1 && ggml_is_contiguous(dst); +} + +static __global__ void qsa_pack_k(const char * src, uint16_t * dst, size_t nb1, size_t nb2, int nk, uint32_t count) { + const uint32_t i=blockIdx.x*blockDim.x+threadIdx.x; + if (i>=count) return; + const uint32_t d=(i%32)*8, key=(i/32)%nk, head=i/(uint32_t(nk)*32); + const size_t group=size_t(head)*(nk/4)+key/4; + const auto * in=reinterpret_cast<const uint4 *>(src+size_t(head)*nb2+size_t(key)*nb1+d*2); + auto * out=reinterpret_cast<uint4 *>(dst+group*1024+(d/16)*64+(key%4)*16+d%16); + *out=*in; +} + +static __global__ void qsa_pack_v(const char * src, uint16_t * dst, size_t nb1, size_t nb2, int nk, uint32_t count) { + const uint32_t i=blockIdx.x*blockDim.x+threadIdx.x; + if (i>=count) return; + const uint32_t d=i%256, group=i/256, head=group/(nk/4), key=(group%(nk/4))*4; + const char * in=src+size_t(head)*nb2+size_t(key)*nb1+d*2; + *reinterpret_cast<ushort4 *>(dst+size_t(group)*1024+d*4)=make_ushort4( + *reinterpret_cast<const uint16_t *>(in), *reinterpret_cast<const uint16_t *>(in+nb1), + *reinterpret_cast<const uint16_t *>(in+2*nb1), *reinterpret_cast<const uint16_t *>(in+3*nb1)); +} + +void ggml_cuda_flash_attn_ext_qsa_prefill(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + const auto * q=dst->src[0], * k=dst->src[1], * v=dst->src[2], * m=dst->src[3], * ids=dst->src[5]; + float scale; memcpy(&scale, dst->op_params, 4); + const int n_q = (int) q->ne[1], ns = (int) ids->ne[0], nk = (int) k->ne[1]; + ggml_cuda_pool_alloc<uint16_t> packed_k(ctx.pool(), ggml_nelements(k)); + ggml_cuda_pool_alloc<uint16_t> packed_v(ctx.pool(), ggml_nelements(v)); + const uint32_t count_k=ggml_nelements(k)/8, count_v=ggml_nelements(v)/4; + qsa_pack_k<<<(count_k+255)/256,256,0,ctx.stream()>>>((const char *)k->data,packed_k.get(),k->nb[1],k->nb[2],nk,count_k); + qsa_pack_v<<<(count_v+255)/256,256,0,ctx.stream()>>>((const char *)v->data,packed_v.get(),v->nb[1],v->nb[2],nk,count_v); + CUDA_CHECK(cudaGetLastError()); + const int ngroups = (n_q + QSA3_G - 1) / QSA3_G; + const int cap = (QSA3_G * ns + 3) & ~3; + ggml_cuda_pool_alloc<uint16_t> ublk(ctx.pool(), (size_t) ngroups * cap); + ggml_cuda_pool_alloc<uint16_t> umask(ctx.pool(), (size_t) ngroups * cap); + ggml_cuda_pool_alloc<int> ucount(ctx.pool(), (size_t) ngroups); + ggml_cuda_pool_alloc<int> srow(ctx.pool(), (size_t) n_q * ns); + ggml_cuda_pool_alloc<int> sflag(ctx.pool(), (size_t) n_q); + { + const ggml_cuda_kernel_launch_params launch(dim3(n_q), dim3(256), (size_t) ns * sizeof(int), ctx.stream()); + ggml_cuda_kernel_launch(qsa3_rows_kernel, launch, (const int *) ids->data, ids->nb[1], ns, nk, srow.get(), sflag.get(), m ? (const char *)m->data : nullptr, m ? m->nb[1] : 0); + CUDA_CHECK(cudaGetLastError()); + const ggml_cuda_kernel_launch_params launch2(dim3(ngroups), dim3(QSA3_MERGE_LANES), (size_t) QSA3_G * ns * sizeof(int), ctx.stream()); + ggml_cuda_kernel_launch(qsa3_merge_kernel, launch2, (const int *) ids->data, ids->nb[1], n_q, ns, nk, + (const int *) srow.get(), (const int *) sflag.get(), ublk.get(), umask.get(), ucount.get(), cap); + CUDA_CHECK(cudaGetLastError()); + } + qsa3_layout layout{q->nb[1], q->nb[2], dst->nb[1], dst->nb[2], m ? m->nb[1] : 0, nk, n_q, 12, cap, scale}; + const ggml_cuda_kernel_launch_params launch(dim3(ngroups, k->ne[2]), dim3(256), 0, ctx.stream()); + ggml_cuda_kernel_launch(qsa3_attn_kernel, launch, (const float *) q->data, packed_k.get(), packed_v.get(), + m ? (const uint16_t *) m->data : nullptr, (const uint16_t *) ublk.get(), (const uint16_t *) umask.get(), + (const int *) ucount.get(), (float *) dst->data, layout); + CUDA_CHECK(cudaGetLastError()); +} + +#endif diff --git a/ggml/src/ggml-cuda/qsa-prefill.cuh b/ggml/src/ggml-cuda/qsa-prefill.cuh new file mode 100644 index 000000000000..d9eecc50ac62 --- /dev/null +++ b/ggml/src/ggml-cuda/qsa-prefill.cuh @@ -0,0 +1,4 @@ +#pragma once +#include "common.cuh" +bool ggml_cuda_flash_attn_ext_qsa_prefill_supported(ggml_backend_cuda_context & ctx, const ggml_tensor * dst); +void ggml_cuda_flash_attn_ext_qsa_prefill(ggml_backend_cuda_context & ctx, ggml_tensor * dst); diff --git a/ggml/src/ggml-cuda/top-k.cu b/ggml/src/ggml-cuda/top-k.cu index 4f41e071f3b2..f03820a569bf 100644 --- a/ggml/src/ggml-cuda/top-k.cu +++ b/ggml/src/ggml-cuda/top-k.cu @@ -53,7 +53,7 @@ static int next_power_of_2(int x) { #endif // CUB_TOP_K_AVAILABLE -#if !defined(GGML_CUDA_USE_CUB) && defined(GGML_USE_HIP) +#if defined(GGML_USE_HIP) static __device__ __forceinline__ uint32_t top_k_float_to_ordered(float value) { const uint32_t bits = __float_as_uint(value); @@ -61,6 +61,40 @@ static __device__ __forceinline__ uint32_t top_k_float_to_ordered(float value) { return bits ^ mask; } +template<int BLOCK_SIZE> +static __device__ void top_k_gather_equal( + const float * src, int * dst, int ncols, uint32_t threshold, int limit, int offset) { + const int tid = threadIdx.x; + const int lane = tid % warpSize; + const int warp = tid / warpSize; + const int nwarps = BLOCK_SIZE / warpSize; + __shared__ int warp_counts[32]; + int count = 0; + + for (int base = 0; base < ncols && count < limit; base += BLOCK_SIZE) { + const int col = base + tid; + const bool equal = col < ncols && top_k_float_to_ordered(src[col]) == threshold; + const unsigned long long mask = __ballot(equal); + if (lane == 0) { + warp_counts[warp] = __popcll(mask); + } + __syncthreads(); + int before = count; + for (int w = 0; w < nwarps; ++w) { + if (w < warp) { + before += warp_counts[w]; + } + count += warp_counts[w]; + } + const unsigned long long lane_mask = (1ULL << lane) - 1; + const int pos = before + __popcll(mask & lane_mask); + if (equal && pos < limit) { + dst[offset + pos] = col; + } + __syncthreads(); + } +} + struct top_k_radix_state { uint32_t prefix; uint32_t prefix_mask; @@ -151,7 +185,7 @@ static __global__ void top_k_radix_reset_counters(top_k_radix_state * states, in } } -template<int BLOCK_SIZE> +template<int BLOCK_SIZE, bool STABLE_TIES = false> static __global__ void top_k_radix_gather( const float * __restrict__ src, int * __restrict__ dst, @@ -173,18 +207,29 @@ static __global__ void top_k_radix_gather( if (key > state->prefix) { const int pos = atomicAdd(&state->greater_count, 1); row_dst[pos] = col; - } else if (key == state->prefix) { - const int pos = atomicAdd(&state->equal_count, 1); - if (pos < state->rank) { - row_dst[k - state->rank + pos] = col; + } else if constexpr (!STABLE_TIES) { + if (key == state->prefix) { + const int pos = atomicAdd(&state->equal_count, 1); + if (pos < state->rank) { + row_dst[k - state->rank + pos] = col; + } } } } } +template<int BLOCK_SIZE> +static __global__ void top_k_radix_gather_equal( + const float * src, int * dst, const top_k_radix_state * states, int ncols, int k) { + const int row = blockIdx.x; + const top_k_radix_state state = states[row]; + top_k_gather_equal<BLOCK_SIZE>(src + (size_t) row*ncols, dst + (size_t) row*k, + ncols, state.prefix, state.rank, k - state.rank); +} + static void top_k_radix_cuda( ggml_cuda_pool & pool, - const float * src, int * dst, int ncols, int nrows, int k, cudaStream_t stream) { + const float * src, int * dst, int ncols, int nrows, int k, cudaStream_t stream, bool stable_ties = false) { constexpr int BLOCK_SIZE = 256; constexpr int RADIX_BITS = 8; constexpr int NBINS = 1 << RADIX_BITS; @@ -208,12 +253,18 @@ static void top_k_radix_cuda( top_k_radix_reset_counters <<<(nrows + BLOCK_SIZE - 1) / BLOCK_SIZE, BLOCK_SIZE, 0, stream>>>(states, nrows); - top_k_radix_gather<BLOCK_SIZE> - <<<row_grid, BLOCK_SIZE, 0, stream>>>( - src, dst, states, ncols, k, blocks_per_row); + if (stable_ties) { + top_k_radix_gather<BLOCK_SIZE, true> + <<<row_grid, BLOCK_SIZE, 0, stream>>>(src, dst, states, ncols, k, blocks_per_row); + top_k_radix_gather_equal<BLOCK_SIZE> + <<<nrows, BLOCK_SIZE, 0, stream>>>(src, dst, states, ncols, k); + } else { + top_k_radix_gather<BLOCK_SIZE> + <<<row_grid, BLOCK_SIZE, 0, stream>>>(src, dst, states, ncols, k, blocks_per_row); + } } -#endif // !defined(GGML_CUDA_USE_CUB) && defined(GGML_USE_HIP) +#endif // defined(GGML_USE_HIP) void ggml_cuda_op_top_k(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { const ggml_tensor * src0 = dst->src[0]; @@ -230,6 +281,16 @@ void ggml_cuda_op_top_k(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { const int64_t nrows = ggml_nrows(src0); const int64_t k = dst->ne[0]; ggml_cuda_pool & pool = ctx.pool(); +#if defined(GGML_USE_HIP) + // gfx1151 measurements (2026-09-14) favor radix by 1.2-5.1x above these cutoffs. + // Recheck the cutoffs when hipCUB changes; smaller batches favor its sort path. + const bool use_radix = ncols >= 8192 || (ncols >= 4096 && nrows >= 128) || (ncols >= 2048 && nrows >= 512); + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[ctx.device].cc) && use_radix && + ncols <= INT_MAX && nrows > 1 && nrows <= INT_MAX && k <= INT_MAX) { + top_k_radix_cuda(pool, src0_d, dst_d, ncols, nrows, k, stream, true); + return; + } +#endif #ifdef CUB_TOP_K_AVAILABLE // TODO: Switch to `DeviceSegmentedTopK` for multi-row TopK once implemented // https://github.com/NVIDIA/cccl/issues/6391 diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index 56f7d864fc21..a6c0d9f43cf5 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -1184,6 +1184,9 @@ class MODEL_TENSOR(IntEnum): NEXTN_HNORM = auto() NEXTN_SHARED_HEAD_HEAD = auto() NEXTN_SHARED_HEAD_NORM = auto() + NEXTN_HC_HEAD_NORM = auto() + NEXTN_HC_HEAD_DOWN = auto() + NEXTN_HC_HEAD_UP = auto() # eagle3 FC = auto() # feature fusion layer D2T = auto() # draft to target vocabulary mapping @@ -1967,6 +1970,9 @@ class MODEL_TENSOR(IntEnum): MODEL_TENSOR.NEXTN_HNORM: "blk.{bid}.nextn.hnorm", MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD: "blk.{bid}.nextn.shared_head_head", MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM: "blk.{bid}.nextn.shared_head_norm", + MODEL_TENSOR.NEXTN_HC_HEAD_NORM: "blk.{bid}.nextn.hc_head_norm", + MODEL_TENSOR.NEXTN_HC_HEAD_DOWN: "blk.{bid}.nextn.hc_head_down", + MODEL_TENSOR.NEXTN_HC_HEAD_UP: "blk.{bid}.nextn.hc_head_up", MODEL_TENSOR.FC: "fc", MODEL_TENSOR.DSPARK_MARKOV_W1: "markov_w1", MODEL_TENSOR.DSPARK_MARKOV_W2: "markov_w2", @@ -2954,6 +2960,14 @@ class MODEL_TENSOR(IntEnum): MODEL_TENSOR.PLE_NORM_QUERY, MODEL_TENSOR.PLE_NORM_CONV, MODEL_TENSOR.PLE_CONV1D, + MODEL_TENSOR.NEXTN_EH_PROJ, + MODEL_TENSOR.NEXTN_EMBED_TOKENS, + MODEL_TENSOR.NEXTN_ENORM, + MODEL_TENSOR.NEXTN_HNORM, + MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD, + MODEL_TENSOR.NEXTN_HC_HEAD_NORM, + MODEL_TENSOR.NEXTN_HC_HEAD_DOWN, + MODEL_TENSOR.NEXTN_HC_HEAD_UP, ], MODEL_ARCH.PLAMO: [ MODEL_TENSOR.TOKEN_EMBD, diff --git a/gguf-py/gguf/tensor_mapping.py b/gguf-py/gguf/tensor_mapping.py index d2dfeece5952..173f1a9cf453 100644 --- a/gguf-py/gguf/tensor_mapping.py +++ b/gguf-py/gguf/tensor_mapping.py @@ -2803,6 +2803,15 @@ class TensorNameMap: MODEL_TENSOR.HC_HEAD_UP: ( "model.hyper_connection_mixer.input_mix_weight_up", ), + MODEL_TENSOR.NEXTN_HC_HEAD_NORM: ( + "model.layers.{bid}.hyper_connection_mixer.hc_norm", + ), + MODEL_TENSOR.NEXTN_HC_HEAD_DOWN: ( + "model.layers.{bid}.hyper_connection_mixer.input_mix_weight_down", + ), + MODEL_TENSOR.NEXTN_HC_HEAD_UP: ( + "model.layers.{bid}.hyper_connection_mixer.input_mix_weight_up", + ), MODEL_TENSOR.INDEXER_Q_NORM: ( "model.layers.{bid}.self_attn.indexer.q_layernorm", ), diff --git a/include/llama.h b/include/llama.h index 429fc1cb8480..74c26e0e9dcd 100644 --- a/include/llama.h +++ b/include/llama.h @@ -236,6 +236,7 @@ extern "C" { LLAMA_LAZY_MODE_OFF = 0, // always read the whole tensor up front LLAMA_LAZY_MODE_AUTO = 1, // lazy only for marked tensors larger than 4 GiB (requires mmap) LLAMA_LAZY_MODE_ON = 2, // read the rows of tensors marked by the arch on demand (requires mmap) + LLAMA_LAZY_MODE_DIRECT = 3, // like on, but the arch reads the rows with explicit pread()s instead of demand paging the mmap (currently only PLE tables) }; enum llama_context_type { diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp index b5efb7206565..c5e06fe28993 100644 --- a/src/llama-arch.cpp +++ b/src/llama-arch.cpp @@ -578,6 +578,9 @@ static const std::map<llm_tensor, const char *> LLM_TENSOR_NAMES = { { LLM_TENSOR_NEXTN_HNORM, "blk.%d.nextn.hnorm" }, { LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, "blk.%d.nextn.shared_head_head" }, { LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, "blk.%d.nextn.shared_head_norm" }, + { LLM_TENSOR_NEXTN_HC_HEAD_NORM, "blk.%d.nextn.hc_head_norm" }, + { LLM_TENSOR_NEXTN_HC_HEAD_DOWN, "blk.%d.nextn.hc_head_down" }, + { LLM_TENSOR_NEXTN_HC_HEAD_UP, "blk.%d.nextn.hc_head_up" }, { LLM_TENSOR_ATTN_SUB_NORM, "blk.%d.attn_sub_norm" }, { LLM_TENSOR_FFN_SUB_NORM, "blk.%d.ffn_sub_norm" }, { LLM_TENSOR_DEC_OUTPUT_NORM, "dec.output_norm" }, @@ -967,6 +970,9 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = { {LLM_TENSOR_NEXTN_HNORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, {LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, {LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, + {LLM_TENSOR_NEXTN_HC_HEAD_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, + {LLM_TENSOR_NEXTN_HC_HEAD_DOWN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_NEXTN_HC_HEAD_UP, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, // Nemotron 3 Super // latent projections feed ggml_mul_mat, the buft probe must use MUL_MAT to keep them on GPU {LLM_TENSOR_FFN_LATENT_DOWN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, diff --git a/src/llama-arch.h b/src/llama-arch.h index f1d173a57556..b8d15e7edd51 100644 --- a/src/llama-arch.h +++ b/src/llama-arch.h @@ -690,6 +690,9 @@ enum llm_tensor { LLM_TENSOR_NEXTN_HNORM, LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, + LLM_TENSOR_NEXTN_HC_HEAD_NORM, + LLM_TENSOR_NEXTN_HC_HEAD_DOWN, + LLM_TENSOR_NEXTN_HC_HEAD_UP, LLM_TENSOR_MASKED_EMBD_CENTROIDS, LLM_TENSOR_MASKED_EMBD_ORDERING, LLM_TENSOR_FC, diff --git a/src/llama-context.cpp b/src/llama-context.cpp index 5fee486c172d..ccfd52175747 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -7,6 +7,7 @@ #include "llama-batch.h" #include "llama-io.h" #include "llama-memory.h" +#include "llama-memory-hybrid-idx.h" #include "llama-mmap.h" #include "llama-model.h" #include "llama-ext.h" @@ -153,7 +154,8 @@ llama_context::llama_context( cparams.ctx_other = params.ctx_other; } - if (model.arch == LLM_ARCH_EAGLE3 || model.arch == LLM_ARCH_DFLASH) { + // a qwen4exp draft head shipped without its embeddings and LM head borrows them from the target context + if (model.arch == LLM_ARCH_EAGLE3 || model.arch == LLM_ARCH_DFLASH || model.arch == LLM_ARCH_QWEN4EXP) { if (model.tok_embd == nullptr || model.output == nullptr) { if (params.ctx_other == nullptr) { throw std::runtime_error(model.arch_name() + " requires ctx_other to be set (this warning is normal during memory fitting)"); @@ -1737,6 +1739,12 @@ int llama_context::decode(const llama_batch & batch_inp) { } const uint32_t n_tokens_all = balloc->get_n_tokens(); + + { // warm the page cache for this batch's per-layer-embedding rows while the first chunk is on the GPU; + // posix_fadvise only, so a wrong prediction costs readahead and nothing else + extern void qwen4exp_ple_prefetch(const llama_model & model, const llama_token * tokens, int32_t n_tokens); + if (batch_inp.token && batch_inp.n_tokens >= 4096) { qwen4exp_ple_prefetch(model, batch_inp.token, batch_inp.n_tokens); } + } const uint32_t n_outputs_all = balloc->get_n_outputs(); if (output_all) { @@ -2539,6 +2547,7 @@ ggml_status llama_context::graph_compute( auto status = ggml_backend_sched_graph_compute_async(sched.get(), gf); if (status != GGML_STATUS_SUCCESS) { + if (auto * qsa = dynamic_cast<llama_memory_hybrid_idx *>(memory.get())) { qsa->qsa_invalidate(); } LLAMA_LOG_ERROR("%s: ggml_backend_sched_graph_compute_async failed with error %d\n", __func__, status); } diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index 010c40bcc500..85ed7f345afb 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -332,7 +332,9 @@ void llm_graph_input_rs::set_input(const llama_ubatch * ubatch) { const int64_t n_rs = mctx->get_n_rs(); - if (s_copy) { + // a graph with no recurrent layers (the qwen4exp draft block on a hybrid-idx memory) never consumes the copy + // map, so the scheduler leaves it unallocated + if (s_copy && s_copy->buffer) { llama_host_write(s_copy); int32_t * data = (int32_t *) s_copy->data; @@ -475,7 +477,10 @@ void llm_graph_input_attn_kv::set_input(const llama_ubatch * ubatch) { // the mask is left unallocated when the graph only stores K/V without attending // (e.g. DFlash's KV-injection pass) if (self_kq_mask && self_kq_mask->buffer) { - mctx->set_input_kq_mask(self_kq_mask, ubatch, cparams.causal_attn); + // a graph that consumes no mask (maskless selected-key attention) leaves it unallocated + if (self_kq_mask && self_kq_mask->buffer) { + mctx->set_input_kq_mask(self_kq_mask, ubatch, cparams.causal_attn); + } } if (self_k_rot && self_k_rot->buffer) { @@ -505,7 +510,10 @@ bool llm_graph_input_attn_kv::can_reuse(const llm_graph_params & params) { void llm_graph_input_attn_k::set_input(const llama_ubatch * ubatch) { mctx->set_input_k_idxs(self_k_idxs, ubatch); - mctx->set_input_kq_mask(self_kq_mask, ubatch, cparams.causal_attn); + // a graph that consumes no mask (maskless selected-key attention) leaves it unallocated + if (self_kq_mask && self_kq_mask->buffer) { + mctx->set_input_kq_mask(self_kq_mask, ubatch, cparams.causal_attn); + } } bool llm_graph_input_attn_k::can_reuse(const llm_graph_params & params) { @@ -561,11 +569,17 @@ bool llm_graph_input_attn_kv_msa::can_reuse(const llm_graph_params & params) { void llm_graph_input_attn_k_dsa::set_input(const llama_ubatch * ubatch) { mctx->get_mla()->set_input_k_idxs(self_k_idxs_mla, ubatch); - mctx->get_mla()->set_input_kq_mask(self_kq_mask_mla, ubatch, cparams.causal_attn); + // a graph that consumes no mask (maskless selected-key attention) leaves it unallocated + if (self_kq_mask_mla && self_kq_mask_mla->buffer) { + mctx->get_mla()->set_input_kq_mask(self_kq_mask_mla, ubatch, cparams.causal_attn); + } mctx->get_lid()->set_input_k_idxs(self_k_idxs_lid, ubatch); - mctx->get_lid()->set_input_kq_mask(self_kq_mask_lid, ubatch, cparams.causal_attn); + // a graph that consumes no mask (maskless selected-key attention) leaves it unallocated + if (self_kq_mask_lid && self_kq_mask_lid->buffer) { + mctx->get_lid()->set_input_kq_mask(self_kq_mask_lid, ubatch, cparams.causal_attn); + } // left unallocated when the indexer does not use the rotation if (self_k_rot_lid && self_k_rot_lid->buffer) { @@ -987,7 +1001,10 @@ void llm_graph_input_dsv4_raw::set_input(const llama_ubatch * ubatch) { } if (self_kq_mask && self_kq_mask->buffer) { - mctx->set_input_kq_mask(self_kq_mask, ubatch, cparams.causal_attn); + // a graph that consumes no mask (maskless selected-key attention) leaves it unallocated + if (self_kq_mask && self_kq_mask->buffer) { + mctx->set_input_kq_mask(self_kq_mask, ubatch, cparams.causal_attn); + } } if (self_k_rot) { @@ -1091,7 +1108,10 @@ void llm_graph_input_mem_hybrid::set_input(const llama_ubatch * ubatch) { mctx->get_attn()->set_input_k_idxs(inp_attn->self_k_idxs, ubatch); mctx->get_attn()->set_input_v_idxs(inp_attn->self_v_idxs, ubatch); - mctx->get_attn()->set_input_kq_mask(inp_attn->self_kq_mask, ubatch, cparams.causal_attn); + // a graph that consumes no mask (maskless selected-key attention) leaves it unallocated + if (inp_attn->self_kq_mask && inp_attn->self_kq_mask->buffer) { + mctx->get_attn()->set_input_kq_mask(inp_attn->self_kq_mask, ubatch, cparams.causal_attn); + } if (inp_attn->self_k_rot) { mctx->get_attn()->set_input_k_rot(inp_attn->self_k_rot); @@ -1103,7 +1123,9 @@ void llm_graph_input_mem_hybrid::set_input(const llama_ubatch * ubatch) { const int64_t n_rs = mctx->get_recr()->get_n_rs(); - if (inp_rs->s_copy) { + // a graph with no recurrent layers (the qwen4exp draft block on a hybrid-idx memory) never consumes the copy + // map, so the scheduler leaves it unallocated + if (inp_rs->s_copy && inp_rs->s_copy->buffer) { llama_host_write(inp_rs->s_copy); int32_t * data = (int32_t *) inp_rs->s_copy->data; @@ -1143,11 +1165,16 @@ bool llm_graph_input_mem_hybrid::can_reuse(const llm_graph_params & params) { void llm_graph_input_mem_hybrid_k::set_input(const llama_ubatch * ubatch) { mctx->get_attn()->set_input_k_idxs(inp_attn->self_k_idxs, ubatch); - mctx->get_attn()->set_input_kq_mask(inp_attn->self_kq_mask, ubatch, cparams.causal_attn); + // a graph that consumes no mask (maskless selected-key attention) leaves it unallocated + if (inp_attn->self_kq_mask && inp_attn->self_kq_mask->buffer) { + mctx->get_attn()->set_input_kq_mask(inp_attn->self_kq_mask, ubatch, cparams.causal_attn); + } const int64_t n_rs = mctx->get_recr()->get_n_rs(); - if (inp_rs->s_copy) { + // a graph with no recurrent layers (the qwen4exp draft block on a hybrid-idx memory) never consumes the copy + // map, so the scheduler leaves it unallocated + if (inp_rs->s_copy && inp_rs->s_copy->buffer) { llama_host_write(inp_rs->s_copy); int32_t * data = (int32_t *) inp_rs->s_copy->data; @@ -1221,7 +1248,9 @@ void llm_graph_input_mem_hybrid_iswa::set_input(const llama_ubatch * ubatch) { const int64_t n_rs = mctx->get_recr()->get_n_rs(); - if (inp_rs->s_copy) { + // a graph with no recurrent layers (the qwen4exp draft block on a hybrid-idx memory) never consumes the copy + // map, so the scheduler leaves it unallocated + if (inp_rs->s_copy && inp_rs->s_copy->buffer) { llama_host_write(inp_rs->s_copy); int32_t * data = (int32_t *) inp_rs->s_copy->data; diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index 3972ce9ce293..b570bcd1cf50 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -1,4 +1,15 @@ #include "llama-memory-hybrid-idx.h" +#include "prefix.h" + +// A qwen4exp MTP context is built with a recurrent filter that matches nothing (the nextn layer is not recurrent) so +// it can carry an indexer cache for sparse draft attention. Skip the recurrent child there: an empty recurrent cache +// still refuses partial seq_rm, which would leave a rejected draft in the cache. +static bool hybrid_idx_no_recr(const llama_memory_recurrent * r) { + if (!r) { return true; } + for (ggml_tensor * t : r->r_l) { if (t) { return false; } } + for (ggml_tensor * t : r->s_l) { if (t) { return false; } } + return true; +} #include "llama-impl.h" #include "llama-batch.h" @@ -9,6 +20,7 @@ #include <algorithm> #include <cassert> #include <cmath> +#include <cstring> #include <iterator> #include <stdexcept> @@ -65,7 +77,33 @@ llama_memory_hybrid_idx::llama_memory_hybrid_idx( 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_"); - }()) {} + }()) { + if (!mem_idx || !offload || n_swa != 0) { return; } + qsa_prefix = qsa_prefix_state(kv_size); + const int layers = model.hparams.n_layer_all; + qsa_keys.resize(layers, nullptr); qsa_ready.resize(layers, 0); + std::map<ggml_backend_buffer_type_t, ggml_context_ptr> contexts; + for (int il=0; il<layers; ++il) { + if (!model.hparams.has_kv(il) || !filter_idx(il) || model.hparams.dsv4_compress_ratios[il] != 4) { continue; } + auto * dev = model.dev_layer(il); + if (std::strcmp(ggml_backend_reg_name(ggml_backend_dev_backend_reg(dev)), "ROCm") != 0) { continue; } + incremental_qsa = true; + auto * buft = ggml_backend_dev_buffer_type(dev); + auto & ctx = contexts[buft]; + if (!ctx) { + ctx.reset(ggml_init({size_t(layers)*ggml_tensor_overhead(), nullptr, true})); + if (!ctx) { throw std::runtime_error("QSA cache context allocation failed"); } + } + qsa_keys[il] = ggml_new_tensor_2d(ctx.get(), GGML_TYPE_F32, model.hparams.indexer_head_size, (kv_size+3)/4 + 1); + ggml_format_name(qsa_keys[il], "cache_qsa_k_l%d", il); + } + for (auto & entry : contexts) { + ggml_backend_buffer_t buffer = ggml_backend_alloc_ctx_tensors_from_buft(entry.second.get(), entry.first); + if (!buffer) { throw std::runtime_error("QSA cache buffer allocation failed"); } + ggml_backend_buffer_clear(buffer, 0); + qsa_buffers.emplace_back(std::move(entry.second), buffer); + } +} 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 @@ -106,7 +144,7 @@ llama_memory_context_ptr llama_memory_hybrid_idx::init_batch(llama_batch_allocr } // prepare the recurrent batches first - if (!get_mem_recr()->prepare(ubatches)) { + if (!hybrid_idx_no_recr(get_mem_recr()) && !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); @@ -137,10 +175,14 @@ llama_memory_context_ptr llama_memory_hybrid_idx::init_full() { } 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); + auto result = std::make_unique<llama_memory_hybrid_idx_context>(this, lctx, optimize); + if (result->get_status() != LLAMA_MEMORY_STATUS_NO_UPDATE) { qsa_invalidate(); } + return result; } void llama_memory_hybrid_idx::clear(bool data) { + qsa_prefix.reset(); qsa_recover_pending = false; std::fill(qsa_ready.begin(), qsa_ready.end(), 0); + if (data) { for (const auto & entry : qsa_buffers) { ggml_backend_buffer_clear(entry.second.get(), 0); } } llama_memory_hybrid::clear(data); if (mem_idx) { @@ -150,10 +192,17 @@ void llama_memory_hybrid_idx::clear(bool 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)) { + if (!hybrid_idx_no_recr(get_mem_recr()) && !get_mem_recr()->seq_rm(seq_id, p0, p1)) { return false; } + if (incremental_qsa && !qsa_prefix.valid) { qsa_recover_pending = true; } + if (incremental_qsa && qsa_prefix.valid && (seq_id < 0 || seq_id == qsa_prefix.sequence)) { + if (p1 < 0 || size_t(p1) >= qsa_prefix.cells.size()) { + qsa_prefix.truncate(std::max(0, p0)); + for (auto & ready : qsa_ready) { ready = std::min<int64_t>(ready, qsa_prefix.cells.size()/4); } + } else { qsa_invalidate(); } + } if (mem_idx) { mem_idx->seq_rm(seq_id, p0, p1); } @@ -162,6 +211,7 @@ bool llama_memory_hybrid_idx::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_po } 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) { + qsa_invalidate(); llama_memory_hybrid::seq_cp(seq_id_src, seq_id_dst, p0, p1); if (mem_idx) { @@ -170,6 +220,7 @@ void llama_memory_hybrid_idx::seq_cp(llama_seq_id seq_id_src, llama_seq_id seq_i } void llama_memory_hybrid_idx::seq_keep(llama_seq_id seq_id) { + if (!qsa_prefix.valid || (qsa_prefix.sequence >= 0 && seq_id != qsa_prefix.sequence)) { qsa_invalidate(); } llama_memory_hybrid::seq_keep(seq_id); if (mem_idx) { @@ -178,6 +229,7 @@ void llama_memory_hybrid_idx::seq_keep(llama_seq_id seq_id) { } void llama_memory_hybrid_idx::seq_add(llama_seq_id seq_id, llama_pos p0, llama_pos p1, llama_pos shift) { + qsa_invalidate(); llama_memory_hybrid::seq_add(seq_id, p0, p1, shift); if (mem_idx) { @@ -186,6 +238,7 @@ void llama_memory_hybrid_idx::seq_add(llama_seq_id seq_id, llama_pos p0, llama_p } void llama_memory_hybrid_idx::seq_div(llama_seq_id seq_id, llama_pos p0, llama_pos p1, int d) { + qsa_invalidate(); llama_memory_hybrid::seq_div(seq_id, p0, p1, d); if (mem_idx) { @@ -202,11 +255,18 @@ std::map<ggml_backend_buffer_type_t, size_t> llama_memory_hybrid_idx::memory_bre } } + for (const auto & entry : qsa_buffers) { + mb[ggml_backend_buffer_get_type(entry.second.get())] += ggml_backend_buffer_get_size(entry.second.get()); + } 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); + // note: repeats llama_memory_hybrid::state_write so the recurrent section can be skipped in step with state_read + if ((flags & LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY) == 0) { + get_mem_attn()->state_write(io, seq_id, flags); + } + if (!hybrid_idx_no_recr(get_mem_recr())) { get_mem_recr()->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. @@ -219,6 +279,7 @@ void llama_memory_hybrid_idx::state_write(llama_io_write_i & io, llama_seq_id se } void llama_memory_hybrid_idx::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) { + if ((flags & LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY) == 0) { qsa_invalidate(); } // 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 @@ -232,7 +293,7 @@ void llama_memory_hybrid_idx::state_read(llama_io_read_i & io, llama_seq_id seq_ 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); + if (!hybrid_idx_no_recr(get_mem_recr())) { 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) { @@ -251,6 +312,7 @@ void llama_memory_hybrid_idx::state_read(llama_io_read_i & io, llama_seq_id seq_ } void llama_memory_hybrid_idx::state_drop(llama_seq_id seq_id) { + qsa_invalidate(); // 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); @@ -259,7 +321,7 @@ void llama_memory_hybrid_idx::state_drop(llama_seq_id seq_id) { } get_mem_attn()->seq_rm(seq_id, -1, -1); - get_mem_recr()->seq_rm(seq_id, -1, -1); + if (!hybrid_idx_no_recr(get_mem_recr())) { get_mem_recr()->seq_rm(seq_id, -1, -1); } if (mem_idx) { mem_idx->seq_rm(seq_id, -1, -1); @@ -278,10 +340,148 @@ void llama_memory_hybrid_idx::set_input_qsa( const llama_ubatch * ubatch, uint32_t ratio, bool blk_bias) const { + if (!set_input_qsa_prefix(cell_blk, blk_cells, blk_pos, bias, ubatch, ratio, blk_bias)) { + set_input_qsa_scan(cell_blk, blk_cells, blk_pos, bias, nullptr, ubatch, ratio, blk_bias); + } +} + +void llama_memory_hybrid_idx::set_input_qsa_blocks( + ggml_tensor * cell_blk, ggml_tensor * blk_cells, ggml_tensor * blk_pos, + ggml_tensor * bias, ggml_tensor * tail_idxs, const llama_ubatch * ubatch, uint32_t ratio) const { + if (tail_idxs && cell_blk->ne[1] == 1 && qsa_metadata(blk_cells, blk_pos, bias, tail_idxs, *ubatch, ratio)) { + return; + } + set_input_qsa_scan(cell_blk, blk_cells, blk_pos, bias, tail_idxs, ubatch, ratio, true); +} + +// On the tracked prefix the compact metadata follows directly: the complete blocks are the first L/4 position +// blocks, every block start is its own limit, unfinished blocks are never visible (INT32_MAX), and a query's +// tail is the cells of its own partial block. +bool llama_memory_hybrid_idx::qsa_metadata(ggml_tensor * members, ggml_tensor * pos, ggml_tensor * bias, + ggml_tensor * tails, const llama_ubatch & u, uint32_t ratio) const { + if (ratio != 4 || bias->type != GGML_TYPE_I32 || !qsa_prefix_matches(u)) { return false; } + const size_t blocks = pos->ne[0]/4, complete = qsa_prefix.cells.size()/4; + if (complete > blocks) { return false; } + auto * c = members ? (int32_t *) members->data : nullptr; auto * p = pos ? (int32_t *) pos->data : nullptr; + auto * lim = (int32_t *) bias->data; auto * tail = (int32_t *) tails->data; + if (!lim || !tail) { return false; } + if (c) { + std::copy_n(qsa_prefix.cells.data(), complete*4, c); + std::fill(c+complete*4, c+blocks*4, 0); + } + if (p) { + for (int a=0; a<4; ++a) { + std::copy_n(qsa_prefix.block_positions.data(), complete, p+a*blocks); + std::fill(p+a*blocks+complete, p+(a+1)*blocks, 0); + } + } + std::copy_n(qsa_prefix.block_positions.data(), complete, lim); + std::fill(lim+complete, lim+blocks, INT32_MAX); + std::fill(tail, tail+3*u.n_tokens, -1); + for (uint32_t i=0; i<u.n_tokens; ++i) { + int32_t end = u.pos[i]+1, start = end/4*4; lim[blocks+i] = start; + for (int j=0; j<end-start; ++j) { tail[3*i+j] = qsa_prefix.cells[start+j]; } + } + return true; +} + +// On the tracked prefix (qsa_prefix), position k of the one sequence lives in cell qsa_prefix.cells[k] and +// nothing else occupies the cache, so the arrays the scan derives per cell follow directly: complete blocks +// are the first L/ratio position blocks, everything else maps to the spare block, and a block's bias is +// -inf past the complete ones, the "always visible" value from the query's incomplete tail onwards, else 0. +bool llama_memory_hybrid_idx::set_input_qsa_prefix( + 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 { + if (!blk_bias || ratio == 0 || cell_blk->ne[1] != 1 || !qsa_prefix_matches(*ubatch)) { + return false; + } + const int64_t n_kv = cell_blk->ne[0]; + const int64_t r = ratio; + const int64_t n_blocks = blk_pos->ne[0]/4; + const int64_t n_tokens = ubatch->n_tokens; + const int64_t L = (int64_t) qsa_prefix.cells.size(); + if (blk_cells->ne[0] != r*n_blocks || blk_cells->ne[1] != 1 || bias->ne[0] != n_blocks || bias->ne[1] != n_tokens || + bias->ne[2] != 1 || bias->type != GGML_TYPE_F32 || qsa_prefix.end != L || L > n_kv || n_blocks*r < n_kv) { + return false; + } + if (!get_mem_idx()) { + return false; + } + const auto & cells = get_mem_idx()->get_cells(ubatch->seq_id[0][0]); + if ((int64_t) cells.get_used() != L) { + return false; + } + for (int64_t k = 0; k < L; ++k) { + if (qsa_prefix.cells[k] < 0 || qsa_prefix.cells[k] >= n_kv) { + return false; + } + } + + const int64_t n_bid = L/r; + const bool have_dead = n_bid < n_blocks; + const int32_t dead_bid = (int32_t) (have_dead ? n_bid : n_blocks - 1); + + 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; + // the incremental key-cache graph leaves the member and position arrays unallocated, as the scan expects + if (!dst_cell_blk || !dst_bias) { + return false; + } + + std::fill(dst_cell_blk, dst_cell_blk + n_kv, dead_bid); + if (dst_blk_cells) { + std::fill(dst_blk_cells, dst_blk_cells + r*n_blocks, 0); + } + if (dst_blk_pos) { + std::fill(dst_blk_pos, dst_blk_pos + 4*n_blocks, 0); + } + for (int64_t k = 0; k < n_bid*r; ++k) { + const int32_t cell = qsa_prefix.cells[k]; + if (dst_blk_cells) { + dst_blk_cells[k] = cell; + } + dst_cell_blk[cell] = (int32_t) (k/r); + } + if (dst_blk_pos) { + for (int64_t sec = 0; sec < 4; ++sec) { + for (int64_t b = 0; b < n_bid; ++b) { + dst_blk_pos[sec*n_blocks + b] = (int32_t) (b*r); + } + } + } + for (int64_t i = 0; i < n_tokens; ++i) { + const int64_t tail_start = ((int64_t) ubatch->pos[i] + 1)/r*r; + float * row = dst_bias + i*n_blocks; + for (int64_t b = 0; b < n_blocks; ++b) { + row[b] = b >= n_bid ? -INFINITY : (b*r >= tail_start ? 1e9f : 0.0f); + } + if (have_dead) { + row[dead_bid] = 1e9f; + } + } + return true; +} + +void llama_memory_hybrid_idx::set_input_qsa_scan( + ggml_tensor * cell_blk, + ggml_tensor * blk_cells, + ggml_tensor * blk_pos, + ggml_tensor * bias, + ggml_tensor * tail_idxs, + const llama_ubatch * ubatch, + uint32_t ratio, + bool blk_bias) const { GGML_ASSERT(ratio > 0); GGML_ASSERT(get_mem_idx() != nullptr); - GGML_ASSERT(ggml_backend_buffer_is_host(cell_blk->buffer)); + GGML_ASSERT(tail_idxs || 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 @@ -292,10 +492,23 @@ void llama_memory_hybrid_idx::set_input_qsa( 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; + // with tails the per-cell block map is not consumed (the selection gathers cells by block) + int32_t * dst_cell_blk = tail_idxs ? nullptr : (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; + const bool compact = bias->type == GGML_TYPE_I32; + float * dst_bias = compact ? nullptr : (float *) bias->data; + int32_t * limits = compact ? (int32_t *) bias->data : nullptr; + if (compact) { + GGML_ASSERT(tail_idxs && blk_bias && n_ns == 1 && ggml_nelements(bias) == n_blocks+n_tps); + for (int64_t i=0;i<n_tokens;++i) { GGML_ASSERT(ubatch->seq_id[i][0] == ubatch->seq_id[0][0]); } + } + int32_t * dst_tail = tail_idxs ? (int32_t *) tail_idxs->data : nullptr; + if (tail_idxs) { + GGML_ASSERT(blk_bias && r > 1 && ggml_backend_buffer_is_host(tail_idxs->buffer)); + GGML_ASSERT(tail_idxs->ne[0] == r-1 && tail_idxs->ne[1] == n_tps && tail_idxs->ne[2] == n_ns); + std::fill(dst_tail, dst_tail + ggml_nelements(tail_idxs), -1); + } // a block is keyed on (sequence set, index bucket): a unified cache counts every sequence // from zero, so the bucket alone would pool two sequences into one block @@ -319,17 +532,17 @@ void llama_memory_hybrid_idx::set_input_qsa( std::vector<int32_t> order; std::vector<int32_t> rank; - std::fill(dst_blk_pos, dst_blk_pos + 4*n_blocks*n_ns, 0); + if (dst_blk_pos) { std::fill(dst_blk_pos, dst_blk_pos + 4*n_blocks*n_ns, 0); } 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 = 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); + int32_t * cur_cell_blk = dst_cell_blk ? dst_cell_blk + s*n_kv : nullptr; + int32_t * cur_blk_cells = dst_blk_cells ? dst_blk_cells + s*(r*n_blocks) : nullptr; - std::fill(cur_blk_cells, cur_blk_cells + r*n_blocks, 0); + if (cur_blk_cells) { std::fill(cur_blk_cells, cur_blk_cells + r*n_blocks, 0); } bid_idx .clear(); bid_cell .clear(); @@ -488,7 +701,7 @@ void llama_memory_hybrid_idx::set_input_qsa( } for (int64_t sec = 0; sec < 4; ++sec) { - dst_blk_pos[sec*(n_blocks*n_ns) + s*n_blocks + b] = sec_pos[sec]; + if (dst_blk_pos) { dst_blk_pos[sec*(n_blocks*n_ns) + s*n_blocks + b] = sec_pos[sec]; } } } @@ -505,10 +718,31 @@ void llama_memory_hybrid_idx::set_input_qsa( if (blk_of[j] >= 0) { const int64_t idx = ranked ? rank[j] : cells.pos_get(j); - cur_blk_cells[blk_of[j]*r + (idx%r)] = (int32_t) j; + if (cur_blk_cells) { cur_blk_cells[blk_of[j]*r + (idx%r)] = (int32_t) j; } } - cur_cell_blk[j] = blk_of[j] < 0 ? dead_bid : blk_of[j]; + if (cur_cell_blk) { cur_cell_blk[j] = blk_of[j] < 0 ? dead_bid : blk_of[j]; } + } + + // member cells per group, for the tails (a group's slot k holds the cell at position k within the block) + std::vector<int32_t> group_members; + if (dst_tail) { + group_members.assign(grp_first.size()*r, -1); + for (int64_t j=0;j<n_kv;++j) { + const int32_t g=cell_grp[j]; + if (g<0) { continue; } + const int64_t idx=ranked ? rank[j] : cells.pos_get(j); + const int64_t slot=g*r+idx%r; + GGML_ASSERT(group_members[slot]<0 || group_members[slot]==j); + group_members[slot]=(int32_t)j; + } + } + + if (compact) { + const llama_seq_id seq = ubatch->seq_id[0][0]; + for (int64_t b=0;b<n_blocks;++b) { + limits[b] = b<n_bid && cells.seq_has((uint32_t)bid_cell[b],seq) ? bid_idx[b] : INT32_MAX; + } } for (int64_t ii = 0; ii < n_tps; ++ii) { @@ -543,6 +777,28 @@ void llama_memory_hybrid_idx::set_input_qsa( // the tail is an incomplete block and is always visible, as in the reference const int64_t tail_start = (q + 1)/r*r; + if (dst_tail && q+1>tail_start) { + const int64_t pb=tail_start/r; + if (pb>=0 && pb<n_blocks) { + int32_t * tail=dst_tail+(s*n_tps+ii)*(r-1); + for (int32_t g=grp_head[pb];g>=0;g=grp_next[g]) { + if (!cells.seq_has((uint32_t)grp_first[g],seq_id)) { continue; } + for (int64_t slot=0;slot<q+1-tail_start;++slot) { + const int32_t cell=group_members[g*r+slot]; + if (cell<0 || !cells.seq_has((uint32_t)cell,seq_id)) { continue; } + GGML_ASSERT(tail[slot]<0 || tail[slot]==cell); + tail[slot]=cell; + } + } + } + } + + if (compact) { + GGML_ASSERT(tail_start >= 0 && tail_start <= 16777216); + limits[n_blocks+ii] = (int32_t)tail_start; + continue; + } + 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 @@ -555,14 +811,15 @@ void llama_memory_hybrid_idx::set_input_qsa( } // finite, so it can never meet a -inf and produce a nan - cur_blk_bias[b] = bid_idx[b] >= tail_start ? 1e9f : 0.0f; + // with tails the own block is never scored (its cells come from the tail), so it is hidden + cur_blk_bias[b] = bid_idx[b] >= tail_start ? (dst_tail ? -INFINITY : 1e9f) : 0.0f; } // the spare block holds the unpooled cells, which are the incomplete tail, so // it gets the tail value. it must stay finite: a sequence with fewer than // `ratio` cells owns no full block, and a row of -inf only gives a nan. if (have_dead) { - cur_blk_bias[dead_bid] = 1e9f; + cur_blk_bias[dead_bid] = dst_tail ? -INFINITY : 1e9f; } continue; @@ -636,6 +893,7 @@ llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context( llama_memory_hybrid_context(mem, std::move(sinfos_attn), ubatches), mem(mem), ns_ubatch(llama_memory_hybrid_idx_ns(sinfos_idx)), + qsa_slots(sinfos_idx), ctx_idx(mem->get_mem_idx() == nullptr ? nullptr : new llama_kv_cache_context(mem->get_mem_idx(), std::move(sinfos_idx), ubatches)) {} @@ -654,6 +912,9 @@ bool llama_memory_hybrid_idx_context::apply() { if (ctx_idx) { res = res & ctx_idx->apply(); + if (res && i_cur < qsa_slots.size()) { + const_cast<llama_memory_hybrid_idx *>(mem)->qsa_apply(ctx_idx->get_ubatch(), qsa_slots[i_cur]); + } else if (!res) { const_cast<llama_memory_hybrid_idx *>(mem)->qsa_invalidate(); } } return res; @@ -681,3 +942,138 @@ void llama_memory_hybrid_idx_context::set_input_qsa( mem->set_input_qsa(cell_blk, blk_cells, blk_pos, bias, ubatch, ratio, blk_bias); } + +void llama_memory_hybrid_idx_context::set_input_qsa_blocks( + ggml_tensor * cell_blk, ggml_tensor * blk_cells, ggml_tensor * blk_pos, + ggml_tensor * bias, ggml_tensor * tail_idxs, const llama_ubatch * ubatch, uint32_t ratio) const { + GGML_ASSERT(mem != nullptr); + mem->set_input_qsa_blocks(cell_blk, blk_cells, blk_pos, bias, tail_idxs, ubatch, ratio); +} + +bool llama_memory_hybrid_idx_context::qsa_position_prefix(const llama_ubatch & ubatch) const { + if (qsa_prefix_matches(ubatch)) { return true; } + if (!qsa_scalar_visibility(ubatch)) { return false; } + const llama_seq_id seq=ubatch.seq_id[0][0]; + return qsa_single_sequence_prefix(mem->get_mem_idx()->get_cells(seq),get_idx()->get_n_kv(),seq); +} + +uint32_t llama_memory_hybrid_idx_context::qsa_n_kv_window() const { + const uint32_t n_kv = get_idx() ? get_idx()->get_n_kv() : 0; + if (!mem || !mem->get_mem_idx()) { return n_kv; } + llama_pos pos_max = -1; + for (llama_seq_id s = 0; s < (llama_seq_id) LLAMA_MAX_SEQ; ++s) { + pos_max = std::max(pos_max, mem->get_mem_idx()->get_cells(s).seq_pos_max(s)); + } + if (pos_max < 0) { return n_kv; } + const uint32_t window = ((uint32_t) pos_max + 1 + 255) / 256 * 256; + return std::max(n_kv, window); +} + +bool llama_memory_hybrid_idx_context::qsa_scalar_visibility(const llama_ubatch & ubatch) const { + if (qsa_prefix_matches(ubatch)) { return true; } + if (get_n_stream()!=1 || !get_idx() || !ubatch.token || !ubatch.pos || !ubatch.n_tokens || + !ubatch.n_pos || !ubatch.seq_id || !ubatch.n_seq_id) { return false; } + if (ubatch.n_seq_id[0]<1 || !ubatch.seq_id[0]) { return false; } + const llama_seq_id seq=ubatch.seq_id[0][0]; + for (uint32_t i=0;i<ubatch.n_tokens;++i) { + if (ubatch.n_seq_id[i]<1 || !ubatch.seq_id[i] || ubatch.seq_id[i][0]!=seq || + ubatch.pos[i]<0 || ubatch.pos[i]>=16777216) { return false; } + for (uint32_t axis=1;axis<ubatch.n_pos;++axis) { + if (ubatch.pos[i+axis*ubatch.n_tokens]!=ubatch.pos[i]) { return false; } + } + } + if (ubatch.is_pos_2d()) { + const auto & cells=mem->get_mem_idx()->get_cells(seq); + for (uint32_t j=0;j<get_idx()->get_n_kv();++j) { + if (!cells.is_empty(j) && cells.seq_has(j,seq) && cells.ext_get(j).is_2d_gt(cells.pos_get(j),cells.pos_get(j))) { return false; } + } + } + return true; +} + +void llama_memory_hybrid_idx::qsa_invalidate() { + qsa_prefix.invalidate(); qsa_recover_pending = true; std::fill(qsa_ready.begin(), qsa_ready.end(), 0); +} + +bool llama_memory_hybrid_idx::qsa_recover(llama_seq_id seq) { + if (mem_idx->get_n_stream() != 1) { return false; } + const auto & raw = mem_idx->get_cells(seq); + if (raw.size() != qsa_prefix.positions.size()) { return false; } + qsa_prefix_state rebuilt(qsa_prefix.positions.size()); + rebuilt.cells.resize(raw.get_used(), -1); + for (uint32_t cell=raw.used_min(); cell<raw.used_max_p1(); ++cell) { + if (raw.is_empty(cell)) { continue; } + const int32_t pos = raw.pos_get(cell); + const auto & ext = raw.ext_get(cell); + if (pos < 0 || size_t(pos) >= rebuilt.cells.size() || rebuilt.cells[pos] >= 0 || + !raw.seq_has(cell, seq) || raw.seq_get_all(cell).count() != 1 || + !((ext.x == 0 && ext.y == 0) || (ext.x == pos && ext.y == pos))) { return false; } + rebuilt.cells[pos] = cell; rebuilt.positions[cell] = pos; + } + if (std::find(rebuilt.cells.begin(), rebuilt.cells.end(), -1) != rebuilt.cells.end()) { return false; } + rebuilt.sequence = seq; + for (size_t b=0; b<rebuilt.cells.size()/4; ++b) { rebuilt.block_positions.push_back(b*4); } + qsa_prefix = std::move(rebuilt); + return true; +} + +void llama_memory_hybrid_idx::qsa_apply(const llama_ubatch & u, const llama_kv_cache::slot_info & slots) { + if (!incremental_qsa) { return; } + const auto reject = [&]() { qsa_invalidate(); qsa_recover_pending = false; }; + if (!u.token || !u.pos || !u.n_tokens || slots.n_stream() != 1 || + slots.size() != u.n_tokens || !u.n_pos || !u.seq_id || !u.n_seq_id || u.n_seq_id[0] != 1 || !u.seq_id[0]) { + reject(); return; + } + const auto seq = u.seq_id[0][0]; + for (uint32_t i=0; i<u.n_tokens; ++i) { + if (u.n_seq_id[i] != 1 || !u.seq_id[i] || u.seq_id[i][0] != seq || int64_t(u.pos[i]) != int64_t(u.pos[0])+i) { + reject(); return; + } + for (uint32_t axis=1; axis<u.n_pos; ++axis) { + if (u.pos[i+axis*u.n_tokens] != u.pos[i]) { reject(); return; } + } + } + if (!qsa_prefix.valid && (!qsa_recover_pending || !qsa_recover(seq))) { qsa_recover_pending = false; return; } + qsa_recover_pending = false; + if (!qsa_prefix.apply(seq, u.pos[0], slots.idxs[0])) { reject(); } +} + +bool llama_memory_hybrid_idx::qsa_prefix_matches(const llama_ubatch & u) const { + if (!incremental_qsa || !qsa_prefix.valid || !u.token || !u.pos || !u.n_tokens || !u.n_pos || !u.seq_id || !u.n_seq_id || + u.pos[0] != qsa_prefix.begin || int64_t(u.n_tokens) != qsa_prefix.end-qsa_prefix.begin) { return false; } + for (uint32_t i=0; i<u.n_tokens; ++i) { + if (u.n_seq_id[i] != 1 || !u.seq_id[i] || u.seq_id[i][0] != qsa_prefix.sequence || u.pos[i] != u.pos[0]+int32_t(i)) { return false; } + for (uint32_t a=1; a<u.n_pos; ++a) { if (u.pos[i+a*u.n_tokens] != u.pos[i]) { return false; } } + } + return true; +} + +bool llama_memory_hybrid_idx::qsa_fast(int il, const llama_ubatch & u) const { + return qsa_prefix_matches(u) && u.n_tokens <= 8 && qsa_keys.at(il) && + qsa_ready.at(il) >= int64_t(qsa_prefix.previous_size/4) && + qsa_prefix.cells.size()/4 >= (u.n_tokens+3)/4+1; +} + +ggml_tensor * llama_memory_hybrid_idx::qsa_cache(ggml_context * ctx, int il, int64_t blocks) const { + if (!incremental_qsa || !qsa_keys.at(il)) { return nullptr; } + auto * k = qsa_keys[il]; GGML_ASSERT(blocks + 1 <= k->ne[1]); + return ggml_view_2d(ctx, k, k->ne[0], blocks + 1, k->nb[1], 0); +} + +void llama_memory_hybrid_idx::qsa_fill_updates(ggml_tensor * members, ggml_tensor * pos, ggml_tensor * rows) const { + const int count = rows->ne[0], complete = qsa_prefix.cells.size()/4; + const int first = qsa_prefix.begin/4, last = std::min(complete, (qsa_prefix.end+3)/4); + GGML_ASSERT(last-first < count && complete >= count - 1); + std::vector<int32_t> ids = { complete }; + for (int b=first; b<last; ++b) { ids.push_back(b); } + for (int b=0; int(ids.size())<count; ++b) { if (b<first || b>=last) { ids.push_back(b); } } + auto * c = (int32_t *) members->data; auto * p = (int32_t *) pos->data; auto * w = (int64_t *) rows->data; + for (int i=0; i<count; ++i) { + w[i] = ids[i]; + for (int j=0; j<4; ++j) { c[4*i+j] = ids[i] == complete ? 0 : qsa_prefix.cells[ids[i]*4+j]; p[j*count+i] = ids[i] == complete ? 0 : ids[i]*4; } + } +} + +void llama_memory_hybrid_idx::qsa_commit(int il) const { + GGML_ASSERT(qsa_prefix.valid); qsa_ready.at(il) = qsa_prefix.cells.size()/4; +} diff --git a/src/llama-memory-hybrid-idx.h b/src/llama-memory-hybrid-idx.h index 705189e7eb58..c0e10691d1f6 100644 --- a/src/llama-memory-hybrid-idx.h +++ b/src/llama-memory-hybrid-idx.h @@ -1,6 +1,7 @@ #pragma once #include "llama-memory-hybrid.h" +#include "qsa-prefix-state.h" #include <memory> #include <vector> @@ -86,8 +87,39 @@ class llama_memory_hybrid_idx : public llama_memory_hybrid { 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; + // complete-block selection metadata: tails and, when the bias is I32, compact limits + void set_input_qsa_blocks(ggml_tensor * cell_blk, ggml_tensor * blk_cells, ggml_tensor * blk_pos, + ggml_tensor * bias, ggml_tensor * tail_idxs, + const llama_ubatch * ubatch, uint32_t ratio) const; + + void qsa_apply(const llama_ubatch & ubatch, const llama_kv_cache::slot_info & slots); + void qsa_invalidate(); + bool qsa_prefix_matches(const llama_ubatch & ubatch) const; + bool qsa_fast(int il, const llama_ubatch & ubatch) const; + ggml_tensor * qsa_cache(ggml_context * ctx, int il, int64_t blocks) const; + void qsa_fill_updates(ggml_tensor * members, ggml_tensor * positions, ggml_tensor * rows) const; + void qsa_commit(int il) const; private: + // closed-form metadata for an incremental ubatch on the tracked prefix; false when the scan is needed + bool set_input_qsa_prefix(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; + // the per-cell scan over the whole window; with tail_idxs it also writes each query's own partial block + // (r-1 cells, -1 padded) and, for an I32 bias, the compact visibility limits [n_blocks + n_tokens] + void set_input_qsa_scan(ggml_tensor * cell_blk, ggml_tensor * blk_cells, ggml_tensor * blk_pos, + ggml_tensor * bias, ggml_tensor * tail_idxs, const llama_ubatch * ubatch, + uint32_t ratio, bool blk_bias) const; + // closed-form block metadata on the tracked prefix for the compact I32 bias; false when the scan is needed + bool qsa_metadata(ggml_tensor * cells, ggml_tensor * positions, ggml_tensor * bias, + ggml_tensor * tails, const llama_ubatch & ubatch, uint32_t ratio) const; + + bool incremental_qsa = false; + bool qsa_recover_pending = false; + bool qsa_recover(llama_seq_id seq); + qsa_prefix_state qsa_prefix; + mutable std::vector<int64_t> qsa_ready; + std::vector<ggml_tensor *> qsa_keys; + std::vector<std::pair<ggml_context_ptr, ggml_backend_buffer_ptr>> qsa_buffers; // 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); @@ -138,12 +170,31 @@ class llama_memory_hybrid_idx_context : public llama_memory_hybrid_context { // nullptr with no indexer const llama_kv_cache_context * get_idx() const; + bool qsa_prefix_matches(const llama_ubatch & u) const { return mem && mem->qsa_prefix_matches(u); } + bool qsa_fast(int il, const llama_ubatch & u) const { return mem && mem->qsa_fast(il, u); } + // cells the block metadata must cover: the KV view is sized by occupied cells, but a cache whose positions + // run ahead of its cells (an MTP draft never receives the image cells an M-RoPE image pins to one position) + // has blocks past that view. Never below get_n_kv(), padded like it so graph reuse keeps its cadence. + uint32_t qsa_n_kv_window() const; + ggml_tensor * qsa_cache(ggml_context * ctx, int il) const { + return mem ? mem->qsa_cache(ctx, il, (qsa_n_kv_window()+3)/4) : nullptr; + } + void qsa_fill_updates(ggml_tensor * members, ggml_tensor * pos, ggml_tensor * rows) const { mem->qsa_fill_updates(members, pos, rows); } + void qsa_commit(int il) const { mem->qsa_commit(il); } + // streams in the current slot info, the `ns` of get_k/get_v; 1 if unified uint32_t get_n_stream() const; 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; + void set_input_qsa_blocks(ggml_tensor * cell_blk, ggml_tensor * blk_cells, ggml_tensor * blk_pos, + ggml_tensor * bias, ggml_tensor * tail_idxs, + const llama_ubatch * ubatch, uint32_t ratio) const; + // one sequence, scalar positions in range, no 2d rope extents: the compact visibility rule applies + bool qsa_scalar_visibility(const llama_ubatch & ubatch) const; + // scalar visibility and the cached cells form a single-sequence prefix with unique positions + bool qsa_position_prefix(const llama_ubatch & ubatch) const; private: const llama_memory_hybrid_idx * mem = nullptr; @@ -151,6 +202,7 @@ class llama_memory_hybrid_idx_context : public llama_memory_hybrid_context { // 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; + const slot_info_vec_t qsa_slots; // null unless the model has an indexer const llama_memory_context_ptr ctx_idx; diff --git a/src/llama-memory-recurrent.cpp b/src/llama-memory-recurrent.cpp index 57919accf095..22fdd32f4934 100644 --- a/src/llama-memory-recurrent.cpp +++ b/src/llama-memory-recurrent.cpp @@ -659,8 +659,18 @@ bool llama_memory_recurrent::find_slot(const llama_ubatch & ubatch) { if (cell.pos >= 0 && last_pos != cell.pos + (llama_pos) n_seq_tokens) { // What should happen when the pos backtracks or skips a value? // Clearing the state mid-batch would require special-casing which isn't done. - LLAMA_LOG_WARN("%s: non-consecutive token position %d after %d for sequence %d with %u new tokens\n", - __func__, last_pos, cell.pos, ubatch.seq_id[i][0], n_seq_tokens); + // A hybrid model that filters out every recurrent layer leaves this cache with no tensors. + // The hybrid then skips seq_rm on it, so its positions are never rewound and a rejected + // speculative draft trips this on every step - with no state to be inconsistent about. + bool has_state = false; + for (ggml_tensor * t : r_l) { if (t) { has_state = true; break; } } + if (!has_state) { + for (ggml_tensor * t : s_l) { if (t) { has_state = true; break; } } + } + if (has_state) { + LLAMA_LOG_WARN("%s: non-consecutive token position %d after %d for sequence %d with %u new tokens\n", + __func__, last_pos, cell.pos, ubatch.seq_id[i][0], n_seq_tokens); + } } cell.pos = last_pos; cell.seq_id.clear(); diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp index 2dfcd6eb9074..fde86b19541f 100644 --- a/src/llama-model-loader.cpp +++ b/src/llama-model-loader.cpp @@ -1110,7 +1110,7 @@ bool llama_model_loader::lazy_read::add(const std::string & name, const ggml_ten // do not lazy-read small tensors, it has significant overhead and is not worth it constexpr size_t auto_min_size = 4ull * 1024 * 1024 * 1024; - if (mode != LLAMA_LAZY_MODE_ON && ggml_nbytes(t) <= auto_min_size) { + if (mode == LLAMA_LAZY_MODE_AUTO && ggml_nbytes(t) <= auto_min_size) { return false; } diff --git a/src/llama-model.cpp b/src/llama-model.cpp index 6b0557a7b437..c6d3e5dc6a7f 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -1706,7 +1706,7 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) { // With the n-gram table left on disk, a populated mapping would pull the table's // third of the file resident for nothing; readahead alone carries the sequential load. - ml.init_mappings(!params.ple_on_disk, use_mlock ? &pimpl->mlock_mmaps : nullptr); + ml.init_mappings(!(params.ple_on_disk || params.lazy_mode == LLAMA_LAZY_MODE_DIRECT), use_mlock ? &pimpl->mlock_mmaps : nullptr); pimpl->mappings.reserve(ml.mappings.size()); // create the backend buffers @@ -2510,11 +2510,44 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params, // checks default: { - // Dense MTP heads use a plain attention KV cache instead of the hybrid wrapper. + // Dense MTP heads use a plain attention KV cache instead of the hybrid wrapper. The qwen4exp + // draft head with an indexer runs the same sparse attention as the trunk, so its MTP context + // gets an indexer cache (hybrid-idx memory holding only the nextn layer) instead. const bool mtp_on_hybrid_qwen = params.ctx_type == LLAMA_CONTEXT_TYPE_MTP && (arch == LLM_ARCH_QWEN3NEXT || arch == LLM_ARCH_QWEN35 || arch == LLM_ARCH_QWEN35MOE || - arch == LLM_ARCH_QWEN4EXP || arch == LLM_ARCH_BAILINGMOE3); + arch == LLM_ARCH_BAILINGMOE3 || + (arch == LLM_ARCH_QWEN4EXP && hparams.indexer_head_size == 0)); + + if (params.ctx_type == LLAMA_CONTEXT_TYPE_MTP && arch == LLM_ARCH_QWEN4EXP && + hparams.indexer_head_size > 0) { + llama_memory_hybrid_idx::layer_filter_cb f_attn = + [&](uint32_t il) { return il >= hparams.n_layer(); }; + llama_memory_hybrid_idx::layer_filter_cb f_recr = + [&](uint32_t /*il*/) { return false; }; // the nextn layer is not recurrent + llama_memory_hybrid_idx::layer_filter_cb f_idx = + [&](uint32_t il) { return il >= hparams.n_layer(); }; + LLAMA_LOG_INFO("%s: MTP context uses a hybrid-idx memory (sparse draft attention)\n", __func__); + return 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(f_attn), + /* filter_recr */ std::move(f_recr), + /* filter_idx */ std::move(f_idx)); + } const bool mtp_on_hybrid_nemotron = params.ctx_type == LLAMA_CONTEXT_TYPE_MTP && arch == LLM_ARCH_NEMOTRON_H_MOE; diff --git a/src/llama-model.h b/src/llama-model.h index a02b30ca7ada..75643a8523d4 100644 --- a/src/llama-model.h +++ b/src/llama-model.h @@ -231,6 +231,10 @@ struct llama_layer_nextn { struct ggml_tensor * shared_head_head_s = nullptr; struct ggml_tensor * shared_head_head_in_s = nullptr; struct ggml_tensor * shared_head_norm = nullptr; + // qwen4exp: the draft head's own final hyper-connection mixer (the target's is not reused) + struct ggml_tensor * hc_head_norm = nullptr; + struct ggml_tensor * hc_head_down = nullptr; + struct ggml_tensor * hc_head_up = nullptr; }; struct llama_layer_switch_lora { diff --git a/src/llama-ple-disk.cpp b/src/llama-ple-disk.cpp index 08263746b863..e21ab0bd1f2b 100644 --- a/src/llama-ple-disk.cpp +++ b/src/llama-ple-disk.cpp @@ -10,6 +10,7 @@ #include <mutex> #include <stdexcept> #include <thread> +#include <utility> #include <vector> #if !defined(_WIN32) @@ -37,14 +38,20 @@ struct llama_ple_disk::impl { std::vector<uint8_t> slab; // per-gather scratch, reused - std::vector<int32_t> uniq; - std::vector<uint8_t> raw; // [uniq.size(), rs] - std::vector<std::pair<int32_t, uint32_t>> misses; // (row, index into uniq) - uint8_t * bounce0 = nullptr; // main-thread bounce buffer + std::vector<std::pair<int32_t, uint32_t>> order; // (row, position in idx) sorted by row: equal rows are adjacent + std::vector<uint32_t> runs; // order[runs[u]..runs[u+1]) are the positions of distinct row u + std::vector<uint8_t> raw; // [distinct rows, rs] + std::vector<std::pair<int32_t, uint32_t>> misses; // (row, index into raw) + float * dst = nullptr; // destination of the gather in progress + uint8_t * bounce0 = nullptr; // calling-thread bounce buffer std::mutex mtx; // one gather at a time; a model is shared by every context built on it - // reader pool, started on first use + // worker pool, started on first use; a job is n_items items handed out `grain` at a time + enum class job_kind { read, dequant }; + job_kind job = job_kind::read; + size_t n_items = 0; + size_t grain = 1; int32_t n_threads = 1; std::vector<std::thread> workers; std::mutex pm; @@ -187,6 +194,37 @@ struct llama_ple_disk::impl { memcpy(dst, bounce + (off - a0), rs); } + // dequantize distinct row u into the first position that asked for it, then copy it to the others + void dequant_row(size_t u) const { + const uint8_t * src = raw.data() + u * rs; + float * first = dst + (size_t) order[runs[u]].second * ne0; + if (to_float) { + to_float(src, first, ne0); + } else { + memcpy(first, src, rs); + } + for (uint32_t j = runs[u] + 1; j < runs[u + 1]; ++j) { + memcpy(dst + (size_t) order[j].second * ne0, first, (size_t) ne0 * sizeof(float)); + } + } + + void run_items(uint8_t * bounce) { + for (;;) { + const size_t i0 = next.fetch_add(grain); + if (i0 >= n_items) { + break; + } + const size_t i1 = std::min(i0 + grain, n_items); + for (size_t i = i0; i < i1; ++i) { + if (job == job_kind::read) { + read_row(misses[i].first, raw.data() + (size_t) misses[i].second * rs, bounce); + } else { + dequant_row(i); + } + } + } + } + void worker() { uint8_t * bounce = direct ? alloc_bounce() : nullptr; uint64_t seen = 0; @@ -199,13 +237,7 @@ struct llama_ple_disk::impl { } seen = gen; } - for (;;) { - const size_t i = next.fetch_add(1); - if (i >= misses.size()) { - break; - } - read_row(misses[i].first, raw.data() + (size_t) misses[i].second * rs, bounce); - } + run_items(bounce); { std::lock_guard<std::mutex> lk(pm); if (--pending == 0) { @@ -216,14 +248,18 @@ struct llama_ple_disk::impl { free(bounce); } - void run_misses() { - if (n_threads <= 1 || misses.size() <= 2) { - if (direct && bounce0 == nullptr) { - bounce0 = alloc_bounce(); - } - for (const auto & m : misses) { - read_row(m.first, raw.data() + (size_t) m.second * rs, bounce0); - } + // run `kind` over n items; up to serial_max of them stay on the calling thread, + // which otherwise works alongside the pool instead of waiting for it + void run_job(job_kind kind, size_t n, size_t g, size_t serial_max) { + job = kind; + n_items = n; + grain = g; + if (direct && bounce0 == nullptr) { + bounce0 = alloc_bounce(); + } + if (n_threads <= 1 || n <= serial_max) { + next = 0; + run_items(bounce0); return; } if (workers.empty()) { @@ -239,37 +275,51 @@ struct llama_ple_disk::impl { ++gen; } cv_work.notify_all(); + run_items(bounce0); std::unique_lock<std::mutex> lk(pm); cv_done.wait(lk, [&] { return pending == 0; }); } - void gather(const int32_t * idx, size_t n, float * dst) { + void gather(const int32_t * idx, size_t n, float * out) { std::lock_guard<std::mutex> lk(mtx); const auto t0 = std::chrono::steady_clock::now(); + GGML_ASSERT(n < UINT32_MAX); - uniq.assign(idx, idx + n); - std::sort(uniq.begin(), uniq.end()); - uniq.erase(std::unique(uniq.begin(), uniq.end()), uniq.end()); - if (!uniq.empty() && (uniq.front() < 0 || (int64_t) uniq.back() >= nrows)) { + // sort the positions by row so equal rows are adjacent: each distinct row is read and dequantized once + order.resize(n); + for (size_t k = 0; k < n; ++k) { + order[k] = { idx[k], (uint32_t) k }; + } + std::sort(order.begin(), order.end()); + if (n && (order.front().first < 0 || (int64_t) order.back().first >= nrows)) { GGML_ABORT("llama_ple_disk: row index out of range (%d..%d of %lld rows)", - uniq.front(), uniq.back(), (long long) nrows); + order.front().first, order.back().first, (long long) nrows); } + runs.clear(); + for (size_t k = 0; k < n; ++k) { + if (k == 0 || order[k].first != order[k - 1].first) { + runs.push_back((uint32_t) k); + } + } + const size_t n_uniq = runs.size(); + runs.push_back((uint32_t) n); - raw.resize(uniq.size() * rs); + raw.resize(n_uniq * rs); misses.clear(); - for (size_t i = 0; i < uniq.size(); ++i) { + for (size_t u = 0; u < n_uniq; ++u) { + const int32_t row = order[runs[u]].first; if (n_slots) { - const size_t slot = (size_t) uniq[i] & mask; - if (tags[slot] == uniq[i]) { - memcpy(raw.data() + i * rs, slab.data() + slot * rs, rs); + const size_t slot = (size_t) row & mask; + if (tags[slot] == row) { + memcpy(raw.data() + u * rs, slab.data() + slot * rs, rs); continue; } } - misses.emplace_back(uniq[i], (uint32_t) i); + misses.emplace_back(row, (uint32_t) u); } if (!misses.empty()) { - run_misses(); + run_job(job_kind::read, misses.size(), 1, 2); if (n_slots) { for (const auto & m : misses) { const size_t slot = (size_t) m.first & mask; @@ -279,21 +329,14 @@ struct llama_ple_disk::impl { } } - for (size_t k = 0; k < n; ++k) { - const size_t i = (size_t) (std::lower_bound(uniq.begin(), uniq.end(), idx[k]) - uniq.begin()); - const uint8_t * src = raw.data() + i * rs; - float * out = dst + k * (size_t) ne0; - if (to_float) { - to_float(src, out, ne0); - } else { - memcpy(out, src, rs); - } - } + dst = out; + run_job(job_kind::dequant, n_uniq, 64, 256); + dst = nullptr; st_calls += 1; st_rows += n; - st_uniq += uniq.size(); - st_hits += uniq.size() - misses.size(); + st_uniq += n_uniq; + st_hits += n_uniq - misses.size(); st_reads += misses.size(); st_bytes += misses.size() * (direct ? block : rs); st_ms += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count(); @@ -322,6 +365,31 @@ void llama_ple_disk::gather(const int32_t * idx, size_t n, float * dst) { #endif } +void llama_ple_disk::prefetch(const int32_t * idx, size_t n) const { +#if defined(_WIN32) + GGML_UNUSED(idx); GGML_UNUSED(n); +#else + if (pimpl->direct || pimpl->fd < 0 || n == 0) { + return; + } + std::vector<int32_t> uniq; + uniq.reserve(n); + for (size_t i = 0; i < n; ++i) { + if (idx[i] >= 0 && (int64_t) idx[i] < pimpl->nrows) { + uniq.push_back(idx[i]); + } + } + std::sort(uniq.begin(), uniq.end()); + uniq.erase(std::unique(uniq.begin(), uniq.end()), uniq.end()); + for (const int32_t row : uniq) { + const off_t off = (off_t) pimpl->offs + (off_t) row * (off_t) pimpl->rs; + posix_fadvise(pimpl->fd, off, (off_t) pimpl->rs, POSIX_FADV_WILLNEED); + } +#endif +} + +bool llama_ple_disk::page_cached() const { return !pimpl->direct; } + int64_t llama_ple_disk::n_rows() const { return pimpl->nrows; } int64_t llama_ple_disk::ne0() const { return pimpl->ne0; } size_t llama_ple_disk::row_size() const { return pimpl->rs; } diff --git a/src/llama-ple-disk.h b/src/llama-ple-disk.h index e8af7c58ded3..ae90681efa1e 100644 --- a/src/llama-ple-disk.h +++ b/src/llama-ple-disk.h @@ -28,6 +28,13 @@ struct llama_ple_disk { // dequantize rows idx[0..n) into dst, laid out [ne0, n]; aborts on an out-of-range index void gather(const int32_t * idx, size_t n, float * dst); + // ask the kernel to read rows idx[0..n) into the page cache ahead of a later gather; only advice, so a + // wrong prediction costs readahead and nothing else. A no-op with O_DIRECT, whose reads bypass the + // page cache. Safe to call from another thread while a gather runs. + void prefetch(const int32_t * idx, size_t n) const; + + bool page_cached() const; // false when reads bypass the page cache (O_DIRECT) + int64_t n_rows() const; int64_t ne0() const; size_t row_size() const; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 359454399fef..fe5dec963730 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -1,10 +1,12 @@ #include "models.h" +#include "../prefix.h" #include "llama-impl.h" #include "llama-memory-hybrid-idx.h" #include "llama-memory-recurrent.h" #include "llama-ple-disk.h" #include <algorithm> +#include <thread> #include <cinttypes> #include <cstdint> #include <cstring> @@ -32,6 +34,18 @@ static void qwen4exp_require_arr_len(llama_model_loader & ml, llm_kv kid, uint32 } } +static const llama_model & qwen4exp_shared_model(const llama_cparams & cparams, const llama_model & model, const char * name) { + if (cparams.ctx_other == nullptr) { + throw std::runtime_error(format("QWEN4EXP MTP: this draft head has no '%s' of its own; " + "load it as a draft of its target model (-md), not on its own", name)); + } + const llama_model & other = *llama_get_model(cparams.ctx_other); + if (other.hparams.n_embd != model.hparams.n_embd || other.vocab.n_tokens() != model.vocab.n_tokens()) { + throw std::runtime_error(format("QWEN4EXP MTP: draft and target disagree on the shape of '%s'", name)); + } + return other; +} + void llama_model_qwen4exp::load_arch_hparams(llama_model_loader & ml) { ml.get_key_or_arr(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp_arr, hparams.n_layer_all, false); ml.get_key(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp, false); @@ -72,6 +86,23 @@ void llama_model_qwen4exp::load_arch_hparams(llama_model_loader & ml) { qwen4exp_require_nonzero(ml, LLM_KV_ATTENTION_INDEXER_TOP_K, hparams.indexer_top_k); ml.get_key_or_arr(LLM_KV_ATTENTION_COMPRESS_RATIOS, hparams.dsv4_compress_ratios, hparams.n_layer_all, false); + { // The converted GGUF leaves the nextn/MTP layer's compress ratio at 0, but the MTP sidecar ships + // blk.N.indexer.* and the reference runs that layer with the same sparse attention as the trunk, so + // inherit the trunk's ratio. + if (hparams.n_layer_nextn > 0 && hparams.indexer_head_size > 0) { + int32_t trunk_r = 0; + for (uint32_t j = 0; j < hparams.n_layer(); ++j) { + if (hparams.dsv4_compress_ratios[j] > 0) { trunk_r = hparams.dsv4_compress_ratios[j]; } + } + for (uint32_t j = hparams.n_layer(); j < hparams.n_layer_all && trunk_r > 0; ++j) { + if (hparams.dsv4_compress_ratios[j] == 0) { + hparams.dsv4_compress_ratios[j] = trunk_r; + LLAMA_LOG_INFO("%s: nextn layer %u compress ratio 0 -> %d\n", __func__, j, trunk_r); + } + } + } + } + // PLE n-gram hash embeddings; if the key group is absent every field stays zero hparams.is_ple_impl.reset(); hparams.ple_n_heads = 0; @@ -165,15 +196,20 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { const int64_t hc_dim = hc * n_embd; const int64_t hc_lr = hparams.hc_low_rank; - tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), { n_embd, n_vocab }, 0); + // An MTP-only file carries just the draft block; a "shared" one also leaves the token embeddings, the + // final mixer and the LM head to the target model it is loaded against (ctx_other) + const bool mtp_only = hparams.n_layer_nextn > 0 && ml.get_weight("blk.0.hc_attn_norm.weight") == nullptr; + const int trunk_flags = mtp_only ? TENSOR_NOT_REQUIRED : 0; + + tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), { n_embd, n_vocab }, trunk_flags); // there is no output_norm: the final hyper-connection mixer carries it - hc_head_norm = create_tensor(tn(LLM_TENSOR_HC_HEAD_NORM, "weight"), { hc_dim }, 0); - hc_head_down = create_tensor(tn(LLM_TENSOR_HC_HEAD_DOWN, "weight"), { hc_dim, hc_lr }, 0); - hc_head_up = create_tensor(tn(LLM_TENSOR_HC_HEAD_UP, "weight"), { hc_lr, hc_dim }, 0); + hc_head_norm = create_tensor(tn(LLM_TENSOR_HC_HEAD_NORM, "weight"), { hc_dim }, trunk_flags); + hc_head_down = create_tensor(tn(LLM_TENSOR_HC_HEAD_DOWN, "weight"), { hc_dim, hc_lr }, trunk_flags); + hc_head_up = create_tensor(tn(LLM_TENSOR_HC_HEAD_UP, "weight"), { hc_lr, hc_dim }, trunk_flags); output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), { n_embd, n_vocab }, TENSOR_NOT_REQUIRED); - if (output == NULL) { + if (output == NULL && tok_embd != NULL) { output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), { n_embd, n_vocab }, TENSOR_DUPLICATED); } @@ -199,11 +235,15 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { // on disk the tensor is counted as created but never allocated, mapped or read: each // ubatch preads exactly the rows it gathers (llm_graph_input_ple::set_input). A model // with no tensor to read from stays on the lazy path. - if (params.ple_on_disk && ple_w) { + // --lazy-mode on-direct is the same reader through the page cache (explicit preads instead of + // demand paging the mmap), with the --ngram-* settings if given + const bool ple_on_disk = params.ple_on_disk || params.lazy_mode == LLAMA_LAZY_MODE_DIRECT; + + if (ple_on_disk && ple_w) { llama_ple_disk::params dp; dp.n_threads = params.ple_io_threads; dp.cache_bytes = params.ple_cache_mb > 0 ? (size_t) params.ple_cache_mb << 20 : 0; - dp.direct_io = params.ple_direct_io; + dp.direct_io = params.ple_on_disk ? params.ple_direct_io : false; ple_disk = std::make_shared<llama_ple_disk>(ml.fnames.at(ple_w->idx), ple_w->offs, ple_w->tensor->type, ple_w->tensor->ne[0], ple_rows, dp); @@ -217,10 +257,7 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { } } - // An MTP-only file carries just the draft block. Keep walking the trunk so the - // per-layer bookkeeping still runs, but let its tensors be absent. - const bool mtp_only = hparams.n_layer_nextn > 0 && ml.get_weight("blk.0.hc_attn_norm.weight") == nullptr; - const int trunk_flags = mtp_only ? TENSOR_NOT_REQUIRED : 0; + // keep walking the trunk of an MTP-only file so the per-layer bookkeeping still runs, with its tensors absent for (int il = 0; il < n_layer; ++il) { auto & layer = layers[il]; @@ -314,8 +351,9 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, flags); layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, flags); - // the draft attends dense, so the indexer weights are present but never read - const int idx_flags = flags | TENSOR_NOT_REQUIRED | TENSOR_SKIP; + // the draft runs the trunk's sparse attention when its context carries an indexer cache, so load the + // indexer weights when the sidecar ships them (a sidecar without them attends dense) + const int idx_flags = flags | TENSOR_NOT_REQUIRED; layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", il), { n_embd, hparams.indexer_n_head * idx_dim }, idx_flags); layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", il), { n_embd, idx_dim }, idx_flags); layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", il), { idx_dim }, idx_flags); @@ -334,6 +372,27 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { layer.nextn.enorm = create_tensor(tn(LLM_TENSOR_NEXTN_ENORM, "weight", il), { n_embd }, flags); layer.nextn.hnorm = create_tensor(tn(LLM_TENSOR_NEXTN_HNORM, "weight", il), { hc_dim }, flags); layer.nextn.eh_proj = create_tensor(tn(LLM_TENSOR_NEXTN_EH_PROJ, "weight", il), { 2 * n_embd, n_embd }, flags); + + // the draft head's own final mixer, and optional private embeddings / LM head; a shared sidecar has neither + // of the latter and borrows the target's at graph time + // optional: a draft-only file converted before the nextn.hc_head_* names existed stores the draft's + // mixer under the trunk's output_hc_* names, and a full file from that converter dropped it + layer.nextn.hc_head_norm = create_tensor(tn(LLM_TENSOR_NEXTN_HC_HEAD_NORM, "weight", il), { hc_dim }, flags | TENSOR_NOT_REQUIRED); + layer.nextn.hc_head_down = create_tensor(tn(LLM_TENSOR_NEXTN_HC_HEAD_DOWN, "weight", il), { hc_dim, hc_lr }, flags | TENSOR_NOT_REQUIRED); + layer.nextn.hc_head_up = create_tensor(tn(LLM_TENSOR_NEXTN_HC_HEAD_UP, "weight", il), { hc_lr, hc_dim }, flags | TENSOR_NOT_REQUIRED); + if ((layer.nextn.hc_head_norm != nullptr) != (layer.nextn.hc_head_down != nullptr) || + (layer.nextn.hc_head_norm != nullptr) != (layer.nextn.hc_head_up != nullptr)) { + throw std::runtime_error(format("MTP block %d has an incomplete nextn.hc_head_* mixer", il)); + } + if (layer.nextn.hc_head_norm == nullptr && hc_head_norm == nullptr) { + throw std::runtime_error(format("MTP block %d has no final mixer (neither nextn.hc_head_* nor output_hc_*)", il)); + } + if (layer.nextn.hc_head_norm == nullptr && !mtp_only) { + LLAMA_LOG_WARN("%s: MTP block %d has no nextn.hc_head_* mixer, using the trunk's output_hc_* mixer for the draft (reconvert for the draft's own)\n", __func__, il); + } + + layer.nextn.embed_tokens = create_tensor(tn(LLM_TENSOR_NEXTN_EMBED_TOKENS, "weight", il), { n_embd, n_vocab }, flags | TENSOR_NOT_REQUIRED); + layer.nextn.shared_head_head = create_tensor(tn(LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, "weight", il), { n_embd, n_vocab }, flags | TENSOR_NOT_REQUIRED); } } @@ -342,6 +401,11 @@ std::unique_ptr<llm_graph_context> llama_model_qwen4exp::build_arch_graph(const if (params.gtype == LLM_GRAPH_TYPE_DECODER_MTP) { return std::make_unique<graph_mtp>(*this, params); } + // a draft-only file may still carry a mixer under output_hc_*, so test the trunk itself + if (hc_head_norm == nullptr || layers.empty() || layers[0].hc_attn_norm == nullptr) { + throw std::runtime_error("this model is an MTP draft head without a trunk; " + "load it as a draft of its target model (-md), not on its own"); + } return std::make_unique<graph>(*this, params); } @@ -378,7 +442,12 @@ llama_model_qwen4exp::graph_mtp::graph_mtp(const llama_model & model, const llm_ ggml_set_input(inp->h); ggml_set_name(inp->h, "mtp_h_input"); - ggml_tensor * tok_embd = ggml_get_rows(ctx0, model.tok_embd, inp->tokens); + // the draft's own embeddings if it ships them, else the target's (a shared sidecar) + ggml_tensor * tok_embd_w = layer.nextn.embed_tokens ? layer.nextn.embed_tokens : model.tok_embd; + if (tok_embd_w == nullptr) { + tok_embd_w = qwen4exp_shared_model(cparams, model, "token_embd.weight").tok_embd; + } + ggml_tensor * tok_embd = ggml_get_rows(ctx0, tok_embd_w, inp->tokens); cb(tok_embd, "mtp_tok_embd", il); ggml_tensor * h = inp->h; @@ -386,9 +455,19 @@ llama_model_qwen4exp::graph_mtp::graph_mtp(const llama_model & model, const llm_ ggml_tensor * inp_pos = build_inp_pos(); ggml_tensor * inp_out_ids = build_inp_out_ids(); - // The draft block is the only layer in an MTP context and attends dense, so it takes a - // plain attention input. create_memory gives that context an attention-only filter. - auto * inp_attn = build_attn_inp_kv(); + // The draft block is the only layer in an MTP context. With an indexer the context holds a hybrid-idx + // memory (create_memory) and the block runs the trunk's sparse attention; otherwise a plain attention input. + llm_graph_input_attn_kv * inp_attn = nullptr; + const llama_memory_hybrid_idx_context * mctx_hyb = nullptr; + if (hparams.indexer_head_size > 0) { + auto * inp_hyb = build_inp_mem_hybrid(); + const auto * m = static_cast<const llama_memory_hybrid_idx_context *>(inp_hyb->mctx); + if (m->get_idx() != nullptr && layer.index_q_proj && layer.index_k_proj && layer.index_q_norm && layer.index_k_norm) { + mctx_hyb = m; + inp_attn = inp_hyb->get_attn(); + } + } + if (!inp_attn) { inp_attn = build_attn_inp_kv(); } // Grouped RMSNorm: each hc stream is normed over its own n_embd, and gamma is applied // across the whole [hc*n_embd] row. Norming the full row instead couples the streams @@ -417,9 +496,9 @@ llama_model_qwen4exp::graph_mtp::graph_mtp(const llama_model & model, const llm_ layer.hc_attn_norm, layer.hc_attn_down, layer.hc_attn_up, layer.hc_attn_inject, &inject, il); cb(cur, "mtp_hc_attn_pre", il); - // dense attention for the draft: a QSA indexer would need a cache of its own, and one - // block spends its time reading weights rather than attending - cur = build_layer_attn(inp_attn, nullptr, cur, inp_pos, sections, il); + // sparse attention for the draft when its context carries an indexer cache (the sidecar ships the + // block's indexer projections and the trunk's compress ratio is inherited at load), else dense + cur = build_layer_attn(inp_attn, mctx_hyb, cur, inp_pos, sections, il); inpL = build_hc_combine(inpL, cur, inject, il); cb(inpL, "mtp_hc_attn_post", il); @@ -443,11 +522,28 @@ llama_model_qwen4exp::graph_mtp::graph_mtp(const llama_model & model, const llm_ inpL = ggml_reshape_3d(ctx0, flat, n_embd, hc, n_outputs); } - cur = build_hc_mix(inpL, model.hc_head_norm, model.hc_head_down, model.hc_head_up, nullptr, nullptr, -1); - cb(cur, "result_norm", -1); - res->t_embd = cur; + // the draft head's own final mixer (the trunk's is trained for the trunk's residual, not the draft's) + // a draft-only file from the older converter holds the draft's mixer under output_hc_* instead + const bool own_mix = layer.nextn.hc_head_norm != nullptr; + GGML_ASSERT((own_mix || model.hc_head_norm) && "MTP block has no final mixer"); + cur = build_hc_mix(inpL, + own_mix ? layer.nextn.hc_head_norm : model.hc_head_norm, + own_mix ? layer.nextn.hc_head_down : model.hc_head_down, + own_mix ? layer.nextn.hc_head_up : model.hc_head_up, nullptr, nullptr, -1); + cb(cur, "mtp_hc_head", -1); + + // no res->t_embd: it is n_embd wide, but the context sizes that buffer by n_embd_out. + + ggml_tensor * head_w = layer.nextn.shared_head_head ? layer.nextn.shared_head_head : model.output; + ggml_tensor * head_s = layer.nextn.shared_head_head ? layer.nextn.shared_head_head_s : model.output_s; + if (head_w == nullptr) { + const llama_model & other = qwen4exp_shared_model(cparams, model, "output.weight"); + head_w = other.output; + head_s = other.output_s; + GGML_ASSERT(head_w && "QWEN4EXP MTP: the target model has no LM head to borrow"); + } - cur = build_lora_mm(model.output, cur, model.output_s); + cur = build_lora_mm(head_w, cur, head_s); cb(cur, "result_output", -1); res->t_logits = cur; @@ -522,12 +618,6 @@ ggml_tensor * llama_model_qwen4exp::graph::build_hc_combine( const int64_t hc = hparams.dsv4_hc_mult; const int64_t nt = residual->ne[2]; - // block_out and inject die at this combine; keep their buffers from being handed to the next grouped norm - // output (the backend fuses combine + norm into one kernel whose blocks all read block_out/inject while - // writing their own stream of the norm output, so the two must not alias) - ggml_set_output(block_out); - ggml_set_output(inject); - // 2*sigmoid centres the scatter weights on 1, so a zero injection is a plain residual add ggml_tensor * w = ggml_sigmoid(ctx0, ggml_scale(ctx0, inject, 1.0f / (float) hc)); w = ggml_scale(ctx0, w, 2.0f); @@ -709,6 +799,114 @@ ggml_tensor * llama_model_qwen4exp::graph::build_norm_gated( return ggml_mul(ctx0, normalized, gated); } +// Scoring strips: the scorer scratch is [n_blocks, strip] instead of [n_blocks, n_tokens], and each strip is +// trimmed to the blocks it can see. 512 queries keep the scratch in the Infinity Cache on gfx1151. +static int64_t qwen4exp_query_strip(int64_t n_tokens, int64_t n_stream) { + return n_stream == 1 ? std::min<int64_t>(n_tokens, 512) : n_tokens; +} + +// Complete-block selection: pick indexer_top_k/ratio whole blocks that lie before the query's own block and +// append the query's own partial block as the tail (up to ratio-1 cells), -1 where nothing is visible. The +// selected rows then carry the visibility themselves, so attention can run without a mask. Only the selected-key +// attention kernels understand that layout (F16 K/V, head 256, single stream, decode-sized or >= 128-query +// ubatches), so the selection is only used where one of them will take the op. +static bool qwen4exp_use_block_selection(bool blk_bias, int64_t n_stream, int64_t ratio, int64_t n_kv, + const llama_ubatch & ubatch, const llama_cparams & cparams, const llama_hparams & hparams, + ggml_type type_k, ggml_type type_v) { + const int64_t n_tps = n_stream > 0 ? ubatch.n_tokens/n_stream : 0; + return blk_bias && n_stream==1 && ratio>1 && hparams.indexer_top_k%ratio==0 && + n_kv>hparams.indexer_top_k+ratio-1 && n_kv<=16777216 && ubatch.token && + cparams.flash_attn && cparams.offload_kqv && hparams.f_max_alibi_bias==0.0f && + !hparams.attn_soft_cap && hparams.n_embd_head_k()==256 && hparams.n_embd_head_v()==256 && + type_k==GGML_TYPE_F16 && type_v==GGML_TYPE_F16 && (n_tps<=8 || n_tps>=128); +} + +// Visible-prefix scoring bound. For one sequence with unique non-negative positions the indexer enumerates +// complete groups in increasing logical block order, so a compressed column's ordinal cannot exceed its logical +// block number: every block visible to any query in a strip lies in the first (max_query_position + 1)/ratio +// columns. Holes and physical cell permutations do not invalidate that upper bound. Blocks past it are invisible +// in the metadata anyway, so trimming them must not change the selection. +static std::vector<int64_t> qwen4exp_score_key_limits(const llama_memory_hybrid_idx_context * mctx, + const llama_ubatch & ubatch, int64_t blocks, int64_t strip, int64_t ratio, int64_t budget, bool compact) { + // llama_context::graph_reserve builds a worst-case graph from a synthetic ubatch whose positions are all 0. + // Bounding that graph would reserve compute buffers for a narrow scorer and then execute full-width ones, so a + // many-token ubatch whose positions are all identical is treated as synthetic and left unbounded. + bool degenerate_pos = ubatch.pos && ubatch.n_tokens > 1; + for (uint32_t i = 1; degenerate_pos && i < ubatch.n_tokens; ++i) { + if (ubatch.pos[i] != ubatch.pos[0]) { degenerate_pos = false; } + } + if (!compact || ubatch.n_tokens<128 || degenerate_pos || !mctx->qsa_position_prefix(ubatch)) { + return {}; + } + return qsa_prefix_limits(ubatch.pos,ubatch.n_tokens,strip,ratio,blocks,budget); +} + +// compact visibility: limits = [block start position per block (INT32_MAX when unfinished)] ++ [tail start per +// query]; a block is visible to a query iff its start < the query's tail start. log(step(.)) is 0 or -inf. +static ggml_tensor * qwen4exp_apply_compact_visibility(ggml_context *ctx,ggml_tensor *score, + ggml_tensor *limits,int64_t blocks,int64_t first,int64_t queries) { + GGML_ASSERT(limits->type==GGML_TYPE_I32 && score->ne[0]<=blocks && score->ne[1]==queries && score->ne[2]==1); + auto *starts=ggml_cast(ctx,ggml_view_1d(ctx,limits,score->ne[0],0),GGML_TYPE_F32); + auto *tails=ggml_cast(ctx,ggml_view_1d(ctx,limits,queries,(blocks+first)*sizeof(int32_t)),GGML_TYPE_F32); + tails=ggml_reshape_2d(ctx,tails,1,queries); + auto *visible=ggml_step(ctx,ggml_sub(ctx,ggml_repeat(ctx,tails,score),starts)); + return ggml_add(ctx,score,ggml_log(ctx,visible)); +} + +// top block_budget blocks by score, in ascending block order, expanded to their cells (-1 for an invisible block: +// its score is -inf, so step(score + 1) is 0), followed by the tail cells +static ggml_tensor * qwen4exp_select_complete_blocks(ggml_context *ctx0, ggml_tensor *score, + ggml_tensor *block_cells, ggml_tensor *tail, int64_t block_budget, int64_t r) { + const int64_t n_blocks=score->ne[0], n_query=score->ne[1], n_stream=score->ne[2]; + GGML_ASSERT(score->type==GGML_TYPE_F32 && ggml_is_contiguous(score)); + GGML_ASSERT(block_cells->type==GGML_TYPE_I32 && block_cells->ne[0]==r*n_blocks && block_cells->ne[1]==n_stream); + GGML_ASSERT(tail->type==GGML_TYPE_I32 && tail->ne[0]==r-1 && tail->ne[1]==n_query && tail->ne[3]==n_stream); + GGML_ASSERT(n_blocks>=block_budget); + ggml_tensor * blocks=ggml_cont(ctx0,ggml_top_k(ctx0,score,block_budget)); + ggml_tensor * order=ggml_argsort(ctx0,ggml_cast(ctx0,blocks,GGML_TYPE_F32),GGML_SORT_ORDER_ASC); + ggml_tensor * blocks_view=ggml_view_4d(ctx0,blocks,1,block_budget,n_query,n_stream, + blocks->nb[0],blocks->nb[1],blocks->nb[2],0); + blocks=ggml_reshape_3d(ctx0,ggml_get_rows(ctx0,blocks_view,order),block_budget,n_query,n_stream); + ggml_tensor * score_view=ggml_view_4d(ctx0,score,1,n_blocks,n_query,n_stream, + score->nb[0],score->nb[1],score->nb[2],0); + ggml_tensor * picked_score=ggml_get_rows(ctx0,score_view,blocks); + ggml_tensor * valid=ggml_step(ctx0,ggml_scale_bias(ctx0,picked_score,1.0f,1.0f)); + ggml_tensor * cells_view=ggml_reshape_3d(ctx0,block_cells,r,n_blocks,n_stream); + ggml_tensor * flat_blocks=ggml_reshape_2d(ctx0,blocks,block_budget*n_query,n_stream); + ggml_tensor * cells=ggml_get_rows(ctx0,cells_view,flat_blocks); + cells=ggml_reshape_4d(ctx0,cells,r,block_budget,n_query,n_stream); + cells=ggml_scale_bias(ctx0,ggml_cast(ctx0,cells,GGML_TYPE_F32),1.0f,1.0f); + cells=ggml_mul(ctx0,cells,valid); + cells=ggml_cast(ctx0,ggml_scale_bias(ctx0,cells,1.0f,-1.0f),GGML_TYPE_I32); + cells=ggml_reshape_4d(ctx0,cells,block_budget*r,n_query,1,n_stream); + return ggml_concat(ctx0,cells,tail,0); +} + +// strips are joined pairwise along ne[1] (balanced concat tree) +static void qwen4exp_append_strip(ggml_context * ctx, ggml_cgraph * graph, + std::vector<ggml_tensor *> & chunks, ggml_tensor * value) { + // the joined selection stays lazy: its consumer (the attention mask or the maskless kernel) pulls it in, as the + // community builder did, so the node order around the attention block is unchanged + GGML_UNUSED(graph); + chunks.push_back(value); + while (chunks.size()>1 && chunks.back()->ne[1]==chunks[chunks.size()-2]->ne[1]) { + auto * right=chunks.back();chunks.pop_back(); + auto * left=chunks.back();chunks.pop_back(); + chunks.push_back(ggml_concat(ctx,left,right,1)); + } +} + +static ggml_tensor * qwen4exp_finish_strips(ggml_context * ctx, ggml_cgraph * graph, + std::vector<ggml_tensor *> & chunks) { + GGML_UNUSED(graph); + while (chunks.size()>1) { + auto * right=chunks.back();chunks.pop_back(); + auto * left=chunks.back();chunks.pop_back(); + chunks.push_back(ggml_concat(ctx,left,right,1)); + } + return chunks.front(); +} + // QSA attends to a budget of whole blocks of compress_ratio tokens, plus the incomplete tail // one mean-pooled indexer key scores each block; set_input resolves the cache layout class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i { @@ -719,7 +917,11 @@ class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i { void set_input(const llama_ubatch * ubatch) override { mctx->get_idx()->set_input_k_idxs(k_idxs, ubatch); - mctx->set_input_qsa(cell_blk, blk_cells, blk_pos, bias, ubatch, ratio, blk_bias); + if (tail_idxs) { + mctx->set_input_qsa_blocks(cell_blk, blk_cells, blk_pos, bias, tail_idxs, ubatch, ratio); + } else { + mctx->set_input_qsa(cell_blk, blk_cells, blk_pos, bias, ubatch, ratio, blk_bias); + } } bool can_reuse(const llm_graph_params & params) override { @@ -732,7 +934,7 @@ class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i { const int64_t n_kv = idx->get_n_kv(); const int64_t n_stream = mctx->get_n_stream(); - const int64_t n_blocks = (n_kv + ratio - 1)/ratio; + const int64_t n_blocks = ((int64_t) mctx->qsa_n_kv_window() + ratio - 1)/ratio; bool res = true; @@ -743,8 +945,23 @@ class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i { res &= cell_blk->ne[1] == n_stream; res &= blk_cells->ne[0] == (int64_t) ratio*n_blocks; res &= blk_pos->ne[0] == 4*n_blocks*n_stream; - res &= bias->ne[0] == (blk_bias ? n_blocks : n_kv); - res &= bias->ne[1] == params.ubatch.n_tokens/n_stream; + res &= bias->ne[0] == (compact ? n_blocks + params.ubatch.n_tokens : (blk_bias ? n_blocks : n_kv)); + res &= compact || bias->ne[1] == params.ubatch.n_tokens/n_stream; + + // the selection mode and the strip bounds are baked into the graph, so a reused graph must agree on them + const auto * attn = mctx->get_attn(); + const bool blocks = attn && qwen4exp_use_block_selection(blk_bias, n_stream, ratio, n_kv, + params.ubatch, params.cparams, params.hparams, attn->type_k(), attn->type_v()); + const bool scalar = blocks && params.hparams.n_swa == 0 && mctx->qsa_scalar_visibility(params.ubatch); + res &= (tail_idxs != nullptr) == scalar; + res &= compact == scalar; + res &= maskless == scalar; + if (tail_idxs) { res &= tail_idxs->ne[1] == params.ubatch.n_tokens/n_stream; } + const int64_t next_strip = qwen4exp_query_strip(params.ubatch.n_tokens/n_stream, n_stream); + const auto next_limits = qwen4exp_score_key_limits(mctx, params.ubatch, n_blocks, next_strip, ratio, + params.hparams.indexer_top_k/ratio, scalar && compact); + res &= score_strip == next_strip; + res &= score_key_limits == next_limits; return res; } @@ -754,7 +971,15 @@ class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i { ggml_tensor * cell_blk = nullptr; // I32 [n_kv, n_stream] ggml_tensor * blk_cells = nullptr; // I32 [ratio*n_blocks, n_stream] ggml_tensor * blk_pos = nullptr; // I32 [4*n_blocks*n_stream] - ggml_tensor * bias = nullptr; // F32 [n_blocks or n_kv, n_tokens/n_stream, n_stream] + ggml_tensor * bias = nullptr; // F32 [n_blocks or n_kv, n_tokens/n_stream, n_stream], or the compact + // I32 [n_blocks + n_tokens] visibility limits + + // complete-block selection (see qwen4exp_use_block_selection): the query's own partial block, -1 padded + ggml_tensor * tail_idxs = nullptr; // I32 [ratio-1, n_tokens/n_stream, n_stream] + bool compact = false; // I32 limits instead of the F32 per-block bias + bool maskless = false; // attention drops the mask: the selected rows carry the visibility + int64_t score_strip = 0; + std::vector<int64_t> score_key_limits; // per strip, empty = no bound const llama_memory_hybrid_idx_context * mctx; const uint32_t ratio; @@ -789,6 +1014,30 @@ class llama_model_qwen4exp::llm_graph_input_qsa_k : public llm_graph_input_i { const llama_memory_hybrid_idx_context * mctx; }; +class llm_graph_input_qsa_cache : public llm_graph_input_i { +public: + llm_graph_input_qsa_cache(const llama_memory_hybrid_idx_context * mctx, int il, bool active, bool fast) : + mctx(mctx), il(il), active(active), fast(fast) {} + void set_input(const llama_ubatch *) override { + if (!active) { return; } + if (fast) { mctx->qsa_fill_updates(members, positions, rows); } + mctx->qsa_commit(il); + } + bool can_reuse(const llm_graph_params & params) override { + mctx = static_cast<const llama_memory_hybrid_idx_context *>(params.mctx); + return mctx->qsa_prefix_matches(params.ubatch) == active && + (!active || mctx->qsa_fast(il, params.ubatch) == fast) && + (!fast || rows->ne[0] == (params.ubatch.n_tokens+3)/4+2); + } + const llama_memory_hybrid_idx_context * mctx; + int il; + bool active; + bool fast; + ggml_tensor * members = nullptr; + ggml_tensor * positions = nullptr; + ggml_tensor * rows = nullptr; +}; + void llama_model_qwen4exp::graph::build_qsa_store_k( const llama_memory_hybrid_idx_context * mctx_hyb, ggml_tensor * cur, @@ -827,7 +1076,8 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( GGML_ASSERT(r > 0); - const int64_t n_blocks = (n_kv + r - 1)/r; + // blocks are keyed by position, which can run ahead of the occupied cells (see qsa_n_kv_window) + const int64_t n_blocks = ((int64_t) mctx_hyb->qsa_n_kv_window() + r - 1)/r; // build_attn_qsa and the KQ mask need the tokens to divide evenly across the streams const int64_t n_stream = mctx_hyb->get_n_stream(); @@ -855,12 +1105,31 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( qsa->cell_blk = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, n_kv, n_stream); qsa->blk_cells = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, r*n_blocks, n_stream); qsa->blk_pos = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, 4*n_blocks*n_stream); - qsa->bias = ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, blk_bias ? n_blocks : n_kv, n_tps, n_stream); + const auto * attn_ctx = mctx_hyb->get_attn(); + const bool blocks = attn_ctx && qwen4exp_use_block_selection(blk_bias, n_stream, r, n_kv, ubatch, cparams, hparams, + attn_ctx->type_k(), attn_ctx->type_v()); + const bool scalar = blocks && hparams.n_swa == 0 && mctx_hyb->qsa_scalar_visibility(ubatch); + qsa->compact = scalar; + qsa->maskless = scalar; + qsa->score_strip = qwen4exp_query_strip(n_tps, n_stream); + qsa->score_key_limits = qwen4exp_score_key_limits(mctx_hyb, ubatch, n_blocks, qsa->score_strip, r, + hparams.indexer_top_k/r, qsa->compact); + qsa->bias = qsa->compact ? ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_blocks + n_tps) : + ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, blk_bias ? n_blocks : n_kv, n_tps, n_stream); ggml_set_input(qsa->cell_blk); ggml_set_input(qsa->blk_cells); ggml_set_input(qsa->blk_pos); ggml_set_input(qsa->bias); + // complete-block selection lists cells with -1 sentinels (invisible blocks, empty tail slots) that only the + // maskless kernel understands; when the visibility is not scalar (2-D image positions in the cache, several + // sequences) the attention takes the masked path, whose set_rows would write row -1, so the block-expanded + // top-k with the per-block bias is used there instead + if (scalar) { + GGML_ASSERT(hparams.indexer_top_k % r == 0); + qsa->tail_idxs = ggml_new_tensor_3d(ctx0, GGML_TYPE_I32, r-1, n_tps, n_stream); + ggml_set_input(qsa->tail_idxs); + } inp = qsa.get(); res->add_input(std::move(qsa)); @@ -878,31 +1147,56 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( ggml_tensor * k_all = mctx_idx->get_k(ctx0, il); k_all = ggml_view_3d(ctx0, k_all, idx_dim, n_kv, n_stream, k_all->nb[2], k_all->nb[3], 0); - // gathers per stream: blk_cells row s indexes stream s's own cells - ggml_tensor * members = ggml_get_rows(ctx0, k_all, inp->blk_cells); - members = ggml_reshape_4d(ctx0, members, idx_dim, r, n_blocks, n_stream); - - // mean over the block members; r is small, so summing slices beats a transpose plus sum_rows - ggml_tensor * pooled = nullptr; - for (int64_t i = 0; i < r; ++i) { - ggml_tensor * slice = ggml_cont(ctx0, - ggml_view_3d(ctx0, members, idx_dim, n_blocks, n_stream, - members->nb[2], members->nb[3], i*members->nb[1])); - pooled = pooled ? ggml_add(ctx0, pooled, slice) : slice; + ggml_tensor * storage = r == 4 && blk_bias && n_stream == 1 ? mctx_hyb->qsa_cache(ctx0, il) : nullptr; + ggml_tensor * cached = storage && mctx_hyb->qsa_prefix_matches(ubatch) ? storage : nullptr; + const bool fast_cache = cached && mctx_hyb->qsa_fast(il, ubatch); + const int64_t prep_blocks = fast_cache ? (n_tokens+3)/4+2 : n_blocks; + ggml_tensor * member_rows = inp->blk_cells; + ggml_tensor * block_positions = inp->blk_pos; + ggml_tensor * cache_rows = nullptr; + if (storage) { + auto cache_input = std::make_unique<llm_graph_input_qsa_cache>(mctx_hyb, il, cached != nullptr, fast_cache); + if (fast_cache) { + member_rows = cache_input->members = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, r*prep_blocks); + block_positions = cache_input->positions = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, 4*prep_blocks); + cache_rows = cache_input->rows = ggml_new_tensor_1d(ctx0, GGML_TYPE_I64, prep_blocks); + ggml_set_input(member_rows); ggml_set_input(block_positions); ggml_set_input(cache_rows); + } + res->add_input(std::move(cache_input)); + } + const auto prepare_keys = [&](ggml_tensor * member_rows, ggml_tensor * block_positions, int64_t prep_blocks) { + ggml_tensor * members = ggml_get_rows(ctx0, k_all, member_rows); + members = ggml_reshape_4d(ctx0, members, idx_dim, r, prep_blocks, n_stream); + + ggml_tensor * pooled = nullptr; + for (int64_t i = 0; i < r; ++i) { + ggml_tensor * slice = ggml_cont(ctx0, + ggml_view_3d(ctx0, members, idx_dim, prep_blocks, n_stream, + members->nb[2], members->nb[3], i*members->nb[1])); + pooled = pooled ? ggml_add(ctx0, pooled, slice) : slice; + } + pooled = ggml_scale(ctx0, pooled, 1.0f/(float) r); + cb(pooled, "indexer_k_pooled", il); + + pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, prep_blocks*n_stream, 1); + pooled = build_norm(pooled, model.layers[il].index_k_norm, nullptr, LLM_NORM_RMS, il); + + pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, 1, prep_blocks*n_stream); + pooled = ggml_rope_multi(ctx0, pooled, block_positions, nullptr, + n_rot, sections, rope_type, n_ctx_orig, freq_base, freq_scale, + ext_factor, attn_factor, beta_fast, beta_slow); + pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, prep_blocks, n_stream); + return pooled; + }; + ggml_tensor * pooled = prepare_keys(member_rows, block_positions, prep_blocks); + if (cached) { + auto * prepared = ggml_reshape_2d(ctx0, pooled, idx_dim, prep_blocks); + auto * destination = fast_cache ? cached : ggml_view_2d(ctx0, cached, idx_dim, n_blocks, cached->nb[1], 0); + auto * written = fast_cache ? ggml_set_rows(ctx0, destination, prepared, cache_rows) : ggml_cpy(ctx0, prepared, destination); + ggml_build_forward_expand(gf, written); + pooled = ggml_view_3d(ctx0, written, idx_dim, n_blocks, 1, written->nb[1], written->nb[1]*n_blocks, 0); + cb(pooled, "indexer_k_cached", il); } - pooled = ggml_scale(ctx0, pooled, 1.0f/(float) r); - cb(pooled, "indexer_k_pooled", il); - - // count blocks along ne1: rms_norm launches gridDim.y = ne2, capped at 65535, and 262144/4 = 65536 - pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, n_blocks*n_stream, 1); - pooled = build_norm(pooled, model.layers[il].index_k_norm, nullptr, LLM_NORM_RMS, il); - - // rope wants [n_dims, n_head, n_tokens]: lay every stream's blocks flat, split after. - pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, 1, n_blocks*n_stream); - pooled = ggml_rope_multi(ctx0, pooled, inp->blk_pos, nullptr, - n_rot, sections, rope_type, n_ctx_orig, freq_base, freq_scale, - ext_factor, attn_factor, beta_fast, beta_slow); - pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, n_blocks, n_stream); cb(pooled, "indexer_k", il); ggml_tensor * q = build_lora_mm(model.layers[il].index_q_proj, cur); @@ -913,51 +1207,87 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( ext_factor, attn_factor, beta_fast, beta_slow); cb(q, "indexer_q", il); - // rectify each head dot product before the sum, as in the DeepSeek lightning indexer - // mul_mat matches ne[2], so the queries of stream s only meet the blocks of stream s - ggml_tensor * score = ggml_mul_mat(ctx0, pooled, - ggml_reshape_3d(ctx0, q, idx_dim, n_idx_h*n_tps, n_stream)); - score = ggml_reshape_4d(ctx0, score, n_blocks, n_idx_h, n_tps, n_stream); - score = ggml_relu(ctx0, score); + const int64_t strip = qwen4exp_query_strip(n_tps, n_stream); + std::vector<ggml_tensor *> selected; + for (int64_t first = 0; first < n_tps; first += strip) { + const int64_t n_query = std::min(strip, n_tps - first); + // trim the scorer to the blocks this strip can actually see + const int64_t score_blocks = inp->score_key_limits.empty() + ? n_blocks : inp->score_key_limits[first/strip]; + ggml_tensor * score_keys = pooled; + if (score_blocks < n_blocks) { + score_keys = ggml_view_3d(ctx0, pooled, idx_dim, score_blocks, n_stream, pooled->nb[1], pooled->nb[2], 0); + cb(score_keys, "indexer_k_bounded", il); + } + const bool whole = first == 0 && n_query == n_tps; // one strip: the original tensors, no views + ggml_tensor * bias = inp->compact ? nullptr : whole ? inp->bias : ggml_view_3d(ctx0, inp->bias, inp->bias->ne[0], n_query, n_stream, + inp->bias->nb[1], inp->bias->nb[2], first*inp->bias->nb[1]); + ggml_tensor * query_mask = kq_mask == nullptr ? nullptr : whole ? kq_mask : ggml_view_4d(ctx0, kq_mask, + n_kv, n_query, 1, n_stream, kq_mask->nb[1], kq_mask->nb[2], kq_mask->nb[3], first*kq_mask->nb[1]); + + // rectify each head dot product before the sum, as in the DeepSeek lightning indexer + // mul_mat matches ne[2], so the queries of stream s only meet the blocks of stream s + ggml_tensor * score = ggml_mul_mat(ctx0, score_keys, whole ? ggml_reshape_3d(ctx0, q, idx_dim, n_idx_h*n_tps, n_stream) : + ggml_view_3d(ctx0, q, idx_dim, n_idx_h*n_query, n_stream, q->nb[1], q->nb[2]*n_tps, first*q->nb[2])); + score = ggml_reshape_4d(ctx0, score, score_blocks, n_idx_h, n_query, n_stream); + score = ggml_relu(ctx0, score); + + // the heads sit side by side on ne[1] and there are only a few of them + ggml_tensor * summed = nullptr; + for (int64_t h = 0; h < n_idx_h; ++h) { + ggml_tensor * slice = ggml_view_3d(ctx0, score, score_blocks, n_query, n_stream, + score->nb[2], score->nb[3], h*score->nb[1]); + summed = summed ? ggml_add(ctx0, summed, slice) : ggml_cont(ctx0, slice); + } - // the heads sit side by side on ne[1] and there are only a few of them - ggml_tensor * summed = nullptr; - for (int64_t h = 0; h < n_idx_h; ++h) { - ggml_tensor * slice = ggml_view_3d(ctx0, score, n_blocks, n_tps, n_stream, - score->nb[2], score->nb[3], h*score->nb[1]); - summed = summed ? ggml_add(ctx0, summed, slice) : ggml_cont(ctx0, slice); - } + score = summed; + cb(score, "indexer_score", il); - score = summed; - cb(score, "indexer_score", il); + // one value per block, so it is cheaper to bias here than after the cells are expanded + if (inp->compact) { + score = qwen4exp_apply_compact_visibility(ctx0, score, inp->bias, n_blocks, first, n_query); + } else if (blk_bias) { + score = ggml_add(ctx0, score, bias); + } - // one value per block, so it is cheaper to bias here than after the cells are expanded - if (blk_bias) { - score = ggml_add(ctx0, score, inp->bias); - } + if (inp->tail_idxs) { + ggml_tensor * tail = whole ? ggml_reshape_4d(ctx0, inp->tail_idxs, r-1, n_tps, 1, n_stream) + : ggml_view_4d(ctx0, inp->tail_idxs, r-1, n_query, 1, n_stream, + inp->tail_idxs->nb[1], inp->tail_idxs->nb[2], inp->tail_idxs->nb[2], first*inp->tail_idxs->nb[1]); + ggml_tensor * block_cells = score_blocks < n_blocks ? + ggml_view_2d(ctx0, inp->blk_cells, r*score_blocks, n_stream, inp->blk_cells->nb[1], 0) : inp->blk_cells; + ggml_tensor * top_k = qwen4exp_select_complete_blocks(ctx0, score, block_cells, tail, + hparams.indexer_top_k/r, r); + cb(top_k, "indexer_top_k", il); + qwen4exp_append_strip(ctx0, gf, selected, top_k); + continue; + } - // every token of a block gets the block score; the budget is whole blocks, so top-k cuts on a block boundary - ggml_tensor * expanded = ggml_get_rows(ctx0, - ggml_cont(ctx0, ggml_permute(ctx0, score, 1, 0, 2, 3)), inp->cell_blk); - expanded = ggml_cont(ctx0, ggml_permute(ctx0, expanded, 1, 0, 2, 3)); + // every token of a block gets the block score; the budget is whole blocks, so top-k cuts on a block boundary + ggml_tensor * expanded = ggml_get_rows(ctx0, + ggml_cont(ctx0, ggml_permute(ctx0, score, 1, 0, 2, 3)), inp->cell_blk); + expanded = ggml_cont(ctx0, ggml_permute(ctx0, expanded, 1, 0, 2, 3)); - if (blk_bias) { - // flash attention keeps the mask in f16; the scores are f32 - ggml_tensor * mask = kq_mask->type == GGML_TYPE_F32 ? kq_mask : ggml_cast(ctx0, kq_mask, GGML_TYPE_F32); - expanded = ggml_add(ctx0, expanded, ggml_reshape_3d(ctx0, mask, n_kv, n_tps, n_stream)); - } else { - expanded = ggml_add(ctx0, expanded, inp->bias); - } - cb(expanded, "indexer_score_tokens", il); + if (blk_bias) { + // flash attention keeps the mask in f16; the scores are f32 + ggml_tensor * mask = query_mask->type == GGML_TYPE_F32 ? query_mask : ggml_cast(ctx0, query_mask, GGML_TYPE_F32); + expanded = ggml_add(ctx0, expanded, ggml_reshape_3d(ctx0, mask, n_kv, n_query, n_stream)); + } else { + expanded = ggml_add(ctx0, expanded, bias); + } + cb(expanded, "indexer_score_tokens", il); - // the reference returns indexer_top_k + compress_ratio - 1: whole blocks plus the tail - const int64_t width = std::min<int64_t>(n_kv, (int64_t) hparams.indexer_top_k + r - 1); + // the reference returns indexer_top_k + compress_ratio - 1: whole blocks plus the tail + const int64_t width = std::min<int64_t>(n_kv, (int64_t) hparams.indexer_top_k + r - 1); - ggml_tensor * top_k = ggml_cont(ctx0, ggml_top_k(ctx0, expanded, width)); + ggml_tensor * top_k = ggml_cont(ctx0, ggml_top_k(ctx0, expanded, width)); - // build_attn_qsa reads [n_top_k, n_batch, 1, n_stream], matching the KQ mask. - top_k = ggml_reshape_4d(ctx0, top_k, width, n_tps, 1, n_stream); - cb(top_k, "indexer_top_k", il); + // build_attn_qsa reads [n_top_k, n_batch, 1, n_stream], matching the KQ mask. + top_k = ggml_reshape_4d(ctx0, top_k, width, n_query, 1, n_stream); + cb(top_k, "indexer_top_k", il); + qwen4exp_append_strip(ctx0, gf, selected, top_k); + } + ggml_tensor * top_k = qwen4exp_finish_strips(ctx0, gf, selected); return top_k; } @@ -1003,6 +1333,22 @@ ggml_tensor * llama_model_qwen4exp::graph::build_attn_qsa( ggml_tensor * kq_mask = inp->get_kq_mask(); + // complete-block selection lists only the cells a query may see (-1 elsewhere), so the selected-key kernels + // need no mask; the dispatcher aborts if such an op ever reaches a dense kernel (ggml-cuda fattn.cu) + const auto shared_qsa = qsa_inps.find((uint32_t) hparams.dsv4_compress_ratios[il]); + const bool maskless = shared_qsa != qsa_inps.end() && shared_qsa->second->maskless; + if (maskless) { + ggml_tensor * q = q_cur; + ggml_tensor * k = mctx_cur->get_k(ctx0, il); + ggml_tensor * v = mctx_cur->get_v(ctx0, il); + ggml_tensor * cur = build_attn_mha(q, k, v, nullptr, nullptr, nullptr, nullptr, 0, kq_scale, il, top_k, 0); + cb(cur, "kqv_out", il); + if (inp->self_v_rot) { + cur = llama_mul_mat_hadamard(ctx0, cur, inp->self_v_rot); + } + return cur; + } + // prepare new kq mask - starts filled with -INFINITY ggml_tensor * kq_mask_all = ggml_fill(ctx0, kq_mask, -INFINITY); @@ -1036,7 +1382,7 @@ ggml_tensor * llama_model_qwen4exp::graph::build_attn_qsa( // TODO: enable sparse attention when we are ready // ref: https://github.com/ggml-org/llama.cpp/pull/27970 //ggml_tensor * cur = build_attn_mha(q, k, v, nullptr, kq_mask_top_k, nullptr, nullptr, top_k->ne[0], kq_scale, il); - ggml_tensor * cur = build_attn_mha(q, k, v, nullptr, kq_mask_top_k, nullptr, nullptr, 0, kq_scale, il); + ggml_tensor * cur = build_attn_mha(q, k, v, nullptr, kq_mask_top_k, nullptr, nullptr, 0, kq_scale, il, top_k, 0); cb(cur, "kqv_out", il); // the rotation is its own inverse, so undo it on the value side of the output @@ -1398,6 +1744,54 @@ class llm_graph_input_ple : public llm_graph_input_i { std::vector<llama_token> prev; }; +// Prefetch hook, called once per llama_decode with the whole batch: the chunk-boundary stall is the PLE row gather +// (hundreds of thousands of scattered ~90-byte reads with the GPU idle). The same row indices are computed here for +// a contiguous single-sequence prefill and the page cache is warmed for them while the previous chunk is still on +// the GPU. Advice only, so a wrong prediction (multiple sequences, an image batch) costs readahead and nothing else. +void qwen4exp_ple_prefetch(const llama_model & model_base, const llama_token * tokens, int32_t n_tokens) { + if (!tokens || n_tokens < 4096) { + return; + } + const auto & pmodel = static_cast<const llama_model_qwen4exp &>(model_base); + if (!pmodel.ple_disk || !pmodel.ple_disk->page_cached()) { + return; + } + const auto & hp = pmodel.hparams; + const int64_t n_gram = hp.ple_ngram_size, n_heads = hp.ple_n_heads, per_gram = hp.ple_heads_per_ngram; + const int64_t eos = hp.ple_eos_token_id; + if (n_heads <= 0 || n_gram < 2) { + return; + } + std::vector<llama_token> toks(tokens, tokens + n_tokens); + std::thread([&pmodel, &hp, toks = std::move(toks), n_gram, n_heads, per_gram, eos]() { + const int64_t n = (int64_t) toks.size(); + std::vector<int32_t> idx((size_t) n_heads * n); + std::vector<int64_t> ctx(n_gram); + for (int64_t i = 0; i < n; ++i) { + ctx[0] = toks[i]; + bool cut = false; + for (int64_t s = 1; s < n_gram; ++s) { + const int64_t j = i - s; // contiguous single-sequence prefill + const llama_token t = (cut || j < 0) ? LLAMA_TOKEN_NULL : toks[j]; + cut = cut || t < 0 || t == eos; + ctx[s] = cut ? eos : t; + } + for (int64_t g = 2; g <= n_gram; ++g) { + uint64_t mixed = (uint64_t) ctx[0] * hp.ple_layer_multipliers[0]; + for (int64_t j = 1; j < g; ++j) { + mixed ^= (uint64_t) ctx[j] * hp.ple_layer_multipliers[j]; + } + const int64_t base = (g - 2) * per_gram; + for (int64_t q = 0; q < per_gram; ++q) { + const int64_t h_i = base + q; + idx[(size_t) i * n_heads + h_i] = (int32_t) (mixed % hp.ple_head_vocab_sizes[h_i] + hp.ple_head_offsets[h_i]); + } + } + } + pmodel.ple_disk->prefetch(idx.data(), idx.size()); + }).detach(); +} + void llm_graph_input_ple::set_input(const llama_ubatch * ubatch) { const auto & hp = pmodel.hparams; diff --git a/src/prefix.h b/src/prefix.h new file mode 100644 index 000000000000..3ca0d1c43985 --- /dev/null +++ b/src/prefix.h @@ -0,0 +1,34 @@ +#pragma once + +#include "llama-kv-cells.h" +#include <algorithm> +#include <cstdint> +#include <vector> + +static bool qsa_single_sequence_prefix(const llama_kv_cells & cells, uint32_t count, llama_seq_id seq) { + if (count>cells.size()) { return false; } + std::vector<llama_pos> positions; + positions.reserve(count); + for (uint32_t i=0;i<count;++i) { + if (cells.is_empty(i)) { continue; } + if (cells.seq_get_all(i).count()!=1 || !cells.seq_has(i,seq) || cells.pos_get(i)<0) { return false; } + positions.push_back(cells.pos_get(i)); + } + std::sort(positions.begin(),positions.end()); + return std::adjacent_find(positions.begin(),positions.end())==positions.end(); +} + +static std::vector<int64_t> qsa_prefix_limits(const llama_pos * pos, int64_t tokens, int64_t strip, + int64_t ratio, int64_t blocks, int64_t budget) { + if (!pos || tokens<=0 || strip<=0 || ratio<=0 || budget<=0 || blocks<budget) { return {}; } + std::vector<int64_t> limits; + for (int64_t first=0;first<tokens;first+=strip) { + int64_t maximum=-1; + for (int64_t i=first;i<std::min(tokens,first+strip);++i) { + if (pos[i]<0 || pos[i]>=16777216) { return {}; } + maximum=std::max(maximum,int64_t(pos[i])); + } + limits.push_back(std::min(blocks,std::max(budget,(maximum+1)/ratio))); + } + return limits; +} diff --git a/src/qsa-prefix-state.h b/src/qsa-prefix-state.h new file mode 100644 index 000000000000..e2aa2e83a61f --- /dev/null +++ b/src/qsa-prefix-state.h @@ -0,0 +1,53 @@ +#pragma once +#include <algorithm> +#include <cstdint> +#include <cstddef> +#include <vector> + +struct qsa_prefix_state { + bool valid = true; + int32_t sequence = -1; + int32_t begin = 0, end = 0; + size_t previous_size = 0; + std::vector<int32_t> cells; + std::vector<int32_t> positions; + std::vector<int32_t> block_positions; + + explicit qsa_prefix_state(size_t capacity = 0) : positions(capacity, -1) {} + void reset() { + valid = true; sequence = -1; begin = end = 0; previous_size = 0; + cells.clear(); block_positions.clear(); std::fill(positions.begin(), positions.end(), -1); + } + void invalidate() { valid = false; } + void truncate(size_t size) { + size = std::min(size, cells.size()); + for (size_t i = size; i < cells.size(); ++i) { positions[cells[i]] = -1; } + cells.resize(size); block_positions.resize(size/4); + if (cells.empty()) { sequence = -1; } + } + bool apply(int32_t seq, int32_t start, const std::vector<uint32_t> & slots) { + if (!valid || slots.empty() || start < 0 || size_t(start) > cells.size() || + (sequence >= 0 && sequence != seq) || size_t(start)+slots.size() > positions.size()) { + invalidate(); return false; + } + const int32_t finish = start + slots.size(); + for (size_t i = 0; i < slots.size(); ++i) { + const uint32_t cell = slots[i]; + if (cell >= positions.size()) { invalidate(); return false; } + const int32_t old = positions[cell]; + if (old >= 0 && (old < start || old >= finish)) { invalidate(); return false; } + } + for (size_t i = start; i < std::min(size_t(finish), cells.size()); ++i) { + if (slots[i-start] != uint32_t(cells[i])) { invalidate(); return false; } + } + previous_size = cells.size(); begin = start; end = finish; sequence = seq; + for (size_t i = start; i < std::min(size_t(finish), cells.size()); ++i) { positions[cells[i]] = -1; } + cells.resize(std::max(cells.size(), size_t(finish)), -1); + for (size_t i = 0; i < slots.size(); ++i) { + if (positions[slots[i]] >= 0) { invalidate(); return false; } + cells[start+i] = slots[i]; positions[slots[i]] = start+i; + } + while (block_positions.size() < cells.size()/4) { block_positions.push_back(block_positions.size()*4); } + return true; + } +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2e1f67df181c..e013c9a984c3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -148,6 +148,8 @@ if (LLAMA_LLGUIDANCE) llama_build_and_test(test-grammar-llguidance.cpp ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-llama-bpe.gguf) endif () +llama_build_and_test(test-qsa-prefix.cpp) +llama_build_and_test(test-ple-disk.cpp) llama_build(test-recurrent-state-rollback.cpp) llama_build(test-save-load-state.cpp) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 37602375fe3b..95676227fbc0 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -1242,6 +1242,7 @@ struct test_case { virtual bool run_whole_graph() { return false; } virtual std::vector<ggml_tensor *> fusion_test_nodes() { return {}; } + virtual bool use_scheduler_allocation() { return false; } virtual bool use_weight_context() { return false; } ggml_cgraph * gf = nullptr; @@ -1273,6 +1274,10 @@ struct test_case { } ggml_tensor * sentinel = ::ggml_new_tensor_1d(ctx, GGML_TYPE_F32, sentinel_size); ggml_format_name(sentinel, "sent_%zu", sentinels.size()); + if (use_scheduler_allocation()) { + ggml_set_input(sentinel); + ggml_set_output(sentinel); + } sentinels.push_back(sentinel); } @@ -1414,12 +1419,16 @@ struct test_case { ggml_backend_buffer_set_usage(buf_weights.get(), GGML_BACKEND_BUFFER_USAGE_WEIGHTS); } - // allocate - ggml_backend_buffer_ptr buf(ggml_backend_alloc_ctx_tensors(ctx.get(), backend1)); - - if (buf == NULL) { - printf("failed to allocate tensors [%s] ", ggml_backend_name(backend1)); - return test_status_t::FAIL; + // allocate: through the scheduler when the fusion under test depends on graph_optimize (allocation + // dependencies, marks); the scheduler needs the finished graph, so that path allocates below + ggml_backend_buffer_ptr buf(nullptr); + ggml_backend_sched_ptr sched(nullptr); + if (!use_scheduler_allocation()) { + buf.reset(ggml_backend_alloc_ctx_tensors(ctx.get(), backend1)); + if (buf == NULL) { + printf("failed to allocate tensors [%s] ", ggml_backend_name(backend1)); + return test_status_t::FAIL; + } } // build graph @@ -1430,6 +1439,18 @@ struct test_case { ggml_graph_add_node(gf, sentinel); } + if (use_scheduler_allocation()) { + ggml_backend_t backends[] = {backend1, backend2}; + sched.reset(ggml_backend_sched_new(backends, nullptr, 2, GGML_DEFAULT_GRAPH_SIZE, false, true)); + for (ggml_tensor * tensor = ggml_get_first_tensor(ctx.get()); tensor; tensor = ggml_get_next_tensor(ctx.get(), tensor)) { + ggml_backend_sched_set_tensor_backend(sched.get(), tensor, backend1); + } + if (!ggml_backend_sched_alloc_graph(sched.get(), gf)) { + printf("failed to allocate graph through the scheduler [%s] ", ggml_backend_name(backend1)); + return test_status_t::FAIL; + } + } + // randomize tensors initialize_tensors(ctx.get()); if (ctx_weights) { @@ -2343,6 +2364,10 @@ struct test_get_rows : public test_case { return VARS_TO_STR8(type, n, m, r, be1, be2, v, vs0); } + double max_nmse_err() override { + return type == GGML_TYPE_F32 || type == GGML_TYPE_F16 ? 0.0 : test_case::max_nmse_err(); + } + test_get_rows(ggml_type type = GGML_TYPE_F32, int n = 10, int m = 5, int r = 3, int be1 = 1, int be2 = 1, bool v = false, bool vs0 = false) : type(type), n(n), m(m), r(r), be1(be1), be2(be2), v(v), vs0(vs0) {} @@ -3670,6 +3695,312 @@ struct test_rms_norm : public test_case { } }; +struct test_hc_combine_norm_lifetime : public test_case { + const int embd, hc, tokens; + const bool res_inplace; + const int alias; + ggml_tensor * out_res = nullptr, * out_norm = nullptr; + test_hc_combine_norm_lifetime(int embd, int hc, int tokens, bool res_inplace=false, int alias=0) + : embd(embd), hc(hc), tokens(tokens), res_inplace(res_inplace), alias(alias) {} + std::string op_desc(ggml_tensor *) override { return "HC_COMBINE_NORM_LIFETIME"; } + std::string vars() override { return VARS_TO_STR5(embd,hc,tokens,res_inplace,alias); } + bool run_whole_graph() override { return true; } + std::vector<ggml_tensor *> fusion_test_nodes() override { return alias == 3 ? std::vector<ggml_tensor *>{out_norm} : std::vector<ggml_tensor *>{out_res,out_norm}; } + ggml_tensor * build_graph(ggml_context * ctx) override { + auto * residual_backing=ggml_new_tensor_1d(ctx,GGML_TYPE_F32,int64_t(embd)*hc*tokens+1); + auto * residual=ggml_view_3d(ctx,residual_backing,embd,hc,tokens,embd*sizeof(float),int64_t(embd)*hc*sizeof(float),0); + auto * block_backing=ggml_new_tensor_1d(ctx,GGML_TYPE_F32,int64_t(embd)*hc*tokens); + auto * block=ggml_view_3d(ctx,block_backing,embd,1,tokens,embd*sizeof(float),embd*sizeof(float),0); + auto * inject_backing=ggml_new_tensor_1d(ctx,GGML_TYPE_F32,int64_t(embd)*hc*tokens); + auto * inject=ggml_view_2d(ctx,inject_backing,hc,tokens,hc*sizeof(float),0); + auto * gamma=ggml_new_tensor_2d(ctx,GGML_TYPE_F32,embd,hc); + auto * weight=ggml_scale(ctx,ggml_sigmoid(ctx,ggml_scale(ctx,inject,1.0f/hc)),2.0f); + weight=ggml_reshape_3d(ctx,weight,1,hc,tokens); + if (gf) { ggml_build_forward_expand(gf,block);ggml_build_forward_expand(gf,weight); } + auto * expanded=ggml_repeat(ctx,block,residual); + auto * update=ggml_mul(ctx,expanded,weight); + out_res=res_inplace ? ggml_add_inplace(ctx,residual,update) : ggml_add(ctx,residual,update); + if (alias != 3) ggml_set_output(out_res); + out_norm=ggml_mul(ctx,ggml_rms_norm(ctx,out_res,1e-6f),gamma); + if(alias == 3) { out_norm->view_src=residual_backing;out_norm->view_offs=sizeof(float); } + else if(alias) { out_norm->view_src=alias==1 ? block_backing : inject_backing;out_norm->view_offs=0; } + return out_norm; + } +}; + +// qwen4exp conv history: concat(history, x^T), one tail copy per rollback slot, then either the PLE dilated +// tap chain (F16 weight columns, ADD chain, SiLU) or ssm_conv + SiLU. HIP fuses both for T >= 256 and must +// still produce every tail copy; an extra reader of the whole concat has to disable the fusion. +struct test_conv_state_chain : public test_case { + const bool ple; + const int C, T, n_slots; + const bool extra_reader; + const bool split_root; // x produced as [C/4, 4, T], as qwen4exp's grouped norm leaves it + ggml_tensor * out = nullptr; + std::vector<ggml_tensor *> verify; + test_conv_state_chain(bool ple, int C, int T, int n_slots, bool extra_reader = false, bool split_root = false) + : ple(ple), C(C), T(T), n_slots(n_slots), extra_reader(extra_reader), split_root(split_root) {} + std::string op_desc(ggml_tensor *) override { return "CONV_STATE_CHAIN"; } + std::string vars() override { return VARS_TO_STR6(ple, C, T, n_slots, extra_reader, split_root); } + bool run_whole_graph() override { return true; } + std::vector<ggml_tensor *> fusion_test_nodes() override { return verify; } + ggml_tensor * build_graph(ggml_context * ctx) override { + const int K = 4, dil = ple ? 3 : 1, H = (K - 1) * dil; + ggml_tensor * state = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, H, C); + ggml_tensor * x = split_root ? ggml_new_tensor_3d(ctx, GGML_TYPE_F32, C / 4, 4, T) : ggml_new_tensor_2d(ctx, GGML_TYPE_F32, C, T); + ggml_tensor * cache = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, (int64_t) H * C, n_slots); + ggml_set_name(state, "history"); ggml_set_name(x, "x"); ggml_set_name(cache, "cache"); + ggml_tensor * x2 = split_root ? ggml_reshape_2d(ctx, x, C, T) : x; + ggml_tensor * cc = ggml_concat(ctx, state, ggml_transpose(ctx, x2), 0); + verify.clear(); + for (int slot = 0; slot < n_slots; ++slot) { + const int64_t s_idx = std::max<int64_t>(0, (int64_t) T - slot); + ggml_tensor * tail = ggml_view_2d(ctx, cc, H, C, cc->nb[1], s_idx * sizeof(float)); + ggml_tensor * dst = ggml_view_1d(ctx, cache, (int64_t) H * C, slot * cache->nb[1]); + ggml_tensor * cpy = ggml_cpy(ctx, tail, dst); + if (gf) { ggml_build_forward_expand(gf, cpy); } + verify.push_back(cpy); + } + if (ple) { + ggml_tensor * W = ggml_new_tensor_2d(ctx, GGML_TYPE_F16, K, C); + ggml_set_name(W, "conv_w"); + ggml_tensor * acc = nullptr; + for (int k = 0; k < K; ++k) { + ggml_tensor * shifted = ggml_cont(ctx, ggml_transpose(ctx, ggml_view_2d(ctx, cc, T, C, cc->nb[1], (size_t) k * dil * sizeof(float)))); + ggml_tensor * wk = ggml_cont(ctx, ggml_view_2d(ctx, W, 1, C, W->nb[1], k * W->nb[0])); + wk = ggml_cast(ctx, ggml_reshape_1d(ctx, wk, C), GGML_TYPE_F32); + ggml_tensor * term = ggml_mul(ctx, shifted, wk); + acc = acc ? ggml_add(ctx, acc, term) : term; + } + out = ggml_silu(ctx, acc); + } else { + ggml_tensor * W = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, K, C); + ggml_set_name(W, "conv_w"); + out = ggml_silu(ctx, ggml_ssm_conv(ctx, ggml_reshape_3d(ctx, cc, T + H, C, 1), W)); + } + verify.push_back(out); + if (extra_reader) { + ggml_tensor * whole = ggml_scale(ctx, cc, 2.0f); + if (gf) { ggml_build_forward_expand(gf, whole); } + verify.push_back(whole); + } + return out; + } +}; + +struct test_mmb_quant_hc : test_case { + const ggml_type type; + explicit test_mmb_quant_hc(ggml_type type) : type(type) {} + std::string op_desc(ggml_tensor *) override { return "MMB_QUANT_HC"; } + std::string vars() override { return VAR_TO_STR(type); } + bool run_whole_graph() override { return true; } + bool use_scheduler_allocation() override { return true; } + void initialize_tensors(ggml_context * ctx) override { + for (auto * t=ggml_get_first_tensor(ctx); t; t=ggml_get_next_tensor(ctx,t)) { + if (t->op==GGML_OP_NONE) init_tensor_uniform(t); + } + } + double max_nmse_err() override { return 5e-4; } + ggml_tensor * build_graph(ggml_context * ctx) override { + const int embd=2560, hc=4, tokens=512, k=256; + auto * residual=ggml_new_tensor_3d(ctx, GGML_TYPE_F32, embd, hc, tokens); + auto * block=ggml_new_tensor_3d(ctx, GGML_TYPE_F32, embd, 1, tokens); + auto * injection=ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hc, tokens); + // gamma is [embd, hc] and applied to the 3D norm output: RMS_NORM and MUL adjacent, as the model builder emits them + auto * gamma=ggml_new_tensor_2d(ctx, GGML_TYPE_F32, embd, hc); + auto * weight=ggml_reshape_3d(ctx,ggml_scale(ctx,ggml_sigmoid(ctx,ggml_scale(ctx,injection,.25f)),2.f),1,hc,tokens); + if(gf){ggml_build_forward_expand(gf,block);ggml_build_forward_expand(gf,weight);} + auto * combined=ggml_add(ctx,residual,ggml_mul(ctx,ggml_repeat(ctx,block,residual),weight)); + auto * xn=ggml_reshape_2d(ctx,ggml_mul(ctx,ggml_rms_norm(ctx,combined,1e-6f),gamma),embd*hc,tokens); + if(gf)ggml_build_forward_expand(gf,xn); + auto * lo=ggml_new_tensor_2d(ctx,GGML_TYPE_F32,k,tokens); + auto * w=ggml_new_tensor_2d(ctx,type,k,embd*hc); + auto * gate=ggml_sigmoid(ctx,ggml_mul_mat(ctx,w,lo)); + auto * gated=ggml_reshape_3d(ctx,ggml_mul(ctx,xn,gate),embd,hc,tokens); + auto * mixed=ggml_cont(ctx,ggml_view_2d(ctx,gated,embd,tokens,embd*hc*sizeof(float),0)); + for(int c=1;c<hc;++c)mixed=ggml_add(ctx,mixed,ggml_view_2d(ctx,gated,embd,tokens,embd*hc*sizeof(float),embd*c*sizeof(float))); + return ggml_scale(ctx,mixed,.25f); + } +}; + + +struct test_hc_f32_consumer : public test_case { + const int tokens; + const ggml_type type_w; + + test_hc_f32_consumer(int tokens, ggml_type type_w) : tokens(tokens), type_w(type_w) {} + std::string op_desc(ggml_tensor *) override { return "HC_F32_CONSUMER"; } + std::string vars() override { return VARS_TO_STR2(tokens, type_w); } + bool run_whole_graph() override { return true; } + bool use_scheduler_allocation() override { return true; } + double max_nmse_err() override { return type_w == GGML_TYPE_F32 ? 1e-5 : 5e-4; } + + ggml_tensor * build_graph(ggml_context * ctx) override { + const int embd = 2560, hc = 4; + auto * residual = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, embd, hc, tokens); + auto * block = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, embd, 1, tokens); + auto * inject = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hc, tokens); + auto * gamma = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, embd, hc); // RMS_NORM and MUL adjacent, as the model builder emits them + auto * weight = ggml_scale(ctx, ggml_sigmoid(ctx, ggml_scale(ctx, inject, 0.25f)), 2.0f); + weight = ggml_reshape_3d(ctx, weight, 1, hc, tokens); + if (gf) { ggml_build_forward_expand(gf, block); ggml_build_forward_expand(gf, weight); } + auto * update = ggml_mul(ctx, ggml_repeat(ctx, block, residual), weight); + auto * combined = ggml_add(ctx, residual, update); + auto * norm = ggml_mul(ctx, ggml_rms_norm(ctx, combined, 1e-6f), gamma); + norm = ggml_reshape_2d(ctx, norm, embd * hc, tokens); + ggml_set_name(norm, "test_hc_norm"); + auto * projection = ggml_new_tensor_2d(ctx, type_w, embd * hc, 4); + ggml_set_name(projection, "test_hc_projection"); + return ggml_mul_mat(ctx, projection, norm); + } +}; + + +// HC16: two chained combine-norms at 512 tokens. The first combined stream is read only by its own norm and the next +// combine (BF16-only residual stream), the second is dead after its norm, so without the out_res -> out_xn allocation +// dependency the allocator hands its buffer to the normalized output, the alias check refuses the fused kernel, and the +// fallback would read the BF16-only stream as F32 (the last-layer perplexity failure). +struct test_hc_chain : public test_case { + const int tokens; + const ggml_type type_w; + + test_hc_chain(int tokens, ggml_type type_w) : tokens(tokens), type_w(type_w) {} + std::string op_desc(ggml_tensor *) override { return "HC_CHAIN"; } + std::string vars() override { return VARS_TO_STR2(tokens, type_w); } + bool run_whole_graph() override { return true; } + bool use_scheduler_allocation() override { return true; } + double max_nmse_err() override { return 5e-4; } + + ggml_tensor * build_graph(ggml_context * ctx) override { + const int embd = 2560, hc = 4; + auto * residual = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, embd, hc, tokens); + ggml_tensor * out = nullptr; + for (int layer = 0; layer < 2; ++layer) { + auto * block = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, embd, 1, tokens); + auto * inject = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hc, tokens); + auto * gamma = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, embd, hc); // RMS_NORM and MUL adjacent + auto * weight = ggml_scale(ctx, ggml_sigmoid(ctx, ggml_scale(ctx, inject, 0.25f)), 2.0f); + weight = ggml_reshape_3d(ctx, weight, 1, hc, tokens); + if (gf) { ggml_build_forward_expand(gf, residual); ggml_build_forward_expand(gf, block); ggml_build_forward_expand(gf, weight); } + auto * combined = ggml_add(ctx, residual, ggml_mul(ctx, ggml_repeat(ctx, block, residual), weight)); + auto * norm = ggml_reshape_2d(ctx, ggml_mul(ctx, ggml_rms_norm(ctx, combined, 1e-6f), gamma), embd * hc, tokens); + auto * projection = ggml_new_tensor_2d(ctx, type_w, embd * hc, 4); + auto * proj = ggml_mul_mat(ctx, projection, norm); + if (gf) ggml_build_forward_expand(gf, proj); + out = out ? ggml_add(ctx, out, proj) : proj; + residual = combined; + } + return out; + } +}; + + +// QSA indexer key pooling as the qwen4exp builder emits it: get_rows of 4 member rows per block, reshape to +// [128, 4, n_blocks, n_stream], the four stream slices summed in order, scaled by 1/4. One fused kernel on HIP. +struct test_get_rows_mean4 : public test_case { + const ggml_type type; + const int n_rows, n_blocks, n_stream; + + test_get_rows_mean4(ggml_type type, int n_rows, int n_blocks, int n_stream) + : type(type), n_rows(n_rows), n_blocks(n_blocks), n_stream(n_stream) {} + std::string op_desc(ggml_tensor *) override { return "GET_ROWS_MEAN4"; } + std::string vars() override { return VARS_TO_STR4(type, n_rows, n_blocks, n_stream); } + bool run_whole_graph() override { return true; } + bool use_scheduler_allocation() override { return true; } + double max_nmse_err() override { return 0.0; } + + ggml_tensor * build_graph(ggml_context * ctx) override { + const int64_t dim = 128, r = 4; + auto * keys = ggml_new_tensor_3d(ctx, type, dim, n_rows, n_stream); + auto * rows = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, r * n_blocks, n_stream); + auto * members = ggml_get_rows(ctx, keys, rows); + members = ggml_reshape_4d(ctx, members, dim, r, n_blocks, n_stream); + ggml_tensor * pooled = nullptr; + for (int64_t i = 0; i < r; ++i) { + auto * slice = ggml_cont(ctx, ggml_view_3d(ctx, members, dim, n_blocks, n_stream, + members->nb[2], members->nb[3], i * members->nb[1])); + pooled = pooled ? ggml_add(ctx, pooled, slice) : slice; + } + return ggml_scale(ctx, pooled, 1.0f / (float) r); + } + + void initialize_tensors(ggml_context * ctx) override { + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { + if (ggml_is_view_op(t->op)) continue; + if (t->type == GGML_TYPE_I32) { + std::vector<int> data(ggml_nelements(t)); + for (size_t i = 0; i < data.size(); i++) data[i] = rand() % n_rows; + ggml_backend_tensor_set(t, data.data(), 0, data.size() * sizeof(int)); + } else if (t->op == GGML_OP_NONE) { + init_tensor_uniform(t); + } + } + } +}; + + +struct test_indexer_head_sum : public test_case { + const int blocks, heads, tokens, streams; + const bool view, escape; + const int alias; + test_indexer_head_sum(int blocks, int heads, int tokens, int streams=1, bool view=false, bool escape=false, int alias=0) + : blocks(blocks), heads(heads), tokens(tokens), streams(streams), view(view), escape(escape), alias(alias) {} + std::string op_desc(ggml_tensor *) override { return "INDEXER_HEAD_SUM"; } + std::string vars() override { return VARS_TO_STR7(blocks, heads, tokens, streams, view, escape, alias); } + bool run_whole_graph() override { return true; } + double max_nmse_err() override { return 0.0; } + ggml_tensor * build_graph(ggml_context * ctx) override { + auto * backing = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, view ? 2*blocks : blocks, heads, tokens, streams); + auto * src = view ? ggml_view_4d(ctx, backing, blocks, heads, tokens, streams, backing->nb[1], backing->nb[2], backing->nb[3], sizeof(float)) : backing; + auto * relu = ggml_relu(ctx, src); + ggml_tensor * sum = nullptr, * first = nullptr; + for (int h=0;h<heads;++h) { + auto * slice = ggml_view_3d(ctx, relu, blocks, tokens, streams, relu->nb[2], relu->nb[3], h*relu->nb[1]); + if (!first) first=slice; + sum = sum ? ggml_add(ctx, sum, slice) : ggml_cont(ctx, slice); + } + if (escape) sum=ggml_add(ctx, sum, first); + if (alias) { sum->view_src=backing;sum->view_offs=alias==2 ? sizeof(float) : 0; } + return sum; + } +}; + +struct test_rms_norm_narrow : public test_case { + const int cols, rows; + const bool gated, view, escape; + const float eps; + const int alias; + const bool projection; + const std::array<int, 2> batches; + test_rms_norm_narrow(int cols, int rows, bool gated, bool view = false, bool escape = false, + float eps = 1e-6f, int alias = 0, bool projection = false, std::array<int, 2> batches = {1, 1}) + : cols(cols), rows(rows), gated(gated), view(view), escape(escape), eps(eps), alias(alias), projection(projection), batches(batches) {} + std::string op_desc(ggml_tensor *) override { return "RMS_NORM_NARROW"; } + std::string vars() override { return VARS_TO_STR9(cols, rows, gated, view, escape, eps, alias, projection, batches); } + bool run_whole_graph() override { return true; } + ggml_tensor * build_graph(ggml_context * ctx) override { + auto * backing = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, view || alias == 3 ? 2*cols : cols, rows+(alias != 0), batches[0], batches[1]); + auto * x = backing; + if (view || alias) { x = ggml_view_4d(ctx, backing, cols, rows, batches[0], batches[1], backing->nb[1], backing->nb[2], backing->nb[3], view ? sizeof(float) : 0); } + auto * w = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, cols); + auto * gate_backing = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, cols, rows+(alias >= 4), batches[0], batches[1]); + auto * z = alias >= 4 ? ggml_view_4d(ctx, gate_backing, cols, rows, batches[0], batches[1], gate_backing->nb[1], gate_backing->nb[2], gate_backing->nb[3], 0) : gate_backing; + auto * norm = ggml_rms_norm(ctx, x, eps); + auto * out = ggml_mul(ctx, norm, w); + if (projection) { + z = ggml_mul_mat(ctx, ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols, cols), z); + ggml_prec_set_acc(z, GGML_PREC_F32); + } + if (gated) { out = ggml_mul(ctx, out, ggml_sigmoid(ctx, z)); } + if (escape) { out = ggml_add(ctx, out, norm); } + if (alias) { + out->view_src = alias >= 4 ? gate_backing : backing; + out->view_offs = alias == 2 || alias == 5 ? sizeof(float) : 0; + } + return out; + } +}; + // GGML_OP_RMS_NORM_BACK struct test_rms_norm_back : public test_case { const ggml_type type; @@ -5355,6 +5686,37 @@ struct test_mul_mat_id : public test_case { } }; +// quantized-weight BF16 WMMA GEMM (mmb.cu): dense, routed, and the fused routed gate/up + SwiGLU +struct test_mmb_quant_dense : test_mul_mat { + explicit test_mmb_quant_dense(ggml_type type, int tokens, int rows, int inner) + : test_mul_mat(type, GGML_TYPE_F32, rows, tokens, inner, {1, 1}, {1, 1}) {} + std::string op_desc(ggml_tensor *) override { return "MMB_QUANT"; } +}; + +struct test_mmb_quant_routed : test_mul_mat_id { + const bool fused; + test_mmb_quant_routed(ggml_type type, int tokens, bool broadcast, bool fused, int rows = 128) + : test_mul_mat_id(type, GGML_TYPE_F32, 8, 2, broadcast, rows, tokens, 256), fused(fused) {} + std::string op_desc(ggml_tensor *) override { return "MMB_QUANT"; } + std::string vars() override { return test_mul_mat_id::vars() + "," + VAR_TO_STR(fused); } + bool run_whole_graph() override { return fused; } + bool use_scheduler_allocation() override { return fused; } + void initialize_tensors(ggml_context * ctx) override { + for (auto * t=ggml_get_first_tensor(ctx); t; t=ggml_get_next_tensor(ctx,t)) { + if (t->op==GGML_OP_NONE && t->type!=GGML_TYPE_I32) init_tensor_uniform(t); + } + init_mul_mat_id_ids(ctx,n_mats); + } + ggml_tensor * build_graph(ggml_context * ctx) override { + auto * gate = test_mul_mat_id::build_graph(ctx); + if (!fused) return gate; + auto * uw = ggml_new_tensor_3d(ctx, type_a, k, m, n_mats); + ggml_set_name(uw, "uw"); + auto * up = ggml_mul_mat_id(ctx, uw, gate->src[1], gate->src[2]); + return ggml_glu_split(ctx, gate, up, GGML_GLU_OP_SWIGLU); + } +}; + struct test_swiglu_q8_mmq : public test_case { const bool use_id; const int n_mats; @@ -7048,6 +7410,28 @@ struct test_top_k : public test_case { } }; +struct test_top_k_extreme : public test_top_k { + test_top_k_extreme(int cols, int k) : test_top_k(GGML_TYPE_F32, {cols, 2, 1, 1}, k) {} + std::string vars() override { return test_top_k::vars() + ",extreme=1"; } + void initialize_tensors(ggml_context * ctx) override { + test_top_k::initialize_tensors(ctx); + std::vector<float> data(ggml_nelements(input)); + for (int row = 0; row < 2; ++row) { + auto * values = data.data() + row * ne[0]; + for (int i = 0; i < ne[0]; ++i) { + const uint32_t bits = 0x3f800000u + (uint32_t) i; + memcpy(values + i, &bits, sizeof(float)); + } + values[0] = -INFINITY; + values[ne[0] - 3] = std::numeric_limits<float>::lowest(); + values[ne[0] - 2] = std::numeric_limits<float>::max(); + values[ne[0] - 1] = INFINITY; + } + ggml_backend_tensor_set(input, data.data(), 0, data.size() * sizeof(float)); + } +}; + + // qwen4exp QSA indexer top-k fusion: expand per-block scores to cells, add the f16 mask, top-k. struct test_topk_qsa : public test_case { const int64_t n_blocks; @@ -8417,6 +8801,230 @@ struct test_flash_attn_ext_top_k : public test_case { } }; +struct test_qsa_prefill : public test_case { + const int dim, queries, keys, selected, streams, ratio; + const bool interleaved, poison; + ggml_tensor * q = nullptr, * k = nullptr, * v = nullptr, * mask = nullptr, * ids = nullptr; + test_qsa_prefill(int queries, int keys, int selected, bool interleaved=true, bool poison=false, int streams=1, int ratio=12, int dim=256) + : dim(dim), queries(queries), keys(keys), selected(selected), streams(streams), ratio(ratio), interleaved(interleaved), poison(poison) {} + std::string op_desc(ggml_tensor *) override { return "QSA_PREFILL"; } + std::string vars() override { return VARS_TO_STR8(dim,queries,keys,selected,streams,ratio,interleaved,poison); } + bool run_whole_graph() override { return true; } + double max_nmse_err() override { return 5e-4; } + ggml_tensor * build_graph(ggml_context * ctx) override { + q = ggml_new_tensor_4d(ctx,GGML_TYPE_F32,dim,2*ratio,queries,streams); + q = ggml_permute(ctx,q,0,2,1,3); + k = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,dim,interleaved ? 2 : keys,interleaved ? keys : 2,streams); + v = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,dim,interleaved ? 2 : keys,interleaved ? keys : 2,streams); + if (interleaved) { k=ggml_permute(ctx,k,0,2,1,3); v=ggml_permute(ctx,v,0,2,1,3); } + mask = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,keys,queries,1,streams); + ids = ggml_new_tensor_4d(ctx,GGML_TYPE_I32,selected,queries,1,streams); + auto * out = ggml_flash_attn_ext(ctx,q,k,v,mask,1.0f/sqrtf(dim),0.0f,0.0f); + ggml_flash_attn_ext_add_top_k(out,ids,0); + ggml_prec_set_acc(out,GGML_PREC_F32); + return out; + } + void initialize_tensors(ggml_context * ctx) override { + for (auto * t=ggml_get_first_tensor(ctx); t; t=ggml_get_next_tensor(ctx,t)) { + if (t->op==GGML_OP_NONE && t!=mask && t!=ids) init_tensor_uniform(t); + } + std::vector<int32_t> picks(ggml_nelements(ids)); + std::vector<ggml_fp16_t> masks(ggml_nelements(mask),ggml_fp32_to_fp16(-INFINITY)); + for (int stream=0;stream<streams;++stream) for (int q=0;q<queries;++q) { + for (int j=0;j<selected;++j) { + int key=1+(j*37+q*13+stream*7)%(keys-4); + if (key==3) key=4; + if (j==0) key=selected==1 ? 2 : (poison ? 3 : -1); + if (j==1) key=2; + picks[(stream*queries+q)*selected+j]=key; + if (key>=0 && !(poison && key==3)) masks[(stream*queries+q)*keys+key]=ggml_fp32_to_fp16(0.015625f*(key%5-2)); + } + } + ggml_backend_tensor_set(ids,picks.data(),0,ggml_nbytes(ids)); + ggml_backend_tensor_set(mask,masks.data(),0,ggml_nbytes(mask)); + if (poison) { + std::vector<ggml_fp16_t> nan(dim,ggml_fp32_to_fp16(NAN)); + for (auto * t : {k,v}) for (int stream=0;stream<streams;++stream) for (int head=0;head<2;++head) { + ggml_backend_tensor_set(t,nan.data(),stream*t->nb[3]+head*t->nb[2]+3*t->nb[1],dim*sizeof(ggml_fp16_t)); + } + } + } +}; + +struct test_qsa_decode : public test_qsa_prefill { + const bool duplicates; + test_qsa_decode(int q, int keys, int selected, bool poison=false, int ratio=12, bool duplicates=false) + : test_qsa_prefill(q,keys,selected,true,poison,1,ratio), duplicates(duplicates) {} + std::string op_desc(ggml_tensor *) override { return "QSA_DECODE"; } + std::string vars() override { return test_qsa_prefill::vars()+",duplicates="+std::to_string(duplicates); } + double err(const float * actual, const float * cpu, size_t n) override { + const double agreement=nmse(actual,cpu,n); + if (queries>8 || selected>2560) return agreement; + const auto qv=tensor_to_float(q), kv=tensor_to_float(k), vv=tensor_to_float(v), mv=tensor_to_float(mask); + std::vector<float> reference(n); + for (int query=0;query<queries;++query) { + std::vector<int> active; + for (int key=0;key<keys;++key) if (mv[size_t(query)*keys+key]!=-INFINITY) active.push_back(key); + for (int head=0;head<2*ratio;++head) { + std::vector<double> scores(active.size()), values(dim,0.0); + double maximum=-INFINITY; + for (size_t j=0;j<active.size();++j) { + const int key=active[j];double dot=0; + for (int d=0;d<dim;++d) dot+=double(qv[d+size_t(dim)*(query+queries*head)])*kv[d+size_t(dim)*(key+keys*(head/ratio))]; + scores[j]=dot/std::sqrt(double(dim))+mv[size_t(query)*keys+key]; + maximum=std::max(maximum,scores[j]); + } + double sum=0; + for (size_t j=0;j<active.size();++j) { + const double weight=std::exp(scores[j]-maximum);sum+=weight; + for (int d=0;d<dim;++d) values[d]+=weight*vv[d+size_t(dim)*(active[j]+keys*(head/ratio))]; + } + for (int d=0;d<dim;++d) reference[d+size_t(dim)*(head+2*ratio*query)]=sum>0 ? float(values[d]/sum) : 0.0f; + } + } + const double gpu_error=nmse(reference.data(),actual,n), cpu_error=nmse(reference.data(),cpu,n); + fprintf(stderr,"QSA_FP64 q=%d keys=%d selected=%d ratio=%d gpu=%.9g cpu=%.9g\n",queries,keys,selected,ratio,gpu_error,cpu_error); + return std::max(agreement,gpu_error); + } + void initialize_tensors(ggml_context * ctx) override { + test_qsa_prefill::initialize_tensors(ctx); + if (!duplicates) return; + std::vector<int32_t> picks(ggml_nelements(ids)); + std::vector<ggml_fp16_t> masks(ggml_nelements(mask),ggml_fp32_to_fp16(-INFINITY)); + for (int q=0;q<queries;++q) for (int j=0;j<selected;++j) { + const int key=j<selected/2 ? 2 : 4+(j*13+q*7)%(keys-8); + picks[q*selected+j]=key;masks[q*keys+key]=ggml_fp32_to_fp16(0.f); + } + ggml_backend_tensor_set(ids,picks.data(),0,ggml_nbytes(ids)); + ggml_backend_tensor_set(mask,masks.data(),0,ggml_nbytes(mask)); + } +}; + +// Maskless selected-key prefill: the rows name only the visible cells (-1 elsewhere) and no mask is given, as the +// complete-block selection graph does. The CPU graph cannot express that (its flash attention ignores src[5]), so +// the check is against a host FP64 oracle over the listed cells; the CPU result is ignored. +struct test_qsa_prefill_maskless : public test_qsa_prefill { + test_qsa_prefill_maskless(int queries, int keys, int selected, int ratio=12) + : test_qsa_prefill(queries,keys,selected,true,false,1,ratio) {} + std::string op_desc(ggml_tensor *) override { return "QSA_PREFILL_MASKLESS"; } + ggml_tensor * build_graph(ggml_context * ctx) override { + q = ggml_new_tensor_4d(ctx,GGML_TYPE_F32,dim,2*ratio,queries,streams); + q = ggml_permute(ctx,q,0,2,1,3); + k = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,dim,2,keys,streams); + v = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,dim,2,keys,streams); + k=ggml_permute(ctx,k,0,2,1,3); v=ggml_permute(ctx,v,0,2,1,3); + mask = nullptr; + ids = ggml_new_tensor_4d(ctx,GGML_TYPE_I32,selected,queries,1,streams); + auto * out = ggml_flash_attn_ext(ctx,q,k,v,nullptr,1.0f/sqrtf(dim),0.0f,0.0f); + ggml_flash_attn_ext_add_top_k(out,ids,0); + ggml_prec_set_acc(out,GGML_PREC_F32); + return out; + } + void initialize_tensors(ggml_context * ctx) override { + for (auto * t=ggml_get_first_tensor(ctx); t; t=ggml_get_next_tensor(ctx,t)) { + if (t->op==GGML_OP_NONE && t!=ids) init_tensor_uniform(t); + } + std::vector<int32_t> picks(ggml_nelements(ids)); + for (int q=0;q<queries;++q) { + for (int j=0;j<selected;++j) { + int key=1+(j*37+q*13)%(keys-4); + if (j==0) key=selected==1 ? 2 : -1; // -1: an invisible block's cell + if (j==1) key=2; + if (j>selected-3 && selected>4) key=-1; // tail padding + picks[q*selected+j]=key; + } + } + ggml_backend_tensor_set(ids,picks.data(),0,ggml_nbytes(ids)); + } + double err(const float * actual, const float * cpu, size_t n) override { + GGML_UNUSED(cpu); + const auto qv=tensor_to_float(q), kv=tensor_to_float(k), vv=tensor_to_float(v); + std::vector<int32_t> picks(ggml_nelements(ids)); + ggml_backend_tensor_get(ids,picks.data(),0,ggml_nbytes(ids)); + std::vector<float> reference(n); + for (int query=0;query<queries;++query) { + std::vector<int> active; + for (int j=0;j<selected;++j) { const int key=picks[query*selected+j]; if (key>=0 && key<keys && std::find(active.begin(),active.end(),key)==active.end()) active.push_back(key); } + for (int head=0;head<2*ratio;++head) { + std::vector<double> scores(active.size()), values(dim,0.0); + double maximum=-INFINITY; + for (size_t j=0;j<active.size();++j) { + const int key=active[j];double dot=0; + for (int d=0;d<dim;++d) dot+=double(qv[d+size_t(dim)*(query+queries*head)])*kv[d+size_t(dim)*(key+keys*(head/ratio))]; + scores[j]=dot/std::sqrt(double(dim)); + maximum=std::max(maximum,scores[j]); + } + double sum=0; + for (size_t j=0;j<active.size();++j) { + const double weight=std::exp(scores[j]-maximum);sum+=weight; + for (int d=0;d<dim;++d) values[d]+=weight*vv[d+size_t(dim)*(active[j]+keys*(head/ratio))]; + } + for (int d=0;d<dim;++d) reference[d+size_t(dim)*(head+2*ratio*query)]=sum>0 ? float(values[d]/sum) : 0.0f; + } + } + const double gpu_error=nmse(reference.data(),actual,n); + fprintf(stderr,"QSA_MASKLESS_FP64 q=%d keys=%d selected=%d gpu=%.9g\n",queries,keys,selected,gpu_error); + return gpu_error; + } +}; + +// Complete-block selection as qwen4exp_select_complete_blocks builds it (512-block budget, ratio 4): the HIP +// backend fuses the whole subgraph into one kernel; the CPU computes it node by node. Exact integer comparison. +struct test_qsa_expand : public test_case { + const int n_blocks, n_query; + ggml_tensor * score = nullptr, * cells = nullptr, * tail = nullptr; + test_qsa_expand(int n_blocks, int n_query) : n_blocks(n_blocks), n_query(n_query) {} + std::string op_desc(ggml_tensor *) override { return "QSA_EXPAND"; } + std::string vars() override { return VARS_TO_STR2(n_blocks, n_query); } + bool run_whole_graph() override { return true; } + bool use_scheduler_allocation() override { return true; } + double max_nmse_err() override { return 0.0; } + ggml_tensor * build_graph(ggml_context * ctx) override { + const int64_t r = 4, budget = 512; + score = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, n_blocks, n_query, 1); + cells = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, r*n_blocks, 1); + tail = ggml_new_tensor_3d(ctx, GGML_TYPE_I32, r-1, n_query, 1); + ggml_tensor * blocks=ggml_cont(ctx,ggml_top_k(ctx,score,budget)); + ggml_tensor * order=ggml_argsort(ctx,ggml_cast(ctx,blocks,GGML_TYPE_F32),GGML_SORT_ORDER_ASC); + ggml_tensor * blocks_view=ggml_view_4d(ctx,blocks,1,budget,n_query,1,blocks->nb[0],blocks->nb[1],blocks->nb[2],0); + blocks=ggml_reshape_3d(ctx,ggml_get_rows(ctx,blocks_view,order),budget,n_query,1); + ggml_tensor * score_view=ggml_view_4d(ctx,score,1,n_blocks,n_query,1,score->nb[0],score->nb[1],score->nb[2],0); + ggml_tensor * picked=ggml_get_rows(ctx,score_view,blocks); + ggml_tensor * valid=ggml_step(ctx,ggml_scale_bias(ctx,picked,1.0f,1.0f)); + ggml_tensor * cells_view=ggml_reshape_3d(ctx,cells,r,n_blocks,1); + ggml_tensor * flat=ggml_reshape_2d(ctx,blocks,budget*n_query,1); + ggml_tensor * out=ggml_get_rows(ctx,cells_view,flat); + out=ggml_reshape_4d(ctx,out,r,budget,n_query,1); + out=ggml_scale_bias(ctx,ggml_cast(ctx,out,GGML_TYPE_F32),1.0f,1.0f); + out=ggml_mul(ctx,out,valid); + out=ggml_cast(ctx,ggml_scale_bias(ctx,out,1.0f,-1.0f),GGML_TYPE_I32); + out=ggml_reshape_4d(ctx,out,budget*r,n_query,1,1); + return ggml_concat(ctx,out,tail,0); + } + void initialize_tensors(ggml_context * ctx) override { + for (auto * t=ggml_get_first_tensor(ctx); t; t=ggml_get_next_tensor(ctx,t)) { + if (t->op!=GGML_OP_NONE) continue; + if (t==score) { + std::vector<float> v((size_t) n_blocks*n_query); + for (int q=0;q<n_query;++q) for (int b=0;b<n_blocks;++b) { + // distinct scores (no ties), the blocks past a per-query limit invisible + const int limit = n_blocks - (q*7)%std::max(1,n_blocks-520); + v[(size_t) q*n_blocks+b] = b < limit ? 0.001f*((b*7919+q*104729)%100003) : -INFINITY; + } + ggml_backend_tensor_set(t,v.data(),0,ggml_nbytes(t)); + } else if (t==cells) { + std::vector<int32_t> v((size_t) 4*n_blocks); + for (size_t i=0;i<v.size();++i) v[i]=(int32_t)((i*31+5)%(4*n_blocks)); + ggml_backend_tensor_set(t,v.data(),0,ggml_nbytes(t)); + } else if (t==tail) { + std::vector<int32_t> v((size_t) 3*n_query); + for (int q=0;q<n_query;++q) for (int j=0;j<3;++j) v[q*3+j]= j < q%4 ? 4*n_blocks-3+j : -1; + ggml_backend_tensor_set(t,v.data(),0,ggml_nbytes(t)); + } + } + } +}; + // GGML_OP_CROSS_ENTROPY_LOSS struct test_cross_entropy_loss : public test_case { const ggml_type type; @@ -9523,8 +10131,348 @@ static const ggml_type other_types[] = { #endif // Test cases for evaluation: should try to cover edge cases while using small input sizes to keep the runtime low +struct test_ple_conv : test_case { + const ggml_type type; + const int channels, tokens, slots, escape; + test_ple_conv(ggml_type type, int channels, int tokens, int slots=4, int escape=0) + : type(type), channels(channels), tokens(tokens), slots(slots), escape(escape) {} + std::string op_desc(ggml_tensor *) override { return "PLE_CONV"; } + std::string vars() override { return VARS_TO_STR5(type, channels, tokens, slots, escape); } + bool run_whole_graph() override { return true; } + bool use_weight_context() override { return true; } + bool use_scheduler_allocation() override { return true; } + double max_nmse_err() override { return 1e-6; } + void initialize_tensors(ggml_context * ctx) override { + for (auto * t=ggml_get_first_tensor(ctx);t;t=ggml_get_next_tensor(ctx,t)) if(t->op==GGML_OP_NONE) init_tensor_uniform(t); + } + ggml_tensor * build_graph(ggml_context * ctx) override { return build_graph(ctx, ctx); } + ggml_tensor * build_graph(ggml_context * ctx, ggml_context * ctx_weights) override { + const int H=9; + auto * state=ggml_new_tensor_2d(ctx,GGML_TYPE_F32,H,channels); + auto * x=ggml_new_tensor_2d(ctx,GGML_TYPE_F32,channels,tokens); + auto * w=ggml_new_tensor_2d(ctx_weights,type,4,channels); + auto * cc=ggml_concat(ctx,state,ggml_transpose(ctx,x),0); + auto * checkpoint_store=ggml_new_tensor_2d(ctx,GGML_TYPE_F32,H*channels,slots); + std::vector<ggml_tensor *> checkpoints; + for(int i=0;i<slots;i++) { + auto * dst=ggml_view_2d(ctx,checkpoint_store,H*channels,1,checkpoint_store->nb[1],i*checkpoint_store->nb[1]); + auto * tail=ggml_view_2d(ctx,cc,H,channels,cc->nb[1],(tokens-i)*sizeof(float)); + auto * saved=ggml_cpy(ctx,ggml_cont(ctx,tail),dst); + if(gf)ggml_build_forward_expand(gf,saved); + checkpoints.push_back(saved); + } + ggml_tensor * sum=nullptr,*first=nullptr; + for(int k=0;k<4;k++) { + auto * tap=ggml_cont(ctx,ggml_transpose(ctx,ggml_view_2d(ctx,cc,tokens,channels,cc->nb[1],k*3*sizeof(float)))); + if(!first)first=tap; + auto * wk=ggml_reshape_1d(ctx,ggml_cont(ctx,ggml_view_2d(ctx,w,1,channels,w->nb[1],k*w->nb[0])),channels); + if(type!=GGML_TYPE_F32)wk=ggml_cast(ctx,wk,GGML_TYPE_F32); + auto * term=ggml_mul(ctx,tap,wk);sum=sum?ggml_add(ctx,sum,term):term; + } + auto * conv=ggml_silu(ctx,sum); + ggml_format_name(conv,"ple_case_%s_C%d_T%d_S%d_E%d",ggml_type_name(type),channels,tokens,slots,escape); + auto * result=ggml_reshape_1d(ctx,conv,channels*tokens); + for(auto * t:checkpoints)result=ggml_concat(ctx,result,ggml_reshape_1d(ctx,t,H*channels),0); + if(escape==1)result=ggml_concat(ctx,result,ggml_reshape_1d(ctx,ggml_cont(ctx,ggml_view_2d(ctx,cc,1,channels,cc->nb[1],0)),channels),0); + if(escape==2)result=ggml_concat(ctx,result,ggml_reshape_1d(ctx,first,channels*tokens),0); + return result; + } +}; + +struct test_get_rows_packed : public test_case { + const ggml_type type; + const int width, table_rows, rows, b1, b2, layout, pattern; + const bool packed_perf; + ggml_tensor * storage = nullptr; + ggml_tensor * indices = nullptr; + ggml_tensor * index_storage = nullptr; + + test_get_rows_packed(ggml_type type, int width, int table_rows, int rows, + int b1 = 1, int b2 = 1, int layout = 0, int pattern = 0, bool packed_perf = false) + : type(type), width(width), table_rows(table_rows), rows(rows), b1(b1), b2(b2), + layout(layout), pattern(pattern), packed_perf(packed_perf) {} + std::string op_desc(ggml_tensor *) override { return "GET_ROWS"; } + std::string vars() override { + return VARS_TO_STR9(type, width, table_rows, rows, b1, b2, layout, pattern, packed_perf); + } + double max_nmse_err() override { return 0.0; } + double err(const float * a, const float * b, size_t n) override { + return memcmp(a, b, n * sizeof(float)) == 0 ? 0.0 : 1.0; + } + ggml_tensor * build_graph(ggml_context * ctx) override { + storage = ggml_new_tensor_4d(ctx, type, width + ((layout & 1) ? 7 : 0), + table_rows + ((layout & 1) ? 1 : 0), b1 + ((layout & 1) ? 1 : 0), b2); + ggml_tensor * input = storage; + if (layout & 1) input = ggml_view_4d(ctx, storage, width, table_rows, b1, b2, + storage->nb[1], storage->nb[2], storage->nb[3], ggml_type_size(type)); + index_storage = ggml_new_tensor_3d(ctx, GGML_TYPE_I32, + rows + ((layout & 2) ? 3 : 0), b1 + ((layout & 2) ? 1 : 0), b2); + indices = index_storage; + if (layout & 2) indices = ggml_view_3d(ctx, index_storage, rows, b1, b2, + index_storage->nb[1], index_storage->nb[2], sizeof(int32_t)); + auto * out = ggml_get_rows(ctx, input, indices); + if (layout & 4) { + out->nb[1] = (width + 5) * sizeof(float); + out->nb[2] = out->nb[1] * (rows + 3); + out->nb[3] = out->nb[2] * (b1 + 1); + } + ggml_set_name(storage, "packed_source_storage"); + ggml_set_name(index_storage, "packed_index_storage"); + ggml_set_name(out, "packed_result"); + return out; + } + void initialize_tensors(ggml_context * ctx) override { + for (auto * t = ggml_get_first_tensor(ctx); t; t = ggml_get_next_tensor(ctx, t)) { + if (t->data && t->type != GGML_TYPE_I32) init_tensor_uniform(t, -1.0f, 1.0f); + } + std::vector<float> values(ggml_nelements(storage)); + for (size_t i = 0; i < values.size(); ++i) { + values[i] = float(int((i * 137 + i / 31) % 4093) - 2046) / 1024.0f; + if (!packed_perf && i % 37 == 0) values[i] = -0.0f; + if (!packed_perf && i % 41 == 0) values[i] = 0x1p-24f; + if (!packed_perf && i % 43 == 0) values[i] = -0x1p-24f; + if (!packed_perf && i % 4091 == 0) values[i] = INFINITY; + if (!packed_perf && i % 4093 == 0) values[i] = -INFINITY; + } + if (type == GGML_TYPE_F16) { + std::vector<ggml_fp16_t> h(values.size()); + ggml_fp32_to_fp16_row(values.data(), h.data(), values.size()); + ggml_backend_tensor_set(storage, h.data(), 0, h.size() * sizeof(h[0])); + } else { + ggml_backend_tensor_set(storage, values.data(), 0, values.size() * sizeof(values[0])); + } + std::vector<uint8_t> raw(ggml_nbytes(index_storage), 0); + const size_t offset = indices->view_src ? indices->view_offs : 0; + for (int z = 0; z < b2; ++z) for (int y = 0; y < b1; ++y) for (int i = 0; i < rows; ++i) { + int32_t index; + if (pattern == 1) index = i % table_rows; + else if (pattern == 2) index = (i / 4) % table_rows; + else { + index = (uint64_t(i) * 131 + y * 17 + z * 31) % table_rows; + if (i % 7 == 0) index = 0; + if (i % 11 == 0) index = table_rows - 1; + } + memcpy(raw.data() + offset + i * indices->nb[0] + y * indices->nb[1] + z * indices->nb[2], &index, sizeof(index)); + } + ggml_backend_tensor_set(index_storage, raw.data(), 0, raw.size()); + } +}; + +struct test_topk_moe_lifetime : public test_topk_moe { + const bool offset; + ggml_tensor * logits = nullptr; + ggml_tensor * bias = nullptr; + ggml_tensor * ids_result = nullptr; + ggml_tensor * weights_result = nullptr; + test_topk_moe_lifetime(int tokens, bool offset, bool bias_probs, MoeGatingFunc gating_func, bool norm, float scale) + : test_topk_moe({512, tokens, 1, 1}, 10, norm, bias_probs, gating_func, scale), offset(offset) {} + std::string op_desc(ggml_tensor *) override { return "TOPK_MOE_LIFETIME"; } + std::string vars() override { return test_topk_moe::vars() + "," + VARS_TO_STR1(offset); } + bool use_scheduler_allocation() override { return true; } + std::vector<ggml_tensor *> fusion_test_nodes() override { return {ids_result, weights_result}; } + ggml_tensor * build_graph(ggml_context * ctx) override { + auto * input = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, ne[0], ne[1] + offset); + ggml_set_name(input, "routing_input"); + ggml_set_input(input); + auto * root = ggml_scale(ctx, input, 0.75f); + logits = offset ? ggml_view_2d(ctx, root, ne[0], ne[1], root->nb[1], root->nb[1]) : root; + ggml_set_name(logits, "routing_logits"); + ggml_tensor * probs = gating_func == GATING_FUNC_SOFTMAX ? ggml_soft_max(ctx, logits) : + gating_func == GATING_FUNC_SIGMOID ? ggml_sigmoid(ctx, logits) : + gating_func == GATING_FUNC_SQRT_SOFTPLUS ? ggml_sqrt(ctx, ggml_softplus(ctx, logits)) : logits; + auto * select = probs; + if (bias_probs) { + auto * bias_in = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, ne[0]); + ggml_set_name(bias_in, "routing_bias_input"); + ggml_set_input(bias_in); + bias = ggml_scale(ctx, bias_in, 0.5f); + ggml_build_forward_expand(gf, bias); + select = ggml_add(ctx, probs, bias); + } + selected_experts = ggml_argsort_top_k(ctx, select, n_expert_used); + ggml_set_name(selected_experts, "routing_ids"); + weights = ggml_get_rows(ctx, ggml_reshape_3d(ctx, probs, 1, ne[0], ne[1]), selected_experts); + if (gating_func == GATING_FUNC_SOFTMAX_WEIGHT) { + weights = ggml_soft_max(ctx, ggml_reshape_2d(ctx, weights, n_expert_used, ne[1])); + weights = ggml_reshape_3d(ctx, weights, 1, n_expert_used, ne[1]); + } + if (with_norm) { + weights = ggml_reshape_2d(ctx, weights, n_expert_used, ne[1]); + auto * sum = ggml_clamp(ctx, ggml_sum_rows(ctx, weights), 6.103515625e-5f, INFINITY); + weights = ggml_reshape_3d(ctx, ggml_div(ctx, weights, sum), 1, n_expert_used, ne[1]); + } + if (scale_w) weights = ggml_scale(ctx, weights, scale_w); + ggml_set_name(weights, "routing_weights"); + weights_result = ggml_cont(ctx, weights); + ggml_set_output(weights_result); + ggml_build_forward_expand(gf, weights_result); + ids_result = ggml_cont(ctx, selected_experts); + ggml_set_output(ids_result); + return ids_result; + } + void initialize_tensors(ggml_context * ctx) override { + for (auto * t = ggml_get_first_tensor(ctx); t; t = ggml_get_next_tensor(ctx, t)) { + if (t->data) init_tensor_uniform(t, -0.9f, 1.1f); + } + for (auto * t = ggml_get_first_tensor(ctx); t; t = ggml_get_next_tensor(ctx, t)) { + if (!t->data || !(t->flags & GGML_TENSOR_FLAG_INPUT)) continue; + std::vector<float> data(ggml_nelements(t)); + for (int64_t j = 0; j < ggml_nrows(t); ++j) { + for (int64_t i = 0; i < t->ne[0]; ++i) { + data[j*t->ne[0]+i] = (float) (((i*137+j*31)%512)-256)/256.0f; + if (t == (bias ? bias->src[0] : nullptr)) data[j*t->ne[0]+i] *= 0.001f; + } + } + ggml_backend_tensor_set(t, data.data(), 0, data.size()*sizeof(float)); + } + } +}; + static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { std::vector<std::unique_ptr<test_case>> test_cases; + for (auto type : {GGML_TYPE_F16, GGML_TYPE_F32}) { + for (int tokens : {255,256,257,383,384,385}) test_cases.emplace_back(new test_ple_conv(type,256,tokens)); + test_cases.emplace_back(new test_ple_conv(type,256,256,1)); + test_cases.emplace_back(new test_ple_conv(type,256,256,4,1)); + test_cases.emplace_back(new test_ple_conv(type,256,256,4,2)); + test_cases.emplace_back(new test_ple_conv(type,257,256)); + test_cases.emplace_back(new test_ple_conv(type,10240,512)); + } + + for (ggml_type type : {GGML_TYPE_F32, GGML_TYPE_F16}) { + for (int width : {1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129}) { + for (int layout : {0, 1, 6, 7}) { + test_cases.emplace_back(new test_get_rows_packed(type, width, 257, 259, 3, 2, layout)); + } + } + for (int width : {1, 3, 128}) { + for (int rows : {1, 2, 127, 128, 129, 255, 256, 257}) { + test_cases.emplace_back(new test_get_rows_packed(type, width, 17, rows)); + } + } + } + + test_cases.emplace_back(new test_get_rows_packed(GGML_TYPE_F32, 1, 2, 128, 1, 65537)); + for (int kv : {2048, 40704, 40960, 101120, 101376}) { + for (int width : {1, 2, 3}) { + test_cases.emplace_back(new test_get_rows_packed(GGML_TYPE_F32, width, kv / 4, kv, 1, 1, 0, 2, true)); + } + test_cases.emplace_back(new test_get_rows_packed(GGML_TYPE_F16, 128, kv, kv, 1, 1, 0, 1, true)); + } + + for (int nt : {1, 8, 9, 256, 2048}) { + for (bool offset : {false, true}) { + test_cases.emplace_back(new test_topk_moe_lifetime(nt, offset, false, GATING_FUNC_SOFTMAX, true, 0.0f)); + test_cases.emplace_back(new test_topk_moe_lifetime(nt, offset, true, GATING_FUNC_SIGMOID, true, 2.0f)); + test_cases.emplace_back(new test_topk_moe_lifetime(nt, offset, true, GATING_FUNC_SQRT_SOFTPLUS, true, 0.0f)); + test_cases.emplace_back(new test_topk_moe_lifetime(nt, offset, false, GATING_FUNC_SOFTMAX_WEIGHT, false, 0.0f)); + } + } + + for (int tokens : {128, 511, 512, 513, 1024}) { + for (ggml_type type : {GGML_TYPE_F32, GGML_TYPE_Q8_0}) { + test_cases.emplace_back(new test_hc_f32_consumer(tokens, type)); + } + } + for (int tokens : {128, 512, 1024}) { + for (ggml_type type : {GGML_TYPE_F32, GGML_TYPE_Q8_0}) { + test_cases.emplace_back(new test_hc_chain(tokens, type)); + } + } + for (ggml_type type : {GGML_TYPE_F16, GGML_TYPE_F32}) { + for (int n_stream : {1, 2}) { + for (int n_blocks : {1, 7, 512, 2048}) { + test_cases.emplace_back(new test_get_rows_mean4(type, 4 * n_blocks + 5, n_blocks, n_stream)); + } + } + } + for (ggml_type type : {GGML_TYPE_Q1_0, GGML_TYPE_Q2_0, GGML_TYPE_Q4_0, GGML_TYPE_Q4_1, GGML_TYPE_Q5_0, GGML_TYPE_Q5_1, GGML_TYPE_Q8_0, GGML_TYPE_Q2_K, GGML_TYPE_Q3_K, GGML_TYPE_Q4_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, GGML_TYPE_IQ1_S, GGML_TYPE_IQ1_M, GGML_TYPE_IQ2_XXS, GGML_TYPE_IQ2_XS, GGML_TYPE_IQ2_S, GGML_TYPE_IQ3_XXS, GGML_TYPE_IQ3_S, GGML_TYPE_IQ4_XS, GGML_TYPE_IQ4_NL, GGML_TYPE_MXFP4, GGML_TYPE_NVFP4}) { + test_cases.emplace_back(new test_mmb_quant_dense(type, 512, 128, 256)); + test_cases.emplace_back(new test_mmb_quant_dense(type, 513, 129, 512)); + test_cases.emplace_back(new test_mmb_quant_routed(type, 512, false, false)); + test_cases.emplace_back(new test_mmb_quant_routed(type, 513, true, false)); + test_cases.emplace_back(new test_mmb_quant_routed(type, 512, false, true)); + test_cases.emplace_back(new test_mmb_quant_routed(type, 513, true, true)); + test_cases.emplace_back(new test_mmb_quant_hc(type)); + if (type == GGML_TYPE_Q4_K) { + test_cases.emplace_back(new test_mmb_quant_routed(type, 513, true, true, 65)); + test_cases.emplace_back(new test_mmb_quant_routed(type, 1025, false, true, 129)); + } + } + for (bool ple : {true, false}) { + for (int C : {256, 2560}) { + for (int T : {255, 256, 257, 300, 2048}) { + for (int n_slots : {1, 5}) test_cases.emplace_back(new test_conv_state_chain(ple, C, T, n_slots)); + } + } + test_cases.emplace_back(new test_conv_state_chain(ple, 10240, 2048, 5)); + test_cases.emplace_back(new test_conv_state_chain(ple, 10240, 383, 3)); + for (int T : {256, 2048}) test_cases.emplace_back(new test_conv_state_chain(ple, 2560, T, 5, true)); + for (int T : {256, 2048}) test_cases.emplace_back(new test_conv_state_chain(ple, 2560, T, 5, false, true)); + } + for(int embd : {1023,1024,2560,3072,3073}) { + for(int hc : {1,4,5,16,17}) { + for(int tokens : {1,4,64}) test_cases.emplace_back(new test_hc_combine_norm_lifetime(embd,hc,tokens)); + } + } + for(int embd : {1024,2560,3072}) test_cases.emplace_back(new test_hc_combine_norm_lifetime(embd,4,1,true,3)); + for(int tokens : {1,4,128}) { + test_cases.emplace_back(new test_hc_combine_norm_lifetime(2560,4,tokens,true)); + for(int alias : {1,2}) test_cases.emplace_back(new test_hc_combine_norm_lifetime(2560,4,tokens,false,alias)); + } + + for (ggml_type type : {GGML_TYPE_IQ4_NL, GGML_TYPE_Q8_0}) { + for (int tokens : {4095,4096,4097,8192,16384,32768,32769}) { + for (bool broadcast : {false,true}) { + test_cases.emplace_back(new test_mul_mat_id(type,GGML_TYPE_F32,512,10,broadcast,32,tokens,256)); + } + } + } + + for (int blocks : {63,64,65,1024}) { + for (int heads : {1,2,4,15,16,32}) { + test_cases.emplace_back(new test_indexer_head_sum(blocks,heads,64)); + } + } + for (int tokens : {63,65,65535,65536}) test_cases.emplace_back(new test_indexer_head_sum(64,2,tokens)); + test_cases.emplace_back(new test_indexer_head_sum(128,4,32,2)); + test_cases.emplace_back(new test_indexer_head_sum(128,4,64,2,true)); + test_cases.emplace_back(new test_indexer_head_sum(128,4,64,2,false,true)); + for (int alias : {1,2}) test_cases.emplace_back(new test_indexer_head_sum(128,4,64,2,false,false,alias)); + test_cases.emplace_back(new test_indexer_head_sum(10112,4,64)); + + for (int cols : {31, 32, 64, 128, 256, 288}) { + for (int rows : {4095, 4096, 4097}) { + for (bool gated : {false, true}) { + test_cases.emplace_back(new test_rms_norm_narrow(cols, rows, gated)); + } + } + } + for (bool gated : {false, true}) { + for (bool view : {false, true}) { + test_cases.emplace_back(new test_rms_norm_narrow(128, 48, gated, view, false, 1e-6f, 0, false, std::array<int, 2>{128, 3})); + test_cases.emplace_back(new test_rms_norm_narrow(256, 24, gated, view, false, 1e-6f, 0, false, std::array<int, 2>{256, 2})); + test_cases.emplace_back(new test_rms_norm_narrow(256, 2, gated, view, false, 1e-6f, 0, false, std::array<int, 2>{2048, 1})); + test_cases.emplace_back(new test_rms_norm_narrow(128, 4, gated, view, false, 1e-6f, 0, false, std::array<int, 2>{2048, 1})); + } + } + for (bool gated : {false, true}) { + for (int alias : {1, 2, 3}) { + test_cases.emplace_back(new test_rms_norm_narrow(128, 4096, gated, false, false, 1e-6f, alias)); + } + } + for (int alias : {4, 5}) { + test_cases.emplace_back(new test_rms_norm_narrow(128, 4096, true, false, false, 1e-6f, alias)); + } + for (bool view : {false, true}) { + test_cases.emplace_back(new test_rms_norm_narrow(128, 4096, true, view, false, 1e-6f, 0, true)); + } + for (bool gated : {false, true}) { + test_cases.emplace_back(new test_rms_norm_narrow(128, 8192, gated, true)); + test_cases.emplace_back(new test_rms_norm_narrow(256, 8192, gated, false, true)); + test_cases.emplace_back(new test_rms_norm_narrow(256, 8192, gated, false, false, 0.0f)); + } + std::default_random_engine rng(0); // unary ops @@ -9642,6 +10590,21 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_get_rows(type, 256, 5, 4, 700, 100, false)); } + for (ggml_type type : {GGML_TYPE_F32, GGML_TYPE_F16}) { + for (int width : {1, 3, 7, 15, 31, 32, 33, 64, 65, 90, 127, 128, 129}) { + for (int rows : {127, 128, 129}) { + test_cases.emplace_back(new test_get_rows(type, width, 257, rows, 1, 1, false)); + } + } + for (bool view_ids : {false, true}) { + for (bool view_src : {false, true}) { + test_cases.emplace_back(new test_get_rows(type, 128, 257, 512, 3, 2, view_ids, view_src)); + } + } + test_cases.emplace_back(new test_get_rows(type, 1, 3, 128, 1, 65536, false)); + test_cases.emplace_back(new test_get_rows(type, 128, 65537, 65536, 1, 1, false)); + } + test_cases.emplace_back(new test_get_rows(GGML_TYPE_F32, 1, 8, 2, 1, 1, false)); for (ggml_type type : all_types) { for (int b : {1, 7}) { @@ -11221,6 +12184,24 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { } // Large-k, including multi-row and ties (qwen4exp) + for (int cols : {1025, 4097}) { + for (int k : {2, 16, 512}) test_cases.emplace_back(new test_top_k_extreme(cols, k)); + } + test_cases.emplace_back(new test_top_k_extreme(4097, 2051)); + for (auto shape : {std::array<int, 3>{2047, 512, 1024}, {2048, 511, 1024}, {2048, 512, 1024}, + {4095, 128, 2051}, {4096, 127, 2051}, {8191, 2, 2051}, {8192, 1, 2051}, {8192, 2, 2051}}) { + test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {shape[0], shape[1], 1, 1}, shape[2], true)); + } + for (int cols : {4096, 40064}) { + for (int rows : {2, 128}) { + for (bool ties : {false, true}) test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, rows, 1, 1}, 2051, ties)); + } + } + for (bool ties : {false, true}) test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {8192, 512, 1, 1}, 2051, ties)); + for (int cols : {1024, 1025}) { + for (int rows : {1, 2}) test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, rows, 1, 1}, 512, true)); + } + test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, { 1024, 1, 1, 1 }, 1024)); test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, { 2048, 2, 1, 1 }, 1024)); test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, { 4096, 1, 1, 1 }, 2048)); @@ -11753,6 +12734,36 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { } } + for (int q : {1,2,4,8,9}) { + for (int k : {1,17,64,65,128,129,2051,2560,2561}) test_cases.emplace_back(new test_qsa_decode(q,4096,k)); + } + for (int q : {1,4,8}) { + test_cases.emplace_back(new test_qsa_decode(q,40064,2051)); + test_cases.emplace_back(new test_qsa_decode(q,4096,257,true)); + test_cases.emplace_back(new test_qsa_decode(q,4096,257,false,4)); + test_cases.emplace_back(new test_qsa_decode(q,4096,2051,false,12,true)); + test_cases.emplace_back(new test_qsa_decode(q,4096,257,true,4)); + } + + for (bool interleaved : {false,true}) { + test_cases.emplace_back(new test_qsa_prefill(128,512,17,interleaved)); + test_cases.emplace_back(new test_qsa_prefill(129,4096,257,interleaved)); + test_cases.emplace_back(new test_qsa_prefill(512,4096,2051,interleaved)); + test_cases.emplace_back(new test_qsa_prefill(128,40064,2051,interleaved)); + } + for (int q : {1,4,127}) test_cases.emplace_back(new test_qsa_prefill(q,4096,128)); + test_cases.emplace_back(new test_qsa_prefill_maskless(128,512,17)); + test_cases.emplace_back(new test_qsa_prefill_maskless(129,4096,257)); + test_cases.emplace_back(new test_qsa_prefill_maskless(130,4096,2051)); + for (int n_blocks : {512, 700, 2048}) for (int n_query : {1, 3, 130}) test_cases.emplace_back(new test_qsa_expand(n_blocks, n_query)); + test_cases.emplace_back(new test_qsa_prefill(128,4096,128,true,false,2)); + test_cases.emplace_back(new test_qsa_prefill(128,4096,128,true,false,1,4)); + test_cases.emplace_back(new test_qsa_prefill(128,4096,128,true,false,1,12,128)); + test_cases.emplace_back(new test_qsa_prefill(128,4096,2560)); + test_cases.emplace_back(new test_qsa_prefill(128,4096,2561)); + test_cases.emplace_back(new test_qsa_prefill(128,512,17,true,true)); + test_cases.emplace_back(new test_qsa_prefill(129,4096,257,true,true)); + // sparse top-k FA: (kv, nb, n_kv_raw, n_top_k, sinks). The Vulkan sparse path engages // when kv >= 3*(n_kv_raw + n_top_k) AND nb >= 64 (prefill-only); the nb < 64 cases // and the kv=512 case verify dense-fallback parity with the hint attached, the @@ -11817,6 +12828,13 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() { std::vector<std::unique_ptr<test_case>> test_cases; + + for (ggml_type type : {GGML_TYPE_F32, GGML_TYPE_F16}) { + for (int width : {90, 128}) { + test_cases.emplace_back(new test_get_rows(type, width, 65537, 65536, 1, 1, false)); + } + } + // Qwen3.8-Flash-Next decode hot path: fused gate/up uses this exact shape. test_cases.emplace_back(new test_mul_mat_id( GGML_TYPE_IQ3_S, GGML_TYPE_F32, 512, 10, false, 640, 1, 2560)); diff --git a/tests/test-ple-disk.cpp b/tests/test-ple-disk.cpp new file mode 100644 index 000000000000..8b116cebed41 --- /dev/null +++ b/tests/test-ple-disk.cpp @@ -0,0 +1,173 @@ +// llama_ple_disk must return, in every configuration, exactly the rows the CPU dequantizer +// produces for the same bytes: duplicates, cache hits, the serial paths and the pool alike. +#include "../src/llama-ple-disk.h" +#include "ggml.h" + +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <numeric> +#include <random> +#include <string> +#include <vector> + +#if defined(_WIN32) +int main() { + printf("llama_ple_disk is not available on Windows, skipping\n"); + return 0; +} +#else + +#include <unistd.h> + +struct table { + ggml_type type; + int64_t ne0; + int64_t nrows; + size_t offs; + size_t rs; + std::string fname; + std::vector<uint8_t> bytes; // the rows as they follow the `offs` prefix bytes in the file +}; + +static table make_table(ggml_type type, int64_t ne0, int64_t nrows, std::mt19937 & rng) { + table t; + t.type = type; + t.ne0 = ne0; + t.nrows = nrows; + t.rs = ggml_row_size(type, ne0); + t.offs = 3 * 4096 + 77; // rows deliberately start off any I/O alignment + t.bytes.resize((size_t) nrows * t.rs); + for (auto & b : t.bytes) { + b = (uint8_t) rng(); + } + + const char * dir = getenv("TMPDIR"); + std::string path = std::string(dir ? dir : "/tmp") + "/test-ple-disk-XXXXXX"; + std::vector<char> buf(path.begin(), path.end()); + buf.push_back(0); + const int fd = mkstemp(buf.data()); + GGML_ASSERT(fd >= 0); + std::vector<uint8_t> prefix(t.offs); + for (auto & b : prefix) { + b = (uint8_t) rng(); + } + GGML_ASSERT(write(fd, prefix.data(), prefix.size()) == (ssize_t) prefix.size()); + size_t done = 0; + while (done < t.bytes.size()) { + const ssize_t w = write(fd, t.bytes.data() + done, t.bytes.size() - done); + GGML_ASSERT(w > 0); + done += (size_t) w; + } + close(fd); + t.fname = buf.data(); + return t; +} + +static void reference(const table & t, const int32_t * idx, size_t n, float * dst) { + const auto * traits = ggml_get_type_traits(t.type); + for (size_t k = 0; k < n; ++k) { + const uint8_t * src = t.bytes.data() + (size_t) idx[k] * t.rs; + float * out = dst + k * (size_t) t.ne0; + if (t.type == GGML_TYPE_F32) { + memcpy(out, src, t.rs); + } else { + traits->to_float(src, out, t.ne0); + } + } +} + +int main() { + { + struct ggml_init_params ip = { 16 * ggml_tensor_overhead(), nullptr, true }; + ggml_free(ggml_init(ip)); // conversion tables + } + + std::mt19937 rng(20260915); + const int64_t nrows = 50000; + const struct { ggml_type type; int64_t ne0; } types[] = { + { GGML_TYPE_IQ4_NL, 160 }, // the qwen4exp n-gram table layout + { GGML_TYPE_Q8_0, 64 }, + { GGML_TYPE_F16, 48 }, + { GGML_TYPE_F32, 40 }, // no dequantizer: rows are copied + }; + + size_t n_gathers = 0, n_rows = 0; + for (const auto & ty : types) { + const table t = make_table(ty.type, ty.ne0, nrows, rng); + + std::vector<std::vector<int32_t>> patterns; + patterns.push_back({}); // nothing to do + patterns.push_back({ 7 }); + patterns.push_back({ 7, 7 }); // one distinct row, serial read + patterns.push_back({ 3, 1, 2 }); // three distinct rows, still serial + patterns.push_back({ 0, (int32_t) nrows - 1, 0, (int32_t) nrows - 1, 0 }); + { + std::vector<int32_t> v(257); // just past the serial dequantization limit + std::iota(v.begin(), v.end(), 100); + patterns.push_back(v); + } + { + std::vector<int32_t> v(5000); // descending, so sorting reorders everything + std::iota(v.rbegin(), v.rend(), 20000); + patterns.push_back(v); + } + { + std::vector<int32_t> v(20000); // random over the whole table + for (auto & r : v) { + r = (int32_t) (rng() % nrows); + } + patterns.push_back(v); + } + { + std::vector<int32_t> v(200000); // 500 distinct rows asked for 400 times each + for (auto & r : v) { + r = 1000 + (int32_t) (rng() % 500); + } + patterns.push_back(v); + } + { + std::vector<int32_t> v = patterns[7]; // half repeats a previous gather, half is new + for (size_t k = 0; k < v.size(); k += 2) { + v[k] = (int32_t) (rng() % nrows); + } + patterns.push_back(v); + } + + for (const int32_t threads : { 1, 3, 64 }) { + for (const size_t cache_mb : { (size_t) 0, (size_t) 1 }) { + for (const bool direct : { false, true }) { + llama_ple_disk::params p; + p.n_threads = threads; + p.cache_bytes = cache_mb << 20; + p.direct_io = direct; + llama_ple_disk disk(t.fname, t.offs, t.type, t.ne0, t.nrows, p); + GGML_ASSERT(disk.n_rows() == nrows && disk.ne0() == t.ne0 && disk.row_size() == t.rs); + + for (int pass = 0; pass < 2; ++pass) { // the second pass finds the first one's rows in the cache + for (const auto & idx : patterns) { + std::vector<float> got(idx.size() * (size_t) t.ne0 + 1), want(got.size()); + memset(got.data(), 0xff, got.size() * sizeof(float)); + memset(want.data(), 0xff, want.size() * sizeof(float)); + reference(t, idx.data(), idx.size(), want.data()); + disk.gather(idx.data(), idx.size(), got.data()); + if (memcmp(got.data(), want.data(), got.size() * sizeof(float)) != 0) { + fprintf(stderr, "FAIL: %s, %zu rows, threads=%d cache=%zuMiB direct=%d pass=%d\n", + ggml_type_name(t.type), idx.size(), threads, cache_mb, (int) direct, pass); + unlink(t.fname.c_str()); + return 1; + } + n_gathers += 1; + n_rows += idx.size(); + } + } + } + } + } + unlink(t.fname.c_str()); + } + printf("PASS: %zu gathers, %zu rows, byte-identical to the CPU dequantizer across 4 types x 3 thread counts x cache on/off x direct/buffered\n", + n_gathers, n_rows); + return 0; +} +#endif diff --git a/tests/test-qsa-prefix.cpp b/tests/test-qsa-prefix.cpp new file mode 100644 index 000000000000..f7223d34a627 --- /dev/null +++ b/tests/test-qsa-prefix.cpp @@ -0,0 +1,41 @@ +#include "../src/qsa-prefix-state.h" +#include "ggml.h" +#include <random> +#include <iostream> + +int main() { + qsa_prefix_state s(4096); + std::mt19937 rng(414); + for (int round=0; round<200; ++round) { + s.reset(); + for (int step=0; step<100; ++step) { + if (!s.cells.empty() && rng()%3==0) { s.truncate(rng()%(s.cells.size()+1)); } + size_t start=s.cells.size(); + int n=1+rng()%8; + std::vector<uint32_t> slots; + while (int(slots.size())<n) { + uint32_t c=rng()%4096; + if (s.positions[c]<0 && std::find(slots.begin(),slots.end(),c)==slots.end()) slots.push_back(c); + } + auto before=s.cells; + GGML_ASSERT(s.apply(0,start,slots)); + GGML_ASSERT(s.previous_size==before.size()); + GGML_ASSERT(std::equal(before.begin(),before.end(),s.cells.begin())); + for (size_t i=0;i<s.cells.size();++i) GGML_ASSERT(s.positions[s.cells[i]]==int(i)); + for (size_t b=0;b<s.block_positions.size();++b) GGML_ASSERT(s.block_positions[b]==int(4*b)); + if (s.cells.size()>8) { + int at=rng()%(s.cells.size()-4); + std::vector<uint32_t> same(s.cells.begin()+at,s.cells.begin()+at+4); + auto old=s.cells;GGML_ASSERT(s.apply(0,at,same));GGML_ASSERT(s.cells==old); + } + } + } + s.reset(); GGML_ASSERT(!s.apply(0,2,{3})); + s.reset(); GGML_ASSERT(!s.apply(0,0,{3,3})); + s.reset(); GGML_ASSERT(s.apply(0,0,{3,7})); GGML_ASSERT(!s.apply(1,2,{4})); + s.reset(); GGML_ASSERT(s.apply(0,0,{3,7})); GGML_ASSERT(!s.apply(0,2,{3})); + s.reset(); GGML_ASSERT(s.apply(0,0,{3,7})); GGML_ASSERT(!s.apply(0,0,{9})); + s.reset(); GGML_ASSERT(s.apply(0,0,{3,7})); s.truncate(0); GGML_ASSERT(s.apply(1,0,{3})); + s.invalidate(); GGML_ASSERT(!s.apply(1,1,{4})); + std::cout << "PASS: 20000 randomized append/rollback steps, in-place rewrites, holes, duplicates, collisions, sequence changes\n"; +} diff --git a/tools/llama-bench/llama-bench.cpp b/tools/llama-bench/llama-bench.cpp index 17adda38091f..963de9e2e685 100644 --- a/tools/llama-bench/llama-bench.cpp +++ b/tools/llama-bench/llama-bench.cpp @@ -279,6 +279,8 @@ static const char * lazy_mode_str(llama_lazy_mode mode) { return "auto"; case LLAMA_LAZY_MODE_ON: return "on"; + case LLAMA_LAZY_MODE_DIRECT: + return "on-direct"; default: GGML_ABORT("invalid lazy mode"); } @@ -475,7 +477,7 @@ static void print_usage(int /* argc */, char ** argv) { printf(" -fa, --flash-attn <on|off|auto> (default: %s)\n", join(transform_to_str(cmd_params_defaults.flash_attn, llama_flash_attn_type_name), ",").c_str()); printf(" -dev, --device <dev0/dev1/...> (default: auto)\n"); printf(" -lm, --load-mode <auto|none|mmap|mlock|mmap+mlock|dio> (default: %s)\n", join(transform_to_str(cmd_params_defaults.load_mode, llama_load_mode_name), ",").c_str()); - printf(" -lzm, --lazy-mode <on|auto|off> (default: %s)\n", join(transform_to_str(cmd_params_defaults.lazy_mode, lazy_mode_str), ",").c_str()); + printf(" -lzm, --lazy-mode <on|on-direct|auto|off> (default: %s)\n", join(transform_to_str(cmd_params_defaults.lazy_mode, lazy_mode_str), ",").c_str()); printf(" -embd, --embeddings <0|1> (default: %s)\n", join(cmd_params_defaults.embeddings, ",").c_str()); printf(" -ts, --tensor-split <ts0/ts1/..> (default: 0)\n"); printf(" -ot --override-tensor <tensor name pattern>=<buffer type>;...\n"); @@ -812,6 +814,8 @@ static cmd_params parse_cmd_params(int argc, char ** argv) { llama_lazy_mode mode; if (m == "on") { mode = LLAMA_LAZY_MODE_ON; + } else if (m == "on-direct") { + mode = LLAMA_LAZY_MODE_DIRECT; } else if (m == "auto") { mode = LLAMA_LAZY_MODE_AUTO; } else if (m == "off") { diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index ae9b028fa710..589493b9839e 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -1108,6 +1108,14 @@ struct server_context_impl { const bool is_resume = sleeping; params_base = params; + // A zero draft length means no speculation. Without this the context is sized from + // common_speculative_n_max() == 0 while the draft path is still initialized, and the + // first decode trips GGML_ASSERT(n_outputs_max <= cparams.n_outputs_max). + if (params_base.speculative.has_dft() && common_speculative_n_max(¶ms_base.speculative) <= 0) { + SRV_WRN("%s", "draft length is 0 - disabling speculative decoding\n"); + params_base.speculative.types = { COMMON_SPECULATIVE_TYPE_NONE }; + } + const auto output_limits = server_output_limits(params_base); params_base.n_outputs_max = output_limits.total; params_base.n_outputs_max_per_seq = output_limits.per_seq; From f86f7a170605bbad277539241c929949b18a851e Mon Sep 17 00:00:00 2001 From: nyovelt <github@feiranq.in> Date: Thu, 17 Sep 2026 14:33:07 -0400 Subject: [PATCH 167/212] HIP : route Q6_K through MMQ up to ne11 = 1024 on RDNA 3.5 (#38) Validated on gfx1151: Q6_K MUL_MAT 1238/1238 ROCm correctness cases, and test-backend-ops perf at m=4096 n=512 k=14336 improved 5342.90 -> 3507.31 us/run (1.52x). Gated to GGML_CUDA_CC_IS_RDNA3_5, so no other architecture changes behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- ggml/src/ggml-cuda/mmq.cu | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ggml/src/ggml-cuda/mmq.cu b/ggml/src/ggml-cuda/mmq.cu index dc7d2fdbc3a1..0e5479dc8eea 100644 --- a/ggml/src/ggml-cuda/mmq.cu +++ b/ggml/src/ggml-cuda/mmq.cu @@ -566,6 +566,11 @@ bool ggml_cuda_should_use_mmq(enum ggml_type type, int cc, int64_t ne11, int64_t case GGML_TYPE_Q2_K: return ne11 <= 128; case GGML_TYPE_Q6_K: + // RDNA 3.5 (gfx1151): dequantize + hipBLASLt loses to MMQ up to ne11 = 1024; at 2048 the two + // paths trade places per shape (K = 4096 favours hipBLASLt, K >= 12288 favours MMQ), see PR. + if (GGML_CUDA_CC_IS_RDNA3_5(cc)) { + return ne11 <= 1024; + } return ne11 <= (GGML_CUDA_CC_IS_RDNA3_0(cc) ? 128 : 256); case GGML_TYPE_IQ2_XS: case GGML_TYPE_IQ2_S: From 8c1c282ecb194e8f02613defcc4a07c22b6d1c08 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Thu, 17 Sep 2026 19:41:42 +0100 Subject: [PATCH 168/212] docs: record why upstream Vulkan sparse FA was deferred (#65) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --- docs/development/vulkan-sparse-fa-deferred.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/development/vulkan-sparse-fa-deferred.md diff --git a/docs/development/vulkan-sparse-fa-deferred.md b/docs/development/vulkan-sparse-fa-deferred.md new file mode 100644 index 000000000000..caea367c874f --- /dev/null +++ b/docs/development/vulkan-sparse-fa-deferred.md @@ -0,0 +1,81 @@ +# Deferred: Vulkan sparse Flash Attention (Strix Halo) + +Written during the 2026-09-15 sync of `halo-box/llama.cpp` into this fork (PR #64). +Upstream's sparse Flash Attention could not be merged mechanically and was deferred. +This file says why, and what integrating it actually requires, so the next person does +not have to rediscover it. + +Nothing below is a measurement. + +--- + +## What is not in this fork + +`fc82583e6 vulkan: support sparse Flash Attention (ggml-org#28105)` is **not** here. +`ggml/src/ggml-vulkan/` was reverted wholesale to the strix version during the sync. + +Deferred with it: `f1e44dcc1 vulkan: workaround NV queuesubmit driver bug +(ggml-org#28830)`. + +## Why it could not be merged mechanically + +Upstream and strix collide on three things at once: + +| | strix | upstream #28105 | +|---|---|---| +| Spec-constant bit | `DYNAMIC_KV = (Flags & 16)` | `USE_SPARSE = (Flags & 16)` | +| FA push constants | `m_row_len, gqa_ratio, split_k_num, output_k_num, partial_output` | `sparse_base` | +| Sparse mechanism | gather/compaction family + DeepSeek-V4 split keyed on bit 31 of `gqa_ratio` | binding-7 index gather, `fa_kv_index()` | + +They are rival implementations of overlapping functionality on the same bit. + +Taking either side alone breaks the build. Upstream's sparse code merged *cleanly* into +`flash_attn.comp`, `flash_attn_cm1.comp` and `flash_attn_cm2.comp` — strix had not +touched those regions — so keeping strix's `flash_attn_base.glsl` leaves those three +files calling an undefined `fa_kv_index()` / `USE_SPARSE` / `data_sparse`. Reverting the +whole directory is the only self-consistent option without a GPU. + +## What integration actually requires + +1. Renumber one feature's `Flags` bit. Audit every `string_to_spv` call in + `vulkan-shaders-gen.cpp` and every `Flags` write in `ggml-vulkan.cpp` — the bit is set + on the C++ side and read in GLSL, with no shared header to keep the two honest. +2. Merge the two push-constant structs and update the single C++ writer. + **A mismatched layout does not crash.** The shader reads whatever bytes sit at those + offsets and emits plausible-looking wrong tensors. Validate numerically, not by + "it ran". +3. Decide whether `DYNAMIC_KV` and `USE_SPARSE` coexist or one supersedes. strix's gather + family (`flash_attn_gather{,_dq,_union,_union_dq}.comp`) may already cover upstream's + case, in which case the answer is to drop one. + +## How to settle it + +```sh +cmake -B build-vk -DGGML_VULKAN=ON && cmake --build build-vk -j +build-vk/bin/test-backend-ops -b Vulkan0 -o FLASH_ATTN_EXT # correctness vs CPU ref +build-vk/bin/test-backend-ops -b Vulkan0 perf -o FLASH_ATTN_EXT # includes the strix probes +``` + +The strix probes already in `test-backend-ops.cpp` are the ones that matter here: +MALL-spill (32 KV heads, K/V footprint 8x > 32MB MALL), L2-residence (1 KV head x GQA 32, +K/V 5.2MB fits L2), and the KV-cache-layout probe (`{0, 2, 1, 3}`). They were written to +distinguish cache-bandwidth-bound from issue-bound on this part. If upstream's sparse path +is integrated, it must not regress them. + +Baseline, measured on a Radeon 8060S (RADV STRIX_HALO) at `dffbb7888`: FLASH_ATTN_EXT +5339/5339 pass on Vulkan0. + +--- + +## Provenance + +| Ref | What | +|---|---| +| `fc82583e6` | upstream sparse FA (ggml-org#28105) — deferred | +| `f1e44dcc1` | upstream NV queuesubmit workaround (ggml-org#28830) — deferred | +| `dced43c58` | the sync merge (PR #64) | +| `82aed6017` | #63 merged into that sync | + +The PLE n-gram table's disk reader survived the sync as the backend of +`--lazy-mode on-direct`; see PR #64's description for what changed and what was dropped +(O_DIRECT reads and the `--ngram-*` tuning knobs). From 57f5bde666cf99cc7061116b991fd9da008f8053 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Thu, 17 Sep 2026 22:34:13 +0100 Subject: [PATCH 169/212] fix: guard the qwen4exp PLE prefetch on the model architecture llama_context::decode calls qwen4exp_ple_prefetch for every architecture once a batch reaches 4096 tokens, and the function immediately downcasts the model to llama_model_qwen4exp. On any other architecture that reads ple_disk out of an unrelated object; when those bytes are non-zero the null check passes and page_cached() dereferences them. Reproduced on gfx1151 (ROCm 7.2.1) with Signal-3.8-27B-AP-Q4_K_XL (qwen35): llama-bench -m <model> -ngl 99 -fa on -p 4096 -b 4096 -ub 4096 -> SIGSEGV in llama_ple_disk::page_cached() const from qwen4exp_ple_prefetch(llama_model const&, int const*, int) from llama_context::decode(llama_batch const&) Any non-qwen4exp model crashes at ubatch >= 4096; ubatch <= 2048 never reaches the call. qwen4exp models are unaffected, which is why this survived review. Guards both the call site and the function. After this, the same command runs at ubatch 4096, 8192 and 16384 on qwen35 (dense) and gemma4/gpt-oss (MoE). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- src/llama-context.cpp | 4 +++- src/models/qwen4exp.cpp | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/llama-context.cpp b/src/llama-context.cpp index ccfd52175747..53668fa316aa 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -1743,7 +1743,9 @@ int llama_context::decode(const llama_batch & batch_inp) { { // warm the page cache for this batch's per-layer-embedding rows while the first chunk is on the GPU; // posix_fadvise only, so a wrong prediction costs readahead and nothing else extern void qwen4exp_ple_prefetch(const llama_model & model, const llama_token * tokens, int32_t n_tokens); - if (batch_inp.token && batch_inp.n_tokens >= 4096) { qwen4exp_ple_prefetch(model, batch_inp.token, batch_inp.n_tokens); } + if (model.arch == LLM_ARCH_QWEN4EXP && batch_inp.token && batch_inp.n_tokens >= 4096) { + qwen4exp_ple_prefetch(model, batch_inp.token, batch_inp.n_tokens); + } } const uint32_t n_outputs_all = balloc->get_n_outputs(); diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 899258ab493e..b1a127d1b546 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -1749,6 +1749,12 @@ void qwen4exp_ple_prefetch(const llama_model & model_base, const llama_token * t if (!tokens || n_tokens < 4096) { return; } + // llama_context::decode calls this for every architecture, so the downcast below is only valid + // once the arch is known: on any other model it reads ple_disk out of an unrelated object and + // dereferences whatever that happens to hold. + if (model_base.arch != LLM_ARCH_QWEN4EXP) { + return; + } const auto & pmodel = static_cast<const llama_model_qwen4exp &>(model_base); if (!pmodel.ple_disk || !pmodel.ple_disk->page_cached()) { return; From 42f7e8948b686d86392c8299d66790565350f875 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Thu, 17 Sep 2026 23:08:09 +0100 Subject: [PATCH 170/212] cuda: enable the BF16 WMMA matmul path only for qwen4exp mmb_enabled() was true for every gfx1151 device, and mmb_quant_type() claims almost every quant type, so the BF16 WMMA path took MUL_MAT/MUL_MAT_ID away from MMQ for every model. Its tiles and fusions are tuned for the qwen4exp shapes (the hardcoded 320/10240 gate-mix shapes among them); elsewhere it loses. Measured on gfx1151, ROCm 7.2.1, llama-bench -ngl 99 -fa on, prefill t/s: model ub mmb on this PR pre-#63 (cfe6bb14e) Signal-3.8-27B Q4_K_XL 512 119.6 467.7 461.2 Signal-3.8-27B Q4_K_XL 16384 123.3 421.8 416.1 Qwen3.8-27B GSQ-RCO IQ3_S 512 116.3 438.8 434.9 gemma-4-26B-A4B Q4_0 2048 1502.2 2087.6 2083.0 gpt-oss-20b MXFP4 2048 2066.2 2406.6 2411.3 Dense qwen35 paid 3.4-3.9x, MoE 14-28%. Token generation is unchanged in every case (the path only engages from 512 tokens up, so it never runs during decode). The llama layer now opts a model in by architecture through the backend's get_proc_address, so qwen4exp keeps the path it was tuned for and nothing else pays for it. A process that loads several models shares the flag, last load wins; that is noted at the definition. Not measured: qwen4exp itself, for want of a checkpoint on this box. The gain there is pwilkin's (#63) and this does not change that path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- ggml/src/ggml-cuda/ggml-cuda.cu | 8 ++++++++ ggml/src/ggml-cuda/mmb.cu | 18 +++++++++++++++++- ggml/src/ggml-cuda/mmb.cuh | 2 ++ src/llama.cpp | 11 +++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 255fcf8294b5..b01406877642 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -7481,8 +7481,16 @@ static ggml_backend_feature * ggml_backend_cuda_get_features(ggml_backend_reg_t GGML_UNUSED(reg); } +// enable the BF16 WMMA matmul path (mmb); the llama layer calls this per model, by architecture +static void ggml_backend_cuda_set_mmb_enabled(bool enable) { + ggml_cuda_mmb_set_opt_in(enable); +} + static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, const char * name) { GGML_UNUSED(reg); + if (strcmp(name, "ggml_backend_cuda_set_mmb_enabled") == 0) { + return (void *)ggml_backend_cuda_set_mmb_enabled; + } if (strcmp(name, "ggml_backend_comm_init") == 0) { return (void *)ggml_backend_cuda_comm_init; } diff --git a/ggml/src/ggml-cuda/mmb.cu b/ggml/src/ggml-cuda/mmb.cu index 4fc7e7440971..0f73f3fdb277 100644 --- a/ggml/src/ggml-cuda/mmb.cu +++ b/ggml/src/ggml-cuda/mmb.cu @@ -1,4 +1,6 @@ #include "mmb.cuh" + +#include <atomic> #include "unary.cuh" #include <unordered_map> #include <map> @@ -679,8 +681,17 @@ static const uint16_t * mmb_shadow_lookup(const ggml_tensor * w) { auto it = g_mmb_shadow.find(w->data); return it == g_mmb_shadow.end() ? nullptr : it->second; } -// the BF16 WMMA kernels are RDNA3.5 (gfx1151) work: other devices keep the MMQ/MMVQ paths +// The BF16 WMMA kernels are RDNA3.5 (gfx1151) work, and their tiles and fusions are tuned for the +// qwen4exp shapes. On other architectures they take MUL_MATs away from MMQ and lose: dense qwen35 +// prefill measured 3.4-3.6x slower on gfx1151 at every ubatch from 512 to 16384, and MoE 1.1-1.2x. +// So the llama layer opts a model in by arch (ggml_backend_cuda_set_mmb_enabled); default is off. +// A process that loads several models shares this flag: the last load wins. +static std::atomic<bool> g_mmb_opt_in{false}; + bool mmb_enabled() { + if (!g_mmb_opt_in.load(std::memory_order_relaxed)) { + return false; + } const int id = ggml_cuda_get_device(); return GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[id].cc); } @@ -697,6 +708,11 @@ bool mmb_glu() { return true; } } // namespace +// opted in per model by the llama layer, by architecture (see the note on mmb_enabled above) +void ggml_cuda_mmb_set_opt_in(bool enable) { + g_mmb_opt_in.store(enable, std::memory_order_relaxed); +} + const uint16_t * ggml_cuda_mmb_cache_lookup(const ggml_tensor * t) { const ggml_tensor * root = mmb_root(t); for (auto & e : g_mmb_slots) if (e.buf && e.root == root && e.data == t->data) return e.buf->get(); diff --git a/ggml/src/ggml-cuda/mmb.cuh b/ggml/src/ggml-cuda/mmb.cuh index 431785828cea..09f4e0a0bee8 100644 --- a/ggml/src/ggml-cuda/mmb.cuh +++ b/ggml/src/ggml-cuda/mmb.cuh @@ -1,6 +1,8 @@ #pragma once #include "common.cuh" // Quantized-weight BF16 WMMA GEMM on gfx1151, from 512 tokens up. +// opted in per model by the llama layer (see ggml_backend_cuda_set_mmb_enabled): tuned for qwen4exp +void ggml_cuda_mmb_set_opt_in(bool enable); bool ggml_cuda_mmb_supported_mm (const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst); bool ggml_cuda_mmb_supported_mmid(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst); void ggml_cuda_mul_mat_mmb (ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst); diff --git a/src/llama.cpp b/src/llama.cpp index ad8e443882ad..c3683237532f 100644 --- a/src/llama.cpp +++ b/src/llama.cpp @@ -344,6 +344,17 @@ static std::pair<int, llama_model *> llama_model_load(struct gguf_context * meta model->hparams.vocab_only = params.vocab_only; model->hparams.no_alloc = params.no_alloc; + // The CUDA/HIP BF16 WMMA matmul path is tuned for the qwen4exp shapes; on other architectures it + // takes MUL_MATs away from MMQ and loses (dense qwen35 prefill measured 3.4x slower on gfx1151). + // Opt in by architecture rather than by device. + for (size_t i = 0; i < ggml_backend_reg_count(); ++i) { + auto * set_mmb_fn = (void (*)(bool)) ggml_backend_reg_get_proc_address( + ggml_backend_reg_get(i), "ggml_backend_cuda_set_mmb_enabled"); + if (set_mmb_fn) { + set_mmb_fn(model->arch == LLM_ARCH_QWEN4EXP); + } + } + try { model->load_hparams(ml); } catch(const std::exception & e) { From 1a005ba1c9bdcf53ed92e8a2914344667ce4b717 Mon Sep 17 00:00:00 2001 From: Jaap Buurman <jaapbuurman@gmail.com> Date: Thu, 24 Sep 2026 12:05:40 +0200 Subject: [PATCH 171/212] hip : isolate MMB scratch by backend context Keep MMB scratch, BF16 marks, graph fingerprints, and converted weights owned by their backend context. Release them after queued work and captured graphs finish. Add a two-context replay and teardown regression, and record gfx1151 correctness, memory, and upstream applicability findings. Assisted-by: Codex --- docs/development/rocm-mmb-context.md | 53 ++++++++ ggml/src/ggml-cuda/common.cuh | 7 + ggml/src/ggml-cuda/ggml-cuda.cu | 76 ++++++----- ggml/src/ggml-cuda/hyperconn.cu | 4 +- ggml/src/ggml-cuda/mmb.cu | 133 ++++++++++++------- ggml/src/ggml-cuda/mmb.cuh | 14 +- ggml/src/ggml-cuda/moe-weighted-reduction.cu | 6 +- tests/test-backend-ops.cpp | 111 +++++++++++++++- 8 files changed, 307 insertions(+), 97 deletions(-) create mode 100644 docs/development/rocm-mmb-context.md diff --git a/docs/development/rocm-mmb-context.md b/docs/development/rocm-mmb-context.md new file mode 100644 index 000000000000..c411b0098423 --- /dev/null +++ b/docs/development/rocm-mmb-context.md @@ -0,0 +1,53 @@ +# ROCm MMB context ownership + +The gfx1151 MMB kernels used process-global activation scratch, BF16 marks, graph fingerprints, and converted-weight caches. Target and MTP draft contexts have independent asynchronous streams. Draft catch-up could overwrite storage still used by the target, including storage referenced by captured HIP graphs. Freeing either backend also released the other backend's cached storage. + +Keep this state in its backend context. Scratch allocations and temporary conversions belong to the active stream's pool. Producer slots can be read across streams through graph dependencies. Converted weights and BF16 marks belong to the context. At context destruction, wait for its queued work, destroy its captured graphs, then release its MMB allocations before destroying the pools. No target/draft synchronization is added to inference. + +Sharing immutable token embeddings or output projection weights is not itself the cause. The unsafe sharing was in the backend's mutable temporary storage and its lifetime management. + +## Regression coverage + +Run `test-backend-ops test -b ROCm0 -o MMB_CONTEXT` on gfx1151. The case uses two independent schedulers and the existing HC_CHAIN graph, with 512 tokens each. It preserves graph inputs across replay, checks stable serialized references, warms captured graphs, overlaps target and draft 16 times, and checks the surviving target after draft destruction. Every output must be finite and byte-identical to its serialized reference. + +The final test fails against unchanged backend revision `8c1c282ecb194e8f02613defcc4a07c22b6d1c08` and passes against the fix, including with `GGML_CUDA_DISABLE_GRAPHS=1`. During test development, an initial version let the allocator overwrite inputs between replays; that version was not a valid race oracle. The final test protects those inputs and was rerun against the unchanged libraries. + +## Model validation + +Hardware: Ryzen AI Max+ PRO 395, Radeon 8060S/gfx1151, 128 GiB unified memory, balanced power profile. ROCm 10.0.0-2, HIP Clang 23, GCC 16.2.1, Release build, native CPU and gfx1151 GPU targets, HIP graphs enabled. + +Target: AesSedai Qwen3.8-Flash-Next Q4_K_M, snapshot `b90616d610f47a124fe246b70bb136aeeb25a2ad`, five GGUF shards. Draft: `mtp-Qwen3.8-Flash-Next-Q4_K_M.gguf`. Model hashes and corpus hashes are in `/tmp/strix-mtp-investigation/{model,corpus}-sha256.txt` on the test machine. + +A diagnostic probe uses the production draft catch-up hook, fixed token IDs, FA enabled, full ROCm0 offload, batch/ubatch 2048, context 32768, lazy-mode on-direct, and load-mode dio. It saves complete final-prefill logits and 32 fixed-token continuation logit vectors, each with 248320 vocabulary entries. The reference is the unchanged fork with target-only MTP hidden extraction enabled. This isolates target/draft interference without sampling differences. + +| Corpus | Prefill tokens | Token IDs, all logits, and top-1 | +| --- | ---: | --- | +| Code | 4096 | Byte-identical | +| Prose | 4096 | Byte-identical | +| Structured | 4096 | Byte-identical | +| Numeric | 4096 | Byte-identical | +| Code | 26776 | Byte-identical | + +The HC_CHAIN and GATED_DELTA_NET focused suite passed 56/56 cases. The complete ROCm backend suite passed 29695/29695 cases, followed by the new MMB_CONTEXT regression. Unsupported operations were skipped by the existing test harness. + +## Memory and performance + +Context ownership can increase memory use because target and draft retain separate scratch and converted-weight caches. It does not duplicate the entire model or KV cache. Converted-weight initialization can also be repeated per context. The default inference path remains asynchronous; the added wait is at context destruction. + +For the same 4096-token code probe with synchronized draft catch-up in both builds, intercepting hipMalloc/hipFree measured peak live allocations of 87,896,241,152 bytes before and 87,978,816,512 bytes after: an increase of 78.75 MiB. Both ended with zero tracked live bytes. This is one workload and measures HIP allocation requests, not total physical memory, registered host mappings, or allocator-internal overhead. It is not a maximum for other batch sizes or contexts. No throughput or latency conclusion is established by this allocation measurement. + +## Upstream applicability + +Checked upstream ggml-org/llama.cpp revision `53ed051ce5e8193652e449f43216ca3859454f49` on 2026-09-24. Its complete source tree has neither MMB nor hyperconnection kernel files. Its CUDA/HIP allocation pools already belong to each backend context and stream. Upstream also supports asynchronous MTP draft catch-up, but does not contain the process-global MMB state responsible for this failure. + +The affected file entered this fork in commit `0636c9aee4a2ab1d38bb7e8c038227c6634c3857` (PR #63). This specific bug and fix are fork-specific. This source inspection does not certify upstream MTP against unrelated defects; no upstream runtime comparison was performed. + +Sources: [upstream backend](https://github.com/ggml-org/llama.cpp/tree/53ed051ce5e8193652e449f43216ca3859454f49/ggml/src/ggml-cuda), [context-owned pools](https://github.com/ggml-org/llama.cpp/blob/53ed051ce5e8193652e449f43216ca3859454f49/ggml/src/ggml-cuda/common.cuh#L1559), [fork introduction](https://github.com/halo-box/strix-llama.cpp/commit/0636c9aee4a2ab1d38bb7e8c038227c6634c3857). + +## Limits and artifacts + +Status: TARGET PASS for the ROCm scratch-race regression and model interference checks; INCOMPLETE for the broader project certification matrix described below. + +Raw logs, probe outputs, commands, source diff, allocation tracer, and upstream source snapshots are under `/tmp/strix-mmb-fix` on the test machine. The earlier reproduction and probe source are under `/tmp/strix-mtp-investigation`. + +The model probe exercises catch-up and deterministic target decode, not the complete server sampling/acceptance loop or the original browser conversation. Multi-device configurations, NVIDIA CUDA, the optional graph-fork optimization, and a complete throughput/acceptance benchmark matrix are not certified. The separate rollback-validity and Vulkan attention issues are not changed by this fix. diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index c507807acc40..89b1e3b0ade0 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -1460,6 +1460,8 @@ struct ggml_cuda_stream_context { } }; +struct ggml_cuda_mmb_context; + struct ggml_backend_cuda_context { int device; std::string name; @@ -1472,6 +1474,11 @@ struct ggml_backend_cuda_context { int curr_stream_no = 0; + ggml_cuda_mmb_context * mmb = nullptr; + bool mmb_after_compute = true; + const void * mmb_first_split = nullptr; + std::vector<uint64_t> mmb_graph_sigs; + #ifdef USE_CUDA_GRAPH // Map from first_node_ptr to cuda_graph - allows multiple graphs per context // when the computation is split across CPU/GPU (e.g., with --n-cpu-moe) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 255fcf8294b5..f4dd212997d6 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -709,6 +709,23 @@ ggml_backend_cuda_context::~ggml_backend_cuda_context() { std::unique_lock<std::mutex> lock(ggml_cuda_lock); ggml_cuda_lock_cv.wait(lock, []{ return ggml_cuda_lock_counter.load(std::memory_order_relaxed) == 0; }); + if (mmb) { + // Queued work and captured graphs can still reference the MMB buffers. + for (int i = 0; i < GGML_CUDA_MAX_DEVICES; ++i) { + for (int j = 0; j < GGML_CUDA_MAX_STREAMS; ++j) { + if (streams[i][j]) { + ggml_cuda_set_device(i); + CUDA_CHECK(cudaStreamSynchronize(streams[i][j])); + } + } + } + ggml_cuda_set_device(device); +#ifdef USE_CUDA_GRAPH + cuda_graphs.clear(); +#endif + ggml_cuda_mmb_release_all(*this); + } + if (copy_event != nullptr) { CUDA_CHECK(cudaEventDestroy(copy_event)); } @@ -2082,11 +2099,11 @@ static void ggml_cuda_mul_mat_id(ggml_backend_cuda_context & ctx, ggml_tensor * static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct ggml_tensor * dst) { // a tensor marked BF16-only was never written as F32: any reader outside the fused paths is a bug, not a fallback - if (ggml_cuda_mmb_marks_count() > 0 && dst->op != GGML_OP_MUL_MAT && dst->op != GGML_OP_MUL_MAT_ID && dst->op != GGML_OP_VIEW && + if (ggml_cuda_mmb_marks_count(ctx) > 0 && dst->op != GGML_OP_MUL_MAT && dst->op != GGML_OP_MUL_MAT_ID && dst->op != GGML_OP_VIEW && dst->op != GGML_OP_RESHAPE && dst->op != GGML_OP_PERMUTE && dst->op != GGML_OP_TRANSPOSE && dst->op != GGML_OP_NONE) { for (int s = 0; s < GGML_MAX_SRC && dst->src[s]; ++s) { const ggml_tensor * src = dst->src[s]; - if (ggml_cuda_mmb_is_bf16_only(src) || (src->view_src && ggml_cuda_mmb_is_bf16_only(src->view_src))) { + if (ggml_cuda_mmb_is_bf16_only(ctx, src) || (src->view_src && ggml_cuda_mmb_is_bf16_only(ctx, src->view_src))) { GGML_ABORT("MMB: %s(%s) reads BF16-only tensor %s through the unfused path", ggml_op_name(dst->op), dst->name, src->name); } } @@ -2462,7 +2479,6 @@ static const char * ggml_backend_cuda_get_name(ggml_backend_t backend) { } static void ggml_backend_cuda_free(ggml_backend_t backend) { - ggml_cuda_mmb_release_all(); // release cached BF16 conversions before the pool is destroyed (pool leak assert) ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *)backend->context; delete cuda_ctx; @@ -4372,7 +4388,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph if (ggml_cuda_mmb_blk16() && nadd < cgraph->n_nodes && ggml_node_has_n_uses(cgraph, i + match.node_count - 1, 1)) { const ggml_tensor * add = cgraph->nodes[nadd]; if (add->op == GGML_OP_ADD && add->type == GGML_TYPE_F32 && ggml_is_contiguous(add) && - ggml_are_same_shape(add, match.dst) && ggml_cuda_mmb_is_bf16_only(add)) { + ggml_are_same_shape(add, match.dst) && ggml_cuda_mmb_is_bf16_only(*cuda_ctx, add)) { const ggml_tensor * o = add->src[0] == match.dst ? add->src[1] : (add->src[1] == match.dst ? add->src[0] : nullptr); if (o && o->type == GGML_TYPE_F32 && ggml_is_contiguous(o) && ggml_are_same_shape(o, match.dst)) { merge = o; match.dst = const_cast<ggml_tensor *>(add); count = match.node_count + 1; @@ -4423,7 +4439,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } if (node->op == GGML_OP_GLU && i + 1 < cgraph->n_nodes && ggml_get_glu_op(node) == GGML_GLU_OP_SWIGLU && node->src[1] && - !ggml_cuda_mmb_is_bf16_only(node) && !ggml_cuda_mmb_claims(cgraph->nodes[i + 1])) { + !ggml_cuda_mmb_is_bf16_only(*cuda_ctx, node) && !ggml_cuda_mmb_claims(cgraph->nodes[i + 1])) { ggml_tensor * next = cgraph->nodes[i + 1]; const bool has_ids = next->op == GGML_OP_MUL_MAT_ID; if (has_ids || next->op == GGML_OP_MUL_MAT) { @@ -5467,18 +5483,18 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph if (skip > 0 && !args.single_block) { // HC16: a BF16 copy of xn for its consumers, and BF16 residual / block_out streams where every reader agreed args.out_xn_bf16 = ggml_cuda_mmb_cache_reserve(*cuda_ctx, args.out_xn, (size_t) ggml_nelements(args.out_xn)); - args.store_xn_f32 = !(args.out_xn_bf16 && ggml_cuda_mmb_is_bf16_only(args.out_xn)); + args.store_xn_f32 = !(args.out_xn_bf16 && ggml_cuda_mmb_is_bf16_only(*cuda_ctx, args.out_xn)); if (ggml_cuda_mmb_res16()) { const ggml_tensor * rin = args.residual->view_src ? args.residual->view_src : args.residual; const ggml_tensor * rout = args.out_res->view_src ? args.out_res->view_src : args.out_res; - const bool in16 = ggml_cuda_mmb_is_bf16_only(rin) && args.residual->view_offs == 0; - const bool out16 = ggml_cuda_mmb_is_bf16_only(rout) && args.out_res->view_offs == 0; + const bool in16 = ggml_cuda_mmb_is_bf16_only(*cuda_ctx, rin) && args.residual->view_offs == 0; + const bool out16 = ggml_cuda_mmb_is_bf16_only(*cuda_ctx, rout) && args.out_res->view_offs == 0; if (args.out_res->data == args.residual->data && in16 != out16) { GGML_ABORT("hc_combine_norm: in-place residual with mismatched BF16 marks (%s); the graph_optimize " "allocation dependency should have separated them", args.out_res->name); } const ggml_tensor * rblk = args.block_out->view_src ? args.block_out->view_src : args.block_out; - args.blk_in_bf16 = (ggml_cuda_mmb_blk16() && ggml_cuda_mmb_is_bf16_only(rblk) && args.block_out->view_offs == 0) + args.blk_in_bf16 = (ggml_cuda_mmb_blk16() && ggml_cuda_mmb_is_bf16_only(*cuda_ctx, rblk) && args.block_out->view_offs == 0) ? (const uint16_t *) args.block_out->data : nullptr; args.res_in_bf16 = in16 ? (const uint16_t *) args.residual->data : nullptr; args.res_out_bf16 = out16 ? (uint16_t *) args.out_res->data : nullptr; @@ -5491,7 +5507,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph ggml_cuda_info().devices[cuda_ctx->device].warp_size, false); if (structural > 0) { const ggml_cuda_hc_combine_norm_args & a = skip > 0 ? args : sa; - auto marked = [](const ggml_tensor * t) { return ggml_cuda_mmb_is_bf16_only(t) || (t->view_src && ggml_cuda_mmb_is_bf16_only(t->view_src)); }; + auto marked = [cuda_ctx](const ggml_tensor * t) { return ggml_cuda_mmb_is_bf16_only(*cuda_ctx, t) || (t->view_src && ggml_cuda_mmb_is_bf16_only(*cuda_ctx, t->view_src)); }; if (marked(a.out_xn) || marked(a.residual) || marked(a.out_res) || marked(a.block_out)) { GGML_ABORT("hc_combine_norm: %s has BF16-only streams but the fused kernel was refused (%s); the graph_optimize " "allocation dependencies should have prevented the aliasing", a.out_res->name, @@ -5888,16 +5904,10 @@ static bool ggml_cuda_graph_set_enabled(ggml_backend_cuda_context * cuda_ctx, co } #endif // USE_CUDA_GRAPH -// tracks when a new split sequence starts, so the mmb marks are cleared once per sequence -static bool g_gt_after_compute = true; -static const void * g_gt_first_split = nullptr; - // the marks are keyed by tensor pointer and belong to the graphs graph_optimize last saw. A graph computed without // that optimize (test-backend-ops, any direct ggml_backend_graph_compute caller) can recycle those addresses, so a // producer would store BF16 into a tensor of another graph and a plain reader would abort. Fingerprint each optimized // split and drop the marks when a graph arrives that none of them match; a reused (identical) graph keeps them. -static std::vector<uint64_t> g_gt_graph_sigs; - static uint64_t ggml_cuda_graph_sig(const ggml_cgraph * cgraph) { // no data pointers: the scheduler allocates after graph_optimize, so they differ between optimize and compute uint64_t h = 1469598103934665603ull ^ (uint64_t) cgraph->n_nodes; @@ -5913,16 +5923,16 @@ static uint64_t ggml_cuda_graph_sig(const ggml_cgraph * cgraph) { } static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) { - if (ggml_cuda_mmb_marks_count() > 0) { + ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context; + if (ggml_cuda_mmb_marks_count(*cuda_ctx) > 0) { const uint64_t sig = ggml_cuda_graph_sig(cgraph); - if (std::find(g_gt_graph_sigs.begin(), g_gt_graph_sigs.end(), sig) == g_gt_graph_sigs.end()) { - ggml_cuda_mmb_marks_clear(); - g_gt_graph_sigs.clear(); - g_gt_first_split = nullptr; + if (std::find(cuda_ctx->mmb_graph_sigs.begin(), cuda_ctx->mmb_graph_sigs.end(), sig) == cuda_ctx->mmb_graph_sigs.end()) { + ggml_cuda_mmb_marks_clear(*cuda_ctx); + cuda_ctx->mmb_graph_sigs.clear(); + cuda_ctx->mmb_first_split = nullptr; } } - ggml_cuda_mmb_begin_graph(); - ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context; + ggml_cuda_mmb_begin_graph(*cuda_ctx); ggml_cuda_set_device(cuda_ctx->device); @@ -5977,7 +5987,7 @@ static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, ggml_cuda_graph_evaluate_and_capture(cuda_ctx, cgraph, use_cuda_graph, cuda_graph_update_required, graph_key); - g_gt_after_compute = true; + cuda_ctx->mmb_after_compute = true; return GGML_STATUS_SUCCESS; } @@ -6041,7 +6051,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context; { // marks live for the whole scheduled graph (all splits): clear at the first optimize after a compute, or when the first split repeats const void * key = cgraph->n_nodes ? cgraph->nodes[0] : nullptr; - if (g_gt_after_compute || g_gt_first_split == nullptr || key == g_gt_first_split) { ggml_cuda_mmb_marks_clear(); g_gt_graph_sigs.clear(); g_gt_first_split = key; g_gt_after_compute = false; } + if (cuda_ctx->mmb_after_compute || cuda_ctx->mmb_first_split == nullptr || key == cuda_ctx->mmb_first_split) { ggml_cuda_mmb_marks_clear(*cuda_ctx); cuda_ctx->mmb_graph_sigs.clear(); cuda_ctx->mmb_first_split = key; cuda_ctx->mmb_after_compute = false; } } { // HC16: mark tensors whose readers all take the BF16 copy, so their producers skip the F32 store if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { @@ -6064,7 +6074,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE) continue; ok = false; } - if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, xn)) ggml_cuda_mmb_mark_bf16_only(xn); + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, xn)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, xn); } if (ggml_cuda_mmb_blk16()) { // block_out (attention out-proj / MoE merge) is read only by the fused combine std::vector<const ggml_tensor *> blks; @@ -6102,7 +6112,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph if (t->op == GGML_OP_MUL || t->op == GGML_OP_ADD) continue; ok = false; } - if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, b)) ggml_cuda_mmb_mark_bf16_only(b); + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, b)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, b); } } if (ggml_cuda_mmb_res16()) { // residual stream: the only readers are the fused norm and the next combine @@ -6132,7 +6142,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph if (next_combine) continue; ok = false; } - if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, r)) ggml_cuda_mmb_mark_bf16_only(r); + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, r)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, r); } } for (int i = 0; i < cgraph->n_nodes; ++i) { // fused stream mix output read only by MMB GEMMs @@ -6150,7 +6160,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) continue; ok = false; } - if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, d)) ggml_cuda_mmb_mark_bf16_only(d); + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, d)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, d); } for (int i = 0; i < cgraph->n_nodes; ++i) { // the HC gate GEMM feeding only the fused mix keeps its BF16 epilogue const ggml_tensor * t = cgraph->nodes[i]; @@ -6163,7 +6173,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph if (u->op == GGML_OP_UNARY && ggml_get_unary_op(u) == GGML_UNARY_OP_SIGMOID) { ggml_cuda_hc_mix_args ma; if (ggml_cuda_hc_mix_closed(cgraph, n, ma) > 0 && ma.gate == t) continue; } ok = false; } - if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, t)) ggml_cuda_mmb_mark_bf16_only(t); + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, t)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, t); } if (ggml_cuda_mmb_down16()) { // IQ4_NL routed down GEMM read only by the weighted reduction keeps BF16 outputs for (int i = 0; i < cgraph->n_nodes; ++i) { @@ -6177,7 +6187,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph if (ex->src[0]->type != GGML_TYPE_IQ4_NL) continue; // other formats keep F32 expert outputs if (!ggml_node_has_n_uses(cgraph, xi, 1)) continue; if (ex->ne[0] % 8 != 0 || ggml_nrows(ex) < 512) continue; - if (ggml_cuda_marks_readers_local(cgraph, ex)) ggml_cuda_mmb_mark_bf16_only(ex); + if (ggml_cuda_marks_readers_local(cgraph, ex)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, ex); } } if (ggml_cuda_mmb_res16()) { // 16-bit residual stream: producer and consumer on different buffers @@ -6214,7 +6224,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph if (t->op == GGML_OP_MUL_MAT_ID && t->src[1] == glu && ggml_cuda_mmb_supported_mmid(t->src[0], t->src[1], t->src[2], t)) continue; ok = false; } - if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, glu)) ggml_cuda_mmb_mark_bf16_only(glu); + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, glu)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, glu); } } @@ -6340,7 +6350,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph #endif // the marks for this split are complete here; the fingerprint must be recorded before the early returns below - g_gt_graph_sigs.push_back(ggml_cuda_graph_sig(cgraph)); + cuda_ctx->mmb_graph_sigs.push_back(ggml_cuda_graph_sig(cgraph)); static bool enable_graph_optimization = [] { const char * env = getenv("GGML_CUDA_GRAPH_OPT"); diff --git a/ggml/src/ggml-cuda/hyperconn.cu b/ggml/src/ggml-cuda/hyperconn.cu index 46b75160aa8e..4bf6528fee83 100644 --- a/ggml/src/ggml-cuda/hyperconn.cu +++ b/ggml/src/ggml-cuda/hyperconn.cu @@ -166,8 +166,8 @@ void ggml_cuda_op_hc_mix_reduce(ggml_backend_cuda_context & ctx, const ggml_cuda float * out = stage ? staged.alloc(n_embd * n_tokens) : (float *) dst->data; const int64_t n_items = n_embd * n_tokens; - const uint16_t * xn16 = ggml_cuda_mmb_cache_lookup(xn); - const uint16_t * g16 = ggml_cuda_mmb_cache_lookup(gate); + const uint16_t * xn16 = ggml_cuda_mmb_cache_lookup(ctx, xn); + const uint16_t * g16 = ggml_cuda_mmb_cache_lookup(ctx, gate); if (xn16 && g16) { const int threads = 256; const int blocks = (int) ((n_items + threads - 1) / threads); const ggml_cuda_kernel_launch_params launch_params(blocks, threads, 0, ctx.stream()); diff --git a/ggml/src/ggml-cuda/mmb.cu b/ggml/src/ggml-cuda/mmb.cu index 4fc7e7440971..082eb1d75178 100644 --- a/ggml/src/ggml-cuda/mmb.cu +++ b/ggml/src/ggml-cuda/mmb.cu @@ -596,23 +596,50 @@ __global__ void mmb_build_desc2(const int32_t * __restrict__ bounds, uint32_t * for (int jt = 0; jt < ts; ++jt) { const int idx = bs + jt; if (idx < nsmall_max) desc_small[idx] = (uint32_t)e | ((uint32_t)jt << 16); } } +} // namespace + struct mmb_cache_entry { const ggml_tensor * root; const void * data; size_t n; ggml_cuda_pool_alloc<uint16_t> * buf; }; -static std::vector<mmb_cache_entry> g_mmb_cache; -static mmb_cache_entry g_mmb_slots[4] = {{nullptr,nullptr,0,nullptr},{nullptr,nullptr,0,nullptr},{nullptr,nullptr,0,nullptr},{nullptr,nullptr,0,nullptr}}; -static std::unordered_set<const ggml_tensor *> g_mmb_bf16_only; +struct ggml_cuda_mmb_context { + struct workspace { + std::vector<mmb_cache_entry> cache; + mmb_cache_entry slots[4] = {}; + size_t slot_cap[4] = {}; + }; + workspace streams[GGML_CUDA_MAX_STREAMS]; + std::unordered_set<const ggml_tensor *> bf16_only; + std::unordered_map<const void *, uint16_t *> shadow; + std::map<std::pair<const void *, const void *>, uint16_t *> shadow_pair; + size_t shadow_bytes = 0; +}; + +namespace { + +static ggml_cuda_mmb_context & mmb_state(ggml_backend_cuda_context & ctx) { + if (!ctx.mmb) { + ctx.mmb = new ggml_cuda_mmb_context; + } + return *ctx.mmb; +} + +static ggml_cuda_mmb_context::workspace & mmb_workspace(ggml_backend_cuda_context & ctx) { + return mmb_state(ctx).streams[ctx.curr_stream_no]; +} static size_t mmb_cache_max() { return 4; } static const ggml_tensor * mmb_root(const ggml_tensor * t) { return t->view_src ? t->view_src : t; } static uint16_t * mmb_cache_insert(ggml_backend_cuda_context & ctx, const ggml_tensor * t, const size_t n) { - if (g_mmb_cache.size() >= mmb_cache_max()) { delete g_mmb_cache.front().buf; g_mmb_cache.erase(g_mmb_cache.begin()); } + if (mmb_workspace(ctx).cache.size() >= mmb_cache_max()) { delete mmb_workspace(ctx).cache.front().buf; mmb_workspace(ctx).cache.erase(mmb_workspace(ctx).cache.begin()); } auto * buf = new ggml_cuda_pool_alloc<uint16_t>(ctx.pool(), n); - g_mmb_cache.push_back({mmb_root(t), t->data, n, buf}); + mmb_workspace(ctx).cache.push_back({mmb_root(t), t->data, n, buf}); return buf->get(); } static const uint16_t * mmb_bf16_activation(ggml_backend_cuda_context & ctx, const ggml_tensor * src1, const size_t n, cudaStream_t stream) { const ggml_tensor * root = mmb_root(src1); - for (auto & e : g_mmb_slots) if (e.buf && e.root == root && e.data == src1->data && e.n == n) return e.buf->get(); - for (auto & e : g_mmb_cache) if (e.root == root && e.data == src1->data && e.n == n) return e.buf->get(); + // Slot producers have graph dependencies; temporary conversions belong to their stream. + for (auto & work : mmb_state(ctx).streams) { + for (auto & e : work.slots) if (e.buf && e.root == root && e.data == src1->data && e.n == n) return e.buf->get(); + } + for (auto & e : mmb_workspace(ctx).cache) if (e.root == root && e.data == src1->data && e.n == n) return e.buf->get(); uint16_t * buf = mmb_cache_insert(ctx, src1, n); mmb_cvt_f32_bf16<<<(unsigned)((n / 8 + 255) / 256), 256, 0, stream>>>((const float *) src1->data, buf, n); return buf; @@ -661,9 +688,6 @@ __global__ void mmb_dq_iq4nl_bf16_kernel(const uint8_t * __restrict__ W, uint16_ o[2*w] = mmb_pack2(l0, l1); o[2*w + 1] = mmb_pack2(l2, l3); o[8 + 2*w] = mmb_pack2(h0, h1); o[8 + 2*w + 1] = mmb_pack2(h2, h3); } } -static std::unordered_map<const void *, uint16_t *> g_mmb_shadow; -static std::map<std::pair<const void *, const void *>, uint16_t *> g_mmb_shadow_pair; // concat(w0, w1) along rows -> BF16 copy -static size_t g_mmb_shadow_bytes = 0; int mmb_shadow_mode(){ return 2; } bool mmb_shadow() { return mmb_shadow_mode() != 0; } bool mmb_shadow_q6k(){ return mmb_shadow_mode() >= 1; } @@ -674,9 +698,9 @@ static bool mmb_is_row_concat(const ggml_tensor * w) { return w && w->op == GGML_OP_CONCAT && w->type == GGML_TYPE_IQ4_NL && ggml_get_op_params_i32(w, 0) == 1 && mmb_is_resident_iq4(w->src[0]) && mmb_is_resident_iq4(w->src[1]) && w->src[0]->ne[0] == w->src[1]->ne[0] && w->ne[0] == w->src[0]->ne[0] && w->ne[1] == w->src[0]->ne[1] + w->src[1]->ne[1]; } -static const uint16_t * mmb_shadow_lookup(const ggml_tensor * w) { - if (w->op == GGML_OP_CONCAT) { auto it = g_mmb_shadow_pair.find({w->src[0]->data, w->src[1]->data}); return it == g_mmb_shadow_pair.end() ? nullptr : it->second; } - auto it = g_mmb_shadow.find(w->data); return it == g_mmb_shadow.end() ? nullptr : it->second; +static const uint16_t * mmb_shadow_lookup(ggml_backend_cuda_context & ctx, const ggml_tensor * w) { + if (w->op == GGML_OP_CONCAT) { auto it = mmb_state(ctx).shadow_pair.find({w->src[0]->data, w->src[1]->data}); return it == mmb_state(ctx).shadow_pair.end() ? nullptr : it->second; } + auto it = mmb_state(ctx).shadow.find(w->data); return it == mmb_state(ctx).shadow.end() ? nullptr : it->second; } // the BF16 WMMA kernels are RDNA3.5 (gfx1151) work: other devices keep the MMQ/MMVQ paths @@ -697,36 +721,43 @@ bool mmb_glu() { return true; } } // namespace -const uint16_t * ggml_cuda_mmb_cache_lookup(const ggml_tensor * t) { +const uint16_t * ggml_cuda_mmb_cache_lookup(ggml_backend_cuda_context & ctx, const ggml_tensor * t) { const ggml_tensor * root = mmb_root(t); - for (auto & e : g_mmb_slots) if (e.buf && e.root == root && e.data == t->data) return e.buf->get(); - for (auto & e : g_mmb_cache) if (e.root == root && e.data == t->data) return e.buf->get(); + for (auto & work : mmb_state(ctx).streams) { + for (auto & e : work.slots) if (e.buf && e.root == root && e.data == t->data) return e.buf->get(); + } + for (auto & e : mmb_workspace(ctx).cache) if (e.root == root && e.data == t->data) return e.buf->get(); return nullptr; } -static size_t g_mmb_slot_cap[4] = {0, 0, 0}; uint16_t * ggml_cuda_mmb_slot_reserve(ggml_backend_cuda_context & ctx, int slot, const ggml_tensor * t, size_t n) { - mmb_cache_entry & e = g_mmb_slots[slot]; - if (e.buf && g_mmb_slot_cap[slot] < n) { delete e.buf; e.buf = nullptr; } - if (!e.buf) { e.buf = new ggml_cuda_pool_alloc<uint16_t>(ctx.pool(), n); g_mmb_slot_cap[slot] = n; } + mmb_cache_entry & e = mmb_workspace(ctx).slots[slot]; + if (e.buf && mmb_workspace(ctx).slot_cap[slot] < n) { delete e.buf; e.buf = nullptr; } + if (!e.buf) { e.buf = new ggml_cuda_pool_alloc<uint16_t>(ctx.pool(), n); mmb_workspace(ctx).slot_cap[slot] = n; } e.root = mmb_root(t); e.data = t->data; e.n = n; return e.buf->get(); } -void ggml_cuda_mmb_marks_clear() { g_mmb_bf16_only.clear(); } -size_t ggml_cuda_mmb_marks_count() { return g_mmb_bf16_only.size(); } -void ggml_cuda_mmb_mark_bf16_only(const ggml_tensor * t) { g_mmb_bf16_only.insert(t); } -bool ggml_cuda_mmb_is_bf16_only(const ggml_tensor * t) { return g_mmb_bf16_only.count(t) > 0; } -void ggml_cuda_mmb_begin_graph() { for (auto & e : g_mmb_cache) delete e.buf; g_mmb_cache.clear(); for (auto & e : g_mmb_slots) { e.root = nullptr; e.data = nullptr; e.n = 0; } } -void ggml_cuda_mmb_release_all() { - ggml_cuda_mmb_begin_graph(); - for (int i = 0; i < 4; ++i) { if (g_mmb_slots[i].buf) delete g_mmb_slots[i].buf; g_mmb_slots[i].buf = nullptr; g_mmb_slot_cap[i] = 0; } - // the shadow weights are raw cudaMalloc, keyed by data pointer and held for the life of the - // process. A model has finitely many weights so this never mattered, but a long-lived process - // that sees many distinct tensors (test-backend-ops) keeps every one of them. - for (auto & e : g_mmb_shadow) { if (e.second) CUDA_CHECK(cudaFree(e.second)); } - for (auto & e : g_mmb_shadow_pair) { if (e.second) CUDA_CHECK(cudaFree(e.second)); } - g_mmb_shadow.clear(); - g_mmb_shadow_pair.clear(); - g_mmb_shadow_bytes = 0; +void ggml_cuda_mmb_marks_clear(ggml_backend_cuda_context & ctx) { if (ctx.mmb) ctx.mmb->bf16_only.clear(); } +size_t ggml_cuda_mmb_marks_count(ggml_backend_cuda_context & ctx) { return ctx.mmb ? ctx.mmb->bf16_only.size() : 0; } +void ggml_cuda_mmb_mark_bf16_only(ggml_backend_cuda_context & ctx, const ggml_tensor * t) { mmb_state(ctx).bf16_only.insert(t); } +bool ggml_cuda_mmb_is_bf16_only(ggml_backend_cuda_context & ctx, const ggml_tensor * t) { return ctx.mmb && ctx.mmb->bf16_only.count(t) > 0; } +void ggml_cuda_mmb_begin_graph(ggml_backend_cuda_context & ctx) { + if (!ctx.mmb) return; + for (auto & work : mmb_state(ctx).streams) { + for (auto & e : work.cache) delete e.buf; + work.cache.clear(); + for (auto & e : work.slots) { e.root = nullptr; e.data = nullptr; e.n = 0; } + } +} +void ggml_cuda_mmb_release_all(ggml_backend_cuda_context & ctx) { + if (!ctx.mmb) return; + ggml_cuda_mmb_begin_graph(ctx); + for (auto & work : ctx.mmb->streams) { + for (auto & e : work.slots) delete e.buf; + } + for (auto & e : ctx.mmb->shadow) CUDA_CHECK(cudaFree(e.second)); + for (auto & e : ctx.mmb->shadow_pair) CUDA_CHECK(cudaFree(e.second)); + delete ctx.mmb; + ctx.mmb = nullptr; } uint16_t * ggml_cuda_mmb_cache_reserve(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n) { if (!mmb_enabled() || ggml_nrows(t) < mmb_min_t()) return nullptr; @@ -785,11 +816,11 @@ void ggml_cuda_mul_mat_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * CUDA_CHECK(cudaGetLastError()); return; } - const uint16_t * shadow_pre = ((src0->type == GGML_TYPE_IQ4_NL && mmb_shadow()) || src0->type == GGML_TYPE_Q6_K) ? mmb_shadow_lookup(src0) : nullptr; + const uint16_t * shadow_pre = ((src0->type == GGML_TYPE_IQ4_NL && mmb_shadow()) || src0->type == GGML_TYPE_Q6_K) ? mmb_shadow_lookup(ctx, src0) : nullptr; const bool big = (M >= 6144 && K >= 2560) || (shadow_pre && K >= 2560 && T >= 4096); uint16_t * Dh = (mmb_hc16() && K == 320 && M == 10240) ? ggml_cuda_mmb_slot_reserve(ctx, 1, dst, (size_t) T * M) : nullptr; - bool store_f32 = !(Dh && ggml_cuda_mmb_is_bf16_only(dst)); - if (ggml_cuda_mmb_blk16() && !Dh && ggml_cuda_mmb_is_bf16_only(dst) && (M & 7) == 0) { + bool store_f32 = !(Dh && ggml_cuda_mmb_is_bf16_only(ctx, dst)); + if (ggml_cuda_mmb_blk16() && !Dh && ggml_cuda_mmb_is_bf16_only(ctx, dst) && (M & 7) == 0) { Dh = (uint16_t *) dst->data; store_f32 = false; } dim3 grid((M + 127) / 128, big ? (T + 255) / 256 : (T + 127) / 128); @@ -827,12 +858,12 @@ bool ggml_cuda_hc_gate_mix(ggml_backend_cuda_context & ctx, const ggml_tensor * const int K = (int) w->ne[0], M = (int) w->ne[1], E = (int) dst->ne[0]; const int T = (int) ggml_nrows(dst); if (K % ggml_blck_size(w->type) != 0) return false; if (K % MMB_BK != 0 || M != hc * E || E % 32 != 0 || lo->ne[0] != K || ggml_nrows(lo) != T || xn->ne[0] != M || ggml_nrows(xn) != T || T < mmb_min_t()) return false; - const uint16_t * xn16 = ggml_cuda_mmb_cache_lookup(xn); + const uint16_t * xn16 = ggml_cuda_mmb_cache_lookup(ctx, xn); if (!xn16) return false; cudaStream_t stream = ctx.stream(); const uint16_t * lo16 = mmb_bf16_activation(ctx, lo, (size_t) T * K, stream); uint16_t * outh = ggml_cuda_mmb_slot_reserve(ctx, 3, dst, (size_t) T * E); - const bool store_f32 = !(outh && ggml_cuda_mmb_is_bf16_only(dst)); + const bool store_f32 = !(outh && ggml_cuda_mmb_is_bf16_only(ctx, dst)); dim3 grid(E / 32, (T + 127) / 128); mmb_dispatch_quant(w->type, [&](auto tag) { constexpr int WT = decltype(tag)::value; @@ -869,7 +900,7 @@ void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor mmb_build_desc2<<<1, 1024, 0, stream>>>(bounds.get(), desc_big.get(), desc_small.get(), E, nbig_max, nsmall_max, BN, BN_SMALL, THRESH); const uint8_t * W = (const uint8_t *) src0->data; float * D = (float *) dst->data; const size_t eb = (size_t) src0->nb[2]; - uint16_t * Dh = (mmb_down16_flag() && ggml_cuda_mmb_is_bf16_only(dst)) ? (uint16_t *) dst->data : nullptr; + uint16_t * Dh = (mmb_down16_flag() && ggml_cuda_mmb_is_bf16_only(ctx, dst)) ? (uint16_t *) dst->data : nullptr; const bool store_f32 = Dh == nullptr; dim3 gbig((M + 127) / 128, nbig_max), gsmall((M + 127) / 128, nsmall_max); mmb_dispatch_quant(src0->type, [&](auto tag) { @@ -916,7 +947,7 @@ void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_te ggml_cuda_pool_alloc<uint32_t> desc_small(ctx.pool(), nsmall_max); mmb_build_desc2<<<1, 1024, 0, stream>>>(bounds.get(), desc_big.get(), desc_small.get(), E, nbig_max, nsmall_max, BN, BN_SMALL, THRESH); uint16_t * Dh = ggml_cuda_mmb_slot_reserve(ctx, 2, glu, (size_t) n_rows * M); - const bool store_f32 = !ggml_cuda_mmb_is_bf16_only(glu); + const bool store_f32 = !ggml_cuda_mmb_is_bf16_only(ctx, glu); const uint8_t * Wg = (const uint8_t *) gw->data, * Wu = (const uint8_t *) uw->data; float * D = (float *) glu->data; const size_t eb = (size_t) gw->nb[2]; dim3 gbig((M + 63) / 64, nbig_max), gsmall((M + 63) / 64, nsmall_max); mmb_dispatch_quant(gw->type, [&](auto tag) { @@ -931,34 +962,34 @@ void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_te void ggml_cuda_mmb_shadow_prepare(ggml_backend_cuda_context & ctx, const ggml_tensor * w) { if (!w) return; if (mmb_is_resident_q6k(w)) { - if (!mmb_shadow_q6k() || g_mmb_shadow.count(w->data) > 0) return; + if (!mmb_shadow_q6k() || mmb_state(ctx).shadow.count(w->data) > 0) return; const size_t n = (size_t) w->ne[0] * w->ne[1], bytes = n * 2; - if (g_mmb_shadow_bytes + bytes > mmb_shadow_cap()) { GGML_LOG_INFO("MMB_SHADOW cap reached; %s stays Q6_K\n", w->name); return; } + if (mmb_state(ctx).shadow_bytes + bytes > mmb_shadow_cap()) { GGML_LOG_INFO("MMB_SHADOW cap reached; %s stays Q6_K\n", w->name); return; } uint16_t * buf = nullptr; if (cudaMalloc((void **) &buf, bytes) != cudaSuccess) { GGML_LOG_WARN("MMB_SHADOW alloc failed (%zu bytes)\n", bytes); return; } mmb_dq_q6k_bf16_kernel<<<(unsigned) ((n / 256 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->data, buf, n / 256); CUDA_CHECK(cudaGetLastError()); - g_mmb_shadow[w->data] = buf; g_mmb_shadow_bytes += bytes; + mmb_state(ctx).shadow[w->data] = buf; mmb_state(ctx).shadow_bytes += bytes; return; } if (mmb_shadow_mode() != 1) return; // mode 2: Q6_K only const bool concat = mmb_is_row_concat(w); if (!concat && !mmb_is_resident_iq4(w)) return; - if (concat ? g_mmb_shadow_pair.count({w->src[0]->data, w->src[1]->data}) > 0 : g_mmb_shadow.count(w->data) > 0) return; + if (concat ? mmb_state(ctx).shadow_pair.count({w->src[0]->data, w->src[1]->data}) > 0 : mmb_state(ctx).shadow.count(w->data) > 0) return; const size_t n = (size_t) w->ne[0] * w->ne[1]; const size_t bytes = n * 2; - if (g_mmb_shadow_bytes + bytes > mmb_shadow_cap()) { static bool warned = false; if (!warned) { GGML_LOG_INFO("MMB_SHADOW cap reached at %.1f MB; further weights stay IQ4_NL\n", g_mmb_shadow_bytes / 1048576.0); warned = true; } return; } + if (mmb_state(ctx).shadow_bytes + bytes > mmb_shadow_cap()) { static bool warned = false; if (!warned) { GGML_LOG_INFO("MMB_SHADOW cap reached at %.1f MB; further weights stay IQ4_NL\n", mmb_state(ctx).shadow_bytes / 1048576.0); warned = true; } return; } uint16_t * buf = nullptr; if (cudaMalloc((void **) &buf, bytes) != cudaSuccess) { GGML_LOG_WARN("MMB_SHADOW alloc failed (%zu bytes)\n", bytes); return; } if (concat) { const size_t n0 = (size_t) w->src[0]->ne[0] * w->src[0]->ne[1], n1 = (size_t) w->src[1]->ne[0] * w->src[1]->ne[1]; mmb_dq_iq4nl_bf16_kernel<<<(unsigned) ((n0 / 32 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->src[0]->data, buf, n0 / 32); mmb_dq_iq4nl_bf16_kernel<<<(unsigned) ((n1 / 32 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->src[1]->data, buf + n0, n1 / 32); - g_mmb_shadow_pair[{w->src[0]->data, w->src[1]->data}] = buf; + mmb_state(ctx).shadow_pair[{w->src[0]->data, w->src[1]->data}] = buf; } else { mmb_dq_iq4nl_bf16_kernel<<<(unsigned) ((n / 32 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->data, buf, n / 32); - g_mmb_shadow[w->data] = buf; + mmb_state(ctx).shadow[w->data] = buf; } CUDA_CHECK(cudaGetLastError()); - g_mmb_shadow_bytes += bytes; + mmb_state(ctx).shadow_bytes += bytes; } diff --git a/ggml/src/ggml-cuda/mmb.cuh b/ggml/src/ggml-cuda/mmb.cuh index 431785828cea..0090bae52c14 100644 --- a/ggml/src/ggml-cuda/mmb.cuh +++ b/ggml/src/ggml-cuda/mmb.cuh @@ -5,17 +5,17 @@ bool ggml_cuda_mmb_supported_mm (const ggml_tensor * src0, const ggml_tensor * bool ggml_cuda_mmb_supported_mmid(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst); void ggml_cuda_mul_mat_mmb (ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst); void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst); -void ggml_cuda_mmb_begin_graph(); +void ggml_cuda_mmb_begin_graph(ggml_backend_cuda_context & ctx); // producers that can emit a BF16 copy of an F32 output register it here; returns the BF16 buffer to fill (n elements) uint16_t * ggml_cuda_mmb_cache_reserve(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n); // BF16 copy of tensor t if one is cached for the current graph (consumers may read it instead of the F32 data) -const uint16_t * ggml_cuda_mmb_cache_lookup(const ggml_tensor * t); +const uint16_t * ggml_cuda_mmb_cache_lookup(ggml_backend_cuda_context & ctx, const ggml_tensor * t); // producer slots (pinned until the next producer of the same kind): 0 = HC normalized stream xn, 1 = HC gate uint16_t * ggml_cuda_mmb_slot_reserve(ggml_backend_cuda_context & ctx, int slot, const ggml_tensor * t, size_t n); -void ggml_cuda_mmb_marks_clear(); -size_t ggml_cuda_mmb_marks_count(); -void ggml_cuda_mmb_mark_bf16_only(const ggml_tensor * t); -bool ggml_cuda_mmb_is_bf16_only(const ggml_tensor * t); +void ggml_cuda_mmb_marks_clear(ggml_backend_cuda_context & ctx); +size_t ggml_cuda_mmb_marks_count(ggml_backend_cuda_context & ctx); +void ggml_cuda_mmb_mark_bf16_only(ggml_backend_cuda_context & ctx, const ggml_tensor * t); +bool ggml_cuda_mmb_is_bf16_only(ggml_backend_cuda_context & ctx, const ggml_tensor * t); bool ggml_cuda_mmb_gatemix(); bool ggml_cuda_mmb_down16(); bool ggml_cuda_mmb_res16(); @@ -24,4 +24,4 @@ bool ggml_cuda_hc_gate_mix(ggml_backend_cuda_context & ctx, const ggml_tensor * bool ggml_cuda_mmb_supported_glu(const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * glu); void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * glu); void ggml_cuda_mmb_shadow_prepare(ggml_backend_cuda_context & ctx, const ggml_tensor * w); -void ggml_cuda_mmb_release_all(); +void ggml_cuda_mmb_release_all(ggml_backend_cuda_context & ctx); diff --git a/ggml/src/ggml-cuda/moe-weighted-reduction.cu b/ggml/src/ggml-cuda/moe-weighted-reduction.cu index 017d7688527d..a826c70bfcf6 100644 --- a/ggml/src/ggml-cuda/moe-weighted-reduction.cu +++ b/ggml/src/ggml-cuda/moe-weighted-reduction.cu @@ -179,9 +179,9 @@ void ggml_cuda_op_moe_weighted_reduction(ggml_backend_cuda_context & ctx, cudaStream_t stream = ctx.stream(); const float * mrg = merge ? (const float *) merge->data : nullptr; - if (ggml_cuda_mmb_blk16() && ggml_cuda_mmb_is_bf16_only(dst)) { + if (ggml_cuda_mmb_blk16() && ggml_cuda_mmb_is_bf16_only(ctx, dst)) { GGML_ASSERT(n_embd % 4 == 0); - const bool ein = ggml_cuda_mmb_is_bf16_only(experts); + const bool ein = ggml_cuda_mmb_is_bf16_only(ctx, experts); constexpr int threads = 256; const dim3 blocks(n_tokens, (n_embd / 4 + threads - 1) / threads, 1); if (ein) moe_weighted_reduction_bf16_v4_out<<<blocks, threads, 0, stream>>>((const uint16_t *) experts->data, @@ -194,7 +194,7 @@ void ggml_cuda_op_moe_weighted_reduction(ggml_backend_cuda_context & ctx, return; } GGML_ASSERT(merge == nullptr && "shared-expert merge is only fused on the BF16 output path"); - if (ggml_cuda_mmb_down16() && ggml_cuda_mmb_is_bf16_only(experts)) { + if (ggml_cuda_mmb_down16() && ggml_cuda_mmb_is_bf16_only(ctx, experts)) { GGML_ASSERT(n_embd % 4 == 0 && ((uintptr_t) experts->data % 16) == 0 && ((uintptr_t) dst->data % 16) == 0); constexpr int threads = 256; const dim3 blocks(n_tokens, (n_embd / 4 + threads - 1) / threads, 1); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index a16501e75958..873955d70f79 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -13637,6 +13637,114 @@ static bool run_fa_vec_slice(ggml_backend_t backend, ggml_backend_t backend_cpu, return n_fail == 0; } +// Target and draft contexts must not share mutable MMB scratch, including during graph replay and teardown. +static bool run_mmb_context_test(ggml_backend_dev_t dev, const char * op_names_filter, printer * output_printer) { + if (!op_names_filter_selects(op_names_filter, "MMB_CONTEXT") || + strcmp(ggml_backend_reg_name(ggml_backend_dev_backend_reg(dev)), "ROCm") != 0) { + return true; + } + + struct context { + ggml_backend_ptr backend; + ggml_backend_ptr cpu; + ggml_context_ptr tensors; + ggml_backend_sched_ptr sched; + test_hc_chain test; + ggml_tensor * output = nullptr; + std::vector<float> reference; + + context(ggml_backend_dev_t dev, int tokens) : backend(ggml_backend_dev_init(dev, nullptr)), cpu(ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr)), test(tokens, GGML_TYPE_Q4_0) { + tensors.reset(ggml_init({ggml_tensor_overhead()*128 + ggml_graph_overhead(), nullptr, true})); + GGML_ASSERT(backend && cpu && tensors); + test.gf = ggml_new_graph(tensors.get()); + output = test.build_graph(tensors.get()); + for (ggml_tensor * t = ggml_get_first_tensor(tensors.get()); t; t = ggml_get_next_tensor(tensors.get(), t)) { + if (t->op == GGML_OP_NONE) { + ggml_set_input(t); + ggml_set_output(t); // keep inputs intact across graph replays + } + } + ggml_set_output(output); + ggml_build_forward_expand(test.gf, output); + ggml_backend_t backends[] = {backend.get(), cpu.get()}; + sched.reset(ggml_backend_sched_new(backends, nullptr, 2, GGML_DEFAULT_GRAPH_SIZE, false, true)); + for (ggml_tensor * t = ggml_get_first_tensor(tensors.get()); t; t = ggml_get_next_tensor(tensors.get(), t)) { + ggml_backend_sched_set_tensor_backend(sched.get(), t, backend.get()); + } + GGML_ASSERT(ggml_backend_sched_alloc_graph(sched.get(), test.gf)); + for (ggml_tensor * t = ggml_get_first_tensor(tensors.get()); t; t = ggml_get_next_tensor(tensors.get(), t)) { + if (t->op == GGML_OP_NONE && t->buffer) { + init_tensor_uniform(t); + } + } + reference.resize(ggml_nelements(output)); + } + + bool compute() { + return ggml_backend_sched_graph_compute_async(sched.get(), test.gf) == GGML_STATUS_SUCCESS; + } + + void synchronize() { + ggml_backend_sched_synchronize(sched.get()); + } + + void save() { + synchronize(); + ggml_backend_tensor_get(output, reference.data(), 0, ggml_nbytes(output)); + } + + bool matches() { + synchronize(); + std::vector<float> actual(reference.size()); + ggml_backend_tensor_get(output, actual.data(), 0, ggml_nbytes(output)); + for (size_t i = 0; i < actual.size(); ++i) { + if (!std::isfinite(actual[i]) || memcmp(&actual[i], &reference[i], sizeof(float)) != 0) { + fprintf(stderr, "MMB_CONTEXT tokens=%d element=%zu expected=%g actual=%g\n", test.tokens, i, reference[i], actual[i]); + return false; + } + } + return true; + } + }; + + bool ok = true; + std::string error; + context target(dev, 512); + for (int i = 0; i < 4; ++i) { + ok &= target.compute(); + target.synchronize(); + } + target.save(); + ok &= target.compute() && target.matches(); + { + context draft(dev, 512); + for (int i = 0; i < 4; ++i) { + ok &= draft.compute(); + draft.synchronize(); + } + draft.save(); + ok &= draft.compute() && draft.matches(); + for (int i = 0; i < 16 && ok; ++i) { + ok &= target.compute(); + ok &= draft.compute(); + target.synchronize(); + draft.synchronize(); + if (!target.matches() || !draft.matches()) { + ok = false; + error = "overlapping contexts differ from serialized outputs at iteration " + std::to_string(i); + } + } + } + if (ok) { + ok = target.compute() && target.matches(); + if (!ok) { + error = "destroying the draft context changed the target output"; + } + } + output_printer->print_test_result(test_result(ggml_backend_dev_name(dev), "MMB_CONTEXT", "target=512,draft=512", "test", true, ok, error)); + return ok; +} + static bool test_backend(ggml_backend_t backend, ggml_backend_dev_t dev, test_mode mode, const char * op_names_filter, const char * params_filter, printer * output_printer, const char * test_file_path, int parallel_workers) { auto filter_test_cases = [](std::vector<std::unique_ptr<test_case>> & test_cases, const char * params_filter) { @@ -13776,7 +13884,8 @@ static bool test_backend(ggml_backend_t backend, ggml_backend_dev_t dev, test_mo const bool slice_ok = run_fa_vec_slice(backend, backend_cpu.get(), op_names_filter); - return n_ok == tests_run && slice_ok; + const bool context_ok = run_mmb_context_test(dev, op_names_filter, output_printer); + return n_ok == tests_run && slice_ok && context_ok; } if (mode == MODE_GRAD) { From 27f2476c8f575f73d0faeb6a5acf0e05cabc4c64 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:43:11 +0200 Subject: [PATCH 172/212] qsa: maskless multi-sequence block selection Store one block-start row per sequence plus a row index per query, so each query reads its own sequence's starts. The compact limits layout becomes [n_seq x blocks starts] ++ [n_tps tails] ++ [n_tps row_idx]. With n_seq=1 this reduces to the original single-seq layout and is byte-identical. Relax qsa_scalar_visibility to accept multi-seq ubatches (one seq per token, no 2-D positions). Disable the scorer trim when a ubatch mixes sequences. Update the prefix fast path to write the tracked seq's row. Assisted-by: opencode (qwen3.8-flash-next) --- src/llama-memory-hybrid-idx.cpp | 65 ++++++++++++++++++++++++--------- src/models/qwen4exp.cpp | 33 +++++++++++++---- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index b570bcd1cf50..47f15400f745 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -375,11 +375,18 @@ bool llama_memory_hybrid_idx::qsa_metadata(ggml_tensor * members, ggml_tensor * std::fill(p+a*blocks+complete, p+(a+1)*blocks, 0); } } - std::copy_n(qsa_prefix.block_positions.data(), complete, lim); - std::fill(lim+complete, lim+blocks, INT32_MAX); + const int64_t n_tps = u.n_tokens; + const int64_t n_seq = (bias->ne[0] - 2*n_tps) / blocks; + const int64_t row = qsa_prefix.sequence; + if (n_seq < 1 || row < 0 || row >= n_seq) { return false; } + const int64_t base = blocks * n_seq; + std::fill(lim, lim + base, INT32_MAX); + std::copy_n(qsa_prefix.block_positions.data(), complete, lim + row*blocks); + std::fill(lim + row*blocks + complete, lim + (row+1)*blocks, INT32_MAX); std::fill(tail, tail+3*u.n_tokens, -1); for (uint32_t i=0; i<u.n_tokens; ++i) { - int32_t end = u.pos[i]+1, start = end/4*4; lim[blocks+i] = start; + int32_t end = u.pos[i]+1, start = end/4*4; lim[base+i] = start; + lim[base + n_tps + i] = (int32_t) row; for (int j=0; j<end-start; ++j) { tail[3*i+j] = qsa_prefix.cells[start+j]; } } return true; @@ -500,9 +507,9 @@ void llama_memory_hybrid_idx::set_input_qsa_scan( float * dst_bias = compact ? nullptr : (float *) bias->data; int32_t * limits = compact ? (int32_t *) bias->data : nullptr; if (compact) { - GGML_ASSERT(tail_idxs && blk_bias && n_ns == 1 && ggml_nelements(bias) == n_blocks+n_tps); - for (int64_t i=0;i<n_tokens;++i) { GGML_ASSERT(ubatch->seq_id[i][0] == ubatch->seq_id[0][0]); } + GGML_ASSERT(tail_idxs && blk_bias && n_ns == 1 && (ggml_nelements(bias) - 2*n_tps) % n_blocks == 0); } + const int64_t n_seq = compact ? (ggml_nelements(bias) - 2*n_tps) / n_blocks : 1; int32_t * dst_tail = tail_idxs ? (int32_t *) tail_idxs->data : nullptr; if (tail_idxs) { GGML_ASSERT(blk_bias && r > 1 && ggml_backend_buffer_is_host(tail_idxs->buffer)); @@ -739,9 +746,16 @@ void llama_memory_hybrid_idx::set_input_qsa_scan( } if (compact) { - const llama_seq_id seq = ubatch->seq_id[0][0]; - for (int64_t b=0;b<n_blocks;++b) { - limits[b] = b<n_bid && cells.seq_has((uint32_t)bid_cell[b],seq) ? bid_idx[b] : INT32_MAX; + // one start row per sequence: block b is a complete block of seq s iff its group is a singleton {s} + std::fill(limits, limits + n_blocks*n_seq, INT32_MAX); + for (int64_t b=0;b<n_bid;++b) { + if (cells.seq_count((uint32_t) bid_cell[b]) != 1) { continue; } + for (int64_t row=0;row<n_seq;++row) { + if (cells.seq_has((uint32_t) bid_cell[b], (llama_seq_id) row)) { + limits[row*n_blocks + b] = bid_idx[b]; + break; + } + } } } @@ -795,7 +809,9 @@ void llama_memory_hybrid_idx::set_input_qsa_scan( if (compact) { GGML_ASSERT(tail_start >= 0 && tail_start <= 16777216); - limits[n_blocks+ii] = (int32_t)tail_start; + const int64_t base = n_blocks*n_seq; + limits[base + ii] = (int32_t)tail_start; + limits[base + n_tps + ii] = (int32_t)seq_id; continue; } @@ -975,17 +991,32 @@ bool llama_memory_hybrid_idx_context::qsa_scalar_visibility(const llama_ubatch & !ubatch.n_pos || !ubatch.seq_id || !ubatch.n_seq_id) { return false; } if (ubatch.n_seq_id[0]<1 || !ubatch.seq_id[0]) { return false; } const llama_seq_id seq=ubatch.seq_id[0][0]; + bool single = true; for (uint32_t i=0;i<ubatch.n_tokens;++i) { - if (ubatch.n_seq_id[i]<1 || !ubatch.seq_id[i] || ubatch.seq_id[i][0]!=seq || - ubatch.pos[i]<0 || ubatch.pos[i]>=16777216) { return false; } - for (uint32_t axis=1;axis<ubatch.n_pos;++axis) { - if (ubatch.pos[i+axis*ubatch.n_tokens]!=ubatch.pos[i]) { return false; } + if (ubatch.n_seq_id[i]<1 || !ubatch.seq_id[i] || ubatch.seq_id[i][0]!=seq) { single = false; break; } + } + if (single) { + for (uint32_t i=0;i<ubatch.n_tokens;++i) { + if (ubatch.pos[i]<0 || ubatch.pos[i]>=16777216) { return false; } + for (uint32_t axis=1;axis<ubatch.n_pos;++axis) { + if (ubatch.pos[i+axis*ubatch.n_tokens]!=ubatch.pos[i]) { return false; } + } + } + if (ubatch.is_pos_2d()) { + const auto & cells=mem->get_mem_idx()->get_cells(seq); + for (uint32_t j=0;j<get_idx()->get_n_kv();++j) { + if (!cells.is_empty(j) && cells.seq_has(j,seq) && cells.ext_get(j).is_2d_gt(cells.pos_get(j),cells.pos_get(j))) { return false; } + } } + return true; } - if (ubatch.is_pos_2d()) { - const auto & cells=mem->get_mem_idx()->get_cells(seq); - for (uint32_t j=0;j<get_idx()->get_n_kv();++j) { - if (!cells.is_empty(j) && cells.seq_has(j,seq) && cells.ext_get(j).is_2d_gt(cells.pos_get(j),cells.pos_get(j))) { return false; } + // multi-seq maskless: exactly one sequence per token, and no 2-D (image) positions in the batch + if (ubatch.is_pos_2d()) { return false; } + for (uint32_t i=0;i<ubatch.n_tokens;++i) { + if (ubatch.n_seq_id[i]!=1 || !ubatch.seq_id[i]) { return false; } + if (ubatch.pos[i]<0 || ubatch.pos[i]>=16777216) { return false; } + for (uint32_t axis=1;axis<ubatch.n_pos;++axis) { + if (ubatch.pos[i+axis*ubatch.n_tokens]!=ubatch.pos[i]) { return false; } } } return true; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 899258ab493e..e02fd1693cef 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -832,19 +832,32 @@ static std::vector<int64_t> qwen4exp_score_key_limits(const llama_memory_hybrid_ for (uint32_t i = 1; degenerate_pos && i < ubatch.n_tokens; ++i) { if (ubatch.pos[i] != ubatch.pos[0]) { degenerate_pos = false; } } - if (!compact || ubatch.n_tokens<128 || degenerate_pos || !mctx->qsa_position_prefix(ubatch)) { + // the trim assumes one causal visible range; a ubatch that mixes sequences has several, so leave it unbounded + bool multi_seq = false; + if (ubatch.n_seq_id && ubatch.seq_id && ubatch.n_seq_id[0] > 0) { + const llama_seq_id s0 = ubatch.seq_id[0][0]; + for (uint32_t i = 1; i < ubatch.n_tokens && !multi_seq; ++i) { + if (ubatch.n_seq_id[i] != 1 || ubatch.seq_id[i][0] != s0) { multi_seq = true; } + } + } + if (!compact || multi_seq || ubatch.n_tokens<128 || degenerate_pos || !mctx->qsa_position_prefix(ubatch)) { return {}; } return qsa_prefix_limits(ubatch.pos,ubatch.n_tokens,strip,ratio,blocks,budget); } -// compact visibility: limits = [block start position per block (INT32_MAX when unfinished)] ++ [tail start per -// query]; a block is visible to a query iff its start < the query's tail start. log(step(.)) is 0 or -inf. +// compact visibility: limits = [block start per seq row (n_seq x blocks, INT32_MAX when the block is not a +// complete block of that seq)] ++ [tail start per query] ++ [row index per query]. Each query reads the start +// row of its own sequence, so one graph serves several sequences without a mask. A block is visible to a query +// iff its start < the query's tail start. log(step(.)) is 0 or -inf. With n_seq=1 this is the old single-seq +// layout (row_idx all 0) and produces byte-identical scores. static ggml_tensor * qwen4exp_apply_compact_visibility(ggml_context *ctx,ggml_tensor *score, - ggml_tensor *limits,int64_t blocks,int64_t first,int64_t queries) { + ggml_tensor *limits,int64_t blocks,int64_t n_seq,int64_t n_tps,int64_t first,int64_t queries) { GGML_ASSERT(limits->type==GGML_TYPE_I32 && score->ne[0]<=blocks && score->ne[1]==queries && score->ne[2]==1); - auto *starts=ggml_cast(ctx,ggml_view_1d(ctx,limits,score->ne[0],0),GGML_TYPE_F32); - auto *tails=ggml_cast(ctx,ggml_view_1d(ctx,limits,queries,(blocks+first)*sizeof(int32_t)),GGML_TYPE_F32); + auto *starts2d=ggml_view_2d(ctx,limits,score->ne[0],n_seq,blocks*sizeof(int32_t),0); + auto *row_idx=ggml_view_1d(ctx,limits,queries,(blocks*n_seq+n_tps+first)*sizeof(int32_t)); + auto *starts=ggml_cast(ctx,ggml_get_rows(ctx,starts2d,row_idx),GGML_TYPE_F32); + auto *tails=ggml_cast(ctx,ggml_view_1d(ctx,limits,queries,(blocks*n_seq+first)*sizeof(int32_t)),GGML_TYPE_F32); tails=ggml_reshape_2d(ctx,tails,1,queries); auto *visible=ggml_step(ctx,ggml_sub(ctx,ggml_repeat(ctx,tails,score),starts)); return ggml_add(ctx,score,ggml_log(ctx,visible)); @@ -1081,6 +1094,10 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( GGML_ASSERT(n_tokens % n_stream == 0); const int64_t n_tps = n_tokens/n_stream; + // maskless multi-seq: one block-start row per sequence, row index = seq id, bounded by n_seq_max so every + // runtime seq id lands in range. n_seq=1 (np1) reduces the compact layout to the original single-seq one. + const int64_t n_seq = cparams.n_seq_max; + // only the "which block is visible" half of the bias varies per block // the rest is the visible/not test the attention mask already carries, so upload the per-block half only: 1/ratio of the cells // alibi writes distances instead of a mask and non-causal keeps future cells, so both opt out @@ -1111,7 +1128,7 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( qsa->score_strip = qwen4exp_query_strip(n_tps, n_stream); qsa->score_key_limits = qwen4exp_score_key_limits(mctx_hyb, ubatch, n_blocks, qsa->score_strip, r, hparams.indexer_top_k/r, qsa->compact); - qsa->bias = qsa->compact ? ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_blocks + n_tps) : + qsa->bias = qsa->compact ? ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_blocks*n_seq + 2*n_tps) : ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, blk_bias ? n_blocks : n_kv, n_tps, n_stream); ggml_set_input(qsa->cell_blk); @@ -1242,7 +1259,7 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( // one value per block, so it is cheaper to bias here than after the cells are expanded if (inp->compact) { - score = qwen4exp_apply_compact_visibility(ctx0, score, inp->bias, n_blocks, first, n_query); + score = qwen4exp_apply_compact_visibility(ctx0, score, inp->bias, n_blocks, n_seq, n_tps, first, n_query); } else if (blk_bias) { score = ggml_add(ctx0, score, bias); } From adb3c5043da1c87d5202054b7b95d72f301e5392 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:23:07 +0200 Subject: [PATCH 173/212] llama : fix qsa_n_kv_window crash when n_stream > 1 qsa_n_kv_window looped seq ids over LLAMA_MAX_SEQ and called get_cells(s), but with n_stream > 1 the kv-cache seq_to_stream vector only has n_stream entries, so get_cells(s) tripped GGML_ASSERT(seq_id < seq_to_stream.size()) at sched_reserve. This crashed llama-server at parallel > 1 without --kv-unified on qwen4exp (issue #73). Scan all LLAMA_MAX_SEQ seq ids only when n_stream == 1 (every seq maps to stream 0); otherwise scan n_stream, since seq s maps to stream s. The n_stream == 1 path is unchanged, so np1 output is byte-identical. Assisted-by: opencode (qwen3.8-flash-next) --- src/llama-memory-hybrid-idx.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index 47f15400f745..3b9eca8bd3e3 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -977,7 +977,10 @@ uint32_t llama_memory_hybrid_idx_context::qsa_n_kv_window() const { const uint32_t n_kv = get_idx() ? get_idx()->get_n_kv() : 0; if (!mem || !mem->get_mem_idx()) { return n_kv; } llama_pos pos_max = -1; - for (llama_seq_id s = 0; s < (llama_seq_id) LLAMA_MAX_SEQ; ++s) { + // n_stream == 1 maps every seq id to stream 0, so scan all LLAMA_MAX_SEQ of them; + // otherwise seq s maps to stream s and only n_stream seq ids exist in seq_to_stream + const llama_seq_id n_seq_scan = get_n_stream() == 1 ? (llama_seq_id) LLAMA_MAX_SEQ : (llama_seq_id) get_n_stream(); + for (llama_seq_id s = 0; s < n_seq_scan; ++s) { pos_max = std::max(pos_max, mem->get_mem_idx()->get_cells(s).seq_pos_max(s)); } if (pos_max < 0) { return n_kv; } From c3e9ce9c323d08e9363d5adb12086adb27669a8e Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:27:59 +0200 Subject: [PATCH 174/212] qsa: unify scalar visibility gate for single and multi sequence The first multi-seq cut rejected every 2-D ubatch, but this model uses mrope (n_pos=3), so is_pos_2d() is true for plain text and the gate never passed at np>1 - the maskless path stayed dormant. Mirror the single-seq handling: require one seq id per token, then check each present sequence's cells for 2-D positions that break the scalar test. Maskless now activates at np>1. Assisted-by: opencode (qwen3.8-flash-next) --- src/llama-memory-hybrid-idx.cpp | 36 +++++++++++++-------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index 3b9eca8bd3e3..22175360a3f8 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -993,28 +993,8 @@ bool llama_memory_hybrid_idx_context::qsa_scalar_visibility(const llama_ubatch & if (get_n_stream()!=1 || !get_idx() || !ubatch.token || !ubatch.pos || !ubatch.n_tokens || !ubatch.n_pos || !ubatch.seq_id || !ubatch.n_seq_id) { return false; } if (ubatch.n_seq_id[0]<1 || !ubatch.seq_id[0]) { return false; } - const llama_seq_id seq=ubatch.seq_id[0][0]; - bool single = true; - for (uint32_t i=0;i<ubatch.n_tokens;++i) { - if (ubatch.n_seq_id[i]<1 || !ubatch.seq_id[i] || ubatch.seq_id[i][0]!=seq) { single = false; break; } - } - if (single) { - for (uint32_t i=0;i<ubatch.n_tokens;++i) { - if (ubatch.pos[i]<0 || ubatch.pos[i]>=16777216) { return false; } - for (uint32_t axis=1;axis<ubatch.n_pos;++axis) { - if (ubatch.pos[i+axis*ubatch.n_tokens]!=ubatch.pos[i]) { return false; } - } - } - if (ubatch.is_pos_2d()) { - const auto & cells=mem->get_mem_idx()->get_cells(seq); - for (uint32_t j=0;j<get_idx()->get_n_kv();++j) { - if (!cells.is_empty(j) && cells.seq_has(j,seq) && cells.ext_get(j).is_2d_gt(cells.pos_get(j),cells.pos_get(j))) { return false; } - } - } - return true; - } - // multi-seq maskless: exactly one sequence per token, and no 2-D (image) positions in the batch - if (ubatch.is_pos_2d()) { return false; } + // one sequence per token keeps the per-query row mapping well-defined; a token carrying several seq ids + // (shared prefix) has no single row to read, so it keeps the masked path for (uint32_t i=0;i<ubatch.n_tokens;++i) { if (ubatch.n_seq_id[i]!=1 || !ubatch.seq_id[i]) { return false; } if (ubatch.pos[i]<0 || ubatch.pos[i]>=16777216) { return false; } @@ -1022,6 +1002,18 @@ bool llama_memory_hybrid_idx_context::qsa_scalar_visibility(const llama_ubatch & if (ubatch.pos[i+axis*ubatch.n_tokens]!=ubatch.pos[i]) { return false; } } } + if (ubatch.is_pos_2d()) { + // a 2-D (image) cell whose position exceeds its linear position breaks the scalar test; check every + // sequence present in the ubatch, as the single-seq case did + const int64_t n_kv = get_idx()->get_n_kv(); + for (uint32_t i=0;i<ubatch.n_tokens;++i) { + const llama_seq_id s = ubatch.seq_id[i][0]; + const auto & cells = mem->get_mem_idx()->get_cells(s); + for (int64_t j=0;j<n_kv;++j) { + if (!cells.is_empty(j) && cells.seq_has(j,s) && cells.ext_get(j).is_2d_gt(cells.pos_get(j),cells.pos_get(j))) { return false; } + } + } + } return true; } From d367e2631bd1db5e96ff8044bebe6149af9e4516 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:43:47 +0200 Subject: [PATCH 175/212] qsa: fix compact graph reuse, shared-block visibility, and 2-D scan cost Three fixes to the maskless multi-seq path found in review: - can_reuse still sized the compact bias as n_blocks + n_tokens, so the check never passed and the graph rebuilt on every ubatch. Match the new n_seq_max*n_blocks + 2*n_tps layout. Verified: graphs reused = 447 at np4. - The compact block scan skipped any block whose rep cell carried more than one seq id, hiding shared-prefix (seq_cp) blocks from every sequence. Set the entry for every row where seq_has is true, as the masked path does. Verified: 4/4 concurrent requests honor a shared system prompt. - The 2-D cell check ran once per token instead of once per distinct sequence, O(n_tokens x n_kv) on every mrope ubatch off the tracked prefix. Dedupe seq ids and scan each once. Assisted-by: opencode (kimi-k2.8) --- src/llama-memory-hybrid-idx.cpp | 12 +++++++----- src/llama-memory-hybrid-idx.h | 3 ++- src/models/qwen4exp.cpp | 5 +++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index 22175360a3f8..0c1c9a8df872 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -746,14 +746,13 @@ void llama_memory_hybrid_idx::set_input_qsa_scan( } if (compact) { - // one start row per sequence: block b is a complete block of seq s iff its group is a singleton {s} + // one start row per sequence: block b is visible to seq s iff the group's rep cell carries s, + // so blocks shared by several sequences (seq_cp) stay visible to each of them, as in the masked path std::fill(limits, limits + n_blocks*n_seq, INT32_MAX); for (int64_t b=0;b<n_bid;++b) { - if (cells.seq_count((uint32_t) bid_cell[b]) != 1) { continue; } for (int64_t row=0;row<n_seq;++row) { if (cells.seq_has((uint32_t) bid_cell[b], (llama_seq_id) row)) { limits[row*n_blocks + b] = bid_idx[b]; - break; } } } @@ -1003,11 +1002,14 @@ bool llama_memory_hybrid_idx_context::qsa_scalar_visibility(const llama_ubatch & } } if (ubatch.is_pos_2d()) { - // a 2-D (image) cell whose position exceeds its linear position breaks the scalar test; check every - // sequence present in the ubatch, as the single-seq case did + // a 2-D (image) cell whose position exceeds its linear position breaks the scalar test; scan the cells + // once per distinct sequence in the ubatch, not once per token const int64_t n_kv = get_idx()->get_n_kv(); + bool seen[LLAMA_MAX_SEQ] = { false }; for (uint32_t i=0;i<ubatch.n_tokens;++i) { const llama_seq_id s = ubatch.seq_id[i][0]; + if (s < 0 || s >= LLAMA_MAX_SEQ || seen[s]) { continue; } + seen[s] = true; const auto & cells = mem->get_mem_idx()->get_cells(s); for (int64_t j=0;j<n_kv;++j) { if (!cells.is_empty(j) && cells.seq_has(j,s) && cells.ext_get(j).is_2d_gt(cells.pos_get(j),cells.pos_get(j))) { return false; } diff --git a/src/llama-memory-hybrid-idx.h b/src/llama-memory-hybrid-idx.h index c0e10691d1f6..f424b0f4bbeb 100644 --- a/src/llama-memory-hybrid-idx.h +++ b/src/llama-memory-hybrid-idx.h @@ -105,7 +105,8 @@ class llama_memory_hybrid_idx : public llama_memory_hybrid { bool set_input_qsa_prefix(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; // the per-cell scan over the whole window; with tail_idxs it also writes each query's own partial block - // (r-1 cells, -1 padded) and, for an I32 bias, the compact visibility limits [n_blocks + n_tokens] + // (r-1 cells, -1 padded) and, for an I32 bias, the compact visibility limits + // [n_seq x n_blocks starts] ++ [n_tps tails] ++ [n_tps row idx] void set_input_qsa_scan(ggml_tensor * cell_blk, ggml_tensor * blk_cells, ggml_tensor * blk_pos, ggml_tensor * bias, ggml_tensor * tail_idxs, const llama_ubatch * ubatch, uint32_t ratio, bool blk_bias) const; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index e02fd1693cef..a77c2a1e9f55 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -955,7 +955,8 @@ class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i { res &= cell_blk->ne[1] == n_stream; res &= blk_cells->ne[0] == (int64_t) ratio*n_blocks; res &= blk_pos->ne[0] == 4*n_blocks*n_stream; - res &= bias->ne[0] == (compact ? n_blocks + params.ubatch.n_tokens : (blk_bias ? n_blocks : n_kv)); + const int64_t n_tps = params.ubatch.n_tokens / n_stream; + res &= bias->ne[0] == (compact ? n_blocks * params.cparams.n_seq_max + 2*n_tps : (blk_bias ? n_blocks : n_kv)); res &= compact || bias->ne[1] == params.ubatch.n_tokens/n_stream; // the selection mode and the strip bounds are baked into the graph, so a reused graph must agree on them @@ -982,7 +983,7 @@ class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i { ggml_tensor * blk_cells = nullptr; // I32 [ratio*n_blocks, n_stream] ggml_tensor * blk_pos = nullptr; // I32 [4*n_blocks*n_stream] ggml_tensor * bias = nullptr; // F32 [n_blocks or n_kv, n_tokens/n_stream, n_stream], or the compact - // I32 [n_blocks + n_tokens] visibility limits + // I32 [n_seq_max*n_blocks + 2*n_tps] visibility limits // complete-block selection (see qwen4exp_use_block_selection): the query's own partial block, -1 padded ggml_tensor * tail_idxs = nullptr; // I32 [ratio-1, n_tokens/n_stream, n_stream] From dd601a1427aefb4cda46ff65a6bfe5ab07b97ad9 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Thu, 24 Sep 2026 22:29:15 +0200 Subject: [PATCH 176/212] hip: port gufo's Flash-Next prefill/decode ideas to qwen4exp on gfx1151 - mmvq, quantize, binbcast: replace the volatile float rounding barriers with ggml_cuda_materialize (a register-only asm barrier). The volatile locals reserved a private segment, and once flash_attn_ext_f16<256> (252 B/lane scratch) has run the CP throttles every scratch-enabled dispatch ~3.3x: the Q5_1 expert-down GEMV took 220 us instead of 67 us. Bit-identical results. - hyperconn: for T >= 512 the combine + norm kernel also computes the next mix's inject projection [hc * n_embd -> hc] in F32 with a fixed-order reduction; qwen4exp emits inject right after xn. The skinny F32 GEMM no longer runs and xn becomes BF16-only. - mmb: the F32-weight GEMM (MoE router, SSM alpha/beta) rounds activations to F16, one WMMA product per K step, with a 64-row tile for M <= 64. Perplexity-neutral on Flash-Next (64 wikitext-2 chunks, paired: dNLL -0.0012 +- 0.0008); every F32 weight of these GGUFs is F16-exact. - producers of MMB GEMM activations (gated norm, SwiGLU, gated attention, HC low-rank scale + SiLU) write the BF16 copy themselves instead of a separate conversion launch. - cont_sigmoid_mul: accept qwen4exp's [head_dim, n_head, T] gate view against the [head_dim * n_head, T] attention output. - ple-conv: fuse the dilated PLE convolution with F32 weights too (UD GGUFs), removing four 80 MB strided transposes per prefill. - tests: UD-Q4_K_XL MoE shapes, MUL_MAT_ID_COLD, HC_COMBINE_NORM_INJECT, CONT_SIGMOID_MUL, CONV_STATE_CHAIN f32w, and perf cases. Flash-Next UD-Q4_K_XL, llama-server, same prompts (t/s): pp2048 775 -> 843, pp16384 920 -> 979 (hot 1062 -> 1182), tg 18.7 -> 22.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- ggml/src/ggml-cuda/binbcast.cu | 2 +- ggml/src/ggml-cuda/common.cuh | 18 ++++ ggml/src/ggml-cuda/ggml-cuda.cu | 121 +++++++++++++++++++++-- ggml/src/ggml-cuda/hyperconn.cu | 89 ++++++++++++++++- ggml/src/ggml-cuda/hyperconn.cuh | 2 + ggml/src/ggml-cuda/mmb.cu | 35 ++++--- ggml/src/ggml-cuda/mmb.cuh | 1 + ggml/src/ggml-cuda/mmvq.cu | 10 +- ggml/src/ggml-cuda/norm-gated.cu | 12 ++- ggml/src/ggml-cuda/norm-gated.cuh | 1 + ggml/src/ggml-cuda/ple-conv.cu | 30 ++++-- ggml/src/ggml-cuda/ple-conv.cuh | 4 +- ggml/src/ggml-cuda/quantize.cu | 3 +- ggml/src/ggml-cuda/unary.cu | 45 +++++---- ggml/src/ggml-cuda/unary.cuh | 7 +- src/models/qwen4exp.cpp | 8 +- tests/test-backend-ops.cpp | 155 +++++++++++++++++++++++++++++- 17 files changed, 465 insertions(+), 78 deletions(-) diff --git a/ggml/src/ggml-cuda/binbcast.cu b/ggml/src/ggml-cuda/binbcast.cu index 20dd6682d397..e12e91336105 100644 --- a/ggml/src/ggml-cuda/binbcast.cu +++ b/ggml/src/ggml-cuda/binbcast.cu @@ -720,7 +720,7 @@ static __global__ void shared_mul_add_f32( if (index >= nelements) { return; } - volatile float product = src[index] * gate[index / n_embd]; + const float product = ggml_cuda_materialize(src[index] * gate[index / n_embd]); float value = other[index] + product; dst[index] = value + residual[index]; } diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index 89b1e3b0ade0..02d8e1ca5970 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -725,6 +725,24 @@ static __device__ __forceinline__ uint32_t __hgt2_mask(const half2 a, const half } #endif // (defined(CUDART_VERSION) && CUDART_VERSION < CUDART_HMASK) || defined(GGML_USE_HIP) || (defined(MUSART_VERSION) && MUSART_VERSION < MUSART_HMASK) +// Rounds x to a float in a register before its next use, so the compiler cannot contract it into the following add or multiply. +static __device__ __forceinline__ float ggml_cuda_materialize(float x) { +#if defined(GGML_USE_HIP) + asm volatile("" : "+v"(x)); +#else + volatile float v = x; + x = v; +#endif // defined(GGML_USE_HIP) + return x; +} + +// F32 -> BF16, round to nearest even: the rounding of the MMB activation conversion (mmb_cvt_f32_bf16). +static __device__ __forceinline__ uint16_t ggml_cuda_f32_to_bf16_rne(const float f) { + uint32_t u = __float_as_uint(f); + u += 0x7fffu + ((u >> 16) & 1u); + return (uint16_t) (u >> 16); +} + static __device__ __forceinline__ int ggml_cuda_dp4a(const int a, const int b, int c) { #if defined(GGML_USE_HIP) #if defined(CDNA) || defined(RDNA2) || defined(__gfx906__) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index f4dd212997d6..f3a2bf7d881b 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -3932,6 +3932,29 @@ static int ggml_cuda_match_hc_combine_norm(ggml_cgraph * cgraph, int i, args.b2 = ggml_get_op_params_f32(scale2, 1); memcpy(&args.eps, rms->op_params, sizeof(float)); + int k_inj = -1; + args.w_inject = nullptr; + args.out_inject = nullptr; + if (hc == 4 && n_tok >= 512) { + int k = j + 5; + while (k < cgraph->n_nodes && k < j + 9 && ggml_cuda_is_view_or_noop(cgraph->nodes[k])) { + k++; + } + ggml_tensor * mm = k < cgraph->n_nodes ? cgraph->nodes[k] : nullptr; + const ggml_tensor * wi = mm ? mm->src[0] : nullptr; + const ggml_tensor * xi = mm ? mm->src[1] : nullptr; + if (mm && mm->op == GGML_OP_MUL_MAT && (mm->flags & GGML_TENSOR_FLAG_COMPUTE) && + (xi == mulg || (xi->view_src == mulg && xi->view_offs == 0)) && ggml_is_contiguous(xi) && + wi->op == GGML_OP_NONE && wi->type == GGML_TYPE_F32 && ggml_is_contiguous(wi) && + wi->ne[0] == n_embd * hc && wi->ne[1] == hc && wi->ne[2] == 1 && wi->ne[3] == 1 && + mm->type == GGML_TYPE_F32 && ggml_is_contiguous(mm) && + mm->ne[0] == hc && mm->ne[1] == n_tok && mm->ne[2] == 1 && mm->ne[3] == 1) { + k_inj = k; + args.w_inject = wi; + args.out_inject = mm; + } + } + bool alias_ok = true; if (check_alias) { // every block reads block_out and inject and writes its own stream of both outputs: the outputs @@ -3947,6 +3970,15 @@ static int ggml_cuda_match_hc_combine_norm(ggml_cgraph * cgraph, int i, const bool res_inplace = res->data == add->data && ggml_are_same_layout(res, add); alias_ok = !overlap(add, b) && !overlap(add, inject) && (res_inplace || !overlap(add, res)) && !overlap(mulg, b) && !overlap(mulg, inject) && !overlap(mulg, res) && !overlap(mulg, add); + if (k_inj >= 0) { + const ggml_tensor * mm = args.out_inject; + if (overlap(mm, add) || overlap(mm, mulg) || overlap(mm, res) || overlap(mm, b) || + overlap(mm, inject) || overlap(mm, gamma) || overlap(mm, args.w_inject)) { + k_inj = -1; + args.w_inject = nullptr; + args.out_inject = nullptr; + } + } // The single-block kernel reads aliased inputs before writing either output. if (!alias_ok && n_tok == 1 && hc <= 4) { args.single_block = true; @@ -3957,7 +3989,8 @@ static int ggml_cuda_match_hc_combine_norm(ggml_cgraph * cgraph, int i, int idxs[16]; enum ggml_op ops[16]; int count = 0; - for (int k = i; k <= j + 4; ++k) { + const int last = k_inj >= 0 ? k_inj : j + 4; + for (int k = i; k <= last; ++k) { const ggml_tensor * n = cgraph->nodes[k]; if (ggml_cuda_is_view_or_noop(n) && n->view_src != scale2) { continue; @@ -3966,10 +3999,29 @@ static int ggml_cuda_match_hc_combine_norm(ggml_cgraph * cgraph, int i, ops[count] = n->op; count++; } - const int out_nodes[] = { j + 2, j + 4 }; + const int out_nodes[] = { j + 2, j + 4, k_inj }; if (alias_ok && ggml_cuda_hc_combine_norm_supported(args, warp_size) && - ggml_can_fuse_subgraph_ext(cgraph, idxs, count, ops, out_nodes, 2)) { - return j + 4 - i; + ggml_can_fuse_subgraph_ext(cgraph, idxs, count, ops, out_nodes, k_inj >= 0 ? 3 : 2)) { + return last - i; + } + if (k_inj >= 0) { + args.w_inject = nullptr; + args.out_inject = nullptr; + const int out_nodes2[] = { j + 2, j + 4 }; + count = 0; + for (int k = i; k <= j + 4; ++k) { + const ggml_tensor * n = cgraph->nodes[k]; + if (ggml_cuda_is_view_or_noop(n) && n->view_src != scale2) { + continue; + } + idxs[count] = k; + ops[count] = n->op; + count++; + } + if (alias_ok && ggml_cuda_hc_combine_norm_supported(args, warp_size) && + ggml_can_fuse_subgraph_ext(cgraph, idxs, count, ops, out_nodes2, 2)) { + return j + 4 - i; + } } } } @@ -4168,6 +4220,30 @@ static bool ggml_cuda_mmb_claims(const ggml_tensor * t) { return t->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t); } +// ggml_cuda_try_fuse: node i was computed on its own and no later node is skipped +static constexpr int GGML_CUDA_FUSED_SELF = -1; + +// Returns a BF16 cache entry for the producer of t to fill when the next node that computes anything is an MMB GEMM reading t, else nullptr. +static uint16_t * ggml_cuda_bf16_for_mmb_reader(ggml_backend_cuda_context & ctx, const ggml_cgraph * cgraph, int i_last, + const ggml_tensor * t) { + if (t->type != GGML_TYPE_F32 || !ggml_is_contiguous(t)) { + return nullptr; + } + for (int k = i_last + 1; k < cgraph->n_nodes && k <= i_last + 4; ++k) { + ggml_tensor * n = cgraph->nodes[k]; + if (ggml_cuda_is_view_or_noop(n)) { + continue; + } + const ggml_tensor * x = n->src[1]; + if (n->op == GGML_OP_MUL_MAT && x && (x == t || (x->view_src == t && x->view_offs == 0)) && ggml_is_contiguous(x) && + ggml_nelements(x) == ggml_nelements(t) && ggml_cuda_mmb_supported_mm(n->src[0], x, n)) { + return ggml_cuda_mmb_cache_produce(ctx, x, (size_t) ggml_nelements(x)); + } + return nullptr; + } + return nullptr; +} + static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph * cgraph, int i) { static bool disable_fusion = getenv("GGML_CUDA_DISABLE_FUSION") != nullptr && std::atoi(getenv("GGML_CUDA_DISABLE_FUSION")); @@ -4474,6 +4550,15 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } } + if (node->op == GGML_OP_GLU && ggml_get_glu_op(node) == GGML_GLU_OP_SWIGLU && node->type == GGML_TYPE_F32 && + ggml_is_contiguous(node) && !ggml_cuda_mmb_is_bf16_only(*cuda_ctx, node)) { + uint16_t * dst16 = ggml_cuda_bf16_for_mmb_reader(*cuda_ctx, cgraph, i, node); + if (dst16) { + ggml_cuda_op_swiglu(*cuda_ctx, node, dst16); + return GGML_CUDA_FUSED_SELF; + } + } + //topk-moe if (cgraph->nodes[i]->op == GGML_OP_UNARY || cgraph->nodes[i]->op == GGML_OP_SOFT_MAX || cgraph->nodes[i]->op == GGML_OP_ARGSORT) { @@ -5398,6 +5483,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph if (match.pre >= 0 && !ggml_cuda_compute_forward(*cuda_ctx, cgraph->nodes[match.pre])) { GGML_ABORT("norm-gated: gate MUL_MAT dispatch failed"); } + match.dst16 = ggml_cuda_bf16_for_mmb_reader(*cuda_ctx, cgraph, i + skip, match.dst); ggml_cuda_op_norm_gated(*cuda_ctx, match); } return skip; @@ -5484,6 +5570,14 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph // HC16: a BF16 copy of xn for its consumers, and BF16 residual / block_out streams where every reader agreed args.out_xn_bf16 = ggml_cuda_mmb_cache_reserve(*cuda_ctx, args.out_xn, (size_t) ggml_nelements(args.out_xn)); args.store_xn_f32 = !(args.out_xn_bf16 && ggml_cuda_mmb_is_bf16_only(*cuda_ctx, args.out_xn)); + if (!args.store_xn_f32 && !args.out_inject) { + ggml_cuda_hc_combine_norm_args sa; + ggml_cuda_match_hc_combine_norm(cgraph, i, sa, ggml_cuda_info().devices[cuda_ctx->device].warp_size, false); + if (sa.out_inject) { + GGML_ABORT("hc_combine_norm: %s is BF16-only but the fused inject projection was refused (aliasing); " + "the graph_optimize allocation dependencies should have prevented it", args.out_xn->name); + } + } if (ggml_cuda_mmb_res16()) { const ggml_tensor * rin = args.residual->view_src ? args.residual->view_src : args.residual; const ggml_tensor * rout = args.out_res->view_src ? args.out_res->view_src : args.out_res; @@ -5608,10 +5702,11 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph (mul->src[0] == unary || mul->src[1] == unary) && gate->type == GGML_TYPE_F32 && other->type == GGML_TYPE_F32 && gate->nb[0] == sizeof(float) && gate->nb[1] == 2 * gate->ne[0] * sizeof(float) && gate->nb[2] == gate->nb[1] * gate->ne[1] && gate->nb[3] == gate->nb[2] * gate->ne[2] && - ggml_is_contiguous(other) && ggml_are_same_shape(gate, other) && + ggml_is_contiguous(other) && ggml_are_same_shape(node, other) && ggml_are_same_shape(unary, node) && + ggml_are_same_shape(mul, other) && ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_CONT, GGML_OP_UNARY, GGML_OP_MUL }, { i + 2 }) && ggml_cuda_check_fusion_memory_ranges(cgraph, i, 3, out_nodes, 1)) { - ggml_cuda_op_cont_sigmoid_mul(*cuda_ctx, node, unary, mul); + ggml_cuda_op_cont_sigmoid_mul(*cuda_ctx, node, unary, mul, ggml_cuda_bf16_for_mmb_reader(*cuda_ctx, cgraph, i + 2, mul)); return 2; } } @@ -5657,7 +5752,8 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph cgraph->nodes[i + 1]->type == GGML_TYPE_F32 && node->src[0]->type == GGML_TYPE_F32 && ggml_is_contiguous(node->src[0]) && ggml_is_contiguous(cgraph->nodes[i + 1]) && ggml_can_fuse(cgraph, i, { GGML_OP_SCALE, GGML_OP_UNARY })) { - ggml_cuda_op_scale_unary(*cuda_ctx, node, cgraph->nodes[i + 1]); + ggml_cuda_op_scale_unary(*cuda_ctx, node, cgraph->nodes[i + 1], + ggml_cuda_bf16_for_mmb_reader(*cuda_ctx, cgraph, i + 1, cgraph->nodes[i + 1])); return 1; } @@ -5806,6 +5902,9 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud int nodes_to_skip = ggml_cuda_try_fuse(cuda_ctx, cgraph, i); + if (nodes_to_skip == GGML_CUDA_FUSED_SELF) { + continue; + } if (nodes_to_skip != 0) { #ifdef GGML_CUDA_DEBUG const int last_fused = i + nodes_to_skip; @@ -6068,6 +6167,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph const ggml_tensor * t = cgraph->nodes[n]; if (!reads(t, xn)) continue; ++nread; + if (t == ca.out_inject) continue; if (t->op == GGML_OP_MUL_MAT && t->src[0]->type != GGML_TYPE_F32 && ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) continue; if (t->op == GGML_OP_MUL && n >= 1) { ggml_cuda_hc_mix_args ma; if (ggml_cuda_hc_mix_closed(cgraph, n - 1, ma) > 0 && (ma.xn == t->src[0] || ma.xn == t->src[1]) && (ma.xn == xn || ma.xn->view_src == xn)) continue; } @@ -6244,6 +6344,13 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph // that overlap, and a refused combine whose streams are BF16-only has no fallback (last layer: out_res is // otherwise dead after its own norm, so the allocator would hand its buffer to out_xn) params->add_alloc_dep(params->user_data, args.out_res, args.out_xn); + if (args.out_inject) { + const ggml_tensor * live[] = {args.block_out, args.inject, args.residual, args.out_res, args.out_xn}; + for (const auto * t : live) { + auto * root = const_cast<ggml_tensor *>(t->view_src ? t->view_src : t); + params->add_alloc_dep(params->user_data, root, args.out_inject); + } + } i += skip; } } diff --git a/ggml/src/ggml-cuda/hyperconn.cu b/ggml/src/ggml-cuda/hyperconn.cu index 4bf6528fee83..c19128eb4672 100644 --- a/ggml/src/ggml-cuda/hyperconn.cu +++ b/ggml/src/ggml-cuda/hyperconn.cu @@ -397,21 +397,24 @@ static __global__ void __launch_bounds__(HC_CN_BLOCK, 1) hc_combine_norm_single_ } #define HC_CN_BLOCK2 256 +#define HC_INJ_HC 4 // streams (and inject outputs) the fused combine + inject kernel handles __device__ __forceinline__ float hc_lo(const uint32_t u) { return __uint_as_float(u << 16); } __device__ __forceinline__ float hc_hi(const uint32_t u) { return __uint_as_float(u & 0xffff0000u); } __device__ __forceinline__ uint32_t hc_pack2(const float a, const float b) { return (uint32_t) hc_f2bf32(a) | ((uint32_t) hc_f2bf32(b) << 16); } -// two elements per thread per iteration, packed 32-bit accesses; optional BF16 residual / block_out in and BF16 outputs -static __global__ void __launch_bounds__(HC_CN_BLOCK2, 4) hc_combine_norm_f32_b256( +// One (token, stream) row of the combine + norm; with INJECT it also accumulates the row's share of the next mix's inject projection. +template <bool INJECT> +static __device__ __forceinline__ void hc_combine_norm_row_b256( + const int c, const int t, const int hc, float * s_sum, const float * inject, const float * residual, const float * block_out, const float * gamma, float * out_res, float * out_xn, uint16_t * out_xn_bf16, const bool store_xn_f32, const uint16_t * res_in_bf16, uint16_t * res_out_bf16, const uint16_t * blk_in_bf16, - const int n_embd, const float s1, const float b1, const float s2, const float b2, const float eps) { - __shared__ float s_sum[32]; - const int c = blockIdx.x, t = blockIdx.y, hc = gridDim.x, tid = threadIdx.x; + const int n_embd, const float s1, const float b1, const float s2, const float b2, const float eps, + const float * w_inj, float * inj) { + const int tid = threadIdx.x; const float x1 = s1 * inject[(int64_t) t * hc + c] + b1; const float x2 = hc_sigmoid(x1); const float w = s2 * x2 + b2; @@ -464,14 +467,76 @@ static __global__ void __launch_bounds__(HC_CN_BLOCK2, 4) hc_combine_norm_f32_b2 const float v0 = scale * xs[2 * k] * gv.x, v1 = scale * xs[2 * k + 1] * gv.y; if (store_xn_f32) *(float2 *)(xn + col) = make_float2(v0, v1); if (xh) *(uint32_t *)(xh + col) = hc_pack2(v0, v1); + if constexpr (INJECT) { +#pragma unroll + for (int r = 0; r < HC_INJ_HC; ++r) { + const float2 wv = *(const float2 *)(w_inj + (int64_t) r * hc * n_embd + (int64_t) c * n_embd + col); + inj[r] += wv.x * v0 + wv.y * v1; + } + } } else if (col < n_embd) { const float v0 = scale * xs[2 * k] * g[col]; if (store_xn_f32) xn[col] = v0; if (xh) xh[col] = hc_f2bf32(v0); + if constexpr (INJECT) { +#pragma unroll + for (int r = 0; r < HC_INJ_HC; ++r) { + inj[r] += w_inj[(int64_t) r * hc * n_embd + (int64_t) c * n_embd + col] * v0; + } + } } } } +static __global__ void __launch_bounds__(HC_CN_BLOCK2, 4) hc_combine_norm_f32_b256( + const float * inject, const float * residual, + const float * block_out, const float * gamma, + float * out_res, float * out_xn, uint16_t * out_xn_bf16, const bool store_xn_f32, + const uint16_t * res_in_bf16, uint16_t * res_out_bf16, const uint16_t * blk_in_bf16, + const int n_embd, const float s1, const float b1, const float s2, const float b2, const float eps) { + __shared__ float s_sum[32]; + hc_combine_norm_row_b256<false>(blockIdx.x, blockIdx.y, gridDim.x, s_sum, inject, residual, block_out, gamma, + out_res, out_xn, out_xn_bf16, store_xn_f32, res_in_bf16, res_out_bf16, blk_in_bf16, + n_embd, s1, b1, s2, b2, eps, nullptr, nullptr); +} + +// One block per token over all HC_INJ_HC streams: the rows of hc_combine_norm_f32_b256 plus the next mix's inject projection. +static __global__ void __launch_bounds__(HC_CN_BLOCK2, 2) hc_combine_norm_inject_f32_b256( + const float * inject, const float * residual, + const float * block_out, const float * gamma, + float * out_res, float * out_xn, uint16_t * out_xn_bf16, const bool store_xn_f32, + const uint16_t * res_in_bf16, uint16_t * res_out_bf16, const uint16_t * blk_in_bf16, + const int n_embd, const float s1, const float b1, const float s2, const float b2, const float eps, + const float * w_inj, float * out_inj) { + __shared__ float s_sum[HC_INJ_HC][32]; + __shared__ float s_inj[HC_INJ_HC][HC_CN_BLOCK2 / WARP_SIZE]; + const int t = blockIdx.x; + float inj[HC_INJ_HC] = {}; +#pragma unroll + for (int c = 0; c < HC_INJ_HC; ++c) { + hc_combine_norm_row_b256<true>(c, t, HC_INJ_HC, s_sum[c], inject, residual, block_out, gamma, + out_res, out_xn, out_xn_bf16, store_xn_f32, res_in_bf16, res_out_bf16, blk_in_bf16, + n_embd, s1, b1, s2, b2, eps, w_inj, inj); + } + const int warp = threadIdx.x / WARP_SIZE, lane = threadIdx.x % WARP_SIZE; +#pragma unroll + for (int r = 0; r < HC_INJ_HC; ++r) { + const float v = warp_reduce_sum(inj[r]); + if (lane == 0) { + s_inj[r][warp] = v; + } + } + __syncthreads(); + if (threadIdx.x < HC_INJ_HC) { + float sum = 0.0f; +#pragma unroll + for (int w = 0; w < HC_CN_BLOCK2 / WARP_SIZE; ++w) { + sum += s_inj[threadIdx.x][w]; + } + out_inj[(int64_t) t * HC_INJ_HC + threadIdx.x] = sum; + } +} + bool ggml_cuda_hc_combine_norm_supported(const ggml_cuda_hc_combine_norm_args & a, const int warp_size) { const int64_t n_embd = a.out_res->ne[0]; const int64_t hc = a.out_res->ne[1]; @@ -501,6 +566,20 @@ void ggml_cuda_op_hc_combine_norm(ggml_backend_cuda_context & ctx, const ggml_cu return; } + if (a.out_inject) { + GGML_ASSERT(hc == HC_INJ_HC && a.w_inject && ggml_nelements(a.out_inject) == hc * n_tokens && + a.w_inject->ne[0] == n_embd * hc && a.w_inject->ne[1] == hc); + const ggml_cuda_kernel_launch_params lpi(dim3((int) n_tokens, 1, 1), HC_CN_BLOCK2, 0, ctx.stream()); + ggml_cuda_kernel_launch(hc_combine_norm_inject_f32_b256, lpi, + (const float *) a.inject->data, (const float *) a.residual->data, + (const float *) a.block_out->data, (const float *) a.gamma->data, + (float *) a.out_res->data, (float *) a.out_xn->data, a.out_xn_bf16, a.store_xn_f32, + a.res_in_bf16, a.res_out_bf16, a.blk_in_bf16, + (int) n_embd, a.s1, a.b1, a.s2, a.b2, a.eps, + (const float *) a.w_inject->data, (float *) a.out_inject->data); + return; + } + // 256 threads x 2 packed elements per iteration (the shipped layout); the BF16 options ride on this kernel const ggml_cuda_kernel_launch_params lp2(dim3((int) hc, (int) n_tokens, 1), HC_CN_BLOCK2, 0, ctx.stream()); ggml_cuda_kernel_launch(hc_combine_norm_f32_b256, lp2, diff --git a/ggml/src/ggml-cuda/hyperconn.cuh b/ggml/src/ggml-cuda/hyperconn.cuh index a2de7aab80c7..0d76c04a5f14 100644 --- a/ggml/src/ggml-cuda/hyperconn.cuh +++ b/ggml/src/ggml-cuda/hyperconn.cuh @@ -55,6 +55,8 @@ struct ggml_cuda_hc_combine_norm_args { const uint16_t * res_in_bf16 = nullptr; // `residual` is BF16 in place (marked bf16-only) const uint16_t * blk_in_bf16 = nullptr; uint16_t * res_out_bf16 = nullptr; + const ggml_tensor * w_inject = nullptr; // [hc * n_embd, hc] F32 weight + ggml_tensor * out_inject = nullptr; // [hc, T] F32, the skipped MUL_MAT node }; bool ggml_cuda_hc_combine_norm_supported(const ggml_cuda_hc_combine_norm_args & args, int warp_size); diff --git a/ggml/src/ggml-cuda/mmb.cu b/ggml/src/ggml-cuda/mmb.cu index 082eb1d75178..1a0ae824ae71 100644 --- a/ggml/src/ggml-cuda/mmb.cu +++ b/ggml/src/ggml-cuda/mmb.cu @@ -503,12 +503,11 @@ mmb_routed_glu_kernel(const uint8_t * __restrict__ Wg, const uint8_t * __restric #endif } -// F32 x F32 -> F32 GEMM with F32-equivalent precision on WMMA: each operand is split into F16 hi + F16 lo at tile load and -// the product is accumulated as hi*hi + hi*lo + lo*hi in F32 (lo*lo ~2^-22 relative, dropped). Used for the F32 MoE router. +// F32 x F32 -> F32 GEMM on WMMA: operands split into F16 hi + lo at tile load; TWO: Xhi*Whi + Xlo*Whi, !TWO adds Xhi*Wlo, !XSPLIT keeps only Xhi*Whi. __device__ __forceinline__ void mmb_split2(float x, uint16_t & hi, uint16_t & lo) { hi = __builtin_bit_cast(uint16_t, (_Float16) x); lo = __builtin_bit_cast(uint16_t, (_Float16) (x - (float) __builtin_bit_cast(_Float16, hi))); } -template <int BM, int BN, int WTM, int WTN, bool TWO> +template <int BM, int BN, int WTM, int WTN, bool TWO, bool XSPLIT> __global__ void __launch_bounds__(MMB_NT, 2) mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, float * __restrict__ D, const int M, const int K, const int T) { #if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) @@ -516,7 +515,8 @@ mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, NO_DEVICE_CODE; #else constexpr int BKs = 32, LS = BKs + 8, WAVES_M = BM / WTM, TM = WTM / 16, TN = WTN / 16; - __shared__ __align__(16) uint16_t Ah[BM * LS], Al[BM * LS], Bh[BN * LS], Bl[BN * LS]; + static_assert(WAVES_M * (BN / WTN) * 32 == MMB_NT, "tile must use every wave"); + __shared__ __align__(16) uint16_t Ah[BM * LS], Al[TWO ? 8 : BM * LS], Bh[BN * LS], Bl[XSPLIT ? BN * LS : 8]; const int tid = threadIdx.x, lane = tid & 31, wave = tid >> 5, wm = wave % WAVES_M, wn = wave / WAVES_M; const int m0 = blockIdx.x * BM, t0 = blockIdx.y * BN; v8f acc[TM][TN]; @@ -532,12 +532,12 @@ mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, float4 v = make_float4(0.f,0.f,0.f,0.f); if (m < M) v = *(const float4 *)(W + (size_t) m * K + k0 + c4); uint16_t h[4], l[4]; mmb_split2(v.x,h[0],l[0]); mmb_split2(v.y,h[1],l[1]); mmb_split2(v.z,h[2],l[2]); mmb_split2(v.w,h[3],l[3]); *(uint2 *)(Ah + row * LS + c4) = make_uint2((uint32_t)h[0] | ((uint32_t)h[1] << 16), (uint32_t)h[2] | ((uint32_t)h[3] << 16)); - *(uint2 *)(Al + row * LS + c4) = make_uint2((uint32_t)l[0] | ((uint32_t)l[1] << 16), (uint32_t)l[2] | ((uint32_t)l[3] << 16)); } + if constexpr (!TWO) *(uint2 *)(Al + row * LS + c4) = make_uint2((uint32_t)l[0] | ((uint32_t)l[1] << 16), (uint32_t)l[2] | ((uint32_t)l[3] << 16)); } for (int idx = tid; idx < B_CH; idx += MMB_NT) { const int row = idx >> 3, c4 = (idx & 7) * 4; const int t = t0 + row; float4 v = make_float4(0.f,0.f,0.f,0.f); if (t < T) v = *(const float4 *)(X + (size_t) t * K + k0 + c4); uint16_t h[4], l[4]; mmb_split2(v.x,h[0],l[0]); mmb_split2(v.y,h[1],l[1]); mmb_split2(v.z,h[2],l[2]); mmb_split2(v.w,h[3],l[3]); *(uint2 *)(Bh + row * LS + c4) = make_uint2((uint32_t)h[0] | ((uint32_t)h[1] << 16), (uint32_t)h[2] | ((uint32_t)h[3] << 16)); - *(uint2 *)(Bl + row * LS + c4) = make_uint2((uint32_t)l[0] | ((uint32_t)l[1] << 16), (uint32_t)l[2] | ((uint32_t)l[3] << 16)); } + if constexpr (XSPLIT) *(uint2 *)(Bl + row * LS + c4) = make_uint2((uint32_t)l[0] | ((uint32_t)l[1] << 16), (uint32_t)l[2] | ((uint32_t)l[3] << 16)); } __syncthreads(); const int r = lane & 15; #pragma unroll @@ -546,17 +546,17 @@ mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, #pragma unroll for (int i = 0; i < TM; ++i) { const int off = (wm * WTM + i * 16 + r) * LS + kk; ah[i] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Ah + off), *(const uint4 *)(Ah + off + 8)}); - al[i] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Al + off), *(const uint4 *)(Al + off + 8)}); } + if constexpr (!TWO) al[i] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Al + off), *(const uint4 *)(Al + off + 8)}); } #pragma unroll for (int j = 0; j < TN; ++j) { const int off = (wn * WTN + j * 16 + r) * LS + kk; bh[j] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Bh + off), *(const uint4 *)(Bh + off + 8)}); - bl[j] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Bl + off), *(const uint4 *)(Bl + off + 8)}); } + if constexpr (XSPLIT) bl[j] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Bl + off), *(const uint4 *)(Bl + off + 8)}); } #pragma unroll for (int i = 0; i < TM; ++i) #pragma unroll for (int j = 0; j < TN; ++j) { acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(bh[j], ah[i], acc[i][j]); - acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(bl[j], ah[i], acc[i][j]); + if constexpr (XSPLIT) acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(bl[j], ah[i], acc[i][j]); if constexpr (!TWO) acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(bh[j], al[i], acc[i][j]); } } @@ -759,6 +759,11 @@ void ggml_cuda_mmb_release_all(ggml_backend_cuda_context & ctx) { delete ctx.mmb; ctx.mmb = nullptr; } +// A producer writes the BF16 copy of t itself: the entry is found by the MMB GEMM that reads t next. +uint16_t * ggml_cuda_mmb_cache_produce(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n) { + if (!mmb_enabled()) return nullptr; + return mmb_cache_insert(ctx, t, n); +} uint16_t * ggml_cuda_mmb_cache_reserve(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n) { if (!mmb_enabled() || ggml_nrows(t) < mmb_min_t()) return nullptr; return ggml_cuda_mmb_slot_reserve(ctx, 0, t, n); @@ -801,10 +806,14 @@ void ggml_cuda_mul_mat_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * const int K = (int) src0->ne[0], M = (int) src0->ne[1]; const int T = (int) (src1->ne[1] * src1->ne[2] * src1->ne[3]); if (src0->type == GGML_TYPE_F32) { - dim3 grid((M + 127) / 128, (T + 127) / 128); - static const bool two = true; - if (two) mmb_f32split_kernel<128, 128, 32, 64, true ><<<grid, MMB_NT, 0, stream>>>((const float *) src0->data, (const float *) src1->data, (float *) dst->data, M, K, T); - else mmb_f32split_kernel<128, 128, 32, 64, false><<<grid, MMB_NT, 0, stream>>>((const float *) src0->data, (const float *) src1->data, (float *) dst->data, M, K, T); + const float * W = (const float *) src0->data, * X = (const float *) src1->data; float * D = (float *) dst->data; + if (M <= 64) { + dim3 grid((M + 63) / 64, (T + 127) / 128); + mmb_f32split_kernel<64, 128, 16, 64, true, false><<<grid, MMB_NT, 0, stream>>>(W, X, D, M, K, T); + } else { + dim3 grid((M + 127) / 128, (T + 127) / 128); + mmb_f32split_kernel<128, 128, 32, 64, true, false><<<grid, MMB_NT, 0, stream>>>(W, X, D, M, K, T); + } CUDA_CHECK(cudaGetLastError()); return; } const uint16_t * xhp = mmb_bf16_activation(ctx, src1, (size_t) T * K, stream); diff --git a/ggml/src/ggml-cuda/mmb.cuh b/ggml/src/ggml-cuda/mmb.cuh index 0090bae52c14..b975fe044b6b 100644 --- a/ggml/src/ggml-cuda/mmb.cuh +++ b/ggml/src/ggml-cuda/mmb.cuh @@ -7,6 +7,7 @@ void ggml_cuda_mul_mat_mmb (ggml_backend_cuda_context & ctx, const ggml_tensor void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst); void ggml_cuda_mmb_begin_graph(ggml_backend_cuda_context & ctx); // producers that can emit a BF16 copy of an F32 output register it here; returns the BF16 buffer to fill (n elements) +uint16_t * ggml_cuda_mmb_cache_produce(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n); uint16_t * ggml_cuda_mmb_cache_reserve(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n); // BF16 copy of tensor t if one is cached for the current graph (consumers may read it instead of the F32 data) const uint16_t * ggml_cuda_mmb_cache_lookup(ggml_backend_cuda_context & ctx, const ggml_tensor * t); diff --git a/ggml/src/ggml-cuda/mmvq.cu b/ggml/src/ggml-cuda/mmvq.cu index 5b2f213838f5..101da1f5acee 100644 --- a/ggml/src/ggml-cuda/mmvq.cu +++ b/ggml/src/ggml-cuda/mmvq.cu @@ -814,7 +814,7 @@ static __global__ void mul_mat_vec_q4_columns_rdna3_5( u[2*i+1] = get_int_b4(by->qs, kqs + i + QI5_1); } // ROCm 7.14 changes gfx1151 Q5_1 rounding under four-column register pressure. Keep both paths materialized until LLVM preserves this expression. - volatile float dot = vec_dot_q5_1_q8_1_impl<VDR_Q5_1_Q8_1_MMVQ>(vl, vh, u, dm, by->ds); + const float dot = ggml_cuda_materialize(vec_dot_q5_1_q8_1_impl<VDR_Q5_1_Q8_1_MMVQ>(vl, vh, u, dm, by->ds)); tmp[j] = __fadd_rn(tmp[j], dot); } } else if constexpr (type == GGML_TYPE_Q4_K) { @@ -1423,8 +1423,8 @@ static __global__ void mul_mat_vec_q( } } if constexpr (type == GGML_TYPE_Q5_1 && table_id == MMVQ_PARAMETERS_RDNA3_5) { - volatile float dot = vec_dot_q_cuda( - vx, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs); + const float dot = ggml_cuda_materialize(vec_dot_q_cuda( + vx, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs)); tmp[j][i] = __fadd_rn(tmp[j][i], dot); } else { tmp[j][i] += vec_dot_q_cuda( @@ -1433,8 +1433,8 @@ static __global__ void mul_mat_vec_q( if constexpr (has_fusion) { if (use_gate) { if constexpr (type == GGML_TYPE_Q5_1 && table_id == MMVQ_PARAMETERS_RDNA3_5) { - volatile float dot_gate = vec_dot_q_cuda( - vgate, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs); + const float dot_gate = ggml_cuda_materialize(vec_dot_q_cuda( + vgate, &y[j*stride_col_y + kby], kbx_offset + i*stride_row_x + kbx, kqs)); tmp_gate[j][i] = __fadd_rn(tmp_gate[j][i], dot_gate); } else { tmp_gate[j][i] += vec_dot_q_cuda( diff --git a/ggml/src/ggml-cuda/norm-gated.cu b/ggml/src/ggml-cuda/norm-gated.cu index ff509550e1b0..6e09b7738c2a 100644 --- a/ggml/src/ggml-cuda/norm-gated.cu +++ b/ggml/src/ggml-cuda/norm-gated.cu @@ -10,7 +10,7 @@ static __device__ __forceinline__ float xor_tree(float v) { template <bool GATE> static __global__ void __launch_bounds__(256) rms_rows_f32(const float * x, const float * w, const float * z, float * dst, const int ncols, const uint3 row_div, const uint3 channel_div, const int64_t total_rows, - const int64_t stride_row, const int64_t stride_channel, const int64_t stride_sample, const float eps) { + const int64_t stride_row, const int64_t stride_channel, const int64_t stride_sample, const float eps, uint16_t * dst16) { const int lane = threadIdx.x & 31; const int64_t g = (int64_t) blockIdx.x * 8 + (threadIdx.x >> 5); if (g >= total_rows) return; @@ -22,6 +22,7 @@ static __global__ void __launch_bounds__(256) rms_rows_f32(const float * x, cons } x += sample * stride_sample + channel * stride_channel + row * stride_row; dst += g * ncols; if (GATE) z += g * ncols; + if (dst16) dst16 += g * ncols; float part[8]; #pragma unroll for (int wv = 0; wv < 8; ++wv) { @@ -40,7 +41,10 @@ static __global__ void __launch_bounds__(256) rms_rows_f32(const float * x, cons const int col = 32 * wv + lane; if (col < ncols) { const float t = scale * x[col] * w[col]; - if (GATE) { const float s = 1.0f / (1.0f + expf(-z[col])); dst[col] = t * s; } else { dst[col] = t; } + float v; + if (GATE) { const float s = 1.0f / (1.0f + expf(-z[col])); v = t * s; } else { v = t; } + dst[col] = v; + if (dst16) dst16[col] = ggml_cuda_f32_to_bf16_rne(v); } } } @@ -128,7 +132,7 @@ void ggml_cuda_op_norm_gated(ggml_backend_cuda_context & ctx, const ggml_cuda_no const dim3 grid((unsigned) ((total + 7) / 8)), block(256); const ggml_cuda_kernel_launch_params lp(grid, block, 0, ctx.stream()); if (m.z) ggml_cuda_kernel_launch(rms_rows_f32<true>, lp, (const float *) x->data, (const float *) m.w->data, (const float *) m.z->data, (float *) m.dst->data, - (int) x->ne[0], row_div, channel_div, total, (int64_t) (x->nb[1] / 4), (int64_t) (x->nb[2] / 4), (int64_t) (x->nb[3] / 4), m.eps); + (int) x->ne[0], row_div, channel_div, total, (int64_t) (x->nb[1] / 4), (int64_t) (x->nb[2] / 4), (int64_t) (x->nb[3] / 4), m.eps, m.dst16); else ggml_cuda_kernel_launch(rms_rows_f32<false>, lp, (const float *) x->data, (const float *) m.w->data, (const float *) nullptr, (float *) m.dst->data, - (int) x->ne[0], row_div, channel_div, total, (int64_t) (x->nb[1] / 4), (int64_t) (x->nb[2] / 4), (int64_t) (x->nb[3] / 4), m.eps); + (int) x->ne[0], row_div, channel_div, total, (int64_t) (x->nb[1] / 4), (int64_t) (x->nb[2] / 4), (int64_t) (x->nb[3] / 4), m.eps, m.dst16); } diff --git a/ggml/src/ggml-cuda/norm-gated.cuh b/ggml/src/ggml-cuda/norm-gated.cuh index 5280a9c5581e..b94c5357bea9 100644 --- a/ggml/src/ggml-cuda/norm-gated.cuh +++ b/ggml/src/ggml-cuda/norm-gated.cuh @@ -8,6 +8,7 @@ struct ggml_cuda_norm_gated_match { float eps; int pre = -1; bool staged = false; + uint16_t * dst16 = nullptr; // optional BF16 copy of dst for the MMB GEMM that reads it next }; int ggml_cuda_norm_gated_match_at(const ggml_cgraph * cgraph, int i, ggml_cuda_norm_gated_match & m); void ggml_cuda_op_norm_gated(ggml_backend_cuda_context & ctx, const ggml_cuda_norm_gated_match & m); diff --git a/ggml/src/ggml-cuda/ple-conv.cu b/ggml/src/ggml-cuda/ple-conv.cu index e0c5015ea17b..30aa8b7793b2 100644 --- a/ggml/src/ggml-cuda/ple-conv.cu +++ b/ggml/src/ggml-cuda/ple-conv.cu @@ -23,9 +23,12 @@ static __global__ void ple_concat_tail(const float * __restrict__ state, const f out[(size_t) c * row_stride + j] = (j < H) ? state[c * H + j] : x[(size_t) (j - H) * C + c]; } -template <int K, int DIL, int TT> +static __device__ __forceinline__ float ple_weight(const half w) { return __half2float(w); } +static __device__ __forceinline__ float ple_weight(const float w) { return w; } + +template <int K, int DIL, int TT, typename TW> static __global__ void __launch_bounds__(256) ple_conv_kernel(const float * __restrict__ state, const float * __restrict__ x, - const half * __restrict__ w, float * __restrict__ y, const int C, const int T) { + const TW * __restrict__ w, float * __restrict__ y, const int C, const int T) { constexpr int H = (K - 1) * DIL, WIN = H + 1; const int c = blockIdx.x * 256 + threadIdx.x, t0 = blockIdx.y * TT; if (c >= C) { @@ -34,7 +37,7 @@ static __global__ void __launch_bounds__(256) ple_conv_kernel(const float * __re float wr[K]; #pragma unroll for (int k = 0; k < K; ++k) { - wr[k] = __half2float(w[c * K + k]); + wr[k] = ple_weight(w[c * K + k]); } float win[WIN]; auto ld = [&](int jp) -> float { @@ -75,12 +78,13 @@ static int ple_tap_index(const ggml_tensor * t, const ggml_tensor * cc, int64_t return (k >= 0 && k < K) ? (int) k : -1; } -// the weight operand of a tap MUL: cast(F32) <- reshape <- CONT <- VIEW of column k of the F16 [K, C] weight +// the weight operand of a tap MUL: reshape <- CONT <- VIEW of column k of the [K, C] weight, behind a cast(F32) when it is F16 static const ggml_tensor * ple_weight_root(const ggml_tensor * wk, int64_t C, int k) { - if (wk->type != GGML_TYPE_F32 || ggml_nelements(wk) != C || wk->op != GGML_OP_CPY) { + if (wk->type != GGML_TYPE_F32 || ggml_nelements(wk) != C) { return nullptr; } - const ggml_tensor * src = wk->src[0]; + const bool cast = wk->op == GGML_OP_CPY; + const ggml_tensor * src = cast ? wk->src[0] : wk; while (src && (src->op == GGML_OP_RESHAPE || src->op == GGML_OP_VIEW)) { src = src->src[0]; } @@ -92,8 +96,9 @@ static const ggml_tensor * ple_weight_root(const ggml_tensor * wk, int64_t C, in return nullptr; } const ggml_tensor * W = wv->view_src; - if (W->type != GGML_TYPE_F16 || W->ne[1] != C || W->ne[2] != 1 || W->ne[3] != 1 || !ggml_is_contiguous(W) || - wv->ne[0] != 1 || wv->ne[1] != C || wv->nb[1] != W->nb[1] || wv->view_offs != (size_t) k * sizeof(ggml_fp16_t)) { + if ((cast ? W->type != GGML_TYPE_F16 : W->type != GGML_TYPE_F32) || W->ne[1] != C || W->ne[2] != 1 || W->ne[3] != 1 || + !ggml_is_contiguous(W) || wv->ne[0] != 1 || wv->ne[1] != C || wv->nb[1] != W->nb[1] || + wv->view_offs != (size_t) k * ggml_element_size(W)) { return nullptr; } return W; @@ -293,7 +298,12 @@ void ggml_cuda_ple_conv_direct(ggml_backend_cuda_context & ctx, const ggml_cuda_ constexpr int TT = 128; GGML_ASSERT(m.K == 4 && m.dil == 3); dim3 grid((unsigned) (m.C / 256), (unsigned) ((m.T + TT - 1) / TT)); - ple_conv_kernel<4, 3, TT><<<grid, 256, 0, ctx.stream()>>>((const float *) m.state->data, (const float *) m.x->data, - (const half *) m.w->data, (float *) m.out->data, (int) m.C, (int) m.T); + if (m.w->type == GGML_TYPE_F16) { + ple_conv_kernel<4, 3, TT, half><<<grid, 256, 0, ctx.stream()>>>((const float *) m.state->data, (const float *) m.x->data, + (const half *) m.w->data, (float *) m.out->data, (int) m.C, (int) m.T); + } else { + ple_conv_kernel<4, 3, TT, float><<<grid, 256, 0, ctx.stream()>>>((const float *) m.state->data, (const float *) m.x->data, + (const float *) m.w->data, (float *) m.out->data, (int) m.C, (int) m.T); + } CUDA_CHECK(cudaGetLastError()); } diff --git a/ggml/src/ggml-cuda/ple-conv.cuh b/ggml/src/ggml-cuda/ple-conv.cuh index 26af69b165fe..b5f60ad21cc1 100644 --- a/ggml/src/ggml-cuda/ple-conv.cuh +++ b/ggml/src/ggml-cuda/ple-conv.cuh @@ -1,7 +1,7 @@ #pragma once #include "common.cuh" -// qwen4exp PLE convolution: concat(history, x^T) -> four dilated taps (CONT of transposed views) x F16 weight +// qwen4exp PLE convolution: concat(history, x^T) -> four dilated taps (CONT of transposed views) x F16 or F32 weight // columns -> ADD chain -> SiLU. On a match the concat writes only the columns its tail copies read and one // kernel computes the SiLU output straight from the history and x. struct ggml_cuda_ple_conv_match { @@ -9,7 +9,7 @@ struct ggml_cuda_ple_conv_match { const ggml_tensor * x = nullptr; // [C, T] F32 contiguous const ggml_tensor * state = nullptr; // [H, C] F32 contiguous, H = (K-1)*dil const ggml_tensor * concat = nullptr; // [T+H, C] - const ggml_tensor * w = nullptr; // [K, C] F16 + const ggml_tensor * w = nullptr; // [K, C] F16 or F32 ggml_tensor * out = nullptr; // SiLU output [C, T] int64_t C = 0, T = 0, H = 0, K = 0, dil = 0, tail_from = 0; }; diff --git a/ggml/src/ggml-cuda/quantize.cu b/ggml/src/ggml-cuda/quantize.cu index d4bb2af0e115..8a2112224123 100644 --- a/ggml/src/ggml-cuda/quantize.cu +++ b/ggml/src/ggml-cuda/quantize.cu @@ -572,8 +572,7 @@ static __global__ void quantize_mmq_q8_1_swiglu( #pragma unroll for (int i = 0; i < 4; ++i) { if (i0 + i < ne00) { - volatile float value = ggml_cuda_op_silu_single(gate[gate_base + i0 + i]) * up[up_base + i0 + i]; - values[i] = value; + values[i] = ggml_cuda_materialize(ggml_cuda_op_silu_single(gate[gate_base + i0 + i]) * up[up_base + i0 + i]); } else { values[i] = 0.0f; } diff --git a/ggml/src/ggml-cuda/unary.cu b/ggml/src/ggml-cuda/unary.cu index 1508bb857a89..adc42022701f 100644 --- a/ggml/src/ggml-cuda/unary.cu +++ b/ggml/src/ggml-cuda/unary.cu @@ -260,7 +260,8 @@ void ggml_cuda_op_softplus(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { /* gated ops */ template <float (*op)(float), typename T> -static __global__ void unary_gated_op_kernel(const T * x, const T * g, T * dst, const int64_t k, const int64_t n, const int64_t o0, const int64_t o1) { +static __global__ void unary_gated_op_kernel(const T * x, const T * g, T * dst, const int64_t k, const int64_t n, const int64_t o0, const int64_t o1, + uint16_t * dst16) { ggml_cuda_pdl_lc(); const int64_t i = int64_t(blockDim.x)*blockIdx.x + threadIdx.x; @@ -273,18 +274,23 @@ static __global__ void unary_gated_op_kernel(const T * x, const T * g, T * dst, const int64_t j1 = o0 == o1 ? j0 : (i / n) * o1 + (i % n); ggml_cuda_pdl_sync(); - dst[i] = (T)(op((float)x[j0]) * (float)g[j1]); + const T v = (T)(op((float)x[j0]) * (float)g[j1]); + dst[i] = v; + if (dst16) { + dst16[i] = ggml_cuda_f32_to_bf16_rne((float) v); + } } template <float (*op)(float), typename T> -static void unary_gated_cuda(const T * x, const T * g, T * dst, const int64_t k, const int64_t n, const int64_t o0, const int64_t o1, cudaStream_t stream) { +static void unary_gated_cuda(const T * x, const T * g, T * dst, const int64_t k, const int64_t n, const int64_t o0, const int64_t o1, cudaStream_t stream, + uint16_t * dst16 = nullptr) { const int64_t num_blocks = (k + CUDA_GLU_BLOCK_SIZE - 1) / CUDA_GLU_BLOCK_SIZE; const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params((dim3)num_blocks, CUDA_GLU_BLOCK_SIZE, 0, stream); - ggml_cuda_kernel_launch(unary_gated_op_kernel<op, T>, launch_params, x, g, dst, k, n, o0, o1); + ggml_cuda_kernel_launch(unary_gated_op_kernel<op, T>, launch_params, x, g, dst, k, n, o0, o1, dst16); } template <float (*op)(float)> -void ggml_cuda_op_unary_gated(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { +void ggml_cuda_op_unary_gated(ggml_backend_cuda_context & ctx, ggml_tensor * dst, uint16_t * dst16 = nullptr) { const ggml_tensor * src0 = dst->src[0]; const ggml_tensor * src1 = dst->src[1]; void * src0_d = src0->data; @@ -333,7 +339,7 @@ void ggml_cuda_op_unary_gated(ggml_backend_cuda_context & ctx, ggml_tensor * dst src1_p += swapped ? 0 : nc; } - unary_gated_cuda<op>(src0_p, src1_p, (float *)dst_d, ggml_nelements(dst), nc, src0_o / sizeof(float), src1_o / sizeof(float), stream); + unary_gated_cuda<op>(src0_p, src1_p, (float *)dst_d, ggml_nelements(dst), nc, src0_o / sizeof(float), src1_o / sizeof(float), stream, dst16); } } @@ -345,8 +351,8 @@ void ggml_cuda_op_geglu(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { ggml_cuda_op_unary_gated<op_gelu>(ctx, dst); } -void ggml_cuda_op_swiglu(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { - ggml_cuda_op_unary_gated<op_silu>(ctx, dst); +void ggml_cuda_op_swiglu(ggml_backend_cuda_context & ctx, ggml_tensor * dst, uint16_t * dst16) { + ggml_cuda_op_unary_gated<op_silu>(ctx, dst, dst16); } void ggml_cuda_op_geglu_erf(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { @@ -703,7 +709,7 @@ void ggml_cuda_op_unary_mul(ggml_backend_cuda_context & ctx, ggml_tensor * unary } void ggml_cuda_op_cont_sigmoid_mul( - ggml_backend_cuda_context & ctx, ggml_tensor * cont_node, ggml_tensor * unary_node, ggml_tensor * mul_node) { + ggml_backend_cuda_context & ctx, ggml_tensor * cont_node, ggml_tensor * unary_node, ggml_tensor * mul_node, uint16_t * dst16) { const ggml_tensor * gate = cont_node->src[0]; const ggml_tensor * other = mul_node->src[0] == unary_node ? mul_node->src[1] : mul_node->src[0]; @@ -716,7 +722,7 @@ void ggml_cuda_op_cont_sigmoid_mul( const int64_t k = ggml_nelements(other); const int64_t n = gate->ne[0]; unary_gated_cuda<op_sigmoid>((const float *) gate->data, (const float *) other->data, (float *) mul_node->data, - k, n, gate->nb[1] / sizeof(float), n, ctx.stream()); + k, n, gate->nb[1] / sizeof(float), n, ctx.stream(), dst16); } /* fused relu + sqr */ @@ -740,7 +746,7 @@ void ggml_cuda_op_relu_sqr(ggml_backend_cuda_context & ctx, ggml_tensor * relu_n /* fused scale + unary */ template <float (*op)(float)> -static __global__ void scale_unary_kernel(const float * x, float * dst, const float scale, const float bias, const int k) { +static __global__ void scale_unary_kernel(const float * x, float * dst, const float scale, const float bias, const int k, uint16_t * dst16) { const int i = blockDim.x*blockIdx.x + threadIdx.x; if (i >= k) { @@ -748,17 +754,22 @@ static __global__ void scale_unary_kernel(const float * x, float * dst, const fl } // identical expression to scale_f32 in scale.cu, followed by the unary op - dst[i] = op(scale * x[i] + bias); + const float v = op(scale * x[i] + bias); + dst[i] = v; + if (dst16) { + dst16[i] = ggml_cuda_f32_to_bf16_rne(v); + } } template <float (*op)(float)> -static void scale_unary_cuda(const float * x, float * dst, const float scale, const float bias, const int k, cudaStream_t stream) { +static void scale_unary_cuda(const float * x, float * dst, const float scale, const float bias, const int k, cudaStream_t stream, + uint16_t * dst16) { const int num_blocks = (k + CUDA_NEG_BLOCK_SIZE - 1) / CUDA_NEG_BLOCK_SIZE; const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params((dim3)num_blocks, CUDA_NEG_BLOCK_SIZE, 0, stream); - ggml_cuda_kernel_launch(scale_unary_kernel<op>, launch_params, x, dst, scale, bias, k); + ggml_cuda_kernel_launch(scale_unary_kernel<op>, launch_params, x, dst, scale, bias, k, dst16); } -void ggml_cuda_op_scale_unary(ggml_backend_cuda_context & ctx, ggml_tensor * scale_node, ggml_tensor * unary_node) { +void ggml_cuda_op_scale_unary(ggml_backend_cuda_context & ctx, ggml_tensor * scale_node, ggml_tensor * unary_node, uint16_t * dst16) { const ggml_tensor * src = scale_node->src[0]; GGML_ASSERT(src->type == GGML_TYPE_F32 && scale_node->type == GGML_TYPE_F32 && unary_node->type == GGML_TYPE_F32); @@ -771,10 +782,10 @@ void ggml_cuda_op_scale_unary(ggml_backend_cuda_context & ctx, ggml_tensor * sca switch (ggml_get_unary_op(unary_node)) { case GGML_UNARY_OP_SILU: - scale_unary_cuda<op_silu>((const float *) src->data, (float *) unary_node->data, scale, bias, k, ctx.stream()); + scale_unary_cuda<op_silu>((const float *) src->data, (float *) unary_node->data, scale, bias, k, ctx.stream(), dst16); break; case GGML_UNARY_OP_SIGMOID: - scale_unary_cuda<op_sigmoid>((const float *) src->data, (float *) unary_node->data, scale, bias, k, ctx.stream()); + scale_unary_cuda<op_sigmoid>((const float *) src->data, (float *) unary_node->data, scale, bias, k, ctx.stream(), dst16); break; default: GGML_ABORT("unsupported unary op for fused scale+unary"); diff --git a/ggml/src/ggml-cuda/unary.cuh b/ggml/src/ggml-cuda/unary.cuh index f2a543cffbe2..dddcabc5bed5 100644 --- a/ggml/src/ggml-cuda/unary.cuh +++ b/ggml/src/ggml-cuda/unary.cuh @@ -79,7 +79,8 @@ void ggml_cuda_op_reglu(ggml_backend_cuda_context & ctx, ggml_tensor * dst); void ggml_cuda_op_geglu(ggml_backend_cuda_context & ctx, ggml_tensor * dst); -void ggml_cuda_op_swiglu(ggml_backend_cuda_context & ctx, ggml_tensor * dst); +// dst16: optional BF16 copy of dst (round to nearest even) for an MMB GEMM that reads it next +void ggml_cuda_op_swiglu(ggml_backend_cuda_context & ctx, ggml_tensor * dst, uint16_t * dst16 = nullptr); void ggml_cuda_op_swiglu_oai(ggml_backend_cuda_context & ctx, ggml_tensor * dst); @@ -94,7 +95,7 @@ void ggml_cuda_op_xielu(ggml_backend_cuda_context & ctx, ggml_tensor * dst); void ggml_cuda_op_unary_mul(ggml_backend_cuda_context & ctx, ggml_tensor * unary_node, ggml_tensor * mul_node); void ggml_cuda_op_cont_sigmoid_mul( - ggml_backend_cuda_context & ctx, ggml_tensor * cont_node, ggml_tensor * unary_node, ggml_tensor * mul_node); + ggml_backend_cuda_context & ctx, ggml_tensor * cont_node, ggml_tensor * unary_node, ggml_tensor * mul_node, uint16_t * dst16 = nullptr); void ggml_cuda_op_relu_sqr(ggml_backend_cuda_context & ctx, ggml_tensor * relu_node, ggml_tensor * sqr_node); @@ -126,4 +127,4 @@ __device__ __forceinline__ float ggml_cuda_op_swiglu_clamp_single(float gate, fl } // fused GGML_OP_SCALE + GGML_UNARY_OP_{SILU,SIGMOID}: dst = op(scale*x + bias) -void ggml_cuda_op_scale_unary(ggml_backend_cuda_context & ctx, ggml_tensor * scale_node, ggml_tensor * unary_node); +void ggml_cuda_op_scale_unary(ggml_backend_cuda_context & ctx, ggml_tensor * scale_node, ggml_tensor * unary_node, uint16_t * dst16 = nullptr); diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 899258ab493e..2103b4ef77bc 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -575,15 +575,15 @@ ggml_tensor * llama_model_qwen4exp::graph::build_hc_mix( xn = ggml_reshape_2d(ctx0, xn, hc_dim, nt); cb(xn, "hc_norm", il); - ggml_tensor * lo = build_lora_mm(w_down, xn); if (inject) { - // the inject projection reads the same xn as the down projection: emit the two matvecs back to back so - // that the backend can launch them as one grouped kernel (the inject result is only used by hc_combine) - ggml_build_forward_expand(gf, lo); *inject = build_lora_mm(w_inject, xn); cb(*inject, "hc_inject", il); ggml_build_forward_expand(gf, *inject); } + ggml_tensor * lo = build_lora_mm(w_down, xn); + if (inject) { + ggml_build_forward_expand(gf, lo); + } lo = ggml_silu(ctx0, ggml_scale(ctx0, lo, 1.0f / (float) hc)); ggml_tensor * gate = ggml_sigmoid(ctx0, build_lora_mm(w_up, lo)); cb(gate, "hc_gate", il); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 873955d70f79..f9a23809422c 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -3728,6 +3728,57 @@ struct test_hc_combine_norm_lifetime : public test_case { } }; +struct test_cont_sigmoid_mul : public test_case { + const int head_dim, n_head, tokens; + const bool flat; + test_cont_sigmoid_mul(int head_dim, int n_head, int tokens, bool flat) + : head_dim(head_dim), n_head(n_head), tokens(tokens), flat(flat) {} + std::string op_desc(ggml_tensor *) override { return "CONT_SIGMOID_MUL"; } + std::string vars() override { return VARS_TO_STR4(head_dim, n_head, tokens, flat); } + bool run_whole_graph() override { return true; } + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * qg = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 2 * head_dim * n_head, tokens); + ggml_tensor * gate = ggml_view_3d(ctx, qg, head_dim, n_head, tokens, + sizeof(float) * head_dim * 2, sizeof(float) * head_dim * 2 * n_head, sizeof(float) * head_dim); + ggml_tensor * attn; + if (flat) { + gate = ggml_cont_2d(ctx, gate, head_dim * n_head, tokens); + attn = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, head_dim * n_head, tokens); + } else { + gate = ggml_cont(ctx, gate); + attn = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, head_dim, n_head, tokens); + } + return ggml_mul(ctx, attn, ggml_sigmoid(ctx, gate)); + } +}; + +struct test_hc_combine_norm_inject : public test_case { + const int embd, hc, tokens; + ggml_tensor * out_res = nullptr, * out_norm = nullptr, * out_inj = nullptr; + test_hc_combine_norm_inject(int embd, int hc, int tokens) : embd(embd), hc(hc), tokens(tokens) {} + std::string op_desc(ggml_tensor *) override { return "HC_COMBINE_NORM_INJECT"; } + std::string vars() override { return VARS_TO_STR3(embd, hc, tokens); } + bool run_whole_graph() override { return true; } + std::vector<ggml_tensor *> fusion_test_nodes() override { return {out_res, out_norm, out_inj}; } + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * residual = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, embd, hc, tokens); + ggml_tensor * block = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, embd, 1, tokens); + ggml_tensor * inject = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, hc, tokens); + ggml_tensor * gamma = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, embd, hc); + ggml_tensor * w_inj = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, int64_t(embd) * hc, hc); + ggml_tensor * weight = ggml_scale(ctx, ggml_sigmoid(ctx, ggml_scale(ctx, inject, 1.0f / hc)), 2.0f); + weight = ggml_reshape_3d(ctx, weight, 1, hc, tokens); + if (gf) { ggml_build_forward_expand(gf, block); ggml_build_forward_expand(gf, weight); } + ggml_tensor * update = ggml_mul(ctx, ggml_repeat(ctx, block, residual), weight); + out_res = ggml_add(ctx, residual, update); + ggml_set_output(out_res); + out_norm = ggml_mul(ctx, ggml_rms_norm(ctx, out_res, 1e-6f), gamma); + ggml_set_output(out_norm); + out_inj = ggml_mul_mat(ctx, w_inj, ggml_reshape_2d(ctx, out_norm, int64_t(embd) * hc, tokens)); + return out_inj; + } +}; + // qwen4exp conv history: concat(history, x^T), one tail copy per rollback slot, then either the PLE dilated // tap chain (F16 weight columns, ADD chain, SiLU) or ssm_conv + SiLU. HIP fuses both for T >= 256 and must // still produce every tail copy; an extra reader of the whole concat has to disable the fusion. @@ -3736,12 +3787,13 @@ struct test_conv_state_chain : public test_case { const int C, T, n_slots; const bool extra_reader; const bool split_root; // x produced as [C/4, 4, T], as qwen4exp's grouped norm leaves it + const bool f32w; // PLE weight kept F32 (UD GGUFs): no cast in the tap weight columns ggml_tensor * out = nullptr; std::vector<ggml_tensor *> verify; - test_conv_state_chain(bool ple, int C, int T, int n_slots, bool extra_reader = false, bool split_root = false) - : ple(ple), C(C), T(T), n_slots(n_slots), extra_reader(extra_reader), split_root(split_root) {} + test_conv_state_chain(bool ple, int C, int T, int n_slots, bool extra_reader = false, bool split_root = false, bool f32w = false) + : ple(ple), C(C), T(T), n_slots(n_slots), extra_reader(extra_reader), split_root(split_root), f32w(f32w) {} std::string op_desc(ggml_tensor *) override { return "CONV_STATE_CHAIN"; } - std::string vars() override { return VARS_TO_STR6(ple, C, T, n_slots, extra_reader, split_root); } + std::string vars() override { return VARS_TO_STR7(ple, C, T, n_slots, extra_reader, split_root, f32w); } bool run_whole_graph() override { return true; } std::vector<ggml_tensor *> fusion_test_nodes() override { return verify; } ggml_tensor * build_graph(ggml_context * ctx) override { @@ -3762,13 +3814,16 @@ struct test_conv_state_chain : public test_case { verify.push_back(cpy); } if (ple) { - ggml_tensor * W = ggml_new_tensor_2d(ctx, GGML_TYPE_F16, K, C); + ggml_tensor * W = ggml_new_tensor_2d(ctx, f32w ? GGML_TYPE_F32 : GGML_TYPE_F16, K, C); ggml_set_name(W, "conv_w"); ggml_tensor * acc = nullptr; for (int k = 0; k < K; ++k) { ggml_tensor * shifted = ggml_cont(ctx, ggml_transpose(ctx, ggml_view_2d(ctx, cc, T, C, cc->nb[1], (size_t) k * dil * sizeof(float)))); ggml_tensor * wk = ggml_cont(ctx, ggml_view_2d(ctx, W, 1, C, W->nb[1], k * W->nb[0])); - wk = ggml_cast(ctx, ggml_reshape_1d(ctx, wk, C), GGML_TYPE_F32); + wk = ggml_reshape_1d(ctx, wk, C); + if (!f32w) { + wk = ggml_cast(ctx, wk, GGML_TYPE_F32); + } ggml_tensor * term = ggml_mul(ctx, shifted, wk); acc = acc ? ggml_add(ctx, acc, term) : term; } @@ -5686,6 +5741,51 @@ struct test_mul_mat_id : public test_case { } }; +struct test_mul_mat_id_cold : public test_case { + const ggml_type type_a; + const int n_mats; + const int n_used; + const bool b; + const int64_t m; + const int64_t n; + const int64_t k; + const int reps; + + std::string vars() override { + return VARS_TO_STR8(type_a, n_mats, n_used, b, m, n, k, reps); + } + + std::string op_desc(ggml_tensor *) override { return "MUL_MAT_ID_COLD"; } + + double max_nmse_err() override { return 5e-4; } + + uint64_t op_flops(ggml_tensor *) override { return 2 * m * k * n * n_used * reps; } + + bool run_whole_graph() override { return true; } + + test_mul_mat_id_cold(ggml_type type_a, int n_mats, int n_used, bool b, int64_t m, int64_t n, int64_t k, int reps) + : type_a(type_a), n_mats(n_mats), n_used(n_used), b(b), m(m), n(n), k(k), reps(reps) {} + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * as = ggml_new_tensor_3d(ctx, type_a, k, m, n_mats); + ggml_set_name(as, "as"); + ggml_tensor * out = nullptr; + for (int r = 0; r < reps; ++r) { + ggml_tensor * ids = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, n_mats, n); + ids = ggml_view_2d(ctx, ids, n_used, n, ids->nb[1], 0); + ggml_tensor * x = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, k, b ? 1 : n_used, n); + ggml_tensor * y = ggml_mul_mat_id(ctx, as, x, ids); + out = out ? ggml_add(ctx, out, y) : y; + } + ggml_set_name(out, "out"); + return out; + } + + void initialize_tensors(ggml_context * ctx) override { init_mul_mat_id_tensors(ctx, n_mats); } + + void reinit_perf_iter(ggml_context * ctx) override { init_mul_mat_id_ids(ctx, n_mats); } +}; + // quantized-weight BF16 WMMA GEMM (mmb.cu): dense, routed, and the fused routed gate/up + SwiGLU struct test_mmb_quant_dense : test_mul_mat { explicit test_mmb_quant_dense(ggml_type type, int tokens, int rows, int inner) @@ -10422,6 +10522,9 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { for (int T : {256, 2048}) test_cases.emplace_back(new test_conv_state_chain(ple, 2560, T, 5, true)); for (int T : {256, 2048}) test_cases.emplace_back(new test_conv_state_chain(ple, 2560, T, 5, false, true)); } + for (int T : {255, 256, 2048}) { + test_cases.emplace_back(new test_conv_state_chain(true, 10240, T, 5, false, false, true)); + } for(int embd : {1023,1024,2560,3072,3073}) { for(int hc : {1,4,5,16,17}) { for(int tokens : {1,4,64}) test_cases.emplace_back(new test_hc_combine_norm_lifetime(embd,hc,tokens)); @@ -10432,6 +10535,14 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_hc_combine_norm_lifetime(2560,4,tokens,true)); for(int alias : {1,2}) test_cases.emplace_back(new test_hc_combine_norm_lifetime(2560,4,tokens,false,alias)); } + for (int tokens : {64, 512, 2049}) { + test_cases.emplace_back(new test_hc_combine_norm_inject(2560, 4, tokens)); + } + for (bool flat : {false, true}) { + for (int tokens : {1, 7, 2048}) { + test_cases.emplace_back(new test_cont_sigmoid_mul(256, 24, tokens, flat)); + } + } for (ggml_type type : {GGML_TYPE_IQ4_NL, GGML_TYPE_Q8_0}) { for (int tokens : {4095,4096,4097,8192,16384,32768,32769}) { @@ -11460,6 +11571,12 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_IQ4_NL, GGML_TYPE_F32, 512, 10, false, 2560, n, 640)); test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 512, 10, false, 2560, n, 640)); } + for (int n : {1, 2, 3, 4, 8, 64, 512}) { + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q4_K, GGML_TYPE_F32, 512, 10, true, 640, n, 2560)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_1, GGML_TYPE_F32, 512, 10, false, 2560, n, 640)); + } + test_cases.emplace_back(new test_mul_mat_id_fusion(GGML_TYPE_Q5_1, GGML_TYPE_F32, 512, 10, false, 2560, 1, 640, 1, true)); + test_cases.emplace_back(new test_mul_mat_id_fusion(GGML_TYPE_Q5_1, GGML_TYPE_F32, 512, 10, false, 2560, 4, 640, 1, true)); test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 256, 8, false, 512, 128, 2048)); test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 256, 8, false, 512, 512, 2048)); test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 256, 8, false, 512, 1024, 2048)); @@ -12854,10 +12971,22 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() { } } + for (int n : {1, 4}) { + test_cases.emplace_back(new test_mul_mat_id_cold(GGML_TYPE_Q5_1, 512, 10, false, 2560, n, 640, 24)); + test_cases.emplace_back(new test_mul_mat_id_cold(GGML_TYPE_Q8_0, 512, 10, false, 2560, n, 640, 24)); + test_cases.emplace_back(new test_mul_mat_id_cold(GGML_TYPE_Q4_K, 512, 10, true, 640, n, 2560, 24)); + } + // Qwen3.8-Flash-Next decode hot path: fused gate/up uses this exact shape. test_cases.emplace_back(new test_mul_mat_id( GGML_TYPE_IQ3_S, GGML_TYPE_F32, 512, 10, false, 640, 1, 2560)); test_cases.emplace_back(new test_swiglu_iq3_mmvq()); + for (int n : {1, 2, 4}) { + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q4_K, GGML_TYPE_F32, 512, 10, true, 640, n, 2560)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q5_1, GGML_TYPE_F32, 512, 10, false, 2560, n, 640)); + test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q8_0, GGML_TYPE_F32, 512, 10, false, 2560, n, 640)); + test_cases.emplace_back(new test_mul_mat_id_fusion(GGML_TYPE_Q5_1, GGML_TYPE_F32, 512, 10, false, 2560, n, 640, 1, true)); + } // SWIGLU at a 27B-class FFN width, fused [gate|up] vs split operands // note: same bytes either way, so a backend that indexes them differently shows it here @@ -12884,11 +13013,27 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() { test_cases.emplace_back(new test_mul_mat_id(type_a, GGML_TYPE_F32, 256, 6, false, 4096, 512, 2048)); } + test_cases.emplace_back(new test_hc_combine_norm_inject(2560, 4, 2048)); + + for (int64_t n : {2048, 16384}) { + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_F32, GGML_TYPE_F32, 512, n, 2560, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_F32, GGML_TYPE_F32, 48, n, 2560, {1, 1}, {1, 1})); + test_cases.emplace_back(new test_mul_mat(GGML_TYPE_F32, GGML_TYPE_F32, 4, n, 10240, {1, 1}, {1, 1})); + } + // LEAKY_RELU at FFN activation width, for direct comparison with RELU for (int64_t n_tokens : {512, 2048}) { test_cases.emplace_back(new test_leaky_relu(GGML_TYPE_F32, { 17408, n_tokens, 1, 1 }, 0.1f)); } + for (int64_t n_tokens : {1, 32, 2048}) { + test_cases.emplace_back(new test_shared_mul_add(2048, n_tokens)); + } + for (int64_t n_tokens : {32, 128}) { + test_cases.emplace_back(new test_swiglu_q8_mmq(false, 1, 1, 2048, n_tokens, 512)); + test_cases.emplace_back(new test_swiglu_q8_mmq(false, 1, 1, 2560, n_tokens, 640)); + } + // Conv2d: K=CRS=NPQ=4096 matmul performance uint32_t iwh_idx = 0; uint32_t kwh_idx = 1; From 7b1ef030b7081a91ffe4cc36d1531450f65a9462 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Fri, 25 Sep 2026 10:57:09 +0000 Subject: [PATCH 177/212] llama : count the cache's streams in qsa_n_kv_window (#73) qsa_n_kv_window chose how many seq ids to scan from get_n_stream(), but that is the stream count of the current ubatch, not of the cache. Without --kv-unified, a ubatch with one slot has one stream, so the loop scanned all LLAMA_MAX_SEQ ids and get_cells(n_stream) tripped the seq_to_stream assert on the first decode past the dense shortcut. Only sched_reserve, whose context reports the cache's stream count, was fixed before. Use the stream count of the indexer cache. Run test-recurrent-state-rollback on the generated qwen4exp model: its non-unified two-sequence replay decodes one slot at a time and aborted here. Assisted-by: Claude --- src/llama-memory-hybrid-idx.cpp | 7 ++++--- tests/CMakeLists.txt | 10 ++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index 0c1c9a8df872..f1ef948282ce 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -976,9 +976,10 @@ uint32_t llama_memory_hybrid_idx_context::qsa_n_kv_window() const { const uint32_t n_kv = get_idx() ? get_idx()->get_n_kv() : 0; if (!mem || !mem->get_mem_idx()) { return n_kv; } llama_pos pos_max = -1; - // n_stream == 1 maps every seq id to stream 0, so scan all LLAMA_MAX_SEQ of them; - // otherwise seq s maps to stream s and only n_stream seq ids exist in seq_to_stream - const llama_seq_id n_seq_scan = get_n_stream() == 1 ? (llama_seq_id) LLAMA_MAX_SEQ : (llama_seq_id) get_n_stream(); + // a unified cache maps every seq id to stream 0; otherwise seq s is stream s and seq_to_stream holds n_stream ids. + // this is the stream count of the cache: get_n_stream() counts the streams of the ubatch, 1 for a single slot + const uint32_t n_stream = mem->get_mem_idx()->get_n_stream(); + const llama_seq_id n_seq_scan = n_stream == 1 ? (llama_seq_id) LLAMA_MAX_SEQ : (llama_seq_id) n_stream; for (llama_seq_id s = 0; s < n_seq_scan; ++s) { pos_max = std::max(pos_max, mem->get_mem_idx()->get_cells(s).seq_pos_max(s)); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ad70075a87d3..13e7e5c73706 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -245,6 +245,16 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS) set_tests_properties(test-recurrent-state-rollback-kimi-k3 PROPERTIES FIXTURES_REQUIRED generate-models ) + # non-unified multi-seq decodes reach the QSA path with one stream per ubatch (#73) + llama_test( + test-recurrent-state-rollback + NAME test-recurrent-state-rollback-qwen4exp + LABEL main + ARGS -m "${MODEL_DIR}/qwen4exp-moe.gguf" + ) + set_tests_properties(test-recurrent-state-rollback-qwen4exp PROPERTIES + FIXTURES_REQUIRED generate-models + ) # Test state save/load functionality across all architectures, using the generated dummy models llama_test( From 444e7cbc915be393c3fc8c7d907416dff69e7a05 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Fri, 25 Sep 2026 10:57:58 +0000 Subject: [PATCH 178/212] qsa : keep the masked path when a block splits a sequence over sequence sets The compact (maskless) block selection keys blocks on (sequence set, position block). A seq_cp that ends inside a block, as the server's child slots do for n_cmpl > 1, leaves a partial shared group and a partial own group there. Neither is complete, so no sequence can select them: from the query that completes the block on, the shared tail of the prompt and the first own tokens drop out of every QSA layer. The masked path maps those cells to the spare block and keeps them visible. Before the multi-sequence change such decodes had several sequences per ubatch and took the masked path. Move the gate into qsa_scalar_visibility_cells (prefix.h) and reject a ubatch when a position block holds one of its sequences under two sequence sets. The check only scans the blocks that hold a shared cell. A seq_cp on a block boundary still takes the maskless path. Also: - accept tokens with several seq ids again; each query reads the row of its first seq id, as the attention mask and the scan already do (qwen4exp with PLE layers rejects such tokens before this point anyway) - check 2-D cells in one pass, only for the sequences of the ubatch Probe on a head-256 variant of the generated qwen4exp model (CPU, block selection on), prompt of 5 tokens, seq_cp, joint decode of both sequences: with the old gate every query from position 7 on selects only cells 0..3; with this gate all cells up to the budget are selected. Assisted-by: Claude --- src/llama-memory-hybrid-idx.cpp | 38 ++++-------------- src/llama-memory-hybrid-idx.h | 7 ++-- src/models/qwen4exp.cpp | 12 +++--- src/prefix.h | 36 +++++++++++++++++ tests/test-qsa-prefix.cpp | 70 +++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 40 deletions(-) diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index f1ef948282ce..492a9e8252db 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -965,9 +965,9 @@ void llama_memory_hybrid_idx_context::set_input_qsa_blocks( mem->set_input_qsa_blocks(cell_blk, blk_cells, blk_pos, bias, tail_idxs, ubatch, ratio); } -bool llama_memory_hybrid_idx_context::qsa_position_prefix(const llama_ubatch & ubatch) const { +bool llama_memory_hybrid_idx_context::qsa_position_prefix(const llama_ubatch & ubatch, uint32_t ratio) const { if (qsa_prefix_matches(ubatch)) { return true; } - if (!qsa_scalar_visibility(ubatch)) { return false; } + if (!qsa_scalar_visibility(ubatch, ratio)) { return false; } const llama_seq_id seq=ubatch.seq_id[0][0]; return qsa_single_sequence_prefix(mem->get_mem_idx()->get_cells(seq),get_idx()->get_n_kv(),seq); } @@ -988,36 +988,12 @@ uint32_t llama_memory_hybrid_idx_context::qsa_n_kv_window() const { return std::max(n_kv, window); } -bool llama_memory_hybrid_idx_context::qsa_scalar_visibility(const llama_ubatch & ubatch) const { +bool llama_memory_hybrid_idx_context::qsa_scalar_visibility(const llama_ubatch & ubatch, uint32_t ratio) const { if (qsa_prefix_matches(ubatch)) { return true; } - if (get_n_stream()!=1 || !get_idx() || !ubatch.token || !ubatch.pos || !ubatch.n_tokens || - !ubatch.n_pos || !ubatch.seq_id || !ubatch.n_seq_id) { return false; } - if (ubatch.n_seq_id[0]<1 || !ubatch.seq_id[0]) { return false; } - // one sequence per token keeps the per-query row mapping well-defined; a token carrying several seq ids - // (shared prefix) has no single row to read, so it keeps the masked path - for (uint32_t i=0;i<ubatch.n_tokens;++i) { - if (ubatch.n_seq_id[i]!=1 || !ubatch.seq_id[i]) { return false; } - if (ubatch.pos[i]<0 || ubatch.pos[i]>=16777216) { return false; } - for (uint32_t axis=1;axis<ubatch.n_pos;++axis) { - if (ubatch.pos[i+axis*ubatch.n_tokens]!=ubatch.pos[i]) { return false; } - } - } - if (ubatch.is_pos_2d()) { - // a 2-D (image) cell whose position exceeds its linear position breaks the scalar test; scan the cells - // once per distinct sequence in the ubatch, not once per token - const int64_t n_kv = get_idx()->get_n_kv(); - bool seen[LLAMA_MAX_SEQ] = { false }; - for (uint32_t i=0;i<ubatch.n_tokens;++i) { - const llama_seq_id s = ubatch.seq_id[i][0]; - if (s < 0 || s >= LLAMA_MAX_SEQ || seen[s]) { continue; } - seen[s] = true; - const auto & cells = mem->get_mem_idx()->get_cells(s); - for (int64_t j=0;j<n_kv;++j) { - if (!cells.is_empty(j) && cells.seq_has(j,s) && cells.ext_get(j).is_2d_gt(cells.pos_get(j),cells.pos_get(j))) { return false; } - } - } - } - return true; + if (get_n_stream()!=1 || !get_idx() || !ubatch.token || !ubatch.n_tokens || !ubatch.seq_id || !ubatch.n_seq_id || + ubatch.n_seq_id[0]<1 || !ubatch.seq_id[0]) { return false; } + // one stream: every seq id of the ubatch reads the same cells + return qsa_scalar_visibility_cells(mem->get_mem_idx()->get_cells(ubatch.seq_id[0][0]), get_idx()->get_n_kv(), ratio, ubatch); } void llama_memory_hybrid_idx::qsa_invalidate() { diff --git a/src/llama-memory-hybrid-idx.h b/src/llama-memory-hybrid-idx.h index f424b0f4bbeb..283d35967f6b 100644 --- a/src/llama-memory-hybrid-idx.h +++ b/src/llama-memory-hybrid-idx.h @@ -192,10 +192,11 @@ class llama_memory_hybrid_idx_context : public llama_memory_hybrid_context { void set_input_qsa_blocks(ggml_tensor * cell_blk, ggml_tensor * blk_cells, ggml_tensor * blk_pos, ggml_tensor * bias, ggml_tensor * tail_idxs, const llama_ubatch * ubatch, uint32_t ratio) const; - // one sequence, scalar positions in range, no 2d rope extents: the compact visibility rule applies - bool qsa_scalar_visibility(const llama_ubatch & ubatch) const; + // one stream, scalar positions in range, no 2d rope extents, no block split over sequence sets + // (qsa_scalar_visibility_cells): the compact visibility rule applies + bool qsa_scalar_visibility(const llama_ubatch & ubatch, uint32_t ratio) const; // scalar visibility and the cached cells form a single-sequence prefix with unique positions - bool qsa_position_prefix(const llama_ubatch & ubatch) const; + bool qsa_position_prefix(const llama_ubatch & ubatch, uint32_t ratio) const; private: const llama_memory_hybrid_idx * mem = nullptr; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index a77c2a1e9f55..fbc5b3794ecc 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -840,7 +840,7 @@ static std::vector<int64_t> qwen4exp_score_key_limits(const llama_memory_hybrid_ if (ubatch.n_seq_id[i] != 1 || ubatch.seq_id[i][0] != s0) { multi_seq = true; } } } - if (!compact || multi_seq || ubatch.n_tokens<128 || degenerate_pos || !mctx->qsa_position_prefix(ubatch)) { + if (!compact || multi_seq || ubatch.n_tokens<128 || degenerate_pos || !mctx->qsa_position_prefix(ubatch, (uint32_t) ratio)) { return {}; } return qsa_prefix_limits(ubatch.pos,ubatch.n_tokens,strip,ratio,blocks,budget); @@ -963,7 +963,7 @@ class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i { const auto * attn = mctx->get_attn(); const bool blocks = attn && qwen4exp_use_block_selection(blk_bias, n_stream, ratio, n_kv, params.ubatch, params.cparams, params.hparams, attn->type_k(), attn->type_v()); - const bool scalar = blocks && params.hparams.n_swa == 0 && mctx->qsa_scalar_visibility(params.ubatch); + const bool scalar = blocks && params.hparams.n_swa == 0 && mctx->qsa_scalar_visibility(params.ubatch, ratio); res &= (tail_idxs != nullptr) == scalar; res &= compact == scalar; res &= maskless == scalar; @@ -1123,7 +1123,7 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( const auto * attn_ctx = mctx_hyb->get_attn(); const bool blocks = attn_ctx && qwen4exp_use_block_selection(blk_bias, n_stream, r, n_kv, ubatch, cparams, hparams, attn_ctx->type_k(), attn_ctx->type_v()); - const bool scalar = blocks && hparams.n_swa == 0 && mctx_hyb->qsa_scalar_visibility(ubatch); + const bool scalar = blocks && hparams.n_swa == 0 && mctx_hyb->qsa_scalar_visibility(ubatch, (uint32_t) r); qsa->compact = scalar; qsa->maskless = scalar; qsa->score_strip = qwen4exp_query_strip(n_tps, n_stream); @@ -1137,9 +1137,9 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( ggml_set_input(qsa->blk_pos); ggml_set_input(qsa->bias); // complete-block selection lists cells with -1 sentinels (invisible blocks, empty tail slots) that only the - // maskless kernel understands; when the visibility is not scalar (2-D image positions in the cache, several - // sequences) the attention takes the masked path, whose set_rows would write row -1, so the block-expanded - // top-k with the per-block bias is used there instead + // maskless kernel understands; when the visibility is not scalar (2-D image positions in the cache, a block + // split over sequence sets) the attention takes the masked path, whose set_rows would write row -1, so the + // block-expanded top-k with the per-block bias is used there instead if (scalar) { GGML_ASSERT(hparams.indexer_top_k % r == 0); qsa->tail_idxs = ggml_new_tensor_3d(ctx0, GGML_TYPE_I32, r-1, n_tps, n_stream); diff --git a/src/prefix.h b/src/prefix.h index 3ca0d1c43985..cac9926197f3 100644 --- a/src/prefix.h +++ b/src/prefix.h @@ -1,10 +1,46 @@ #pragma once +#include "llama-batch.h" #include "llama-kv-cells.h" #include <algorithm> #include <cstdint> +#include <unordered_map> #include <vector> +// compact (maskless) visibility: each query reads the block row of its first seq id, as the attention mask does. +// Blocks are keyed on (sequence set, position block): a block holding one sequence under two sets (seq_cp inside the +// block) has no complete group for it, so the maskless path would drop its cells; the masked path keeps them +static bool qsa_scalar_visibility_cells(const llama_kv_cells & cells, uint32_t count, uint32_t ratio, const llama_ubatch & u) { + if (ratio==0 || count>cells.size() || !u.pos || !u.n_tokens || !u.n_pos || !u.seq_id || !u.n_seq_id) { return false; } + llama_kv_cells::seq_set_t rows; + for (uint32_t i=0;i<u.n_tokens;++i) { + if (u.n_seq_id[i]<1 || !u.seq_id[i] || u.seq_id[i][0]<0 || u.seq_id[i][0]>=LLAMA_MAX_SEQ) { return false; } + if (u.pos[i]<0 || u.pos[i]>=16777216) { return false; } + for (uint32_t axis=1;axis<u.n_pos;++axis) { + if (u.pos[i+axis*u.n_tokens]!=u.pos[i]) { return false; } + } + rows.set(u.seq_id[i][0]); + } + // a 2-D (image) cell past its linear position breaks the scalar test; only blocks with a shared cell can split + std::unordered_map<llama_pos, std::vector<llama_kv_cells::seq_set_t>> shared; + for (uint32_t j=0;j<count;++j) { + if (cells.is_empty(j) || (cells.seq_get_all(j) & rows).none()) { continue; } + if (u.is_pos_2d() && cells.ext_get(j).is_2d_gt(cells.pos_get(j),cells.pos_get(j))) { return false; } + if (cells.seq_get_all(j).count()>1) { shared[cells.pos_get(j)/ratio]; } + } + for (uint32_t j=0;!shared.empty() && j<count;++j) { + if (cells.is_empty(j) || (cells.seq_get_all(j) & rows).none()) { continue; } + const auto it = shared.find(cells.pos_get(j)/ratio); + if (it == shared.end()) { continue; } + const auto & set = cells.seq_get_all(j); + for (const auto & other : it->second) { + if (other != set && (other & set & rows).any()) { return false; } + } + if (std::find(it->second.begin(), it->second.end(), set) == it->second.end()) { it->second.push_back(set); } + } + return true; +} + static bool qsa_single_sequence_prefix(const llama_kv_cells & cells, uint32_t count, llama_seq_id seq) { if (count>cells.size()) { return false; } std::vector<llama_pos> positions; diff --git a/tests/test-qsa-prefix.cpp b/tests/test-qsa-prefix.cpp index f7223d34a627..05fb927f2683 100644 --- a/tests/test-qsa-prefix.cpp +++ b/tests/test-qsa-prefix.cpp @@ -1,9 +1,79 @@ #include "../src/qsa-prefix-state.h" +#include "../src/prefix.h" #include "ggml.h" #include <random> #include <iostream> +// cells filled in order, one per position +struct qsa_cells { + llama_kv_cells cells; + uint32_t next = 0; + explicit qsa_cells(uint32_t n) { cells.resize(n); } + void add(llama_pos p0, llama_pos p1, std::vector<llama_seq_id> seqs, llama_pos ext_dy = 0) { + for (llama_pos p=p0; p<p1; ++p, ++next) { + cells.pos_set(next, p); + for (auto s : seqs) { cells.seq_add(next, s); } + if (ext_dy) { cells.ext_set(next, { p, p+ext_dy }); } + } + } +}; + +// one token per entry, n_pos axes all equal to the linear position +struct qsa_ubatch { + std::vector<llama_token> tok; std::vector<llama_pos> pos; std::vector<int32_t> n_seq_id; + std::vector<std::vector<llama_seq_id>> ids; std::vector<llama_seq_id *> seq_id; llama_ubatch u{}; + qsa_ubatch(std::vector<std::pair<llama_pos, std::vector<llama_seq_id>>> tokens, uint32_t n_pos = 1) { + const uint32_t n = tokens.size(); + tok.assign(n, 0); pos.resize(n*n_pos); ids.resize(n); + for (uint32_t i=0; i<n; ++i) { + for (uint32_t a=0; a<n_pos; ++a) { pos[i+a*n] = tokens[i].first; } + ids[i] = tokens[i].second; n_seq_id.push_back(ids[i].size()); seq_id.push_back(ids[i].data()); + } + u.n_tokens = n; u.n_pos = n_pos; u.token = tok.data(); u.pos = pos.data(); u.n_seq_id = n_seq_id.data(); u.seq_id = seq_id.data(); + } +}; + +static bool scalar(const qsa_cells & c, const qsa_ubatch & ub) { + return qsa_scalar_visibility_cells(c.cells, c.cells.size(), 4, ub.u); +} + +// the gate that picks the compact (maskless) QSA visibility over the masked one +static void test_scalar_visibility() { + { // independent sequences decode together + qsa_cells c(64); c.add(0, 13, {0}); c.add(0, 13, {1}); + GGML_ASSERT(scalar(c, qsa_ubatch({{13, {0}}, {13, {1}}}))); + } + { // seq_cp inside a block: position 12 is shared, 13 and 14 are not, so block [12, 16) has no complete group + qsa_cells c(64); c.add(0, 13, {0, 1}); c.add(13, 15, {0}); c.add(13, 15, {1}); c.add(0, 6, {2}); + GGML_ASSERT(!scalar(c, qsa_ubatch({{15, {0}}, {15, {1}}}))); + GGML_ASSERT(!scalar(c, qsa_ubatch({{15, {1}}}))); + GGML_ASSERT(scalar(c, qsa_ubatch({{6, {2}}}))); + } + { // seq_cp on a block boundary splits nothing + qsa_cells c(64); c.add(0, 12, {0, 1}); c.add(12, 14, {0}); c.add(12, 14, {1}); + GGML_ASSERT(scalar(c, qsa_ubatch({{14, {0}}, {14, {1}}}))); + } + { // a prompt shared at token level reads the row of its first seq id, as the attention mask does + qsa_cells c(64); c.add(0, 8, {0, 1, 2, 3}); + GGML_ASSERT(scalar(c, qsa_ubatch({{8, {0, 1, 2, 3}}, {9, {0, 1, 2, 3}}}))); + } + { // a 2-D image cell only matters to the sequences that hold it + qsa_cells c(64); c.add(0, 8, {0}); c.add(0, 4, {1}); c.add(4, 5, {1}, 1); + GGML_ASSERT(scalar(c, qsa_ubatch({{8, {0}}}, 3))); + GGML_ASSERT(!scalar(c, qsa_ubatch({{5, {1}}}, 3))); + GGML_ASSERT(!scalar(c, qsa_ubatch({{8, {0}}, {5, {1}}}, 3))); + } + { // no seq id, or a position with 2-D extents in the ubatch + qsa_cells c(64); c.add(0, 8, {0}); + qsa_ubatch none({{8, {0}}}); none.n_seq_id[0] = 0; + GGML_ASSERT(!scalar(c, none)); + qsa_ubatch ub({{8, {0}}}, 3); ub.pos[1] = 9; + GGML_ASSERT(!scalar(c, ub)); + } +} + int main() { + test_scalar_visibility(); qsa_prefix_state s(4096); std::mt19937 rng(414); for (int round=0; round<200; ++round) { From 14dad76c05b942c203da8d7114ca93edc9f58f90 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Fri, 25 Sep 2026 10:58:35 +0000 Subject: [PATCH 179/212] qsa : fill only the compact rows the queries read, harden qsa_metadata - the compact limits scan tested every block against all n_seq_max rows; fill only the rows of the ubatch's first seq ids, and assert that each row index is in range before get_rows reads it on the device - qsa_metadata divided a signed count by a size_t block count; divide as int64_t, and reject a zero block count or a bias size that is not n_seq*blocks + 2*n_tps - drop the multi-sequence check in qwen4exp_score_key_limits: the trim needs qsa_position_prefix, which already requires that every cell belongs only to the first sequence of the ubatch Assisted-by: Claude --- src/llama-memory-hybrid-idx.cpp | 21 +++++++++++++++------ src/models/qwen4exp.cpp | 10 +--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index 492a9e8252db..ad8b0f61bc32 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -361,7 +361,7 @@ bool llama_memory_hybrid_idx::qsa_metadata(ggml_tensor * members, ggml_tensor * ggml_tensor * tails, const llama_ubatch & u, uint32_t ratio) const { if (ratio != 4 || bias->type != GGML_TYPE_I32 || !qsa_prefix_matches(u)) { return false; } const size_t blocks = pos->ne[0]/4, complete = qsa_prefix.cells.size()/4; - if (complete > blocks) { return false; } + if (blocks == 0 || complete > blocks) { return false; } auto * c = members ? (int32_t *) members->data : nullptr; auto * p = pos ? (int32_t *) pos->data : nullptr; auto * lim = (int32_t *) bias->data; auto * tail = (int32_t *) tails->data; if (!lim || !tail) { return false; } @@ -376,9 +376,10 @@ bool llama_memory_hybrid_idx::qsa_metadata(ggml_tensor * members, ggml_tensor * } } const int64_t n_tps = u.n_tokens; - const int64_t n_seq = (bias->ne[0] - 2*n_tps) / blocks; + const int64_t n_lim = bias->ne[0] - 2*n_tps; + const int64_t n_seq = n_lim / (int64_t) blocks; const int64_t row = qsa_prefix.sequence; - if (n_seq < 1 || row < 0 || row >= n_seq) { return false; } + if (n_lim % (int64_t) blocks != 0 || n_seq < 1 || row < 0 || row >= n_seq) { return false; } const int64_t base = blocks * n_seq; std::fill(lim, lim + base, INT32_MAX); std::copy_n(qsa_prefix.block_positions.data(), complete, lim + row*blocks); @@ -747,10 +748,18 @@ void llama_memory_hybrid_idx::set_input_qsa_scan( if (compact) { // one start row per sequence: block b is visible to seq s iff the group's rep cell carries s, - // so blocks shared by several sequences (seq_cp) stay visible to each of them, as in the masked path + // so blocks shared by several sequences (seq_cp) stay visible to each of them, as in the masked path. + // only the rows the queries read are filled std::fill(limits, limits + n_blocks*n_seq, INT32_MAX); - for (int64_t b=0;b<n_bid;++b) { - for (int64_t row=0;row<n_seq;++row) { + llama_kv_cells::seq_set_t rows; + for (int64_t ii=0;ii<n_tps;++ii) { + const llama_seq_id row = ubatch->seq_id[s*n_tps + ii][0]; + GGML_ASSERT(row >= 0 && row < n_seq); + rows.set(row); + } + for (int64_t row=0;row<n_seq;++row) { + if (!rows.test(row)) { continue; } + for (int64_t b=0;b<n_bid;++b) { if (cells.seq_has((uint32_t) bid_cell[b], (llama_seq_id) row)) { limits[row*n_blocks + b] = bid_idx[b]; } diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index fbc5b3794ecc..93cc24a6d3c0 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -832,15 +832,7 @@ static std::vector<int64_t> qwen4exp_score_key_limits(const llama_memory_hybrid_ for (uint32_t i = 1; degenerate_pos && i < ubatch.n_tokens; ++i) { if (ubatch.pos[i] != ubatch.pos[0]) { degenerate_pos = false; } } - // the trim assumes one causal visible range; a ubatch that mixes sequences has several, so leave it unbounded - bool multi_seq = false; - if (ubatch.n_seq_id && ubatch.seq_id && ubatch.n_seq_id[0] > 0) { - const llama_seq_id s0 = ubatch.seq_id[0][0]; - for (uint32_t i = 1; i < ubatch.n_tokens && !multi_seq; ++i) { - if (ubatch.n_seq_id[i] != 1 || ubatch.seq_id[i][0] != s0) { multi_seq = true; } - } - } - if (!compact || multi_seq || ubatch.n_tokens<128 || degenerate_pos || !mctx->qsa_position_prefix(ubatch, (uint32_t) ratio)) { + if (!compact || ubatch.n_tokens<128 || degenerate_pos || !mctx->qsa_position_prefix(ubatch, (uint32_t) ratio)) { return {}; } return qsa_prefix_limits(ubatch.pos,ubatch.n_tokens,strip,ratio,blocks,budget); From 5039d0398a2667f50452117f284370892affd62a Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Fri, 25 Sep 2026 13:54:22 +0200 Subject: [PATCH 180/212] ggml-cpu : attend only the selected keys of flash_attn_ext The CPU flash attention ignored the selected keys (src[5]) and relied on the mask to carry the same selection. The maskless QSA selection of qwen4exp passes no mask, so on the CPU it attended every cell of the window, including empty cells and cells of other sequences. Attend the first n_kv_raw keys plus the unique valid selected keys, in ascending order, as the HIP decode kernel does. With a mask that encodes the same selection the visited keys and their order do not change. The tiled kernel attends every key the mask allows, so a maskless selected-key op takes the per-row path. test_qsa_prefill_maskless now checks the CPU result against its FP64 oracle too. On a head-256 qwen4exp test model the maskless output now matches dense attention exactly while the budget covers every visible cell. With -ffp-contract=off the deepseek4, glm-dsa, deepseek32, qwen4exp (masked) and llama test models give byte-identical attention outputs before and after. Assisted-by: Claude --- ggml/src/ggml-cpu/ggml-cpu.c | 5 +++++ ggml/src/ggml-cpu/ops.cpp | 28 ++++++++++++++++++++++++++-- tests/test-backend-ops.cpp | 10 +++++----- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/ggml/src/ggml-cpu/ggml-cpu.c b/ggml/src/ggml-cpu/ggml-cpu.c index b1a2e8d05ca3..91d25d0a66aa 100644 --- a/ggml/src/ggml-cpu/ggml-cpu.c +++ b/ggml/src/ggml-cpu/ggml-cpu.c @@ -3199,6 +3199,11 @@ struct ggml_cplan ggml_graph_plan( size_t n_chunks = n_tasks; size_t decode = sizeof(float)*(neq2*n_chunks*(2+DV) + n_tasks*(DK + 2*DV)); + // selected-key rows (src[5]) are gathered per thread after the decode partials + if (node->src[5]) { + decode += sizeof(float)*n_tasks*CACHE_LINE_SIZE_F32 + sizeof(int32_t)*n_tasks*node->src[5]->ne[0]; + } + cur += MAX(prefill, decode); } break; case GGML_OP_FLASH_ATTN_BACK: diff --git a/ggml/src/ggml-cpu/ops.cpp b/ggml/src/ggml-cpu/ops.cpp index 66e3659f943c..916a4e648630 100644 --- a/ggml/src/ggml-cpu/ops.cpp +++ b/ggml/src/ggml-cpu/ops.cpp @@ -8666,6 +8666,7 @@ static void ggml_compute_forward_flash_attn_ext_f16_one_chunk( const ggml_tensor * v = dst->src[2]; const ggml_tensor * mask = dst->src[3]; const ggml_tensor * sinks = dst->src[4]; + const ggml_tensor * ids = dst->src[5]; GGML_TENSOR_LOCALS(int64_t, neq, q, ne) GGML_TENSOR_LOCALS(size_t, nbq, q, nb) @@ -8707,6 +8708,12 @@ static void ggml_compute_forward_flash_attn_ext_f16_one_chunk( const int64_t rv2 = neq2/nev2; const int64_t rv3 = neq3/nev3; + // selected keys (src[5]): the first n_kv_raw keys plus key n_kv_raw + i for each valid index i in the query's row. + // they are visited in ascending order, so with a mask that encodes the same selection the result is unchanged + const int64_t n_kv_raw = ids ? ggml_get_op_params_i32(dst, 4) : nek1; + int32_t * keys = ids ? (int32_t *) ((float *) params->wdata + params->nth*(DK + 2*DV + CACHE_LINE_SIZE_F32) + + neq2*params->nth*(2 + DV)) + params->ith*ids->ne[0] : nullptr; + // parallelize by q rows using ggml_vec_dot_f32 float scale = 1.0f; @@ -8773,11 +8780,26 @@ static void ggml_compute_forward_flash_attn_ext_f16_one_chunk( const float * pq = (const float *) ((char *) q->data + (iq1*nbq1 + iq2*nbq2 + iq3*nbq3)); q_to_vec_dot(pq, Q_q, DK); + int64_t n_sel = 0; + if (ids) { + const int32_t * row = (const int32_t *) ((const char *) ids->data + iq1*ids->nb[1] + (iq3%ids->ne[3])*ids->nb[3]); + for (int64_t j = 0; j < ids->ne[0]; ++j) { + const int64_t ic = n_kv_raw + row[j]; + if (row[j] >= 0 && ic >= ic_start && ic < ic_end) { + keys[n_sel++] = (int32_t) ic; + } + } + std::sort(keys, keys + n_sel); + n_sel = std::unique(keys, keys + n_sel) - keys; + } + const int64_t n_dense = std::max<int64_t>(0, std::min(ic_end, n_kv_raw) - ic_start); + // online softmax / attention // loop over n_kv and n_head_kv // ref: https://arxiv.org/pdf/2112.05682.pdf - for (int64_t ic = ic_start; ic < ic_end; ++ic) { + for (int64_t j = 0; j < n_dense + n_sel; ++j) { + const int64_t ic = j < n_dense ? ic_start + j : keys[j - n_dense]; const float mv = mp ? slope*GGML_CPU_FP16_TO_FP32(mp[ic]) : 0.0f; if (mv == -INFINITY) { continue; @@ -9357,11 +9379,13 @@ static void ggml_compute_forward_flash_attn_ext_f16( const int64_t dr = (nr + nchunk - 1) / nchunk; static constexpr int64_t Q_TILE_SZ = ggml_fa_tile_config::Q; + // the tiled kernel attends every key the mask allows, so selected keys without a mask need the per-row path bool use_tiled = !use_ref && (q->type == GGML_TYPE_F32 && kv_is_f32_or_f16 && k->type == v->type && - neq1 >= Q_TILE_SZ); + neq1 >= Q_TILE_SZ && + (!dst->src[5] || dst->src[3])); #ifdef GGML_SIMD #if defined(__ARM_FEATURE_SVE) const int64_t f32_epr = svcntw(); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index a16501e75958..515da740bdef 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -8913,8 +8913,8 @@ struct test_qsa_decode : public test_qsa_prefill { }; // Maskless selected-key prefill: the rows name only the visible cells (-1 elsewhere) and no mask is given, as the -// complete-block selection graph does. The CPU graph cannot express that (its flash attention ignores src[5]), so -// the check is against a host FP64 oracle over the listed cells; the CPU result is ignored. +// complete-block selection graph does. Both the backend and the CPU result are checked against a host FP64 oracle +// over the listed cells. struct test_qsa_prefill_maskless : public test_qsa_prefill { test_qsa_prefill_maskless(int queries, int keys, int selected, int ratio=12) : test_qsa_prefill(queries,keys,selected,true,false,1,ratio) {} @@ -8949,7 +8949,6 @@ struct test_qsa_prefill_maskless : public test_qsa_prefill { ggml_backend_tensor_set(ids,picks.data(),0,ggml_nbytes(ids)); } double err(const float * actual, const float * cpu, size_t n) override { - GGML_UNUSED(cpu); const auto qv=tensor_to_float(q), kv=tensor_to_float(k), vv=tensor_to_float(v); std::vector<int32_t> picks(ggml_nelements(ids)); ggml_backend_tensor_get(ids,picks.data(),0,ggml_nbytes(ids)); @@ -8975,8 +8974,9 @@ struct test_qsa_prefill_maskless : public test_qsa_prefill { } } const double gpu_error=nmse(reference.data(),actual,n); - fprintf(stderr,"QSA_MASKLESS_FP64 q=%d keys=%d selected=%d gpu=%.9g\n",queries,keys,selected,gpu_error); - return gpu_error; + const double cpu_error=nmse(reference.data(),cpu,n); + fprintf(stderr,"QSA_MASKLESS_FP64 q=%d keys=%d selected=%d gpu=%.9g cpu=%.9g\n",queries,keys,selected,gpu_error,cpu_error); + return std::max(gpu_error,cpu_error); } }; From ae2d2fe1c9ba6b063ee92be629a2ad2049e67189 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Fri, 25 Sep 2026 16:54:04 +0200 Subject: [PATCH 181/212] qwen4exp : keep the one-sequence compact visibility a broadcast The multi-sequence compact layout gathered each query's block-start row with get_rows on the I32 limits and then cast the [blocks, queries] result to F32. That adds two passes over a tensor the size of the scores for every strip of every QSA layer, also with one sequence, where every query reads the same row. With one sequence, broadcast the row as before. With several, cast the [blocks, n_seq] starts first and gather the F32 rows. Also copy the block cells and the tails to the compute backend once per layer. The strips sliced these host inputs, so each strip needed its own input copy, and a scheduler split could fall inside a block selection, which then ran unfused on HIP (3 of 48 per ubatch with the broadcast alone). Qwen3.8-Flash-Next IQ3_XXS, pp2048 at depth 65536, gfx1151, ABBA kernel traces: the visibility kernels take 395 ms per run instead of 939 ms, and all 1536 block selections run fused. Cell selections on the generated qwen4exp model are unchanged for one and for several sequences. Assisted-by: Claude --- src/models/qwen4exp.cpp | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 93cc24a6d3c0..0fdd17f1c8b0 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -846,9 +846,16 @@ static std::vector<int64_t> qwen4exp_score_key_limits(const llama_memory_hybrid_ static ggml_tensor * qwen4exp_apply_compact_visibility(ggml_context *ctx,ggml_tensor *score, ggml_tensor *limits,int64_t blocks,int64_t n_seq,int64_t n_tps,int64_t first,int64_t queries) { GGML_ASSERT(limits->type==GGML_TYPE_I32 && score->ne[0]<=blocks && score->ne[1]==queries && score->ne[2]==1); - auto *starts2d=ggml_view_2d(ctx,limits,score->ne[0],n_seq,blocks*sizeof(int32_t),0); - auto *row_idx=ggml_view_1d(ctx,limits,queries,(blocks*n_seq+n_tps+first)*sizeof(int32_t)); - auto *starts=ggml_cast(ctx,ggml_get_rows(ctx,starts2d,row_idx),GGML_TYPE_F32); + // one row is broadcast over the queries; several rows are cast first (n_seq x blocks) and then gathered per + // query, so no pass over the full [blocks, queries] tensor is added + ggml_tensor * starts = nullptr; + if (n_seq == 1) { + starts=ggml_cast(ctx,ggml_view_1d(ctx,limits,score->ne[0],0),GGML_TYPE_F32); + } else { + auto *starts2d=ggml_cast(ctx,ggml_view_2d(ctx,limits,score->ne[0],n_seq,blocks*sizeof(int32_t),0),GGML_TYPE_F32); + auto *row_idx=ggml_view_1d(ctx,limits,queries,(blocks*n_seq+n_tps+first)*sizeof(int32_t)); + starts=ggml_get_rows(ctx,starts2d,row_idx); + } auto *tails=ggml_cast(ctx,ggml_view_1d(ctx,limits,queries,(blocks*n_seq+first)*sizeof(int32_t)),GGML_TYPE_F32); tails=ggml_reshape_2d(ctx,tails,1,queries); auto *visible=ggml_step(ctx,ggml_sub(ctx,ggml_repeat(ctx,tails,score),starts)); @@ -1215,6 +1222,18 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( cb(q, "indexer_q", il); const int64_t strip = qwen4exp_query_strip(n_tps, n_stream); + + // the strips slice the block cells and the tails; copy these host inputs to the compute backend once, so no strip + // needs an input copy and the scheduler cannot cut a split inside a (fused) block selection + ggml_tensor * blk_cells = inp->blk_cells; + ggml_tensor * tail_idxs = inp->tail_idxs; + if (tail_idxs) { + blk_cells = ggml_cont(ctx0, blk_cells); + tail_idxs = ggml_cont(ctx0, tail_idxs); + ggml_build_forward_expand(gf, blk_cells); + ggml_build_forward_expand(gf, tail_idxs); + } + std::vector<ggml_tensor *> selected; for (int64_t first = 0; first < n_tps; first += strip) { const int64_t n_query = std::min(strip, n_tps - first); @@ -1257,12 +1276,12 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k( score = ggml_add(ctx0, score, bias); } - if (inp->tail_idxs) { - ggml_tensor * tail = whole ? ggml_reshape_4d(ctx0, inp->tail_idxs, r-1, n_tps, 1, n_stream) - : ggml_view_4d(ctx0, inp->tail_idxs, r-1, n_query, 1, n_stream, - inp->tail_idxs->nb[1], inp->tail_idxs->nb[2], inp->tail_idxs->nb[2], first*inp->tail_idxs->nb[1]); + if (tail_idxs) { + ggml_tensor * tail = whole ? ggml_reshape_4d(ctx0, tail_idxs, r-1, n_tps, 1, n_stream) + : ggml_view_4d(ctx0, tail_idxs, r-1, n_query, 1, n_stream, + tail_idxs->nb[1], tail_idxs->nb[2], tail_idxs->nb[2], first*tail_idxs->nb[1]); ggml_tensor * block_cells = score_blocks < n_blocks ? - ggml_view_2d(ctx0, inp->blk_cells, r*score_blocks, n_stream, inp->blk_cells->nb[1], 0) : inp->blk_cells; + ggml_view_2d(ctx0, blk_cells, r*score_blocks, n_stream, blk_cells->nb[1], 0) : blk_cells; ggml_tensor * top_k = qwen4exp_select_complete_blocks(ctx0, score, block_cells, tail, hparams.indexer_top_k/r, r); cb(top_k, "indexer_top_k", il); From bc866d53bd9b3f2690ebbe8873c11a579b78973e Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Fri, 25 Sep 2026 19:09:23 +0000 Subject: [PATCH 182/212] cuda : gate mmb per backend context instead of per process - the opt-in was one process-wide flag set at model load, so the last load won: a qwen4exp target followed by a --model-draft or speculative-prefill draft of another arch (DFlash, a small qwen3) lost mmb for the target too. Keep the flag in ggml_backend_cuda_context and set it for each backend when llama_context is created, from the model arch. Target and draft now keep their own setting, and the flag is fixed before the first graph, so the graph_optimize BF16 marks and the compute path always agree - pass the context to ggml_cuda_mmb_supported_mm/mmid/glu and ggml_cuda_mmb_claims; mmb_enabled reads ctx.device, not the current device - test-backend-ops never loads a model, so with the default off every ROCm MUL_MAT case and MMB_CONTEXT ran MMQ; opt its backends in so mmb stays covered - drop <atomic>, shorten the measurement comment and say when the opt-in can go Checked here: CPU build of llama and test-backend-ops; clang -x hip --offload-arch=gfx1151 -fsyntax-only of mmb.cu, ggml-cuda.cu, hyperconn.cu and moe-weighted-reduction.cu against Ubuntu's ROCm 5.7 headers (no new diagnostics; the same 2 hipStreamWaitEvent errors from the old headers on both sides); host-side CUDA syntax check of the same files. Not checked: no gfx1151 hardware here, so no test-backend-ops -b ROCm0, no llama-bench and no logits comparison. Needs on Strix Halo: test-backend-ops -b ROCm0 (with MMB_CONTEXT), qwen4exp prefill with and without a non-qwen4exp draft, and a qwen35 prefill to confirm it stays on MMQ. Assisted-by: Claude Opus 5.5 --- ggml/src/ggml-cuda/common.cuh | 1 + ggml/src/ggml-cuda/ggml-cuda.cu | 45 +++++++++++++++++---------------- ggml/src/ggml-cuda/mmb.cu | 40 +++++++++-------------------- ggml/src/ggml-cuda/mmb.cuh | 9 +++---- src/llama-context.cpp | 10 ++++++++ src/llama.cpp | 11 -------- tests/test-backend-ops.cpp | 12 +++++++++ 7 files changed, 62 insertions(+), 66 deletions(-) diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index 89b1e3b0ade0..ae7240b1c36d 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -1475,6 +1475,7 @@ struct ggml_backend_cuda_context { int curr_stream_no = 0; ggml_cuda_mmb_context * mmb = nullptr; + bool mmb_opt_in = false; // set by ggml_backend_cuda_set_mmb_enabled, before the first graph bool mmb_after_compute = true; const void * mmb_first_split = nullptr; std::vector<uint64_t> mmb_graph_sigs; diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 631474fefc45..7b2ce802caf2 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -1894,7 +1894,7 @@ static void ggml_cuda_mul_mat(ggml_backend_cuda_context & ctx, const ggml_tensor ggml_cuda_mul_mat_vec_q(ctx, src0, src1, nullptr, dst); return; } - if (ggml_cuda_mmb_supported_mm(src0, src1, dst)) { + if (ggml_cuda_mmb_supported_mm(ctx, src0, src1, dst)) { ggml_cuda_mul_mat_mmb(ctx, src0, src1, dst); return; } @@ -1966,7 +1966,7 @@ static void ggml_cuda_mul_mat_id(ggml_backend_cuda_context & ctx, ggml_tensor * } } - if (ggml_cuda_mmb_supported_mmid(src0, src1, ids, dst)) { + if (ggml_cuda_mmb_supported_mmid(ctx, src0, src1, ids, dst)) { ggml_cuda_mul_mat_id_mmb(ctx, src0, src1, ids, dst); return; } @@ -4158,14 +4158,14 @@ static bool ggml_cuda_qsa_expand_alias_ok(const ggml_cuda_qsa_expand_args & args } // true when the BF16 WMMA GEMM would take this MUL_MAT / MUL_MAT_ID, so MMQ-based fusions leave it alone -static bool ggml_cuda_mmb_claims(const ggml_tensor * t) { +static bool ggml_cuda_mmb_claims(ggml_backend_cuda_context & ctx, const ggml_tensor * t) { if (!t || !t->src[0] || !t->src[1]) { return false; } if (t->op == GGML_OP_MUL_MAT_ID) { - return t->src[2] && ggml_cuda_mmb_supported_mmid(t->src[0], t->src[1], t->src[2], t); + return t->src[2] && ggml_cuda_mmb_supported_mmid(ctx, t->src[0], t->src[1], t->src[2], t); } - return t->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t); + return t->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_supported_mm(ctx, t->src[0], t->src[1], t); } static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph * cgraph, int i) { @@ -4192,7 +4192,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph ggml_tensor * gate = glu->src[0]; ggml_tensor * up = glu->src[1]; const bool paired = (gate == node && up == cgraph->nodes[i + 1]) || (gate == cgraph->nodes[i + 1] && up == node); - if (paired && ggml_cuda_mmb_supported_glu(gate->src[0], up->src[0], up->src[1], up->src[2], glu) && + if (paired && ggml_cuda_mmb_supported_glu(*cuda_ctx, gate->src[0], up->src[0], up->src[1], up->src[2], glu) && ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_MUL_MAT_ID, GGML_OP_MUL_MAT_ID, GGML_OP_GLU }, { i + 2 })) { const int out_nodes[] = { i + 2 }; if (ggml_cuda_check_fusion_memory_ranges(cgraph, i, 3, out_nodes, 1)) { @@ -4439,7 +4439,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } if (node->op == GGML_OP_GLU && i + 1 < cgraph->n_nodes && ggml_get_glu_op(node) == GGML_GLU_OP_SWIGLU && node->src[1] && - !ggml_cuda_mmb_is_bf16_only(*cuda_ctx, node) && !ggml_cuda_mmb_claims(cgraph->nodes[i + 1])) { + !ggml_cuda_mmb_is_bf16_only(*cuda_ctx, node) && !ggml_cuda_mmb_claims(*cuda_ctx, cgraph->nodes[i + 1])) { ggml_tensor * next = cgraph->nodes[i + 1]; const bool has_ids = next->op == GGML_OP_MUL_MAT_ID; if (has_ids || next->op == GGML_OP_MUL_MAT) { @@ -4705,7 +4705,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph // [n_embd, n_used] intermediate and its separate reduction launch. if (getenv("GGML_CUDA_DISABLE_WEIGHTED_DOWN") == nullptr && node->op == GGML_OP_MUL_MAT_ID && i + 20 < cgraph->n_nodes && cgraph->nodes[i + 1]->op == GGML_OP_MUL && - !ggml_cuda_mmb_claims(node)) { + !ggml_cuda_mmb_claims(*cuda_ctx, node)) { constexpr int n_used = 10; constexpr int n_ops = 2 + n_used + (n_used - 1); ggml_tensor * mul = cgraph->nodes[i + 1]; @@ -4844,7 +4844,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph const bool use_mmq = shared_inputs && ggml_cuda_should_use_mmq(src0->type, cc, mmq_cols, n_experts) && ggml_cuda_should_use_mmq(src0_next->type, cc, mmq_cols, n_experts); - const bool compatible = use_mmq && !ggml_cuda_mmb_claims(node) && node->src[1]->type == GGML_TYPE_F32 && + const bool compatible = use_mmq && !ggml_cuda_mmb_claims(*cuda_ctx, node) && node->src[1]->type == GGML_TYPE_F32 && node->type == GGML_TYPE_F32 && next->type == GGML_TYPE_F32 && ggml_are_same_shape(src0, src0_next) && ggml_are_same_shape(node, next) && mmq_get_q8_1_ds_layout(src0->type) == mmq_get_q8_1_ds_layout(src0_next->type) && @@ -5178,7 +5178,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph const ggml_tensor * src1 = up->src[1]; const ggml_tensor * ids = up->src[2]; - if (op == GGML_OP_MUL_MAT_ID && ggml_cuda_mmb_supported_glu(gate->src[0], up->src[0], src1, ids, glu)) { + if (op == GGML_OP_MUL_MAT_ID && ggml_cuda_mmb_supported_glu(*cuda_ctx, gate->src[0], up->src[0], src1, ids, glu)) { ggml_cuda_mul_mat_id_mmb_glu(*cuda_ctx, gate->src[0], up->src[0], src1, ids, glu); fused_mul_mat_vec = true; fused_node_count = 3; @@ -5447,7 +5447,7 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph // HC gate GEMM [320 -> 10240] whose only consumer is the fused stream mix: GEMM + sigmoid + mix in one kernel if (node->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_gatemix() && i + 1 < cgraph->n_nodes && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { const ggml_tensor * w = node->src[0], * lo = node->src[1]; - if (ggml_is_quantized(w->type) && ggml_node_has_n_uses(cgraph, i, 1) && ggml_cuda_mmb_supported_mm(w, lo, node)) { + if (ggml_is_quantized(w->type) && ggml_node_has_n_uses(cgraph, i, 1) && ggml_cuda_mmb_supported_mm(*cuda_ctx, w, lo, node)) { ggml_cuda_hc_mix_args ma; const int count = ggml_cuda_hc_mix_closed(cgraph, i + 1, ma); if (count > 0 && ma.gate == node && ggml_cuda_hc_gate_mix(*cuda_ctx, w, lo, ma.xn, ma.dst, ma.hc, ma.scale, ma.bias)) return count; @@ -6069,7 +6069,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph if (!reads(t, xn)) continue; ++nread; if (t->op == GGML_OP_MUL_MAT && t->src[0]->type != GGML_TYPE_F32 && - ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) continue; + ggml_cuda_mmb_supported_mm(*cuda_ctx, t->src[0], t->src[1], t)) continue; if (t->op == GGML_OP_MUL && n >= 1) { ggml_cuda_hc_mix_args ma; if (ggml_cuda_hc_mix_closed(cgraph, n - 1, ma) > 0 && (ma.xn == t->src[0] || ma.xn == t->src[1]) && (ma.xn == xn || ma.xn->view_src == xn)) continue; } if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE) continue; ok = false; @@ -6086,7 +6086,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph for (const ggml_tensor * b : blks) { if (ggml_nrows(b) < 512 || b->ne[0] % 8 != 0) continue; bool producer_ok = false; // only producers that honour a BF16 mark - if (b->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_supported_mm(b->src[0], b->src[1], b)) producer_ok = true; + if (b->op == GGML_OP_MUL_MAT && ggml_cuda_mmb_supported_mm(*cuda_ctx, b->src[0], b->src[1], b)) producer_ok = true; if (!producer_ok) { for (int k = 0; k < cgraph->n_nodes && !producer_ok; ++k) { ggml_cuda_moe_weighted_reduction_match mm; @@ -6157,14 +6157,14 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph ++nread; if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE) continue; if (t->op == GGML_OP_MUL_MAT && t->src[0]->type != GGML_TYPE_F32 && - ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) continue; + ggml_cuda_mmb_supported_mm(*cuda_ctx, t->src[0], t->src[1], t)) continue; ok = false; } if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, d)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, d); } for (int i = 0; i < cgraph->n_nodes; ++i) { // the HC gate GEMM feeding only the fused mix keeps its BF16 epilogue const ggml_tensor * t = cgraph->nodes[i]; - if (t->op != GGML_OP_MUL_MAT || t->src[0]->ne[0] != 320 || t->src[0]->ne[1] != 10240 || !ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) continue; + if (t->op != GGML_OP_MUL_MAT || t->src[0]->ne[0] != 320 || t->src[0]->ne[1] != 10240 || !ggml_cuda_mmb_supported_mm(*cuda_ctx, t->src[0], t->src[1], t)) continue; bool ok = true; int nread = 0; for (int n = i + 1; n < cgraph->n_nodes && ok; ++n) { const ggml_tensor * u = cgraph->nodes[n]; @@ -6183,7 +6183,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph int xi = -1; for (int k = 0; k < i; ++k) { if (cgraph->nodes[k] == ex) { xi = k; break; } } if (xi < 0 || ex->op != GGML_OP_MUL_MAT_ID || ex->type != GGML_TYPE_F32) continue; - if (!ggml_cuda_mmb_supported_mmid(ex->src[0], ex->src[1], ex->src[2], const_cast<ggml_tensor *>(ex))) continue; + if (!ggml_cuda_mmb_supported_mmid(*cuda_ctx, ex->src[0], ex->src[1], ex->src[2], const_cast<ggml_tensor *>(ex))) continue; if (ex->src[0]->type != GGML_TYPE_IQ4_NL) continue; // other formats keep F32 expert outputs if (!ggml_node_has_n_uses(cgraph, xi, 1)) continue; if (ex->ne[0] % 8 != 0 || ggml_nrows(ex) < 512) continue; @@ -6206,12 +6206,12 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph auto reads = [](const ggml_tensor * t, const ggml_tensor * x) { for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) if (t->src[s] == x || t->src[s]->view_src == x) return true; return false; }; for (int i = 0; i < cgraph->n_nodes; ++i) { const ggml_tensor * t = cgraph->nodes[i]; - if (t->op == GGML_OP_MUL_MAT && (t->src[0]->type == GGML_TYPE_IQ4_NL || t->src[0]->type == GGML_TYPE_Q6_K) && ggml_cuda_mmb_supported_mm(t->src[0], t->src[1], t)) ggml_cuda_mmb_shadow_prepare(*cuda_ctx, t->src[0]); + if (t->op == GGML_OP_MUL_MAT && (t->src[0]->type == GGML_TYPE_IQ4_NL || t->src[0]->type == GGML_TYPE_Q6_K) && ggml_cuda_mmb_supported_mm(*cuda_ctx, t->src[0], t->src[1], t)) ggml_cuda_mmb_shadow_prepare(*cuda_ctx, t->src[0]); } for (int i = 0; i < cgraph->n_nodes; ++i) { const ggml_tensor * glu = cgraph->nodes[i]; if (glu->op != GGML_OP_GLU || !glu->src[0] || !glu->src[1] || glu->src[0]->op != GGML_OP_MUL_MAT_ID) continue; - if (!ggml_cuda_mmb_supported_glu(glu->src[0]->src[0], glu->src[1]->src[0], glu->src[0]->src[1], glu->src[0]->src[2], glu)) continue; + if (!ggml_cuda_mmb_supported_glu(*cuda_ctx, glu->src[0]->src[0], glu->src[1]->src[0], glu->src[0]->src[1], glu->src[0]->src[2], glu)) continue; for (const auto * input : {glu->src[0]->src[0], glu->src[1]->src[0], glu->src[0]->src[1], glu->src[0]->src[2], glu->src[0], glu->src[1]}) { auto * root = const_cast<ggml_tensor *>(input->view_src ? input->view_src : input); params->add_alloc_dep(params->user_data, root, const_cast<ggml_tensor *>(glu)); @@ -6221,7 +6221,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph const ggml_tensor * t = cgraph->nodes[n]; if (!reads(t, glu)) continue; ++nread; - if (t->op == GGML_OP_MUL_MAT_ID && t->src[1] == glu && ggml_cuda_mmb_supported_mmid(t->src[0], t->src[1], t->src[2], t)) continue; + if (t->op == GGML_OP_MUL_MAT_ID && t->src[1] == glu && ggml_cuda_mmb_supported_mmid(*cuda_ctx, t->src[0], t->src[1], t->src[2], t)) continue; ok = false; } if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, glu)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, glu); @@ -7491,9 +7491,10 @@ static ggml_backend_feature * ggml_backend_cuda_get_features(ggml_backend_reg_t GGML_UNUSED(reg); } -// enable the BF16 WMMA matmul path (mmb); the llama layer calls this per model, by architecture -static void ggml_backend_cuda_set_mmb_enabled(bool enable) { - ggml_cuda_mmb_set_opt_in(enable); +// opt this backend context in to the BF16 WMMA matmul path (mmb); call before the first graph +static void ggml_backend_cuda_set_mmb_enabled(ggml_backend_t backend, bool enable) { + GGML_ASSERT(ggml_backend_is_cuda(backend)); + ((ggml_backend_cuda_context *) backend->context)->mmb_opt_in = enable; } static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, const char * name) { diff --git a/ggml/src/ggml-cuda/mmb.cu b/ggml/src/ggml-cuda/mmb.cu index b0f5cb5da90a..9233374fe4ca 100644 --- a/ggml/src/ggml-cuda/mmb.cu +++ b/ggml/src/ggml-cuda/mmb.cu @@ -1,6 +1,4 @@ #include "mmb.cuh" - -#include <atomic> #include "unary.cuh" #include <unordered_map> #include <map> @@ -705,19 +703,10 @@ static const uint16_t * mmb_shadow_lookup(ggml_backend_cuda_context & ctx, const auto it = mmb_state(ctx).shadow.find(w->data); return it == mmb_state(ctx).shadow.end() ? nullptr : it->second; } -// The BF16 WMMA kernels are RDNA3.5 (gfx1151) work, and their tiles and fusions are tuned for the -// qwen4exp shapes. On other architectures they take MUL_MATs away from MMQ and lose: dense qwen35 -// prefill measured 3.4-3.6x slower on gfx1151 at every ubatch from 512 to 16384, and MoE 1.1-1.2x. -// So the llama layer opts a model in by arch (ggml_backend_cuda_set_mmb_enabled); default is off. -// A process that loads several models shares this flag: the last load wins. -static std::atomic<bool> g_mmb_opt_in{false}; - -bool mmb_enabled() { - if (!g_mmb_opt_in.load(std::memory_order_relaxed)) { - return false; - } - const int id = ggml_cuda_get_device(); - return GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[id].cc); +// RDNA3.5 (gfx1151) only, tuned for qwen4exp shapes. On gfx1151 (ROCm 7.2.1) it lost to MMQ on other archs: dense qwen35 prefill 3.4-3.9x slower, MoE 14-28% (PR #75). +// So each backend context opts in by model arch. Drop the opt-in when mmb matches MMQ on those archs. +bool mmb_enabled(const ggml_backend_cuda_context & ctx) { + return ctx.mmb_opt_in && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[ctx.device].cc); } int mmb_min_t() { return 512; } int mmb_f32split_mode(){ return 2; } @@ -732,11 +721,6 @@ bool mmb_glu() { return true; } } // namespace -// opted in per model by the llama layer, by architecture (see the note on mmb_enabled above) -void ggml_cuda_mmb_set_opt_in(bool enable) { - g_mmb_opt_in.store(enable, std::memory_order_relaxed); -} - const uint16_t * ggml_cuda_mmb_cache_lookup(ggml_backend_cuda_context & ctx, const ggml_tensor * t) { const ggml_tensor * root = mmb_root(t); for (auto & work : mmb_state(ctx).streams) { @@ -776,12 +760,12 @@ void ggml_cuda_mmb_release_all(ggml_backend_cuda_context & ctx) { ctx.mmb = nullptr; } uint16_t * ggml_cuda_mmb_cache_reserve(ggml_backend_cuda_context & ctx, const ggml_tensor * t, size_t n) { - if (!mmb_enabled() || ggml_nrows(t) < mmb_min_t()) return nullptr; + if (!mmb_enabled(ctx) || ggml_nrows(t) < mmb_min_t()) return nullptr; return ggml_cuda_mmb_slot_reserve(ctx, 0, t, n); } -bool ggml_cuda_mmb_supported_mm(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst) { - if (!mmb_enabled()) return false; +bool ggml_cuda_mmb_supported_mm(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst) { + if (!mmb_enabled(ctx)) return false; const bool quant = mmb_quant_type(src0->type); const bool bf16w = src0->type == GGML_TYPE_BF16 && mmb_bf16w(); const bool f32w = src0->type == GGML_TYPE_F32 && mmb_f32split(); @@ -796,8 +780,8 @@ bool ggml_cuda_mmb_supported_mm(const ggml_tensor * src0, const ggml_tensor * sr return ggml_nrows(dst) == T; } -bool ggml_cuda_mmb_supported_mmid(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst) { - if (!mmb_enabled()) return false; +bool ggml_cuda_mmb_supported_mmid(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst) { + if (!mmb_enabled(ctx)) return false; if (!mmb_quant_type(src0->type) || src1->type != GGML_TYPE_F32 || dst->type != GGML_TYPE_F32 || ids->type != GGML_TYPE_I32) return false; if (!ggml_is_contiguous(src0) || !ggml_is_contiguous(src1) || !ggml_is_contiguous(dst)) return false; const int64_t K = src0->ne[0], M = src0->ne[1], E = src0->ne[2]; @@ -927,8 +911,8 @@ void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor CUDA_CHECK(cudaGetLastError()); } -bool ggml_cuda_mmb_supported_glu(const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * glu) { - if (!mmb_enabled() || !mmb_glu() || !gw || !uw || !src1 || !ids || !glu) return false; +bool ggml_cuda_mmb_supported_glu(ggml_backend_cuda_context & ctx, const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * glu) { + if (!mmb_enabled(ctx) || !mmb_glu() || !gw || !uw || !src1 || !ids || !glu) return false; if (!mmb_quant_type(gw->type) || uw->type != gw->type) return false; if (!ggml_are_same_shape(gw, uw) || gw->nb[1] != uw->nb[1] || gw->nb[2] != uw->nb[2]) return false; if (glu->op != GGML_OP_GLU || ggml_get_glu_op(glu) != GGML_GLU_OP_SWIGLU || ggml_get_op_params_i32(glu, 1) != 0) return false; @@ -936,7 +920,7 @@ bool ggml_cuda_mmb_supported_glu(const ggml_tensor * gw, const ggml_tensor * uw, if (glu->src[0]->op != GGML_OP_MUL_MAT_ID || glu->src[1]->op != GGML_OP_MUL_MAT_ID) return false; if (glu->src[0]->src[0] != gw || glu->src[1]->src[0] != uw || glu->src[0]->src[1] != src1 || glu->src[1]->src[1] != src1 || glu->src[0]->src[2] != ids || glu->src[1]->src[2] != ids) return false; if (ggml_nelements(glu) != ggml_nelements(glu->src[0]) || glu->ne[0] != gw->ne[1]) return false; - return ggml_cuda_mmb_supported_mmid(gw, src1, ids, glu->src[0]) && ggml_cuda_mmb_supported_mmid(uw, src1, ids, glu->src[1]); + return ggml_cuda_mmb_supported_mmid(ctx, gw, src1, ids, glu->src[0]) && ggml_cuda_mmb_supported_mmid(ctx, uw, src1, ids, glu->src[1]); } void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * glu) { diff --git a/ggml/src/ggml-cuda/mmb.cuh b/ggml/src/ggml-cuda/mmb.cuh index eada6ce2bf45..d82297ddfe42 100644 --- a/ggml/src/ggml-cuda/mmb.cuh +++ b/ggml/src/ggml-cuda/mmb.cuh @@ -1,10 +1,9 @@ #pragma once #include "common.cuh" // Quantized-weight BF16 WMMA GEMM on gfx1151, from 512 tokens up. -// opted in per model by the llama layer (see ggml_backend_cuda_set_mmb_enabled): tuned for qwen4exp -void ggml_cuda_mmb_set_opt_in(bool enable); -bool ggml_cuda_mmb_supported_mm (const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst); -bool ggml_cuda_mmb_supported_mmid(const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst); +// Off unless the backend context opts in (ggml_backend_cuda_set_mmb_enabled). +bool ggml_cuda_mmb_supported_mm (ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst); +bool ggml_cuda_mmb_supported_mmid(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst); void ggml_cuda_mul_mat_mmb (ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst); void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst); void ggml_cuda_mmb_begin_graph(ggml_backend_cuda_context & ctx); @@ -23,7 +22,7 @@ bool ggml_cuda_mmb_down16(); bool ggml_cuda_mmb_res16(); bool ggml_cuda_mmb_blk16(); bool ggml_cuda_hc_gate_mix(ggml_backend_cuda_context & ctx, const ggml_tensor * w, const ggml_tensor * lo, const ggml_tensor * xn, ggml_tensor * dst, int hc, float scale, float bias); -bool ggml_cuda_mmb_supported_glu(const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * glu); +bool ggml_cuda_mmb_supported_glu(ggml_backend_cuda_context & ctx, const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * glu); void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_tensor * gw, const ggml_tensor * uw, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * glu); void ggml_cuda_mmb_shadow_prepare(ggml_backend_cuda_context & ctx, const ggml_tensor * w); void ggml_cuda_mmb_release_all(ggml_backend_cuda_context & ctx); diff --git a/src/llama-context.cpp b/src/llama-context.cpp index ccfd52175747..abe70376cd31 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -375,6 +375,16 @@ llama_context::llama_context( } } + // the CUDA/HIP BF16 WMMA matmul path (mmb) is tuned for qwen4exp: other archs keep MMQ + for (auto & backend : backends) { + ggml_backend_dev_t dev = ggml_backend_get_device(backend.get()); + ggml_backend_reg_t reg = dev ? ggml_backend_dev_backend_reg(dev) : nullptr; + auto * set_mmb_fn = reg ? (void (*)(ggml_backend_t, bool)) ggml_backend_reg_get_proc_address(reg, "ggml_backend_cuda_set_mmb_enabled") : nullptr; + if (set_mmb_fn) { + set_mmb_fn(backend.get(), model.arch == LLM_ARCH_QWEN4EXP); + } + } + llama_set_abort_callback(this, params.abort_callback, params.abort_callback_data); // graph outputs buffer diff --git a/src/llama.cpp b/src/llama.cpp index c3683237532f..ad8e443882ad 100644 --- a/src/llama.cpp +++ b/src/llama.cpp @@ -344,17 +344,6 @@ static std::pair<int, llama_model *> llama_model_load(struct gguf_context * meta model->hparams.vocab_only = params.vocab_only; model->hparams.no_alloc = params.no_alloc; - // The CUDA/HIP BF16 WMMA matmul path is tuned for the qwen4exp shapes; on other architectures it - // takes MUL_MATs away from MMQ and loses (dense qwen35 prefill measured 3.4x slower on gfx1151). - // Opt in by architecture rather than by device. - for (size_t i = 0; i < ggml_backend_reg_count(); ++i) { - auto * set_mmb_fn = (void (*)(bool)) ggml_backend_reg_get_proc_address( - ggml_backend_reg_get(i), "ggml_backend_cuda_set_mmb_enabled"); - if (set_mmb_fn) { - set_mmb_fn(model->arch == LLM_ARCH_QWEN4EXP); - } - } - try { model->load_hparams(ml); } catch(const std::exception & e) { diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index e86f9a3fc72f..79d37d358de9 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -13637,6 +13637,15 @@ static bool run_fa_vec_slice(ggml_backend_t backend, ggml_backend_t backend_cpu, return n_fail == 0; } +// The CUDA/HIP BF16 WMMA matmul path (mmb) is off unless a context opts in (llama does it for qwen4exp). Turn it on so the tests cover it. +static void enable_mmb(ggml_backend_t backend) { + ggml_backend_reg_t reg = ggml_backend_dev_backend_reg(ggml_backend_get_device(backend)); + auto set_mmb = (void (*)(ggml_backend_t, bool)) ggml_backend_reg_get_proc_address(reg, "ggml_backend_cuda_set_mmb_enabled"); + if (set_mmb) { + set_mmb(backend, true); + } +} + // Target and draft contexts must not share mutable MMB scratch, including during graph replay and teardown. static bool run_mmb_context_test(ggml_backend_dev_t dev, const char * op_names_filter, printer * output_printer) { if (!op_names_filter_selects(op_names_filter, "MMB_CONTEXT") || @@ -13656,6 +13665,7 @@ static bool run_mmb_context_test(ggml_backend_dev_t dev, const char * op_names_f context(ggml_backend_dev_t dev, int tokens) : backend(ggml_backend_dev_init(dev, nullptr)), cpu(ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr)), test(tokens, GGML_TYPE_Q4_0) { tensors.reset(ggml_init({ggml_tensor_overhead()*128 + ggml_graph_overhead(), nullptr, true})); GGML_ASSERT(backend && cpu && tensors); + enable_mmb(backend.get()); test.gf = ggml_new_graph(tensors.get()); output = test.build_graph(tensors.get()); for (ggml_tensor * t = ggml_get_first_tensor(tensors.get()); t; t = ggml_get_next_tensor(tensors.get(), t)) { @@ -13852,6 +13862,7 @@ static bool test_backend(ggml_backend_t backend, ggml_backend_dev_t dev, test_mo if (b == NULL) { return; } + enable_mmb(b.get()); ggml_backend_ptr b_cpu(ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, NULL)); if (b_cpu == NULL) { @@ -14159,6 +14170,7 @@ int main(int argc, char ** argv) { ggml_backend_ptr backend(ggml_backend_dev_init(dev, NULL)); GGML_ASSERT(backend != NULL); + enable_mmb(backend.get()); ggml_backend_reg_t reg = ggml_backend_dev_backend_reg(dev); auto ggml_backend_set_n_threads_fn = (ggml_backend_set_n_threads_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_set_n_threads"); From 9bc06f5f7c0072cd31d421e2981ccbe895e75d6d Mon Sep 17 00:00:00 2001 From: delgon <delgon95@gmail.com> Date: Wed, 23 Sep 2026 19:38:11 +0200 Subject: [PATCH 183/212] qsa : take 1..512 queries in the per-query decode kernel qsa_decode was limited to 8 queries and qsa_prefill starts at 128, so ubatches of 9..127 tokens could not use the maskless block path, and 128..512-token ubatches paid qsa_prefill's pack of the whole K/V cache per layer (O(n_kv), about 45 ms per ubatch at depth 131072). The decode kernel has no per-query limit (grid y = query); give it a key range per split (chunk) that grows with the query count, so the grid and the partial buffer stay near their 8-query size, and let it take up to 512 queries. qsa_prefill keeps larger ubatches. Up to 8 queries the kernels run exactly as before. Tests: decode cases up to 512 queries (fp64 oracle), maskless decode (WMMA and SIMT, interleaved and not), qsa_prefill cases above 512 queries (poison, 2560 ids, maskless). Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- ggml/src/ggml-cuda/qsa-decode-wmma.cuh | 6 ++-- ggml/src/ggml-cuda/qsa-decode.cu | 19 ++++++---- ggml/src/ggml-cuda/qsa-decode.cuh | 3 ++ tests/test-backend-ops.cpp | 50 ++++++++++++++++++++------ 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/ggml/src/ggml-cuda/qsa-decode-wmma.cuh b/ggml/src/ggml-cuda/qsa-decode-wmma.cuh index fd0fc859197a..63af09d3a568 100644 --- a/ggml/src/ggml-cuda/qsa-decode-wmma.cuh +++ b/ggml/src/ggml-cuda/qsa-decode-wmma.cuh @@ -6,7 +6,7 @@ typedef float qd_float8 __attribute__((ext_vector_type(8))); static __global__ __launch_bounds__(256) void qsa_decode_wmma_partial( const char * q, const char * k, const char * v, const char * mask, const char * ids, size_t q1, size_t q2, size_t k1, size_t k2, size_t v1, size_t v2, size_t m1, size_t i1, - int nk, int ns, int nh, int splits, float scale, float * partial) { + int nk, int ns, int nh, int splits, int chunk, float scale, float * partial) { #if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) NO_DEVICE_CODE; #else @@ -36,8 +36,8 @@ static __global__ __launch_bounds__(256) void qsa_decode_wmma_partial( __syncthreads(); qd_float8 output[2] = {}; float maximum = -INFINITY, normalizer = 0.0f; - const int end = min(ns, split*64+64); - for (int start = split*64; start < end; start += 16) { + const int begin = split*chunk, end = min(ns, begin+chunk); + for (int start = begin; start < end; start += 16) { int keys[16]; #pragma unroll for (int j = 0; j < 16; ++j) { diff --git a/ggml/src/ggml-cuda/qsa-decode.cu b/ggml/src/ggml-cuda/qsa-decode.cu index e0a7f7f62420..ba83164230c4 100644 --- a/ggml/src/ggml-cuda/qsa-decode.cu +++ b/ggml/src/ggml-cuda/qsa-decode.cu @@ -5,7 +5,7 @@ static __global__ __launch_bounds__(128) void qsa_decode_partial( const char * q, const char * k, const char * v, const char * mask, const char * ids, size_t q1, size_t q2, size_t k1, size_t k2, size_t v1, size_t v2, size_t m1, size_t i1, - int nk, int ns, int nh, int ratio, int splits, float scale, float * partial) { + int nk, int ns, int nh, int ratio, int splits, int chunk, float scale, float * partial) { const int lane = threadIdx.x % 32, warp = threadIdx.x / 32; const int head = blockIdx.x, query = blockIdx.y, split = blockIdx.z; const int kv_head = head / ratio; @@ -16,7 +16,7 @@ static __global__ __launch_bounds__(128) void qsa_decode_partial( #pragma unroll for (int d = 0; d < 8; ++d) { qv[d] = qr[lane + d*32]; } float mx = -INFINITY, sum = 0.0f; - const int begin = split*128, end = min(ns, begin + 128); + const int begin = split*chunk, end = min(ns, begin + chunk); for (int j = begin + warp; j < end; j += 4) { const int key = ir[j]; if (key < 0 || key >= nk) { continue; } @@ -88,7 +88,7 @@ bool ggml_cuda_flash_attn_ext_qsa_decode_supported(ggml_backend_cuda_context & c memcpy(&cap, (const char *) dst->op_params + 8, 4); if (bias != 0.0f || cap != 0.0f || q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || v->type != GGML_TYPE_F16 || dst->type != GGML_TYPE_F32 || ids->type != GGML_TYPE_I32 || - q->ne[0] != 256 || k->ne[0] != 256 || v->ne[0] != 256 || q->ne[1] < 1 || q->ne[1] > 8 || + q->ne[0] != 256 || k->ne[0] != 256 || v->ne[0] != 256 || q->ne[1] < 1 || q->ne[1] > QSA_DECODE_MAX_QUERIES || k->ne[1] < 1 || k->ne[1] > 262144 || v->ne[1] != k->ne[1] || k->ne[2] < 1 || q->ne[2] % k->ne[2] || v->ne[2] != k->ne[2] || q->ne[3] != 1 || k->ne[3] != 1 || v->ne[3] != 1 || @@ -127,7 +127,14 @@ void ggml_cuda_flash_attn_ext_qsa_decode(ggml_backend_cuda_context & ctx, ggml_t const bool wmma = q->ne[2] == 12*k->ne[2] && k->nb[1]%16 == 0 && k->nb[2]%16 == 0 && uintptr_t(k->data)%16 == 0; const int ns = ids->ne[0], nq = q->ne[1], nh = q->ne[2], step = wmma ? 64 : 128; - const int splits = (ns + step - 1)/step; + // up to 8 queries every split covers `step` keys; with more queries the splits get longer so the grid stays + // near its 8-query size and the partial buffer stays small (33 splits x 512 queries would need ~420 MB) + int chunk = step; + if (nq > 8) { + const int target = std::max(1, (8*((ns + step - 1)/step) + nq - 1)/nq); + chunk = ((ns + target - 1)/target + 15)/16*16; + } + const int splits = (ns + chunk - 1)/chunk; float scale; memcpy(&scale, dst->op_params, sizeof(scale)); ggml_cuda_pool_alloc<float> partial(ctx.pool(), size_t(nq)*nh*splits*258); @@ -136,13 +143,13 @@ void ggml_cuda_flash_attn_ext_qsa_decode(ggml_backend_cuda_context & ctx, ggml_t (const char *) q->data, (const char *) k->data, (const char *) v->data, m ? (const char *) m->data : nullptr, (const char *) unique_ids.get(), q->nb[1], q->nb[2], k->nb[1], k->nb[2], v->nb[1], v->nb[2], m ? m->nb[1] : 0, size_t(ns)*sizeof(int), - k->ne[1], ns, nh, splits, scale, partial.get()); + k->ne[1], ns, nh, splits, chunk, scale, partial.get()); } else { qsa_decode_partial<<<dim3(nh, nq, splits), 128, 0, ctx.stream()>>>( (const char *) q->data, (const char *) k->data, (const char *) v->data, m ? (const char *) m->data : nullptr, (const char *) unique_ids.get(), q->nb[1], q->nb[2], k->nb[1], k->nb[2], v->nb[1], v->nb[2], m ? m->nb[1] : 0, size_t(ns)*sizeof(int), - k->ne[1], ns, nh, nh/k->ne[2], splits, scale, partial.get()); + k->ne[1], ns, nh, nh/k->ne[2], splits, chunk, scale, partial.get()); } qsa_decode_merge<<<nq*nh, 256, 0, ctx.stream()>>>(partial.get(), (float *) dst->data, splits); CUDA_CHECK(cudaGetLastError()); diff --git a/ggml/src/ggml-cuda/qsa-decode.cuh b/ggml/src/ggml-cuda/qsa-decode.cuh index f032adb52cc3..1c256292a9e6 100644 --- a/ggml/src/ggml-cuda/qsa-decode.cuh +++ b/ggml/src/ggml-cuda/qsa-decode.cuh @@ -1,4 +1,7 @@ #pragma once #include "common.cuh" +// the per-query decode kernels take 1..512 queries; larger ubatches go to qsa_prefill. Up to 512 queries the +// per-query gathers cost less than qsa_prefill's pack of the whole K/V cache, which grows with the depth +#define QSA_DECODE_MAX_QUERIES 512 bool ggml_cuda_flash_attn_ext_qsa_decode_supported(ggml_backend_cuda_context & ctx, const ggml_tensor * dst); void ggml_cuda_flash_attn_ext_qsa_decode(ggml_backend_cuda_context & ctx, ggml_tensor * dst); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 0fc349540150..18ece03b53b3 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -8965,13 +8965,13 @@ struct test_qsa_prefill : public test_case { struct test_qsa_decode : public test_qsa_prefill { const bool duplicates; - test_qsa_decode(int q, int keys, int selected, bool poison=false, int ratio=12, bool duplicates=false) - : test_qsa_prefill(q,keys,selected,true,poison,1,ratio), duplicates(duplicates) {} + test_qsa_decode(int q, int keys, int selected, bool poison=false, int ratio=12, bool duplicates=false, bool interleaved=true) + : test_qsa_prefill(q,keys,selected,interleaved,poison,1,ratio), duplicates(duplicates) {} std::string op_desc(ggml_tensor *) override { return "QSA_DECODE"; } std::string vars() override { return test_qsa_prefill::vars()+",duplicates="+std::to_string(duplicates); } double err(const float * actual, const float * cpu, size_t n) override { const double agreement=nmse(actual,cpu,n); - if (queries>8 || selected>2560) return agreement; + if (queries>512 || selected>2560) return agreement; // 512 = QSA_DECODE_MAX_QUERIES (ggml-cuda/qsa-decode.cuh) const auto qv=tensor_to_float(q), kv=tensor_to_float(k), vv=tensor_to_float(v), mv=tensor_to_float(mask); std::vector<float> reference(n); for (int query=0;query<queries;++query) { @@ -9016,15 +9016,15 @@ struct test_qsa_decode : public test_qsa_prefill { // complete-block selection graph does. Both the backend and the CPU result are checked against a host FP64 oracle // over the listed cells. struct test_qsa_prefill_maskless : public test_qsa_prefill { - test_qsa_prefill_maskless(int queries, int keys, int selected, int ratio=12) - : test_qsa_prefill(queries,keys,selected,true,false,1,ratio) {} + test_qsa_prefill_maskless(int queries, int keys, int selected, int ratio=12, bool interleaved=true) + : test_qsa_prefill(queries,keys,selected,interleaved,false,1,ratio) {} std::string op_desc(ggml_tensor *) override { return "QSA_PREFILL_MASKLESS"; } ggml_tensor * build_graph(ggml_context * ctx) override { q = ggml_new_tensor_4d(ctx,GGML_TYPE_F32,dim,2*ratio,queries,streams); q = ggml_permute(ctx,q,0,2,1,3); - k = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,dim,2,keys,streams); - v = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,dim,2,keys,streams); - k=ggml_permute(ctx,k,0,2,1,3); v=ggml_permute(ctx,v,0,2,1,3); + k = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,dim,interleaved ? 2 : keys,interleaved ? keys : 2,streams); + v = ggml_new_tensor_4d(ctx,GGML_TYPE_F16,dim,interleaved ? 2 : keys,interleaved ? keys : 2,streams); + if (interleaved) { k=ggml_permute(ctx,k,0,2,1,3); v=ggml_permute(ctx,v,0,2,1,3); } mask = nullptr; ids = ggml_new_tensor_4d(ctx,GGML_TYPE_I32,selected,queries,1,streams); auto * out = ggml_flash_attn_ext(ctx,q,k,v,nullptr,1.0f/sqrtf(dim),0.0f,0.0f); @@ -9079,6 +9079,11 @@ struct test_qsa_prefill_maskless : public test_qsa_prefill { return std::max(gpu_error,cpu_error); } }; +struct test_qsa_decode_maskless : public test_qsa_prefill_maskless { + test_qsa_decode_maskless(int queries, int keys, int selected, int ratio=12, bool interleaved=true) + : test_qsa_prefill_maskless(queries,keys,selected,ratio,interleaved) {} + std::string op_desc(ggml_tensor *) override { return "QSA_DECODE_MASKLESS"; } +}; // Complete-block selection as qwen4exp_select_complete_blocks builds it (512-block budget, ratio 4): the HIP // backend fuses the whole subgraph into one kernel; the CPU computes it node by node. Exact integer comparison. @@ -12873,14 +12878,19 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { for (int q : {1,2,4,8,9}) { for (int k : {1,17,64,65,128,129,2051,2560,2561}) test_cases.emplace_back(new test_qsa_decode(q,4096,k)); } - for (int q : {1,4,8}) { - test_cases.emplace_back(new test_qsa_decode(q,40064,2051)); + for (int q : {16,64,127,128,512}) { + for (int k : {17,2051,2560,2561}) test_cases.emplace_back(new test_qsa_decode(q,4096,k)); + } + for (int q : {1,4,8,16,127}) { + if (q <= 16) test_cases.emplace_back(new test_qsa_decode(q,40064,2051)); test_cases.emplace_back(new test_qsa_decode(q,4096,257,true)); test_cases.emplace_back(new test_qsa_decode(q,4096,257,false,4)); test_cases.emplace_back(new test_qsa_decode(q,4096,2051,false,12,true)); test_cases.emplace_back(new test_qsa_decode(q,4096,257,true,4)); } + // QSA_PREFILL cases with <= 512 queries run on qsa_decode (fattn tries it first); qsa_prefill itself is covered by + // the > 512 cases below and by the multi-stream case (which qsa_decode rejects) for (bool interleaved : {false,true}) { test_cases.emplace_back(new test_qsa_prefill(128,512,17,interleaved)); test_cases.emplace_back(new test_qsa_prefill(129,4096,257,interleaved)); @@ -12891,7 +12901,25 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_qsa_prefill_maskless(128,512,17)); test_cases.emplace_back(new test_qsa_prefill_maskless(129,4096,257)); test_cases.emplace_back(new test_qsa_prefill_maskless(130,4096,2051)); - for (int n_blocks : {512, 700, 2048}) for (int n_query : {1, 3, 130}) test_cases.emplace_back(new test_qsa_expand(n_blocks, n_query)); + for (int q : {1,8,9,16,64,127,128,256,512}) for (int k : {257,2051}) test_cases.emplace_back(new test_qsa_decode_maskless(q,4096,k)); + for (int q : {1,9,64,512}) test_cases.emplace_back(new test_qsa_decode_maskless(q,4096,257,4)); // SIMT kernel (GQA 4) + for (int q : {9,64,127}) { // K/V not interleaved + test_cases.emplace_back(new test_qsa_decode(q,4096,2051,false,12,false,false)); + test_cases.emplace_back(new test_qsa_decode_maskless(q,4096,2051,12,false)); + } + // qsa_prefill takes more than 512 queries (the decode kernel takes the rest) + for (bool interleaved : {false,true}) { + test_cases.emplace_back(new test_qsa_prefill(513,4096,257,interleaved)); + test_cases.emplace_back(new test_qsa_prefill(640,40064,2051,interleaved)); + } + test_cases.emplace_back(new test_qsa_prefill_maskless(513,4096,2051)); + test_cases.emplace_back(new test_qsa_prefill_maskless(640,4096,257)); + test_cases.emplace_back(new test_qsa_prefill_maskless(513,4096,2051,12,false)); + test_cases.emplace_back(new test_qsa_prefill(513,4096,2560)); + test_cases.emplace_back(new test_qsa_prefill(513,4096,2561)); + test_cases.emplace_back(new test_qsa_prefill(513,512,17,true,true)); + test_cases.emplace_back(new test_qsa_prefill(640,4096,257,true,true)); + for (int n_blocks : {512, 700, 2048}) for (int n_query : {1, 3, 9, 64, 127, 130}) test_cases.emplace_back(new test_qsa_expand(n_blocks, n_query)); test_cases.emplace_back(new test_qsa_prefill(128,4096,128,true,false,2)); test_cases.emplace_back(new test_qsa_prefill(128,4096,128,true,false,1,4)); test_cases.emplace_back(new test_qsa_prefill(128,4096,128,true,false,1,12,128)); From b4be8b72e55b60fc7d7cfc3f4807bd08b057b958 Mon Sep 17 00:00:00 2001 From: delgon <delgon95@gmail.com> Date: Wed, 23 Sep 2026 19:38:11 +0200 Subject: [PATCH 184/212] qwen4exp : use complete-block selection for every ubatch size The gate kept ubatches of 9..127 tokens on the masked path: a host-filled kq_mask of n_kv x n, block scores expanded to every cell, a top-k over n_kv columns and dense flash attention over the whole cache. At depth 131072 that path costs 64 ms (9..16 tokens) to 457 ms (127 tokens) more than at depth 0. The decode kernel now takes these ubatches, so drop the size clause; the selection semantics are then the same for every ubatch size. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- src/models/qwen4exp.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index d69be93f8a2e..89723279054d 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -805,17 +805,17 @@ static int64_t qwen4exp_query_strip(int64_t n_tokens, int64_t n_stream) { // Complete-block selection: pick indexer_top_k/ratio whole blocks that lie before the query's own block and // append the query's own partial block as the tail (up to ratio-1 cells), -1 where nothing is visible. The // selected rows then carry the visibility themselves, so attention can run without a mask. Only the selected-key -// attention kernels understand that layout (F16 K/V, head 256, single stream, decode-sized or >= 128-query -// ubatches), so the selection is only used where one of them will take the op. +// attention kernels understand that layout (F16 K/V, head 256, single stream; qsa_decode takes 1..512 queries, +// qsa_prefill larger ubatches). They exist only in the HIP backend for RDNA3.5; the gate does not check the device, so +// on any other backend the resulting maskless op is not taken by a QSA kernel. static bool qwen4exp_use_block_selection(bool blk_bias, int64_t n_stream, int64_t ratio, int64_t n_kv, const llama_ubatch & ubatch, const llama_cparams & cparams, const llama_hparams & hparams, ggml_type type_k, ggml_type type_v) { - const int64_t n_tps = n_stream > 0 ? ubatch.n_tokens/n_stream : 0; return blk_bias && n_stream==1 && ratio>1 && hparams.indexer_top_k%ratio==0 && n_kv>hparams.indexer_top_k+ratio-1 && n_kv<=16777216 && ubatch.token && cparams.flash_attn && cparams.offload_kqv && hparams.f_max_alibi_bias==0.0f && !hparams.attn_soft_cap && hparams.n_embd_head_k()==256 && hparams.n_embd_head_v()==256 && - type_k==GGML_TYPE_F16 && type_v==GGML_TYPE_F16 && (n_tps<=8 || n_tps>=128); + type_k==GGML_TYPE_F16 && type_v==GGML_TYPE_F16; } // Visible-prefix scoring bound. For one sequence with unique non-negative positions the indexer enumerates From 10c2b9e64dab48bbe8f32c07603bf2dbe2230ab6 Mon Sep 17 00:00:00 2001 From: delgon <delgon95@gmail.com> Date: Wed, 23 Sep 2026 19:38:11 +0200 Subject: [PATCH 185/212] qwen4exp : incremental pooled-key update for ubatches up to 127 tokens The indexer's pooled-key cache re-pools only the (n+3)/4+2 blocks a ubatch touches when the ubatch continues the cached prefix, but only for up to 8 tokens; larger ubatches re-pooled every block of the cache per attention layer. Extend it to 127 tokens. With GGML_CUDA_DISABLE_FUSION=1 the logits are byte-identical to the full re-pool at ub 64. It is not extended to 128..512 tokens: there the results changed (perplexity at ub 512) and the cause is not known yet. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- src/llama-memory-hybrid-idx.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index ad8b0f61bc32..4fed10c0f352 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -1063,7 +1063,10 @@ bool llama_memory_hybrid_idx::qsa_prefix_matches(const llama_ubatch & u) const { } bool llama_memory_hybrid_idx::qsa_fast(int il, const llama_ubatch & u) const { - return qsa_prefix_matches(u) && u.n_tokens <= 8 && qsa_keys.at(il) && + // re-pool only the (n+3)/4+2 blocks this ubatch touches instead of every block of the cache. Limited to 127 tokens: + // with 128..512-token ubatches the results were not bit-equal to the full re-pool (perplexity at ub 512 changed on + // gfx1151, 2026-09-23) and the cause is not known yet + return qsa_prefix_matches(u) && u.n_tokens <= 127 && qsa_keys.at(il) && qsa_ready.at(il) >= int64_t(qsa_prefix.previous_size/4) && qsa_prefix.cells.size()/4 >= (u.n_tokens+3)/4+1; } From 4208491026634b4332d625d48e89b14f6351f9e6 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Sat, 26 Sep 2026 12:50:11 +0200 Subject: [PATCH 186/212] tests : add --mmb on|off to test-backend-ops With mmb opted in per backend context, one test-backend-ops run can cover only one of the two matmul paths from 512 tokens on RDNA3.5: mmb (what qwen4exp runs) or MMQ (what every other arch runs). --mmb on, the default, keeps the previous behaviour; --mmb off tests MMQ. MMB_CONTEXT ignores the flag and always runs mmb, since it tests mmb's per-context scratch. Checked on gfx1151, kernel trace of test-backend-ops perf -o MUL_MAT q4_0 m=4096 n=512 k=14336: --mmb on launches 305 mmb_dense_kernel and 153 mmb_cvt_f32_bf16 and no mul_mat_q; --mmb off launches 303 mul_mat_q and 303 quantize_mmq_q8_1 and no mmb kernel. Assisted-by: Claude Opus 5.5 --- tests/test-backend-ops.cpp | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 79d37d358de9..43b6cfa7076b 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -13637,12 +13637,13 @@ static bool run_fa_vec_slice(ggml_backend_t backend, ggml_backend_t backend_cpu, return n_fail == 0; } -// The CUDA/HIP BF16 WMMA matmul path (mmb) is off unless a context opts in (llama does it for qwen4exp). Turn it on so the tests cover it. -static void enable_mmb(ggml_backend_t backend) { +// The CUDA/HIP BF16 WMMA matmul path (mmb) is off unless a context opts in (llama does it for qwen4exp). +// --mmb picks which path the >=512-token MUL_MAT/MUL_MAT_ID cases test: mmb, or MMQ as every other arch runs. +static void set_mmb(ggml_backend_t backend, bool enable) { ggml_backend_reg_t reg = ggml_backend_dev_backend_reg(ggml_backend_get_device(backend)); - auto set_mmb = (void (*)(ggml_backend_t, bool)) ggml_backend_reg_get_proc_address(reg, "ggml_backend_cuda_set_mmb_enabled"); - if (set_mmb) { - set_mmb(backend, true); + auto set_mmb_fn = (void (*)(ggml_backend_t, bool)) ggml_backend_reg_get_proc_address(reg, "ggml_backend_cuda_set_mmb_enabled"); + if (set_mmb_fn) { + set_mmb_fn(backend, enable); } } @@ -13665,7 +13666,7 @@ static bool run_mmb_context_test(ggml_backend_dev_t dev, const char * op_names_f context(ggml_backend_dev_t dev, int tokens) : backend(ggml_backend_dev_init(dev, nullptr)), cpu(ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr)), test(tokens, GGML_TYPE_Q4_0) { tensors.reset(ggml_init({ggml_tensor_overhead()*128 + ggml_graph_overhead(), nullptr, true})); GGML_ASSERT(backend && cpu && tensors); - enable_mmb(backend.get()); + set_mmb(backend.get(), true); // this test is about mmb scratch, so it ignores --mmb test.gf = ggml_new_graph(tensors.get()); output = test.build_graph(tensors.get()); for (ggml_tensor * t = ggml_get_first_tensor(tensors.get()); t; t = ggml_get_next_tensor(tensors.get(), t)) { @@ -13756,7 +13757,7 @@ static bool run_mmb_context_test(ggml_backend_dev_t dev, const char * op_names_f } static bool test_backend(ggml_backend_t backend, ggml_backend_dev_t dev, test_mode mode, const char * op_names_filter, const char * params_filter, - printer * output_printer, const char * test_file_path, int parallel_workers) { + printer * output_printer, const char * test_file_path, int parallel_workers, bool mmb) { auto filter_test_cases = [](std::vector<std::unique_ptr<test_case>> & test_cases, const char * params_filter) { if (params_filter == nullptr) { return; @@ -13862,7 +13863,7 @@ static bool test_backend(ggml_backend_t backend, ggml_backend_dev_t dev, test_mo if (b == NULL) { return; } - enable_mmb(b.get()); + set_mmb(b.get(), mmb); ggml_backend_ptr b_cpu(ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, NULL)); if (b_cpu == NULL) { @@ -14044,7 +14045,7 @@ static void show_test_coverage() { static void usage(char ** argv) { printf("Usage: %s [mode] [-o <op,..>] [-b <backend>] [-p <params regex>] [--output <console|sql|csv>] [--list-ops]", argv[0]); - printf(" [--show-coverage] [--test-file <path>] [-j <n>]\n"); + printf(" [--show-coverage] [--test-file <path>] [-j <n>] [--mmb <on|off>]\n"); printf(" valid modes:\n"); printf(" - test (default, compare with CPU backend for correctness)\n"); printf(" - grad (compare gradients from backpropagation with method of finite differences)\n"); @@ -14057,6 +14058,7 @@ static void usage(char ** argv) { printf(" --show-coverage shows test coverage\n"); printf(" --test-file reads test operators from a test file generated by test-export-graph-ops\n"); printf(" -j <n> runs tests using <n> parallel worker threads (default: 1, test mode only)\n"); + printf(" --mmb on|off: CUDA/HIP matmuls from 512 tokens use the BF16 WMMA path (on, default; qwen4exp) or MMQ (off; other archs)\n"); } int main(int argc, char ** argv) { @@ -14067,6 +14069,7 @@ int main(int argc, char ** argv) { const char * params_filter = nullptr; const char * test_file_path = nullptr; int parallel_workers = 1; + bool mmb = true; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "test") == 0) { @@ -14132,6 +14135,13 @@ int main(int argc, char ** argv) { usage(argv); return 1; } + } else if (strcmp(argv[i], "--mmb") == 0) { + if (i + 1 < argc && (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "off") == 0)) { + mmb = strcmp(argv[++i], "on") == 0; + } else { + usage(argv); + return 1; + } } else { usage(argv); return 1; @@ -14170,7 +14180,7 @@ int main(int argc, char ** argv) { ggml_backend_ptr backend(ggml_backend_dev_init(dev, NULL)); GGML_ASSERT(backend != NULL); - enable_mmb(backend.get()); + set_mmb(backend.get(), mmb); ggml_backend_reg_t reg = ggml_backend_dev_backend_reg(dev); auto ggml_backend_set_n_threads_fn = (ggml_backend_set_n_threads_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_set_n_threads"); @@ -14185,7 +14195,7 @@ int main(int argc, char ** argv) { false, "", ggml_backend_dev_description(dev), total / 1024 / 1024, free / 1024 / 1024, true)); - bool ok = test_backend(backend.get(), dev, mode, op_names_filter, params_filter, output_printer.get(), test_file_path, parallel_workers); + bool ok = test_backend(backend.get(), dev, mode, op_names_filter, params_filter, output_printer.get(), test_file_path, parallel_workers, mmb); if (ok) { n_ok++; From cb7dfec0a02c6f3e540733e4d9cb3e41139199f6 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Sat, 26 Sep 2026 15:44:20 +0200 Subject: [PATCH 187/212] cuda : zero quantized padding without a synchronous memset init_tensor zeroed the padding of quantized tensors with a synchronous cudaMemset. On HIP, a synchronous memset or memcpy fails with hipErrorStreamCaptureImplicit while any stream in the process is capturing a graph, in every capture mode including relaxed, and it invalidates that capture too (CHECK_STREAM_CAPTURING in clr). CUDA only rejects this for blocking streams. test-backend-ops -j <n> allocates tensors on one worker while another captures, so with GGML_HIP_GRAPHS every -j run aborted after a random number of cases ("operation would make the legacy stream depend on a capturing blocking stream" from init_tensor, then "operation failed due to a previous error during capture" from hipStreamEndCapture). Upstream master has the same memset and aborts the same way. Use cudaMemsetAsync on cudaStreamPerThread plus a sync of that stream, as memset_tensor and set_tensor already do. Thread-local capture mode would not help: HIP applies the check in every mode. Checked on gfx1151 (ROCm 7.15 runtime), test-backend-ops -b ROCm0 on halo-box master 0fc7226b7: without the fix -j 8 aborted in 2/2 runs, and passed 29800/29800 with GGML_CUDA_DISABLE_GRAPHS=1; with the fix -j 8 passed 29800/29800 in 5/5 runs with graphs on (about 10.5k captures per run), and serial passed 29800/29800. One other serial run failed only ADD_ADD(type=f16,type_addend=f16,ne=[64,5,4,3]) on NMSE 1.03e-7 > 1e-7; that case fails 5/100 runs without the fix and 3/100 on upstream master too. Assisted-by: Claude Opus 5.5 --- ggml/src/ggml-cuda/ggml-cuda.cu | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 962fbebacfc1..158bb676409a 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -791,7 +791,9 @@ static enum ggml_status ggml_backend_cuda_buffer_init_tensor(ggml_backend_buffer if (padded_size > original_size) { ggml_cuda_set_device(ctx->device); - CUDA_CHECK(cudaMemset((char *)tensor->data + original_size, 0, padded_size - original_size)); + // not cudaMemset: on HIP a synchronous memset invalidates a graph capture running on any other thread + CUDA_CHECK(cudaMemsetAsync((char *) tensor->data + original_size, 0, padded_size - original_size, cudaStreamPerThread)); + CUDA_CHECK(cudaStreamSynchronize(cudaStreamPerThread)); } } return GGML_STATUS_SUCCESS; From ee3fbdc9a56289f8be0950dff0357f51dbf94917 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:07:58 +0200 Subject: [PATCH 188/212] hip: select top-k score ties by increasing column index The non-hipCUB fallback for rows wider than 1024 used an atomic gather for scores equal to the k-th largest. QSA scores often tie at exactly zero, so different schedules selected different blocks and changed long-context results. Use the existing stable-tie gather and cover the 1025/2047/2048/2049-column boundary with exact selected-index checks. Assisted-by: OpenCode (gpt-6-sol) --- ggml/src/ggml-cuda/top-k.cu | 3 ++- tests/test-backend-ops.cpp | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-cuda/top-k.cu b/ggml/src/ggml-cuda/top-k.cu index f03820a569bf..97eea70e13b0 100644 --- a/ggml/src/ggml-cuda/top-k.cu +++ b/ggml/src/ggml-cuda/top-k.cu @@ -326,7 +326,8 @@ void ggml_cuda_op_top_k(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { #else // GGML_CUDA_USE_CUB #if defined(GGML_USE_HIP) if (ncols > 1024) { - top_k_radix_cuda(pool, src0_d, dst_d, ncols, nrows, k, stream); + // Select tied scores by increasing column index: QSA has many exact-zero ties at the cut. + top_k_radix_cuda(pool, src0_d, dst_d, ncols, nrows, k, stream, true); } else { #endif // defined(GGML_USE_HIP) ggml_cuda_pool_alloc<int> temp_dst_alloc(pool, ncols * nrows); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 52438272d5d7..1c5f330e6ec7 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -7532,6 +7532,42 @@ struct test_top_k_extreme : public test_top_k { }; +// HIP without hipCUB uses radix top-k for these widths. When the k-th value ties, +// choosing the first columns must be reproducible across workgroup schedules. +struct test_top_k_stable_ties : public test_top_k { + test_top_k_stable_ties(int cols) : test_top_k(GGML_TYPE_F32, {cols, 2, 1, 1}, 512, true) {} + + std::string vars() override { return test_top_k::vars() + ",stable_zero_cut=1"; } + + void initialize_tensors(ggml_context * ctx) override { + test_top_k::initialize_tensors(ctx); + std::vector<float> scores(ggml_nelements(input), 0.0f); + ggml_backend_tensor_set(input, scores.data(), 0, ggml_nbytes(input)); + } + + double err(const float * a, const float * b, size_t n) override { + GGML_UNUSED(a); + if (n != size_t(ggml_nrows(input) * k)) return 1.0; + for (int64_t r = 0; r < ggml_nrows(input); ++r) { + std::vector<int32_t> picked(k); + for (int i = 0; i < k; ++i) { + const float v = b[r*k + i]; + picked[i] = int32_t(v); + if (v != float(picked[i])) return 1.0; + } + std::sort(picked.begin(), picked.end()); + for (int i = 0; i < k; ++i) if (picked[i] != i) return 1.0; + } + return 0.0; + } + + double max_err(ggml_backend_t backend) override { + const char * reg = ggml_backend_reg_name(ggml_backend_dev_backend_reg(ggml_backend_get_device(backend))); + // Other backends may legitimately choose a different subset of equal-valued columns. + return strcmp(reg, "ROCm") == 0 ? 0.0 : double(k * ne[1]); + } +}; + // qwen4exp QSA indexer top-k fusion: expand per-block scores to cells, add the f16 mask, top-k. struct test_topk_qsa : public test_case { const int64_t n_blocks; @@ -12335,6 +12371,7 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { for (int cols : {1024, 1025}) { for (int rows : {1, 2}) test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, rows, 1, 1}, 512, true)); } + for (int cols : {1025, 2047, 2048, 2049}) test_cases.emplace_back(new test_top_k_stable_ties(cols)); test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, { 1024, 1, 1, 1 }, 1024)); test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, { 2048, 2, 1, 1 }, 1024)); From 2b6b43439c03516f8af5f8b36d4d1db7269160ae Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:11:06 +0200 Subject: [PATCH 189/212] hip: keep topk-moe fusion when outputs alias router logits When the allocator reuses dead logits storage for routing outputs, decline the fusion only if non-logit inputs overlap. Copy contiguous logits to the device pool before the fused kernel when needed; otherwise unrelated buffer placement chooses between fused and unfused routing and changes the model output. Existing TOPK_MOE_LIFETIME scheduler-allocation tests cover the production 512-expert shape and alias layouts. Assisted-by: OpenCode (gpt-6-sol) --- ggml/src/ggml-cuda/ggml-cuda.cu | 38 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 158bb676409a..6564bfc62279 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -3044,7 +3044,8 @@ static bool ggml_cuda_check_fusion_memory_ranges(const ggml_cgraph * cgraph, const int node_count, const int * out_nodes, const int out_count, - const bool is_topk_moe = false) { + const bool is_topk_moe = false, + const bool topk_moe_logits_copied = false) { auto nodes_overlap = [&](const ggml_tensor * a, const ggml_tensor * b) { const int64_t a_start = (int64_t) a->data; const int64_t a_end = a_start + ggml_backend_buft_get_alloc_size(a->buffer->buft, a); @@ -3062,7 +3063,7 @@ static bool ggml_cuda_check_fusion_memory_ranges(const ggml_cgraph * cgraph, bool is_ok = true; // one block reads all logits before it writes, so logits may alias the out nodes const ggml_tensor * logits_may_alias = nullptr; - if (is_topk_moe && ggml_nrows(cgraph->nodes[node_idx]) <= TOPK_MOE_ROWS_PER_BLOCK) { + if (is_topk_moe && (topk_moe_logits_copied || ggml_nrows(cgraph->nodes[node_idx]) <= TOPK_MOE_ROWS_PER_BLOCK)) { logits_may_alias = cgraph->nodes[node_idx]->src[0]; } @@ -3103,6 +3104,31 @@ static bool ggml_cuda_check_fusion_memory_ranges(const ggml_cgraph * cgraph, return is_ok; } +// Runs the fused topk-moe. When the allocator placed weights/ids over the logits (they are dead after the gating +// activation in the unfused graph), the kernel reads a private copy of the logits instead of declining the fusion. +// Otherwise buffer placement picks between fused and unfused routing, which round differently, so the model output +// would change with allocation. +static bool ggml_cuda_try_topk_moe(ggml_backend_cuda_context & ctx, const ggml_cgraph * cgraph, int node_idx, + int node_count, const int * out_nodes, const ggml_tensor * logits, + ggml_tensor * weights, ggml_tensor * ids, const ggml_tensor * clamp, + const ggml_tensor * scale, const ggml_tensor * bias, + const ggml_cuda_topk_moe_args & args) { + if (ggml_cuda_check_fusion_memory_ranges(cgraph, node_idx, node_count, out_nodes, 2, /*is_topk_moe=*/true)) { + ggml_cuda_op_topk_moe(ctx, logits, weights, ids, clamp, scale, bias, args); + return true; + } + if (!ggml_is_contiguous(logits) || + !ggml_cuda_check_fusion_memory_ranges(cgraph, node_idx, node_count, out_nodes, 2, true, true)) { + return false; + } + ggml_cuda_pool_alloc<char> copy(ctx.pool(), ggml_nbytes(logits)); + CUDA_CHECK(cudaMemcpyAsync(copy.get(), logits->data, ggml_nbytes(logits), cudaMemcpyDeviceToDevice, ctx.stream())); + ggml_tensor tmp = *logits; + tmp.data = copy.get(); + ggml_cuda_op_topk_moe(ctx, &tmp, weights, ids, clamp, scale, bias, args); + return true; +} + // match the Qwen3.5 Gated DeltaNet decode chain // concat(conv_state, qkv^T) -> cpy(conv state shift -> cache) -> get_rows(state cache) -> ssm_conv -> silu // -> l2_norm(q) -> l2_norm(k) -> add(alpha, dt_bias) -> softplus -> mul(A) -> sigmoid(beta) @@ -4614,8 +4640,8 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph if (ggml_can_fuse_subgraph(cgraph, i, ops.size(), ops.data(), out_nodes, 2) && ggml_cuda_should_use_topk_moe(node, logits, weights, ids) && - ggml_cuda_check_fusion_memory_ranges(cgraph, i, ops.size(), out_nodes, 2, /*is_topk_moe=*/true)) { - ggml_cuda_op_topk_moe(*cuda_ctx, logits, weights, ids, clamp, scale, bias, args); + ggml_cuda_try_topk_moe(*cuda_ctx, cgraph, i, ops.size(), out_nodes, logits, weights, ids, clamp, + scale, bias, args)) { return ops.size() - 1; } } else if (!args.norm && !args.prob_bias) { @@ -4629,8 +4655,8 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph int out_nodes[2] = { i + 1, i + 5 }; if (ggml_can_fuse_subgraph(cgraph, i, ops.size(), ops.data(), out_nodes, 2) && ggml_cuda_should_use_topk_moe(softmax, logits, weights, ids) && - ggml_cuda_check_fusion_memory_ranges(cgraph, i, ops.size(), out_nodes, 2, /*is_topk_moe=*/true)) { - ggml_cuda_op_topk_moe(*cuda_ctx, logits, weights, ids, clamp, scale, bias, args); + ggml_cuda_try_topk_moe(*cuda_ctx, cgraph, i, ops.size(), out_nodes, logits, weights, ids, clamp, + scale, bias, args)) { return ops.size() - 1; } } From 9959849f48f21d0356f09d579cc6dcb637a19523 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:25:18 +0200 Subject: [PATCH 190/212] qwen4exp: own PLE prefetch inputs in the detached thread The detached prefetch thread captured the model and hparams by reference; a context can release them before prefetch finishes. Hold a shared pointer to the disk table and copy the small immutable hash arrays into the thread. This addresses the detached-thread lifetime hazard without changing row selection or prefetch I/O. The reported dense-model crash in issue #79 occurs before thread launch and is not claimed fixed here. Assisted-by: OpenCode (gpt-6-sol) --- src/models/qwen4exp.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index 22fa72369fd0..e4b99fd7feab 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -1795,7 +1795,14 @@ void qwen4exp_ple_prefetch(const llama_model & model_base, const llama_token * t return; } std::vector<llama_token> toks(tokens, tokens + n_tokens); - std::thread([&pmodel, &hp, toks = std::move(toks), n_gram, n_heads, per_gram, eos]() { + // The prefetch thread may outlive the context and model. Keep the disk table and the + // immutable hash parameters alive instead of capturing references into the model. + auto disk = pmodel.ple_disk; + const auto multipliers = hp.ple_layer_multipliers; + const auto vocab_sizes = hp.ple_head_vocab_sizes; + const auto offsets = hp.ple_head_offsets; + std::thread([disk = std::move(disk), multipliers, vocab_sizes, offsets, + toks = std::move(toks), n_gram, n_heads, per_gram, eos]() { const int64_t n = (int64_t) toks.size(); std::vector<int32_t> idx((size_t) n_heads * n); std::vector<int64_t> ctx(n_gram); @@ -1809,18 +1816,18 @@ void qwen4exp_ple_prefetch(const llama_model & model_base, const llama_token * t ctx[s] = cut ? eos : t; } for (int64_t g = 2; g <= n_gram; ++g) { - uint64_t mixed = (uint64_t) ctx[0] * hp.ple_layer_multipliers[0]; + uint64_t mixed = (uint64_t) ctx[0] * multipliers[0]; for (int64_t j = 1; j < g; ++j) { - mixed ^= (uint64_t) ctx[j] * hp.ple_layer_multipliers[j]; + mixed ^= (uint64_t) ctx[j] * multipliers[j]; } const int64_t base = (g - 2) * per_gram; for (int64_t q = 0; q < per_gram; ++q) { const int64_t h_i = base + q; - idx[(size_t) i * n_heads + h_i] = (int32_t) (mixed % hp.ple_head_vocab_sizes[h_i] + hp.ple_head_offsets[h_i]); + idx[(size_t) i * n_heads + h_i] = (int32_t) (mixed % vocab_sizes[h_i] + offsets[h_i]); } } } - pmodel.ple_disk->prefetch(idx.data(), idx.size()); + disk->prefetch(idx.data(), idx.size()); }).detach(); } From f7ff38a82fe63173f454f94a0df059847b9d871a Mon Sep 17 00:00:00 2001 From: Amine pc2 <harrak.amine1987@gmail.com> Date: Sat, 15 Aug 2026 13:48:55 +0200 Subject: [PATCH 191/212] server : preserve context checkpoints across slot save/restore Append the checkpoints after the packed server_tokens payload added in #26640 and count them in n_written / n_read, so a restored slot can still roll back to a checkpoint instead of re-processing the whole prompt. Forward-port of ggml-org/llama.cpp#26004 (authored there by Tough-Respawn), rebased from 03895887a onto current master; applies clean, no code changes. Assisted-by: Hermes Agent (forward-port, build, hardware validation) --- tools/server/server-context.cpp | 120 +++++++++++++++++++++- tools/server/tests/unit/test_slot_save.py | 77 ++++++++++++++ 2 files changed, 195 insertions(+), 2 deletions(-) diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 589493b9839e..bc1acfd330c7 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -2606,6 +2606,117 @@ struct server_context_impl { cur.pos_max, cur.n_tokens, (float) cur.size() / 1024 / 1024); } + // checkpoints are appended to the slot save file, after the llama state payload + // they cannot be recreated from the final state alone (a recurrent state cannot be rewound) + static constexpr uint32_t SLOT_CKPT_MAGIC = 0x504b4353; // "SCKP" + static constexpr uint32_t SLOT_CKPT_VERSION = 1; + + static bool ckpt_read(std::ifstream & ifs, void * dst, size_t size, size_t & n_read) { + if (!ifs.read((char *) dst, size)) { + return false; + } + n_read += size; + return true; + } + + static bool ckpt_read_buf(std::ifstream & ifs, std::vector<uint8_t> & buf, size_t & n_read) { + uint64_t n = 0; + // 16 GiB cap, in case the size field itself is corrupted + if (!ckpt_read(ifs, &n, sizeof(n), n_read) || n > (1ull << 34)) { + return false; + } + buf.resize(n); + return n == 0 || ckpt_read(ifs, buf.data(), n, n_read); + } + + static void ckpt_write(std::ofstream & ofs, const void * src, size_t size, size_t & n_written) { + ofs.write((const char *) src, size); + n_written += size; + } + + static void ckpt_write_buf(std::ofstream & ofs, const std::vector<uint8_t> & buf, size_t & n_written) { + const uint64_t n = buf.size(); + ckpt_write(ofs, &n, sizeof(n), n_written); + if (n > 0) { + ckpt_write(ofs, buf.data(), n, n_written); + } + } + + size_t save_slot_checkpoints(const std::string & filepath, const server_slot & slot) const { + if (slot.prompt.checkpoints.empty()) { + return 0; + } + std::ofstream ofs(filepath, std::ios::binary | std::ios::app); + if (!ofs) { + SRV_WRN("failed to append context checkpoints to '%s'\n", filepath.c_str()); + return 0; + } + size_t n_written = 0; + const uint32_t magic = SLOT_CKPT_MAGIC; + const uint32_t version = SLOT_CKPT_VERSION; + const uint32_t count = (uint32_t) slot.prompt.checkpoints.size(); + ckpt_write(ofs, &magic, sizeof(magic), n_written); + ckpt_write(ofs, &version, sizeof(version), n_written); + ckpt_write(ofs, &count, sizeof(count), n_written); + for (const auto & cur : slot.prompt.checkpoints) { + ckpt_write(ofs, &cur.n_tokens, sizeof(cur.n_tokens), n_written); + ckpt_write(ofs, &cur.pos_min, sizeof(cur.pos_min), n_written); + ckpt_write(ofs, &cur.pos_max, sizeof(cur.pos_max), n_written); + ckpt_write_buf(ofs, cur.data_tgt, n_written); + ckpt_write_buf(ofs, cur.data_dft, n_written); + ckpt_write_buf(ofs, cur.data_spec, n_written); + } + ofs.flush(); + if (!ofs) { + SRV_WRN("failed to append context checkpoints to '%s' - the appendix is incomplete\n", filepath.c_str()); + return 0; + } + SRV_INF("appended %u context checkpoint(s) (%.3f MiB) to '%s'\n", + count, (float) n_written / 1024 / 1024, filepath.c_str()); + return n_written; + } + + // returns the number of bytes consumed, 0 if there is no usable appendix + size_t load_slot_checkpoints(const std::string & filepath, size_t offset, server_slot & slot) const { + std::ifstream ifs(filepath, std::ios::binary); + if (!ifs || !ifs.seekg(offset)) { + return 0; + } + size_t n_read = 0; + uint32_t magic = 0; + uint32_t version = 0; + uint32_t count = 0; + if (!ckpt_read(ifs, &magic, sizeof(magic), n_read) || magic != SLOT_CKPT_MAGIC) { + return 0; + } + if (!ckpt_read(ifs, &version, sizeof(version), n_read) || version != SLOT_CKPT_VERSION || + !ckpt_read(ifs, &count, sizeof(count), n_read) || count > 1024) { + SRV_WRN("invalid context checkpoint appendix in '%s' - ignored\n", filepath.c_str()); + return 0; + } + std::list<common_prompt_checkpoint> checkpoints; + for (uint32_t i = 0; i < count; ++i) { + common_prompt_checkpoint cur; + cur.id_task = -1; + if (!ckpt_read(ifs, &cur.n_tokens, sizeof(cur.n_tokens), n_read) || + !ckpt_read(ifs, &cur.pos_min, sizeof(cur.pos_min), n_read) || + !ckpt_read(ifs, &cur.pos_max, sizeof(cur.pos_max), n_read) || + !ckpt_read_buf(ifs, cur.data_tgt, n_read) || + !ckpt_read_buf(ifs, cur.data_dft, n_read) || + !ckpt_read_buf(ifs, cur.data_spec, n_read)) { + SRV_WRN("truncated context checkpoint appendix in '%s' - ignored\n", filepath.c_str()); + return 0; + } + checkpoints.push_back(std::move(cur)); + } + while (checkpoints.size() > (size_t) params_base.n_ctx_checkpoints) { + checkpoints.pop_front(); + } + slot.prompt.checkpoints = std::move(checkpoints); + SRV_INF("restored %zu context checkpoint(s) from '%s'\n", slot.prompt.checkpoints.size(), filepath.c_str()); + return n_read; + } + // returns false to decline the task, it is offered again after the decode is done bool process_single_task(server_task && task, bool is_yielding) { // while yielding, an encode / decode is running and only reading the server state is safe @@ -2814,6 +2925,8 @@ struct server_context_impl { break; } + const size_t nwrite_ckpt = save_slot_checkpoints(filepath, *slot); + const int64_t t_end = ggml_time_us(); const double t_save_ms = (t_end - t_start) / 1000.0; @@ -2823,7 +2936,7 @@ struct server_context_impl { res->filename = filename; res->is_save = true; res->n_tokens = slot->prompt.tokens.size(); - res->n_bytes = nwrite; + res->n_bytes = nwrite + nwrite_ckpt; res->t_ms = t_save_ms; queue_results.send(std::move(res)); } break; @@ -2879,6 +2992,9 @@ struct server_context_impl { break; } + // nread is the end offset of the llama state payload within the file + const size_t nread_ckpt = load_slot_checkpoints(filepath, nread, *slot); + const int64_t t_end = ggml_time_us(); const double t_restore_ms = (t_end - t_start) / 1000.0; @@ -2888,7 +3004,7 @@ struct server_context_impl { res->filename = filename; res->is_save = false; res->n_tokens = slot->prompt.tokens.size(); - res->n_bytes = nread; + res->n_bytes = nread + nread_ckpt; res->t_ms = t_restore_ms; queue_results.send(std::move(res)); } break; diff --git a/tools/server/tests/unit/test_slot_save.py b/tools/server/tests/unit/test_slot_save.py index 5eca46cb292d..42a08817d8e6 100644 --- a/tools/server/tests/unit/test_slot_save.py +++ b/tools/server/tests/unit/test_slot_save.py @@ -37,6 +37,8 @@ def test_slot_save_restore(): }) assert res.status_code == 200 assert res.body["n_saved"] == 84 + slot_file = os.path.join(server.slot_save_path, "slot1.bin") + assert res.body["n_written"] == os.path.getsize(slot_file) # Since we have cache, this should only process the last tokens res = server.make_request("POST", "/completion", data={ @@ -54,6 +56,7 @@ def test_slot_save_restore(): }) assert res.status_code == 200 assert res.body["n_restored"] == 84 + assert res.body["n_read"] == os.path.getsize(slot_file) # Since we have cache, slot 0 should only process the last tokens res = server.make_request("POST", "/completion", data={ @@ -546,3 +549,77 @@ def test_slot_restore_media_file_without_mmproj(mmproj_server): assert res.status_code == 200 assert res.body["timings"]["cache_n"] == 0 assert res.body["content"] == content + + +@pytest.fixture +def swa_server(): + swa = ServerPreset.tinygemma3() + swa.slot_save_path = "./tmp" + swa.temperature = 0.0 + swa.cache_ram = 0 + # Keep the first prompt checkpoint before the divergence point. + swa.n_ubatch = 32 + return swa + + +def test_slot_restore_preserves_context_checkpoints(swa_server): + server = swa_server + server.start() + + base = "The quick brown fox jumps over the lazy dog. " * 20 + + res = server.make_request("POST", "/completion", data={ + "prompt": base + "The first ending of this story is a happy one.", + "id_slot": 1, + "cache_prompt": True, + }) + assert res.status_code == 200 + n_full = res.body["timings"]["prompt_n"] + + res = server.make_request("POST", "/completion", data={ + "prompt": base + "But the second ending was different and sad.", + "id_slot": 1, + "cache_prompt": True, + }) + assert res.status_code == 200 + n_live = res.body["timings"]["prompt_n"] + assert n_live < n_full + + res = server.make_request("POST", "/slots/1?action=erase") + assert res.status_code == 200 + + res = server.make_request("POST", "/completion", data={ + "prompt": base + "The first ending of this story is a happy one.", + "id_slot": 1, + "cache_prompt": True, + }) + assert res.status_code == 200 + + res = server.make_request("POST", "/slots/1?action=save", data={ + "filename": "ckpt_slot1.bin", + }) + assert res.status_code == 200 + assert res.body["n_saved"] > 0 + ckpt_file = os.path.join(server.slot_save_path, "ckpt_slot1.bin") + assert res.body["n_written"] == os.path.getsize(ckpt_file) + + res = server.make_request("POST", "/completion", data={ + "prompt": "Unrelated text with no common prefix occupies the slot now.", + "id_slot": 1, + "cache_prompt": True, + }) + assert res.status_code == 200 + + res = server.make_request("POST", "/slots/1?action=restore", data={ + "filename": "ckpt_slot1.bin", + }) + assert res.status_code == 200 + assert res.body["n_read"] == os.path.getsize(ckpt_file) + + res = server.make_request("POST", "/completion", data={ + "prompt": base + "But the second ending was different and sad.", + "id_slot": 1, + "cache_prompt": True, + }) + assert res.status_code == 200 + assert res.body["timings"]["prompt_n"] == n_live From 4024eb1c1a0a129c70a9ec5ce80b3ae9d0992a41 Mon Sep 17 00:00:00 2001 From: Simon Teixidor <simon@flaskpost.me> Date: Sun, 13 Sep 2026 22:02:45 +0200 Subject: [PATCH 192/212] spec : MTP draft vocabulary subset (--spec-draft-mtp-vocab N) With --spec-draft-mtp-vocab N, the MTP context copies the LM head rows of token ids < N plus control and user-defined tokens into a smaller head when it is created. The qwen35 and qwen35moe MTP graphs compute the draft logits over that head and scatter them into a full-vocabulary row of -inf (ggml_fill + ggml_set_rows). Only the draft changes: the draft cannot propose other tokens, verification is unchanged. The subset is built only when token ids follow BPE merge order, so that low ids are the frequent tokens; otherwise the option is ignored with an error. Probe contexts with unallocated weights skip the build. New field llama_context_params::mtp_draft_vocab (0 = off). Assisted-by: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC --- common/arg.cpp | 8 +++ common/common.cpp | 3 +- common/common.h | 3 ++ common/speculative.cpp | 3 +- include/llama.h | 1 + src/llama-context.cpp | 109 +++++++++++++++++++++++++++++++++++++++ src/llama-model.h | 6 +++ src/models/qwen35.cpp | 12 ++++- src/models/qwen35moe.cpp | 12 ++++- 9 files changed, 153 insertions(+), 4 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index 6264125de843..52948e045954 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -4271,6 +4271,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex params.speculative.draft.adaptive = true; } ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_LOOKUP, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_DRAFT_ADAPTIVE")); + add_opt(common_arg( + {"--spec-draft-mtp-vocab"}, "N", + string_format("MTP: compute draft logits over token ids < N plus control tokens only; verification is unchanged " + "(0 = full vocabulary, default: %d)", params.speculative.draft.mtp_vocab), + [](common_params & params, int value) { + params.speculative.draft.mtp_vocab = std::max(0, value); + } + ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI})); add_opt(common_arg( {"--spec-draft-n-min"}, "N", string_format("minimum number of draft tokens to use for speculative decoding (default: %d)", params.speculative.draft.n_min), diff --git a/common/common.cpp b/common/common.cpp index bc9f1a7d2692..9ca280d17bd1 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -1306,7 +1306,8 @@ common_init_result::common_init_result(common_params & params, bool model_only) auto mparams_dft = common_model_params_to_llama(params_dft); auto cparams_dft = common_context_params_to_llama(params_dft); if (spec_mtp) { - cparams_dft.ctx_type = LLAMA_CONTEXT_TYPE_MTP; + cparams_dft.ctx_type = LLAMA_CONTEXT_TYPE_MTP; + cparams_dft.mtp_draft_vocab = params.speculative.draft.mtp_vocab; } cparams_dft.n_rs_seq = 0; diff --git a/common/common.h b/common/common.h index 21b276c6fd93..6410b373b3b5 100644 --- a/common/common.h +++ b/common/common.h @@ -363,6 +363,9 @@ struct common_params_speculative_draft { // size each draft from measured acceptance instead of always drafting n_max bool adaptive = false; + // MTP only: draft over token ids < N plus control tokens (0 = full vocabulary) + int32_t mtp_vocab = 0; + common_cpu_params cpuparams; common_cpu_params cpuparams_batch; diff --git a/common/speculative.cpp b/common/speculative.cpp index 507f5f08f286..2ed46e4f002d 100644 --- a/common/speculative.cpp +++ b/common/speculative.cpp @@ -2676,7 +2676,8 @@ common_speculative_init_result::common_speculative_init_result( auto cparams = common_context_params_to_llama(params); if (spec_mtp) { - cparams.ctx_type = LLAMA_CONTEXT_TYPE_MTP; + cparams.ctx_type = LLAMA_CONTEXT_TYPE_MTP; + cparams.mtp_draft_vocab = params.speculative.draft.mtp_vocab; } // the draft context holds as many tokens per sequence as the target context diff --git a/include/llama.h b/include/llama.h index 24cb4b332c0b..8851c113aaed 100644 --- a/include/llama.h +++ b/include/llama.h @@ -385,6 +385,7 @@ extern "C" { uint32_t n_outputs_max_per_seq; // max outputs per sequence (0 = n_outputs_max) int32_t n_threads; // number of threads to use for generation int32_t n_threads_batch; // number of threads to use for batch processing + int32_t mtp_draft_vocab; // MTP contexts only: draft over token ids < N plus control tokens (0 = full vocabulary) [EXPERIMENTAL] enum llama_context_type ctx_type; // set the context type (e.g. MTP) enum llama_rope_scaling_type rope_scaling_type; // RoPE scaling type, from `enum llama_rope_scaling_type` diff --git a/src/llama-context.cpp b/src/llama-context.cpp index a766b5ab38e3..619539e3e910 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -14,6 +14,9 @@ #include "llama-sampler.h" #include "llama.h" +#include "ggml-alloc.h" + +#include <algorithm> #include <cinttypes> #include <cmath> #include <cstring> @@ -82,6 +85,107 @@ static const llm_fused_op_probe llm_fused_op_dsv4_hc_post_probe = { /*.n_tokens_per_seq =*/ 1, }; +// A token id cutoff selects common tokens only if ids follow BPE merge order (an earlier merge is a more frequent pair). +// Check it: the vocab must be BPE, and ids must increase with the rank of the first merge that makes each token. +static bool mtp_draft_vocab_ids_follow_merge_order(const llama_vocab & vocab, std::string & why) { + if (vocab.get_type() != LLAMA_VOCAB_TYPE_BPE) { + why = "the tokenizer is not BPE"; + return false; + } + const std::vector<std::string> merges = vocab.get_bpe_merges(); + const int n_vocab = (int) vocab.n_tokens(); + if (merges.size() < (size_t) n_vocab / 2) { + why = format("the tokenizer has only %zu merges for %d tokens", merges.size(), n_vocab); + return false; + } + std::vector<uint8_t> seen((size_t) n_vocab, 0); + llama_token last = -1; + size_t n_first = 0; + size_t n_out_of_order = 0; + for (const std::string & m : merges) { + const size_t sp = m.find(' '); + if (sp == std::string::npos) { + continue; + } + const llama_token id = vocab.text_to_token(m.substr(0, sp) + m.substr(sp + 1)); + if (id == LLAMA_TOKEN_NULL || id < 0 || id >= n_vocab || seen[(size_t) id]) { + continue; + } + seen[(size_t) id] = 1; + n_first++; + if (id < last) { + n_out_of_order++; + } + last = std::max(last, id); + } + if (n_first < (size_t) n_vocab / 2 || n_out_of_order > 0) { + why = format("token ids do not follow BPE merge order (%zu of %zu merge-produced tokens out of order)", n_out_of_order, n_first); + return false; + } + return true; +} + +// Copy the LM head rows of token ids < n_keep plus control and user-defined tokens into model.mtp_draft_head. +static void mtp_draft_vocab_build(const llama_model & model, int n_keep) { + const ggml_tensor * out = model.output; + // probe contexts (common_fit_params) use a model with unallocated weights: skip them + if (out == nullptr || out->buffer == nullptr || out->data == nullptr || model.mtp_draft_head != nullptr) { + return; + } + const int n_vocab = (int) model.vocab.n_tokens(); + if (n_keep >= n_vocab) { + return; + } + std::string why; + if (!mtp_draft_vocab_ids_follow_merge_order(model.vocab, why)) { + LLAMA_LOG_ERROR("%s: mtp_draft_vocab = %d ignored: %s\n", __func__, n_keep, why.c_str()); + return; + } + std::vector<int64_t> ids; + ids.reserve(n_vocab); + for (int t = 0; t < n_vocab; ++t) { + const int attr = (int) model.vocab.token_get_attr(t); + if (t < n_keep || (attr & (LLAMA_TOKEN_ATTR_CONTROL | LLAMA_TOKEN_ATTR_USER_DEFINED))) { + ids.push_back(t); + } + } + const int64_t n_sel = (int64_t) ids.size(); + if (n_sel >= out->ne[1]) { + return; + } + const size_t row_bytes = out->nb[1]; + std::vector<uint8_t> host((size_t) n_sel * row_bytes); + for (size_t j = 0; j < ids.size(); ) { + size_t k = j; + while (k + 1 < ids.size() && ids[k + 1] == ids[k] + 1) { + k++; + } + ggml_backend_tensor_get(out, host.data() + j * row_bytes, (size_t) ids[j] * row_bytes, (k - j + 1) * row_bytes); + j = k + 1; + } + + ggml_init_params ip = { 2 * ggml_tensor_overhead(), nullptr, true }; + ggml_context_ptr gctx(ggml_init(ip)); + ggml_tensor * head = ggml_new_tensor_2d(gctx.get(), out->type, out->ne[0], n_sel); + ggml_tensor * tids = ggml_new_tensor_1d(gctx.get(), GGML_TYPE_I64, n_sel); + ggml_set_name(head, "mtp_draft_head"); + ggml_set_name(tids, "mtp_draft_ids"); + ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors_from_buft(gctx.get(), ggml_backend_buffer_get_type(out->buffer)); + if (buf == nullptr) { + LLAMA_LOG_ERROR("%s: mtp_draft_vocab: buffer allocation failed, draft head unchanged\n", __func__); + return; + } + ggml_backend_buffer_set_usage(buf, GGML_BACKEND_BUFFER_USAGE_WEIGHTS); + ggml_backend_tensor_set(head, host.data(), 0, host.size()); + ggml_backend_tensor_set(tids, ids.data(), 0, ids.size() * sizeof(int64_t)); + model.mtp_draft_head = head; + model.mtp_draft_ids = tids; + model.mtp_draft_ctx = std::move(gctx); + model.mtp_draft_buf.reset(buf); + LLAMA_LOG_INFO("%s: mtp_draft_vocab = %d: MTP draft head uses %lld of %lld rows (%.2f MiB of %s)\n", __func__, n_keep, + (long long) n_sel, (long long) out->ne[1], (double) (n_sel * row_bytes) / 1048576.0, ggml_type_name(out->type)); +} + llama_context::llama_context( const llama_model & model, llama_context_params params) : @@ -475,6 +579,10 @@ llama_context::llama_context( LLAMA_LOG_INFO("%s: pipeline parallelism enabled\n", __func__); } + if (cparams.ctx_type == LLAMA_CONTEXT_TYPE_MTP && params.mtp_draft_vocab > 0) { + mtp_draft_vocab_build(model, params.mtp_draft_vocab); + } + sched_reserve(); if (!cparams.flash_attn) { @@ -3673,6 +3781,7 @@ llama_context_params llama_context_default_params() { /*.n_outputs_max_per_seq =*/ 1, /*.n_threads =*/ GGML_DEFAULT_N_THREADS, // TODO: better default /*.n_threads_batch =*/ GGML_DEFAULT_N_THREADS, + /*.mtp_draft_vocab =*/ 0, /*.ctx_type =*/ LLAMA_CONTEXT_TYPE_DEFAULT, /*.rope_scaling_type =*/ LLAMA_ROPE_SCALING_TYPE_UNSPECIFIED, /*.pooling_type =*/ LLAMA_POOLING_TYPE_UNSPECIFIED, diff --git a/src/llama-model.h b/src/llama-model.h index 75643a8523d4..b497970bf39a 100644 --- a/src/llama-model.h +++ b/src/llama-model.h @@ -643,6 +643,12 @@ struct llama_model { struct ggml_tensor * output_s = nullptr; struct ggml_tensor * output_in_s = nullptr; + // MTP draft vocabulary: row subset of `output` and the token id of each row (I64). Built by the first MTP context. + mutable struct ggml_tensor * mtp_draft_head = nullptr; + mutable struct ggml_tensor * mtp_draft_ids = nullptr; + mutable ggml_context_ptr mtp_draft_ctx; + mutable ggml_backend_buffer_ptr mtp_draft_buf; + // NextN/MTP model-level projections struct ggml_tensor * nextn_proj_pre = nullptr; struct ggml_tensor * nextn_proj_post = nullptr; diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp index 746719da2edf..bd929bef9e14 100644 --- a/src/models/qwen35.cpp +++ b/src/models/qwen35.cpp @@ -649,7 +649,17 @@ llama_model_qwen35::graph_mtp::graph_mtp(const llama_model & model, const llm_gr ggml_tensor * head_w = layer.nextn.shared_head_head ? layer.nextn.shared_head_head : model.output; ggml_tensor * head_s = layer.nextn.shared_head_head ? layer.nextn.shared_head_head_s : model.output_s; GGML_ASSERT(head_w && "QWEN35 MTP: missing LM head (nextn.shared_head_head or model.output)"); - cur = build_lora_mm(head_w, cur, head_s); + if (model.mtp_draft_head != nullptr && head_w == model.output && n_outputs == 1) { + // draft over a row subset of the LM head: scatter its logits into a full-vocab row of -inf + const int64_t n_sel = model.mtp_draft_head->ne[1]; + const int64_t n_row = model.output->ne[1]; + ggml_tensor * sub = build_lora_mm(model.mtp_draft_head, cur, head_s); + cur = ggml_fill(ctx0, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 1, n_row), -INFINITY); + cur = ggml_set_rows(ctx0, cur, ggml_reshape_2d(ctx0, sub, 1, n_sel), model.mtp_draft_ids); + cur = ggml_reshape_2d(ctx0, cur, n_row, 1); + } else { + cur = build_lora_mm(head_w, cur, head_s); + } cb(cur, "result_output", -1); res->t_logits = cur; diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp index b6b5283303c6..c60d8ac1ca80 100644 --- a/src/models/qwen35moe.cpp +++ b/src/models/qwen35moe.cpp @@ -739,7 +739,17 @@ llama_model_qwen35moe::graph_mtp::graph_mtp(const llama_model & model, const llm ggml_tensor * head_w = layer.nextn.shared_head_head ? layer.nextn.shared_head_head : model.output; ggml_tensor * head_s = layer.nextn.shared_head_head ? layer.nextn.shared_head_head_s : model.output_s; GGML_ASSERT(head_w && "QWEN35MOE MTP: missing LM head (nextn.shared_head_head or model.output)"); - cur = build_lora_mm(head_w, cur, head_s); + if (model.mtp_draft_head != nullptr && head_w == model.output && n_outputs == 1) { + // draft over a row subset of the LM head: scatter its logits into a full-vocab row of -inf + const int64_t n_sel = model.mtp_draft_head->ne[1]; + const int64_t n_row = model.output->ne[1]; + ggml_tensor * sub = build_lora_mm(model.mtp_draft_head, cur, head_s); + cur = ggml_fill(ctx0, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 1, n_row), -INFINITY); + cur = ggml_set_rows(ctx0, cur, ggml_reshape_2d(ctx0, sub, 1, n_sel), model.mtp_draft_ids); + cur = ggml_reshape_2d(ctx0, cur, n_row, 1); + } else { + cur = build_lora_mm(head_w, cur, head_s); + } cb(cur, "result_output", -1); res->t_logits = cur; From 511bc9c5cadcb0ee80605acfb84be1177e60cc29 Mon Sep 17 00:00:00 2001 From: Simon Teixidor <simon@flaskpost.me> Date: Tue, 15 Sep 2026 18:33:05 +0200 Subject: [PATCH 193/212] spec : make the MTP draft vocabulary subset per context The subset was stored in mutable llama_model state by the first MTP context that asked for one, and the qwen35 and qwen35moe MTP graphs used it whenever it existed. A second context over the same model with mtp_draft_vocab = 0, or a different N, silently drafted over the first context's subset. The subset is now an immutable llama_mtp_draft_vocab obtained with llama_model::mtp_draft_vocab_get(n_keep). The model keeps weak references keyed by (n_keep, buffer type) under a mutex, so contexts that ask for the same N at the same time share one copy, and it is freed with the last context holding it. Each context holds its own reference and passes it to its graphs through llm_graph_params; the MTP graphs use only that one, and graph reuse compares it. llama_cparams::mtp_draft_vocab records the N actually in use (0 when no subset was built: probe contexts with unallocated weights, a tokenizer that fails the merge-order guard, or a model whose MTP block has its own LM head, which now logs a warning instead of building an unused subset). Assisted-by: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC --- src/llama-context.cpp | 111 ++------------------------------- src/llama-context.h | 3 + src/llama-cparams.h | 2 + src/llama-graph.cpp | 1 + src/llama-graph.h | 7 +++ src/llama-model.cpp | 131 +++++++++++++++++++++++++++++++++++++++ src/llama-model.h | 22 +++++-- src/models/qwen35.cpp | 11 ++-- src/models/qwen35moe.cpp | 11 ++-- 9 files changed, 178 insertions(+), 121 deletions(-) diff --git a/src/llama-context.cpp b/src/llama-context.cpp index 619539e3e910..0d289ba03d6b 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -14,9 +14,6 @@ #include "llama-sampler.h" #include "llama.h" -#include "ggml-alloc.h" - -#include <algorithm> #include <cinttypes> #include <cmath> #include <cstring> @@ -85,107 +82,6 @@ static const llm_fused_op_probe llm_fused_op_dsv4_hc_post_probe = { /*.n_tokens_per_seq =*/ 1, }; -// A token id cutoff selects common tokens only if ids follow BPE merge order (an earlier merge is a more frequent pair). -// Check it: the vocab must be BPE, and ids must increase with the rank of the first merge that makes each token. -static bool mtp_draft_vocab_ids_follow_merge_order(const llama_vocab & vocab, std::string & why) { - if (vocab.get_type() != LLAMA_VOCAB_TYPE_BPE) { - why = "the tokenizer is not BPE"; - return false; - } - const std::vector<std::string> merges = vocab.get_bpe_merges(); - const int n_vocab = (int) vocab.n_tokens(); - if (merges.size() < (size_t) n_vocab / 2) { - why = format("the tokenizer has only %zu merges for %d tokens", merges.size(), n_vocab); - return false; - } - std::vector<uint8_t> seen((size_t) n_vocab, 0); - llama_token last = -1; - size_t n_first = 0; - size_t n_out_of_order = 0; - for (const std::string & m : merges) { - const size_t sp = m.find(' '); - if (sp == std::string::npos) { - continue; - } - const llama_token id = vocab.text_to_token(m.substr(0, sp) + m.substr(sp + 1)); - if (id == LLAMA_TOKEN_NULL || id < 0 || id >= n_vocab || seen[(size_t) id]) { - continue; - } - seen[(size_t) id] = 1; - n_first++; - if (id < last) { - n_out_of_order++; - } - last = std::max(last, id); - } - if (n_first < (size_t) n_vocab / 2 || n_out_of_order > 0) { - why = format("token ids do not follow BPE merge order (%zu of %zu merge-produced tokens out of order)", n_out_of_order, n_first); - return false; - } - return true; -} - -// Copy the LM head rows of token ids < n_keep plus control and user-defined tokens into model.mtp_draft_head. -static void mtp_draft_vocab_build(const llama_model & model, int n_keep) { - const ggml_tensor * out = model.output; - // probe contexts (common_fit_params) use a model with unallocated weights: skip them - if (out == nullptr || out->buffer == nullptr || out->data == nullptr || model.mtp_draft_head != nullptr) { - return; - } - const int n_vocab = (int) model.vocab.n_tokens(); - if (n_keep >= n_vocab) { - return; - } - std::string why; - if (!mtp_draft_vocab_ids_follow_merge_order(model.vocab, why)) { - LLAMA_LOG_ERROR("%s: mtp_draft_vocab = %d ignored: %s\n", __func__, n_keep, why.c_str()); - return; - } - std::vector<int64_t> ids; - ids.reserve(n_vocab); - for (int t = 0; t < n_vocab; ++t) { - const int attr = (int) model.vocab.token_get_attr(t); - if (t < n_keep || (attr & (LLAMA_TOKEN_ATTR_CONTROL | LLAMA_TOKEN_ATTR_USER_DEFINED))) { - ids.push_back(t); - } - } - const int64_t n_sel = (int64_t) ids.size(); - if (n_sel >= out->ne[1]) { - return; - } - const size_t row_bytes = out->nb[1]; - std::vector<uint8_t> host((size_t) n_sel * row_bytes); - for (size_t j = 0; j < ids.size(); ) { - size_t k = j; - while (k + 1 < ids.size() && ids[k + 1] == ids[k] + 1) { - k++; - } - ggml_backend_tensor_get(out, host.data() + j * row_bytes, (size_t) ids[j] * row_bytes, (k - j + 1) * row_bytes); - j = k + 1; - } - - ggml_init_params ip = { 2 * ggml_tensor_overhead(), nullptr, true }; - ggml_context_ptr gctx(ggml_init(ip)); - ggml_tensor * head = ggml_new_tensor_2d(gctx.get(), out->type, out->ne[0], n_sel); - ggml_tensor * tids = ggml_new_tensor_1d(gctx.get(), GGML_TYPE_I64, n_sel); - ggml_set_name(head, "mtp_draft_head"); - ggml_set_name(tids, "mtp_draft_ids"); - ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors_from_buft(gctx.get(), ggml_backend_buffer_get_type(out->buffer)); - if (buf == nullptr) { - LLAMA_LOG_ERROR("%s: mtp_draft_vocab: buffer allocation failed, draft head unchanged\n", __func__); - return; - } - ggml_backend_buffer_set_usage(buf, GGML_BACKEND_BUFFER_USAGE_WEIGHTS); - ggml_backend_tensor_set(head, host.data(), 0, host.size()); - ggml_backend_tensor_set(tids, ids.data(), 0, ids.size() * sizeof(int64_t)); - model.mtp_draft_head = head; - model.mtp_draft_ids = tids; - model.mtp_draft_ctx = std::move(gctx); - model.mtp_draft_buf.reset(buf); - LLAMA_LOG_INFO("%s: mtp_draft_vocab = %d: MTP draft head uses %lld of %lld rows (%.2f MiB of %s)\n", __func__, n_keep, - (long long) n_sel, (long long) out->ne[1], (double) (n_sel * row_bytes) / 1048576.0, ggml_type_name(out->type)); -} - llama_context::llama_context( const llama_model & model, llama_context_params params) : @@ -579,8 +475,12 @@ llama_context::llama_context( LLAMA_LOG_INFO("%s: pipeline parallelism enabled\n", __func__); } + // the draft vocabulary subset belongs to this context; contexts that ask for the same N share one copy if (cparams.ctx_type == LLAMA_CONTEXT_TYPE_MTP && params.mtp_draft_vocab > 0) { - mtp_draft_vocab_build(model, params.mtp_draft_vocab); + mtp_draft = model.mtp_draft_vocab_get(params.mtp_draft_vocab); + if (mtp_draft) { + cparams.mtp_draft_vocab = mtp_draft->n_keep; + } } sched_reserve(); @@ -2640,6 +2540,7 @@ llm_graph_params llama_context::graph_params( /*.mctx =*/ mctx, /*.cross =*/ &cross, /*.samplers =*/ sampling.samplers, + /*.mtp_draft =*/ mtp_draft.get(), /*.n_outputs =*/ n_outputs, /*.cb =*/ graph_get_cb(), /*.res =*/ res, diff --git a/src/llama-context.h b/src/llama-context.h index b089e1267aaf..75c148cf6c66 100644 --- a/src/llama-context.h +++ b/src/llama-context.h @@ -287,6 +287,9 @@ struct llama_context { llama_cross cross; // TODO: tmp for handling cross-attention - need something better probably + // MTP draft vocabulary subset used by this context's graphs (nullptr = full vocabulary) + std::shared_ptr<const llama_mtp_draft_vocab> mtp_draft; + llama_memory_ptr memory; // decode output (2-dimensional array: [n_outputs][n_vocab]) diff --git a/src/llama-cparams.h b/src/llama-cparams.h index b592de18c794..e0fe7eee6dc6 100644 --- a/src/llama-cparams.h +++ b/src/llama-cparams.h @@ -21,6 +21,8 @@ struct llama_cparams { int32_t nextn_layer_offset = 0; + int32_t mtp_draft_vocab = 0; // MTP draft vocabulary subset in use by this context (0 = full vocabulary) + float rope_freq_base; float rope_freq_scale; diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index b5fdef95a84e..9d5f5e6241ea 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -1520,6 +1520,7 @@ llm_graph_context::llm_graph_context(const llm_graph_params & params) : mctx (params.mctx), cross (params.cross), samplers (params.samplers), + mtp_draft (params.mtp_draft), cb_func (params.cb), res (params.res), ctx0 (res->get_ctx()), diff --git a/src/llama-graph.h b/src/llama-graph.h index ce68832ce47a..ab9e1664d3ff 100644 --- a/src/llama-graph.h +++ b/src/llama-graph.h @@ -93,6 +93,7 @@ struct llama_cross { }; struct llm_graph_params; +struct llama_mtp_draft_vocab; // // llm_graph_input @@ -789,6 +790,8 @@ struct llm_graph_params { std::map<llama_seq_id, llama_sampler *> samplers; + const llama_mtp_draft_vocab * mtp_draft; // MTP draft vocabulary subset of the context (nullptr = full vocabulary) + static bool samplers_equal( const std::map<llama_seq_id, llama_sampler *> & lhs, const std::map<llama_seq_id, llama_sampler *> & rhs) { @@ -876,6 +879,8 @@ struct llm_graph_params { cparams.embeddings_nextn == other.cparams.embeddings_nextn && cparams.embeddings_nextn_masked == other.cparams.embeddings_nextn_masked && cparams.causal_attn == other.cparams.causal_attn && + cparams.mtp_draft_vocab == other.cparams.mtp_draft_vocab && + mtp_draft == other.mtp_draft && arch == other.arch && gtype == other.gtype && cvec == other.cvec && @@ -1029,6 +1034,8 @@ struct llm_graph_context { std::map<llama_seq_id, llama_sampler *> samplers; + const llama_mtp_draft_vocab * mtp_draft; // nullptr = full vocabulary + const llm_graph_cb & cb_func; llm_graph_result * res; diff --git a/src/llama-model.cpp b/src/llama-model.cpp index 68e844c53ed6..b546491caead 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -23,6 +23,7 @@ #include "models/models.h" #include "ggml.h" +#include "ggml-alloc.h" #include "ggml-cpp.h" #include <algorithm> @@ -33,6 +34,7 @@ #include <cmath> #include <functional> #include <map> +#include <mutex> #include <numeric> #include <regex> #include <sstream> @@ -1180,6 +1182,10 @@ struct llama_model::impl { bool has_tensor_overrides; std::vector<float> tensor_split_owned; + + // MTP draft vocabulary subsets by (n_keep, buffer type), alive while a context holds one + std::mutex mtp_draft_mutex; + std::map<std::pair<int32_t, ggml_backend_buffer_type_t>, std::weak_ptr<const llama_mtp_draft_vocab>> mtp_draft_cache; }; llama_model::llama_model(const llama_model_params & params) : params(params), pimpl(std::make_unique<impl>()) { @@ -2225,6 +2231,131 @@ const ggml_tensor * llama_model::get_tensor(const char * name) const { return it->second; } +// A token id cutoff selects common tokens only if ids follow BPE merge order (an earlier merge is a more frequent pair). +// Check it: the vocab must be BPE, and ids must increase with the rank of the first merge that makes each token. +static bool mtp_draft_vocab_ids_follow_merge_order(const llama_vocab & vocab, std::string & why) { + if (vocab.get_type() != LLAMA_VOCAB_TYPE_BPE) { + why = "the tokenizer is not BPE"; + return false; + } + const std::vector<std::string> merges = vocab.get_bpe_merges(); + const int n_vocab = (int) vocab.n_tokens(); + if (merges.size() < (size_t) n_vocab / 2) { + why = format("the tokenizer has only %zu merges for %d tokens", merges.size(), n_vocab); + return false; + } + std::vector<uint8_t> seen((size_t) n_vocab, 0); + llama_token last = -1; + size_t n_first = 0; + size_t n_out_of_order = 0; + for (const std::string & m : merges) { + const size_t sp = m.find(' '); + if (sp == std::string::npos) { + continue; + } + const llama_token id = vocab.text_to_token(m.substr(0, sp) + m.substr(sp + 1)); + if (id == LLAMA_TOKEN_NULL || id < 0 || id >= n_vocab || seen[(size_t) id]) { + continue; + } + seen[(size_t) id] = 1; + n_first++; + if (id < last) { + n_out_of_order++; + } + last = std::max(last, id); + } + if (n_first < (size_t) n_vocab / 2 || n_out_of_order > 0) { + why = format("token ids do not follow BPE merge order (%zu of %zu merge-produced tokens out of order)", n_out_of_order, n_first); + return false; + } + return true; +} + +std::shared_ptr<const llama_mtp_draft_vocab> llama_model::mtp_draft_vocab_get(int32_t n_keep) const { + const ggml_tensor * out = output; + // probe contexts (common_fit_params) use a model with unallocated weights: no subset + if (n_keep <= 0 || out == nullptr || out->buffer == nullptr || out->data == nullptr) { + return nullptr; + } + // only the qwen35 and qwen35moe MTP graphs use the subset, and only when the MTP block scores with the model LM head + if ((arch != LLM_ARCH_QWEN35 && arch != LLM_ARCH_QWEN35MOE) || hparams.n_layer_nextn == 0 || + layers[hparams.n_layer()].nextn.shared_head_head != nullptr) { + LLAMA_LOG_WARN("%s: mtp_draft_vocab = %d ignored: not supported for this model\n", __func__, n_keep); + return nullptr; + } + const int n_vocab = (int) vocab.n_tokens(); + if (n_keep >= n_vocab) { + return nullptr; + } + + const ggml_backend_buffer_type_t buft = ggml_backend_buffer_get_type(out->buffer); + + // the lock covers the build, so that contexts created at the same time with the same n_keep share one subset + std::lock_guard<std::mutex> lock(pimpl->mtp_draft_mutex); + + auto & cache = pimpl->mtp_draft_cache; + for (auto it = cache.begin(); it != cache.end(); ) { + it = it->second.expired() ? cache.erase(it) : std::next(it); + } + const auto key = std::make_pair(n_keep, buft); + if (auto it = cache.find(key); it != cache.end()) { + return it->second.lock(); + } + + std::string why; + if (!mtp_draft_vocab_ids_follow_merge_order(vocab, why)) { + LLAMA_LOG_ERROR("%s: mtp_draft_vocab = %d ignored: %s\n", __func__, n_keep, why.c_str()); + return nullptr; + } + std::vector<int64_t> ids; + ids.reserve(n_vocab); + for (int t = 0; t < n_vocab; ++t) { + const int attr = (int) vocab.token_get_attr(t); + if (t < n_keep || (attr & (LLAMA_TOKEN_ATTR_CONTROL | LLAMA_TOKEN_ATTR_USER_DEFINED))) { + ids.push_back(t); + } + } + const int64_t n_sel = (int64_t) ids.size(); + if (n_sel >= out->ne[1]) { + return nullptr; + } + const size_t row_bytes = out->nb[1]; + std::vector<uint8_t> host((size_t) n_sel * row_bytes); + for (size_t j = 0; j < ids.size(); ) { + size_t k = j; + while (k + 1 < ids.size() && ids[k + 1] == ids[k] + 1) { + k++; + } + ggml_backend_tensor_get(out, host.data() + j * row_bytes, (size_t) ids[j] * row_bytes, (k - j + 1) * row_bytes); + j = k + 1; + } + + auto res = std::make_shared<llama_mtp_draft_vocab>(); + res->n_keep = n_keep; + + ggml_init_params ip = { 2 * ggml_tensor_overhead(), nullptr, true }; + res->ctx.reset(ggml_init(ip)); + res->head = ggml_new_tensor_2d(res->ctx.get(), out->type, out->ne[0], n_sel); + res->ids = ggml_new_tensor_1d(res->ctx.get(), GGML_TYPE_I64, n_sel); + ggml_format_name(res->head, "mtp_draft_head_%d", n_keep); + ggml_format_name(res->ids, "mtp_draft_ids_%d", n_keep); + res->buf.reset(ggml_backend_alloc_ctx_tensors_from_buft(res->ctx.get(), buft)); + if (!res->buf) { + LLAMA_LOG_ERROR("%s: mtp_draft_vocab = %d: buffer allocation failed, drafting over the full vocabulary\n", __func__, n_keep); + return nullptr; + } + ggml_backend_buffer_set_usage(res->buf.get(), GGML_BACKEND_BUFFER_USAGE_WEIGHTS); + ggml_backend_tensor_set(res->head, host.data(), 0, host.size()); + ggml_backend_tensor_set(res->ids, ids.data(), 0, ids.size() * sizeof(int64_t)); + + LLAMA_LOG_INFO("%s: mtp_draft_vocab = %d: MTP draft head uses %lld of %lld rows (%.2f MiB of %s, %s)\n", __func__, n_keep, + (long long) n_sel, (long long) out->ne[1], (double) (n_sel * row_bytes) / 1048576.0, ggml_type_name(out->type), + ggml_backend_buft_name(buft)); + + cache[key] = res; + return res; +} + float llama_model::get_rope_freq_base (const llama_cparams & cparams, int il) const { return hparams.is_swa(il) ? hparams.rope_freq_base_train_swa : cparams.rope_freq_base; } diff --git a/src/llama-model.h b/src/llama-model.h index b497970bf39a..4278aa47e584 100644 --- a/src/llama-model.h +++ b/src/llama-model.h @@ -613,6 +613,18 @@ struct llama_meta_device_get_split_state_userdata { struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const struct ggml_tensor * tensor, void * userdata); +// MTP draft vocabulary subset (llama_context_params::mtp_draft_vocab): the rows of the LM head for token ids < n_keep +// plus control and user-defined tokens. Immutable once built. Contexts that request the same n_keep share one. +struct llama_mtp_draft_vocab { + int32_t n_keep = 0; + + ggml_tensor * head = nullptr; // [n_embd, n_rows], same type as the LM head + ggml_tensor * ids = nullptr; // [n_rows] I64, token id of each row + + ggml_context_ptr ctx; + ggml_backend_buffer_ptr buf; +}; + struct llama_model { llm_type type = LLM_TYPE_UNKNOWN; llm_arch arch = LLM_ARCH_UNKNOWN; @@ -643,12 +655,6 @@ struct llama_model { struct ggml_tensor * output_s = nullptr; struct ggml_tensor * output_in_s = nullptr; - // MTP draft vocabulary: row subset of `output` and the token id of each row (I64). Built by the first MTP context. - mutable struct ggml_tensor * mtp_draft_head = nullptr; - mutable struct ggml_tensor * mtp_draft_ids = nullptr; - mutable ggml_context_ptr mtp_draft_ctx; - mutable ggml_backend_buffer_ptr mtp_draft_buf; - // NextN/MTP model-level projections struct ggml_tensor * nextn_proj_pre = nullptr; struct ggml_tensor * nextn_proj_post = nullptr; @@ -759,6 +765,10 @@ struct llama_model { const struct ggml_tensor * get_tensor(const char * name) const; + // draft vocabulary subset for mtp_draft_vocab = n_keep, built on first request and freed with the last context + // holding it; nullptr if the subset cannot be used with this model (thread-safe) + std::shared_ptr<const llama_mtp_draft_vocab> mtp_draft_vocab_get(int32_t n_keep) const; + float get_rope_freq_base (const llama_cparams & cparams, int il) const; float get_rope_freq_scale(const llama_cparams & cparams, int il) const; diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp index bd929bef9e14..efeb86e4ed92 100644 --- a/src/models/qwen35.cpp +++ b/src/models/qwen35.cpp @@ -649,13 +649,14 @@ llama_model_qwen35::graph_mtp::graph_mtp(const llama_model & model, const llm_gr ggml_tensor * head_w = layer.nextn.shared_head_head ? layer.nextn.shared_head_head : model.output; ggml_tensor * head_s = layer.nextn.shared_head_head ? layer.nextn.shared_head_head_s : model.output_s; GGML_ASSERT(head_w && "QWEN35 MTP: missing LM head (nextn.shared_head_head or model.output)"); - if (model.mtp_draft_head != nullptr && head_w == model.output && n_outputs == 1) { - // draft over a row subset of the LM head: scatter its logits into a full-vocab row of -inf - const int64_t n_sel = model.mtp_draft_head->ne[1]; + if (mtp_draft != nullptr && head_w == model.output && n_outputs == 1) { + // this context drafts over a row subset of the LM head: scatter its logits into a full-vocab row of -inf + GGML_ASSERT(mtp_draft->n_keep == cparams.mtp_draft_vocab); + const int64_t n_sel = mtp_draft->head->ne[1]; const int64_t n_row = model.output->ne[1]; - ggml_tensor * sub = build_lora_mm(model.mtp_draft_head, cur, head_s); + ggml_tensor * sub = build_lora_mm(mtp_draft->head, cur, head_s); cur = ggml_fill(ctx0, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 1, n_row), -INFINITY); - cur = ggml_set_rows(ctx0, cur, ggml_reshape_2d(ctx0, sub, 1, n_sel), model.mtp_draft_ids); + cur = ggml_set_rows(ctx0, cur, ggml_reshape_2d(ctx0, sub, 1, n_sel), mtp_draft->ids); cur = ggml_reshape_2d(ctx0, cur, n_row, 1); } else { cur = build_lora_mm(head_w, cur, head_s); diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp index c60d8ac1ca80..eabca2bc745e 100644 --- a/src/models/qwen35moe.cpp +++ b/src/models/qwen35moe.cpp @@ -739,13 +739,14 @@ llama_model_qwen35moe::graph_mtp::graph_mtp(const llama_model & model, const llm ggml_tensor * head_w = layer.nextn.shared_head_head ? layer.nextn.shared_head_head : model.output; ggml_tensor * head_s = layer.nextn.shared_head_head ? layer.nextn.shared_head_head_s : model.output_s; GGML_ASSERT(head_w && "QWEN35MOE MTP: missing LM head (nextn.shared_head_head or model.output)"); - if (model.mtp_draft_head != nullptr && head_w == model.output && n_outputs == 1) { - // draft over a row subset of the LM head: scatter its logits into a full-vocab row of -inf - const int64_t n_sel = model.mtp_draft_head->ne[1]; + if (mtp_draft != nullptr && head_w == model.output && n_outputs == 1) { + // this context drafts over a row subset of the LM head: scatter its logits into a full-vocab row of -inf + GGML_ASSERT(mtp_draft->n_keep == cparams.mtp_draft_vocab); + const int64_t n_sel = mtp_draft->head->ne[1]; const int64_t n_row = model.output->ne[1]; - ggml_tensor * sub = build_lora_mm(model.mtp_draft_head, cur, head_s); + ggml_tensor * sub = build_lora_mm(mtp_draft->head, cur, head_s); cur = ggml_fill(ctx0, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 1, n_row), -INFINITY); - cur = ggml_set_rows(ctx0, cur, ggml_reshape_2d(ctx0, sub, 1, n_sel), model.mtp_draft_ids); + cur = ggml_set_rows(ctx0, cur, ggml_reshape_2d(ctx0, sub, 1, n_sel), mtp_draft->ids); cur = ggml_reshape_2d(ctx0, cur, n_row, 1); } else { cur = build_lora_mm(head_w, cur, head_s); From 9fd255c657a40685347e4bfa3b6fd27338d5769b Mon Sep 17 00:00:00 2001 From: Simon Teixidor <simon@flaskpost.me> Date: Tue, 15 Sep 2026 18:29:59 +0200 Subject: [PATCH 194/212] tests : per-context MTP draft vocabulary regression test Contexts over one model with different mtp_draft_vocab settings (N then 0, 0 then N, N1 then N2, the same N twice, two contexts created from two threads at once, a subset requested again after its last context was freed) are decoded interleaved with the same inputs. A context with N > 0 must have -inf exactly outside its own draft vocabulary and match a full-vocabulary context inside it; a context with N = 0 must have finite logits everywhere. Needs a qwen35 or qwen35moe model with an MTP head (-m), so it is built but not registered with ctest. Assisted-by: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC --- tests/CMakeLists.txt | 1 + tests/test-mtp-draft-vocab.cpp | 243 +++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 tests/test-mtp-draft-vocab.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 13e7e5c73706..a2855346e9bf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -151,6 +151,7 @@ endif () llama_build_and_test(test-qsa-prefix.cpp) llama_build_and_test(test-ple-disk.cpp) llama_build(test-recurrent-state-rollback.cpp) +llama_build(test-mtp-draft-vocab.cpp) # needs a qwen35/qwen35moe model with an MTP head: -m MODEL llama_build(test-save-load-state.cpp) if (NOT WIN32 OR NOT BUILD_SHARED_LIBS) diff --git a/tests/test-mtp-draft-vocab.cpp b/tests/test-mtp-draft-vocab.cpp new file mode 100644 index 000000000000..ca514151a524 --- /dev/null +++ b/tests/test-mtp-draft-vocab.cpp @@ -0,0 +1,243 @@ +// Regression test for llama_context_params::mtp_draft_vocab. +// +// The draft vocabulary subset belongs to the MTP context that asked for it. Contexts over the same model with a +// different setting (0 = full vocabulary, or another N) must each draft over their own vocabulary, whatever order +// they were created in and while they are alive at the same time. +// +// Needs a qwen35 or qwen35moe model with an MTP head whose MTP block uses the model LM head, e.g.: +// +// test-mtp-draft-vocab -m Qwen3.8-27B-UD-IQ4_XS.gguf -ngl 99 +// +// A context with N > 0 must produce -inf exactly at the token ids outside {id < N} + control + user-defined tokens, +// and its finite logits must match those of a full-vocabulary context fed the same inputs. A context with N = 0 must +// produce finite logits everywhere. + +#include "arg.h" +#include "common.h" +#include "llama.h" + +#include "../src/llama-ext.h" + +#include <algorithm> +#include <atomic> +#include <clocale> +#include <cmath> +#include <cstdio> +#include <memory> +#include <random> +#include <string> +#include <thread> +#include <vector> + +struct mtp_ctx { + int32_t n_vocab_draft; // requested mtp_draft_vocab + llama_context_ptr ctx; +}; + +static llama_context_ptr make_ctx(llama_model * model, int32_t n_vocab_draft) { + llama_context_params cparams = llama_context_default_params(); + cparams.n_ctx = 256; + cparams.n_batch = 16; + cparams.n_ubatch = 16; + cparams.n_seq_max = 1; + cparams.ctx_type = LLAMA_CONTEXT_TYPE_MTP; + cparams.mtp_draft_vocab = n_vocab_draft; + cparams.n_rs_seq = 0; + llama_context_ptr ctx(llama_init_from_model(model, cparams)); + if (ctx) { + llama_set_embeddings_nextn(ctx.get(), true, /*masked*/ true); + } + return ctx; +} + +// true for the token ids a context with mtp_draft_vocab = n may draft +static std::vector<uint8_t> draft_mask(const llama_vocab * vocab, int32_t n) { + const int32_t n_vocab = llama_vocab_n_tokens(vocab); + std::vector<uint8_t> mask((size_t) n_vocab, 1); + if (n <= 0 || n >= n_vocab) { + return mask; + } + for (int32_t t = 0; t < n_vocab; ++t) { + const int attr = (int) llama_vocab_get_attr(vocab, t); + mask[(size_t) t] = t < n || (attr & (LLAMA_TOKEN_ATTR_CONTROL | LLAMA_TOKEN_ATTR_USER_DEFINED)); + } + return mask; +} + +// decode one token with a pseudo-random hidden state at position pos; return the draft logits +static bool decode_step(llama_model * model, llama_context * ctx, llama_pos pos, std::vector<float> & out) { + const int32_t n_embd = llama_model_n_embd_out(model); + const int32_t n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model)); + + llama_batch batch = llama_batch_init(1, n_embd, 1); + batch.token = (llama_token *) malloc(sizeof(llama_token)); + + std::mt19937 rng(1000 + pos); + std::normal_distribution<float> dist(0.0f, 1.0f); + for (int32_t i = 0; i < n_embd; ++i) { + batch.embd[i] = dist(rng); + } + batch.token[0] = (llama_token) ((pos * 7919 + 13) % 30000); + batch.pos[0] = pos; + batch.n_seq_id[0] = 1; + batch.seq_id[0][0] = 0; + batch.logits[0] = 1; + batch.n_tokens = 1; + + const int rc = llama_decode(ctx, batch); + llama_batch_free(batch); + if (rc != 0) { + fprintf(stderr, "llama_decode failed: %d\n", rc); + return false; + } + const float * logits = llama_get_logits_ith(ctx, -1); + out.assign(logits, logits + n_vocab); + return true; +} + +// check the draft logits of a context against its own mask, and against full-vocabulary logits for the same inputs +static bool check_logits(const std::string & label, const std::vector<float> & logits, const std::vector<uint8_t> & mask, + const std::vector<float> & ref) { + size_t n_cmp = 0; // finite logits compared with the reference + size_t n_bad_inf = 0; // -inf (or non-finite) where the context may draft + size_t n_bad_fin = 0; // finite where the context must not draft + size_t n_bad_val = 0; // finite, but different from the full-vocabulary reference + double max_diff = 0.0; + for (size_t t = 0; t < logits.size(); ++t) { + const float v = logits[t]; + if (mask[t]) { + if (!std::isfinite(v)) { + n_bad_inf++; + } else if (!ref.empty()) { + n_cmp++; + const double d = std::fabs((double) v - (double) ref[t]); + max_diff = std::max(max_diff, d); + if (d > 1e-2 * std::max(1.0, std::fabs((double) ref[t]))) { + n_bad_val++; + } + } + } else if (!(std::isinf(v) && v < 0)) { + n_bad_fin++; + } + } + size_t n_keep = 0; + for (uint8_t m : mask) { + n_keep += m; + } + const size_t i_max = std::max_element(logits.begin(), logits.end()) - logits.begin(); + const bool ok = n_bad_inf == 0 && n_bad_fin == 0 && n_bad_val == 0; + fprintf(stderr, " %-44s %s draftable %zu/%zu, non-finite inside %zu, finite outside %zu, argmax %zu (%.3f), " + "vs full vocab: %zu compared, %zu mismatches, max |diff| %.3g\n", + label.c_str(), ok ? "OK " : "FAIL", n_keep, mask.size(), n_bad_inf, n_bad_fin, i_max, logits[i_max], + n_cmp, n_bad_val, max_diff); + return ok; +} + +// decode the same inputs, interleaved, in every context plus a full-vocabulary reference context, and check each +static bool run_interleaved(llama_model * model, const std::string & name, std::vector<mtp_ctx> & ctxs, int n_steps) { + const llama_vocab * vocab = llama_model_get_vocab(model); + bool ok = true; + for (int step = 0; step < n_steps; ++step) { + std::vector<std::vector<float>> logits(ctxs.size()); + for (size_t i = 0; i < ctxs.size(); ++i) { + if (!decode_step(model, ctxs[i].ctx.get(), step, logits[i])) { + return false; + } + } + const std::vector<float> * ref = nullptr; + for (size_t i = 0; i < ctxs.size(); ++i) { + if (ctxs[i].n_vocab_draft == 0) { + ref = &logits[i]; + } + } + for (size_t i = 0; i < ctxs.size(); ++i) { + const std::string label = name + " step " + std::to_string(step) + " ctx " + std::string(1, char('A' + i)) + + " (N=" + std::to_string(ctxs[i].n_vocab_draft) + ")"; + ok &= check_logits(label, logits[i], draft_mask(vocab, ctxs[i].n_vocab_draft), + ref && ctxs[i].n_vocab_draft != 0 ? *ref : std::vector<float>()); + } + } + return ok; +} + +// create contexts one after the other, in the given order, then decode them interleaved +static bool test_sequential(llama_model * model, const std::vector<int32_t> & ns, bool add_ref) { + std::string name; + std::vector<mtp_ctx> ctxs; + for (int32_t n : ns) { + name += (name.empty() ? "" : ",") + std::to_string(n); + ctxs.push_back({ n, make_ctx(model, n) }); + if (!ctxs.back().ctx) { + fprintf(stderr, "failed to create context N=%d\n", n); + return false; + } + } + name = "create " + name; + if (add_ref) { + ctxs.push_back({ 0, make_ctx(model, 0) }); + } + fprintf(stderr, "%s:\n", name.c_str()); + return run_interleaved(model, name, ctxs, 3); +} + +// create two contexts from two threads at the same time, then decode them interleaved +static bool test_concurrent(llama_model * model, int32_t n1, int32_t n2) { + const std::string name = "concurrent " + std::to_string(n1) + "," + std::to_string(n2); + fprintf(stderr, "%s:\n", name.c_str()); + std::vector<mtp_ctx> ctxs(2); + ctxs[0].n_vocab_draft = n1; + ctxs[1].n_vocab_draft = n2; + std::atomic<bool> go(false); + std::thread t0([&] { while (!go) { std::this_thread::yield(); } ctxs[0].ctx = make_ctx(model, n1); }); + std::thread t1([&] { while (!go) { std::this_thread::yield(); } ctxs[1].ctx = make_ctx(model, n2); }); + go = true; + t0.join(); + t1.join(); + if (!ctxs[0].ctx || !ctxs[1].ctx) { + fprintf(stderr, "failed to create contexts\n"); + return false; + } + ctxs.push_back({ 0, make_ctx(model, 0) }); + return run_interleaved(model, name, ctxs, 2); +} + +int main(int argc, char ** argv) { + std::setlocale(LC_NUMERIC, "C"); + + common_params params; + common_init(); + if (!common_params_parse(argc, argv, params, LLAMA_EXAMPLE_COMMON)) { + return 1; + } + + llama_backend_init(); + ggml_backend_load_all(); + + llama_model_params mparams = common_model_params_to_llama(params); + mparams.load_mtp = true; + llama_model_ptr model(llama_model_load_from_file(params.model.path.c_str(), mparams)); + if (!model) { + fprintf(stderr, "failed to load model\n"); + return 1; + } + if (llama_model_n_layer_nextn(model.get()) == 0) { + fprintf(stderr, "model has no MTP layers, skipping\n"); + return 0; + } + + const int32_t n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model.get())); + const int32_t n1 = std::min(65536, n_vocab / 2); + const int32_t n2 = std::min(32768, n_vocab / 4); + + bool ok = true; + ok &= test_sequential(model.get(), { n1, 0 }, false); // A = N then B = 0 + ok &= test_sequential(model.get(), { 0, n1 }, false); // A = 0 then B = N + ok &= test_sequential(model.get(), { n1, n2 }, true); // A = N1 then B = N2 + ok &= test_sequential(model.get(), { n2, n2 }, true); // two contexts with the same N + ok &= test_concurrent(model.get(), n1, n2); // simultaneous creation + // a subset freed with its last context and requested again + ok &= test_sequential(model.get(), { n2 }, true); + + fprintf(stderr, "%s\n", ok ? "PASS" : "FAIL"); + return ok ? 0 : 1; +} From 73aa8cfe333b97fae68d544fa235fce46be04dd5 Mon Sep 17 00:00:00 2001 From: Simon Teixidor <simon@flaskpost.me> Date: Sun, 27 Sep 2026 12:31:06 +0100 Subject: [PATCH 195/212] ggml-cuda : shape-aware graph cache key nodes[0] alone gives the same key to graphs that share one meta arena (different topologies, or one topology at a varying verify width), so those graphs never replay. Mix the node count, the last node and three node shapes into the key. Split out of #58. Assisted-by: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC --- ggml/src/ggml-cuda/ggml-cuda.cu | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 158bb676409a..9f9ad33bd190 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -2634,6 +2634,24 @@ static bool ggml_cuda_graph_check_compability(ggml_cgraph * cgraph) { } static const void * ggml_cuda_graph_get_key(ggml_cgraph * cgraph) { + // nodes[0] alone collides for graphs that share one meta arena (different topologies, or one topology at varying batch width). + // Mix in the node count, the last node and the shapes of three nodes. A hash collision only resets warmup, the node properties are still checked. + if (cgraph->n_nodes > 0) { + uintptr_t k = (uintptr_t) cgraph->nodes[0]; + k ^= (uintptr_t) cgraph->nodes[cgraph->n_nodes - 1] * (uintptr_t) 0x9E3779B97F4A7C15ull; + k ^= (uintptr_t) cgraph->n_nodes << 4; + const ggml_tensor * probe[3] = { + cgraph->nodes[0], + cgraph->nodes[cgraph->n_nodes / 2], + cgraph->nodes[cgraph->n_nodes - 1], + }; + for (int i = 0; i < 3; ++i) { + for (int d = 0; d < GGML_MAX_DIMS; ++d) { + k = (k ^ (uintptr_t) probe[i]->ne[d]) * (uintptr_t) 0x100000001B3ull; + } + } + return (const void *) k; + } return cgraph->nodes[0]; } From b6b813dcd3220c2cdbe0f2d3f32b7307a65368fa Mon Sep 17 00:00:00 2001 From: Laurent Zuijdwijk <laurent.zuijdwijk@gmail.com> Date: Sun, 27 Sep 2026 12:32:21 +0100 Subject: [PATCH 196/212] tests : check the tested backend's picks in test_top_k_stable_ties err(a, b) receives the backend under test as a and the CPU reference as b. The stable-tie check read b, so it validated the CPU's selection and passed regardless of what ROCm returned. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014qCLEUsSbxtwb71y5qRXAi --- tests/test-backend-ops.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 1c5f330e6ec7..68402258accc 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -7545,13 +7545,14 @@ struct test_top_k_stable_ties : public test_top_k { ggml_backend_tensor_set(input, scores.data(), 0, ggml_nbytes(input)); } + // a is the backend under test, b is the CPU reference: check the backend's picks. double err(const float * a, const float * b, size_t n) override { - GGML_UNUSED(a); + GGML_UNUSED(b); if (n != size_t(ggml_nrows(input) * k)) return 1.0; for (int64_t r = 0; r < ggml_nrows(input); ++r) { std::vector<int32_t> picked(k); for (int i = 0; i < k; ++i) { - const float v = b[r*k + i]; + const float v = a[r*k + i]; picked[i] = int32_t(v); if (v != float(picked[i])) return 1.0; } From 063d80419eff419931bfafdff0ac13e8910ce08a Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:21:37 +0200 Subject: [PATCH 197/212] =?UTF-8?q?hip:=20stabilize=20128=E2=80=93512=20to?= =?UTF-8?q?ken=20QSA=20incremental=20key=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HC residual has an extra stream dimension; using ggml_nrows to decide when its BF16-only store is safe applies the mark to small microbatches and makes the output depend on scheduler split placement. Check the actual token dimension for batches of at most 512, then allow incremental indexer-key updates through 512 tokens. QSA_FAST_MAX=127 retains the full re-pool comparison path. With the MoE alias fix as the base, 256- and 512-token runs at 4096 context produced identical probability files with both paths. Assisted-by: OpenCode (gpt-6-sol) --- ggml/src/ggml-cuda/ggml-cuda.cu | 6 +++++- src/llama-memory-hybrid-idx.cpp | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 6564bfc62279..424098f933af 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -6251,7 +6251,11 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph } for (const auto & c : comb) { const ggml_tensor * r = c.second; - if (ggml_nrows(r) < 512) continue; + // A small ubatch may split on either side of the HC residual depending on + // unrelated allocations. Marking it BF16-only in one layout but not the + // other changes model results. ne[2] is the token count; nrows also includes + // the HC stream dimension, so testing nrows alone misses 256/512-token batches. + if (ggml_nrows(r) < 512 || r->ne[2] <= 512) continue; int ri = -1; for (int k = 0; k < cgraph->n_nodes; ++k) if (cgraph->nodes[k] == r) { ri = k; break; } if (ri < 0) continue; diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index 4fed10c0f352..12320e4c6494 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -1063,10 +1063,11 @@ bool llama_memory_hybrid_idx::qsa_prefix_matches(const llama_ubatch & u) const { } bool llama_memory_hybrid_idx::qsa_fast(int il, const llama_ubatch & u) const { - // re-pool only the (n+3)/4+2 blocks this ubatch touches instead of every block of the cache. Limited to 127 tokens: - // with 128..512-token ubatches the results were not bit-equal to the full re-pool (perplexity at ub 512 changed on - // gfx1151, 2026-09-23) and the cause is not known yet - return qsa_prefix_matches(u) && u.n_tokens <= 127 && qsa_keys.at(il) && + // Re-pool only the (n+3)/4+2 blocks this ubatch touches. At 512 tokens the pooled keys + // match full re-pooling exactly; the previous output mismatch came from BF16 residual + // marking at a graph split, not from the indexer-key cache. Larger ubatches are untested. + static const int max_tokens = getenv("QSA_FAST_MAX") ? atoi(getenv("QSA_FAST_MAX")) : 512; + return qsa_prefix_matches(u) && int(u.n_tokens) <= max_tokens && u.n_tokens <= 512 && qsa_keys.at(il) && qsa_ready.at(il) >= int64_t(qsa_prefix.previous_size/4) && qsa_prefix.cells.size()/4 >= (u.n_tokens+3)/4+1; } From 27c308e9c1b6a3e641f0ee3d73690f9fab10e858 Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Sun, 27 Sep 2026 22:35:28 +0200 Subject: [PATCH 198/212] hip: keep HC residual BF16 marks stable across scheduler split cuts A residual whose next fused combine lands in a later split used to lose its BF16-only mark: ggml_cuda_marks_readers_local only sees one split, so precision depended on where the input-count cuts (GGML_SCHED_MAX_SPLIT_INPUTS) fell. The QSA fast path adds three host inputs per layer and moves those cuts, which is what made 128..512-token incremental keys disagree with a full re-pool. Optimize still runs per split, but marks live for the whole graph and every split is optimized before any is computed, so carry the remaining reader count forward (mmb_res16_pending) and mark from the split that completes the check. Restores the res16 store at 512-token ubatches that 063d8041 had disabled and removes the now-redundant ne[2] test. Add test-qsa-fast: CPU-only regression for the lifted qsa_fast gate (size boundaries 1..2048, incremental block selection, prefix/commit/ truncate gates, QSA_FAST_MAX parsing). Validated on gfx1151 (ROCm 10): fast vs full re-pool logits byte-identical at ub 64/128/256/512/1024/4096, with and without split cuts; ctest -R ^test-qsa passes; test-backend-ops MUL_MAT/GET_ROWS passes. Assisted-by: OpenCode (kimi-k3) --- ggml/src/ggml-cuda/common.cuh | 2 + ggml/src/ggml-cuda/ggml-cuda.cu | 86 +++++++++++----- src/llama-memory-hybrid-idx.cpp | 8 +- tests/CMakeLists.txt | 1 + tests/test-qsa-fast.cpp | 168 ++++++++++++++++++++++++++++++++ 5 files changed, 239 insertions(+), 26 deletions(-) create mode 100644 tests/test-qsa-fast.cpp diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index 0f660694a447..3ab92800552e 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -1497,6 +1497,8 @@ struct ggml_backend_cuda_context { bool mmb_after_compute = true; const void * mmb_first_split = nullptr; std::vector<uint64_t> mmb_graph_sigs; + // HC residuals whose remaining readers live in a later split of the same graph on this device: tensor -> uses left + std::unordered_map<const ggml_tensor *, int> mmb_res16_pending; #ifdef USE_CUDA_GRAPH // Map from first_node_ptr to cuda_graph - allows multiple graphs per context diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index 424098f933af..c465879d7e6b 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -6056,6 +6056,7 @@ static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend, if (std::find(cuda_ctx->mmb_graph_sigs.begin(), cuda_ctx->mmb_graph_sigs.end(), sig) == cuda_ctx->mmb_graph_sigs.end()) { ggml_cuda_mmb_marks_clear(*cuda_ctx); cuda_ctx->mmb_graph_sigs.clear(); + cuda_ctx->mmb_res16_pending.clear(); cuda_ctx->mmb_first_split = nullptr; } } @@ -6152,18 +6153,20 @@ static bool ggml_cuda_marks_is_view(const ggml_tensor * t) { return t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE || t->op == GGML_OP_PERMUTE || t->op == GGML_OP_TRANSPOSE; } -static bool ggml_cuda_marks_readers_local(const ggml_cgraph * g, const ggml_tensor * x) { - auto uses = [g](const ggml_tensor * t) -> int { - const size_t h = ggml_hash_find(&g->visited_hash_set, t); - return h == GGML_HASHSET_FULL || !ggml_bitset_get(g->visited_hash_set.used, h) ? -1 : g->use_counts[h]; - }; - int expected = uses(x), found = 0; - if (expected < 0) return false; +static int ggml_cuda_marks_uses(const ggml_cgraph * g, const ggml_tensor * t) { + const size_t h = ggml_hash_find(&g->visited_hash_set, t); + return h == GGML_HASHSET_FULL || !ggml_bitset_get(g->visited_hash_set.used, h) ? -1 : g->use_counts[h]; +} + +// uses of x (and of its views present in this split) that no node of this split accounts for; -1 when unknown +static int ggml_cuda_marks_readers_left(const ggml_cgraph * g, const ggml_tensor * x) { + int expected = ggml_cuda_marks_uses(g, x), found = 0; + if (expected < 0) return -1; for (int i = 0; i < g->n_nodes; ++i) { const ggml_tensor * t = g->nodes[i]; if (t != x && ggml_cuda_marks_is_view(t) && t->view_src == x) { - const int u = uses(t); - if (u < 0) return false; + const int u = ggml_cuda_marks_uses(g, t); + if (u < 0) return -1; expected += u; } for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) { @@ -6171,14 +6174,18 @@ static bool ggml_cuda_marks_readers_local(const ggml_cgraph * g, const ggml_tens if (src == x || (ggml_cuda_marks_is_view(src) && src->view_src == x)) ++found; } } - return expected == found; + return expected - found; +} + +static bool ggml_cuda_marks_readers_local(const ggml_cgraph * g, const ggml_tensor * x) { + return ggml_cuda_marks_readers_left(g, x) == 0; } static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph * cgraph, ggml_backend_graph_optimize_params * params) { ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context; { // marks live for the whole scheduled graph (all splits): clear at the first optimize after a compute, or when the first split repeats const void * key = cgraph->n_nodes ? cgraph->nodes[0] : nullptr; - if (cuda_ctx->mmb_after_compute || cuda_ctx->mmb_first_split == nullptr || key == cuda_ctx->mmb_first_split) { ggml_cuda_mmb_marks_clear(*cuda_ctx); cuda_ctx->mmb_graph_sigs.clear(); cuda_ctx->mmb_first_split = key; cuda_ctx->mmb_after_compute = false; } + if (cuda_ctx->mmb_after_compute || cuda_ctx->mmb_first_split == nullptr || key == cuda_ctx->mmb_first_split) { ggml_cuda_mmb_marks_clear(*cuda_ctx); cuda_ctx->mmb_graph_sigs.clear(); cuda_ctx->mmb_res16_pending.clear(); cuda_ctx->mmb_first_split = key; cuda_ctx->mmb_after_compute = false; } } { // HC16: mark tensors whose readers all take the BF16 copy, so their producers skip the F32 store if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { @@ -6249,13 +6256,48 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph ggml_cuda_hc_combine_norm_args ca; if (ggml_cuda_match_hc_combine_norm(cgraph, i, ca, ws, false) > 0) comb.emplace_back(ca.residual, ca.out_res); } + auto is_next_combine = [&comb](const ggml_tensor * r, const ggml_tensor * t) { + for (const auto & d : comb) { + const ggml_tensor * dr = d.first->view_src ? d.first->view_src : d.first; + if (dr == r && d.second == t) return true; + } + return false; + }; + // The scheduler cuts a split wherever the input count runs out (GGML_SCHED_MAX_SPLIT_INPUTS), so a + // residual's next combine can land in a later split. Every split is optimized before any is computed + // and the marks live for the whole graph, so finish the reader check here instead of dropping the + // mark: otherwise precision depends on where the cuts fall (e.g. QSA fast vs full re-pool inputs). + // Only the next fused combine may read it across a cut; a reader on another backend is never seen, + // so its residual stays F32. + auto & pending = cuda_ctx->mmb_res16_pending; + for (auto it = pending.begin(); it != pending.end(); ) { + const ggml_tensor * r = it->first; + int left = it->second; + bool ok = true; + for (int n = 0; n < cgraph->n_nodes && ok; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + const bool view_of_r = t != r && ggml_cuda_marks_is_view(t) && t->view_src == r; + if (view_of_r) { + const int u = ggml_cuda_marks_uses(cgraph, t); + if (u < 0) { ok = false; break; } + left += u; + } + for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) { + const ggml_tensor * src = t->src[s]; + if (src != r && !(ggml_cuda_marks_is_view(src) && src->view_src == r)) continue; + --left; + if (view_of_r && (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE)) continue; + if (!is_next_combine(r, t)) ok = false; + } + } + if (!ok || left < 0) { it = pending.erase(it); continue; } + if (left == 0) { ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, r); it = pending.erase(it); continue; } + it->second = left; + ++it; + } for (const auto & c : comb) { const ggml_tensor * r = c.second; - // A small ubatch may split on either side of the HC residual depending on - // unrelated allocations. Marking it BF16-only in one layout but not the - // other changes model results. ne[2] is the token count; nrows also includes - // the HC stream dimension, so testing nrows alone misses 256/512-token batches. - if (ggml_nrows(r) < 512 || r->ne[2] <= 512) continue; + if (ggml_nrows(r) < 512) continue; int ri = -1; for (int k = 0; k < cgraph->n_nodes; ++k) if (cgraph->nodes[k] == r) { ri = k; break; } if (ri < 0) continue; @@ -6266,15 +6308,13 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph ++nread; if (t->op == GGML_OP_RMS_NORM) continue; if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE) continue; - bool next_combine = false; - for (const auto & d : comb) { - const ggml_tensor * dr = d.first->view_src ? d.first->view_src : d.first; - if (dr == r && d.second == t) { next_combine = true; break; } - } - if (next_combine) continue; + if (is_next_combine(r, t)) continue; ok = false; } - if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, r)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, r); + if (!ok) continue; + const int left = ggml_cuda_marks_readers_left(cgraph, r); + if (left == 0 && nread > 0) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, r); + else if (left > 0) pending[r] = left; } } for (int i = 0; i < cgraph->n_nodes; ++i) { // fused stream mix output read only by MMB GEMMs diff --git a/src/llama-memory-hybrid-idx.cpp b/src/llama-memory-hybrid-idx.cpp index 12320e4c6494..64a9edc64ee1 100644 --- a/src/llama-memory-hybrid-idx.cpp +++ b/src/llama-memory-hybrid-idx.cpp @@ -1063,9 +1063,11 @@ bool llama_memory_hybrid_idx::qsa_prefix_matches(const llama_ubatch & u) const { } bool llama_memory_hybrid_idx::qsa_fast(int il, const llama_ubatch & u) const { - // Re-pool only the (n+3)/4+2 blocks this ubatch touches. At 512 tokens the pooled keys - // match full re-pooling exactly; the previous output mismatch came from BF16 residual - // marking at a graph split, not from the indexer-key cache. Larger ubatches are untested. + // Re-pool only the (n+3)/4+2 blocks this ubatch touches. The pooled keys match full re-pooling + // exactly. The earlier 128..512 output mismatch was not the cache: the 3 host inputs this path adds + // per layer moved the scheduler's input-count split cuts, and the CUDA backend dropped the BF16 + // residual mark of any residual whose next combine landed across a cut (fixed there, see + // mmb_res16_pending). Larger ubatches are untested; QSA_FAST_MAX=127 restores the old limit. static const int max_tokens = getenv("QSA_FAST_MAX") ? atoi(getenv("QSA_FAST_MAX")) : 512; return qsa_prefix_matches(u) && int(u.n_tokens) <= max_tokens && u.n_tokens <= 512 && qsa_keys.at(il) && qsa_ready.at(il) >= int64_t(qsa_prefix.previous_size/4) && diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a2855346e9bf..ab0d29bddcbf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -149,6 +149,7 @@ if (LLAMA_LLGUIDANCE) endif () llama_build_and_test(test-qsa-prefix.cpp) +llama_build_and_test(test-qsa-fast.cpp) llama_build_and_test(test-ple-disk.cpp) llama_build(test-recurrent-state-rollback.cpp) llama_build(test-mtp-draft-vocab.cpp) # needs a qwen35/qwen35moe model with an MTP head: -m MODEL diff --git a/tests/test-qsa-fast.cpp b/tests/test-qsa-fast.cpp new file mode 100644 index 000000000000..537953bec16f --- /dev/null +++ b/tests/test-qsa-fast.cpp @@ -0,0 +1,168 @@ +#include "../src/llama-memory-hybrid-idx.h" +#include "../src/prefix.h" +#include "ggml.h" +#include <cstring> +#include <iostream> +#include <set> +#include <vector> + +// Host-side regression for the qsa_fast gate lifted from 127 to 512 tokens: +// incremental indexer-key pooling must select exactly the touched blocks, and +// must stay off when the prefix, the commit state or the size guard does not hold. +// Runs on CPU only; no model or backend is involved. + +// qsa_prefix.update-equivalent bookkeeping via qsa_prefix_state::apply +static void feed(qsa_prefix_state & s, int32_t start, int n) { + // position k lives in cell k (cells in order); apply() places them in sequence + std::vector<uint32_t> slots(n); + for (int i = 0; i < n; ++i) slots[i] = start + i; + GGML_ASSERT(s.apply(0, start, slots)); +} + +// single-sequence ubatch of n tokens starting at position start +struct qsa_fast_ubatch { + std::vector<llama_token> tok; std::vector<llama_pos> pos; std::vector<int32_t> n_seq_id; + std::vector<std::vector<llama_seq_id>> ids; std::vector<llama_seq_id *> seq_id; llama_ubatch u{}; + qsa_fast_ubatch(int32_t start, int n, int32_t seq = 0) { + tok.assign(n, 0); pos.resize(n); ids.assign(n, {seq}); + for (int i = 0; i < n; ++i) { pos[i] = start + i; n_seq_id.push_back(1); seq_id.push_back(ids[i].data()); } + u.n_tokens = n; u.n_pos = 1; u.token = tok.data(); u.pos = pos.data(); u.n_seq_id = n_seq_id.data(); u.seq_id = seq_id.data(); + } +}; + +// minimal stand-in for the memory object's fast-path pieces +struct qsa_fast_mock : public qsa_prefix_state { + qsa_fast_mock(size_t capacity = 0) : qsa_prefix_state(capacity) {} + void assign_from(const qsa_prefix_state & s) { static_cast<qsa_prefix_state &>(*this) = s; } + std::vector<int64_t> qsa_ready{0}; + std::vector<bool> qsa_keys{true}; + + const int max_tokens = getenv("QSA_FAST_MAX") ? atoi(getenv("QSA_FAST_MAX")) : 512; // read once, like the production static + bool fast(const llama_ubatch & u) const { + const bool prefix = qsa_prefix_matches(u); + return prefix && int(u.n_tokens) <= max_tokens && u.n_tokens <= 512 && qsa_keys.at(0) && + qsa_ready.at(0) >= int64_t(previous_size/4) && + cells.size()/4 >= (u.n_tokens+3)/4+1; + } + bool qsa_prefix_matches(const llama_ubatch & u) const { + if (!valid || !u.token || !u.pos || !u.n_tokens || !u.n_pos || !u.seq_id || !u.n_seq_id || + u.pos[0] != begin || int64_t(u.n_tokens) != end-begin) { return false; } + for (uint32_t i=0; i<u.n_tokens; ++i) { + if (u.n_seq_id[i] != 1 || !u.seq_id[i] || u.seq_id[i][0] != sequence || u.pos[i] != u.pos[0]+int32_t(i)) { return false; } + } + return true; + } + void commit() { qsa_ready.at(0) = cells.size()/4; } +}; + +// reproduce qsa_fill_updates's id selection and check it is the incremental block set +static void check_fill(const qsa_fast_mock & s, int n) { + const int count = (n+3)/4+2, complete = int(s.cells.size()/4); + const int first = int(s.begin/4), last = std::min(complete, int(s.end+3)/4); + GGML_ASSERT(last-first < count && complete >= count - 1); + std::vector<int> ids = { complete }; + for (int b=first; b<last; ++b) ids.push_back(b); + for (int b=0; int(ids.size())<count; ++b) { if (b<first || b>=last) ids.push_back(b); } + // in range, duplicate-free, and covers exactly [first, last) beyond the placeholder/filler blocks + std::set<int> seen; + for (int id : ids) { GGML_ASSERT(id >= 0 && id <= complete); GGML_ASSERT(seen.insert(id).second); } + GGML_ASSERT(ids.size() == size_t(count)); + std::set<int> covered(ids.begin()+1, ids.end()); + for (int b=first; b<last; ++b) GGML_ASSERT(covered.count(b) == 1); +} + +static void test_size_boundaries() { + // empty cache: the first ubatch never takes the fast path (needs cells/4 >= count-1) + for (int n : {1, 4, 8, 127, 128, 255, 256, 511, 512, 513, 1024}) { + qsa_fast_mock s(8192); s.qsa_ready[0] = 0; + feed(s, 0, n); + const bool fast = s.fast(qsa_fast_ubatch(0, n).u); + GGML_ASSERT(!fast || (int(s.cells.size()/4) >= (n+3)/4+1)); + } + // established cache (full re-pool committed): fast iff n <= 512 + for (int n : {1, 4, 8, 126, 127, 128, 129, 255, 256, 257, 511, 512, 513, 514, 1024, 2048}) { + qsa_fast_mock s(8192); + feed(s, 0, 512); s.commit(); + feed(s, 512, n); + GGML_ASSERT(s.fast(qsa_fast_ubatch(512, n).u) == (n <= 512)); + } + // the last allowed boundary: previous commit covers the skipped blocks + { + qsa_fast_mock s(8192); + feed(s, 0, 512); s.commit(); + feed(s, 512, 512); + GGML_ASSERT(s.fast(qsa_fast_ubatch(512, 512).u)); + GGML_ASSERT(s.qsa_ready[0] >= int64_t(s.previous_size/4)); + } +} + +static void test_incremental_ids() { + for (int begin : {4, 128, 512, 1024}) { + for (int n : {1, 4, 8, 127, 128, 256, 512}) { + qsa_fast_mock s(8192); + feed(s, 0, begin); s.commit(); + feed(s, begin, n); + if (s.fast(qsa_fast_ubatch(begin, n).u)) { check_fill(s, n); } + } + } +} + +static void test_gate_failures() { + { // position must start at the tracked prefix end + qsa_fast_mock s(8192); feed(s, 0, 512); s.commit(); feed(s, 512, 256); + GGML_ASSERT(!s.fast(qsa_fast_ubatch(513, 255).u)); + GGML_ASSERT(!s.fast(qsa_fast_ubatch(256, 256).u)); + } + { // the prefix was not committed (recover/rollback path) -> full re-pool + qsa_fast_mock s(8192); feed(s, 0, 512); /* no commit */ feed(s, 512, 256); + GGML_ASSERT(!s.fast(qsa_fast_ubatch(512, 256).u)); + } + { // truncation below a block boundary re-pools once, then the fast path resumes on the + // committed smaller prefix (qsa_ready is clamped on seq_rm, llama-memory-hybrid-idx.cpp:203) + qsa_fast_mock s(8192); feed(s, 0, 512); s.commit(); s.truncate(200); + s.qsa_ready[0] = std::min<int64_t>(s.qsa_ready[0], s.cells.size()/4); + // production rebuilds the tracked prefix from the cache (prefix stays valid through seq_rm): + // cells [0,200) are rewritten in place (size stays 200), qsa_ready clamps to 200/4 = 50. + // qsa_fast runs against this pre-ubatch state: cells/4 = 50 < (256+3)/4+1 = 65 -> full re-pool. + GGML_ASSERT(int(s.cells.size()) == 200); + GGML_ASSERT(!s.fast(qsa_fast_ubatch(200, 256).u)); // one full re-pool + feed(s, 200, 256); // the full path commits the whole prefix + s.commit(); + feed(s, 456, 8); + GGML_ASSERT(s.fast(qsa_fast_ubatch(456, 8).u)); // fast resumes on the new prefix + } + { // multi-sequence ubatch never matches the prefix + qsa_fast_mock s(8192); feed(s, 0, 512); s.commit(); feed(s, 512, 256); + qsa_fast_ubatch u(512, 256); + u.u.n_seq_id[3] = 2; // break the single-sequence invariant + GGML_ASSERT(!s.fast(u.u)); + } + { // QSA_FAST_MAX: values <= 512 shrink the limit; 0 disables; >512 is capped by the hard limit. + // (The real guard caches the env in a function-local static; this mock re-reads it per call, + // and each case uses a fresh prefix so only the limit under test varies.) + setenv("QSA_FAST_MAX", "127", 1); + qsa_fast_mock a(8192), b(8192); + feed(a, 0, 512); a.commit(); feed(a, 512, 128); + GGML_ASSERT(!a.fast(qsa_fast_ubatch(512, 128).u)); + feed(b, 0, 512); b.commit(); feed(b, 512, 127); + GGML_ASSERT(b.fast(qsa_fast_ubatch(512, 127).u)); + setenv("QSA_FAST_MAX", "0", 1); + qsa_fast_mock b0(8192); + feed(b0, 0, 512); b0.commit(); feed(b0, 512, 127); + GGML_ASSERT(!b0.fast(qsa_fast_ubatch(512, 127).u)); + setenv("QSA_FAST_MAX", "9999", 1); // still capped at 512 + qsa_fast_mock c(8192), d(8192); + feed(c, 0, 512); c.commit(); feed(c, 512, 513); + GGML_ASSERT(!c.fast(qsa_fast_ubatch(512, 513).u)); + feed(d, 0, 512); d.commit(); feed(d, 512, 512); + GGML_ASSERT(d.fast(qsa_fast_ubatch(512, 512).u)); + unsetenv("QSA_FAST_MAX"); + } +} + +int main() { + test_size_boundaries(); + test_incremental_ids(); + test_gate_failures(); + std::cout << "PASS: qsa_fast gate boundaries (1..2048), incremental block selection, prefix/commit/truncate gates, QSA_FAST_MAX\n"; +} From 2fd4c2a770c801dab722ceb37151c9e6ab173ff6 Mon Sep 17 00:00:00 2001 From: Baraxanx <baraxnaxgaming@gmail.com> Date: Thu, 10 Sep 2026 02:12:41 -0400 Subject: [PATCH 199/212] cuda: ROCmFPx load/dequant/dispatch paths for HIP builds Loads Q4_0_ROCMFP4, Q4_0_ROCMFP4_FAST and the Q2/Q3/Q6/Q8_0_ROCMFPX GGUF tensor types on ggml-cuda/HIP (previously CPU+Vulkan only). Adds dequant, convert, get_rows support and MMVQ dispatch; restores the *_hip_* helper headers (scale LUT + codebooks) that the format import pruned. Part 1/2 of the FP4-on-HIP work; part 2 adds the MMQ tile path. --- ggml/rocmfp4/rocmfp4_hip.cu | 93 +++++++++++++++ ggml/rocmfp4/rocmfp4_hip_codebook.cuh | 87 ++++++++++++++ ggml/rocmfp4/rocmfp4_hip_scale.cuh | 165 ++++++++++++++++++++++++++ ggml/rocmfpx/rocmfpx_hip_codebook.cuh | 53 +++++++++ ggml/src/ggml-cuda/common.cuh | 18 +++ ggml/src/ggml-cuda/convert.cu | 38 ++++++ ggml/src/ggml-cuda/dequantize.cuh | 34 ++++++ ggml/src/ggml-cuda/getrows.cu | 8 ++ ggml/src/ggml-cuda/ggml-cuda.cu | 4 + ggml/src/ggml-cuda/mmvq.cu | 16 +++ ggml/src/ggml-cuda/vecdotq.cuh | 50 ++++++++ 11 files changed, 566 insertions(+) create mode 100644 ggml/rocmfp4/rocmfp4_hip.cu create mode 100644 ggml/rocmfp4/rocmfp4_hip_codebook.cuh create mode 100644 ggml/rocmfp4/rocmfp4_hip_scale.cuh create mode 100644 ggml/rocmfpx/rocmfpx_hip_codebook.cuh diff --git a/ggml/rocmfp4/rocmfp4_hip.cu b/ggml/rocmfp4/rocmfp4_hip.cu new file mode 100644 index 000000000000..3174743cc41d --- /dev/null +++ b/ggml/rocmfp4/rocmfp4_hip.cu @@ -0,0 +1,93 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + +#include "rocmfp4.h" + +#include <hip/hip_runtime.h> + +#include "rocmfp4_hip_scale.cuh" + +// Standalone ROCm/HIP dequant kernel for integration tests and future fused +// paths. One lane owns one packed byte and writes the matching low/high +// half-block values, so each byte is read once. +extern "C" __global__ void rocmfp4_dequantize_q4_0_f32_kernel( + const block_rocmfp4 * __restrict__ x, + float * __restrict__ y, + int64_t k) { + const int64_t packed_idx = (int64_t) blockIdx.x*blockDim.x + threadIdx.x; + const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; + const int64_t packed_count = nblocks * (QK_ROCMFP4/2); + + if (packed_idx >= packed_count) { + return; + } + + const int64_t ib = packed_idx / (QK_ROCMFP4/2); + const int tid = packed_idx - ib*(QK_ROCMFP4/2); + const int64_t base = ib*QK_ROCMFP4; + const uint8_t packed = x[ib].qs[tid]; + const float d0 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[0]); + const float d1 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[1]); + + if (base + tid < k) { + y[base + tid] = (float) rocmfp4_decode_i8(packed & 0x0f) * d0; + } + if (base + tid + QK_ROCMFP4/2 < k) { + y[base + tid + QK_ROCMFP4/2] = (float) rocmfp4_decode_i8(packed >> 4) * d1; + } +} + +extern "C" __global__ void rocmfp4_dequantize_q4_0_fast_f32_kernel( + const block_rocmfp4_fast * __restrict__ x, + float * __restrict__ y, + int64_t k) { + const int64_t packed_idx = (int64_t) blockIdx.x*blockDim.x + threadIdx.x; + const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; + const int64_t packed_count = nblocks * (QK_ROCMFP4/2); + + if (packed_idx >= packed_count) { + return; + } + + const int64_t ib = packed_idx / (QK_ROCMFP4/2); + const int tid = packed_idx - ib*(QK_ROCMFP4/2); + const int64_t base = ib*QK_ROCMFP4; + const uint8_t packed = x[ib].qs[tid]; + const float d = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e); + + if (base + tid < k) { + y[base + tid] = (float) rocmfp4_decode_i8(packed & 0x0f) * d; + } + if (base + tid + QK_ROCMFP4/2 < k) { + y[base + tid + QK_ROCMFP4/2] = (float) rocmfp4_decode_i8(packed >> 4) * d; + } +} + +extern "C" void rocmfp4_hip_dequantize_q4_0_to_f32( + const void * src, + float * dst, + int64_t k, + hipStream_t stream) { + const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; + const int64_t packed_count = nblocks * (QK_ROCMFP4/2); + const dim3 block(256); + const dim3 grid((unsigned int) ((packed_count + block.x - 1) / block.x)); + rocmfp4_dequantize_q4_0_f32_kernel<<<grid, block, 0, stream>>>((const block_rocmfp4 *) src, dst, k); +} + +extern "C" void rocmfp4_hip_dequantize_q4_0_fast_to_f32( + const void * src, + float * dst, + int64_t k, + hipStream_t stream) { + const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; + const int64_t packed_count = nblocks * (QK_ROCMFP4/2); + const dim3 block(256); + const dim3 grid((unsigned int) ((packed_count + block.x - 1) / block.x)); + rocmfp4_dequantize_q4_0_fast_f32_kernel<<<grid, block, 0, stream>>>((const block_rocmfp4_fast *) src, dst, k); +} diff --git a/ggml/rocmfp4/rocmfp4_hip_codebook.cuh b/ggml/rocmfp4/rocmfp4_hip_codebook.cuh new file mode 100644 index 000000000000..7fa1357f8e8d --- /dev/null +++ b/ggml/rocmfp4/rocmfp4_hip_codebook.cuh @@ -0,0 +1,87 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + +#pragma once + +#include "rocmfp4_hip_scale.cuh" + +#include <cstdint> +#include <cstring> + +#ifndef GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD +#define GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD 1 +#endif + +static __device__ __forceinline__ int rocmfp4_get_qs_i32(const void * x, const int & i32) { +#if defined(GGML_USE_HIP) && GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD + return *((const int *) ((const uint8_t *) x + 4*i32)); +#else + const uint8_t * x8 = (const uint8_t *) x; + + int x32 = x8[4*i32 + 0] << 0; + x32 |= x8[4*i32 + 1] << 8; + x32 |= x8[4*i32 + 2] << 16; + x32 |= x8[4*i32 + 3] << 24; + + return x32; +#endif +} + +// AMD-specific fast path for expanding eight packed ROCmFP4 nibbles into two +// int32 DP4A operands. This encodes the Codebook10 table directly as four +// 32-bit constants: +// [0, 1, 2, 3], [4, 6, 8, 10], [0, -1, -2, -3], [-4, -6, -8, -10] +// Avoiding the table pointer keeps the ROCm/HIP MMVQ/MMQ hot path fully local +// to this format. Non-HIP builds still use llama.cpp's generic table expander. +static __device__ __forceinline__ int2 rocmfp4_get_int_from_codebook_16(const int & q4, const int8_t * fallback_table) { +#if defined(GGML_USE_HIP) + constexpr uint32_t values0 = 0x03020100u; + constexpr uint32_t values1 = 0x0a080604u; + constexpr uint32_t values2 = 0xfdfeff00u; + constexpr uint32_t values3 = 0xf6f8fafcu; + + const uint32_t q_even = q4; + const uint32_t q_odd = q4 >> 4; + + const uint32_t v_even_low = __builtin_amdgcn_perm(values1, values0, q_even & 0x07070707u); + const uint32_t v_odd_low = __builtin_amdgcn_perm(values1, values0, q_odd & 0x07070707u); + const uint32_t v_even_high = __builtin_amdgcn_perm(values3, values2, q_even & 0x07070707u); + const uint32_t v_odd_high = __builtin_amdgcn_perm(values3, values2, q_odd & 0x07070707u); + + const uint32_t mask_even = 0x03020100u | ((q_even & 0x08080808u) >> 1); + const uint32_t mask_odd = 0x03020100u | ((q_odd & 0x08080808u) >> 1); + + return make_int2( + __builtin_amdgcn_perm(v_even_high, v_even_low, mask_even), + __builtin_amdgcn_perm(v_odd_high, v_odd_low, mask_odd)); +#else + return get_int_from_table_16(q4, fallback_table); +#endif +} + +// Variant for call sites that already selected either the low or high nibble +// stream and only need one DP4A operand. This avoids the extra odd/even table +// expansion work in ROCmFP4 FlashAttention K/V decode. +static __device__ __forceinline__ int rocmfp4_get_low_int_from_codebook_16(const int & q4, const int8_t * fallback_table) { +#if defined(GGML_USE_HIP) + constexpr uint32_t values0 = 0x03020100u; + constexpr uint32_t values1 = 0x0a080604u; + constexpr uint32_t values2 = 0xfdfeff00u; + constexpr uint32_t values3 = 0xf6f8fafcu; + + const uint32_t q = q4; + + const uint32_t v_low = __builtin_amdgcn_perm(values1, values0, q & 0x07070707u); + const uint32_t v_high = __builtin_amdgcn_perm(values3, values2, q & 0x07070707u); + const uint32_t mask = 0x03020100u | ((q & 0x08080808u) >> 1); + + return __builtin_amdgcn_perm(v_high, v_low, mask); +#else + return get_int_from_table_16(q4, fallback_table).x; +#endif +} diff --git a/ggml/rocmfp4/rocmfp4_hip_scale.cuh b/ggml/rocmfp4/rocmfp4_hip_scale.cuh new file mode 100644 index 000000000000..a783be2e9493 --- /dev/null +++ b/ggml/rocmfp4/rocmfp4_hip_scale.cuh @@ -0,0 +1,165 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + +#pragma once + +#include <cstdint> +#include <cstring> +#include <cmath> + +#ifndef GGML_ROCMFP4_USE_SCALE_LUT +#define GGML_ROCMFP4_USE_SCALE_LUT 0 +#endif + +#if defined(GGML_USE_HIP) && GGML_ROCMFP4_USE_SCALE_LUT +#define ROCMFP4_SCALE_SUB(M) ((M) * 0x1p-10f) +#define ROCMFP4_SCALE_E1(M) ((8 + (M)) * 0x1p-10f) +#define ROCMFP4_SCALE_E2(M) ((8 + (M)) * 0x1p-9f) +#define ROCMFP4_SCALE_E3(M) ((8 + (M)) * 0x1p-8f) +#define ROCMFP4_SCALE_E4(M) ((8 + (M)) * 0x1p-7f) +#define ROCMFP4_SCALE_E5(M) ((8 + (M)) * 0x1p-6f) +#define ROCMFP4_SCALE_E6(M) ((8 + (M)) * 0x1p-5f) +#define ROCMFP4_SCALE_E7(M) ((8 + (M)) * 0x1p-4f) +#define ROCMFP4_SCALE_E8(M) ((8 + (M)) * 0x1p-3f) +#define ROCMFP4_SCALE_E9(M) ((8 + (M)) * 0x1p-2f) +#define ROCMFP4_SCALE_E10(M) ((8 + (M)) * 0x1p-1f) +#define ROCMFP4_SCALE_E11(M) ((8 + (M)) * 0x1p0f) +#define ROCMFP4_SCALE_E12(M) ((8 + (M)) * 0x1p1f) +#define ROCMFP4_SCALE_E13(M) ((8 + (M)) * 0x1p2f) +#define ROCMFP4_SCALE_E14(M) ((8 + (M)) * 0x1p3f) +#define ROCMFP4_SCALE_E15(M) ((8 + (M)) * 0x1p4f) + +static __device__ __constant__ const float rocmfp4_scale_ue4m3_half_lut[127] = { + ROCMFP4_SCALE_SUB(0), ROCMFP4_SCALE_SUB(1), ROCMFP4_SCALE_SUB(2), ROCMFP4_SCALE_SUB(3), + ROCMFP4_SCALE_SUB(4), ROCMFP4_SCALE_SUB(5), ROCMFP4_SCALE_SUB(6), ROCMFP4_SCALE_SUB(7), + ROCMFP4_SCALE_E1(0), ROCMFP4_SCALE_E1(1), ROCMFP4_SCALE_E1(2), ROCMFP4_SCALE_E1(3), + ROCMFP4_SCALE_E1(4), ROCMFP4_SCALE_E1(5), ROCMFP4_SCALE_E1(6), ROCMFP4_SCALE_E1(7), + ROCMFP4_SCALE_E2(0), ROCMFP4_SCALE_E2(1), ROCMFP4_SCALE_E2(2), ROCMFP4_SCALE_E2(3), + ROCMFP4_SCALE_E2(4), ROCMFP4_SCALE_E2(5), ROCMFP4_SCALE_E2(6), ROCMFP4_SCALE_E2(7), + ROCMFP4_SCALE_E3(0), ROCMFP4_SCALE_E3(1), ROCMFP4_SCALE_E3(2), ROCMFP4_SCALE_E3(3), + ROCMFP4_SCALE_E3(4), ROCMFP4_SCALE_E3(5), ROCMFP4_SCALE_E3(6), ROCMFP4_SCALE_E3(7), + ROCMFP4_SCALE_E4(0), ROCMFP4_SCALE_E4(1), ROCMFP4_SCALE_E4(2), ROCMFP4_SCALE_E4(3), + ROCMFP4_SCALE_E4(4), ROCMFP4_SCALE_E4(5), ROCMFP4_SCALE_E4(6), ROCMFP4_SCALE_E4(7), + ROCMFP4_SCALE_E5(0), ROCMFP4_SCALE_E5(1), ROCMFP4_SCALE_E5(2), ROCMFP4_SCALE_E5(3), + ROCMFP4_SCALE_E5(4), ROCMFP4_SCALE_E5(5), ROCMFP4_SCALE_E5(6), ROCMFP4_SCALE_E5(7), + ROCMFP4_SCALE_E6(0), ROCMFP4_SCALE_E6(1), ROCMFP4_SCALE_E6(2), ROCMFP4_SCALE_E6(3), + ROCMFP4_SCALE_E6(4), ROCMFP4_SCALE_E6(5), ROCMFP4_SCALE_E6(6), ROCMFP4_SCALE_E6(7), + ROCMFP4_SCALE_E7(0), ROCMFP4_SCALE_E7(1), ROCMFP4_SCALE_E7(2), ROCMFP4_SCALE_E7(3), + ROCMFP4_SCALE_E7(4), ROCMFP4_SCALE_E7(5), ROCMFP4_SCALE_E7(6), ROCMFP4_SCALE_E7(7), + ROCMFP4_SCALE_E8(0), ROCMFP4_SCALE_E8(1), ROCMFP4_SCALE_E8(2), ROCMFP4_SCALE_E8(3), + ROCMFP4_SCALE_E8(4), ROCMFP4_SCALE_E8(5), ROCMFP4_SCALE_E8(6), ROCMFP4_SCALE_E8(7), + ROCMFP4_SCALE_E9(0), ROCMFP4_SCALE_E9(1), ROCMFP4_SCALE_E9(2), ROCMFP4_SCALE_E9(3), + ROCMFP4_SCALE_E9(4), ROCMFP4_SCALE_E9(5), ROCMFP4_SCALE_E9(6), ROCMFP4_SCALE_E9(7), + ROCMFP4_SCALE_E10(0), ROCMFP4_SCALE_E10(1), ROCMFP4_SCALE_E10(2), ROCMFP4_SCALE_E10(3), + ROCMFP4_SCALE_E10(4), ROCMFP4_SCALE_E10(5), ROCMFP4_SCALE_E10(6), ROCMFP4_SCALE_E10(7), + ROCMFP4_SCALE_E11(0), ROCMFP4_SCALE_E11(1), ROCMFP4_SCALE_E11(2), ROCMFP4_SCALE_E11(3), + ROCMFP4_SCALE_E11(4), ROCMFP4_SCALE_E11(5), ROCMFP4_SCALE_E11(6), ROCMFP4_SCALE_E11(7), + ROCMFP4_SCALE_E12(0), ROCMFP4_SCALE_E12(1), ROCMFP4_SCALE_E12(2), ROCMFP4_SCALE_E12(3), + ROCMFP4_SCALE_E12(4), ROCMFP4_SCALE_E12(5), ROCMFP4_SCALE_E12(6), ROCMFP4_SCALE_E12(7), + ROCMFP4_SCALE_E13(0), ROCMFP4_SCALE_E13(1), ROCMFP4_SCALE_E13(2), ROCMFP4_SCALE_E13(3), + ROCMFP4_SCALE_E13(4), ROCMFP4_SCALE_E13(5), ROCMFP4_SCALE_E13(6), ROCMFP4_SCALE_E13(7), + ROCMFP4_SCALE_E14(0), ROCMFP4_SCALE_E14(1), ROCMFP4_SCALE_E14(2), ROCMFP4_SCALE_E14(3), + ROCMFP4_SCALE_E14(4), ROCMFP4_SCALE_E14(5), ROCMFP4_SCALE_E14(6), ROCMFP4_SCALE_E14(7), + ROCMFP4_SCALE_E15(0), ROCMFP4_SCALE_E15(1), ROCMFP4_SCALE_E15(2), ROCMFP4_SCALE_E15(3), + ROCMFP4_SCALE_E15(4), ROCMFP4_SCALE_E15(5), ROCMFP4_SCALE_E15(6), +}; + +#undef ROCMFP4_SCALE_SUB +#undef ROCMFP4_SCALE_E1 +#undef ROCMFP4_SCALE_E2 +#undef ROCMFP4_SCALE_E3 +#undef ROCMFP4_SCALE_E4 +#undef ROCMFP4_SCALE_E5 +#undef ROCMFP4_SCALE_E6 +#undef ROCMFP4_SCALE_E7 +#undef ROCMFP4_SCALE_E8 +#undef ROCMFP4_SCALE_E9 +#undef ROCMFP4_SCALE_E10 +#undef ROCMFP4_SCALE_E11 +#undef ROCMFP4_SCALE_E12 +#undef ROCMFP4_SCALE_E13 +#undef ROCMFP4_SCALE_E14 +#undef ROCMFP4_SCALE_E15 +#endif + +static __device__ __forceinline__ float rocmfp4_u32_as_f32(uint32_t bits) { +#if defined(GGML_USE_HIP) + return __uint_as_float(bits); +#else + float result; + memcpy(&result, &bits, sizeof(float)); + return result; +#endif +} + +// ROCmFP4 validates scale bytes before backend execution, so HIP/ROCm hot +// paths can decode finite unsigned E4M3 half-scales directly without the +// generic FP8 NaN handling used by other formats. +static __device__ __forceinline__ float rocmfp4_ue4m3_to_fp32_half_finite(uint8_t x) { +#if defined(GGML_USE_HIP) && GGML_ROCMFP4_USE_SCALE_LUT + return x <= 0x7e ? rocmfp4_scale_ue4m3_half_lut[x] : 0.0f; +#else + const int exp = (x >> 3) & 0xF; + const int man = x & 0x7; + + if (exp == 0) { + return (float) man * (1.0f / 1024.0f); + } + + const uint32_t bits = ((uint32_t) exp + 119u) << 23 | ((uint32_t) man << 20); + return rocmfp4_u32_as_f32(bits); +#endif +} + +static __device__ __forceinline__ float rocmfpx_ue4m3_to_fp32_finite(uint8_t x) { + if (x > 0x7e) { + return 0.0f; + } + + const int exp = (x >> 3) & 0xF; + const int man = x & 0x7; + + if (exp == 0) { + return (float) man * (1.0f / 1024.0f); + } + + const uint32_t bits = ((uint32_t) exp + 119u) << 23 | ((uint32_t) man << 20); + return rocmfp4_u32_as_f32(bits); +} + +static __device__ __forceinline__ uint8_t rocmfpx_nearest_scale_ue4m3_cuda(float target_scale) { + if (!(target_scale > 0.0f) || !isfinite(target_scale)) { + return 0; + } + + uint8_t lo = 1; + uint8_t hi = 0x7e; + while (lo < hi) { + const uint8_t mid = lo + (hi - lo) / 2; + if (rocmfpx_ue4m3_to_fp32_finite(mid) < target_scale) { + lo = mid + 1; + } else { + hi = mid; + } + } + + if (lo == 1) { + return 1; + } + + const float hi_scale = rocmfpx_ue4m3_to_fp32_finite(lo); + const float lo_scale = rocmfpx_ue4m3_to_fp32_finite((uint8_t) (lo - 1)); + return (target_scale - lo_scale <= hi_scale - target_scale) ? (uint8_t) (lo - 1) : lo; +} + +static __device__ __forceinline__ int8_t rocmfp4_decode_i8(uint8_t q) { + q &= 0x0f; + const int mag3 = q & 0x07; + const int mag = mag3 <= 4 ? mag3 : 2*mag3 - 4; + return (q & 0x08) ? -mag : mag; +} diff --git a/ggml/rocmfpx/rocmfpx_hip_codebook.cuh b/ggml/rocmfpx/rocmfpx_hip_codebook.cuh new file mode 100644 index 000000000000..b9e36528c2fc --- /dev/null +++ b/ggml/rocmfpx/rocmfpx_hip_codebook.cuh @@ -0,0 +1,53 @@ +// ROCmFPx quant formats, hand-ported into this fork. +// +// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. +// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. +// +// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors +// retain their authorship and MIT license credit. See LICENSE. + +#pragma once + +#include <cstdint> + +static __device__ __constant__ const int8_t rocmfpx_fp3_codebook[8] = { + 0, 1, 2, 4, 0, -1, -2, -4, +}; + +static __device__ __constant__ const int8_t rocmfpx_fp6_codebook[64] = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + 0, -1, -2, -3, -4, -5, -6, -7, + -8, -9, -10, -11, -12, -13, -14, -15, + -16, -17, -18, -19, -20, -21, -22, -23, + -24, -25, -26, -27, -28, -29, -30, -31, +}; + +static __device__ __forceinline__ int rocmfpx_pack4_fp3_codes(const uint32_t bits12) { + const uint32_t c0 = (bits12 >> 0) & 7u; + const uint32_t c1 = (bits12 >> 3) & 7u; + const uint32_t c2 = (bits12 >> 6) & 7u; + const uint32_t c3 = (bits12 >> 9) & 7u; + const char4 v = make_char4( + rocmfpx_fp3_codebook[c0], + rocmfpx_fp3_codebook[c1], + rocmfpx_fp3_codebook[c2], + rocmfpx_fp3_codebook[c3]); + return *((const int *) &v); +} + +static __device__ __forceinline__ int rocmfpx_pack4_fp6_codes(const uint32_t bits24) { + const uint32_t c0 = (bits24 >> 0) & 63u; + const uint32_t c1 = (bits24 >> 6) & 63u; + const uint32_t c2 = (bits24 >> 12) & 63u; + const uint32_t c3 = (bits24 >> 18) & 63u; + + const char4 v = make_char4( + rocmfpx_fp6_codebook[c0], + rocmfpx_fp6_codebook[c1], + rocmfpx_fp6_codebook[c2], + rocmfpx_fp6_codebook[c3]); + return *((const int *) &v); +} diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index 3ab92800552e..115ad6d14a94 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -21,6 +21,7 @@ #endif #endif #include "ggml-common.h" +#include "../../rocmfp4/rocmfp4.h" #include <array> #include <algorithm> @@ -40,6 +41,9 @@ #include "vendors/cuda.h" #endif // defined(GGML_USE_HIP) +// device-side UE4M3 scale decoder for ROCmFP4 (needs the runtime header above) +#include "../../rocmfp4/rocmfp4_hip_scale.cuh" + #define STRINGIZE_IMPL(...) #__VA_ARGS__ #define STRINGIZE(...) STRINGIZE_IMPL(__VA_ARGS__) @@ -1086,6 +1090,20 @@ struct ggml_cuda_type_traits<GGML_TYPE_NVFP4> { static constexpr int bs = sizeof(block_nvfp4); }; +template<> +struct ggml_cuda_type_traits<GGML_TYPE_Q4_0_ROCMFP4> { + static constexpr int qk = QK_ROCMFP4; + static constexpr int qr = QR_ROCMFP4; + static constexpr int qi = QI_ROCMFP4; +}; + +template<> +struct ggml_cuda_type_traits<GGML_TYPE_Q4_0_ROCMFP4_FAST> { + static constexpr int qk = QK_ROCMFP4; + static constexpr int qr = QR_ROCMFP4; + static constexpr int qi = QI_ROCMFP4; +}; + template<> struct ggml_cuda_type_traits<GGML_TYPE_Q2_K> { static constexpr int qk = QK_K; diff --git a/ggml/src/ggml-cuda/convert.cu b/ggml/src/ggml-cuda/convert.cu index 47cab5f980d4..6677146c0cfc 100644 --- a/ggml/src/ggml-cuda/convert.cu +++ b/ggml/src/ggml-cuda/convert.cu @@ -243,6 +243,20 @@ static __global__ void dequantize_block_mxfp4(const void * __restrict__ vx, dst_ dequantize_mxfp4(vx, i, yy + i*QK_K, threadIdx.x); } +template<typename dst_t> +static __global__ void dequantize_block_rocmfp4(const void * __restrict__ vx, dst_t * __restrict__ yy) { + const int64_t i = blockIdx.x; + + dequantize_rocmfp4(vx, i, yy + i*QK_K, threadIdx.x); +} + +template<typename dst_t> +static __global__ void dequantize_block_rocmfp4_fast(const void * __restrict__ vx, dst_t * __restrict__ yy) { + const int64_t i = blockIdx.x; + + dequantize_rocmfp4_fast(vx, i, yy + i*QK_K, threadIdx.x); +} + template <int qk, int qr, dequantize_kernel_t dequantize_kernel, typename dst_t> static void dequantize_block_cuda(const void * vx, dst_t * y, const int64_t ne00, const int64_t ne01, const int64_t ne02, const int64_t ne03, @@ -374,6 +388,18 @@ static void dequantize_row_mxfp4_cuda(const void * vx, dst_t * y, const int64_t dequantize_block_mxfp4<<<nb, 32, 0, stream>>>(vx, y); } +template<typename dst_t> +static void dequantize_row_rocmfp4_cuda(const void * vx, dst_t * y, const int64_t k, cudaStream_t stream) { + const int nb = (k + QK_K - 1) / QK_K; + dequantize_block_rocmfp4<<<nb, 32, 0, stream>>>(vx, y); +} + +template<typename dst_t> +static void dequantize_row_rocmfp4_fast_cuda(const void * vx, dst_t * y, const int64_t k, cudaStream_t stream) { + const int nb = (k + QK_K - 1) / QK_K; + dequantize_block_rocmfp4_fast<<<nb, 32, 0, stream>>>(vx, y); +} + template <typename dst_t> static __global__ void dequantize_block_nvfp4( const void * __restrict__ vx, @@ -533,6 +559,10 @@ to_bf16_cuda_t ggml_get_to_bf16_cuda(ggml_type type) { return dequantize_row_iq3_s_cuda; case GGML_TYPE_MXFP4: return dequantize_row_mxfp4_cuda; + case GGML_TYPE_Q4_0_ROCMFP4: + return dequantize_row_rocmfp4_cuda; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + return dequantize_row_rocmfp4_fast_cuda; case GGML_TYPE_NVFP4: return dequantize_row_nvfp4_cuda; case GGML_TYPE_F32: @@ -593,6 +623,10 @@ to_fp16_cuda_t ggml_get_to_fp16_cuda(ggml_type type) { return dequantize_row_iq3_s_cuda; case GGML_TYPE_MXFP4: return dequantize_row_mxfp4_cuda; + case GGML_TYPE_Q4_0_ROCMFP4: + return dequantize_row_rocmfp4_cuda; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + return dequantize_row_rocmfp4_fast_cuda; case GGML_TYPE_NVFP4: return dequantize_row_nvfp4_cuda; case GGML_TYPE_F32: @@ -650,6 +684,10 @@ to_fp32_cuda_t ggml_get_to_fp32_cuda(ggml_type type) { return dequantize_row_iq3_s_cuda; case GGML_TYPE_MXFP4: return dequantize_row_mxfp4_cuda; + case GGML_TYPE_Q4_0_ROCMFP4: + return dequantize_row_rocmfp4_cuda; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + return dequantize_row_rocmfp4_fast_cuda; case GGML_TYPE_NVFP4: return dequantize_row_nvfp4_cuda; case GGML_TYPE_F16: diff --git a/ggml/src/ggml-cuda/dequantize.cuh b/ggml/src/ggml-cuda/dequantize.cuh index fd9b6d6e01bf..6489e65442f4 100644 --- a/ggml/src/ggml-cuda/dequantize.cuh +++ b/ggml/src/ggml-cuda/dequantize.cuh @@ -525,3 +525,37 @@ template<typename dst_t> static __device__ __forceinline__ void dequantize_mxfp4(const void * vx, const int64_t ib, dst_t * yy, const int tid) { dequantize_mxfp4<dst_t, dst_t *>(vx, ib, yy, tid); } + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_rocmfp4(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { + + const block_rocmfp4 * x = (const block_rocmfp4 *) vx + ibs*(QK_K/QK_ROCMFP4); + + const int64_t il = tid/8; // 0...3 + const int64_t ib = tid%8; // 0...7 + dst_t * y = yy + 32*ib + 4*il; + const uint8_t * q4 = x[ib].qs + 4*il; + // dual UE4M3 scales: low nibbles are weights j (e[0]), high nibbles are weights j+16 (e[1]) + const float d0 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[0]); + const float d1 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[1]); + for (int j = 0; j < 4; ++j) { + y[j+ 0] = ggml_cuda_cast<dst_t>(d0 * kvalues_rocmfp4[q4[j] & 0xf]); + y[j+16] = ggml_cuda_cast<dst_t>(d1 * kvalues_rocmfp4[q4[j] >> 4]); + } +} + +template<typename dst_t> +static __device__ __forceinline__ void dequantize_rocmfp4_fast(const void * vx, const int64_t ibs, dst_t * yy, const int tid) { + + const block_rocmfp4_fast * x = (const block_rocmfp4_fast *) vx + ibs*(QK_K/QK_ROCMFP4); + + const int64_t il = tid/8; // 0...3 + const int64_t ib = tid%8; // 0...7 + dst_t * y = yy + 32*ib + 4*il; + const uint8_t * q4 = x[ib].qs + 4*il; + const float d = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e); + for (int j = 0; j < 4; ++j) { + y[j+ 0] = ggml_cuda_cast<dst_t>(d * kvalues_rocmfp4[q4[j] & 0xf]); + y[j+16] = ggml_cuda_cast<dst_t>(d * kvalues_rocmfp4[q4[j] >> 4]); + } +} diff --git a/ggml/src/ggml-cuda/getrows.cu b/ggml/src/ggml-cuda/getrows.cu index ffcb51f00912..d80ed22a172a 100644 --- a/ggml/src/ggml-cuda/getrows.cu +++ b/ggml/src/ggml-cuda/getrows.cu @@ -470,6 +470,14 @@ static void ggml_cuda_get_rows_switch_src0_type( get_rows_cuda_kq<32, dst_t, dequantize_mxfp4<dst_t>>(src0_d, src1_d, dst_d, ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream); break; + case GGML_TYPE_Q4_0_ROCMFP4: + get_rows_cuda_kq<32, dst_t, dequantize_rocmfp4<dst_t>>(src0_d, src1_d, dst_d, + ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream); + break; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + get_rows_cuda_kq<32, dst_t, dequantize_rocmfp4_fast<dst_t>>(src0_d, src1_d, dst_d, + ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream); + break; default: GGML_ABORT("%s: unsupported src0 type: %s\n", __func__, ggml_type_name(src0_type)); break; diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index c465879d7e6b..a93b5905cfec 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -7152,6 +7152,8 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g case GGML_TYPE_Q8_0: case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: @@ -7205,6 +7207,8 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g return true; case GGML_TYPE_IQ4_NL: case GGML_TYPE_MXFP4: + case GGML_TYPE_Q4_0_ROCMFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: // 32-value sub-blocks, the row size does not guarantee // the QK_K super-blocks the get_rows kernel iterates on return op->src[0]->ne[0] % QK_K == 0; diff --git a/ggml/src/ggml-cuda/mmvq.cu b/ggml/src/ggml-cuda/mmvq.cu index 101da1f5acee..56a41ddc4216 100644 --- a/ggml/src/ggml-cuda/mmvq.cu +++ b/ggml/src/ggml-cuda/mmvq.cu @@ -49,6 +49,8 @@ static constexpr __device__ vec_dot_q_cuda_t get_vec_dot_q_cuda(ggml_type type) case GGML_TYPE_Q8_0: return vec_dot_q8_0_q8_1; case GGML_TYPE_MXFP4: return vec_dot_mxfp4_q8_1; case GGML_TYPE_NVFP4: return vec_dot_nvfp4_q8_1; + case GGML_TYPE_Q4_0_ROCMFP4: return vec_dot_q4_0_rocmfp4_q8_1; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: return vec_dot_q4_0_rocmfp4_fast_q8_1; case GGML_TYPE_Q2_K: return vec_dot_q2_K_q8_1; case GGML_TYPE_Q3_K: return vec_dot_q3_K_q8_1; case GGML_TYPE_Q4_K: return vec_dot_q4_K_q8_1; @@ -78,6 +80,8 @@ static constexpr __host__ __device__ int get_vdr_mmvq(ggml_type type) { case GGML_TYPE_Q8_0: return VDR_Q8_0_Q8_1_MMVQ; case GGML_TYPE_MXFP4: return VDR_MXFP4_Q8_1_MMVQ; case GGML_TYPE_NVFP4: return VDR_NVFP4_Q8_1_MMVQ; + case GGML_TYPE_Q4_0_ROCMFP4: return VDR_ROCMFP4_Q8_1_MMVQ; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: return VDR_ROCMFP4_Q8_1_MMVQ; case GGML_TYPE_Q2_K: return VDR_Q2_K_Q8_1_MMVQ; case GGML_TYPE_Q3_K: return VDR_Q3_K_Q8_1_MMVQ; case GGML_TYPE_Q4_K: return VDR_Q4_K_Q8_1_MMVQ; @@ -2843,6 +2847,18 @@ static void mul_mat_vec_q_switch_type( nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, stride_channel_dst, nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride, stream); break; + case GGML_TYPE_Q4_0_ROCMFP4: + mul_mat_vec_q_switch_ncols_dst<GGML_TYPE_Q4_0_ROCMFP4> + (vx, vy, ids, fusion, dst, ncols_x, nrows_x, ncols_dst, stride_row_x, stride_col_y, stride_col_dst, + nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, stride_channel_dst, + nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride, stream); + break; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + mul_mat_vec_q_switch_ncols_dst<GGML_TYPE_Q4_0_ROCMFP4_FAST> + (vx, vy, ids, fusion, dst, ncols_x, nrows_x, ncols_dst, stride_row_x, stride_col_y, stride_col_dst, + nchannels_x, nchannels_y, nchannels_dst, stride_channel_x, stride_channel_y, stride_channel_dst, + nsamples_x, nsamples_dst, stride_sample_x, stride_sample_y, stride_sample_dst, ids_stride, stream); + break; case GGML_TYPE_NVFP4: mul_mat_vec_q_switch_ncols_dst<GGML_TYPE_NVFP4> (vx, vy, ids, fusion, dst, ncols_x, nrows_x, ncols_dst, stride_row_x, stride_col_y, stride_col_dst, diff --git a/ggml/src/ggml-cuda/vecdotq.cuh b/ggml/src/ggml-cuda/vecdotq.cuh index b3e49e0072af..7999f7011bc9 100644 --- a/ggml/src/ggml-cuda/vecdotq.cuh +++ b/ggml/src/ggml-cuda/vecdotq.cuh @@ -328,6 +328,56 @@ static __device__ __forceinline__ float vec_dot_mxfp4_q8_1( return d * sumi; } +#define VDR_ROCMFP4_Q8_1_MMVQ 2 + +// Dual UE4M3 half-block scales: low nibbles of qs[j] are weights j (scale e[0]), +// high nibbles are weights j + 16 (scale e[1]). Two int-dot accumulators, matching +// the q8_1 slot pairing used by vec_dot_mxfp4_q8_1 above. +static __device__ __forceinline__ float vec_dot_q4_0_rocmfp4_q8_1( + const void * __restrict__ vbq, const block_q8_1 * __restrict__ bq8_1, const int & kbx, const int & iqs) { + + const block_rocmfp4 * bq4 = (const block_rocmfp4 *) vbq + kbx; + + const int * q8 = (const int *) bq8_1->qs + iqs; + + int sumi0 = 0; + int sumi1 = 0; +#pragma unroll + for (int l = 0; l < VDR_ROCMFP4_Q8_1_MMVQ; ++l) { + const int aux_q4 = get_int_b1(bq4->qs, iqs + l); + const int2 v = get_int_from_table_16(aux_q4, kvalues_rocmfp4); + + sumi0 = ggml_cuda_dp4a(v.x, q8[l + 0], sumi0); + sumi1 = ggml_cuda_dp4a(v.y, q8[l + 4], sumi1); + } + + const float dq8 = __low2float(bq8_1->ds); + const float d0 = rocmfp4_ue4m3_to_fp32_half_finite(bq4->e[0]) * dq8; + const float d1 = rocmfp4_ue4m3_to_fp32_half_finite(bq4->e[1]) * dq8; + return d0 * sumi0 + d1 * sumi1; +} + +static __device__ __forceinline__ float vec_dot_q4_0_rocmfp4_fast_q8_1( + const void * __restrict__ vbq, const block_q8_1 * __restrict__ bq8_1, const int & kbx, const int & iqs) { + + const block_rocmfp4_fast * bq4 = (const block_rocmfp4_fast *) vbq + kbx; + + const int * q8 = (const int *) bq8_1->qs + iqs; + + int sumi = 0; +#pragma unroll + for (int l = 0; l < VDR_ROCMFP4_Q8_1_MMVQ; ++l) { + const int aux_q4 = get_int_b1(bq4->qs, iqs + l); + const int2 v = get_int_from_table_16(aux_q4, kvalues_rocmfp4); + + sumi = ggml_cuda_dp4a(v.x, q8[l + 0], sumi); + sumi = ggml_cuda_dp4a(v.y, q8[l + 4], sumi); + } + + const float d = rocmfp4_ue4m3_to_fp32_half_finite(bq4->e) * __low2float(bq8_1->ds); + return d * sumi; +} + #define VDR_NVFP4_Q8_1_MMVQ 4 #define VDR_NVFP4_Q8_1_MMQ 8 From b0b72220f7a17d63462b02148b428bbef1350a53 Mon Sep 17 00:00:00 2001 From: Baraxanx <baraxnaxgaming@gmail.com> Date: Mon, 7 Sep 2026 21:00:59 -0400 Subject: [PATCH 200/212] cuda: MMQ tile path for Q4_0_ROCMFP4_FAST (dp4a + WMMA/MFMA layouts) Step 2 of the Strix Halo HIP plan: give the production FP4 format an MMQ path so prefill batches (>8, incl. spec-verify at wide ubatch and MoE routing) stop falling through to dequant+hipBLAS purely because the type was absent from the MMQ switches. Mirrors MXFP4 exactly - same block size (32), one UE4M3 scale per 32 values, SRAM_LAYOUT_Q8_1 tiles, q8_0_q8_1 vec_dot kernels (dp4a and MMA data layout), D4 y-side quantizer. Differences from MXFP4: kvalues_rocmfp4 codebook (Codebook10, max level 10 not 12) and the UE4M3 scale decoder without the e8m0 *0.5 factor. Tile rows mirror this master's current MXFP4 config set, incl. the 128/64/128 row. Not added to Blackwell configs on purpose: use_native_fp4 is false for this type there, so it falls through to the Ampere config like NVFP4-generic does. Dual-scale Q4_0_ROCMFP4 (2 scales/block) deferred to a follow-up slice (NVFP4-style per-16 convention). Validated: test-backend-ops vs CPU ref on RTX 4090 CUDA (default build -p rocmfp4: 288 OK / 0 FAIL; GGML_CUDA_FORCE_MMQ=ON -o MUL_MAT -p rocmfp4: 42 OK / 0 FAIL) and on gfx1151 HIP (-o MUL_MAT -b ROCm0 -p rocmfp4: 78 OK / 0 FAIL). Measured on Ryzen AI Max+ 395 against a rebuilt merge-base in the same session: pp512 +6.1%, pp2048 +5.4% (palindrome x2, zero overlap); MUL_MAT m=4096,n=512 kernel 5389 -> 3503 us (x1.54). --- ggml/src/ggml-cuda/mmq-config-ampere.cuh | 16 +++++ ggml/src/ggml-cuda/mmq-config-cdna.cuh | 7 ++ ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh | 11 +++ .../src/ggml-cuda/mmq-config-pascal-older.cuh | 11 +++ ggml/src/ggml-cuda/mmq-config-rdna2.cuh | 11 +++ ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh | 12 ++++ ggml/src/ggml-cuda/mmq-config-rdna3.cuh | 11 +++ ggml/src/ggml-cuda/mmq-config-rdna4.cuh | 12 ++++ ggml/src/ggml-cuda/mmq-load-tiles.cuh | 70 +++++++++++++++++++ ggml/src/ggml-cuda/mmq.cu | 4 ++ ggml/src/ggml-cuda/mmq.cuh | 16 +++++ .../template-instances/generate_cu_files.py | 2 +- .../mmq-instance-q4_0_rocmfp4_fast.cu | 5 ++ ggml/src/ggml-cuda/vecdotq.cuh | 1 + 14 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 ggml/src/ggml-cuda/template-instances/mmq-instance-q4_0_rocmfp4_fast.cu diff --git a/ggml/src/ggml-cuda/mmq-config-ampere.cuh b/ggml/src/ggml-cuda/mmq-config-ampere.cuh index 9f9fd197382f..fa62f34d6876 100644 --- a/ggml/src/ggml-cuda/mmq-config-ampere.cuh +++ b/ggml/src/ggml-cuda/mmq-config-ampere.cuh @@ -346,21 +346,37 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 256, 1, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 256, 1, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 1, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_NVFP4, 256, 1, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, true); CASE(GGML_TYPE_NVFP4, 256, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, true); diff --git a/ggml/src/ggml-cuda/mmq-config-cdna.cuh b/ggml/src/ggml-cuda/mmq-config-cdna.cuh index 4a8d89f72019..18d9ac6dc698 100644 --- a/ggml/src/ggml-cuda/mmq-config-cdna.cuh +++ b/ggml/src/ggml-cuda/mmq-config-cdna.cuh @@ -166,12 +166,19 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_NVFP4, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, true); CASE(GGML_TYPE_NVFP4, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, true); diff --git a/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh b/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh index 83eb7c146e11..87b76b6c8fc0 100644 --- a/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh +++ b/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh @@ -246,16 +246,27 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh b/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh index 2a8dc9e1a93e..31078d1462b5 100644 --- a/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh +++ b/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh @@ -246,16 +246,27 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-rdna2.cuh b/ggml/src/ggml-cuda/mmq-config-rdna2.cuh index 8324d9e1a830..6cd7ccc17c72 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna2.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna2.cuh @@ -246,16 +246,27 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh b/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh index f8a9e6350926..aaadfaa1f3bf 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh @@ -261,17 +261,29 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh index 3a3ef7bd9c09..8af3423d7d68 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh @@ -247,16 +247,27 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-rdna4.cuh b/ggml/src/ggml-cuda/mmq-config-rdna4.cuh index 9293d9d55885..8ef32188f690 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna4.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna4.cuh @@ -261,17 +261,29 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); + CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-load-tiles.cuh b/ggml/src/ggml-cuda/mmq-load-tiles.cuh index 40c825405e26..7a37603fa22d 100644 --- a/ggml/src/ggml-cuda/mmq-load-tiles.cuh +++ b/ggml/src/ggml-cuda/mmq-load-tiles.cuh @@ -1862,3 +1862,73 @@ template <ggml_type type, int J, bool fallback> static __device__ __forceinline_ x_u32_scale[i*sram_stride] = get_int_b4(bxi->d, 0); } } + +// Q4_0_ROCMFP4_FAST: 32 values / block, one UE4M3 scale per 32 values - same tile structure as MXFP4. +// Differences from the mxfp4 loader above: block_rocmfp4_fast layout, kvalues_rocmfp4 codebook +// (Codebook10), and the UE4M3 scale decoder without the e8m0 0.5 factor. +template <ggml_type type, int J, bool fallback> static __device__ __forceinline__ void ggml_cuda_mmq_load_tiles_q4_0_rocmfp4_fast( + const char * __restrict__ x, int * __restrict__ x_tile, const int kbx0, const int i_max, const int stride) { + constexpr int warp_size = ggml_cuda_get_physical_warp_size(); + constexpr int nwarps = ggml_cuda_mmq_get_nthreads(type, J, fallback) / warp_size; + constexpr int I = ggml_cuda_mmq_get_I(type, J, fallback); + constexpr int sram_stride = ggml_cuda_mmq_get_sram_stride(type, J, fallback); + +#if defined(AMD_MFMA_AVAILABLE) || defined(TURING_MMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE) + int * x_qs = (int *) x_tile; + float * x_df = (float *) (x_qs + MMQ_TILE_NE_K*2); +#else + constexpr tile_x_sizes txs = mmq_get_dp4a_tile_x_sizes(GGML_TYPE_Q4_0_ROCMFP4_FAST, I); + int * x_qs = (int *) x_tile; + float * x_df = (float *) (x_qs + txs.qs); +#endif // defined(AMD_MFMA_AVAILABLE) || defined(TURING_MMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE) + + constexpr int threads_per_row = MMQ_ITER_K / (4 * QR_ROCMFP4); + constexpr int nrows = warp_size / threads_per_row; + const int txi = warp_size > threads_per_row ? threadIdx.x % threads_per_row : threadIdx.x; + const int kbx = txi / QI_ROCMFP4; + const int kqsx = txi % QI_ROCMFP4; + +#pragma unroll + for (int i0 = 0; i0 < I; i0 += nrows*nwarps) { + int i = i0 + (nrows == 1 ? threadIdx.y : threadIdx.y*nrows + threadIdx.x/threads_per_row); + + if (fallback) { + i = min(i, i_max); + } + + const block_rocmfp4_fast * bxi = (const block_rocmfp4_fast *) x + kbx0 + i*stride + kbx; + + const int aux_q4 = get_int_b1(bxi->qs, kqsx); + const int2 v = get_int_from_table_16(aux_q4, kvalues_rocmfp4); + const int k0 = kbx * (2 * QI_ROCMFP4) + kqsx; + +#if defined(AMD_MFMA_AVAILABLE) || defined(TURING_MMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE) + x_qs[i*sram_stride + k0 + 0] = v.x; + x_qs[i*sram_stride + k0 + QI_ROCMFP4] = v.y; +#else + x_qs[i*(2*MMQ_TILE_NE_K + 1) + k0 + 0] = v.x; + x_qs[i*(2*MMQ_TILE_NE_K + 1) + k0 + QI_ROCMFP4] = v.y; +#endif // defined(AMD_MFMA_AVAILABLE) || defined(TURING_MMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE) + } + + constexpr int blocks_per_tile_x_row = MMQ_TILE_NE_K / QI_ROCMFP4; + constexpr int rows_per_warp = warp_size / blocks_per_tile_x_row; + const int kbxd = threadIdx.x % blocks_per_tile_x_row; + +#pragma unroll + for (int i0 = 0; i0 < I; i0 += nwarps * rows_per_warp) { + int i = i0 + threadIdx.y * rows_per_warp + threadIdx.x / blocks_per_tile_x_row; + + if (fallback) { + i = min(i, i_max); + } + + const block_rocmfp4_fast * bxi = (const block_rocmfp4_fast *) x + kbx0 + i*stride + kbxd; + +#if defined(AMD_MFMA_AVAILABLE) || defined(TURING_MMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE) + x_df[i*sram_stride + kbxd] = rocmfp4_ue4m3_to_fp32_half_finite(bxi->e); +#else + x_df[i*(MMQ_TILE_NE_K/QI_ROCMFP4) + i/QI_ROCMFP4 + kbxd] = rocmfp4_ue4m3_to_fp32_half_finite(bxi->e); +#endif // defined(AMD_MFMA_AVAILABLE) || defined(TURING_MMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE) + } +} diff --git a/ggml/src/ggml-cuda/mmq.cu b/ggml/src/ggml-cuda/mmq.cu index 0e5479dc8eea..31e5d3161e6c 100644 --- a/ggml/src/ggml-cuda/mmq.cu +++ b/ggml/src/ggml-cuda/mmq.cu @@ -104,6 +104,9 @@ static void ggml_cuda_mul_mat_q_switch_type(ggml_backend_cuda_context & ctx, con case GGML_TYPE_NVFP4: mul_mat_q_case<GGML_TYPE_NVFP4>(ctx, args, stream); break; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + mul_mat_q_case<GGML_TYPE_Q4_0_ROCMFP4_FAST>(ctx, args, stream); + break; default: GGML_ABORT("fatal error"); break; @@ -494,6 +497,7 @@ bool ggml_cuda_should_use_mmq(enum ggml_type type, int cc, int64_t ne11, int64_t // ------------------------------------------------- case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: + case GGML_TYPE_Q4_0_ROCMFP4_FAST: mmq_supported = true; break; default: diff --git a/ggml/src/ggml-cuda/mmq.cuh b/ggml/src/ggml-cuda/mmq.cuh index 333dac85dcfb..26df051abfc5 100644 --- a/ggml/src/ggml-cuda/mmq.cuh +++ b/ggml/src/ggml-cuda/mmq.cuh @@ -75,6 +75,8 @@ static mmq_q8_1_ds_layout mmq_get_q8_1_ds_layout(const ggml_type type_x) { return MMQ_Q8_1_DS_LAYOUT_D4; case GGML_TYPE_NVFP4: return MMQ_Q8_1_DS_LAYOUT_D4; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + return MMQ_Q8_1_DS_LAYOUT_D4; case GGML_TYPE_Q2_K: return MMQ_Q8_1_DS_LAYOUT_D2S6; case GGML_TYPE_Q3_K: @@ -406,6 +408,7 @@ static constexpr __host__ __device__ tile_x_sizes mmq_get_dp4a_tile_x_sizes(ggml case GGML_TYPE_Q8_0: return MMQ_DP4A_TXS_Q8_0; case GGML_TYPE_MXFP4: return MMQ_DP4A_TXS_Q8_1; case GGML_TYPE_NVFP4: return MMQ_DP4A_TXS_Q8_0_16; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: return MMQ_DP4A_TXS_Q8_1; case GGML_TYPE_Q2_K: return MMQ_DP4A_TXS_Q2_K; case GGML_TYPE_Q3_K: return MMQ_DP4A_TXS_Q3_K; case GGML_TYPE_Q4_K: return MMQ_DP4A_TXS_Q4_K; @@ -686,6 +689,12 @@ static constexpr __device__ ggml_cuda_mmq_util_funcs ggml_cuda_mmq_get_util_func ggml_cuda_mmq_load_tiles_nvfp4<type, J, fallback>, ggml_cuda_mmq_vec_dot_q8_0_16_q8_1_dp4a<type, J, fallback>, ggml_cuda_mmq_write_back_dp4a<type, J, fallback>); + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + return ggml_cuda_mmq_util_funcs( + VDR_ROCMFP4_FAST_Q8_1_MMQ, + ggml_cuda_mmq_load_tiles_q4_0_rocmfp4_fast<type, J, fallback>, + ggml_cuda_mmq_vec_dot_q8_0_q8_1_dp4a<type, J, fallback>, + ggml_cuda_mmq_write_back_dp4a<type, J, fallback>); default: return ggml_cuda_mmq_util_funcs(1, nullptr, nullptr, nullptr); } @@ -850,6 +859,12 @@ static constexpr __device__ ggml_cuda_mmq_util_funcs ggml_cuda_mmq_get_util_func ggml_cuda_mmq_load_tiles_nvfp4<type, J, fallback>, ggml_cuda_mmq_vec_dot_q8_0_16_q8_1_mma<type, J, fallback>, ggml_cuda_mmq_write_back_mma<type, J, fallback>); + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + return ggml_cuda_mmq_util_funcs( + -1, + ggml_cuda_mmq_load_tiles_q4_0_rocmfp4_fast<type, J, fallback>, + ggml_cuda_mmq_vec_dot_q8_0_q8_1_mma<type, J, fallback, MMQ_Q8_1_DS_LAYOUT_D4>, + ggml_cuda_mmq_write_back_mma<type, J, fallback>); default: return ggml_cuda_mmq_util_funcs(1, nullptr, nullptr, nullptr); } @@ -1930,6 +1945,7 @@ extern DECL_MMQ_CASE(GGML_TYPE_IQ4_XS); // ----------------------------------------- extern DECL_MMQ_CASE(GGML_TYPE_MXFP4); extern DECL_MMQ_CASE(GGML_TYPE_NVFP4); +extern DECL_MMQ_CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST); // ------------------------------------------------------------------------------------------------------------------------- diff --git a/ggml/src/ggml-cuda/template-instances/generate_cu_files.py b/ggml/src/ggml-cuda/template-instances/generate_cu_files.py index d7cd271675e0..d12cd8e98da4 100755 --- a/ggml/src/ggml-cuda/template-instances/generate_cu_files.py +++ b/ggml/src/ggml-cuda/template-instances/generate_cu_files.py @@ -40,7 +40,7 @@ "GGML_TYPE_Q4_0", "GGML_TYPE_Q4_1", "GGML_TYPE_Q5_0", "GGML_TYPE_Q5_1", "GGML_TYPE_Q8_0", "GGML_TYPE_Q2_K", "GGML_TYPE_Q3_K", "GGML_TYPE_Q4_K", "GGML_TYPE_Q5_K", "GGML_TYPE_Q6_K", "GGML_TYPE_IQ2_XXS", "GGML_TYPE_IQ2_XS", "GGML_TYPE_IQ2_S", "GGML_TYPE_IQ3_XXS", "GGML_TYPE_IQ3_S", - "GGML_TYPE_IQ1_S", "GGML_TYPE_IQ4_NL", "GGML_TYPE_IQ4_XS", "GGML_TYPE_MXFP4", "GGML_TYPE_NVFP4" + "GGML_TYPE_IQ1_S", "GGML_TYPE_IQ4_NL", "GGML_TYPE_IQ4_XS", "GGML_TYPE_MXFP4", "GGML_TYPE_NVFP4", "GGML_TYPE_Q4_0_ROCMFP4_FAST" ] SOURCE_MMQ = """// This file has been autogenerated by generate_cu_files.py, do not edit manually. diff --git a/ggml/src/ggml-cuda/template-instances/mmq-instance-q4_0_rocmfp4_fast.cu b/ggml/src/ggml-cuda/template-instances/mmq-instance-q4_0_rocmfp4_fast.cu new file mode 100644 index 000000000000..df63d09ae0c6 --- /dev/null +++ b/ggml/src/ggml-cuda/template-instances/mmq-instance-q4_0_rocmfp4_fast.cu @@ -0,0 +1,5 @@ +// This file has been autogenerated by generate_cu_files.py, do not edit manually. + +#include "../mmq.cuh" + +DECL_MMQ_CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST); diff --git a/ggml/src/ggml-cuda/vecdotq.cuh b/ggml/src/ggml-cuda/vecdotq.cuh index 7999f7011bc9..37a04cfd165f 100644 --- a/ggml/src/ggml-cuda/vecdotq.cuh +++ b/ggml/src/ggml-cuda/vecdotq.cuh @@ -306,6 +306,7 @@ template <int vdr> static __device__ __forceinline__ float vec_dot_q8_0_16_q8_1_ #define VDR_MXFP4_Q8_1_MMVQ 2 #define VDR_MXFP4_Q8_1_MMQ 4 +#define VDR_ROCMFP4_FAST_Q8_1_MMQ 4 static __device__ __forceinline__ float vec_dot_mxfp4_q8_1( const void * __restrict__ vbq, const block_q8_1 * __restrict__ bq8_1, const int & kbx, const int & iqs) { From 3db7cc3b639ed960705981c097379444663d9c1b Mon Sep 17 00:00:00 2001 From: Baraxanx <baraxnaxgaming@gmail.com> Date: Mon, 28 Sep 2026 11:54:47 -0400 Subject: [PATCH 201/212] cuda: drop unreferenced rocmfp4 HIP helper files Review finding (halo-box/strix-llama.cpp#40): three files added in the first commit are compiled by nothing and referenced nowhere: - ggml/rocmfp4/rocmfp4_hip.cu - two extern "C" dequant kernels; not in any CMake source list (ggml-hip globs only ../ggml-cuda/*.cu and the template instances) and no callers, so unreachable even from tests. - ggml/rocmfp4/rocmfp4_hip_codebook.cuh - included by nothing; live MMVQ/MMQ paths use get_int_from_table_16 + kvalues_rocmfp4 from ggml-common.h, which already lowers to permb on HIP. The single-nibble perm variant was for an FA K/V decode path not present in this PR. - ggml/rocmfpx/rocmfpx_hip_codebook.cuh - same, included by nothing. The live UE4M3 scale decoder is rocmfp4_hip_scale.cuh (included via common.cuh) and stays. No behavior change: verified zero references to these files/symbols in the rest of the tree. --- ggml/rocmfp4/rocmfp4_hip.cu | 93 --------------------------- ggml/rocmfp4/rocmfp4_hip_codebook.cuh | 87 ------------------------- ggml/rocmfpx/rocmfpx_hip_codebook.cuh | 53 --------------- 3 files changed, 233 deletions(-) delete mode 100644 ggml/rocmfp4/rocmfp4_hip.cu delete mode 100644 ggml/rocmfp4/rocmfp4_hip_codebook.cuh delete mode 100644 ggml/rocmfpx/rocmfpx_hip_codebook.cuh diff --git a/ggml/rocmfp4/rocmfp4_hip.cu b/ggml/rocmfp4/rocmfp4_hip.cu deleted file mode 100644 index 3174743cc41d..000000000000 --- a/ggml/rocmfp4/rocmfp4_hip.cu +++ /dev/null @@ -1,93 +0,0 @@ -// ROCmFPx quant formats, hand-ported into this fork. -// -// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. -// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. -// -// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors -// retain their authorship and MIT license credit. See LICENSE. - -#include "rocmfp4.h" - -#include <hip/hip_runtime.h> - -#include "rocmfp4_hip_scale.cuh" - -// Standalone ROCm/HIP dequant kernel for integration tests and future fused -// paths. One lane owns one packed byte and writes the matching low/high -// half-block values, so each byte is read once. -extern "C" __global__ void rocmfp4_dequantize_q4_0_f32_kernel( - const block_rocmfp4 * __restrict__ x, - float * __restrict__ y, - int64_t k) { - const int64_t packed_idx = (int64_t) blockIdx.x*blockDim.x + threadIdx.x; - const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; - const int64_t packed_count = nblocks * (QK_ROCMFP4/2); - - if (packed_idx >= packed_count) { - return; - } - - const int64_t ib = packed_idx / (QK_ROCMFP4/2); - const int tid = packed_idx - ib*(QK_ROCMFP4/2); - const int64_t base = ib*QK_ROCMFP4; - const uint8_t packed = x[ib].qs[tid]; - const float d0 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[0]); - const float d1 = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e[1]); - - if (base + tid < k) { - y[base + tid] = (float) rocmfp4_decode_i8(packed & 0x0f) * d0; - } - if (base + tid + QK_ROCMFP4/2 < k) { - y[base + tid + QK_ROCMFP4/2] = (float) rocmfp4_decode_i8(packed >> 4) * d1; - } -} - -extern "C" __global__ void rocmfp4_dequantize_q4_0_fast_f32_kernel( - const block_rocmfp4_fast * __restrict__ x, - float * __restrict__ y, - int64_t k) { - const int64_t packed_idx = (int64_t) blockIdx.x*blockDim.x + threadIdx.x; - const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; - const int64_t packed_count = nblocks * (QK_ROCMFP4/2); - - if (packed_idx >= packed_count) { - return; - } - - const int64_t ib = packed_idx / (QK_ROCMFP4/2); - const int tid = packed_idx - ib*(QK_ROCMFP4/2); - const int64_t base = ib*QK_ROCMFP4; - const uint8_t packed = x[ib].qs[tid]; - const float d = rocmfp4_ue4m3_to_fp32_half_finite(x[ib].e); - - if (base + tid < k) { - y[base + tid] = (float) rocmfp4_decode_i8(packed & 0x0f) * d; - } - if (base + tid + QK_ROCMFP4/2 < k) { - y[base + tid + QK_ROCMFP4/2] = (float) rocmfp4_decode_i8(packed >> 4) * d; - } -} - -extern "C" void rocmfp4_hip_dequantize_q4_0_to_f32( - const void * src, - float * dst, - int64_t k, - hipStream_t stream) { - const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; - const int64_t packed_count = nblocks * (QK_ROCMFP4/2); - const dim3 block(256); - const dim3 grid((unsigned int) ((packed_count + block.x - 1) / block.x)); - rocmfp4_dequantize_q4_0_f32_kernel<<<grid, block, 0, stream>>>((const block_rocmfp4 *) src, dst, k); -} - -extern "C" void rocmfp4_hip_dequantize_q4_0_fast_to_f32( - const void * src, - float * dst, - int64_t k, - hipStream_t stream) { - const int64_t nblocks = (k + QK_ROCMFP4 - 1) / QK_ROCMFP4; - const int64_t packed_count = nblocks * (QK_ROCMFP4/2); - const dim3 block(256); - const dim3 grid((unsigned int) ((packed_count + block.x - 1) / block.x)); - rocmfp4_dequantize_q4_0_fast_f32_kernel<<<grid, block, 0, stream>>>((const block_rocmfp4_fast *) src, dst, k); -} diff --git a/ggml/rocmfp4/rocmfp4_hip_codebook.cuh b/ggml/rocmfp4/rocmfp4_hip_codebook.cuh deleted file mode 100644 index 7fa1357f8e8d..000000000000 --- a/ggml/rocmfp4/rocmfp4_hip_codebook.cuh +++ /dev/null @@ -1,87 +0,0 @@ -// ROCmFPx quant formats, hand-ported into this fork. -// -// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. -// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. -// -// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors -// retain their authorship and MIT license credit. See LICENSE. - -#pragma once - -#include "rocmfp4_hip_scale.cuh" - -#include <cstdint> -#include <cstring> - -#ifndef GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD -#define GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD 1 -#endif - -static __device__ __forceinline__ int rocmfp4_get_qs_i32(const void * x, const int & i32) { -#if defined(GGML_USE_HIP) && GGML_ROCMFP4_UNALIGNED_QS_DWORD_LOAD - return *((const int *) ((const uint8_t *) x + 4*i32)); -#else - const uint8_t * x8 = (const uint8_t *) x; - - int x32 = x8[4*i32 + 0] << 0; - x32 |= x8[4*i32 + 1] << 8; - x32 |= x8[4*i32 + 2] << 16; - x32 |= x8[4*i32 + 3] << 24; - - return x32; -#endif -} - -// AMD-specific fast path for expanding eight packed ROCmFP4 nibbles into two -// int32 DP4A operands. This encodes the Codebook10 table directly as four -// 32-bit constants: -// [0, 1, 2, 3], [4, 6, 8, 10], [0, -1, -2, -3], [-4, -6, -8, -10] -// Avoiding the table pointer keeps the ROCm/HIP MMVQ/MMQ hot path fully local -// to this format. Non-HIP builds still use llama.cpp's generic table expander. -static __device__ __forceinline__ int2 rocmfp4_get_int_from_codebook_16(const int & q4, const int8_t * fallback_table) { -#if defined(GGML_USE_HIP) - constexpr uint32_t values0 = 0x03020100u; - constexpr uint32_t values1 = 0x0a080604u; - constexpr uint32_t values2 = 0xfdfeff00u; - constexpr uint32_t values3 = 0xf6f8fafcu; - - const uint32_t q_even = q4; - const uint32_t q_odd = q4 >> 4; - - const uint32_t v_even_low = __builtin_amdgcn_perm(values1, values0, q_even & 0x07070707u); - const uint32_t v_odd_low = __builtin_amdgcn_perm(values1, values0, q_odd & 0x07070707u); - const uint32_t v_even_high = __builtin_amdgcn_perm(values3, values2, q_even & 0x07070707u); - const uint32_t v_odd_high = __builtin_amdgcn_perm(values3, values2, q_odd & 0x07070707u); - - const uint32_t mask_even = 0x03020100u | ((q_even & 0x08080808u) >> 1); - const uint32_t mask_odd = 0x03020100u | ((q_odd & 0x08080808u) >> 1); - - return make_int2( - __builtin_amdgcn_perm(v_even_high, v_even_low, mask_even), - __builtin_amdgcn_perm(v_odd_high, v_odd_low, mask_odd)); -#else - return get_int_from_table_16(q4, fallback_table); -#endif -} - -// Variant for call sites that already selected either the low or high nibble -// stream and only need one DP4A operand. This avoids the extra odd/even table -// expansion work in ROCmFP4 FlashAttention K/V decode. -static __device__ __forceinline__ int rocmfp4_get_low_int_from_codebook_16(const int & q4, const int8_t * fallback_table) { -#if defined(GGML_USE_HIP) - constexpr uint32_t values0 = 0x03020100u; - constexpr uint32_t values1 = 0x0a080604u; - constexpr uint32_t values2 = 0xfdfeff00u; - constexpr uint32_t values3 = 0xf6f8fafcu; - - const uint32_t q = q4; - - const uint32_t v_low = __builtin_amdgcn_perm(values1, values0, q & 0x07070707u); - const uint32_t v_high = __builtin_amdgcn_perm(values3, values2, q & 0x07070707u); - const uint32_t mask = 0x03020100u | ((q & 0x08080808u) >> 1); - - return __builtin_amdgcn_perm(v_high, v_low, mask); -#else - return get_int_from_table_16(q4, fallback_table).x; -#endif -} diff --git a/ggml/rocmfpx/rocmfpx_hip_codebook.cuh b/ggml/rocmfpx/rocmfpx_hip_codebook.cuh deleted file mode 100644 index b9e36528c2fc..000000000000 --- a/ggml/rocmfpx/rocmfpx_hip_codebook.cuh +++ /dev/null @@ -1,53 +0,0 @@ -// ROCmFPx quant formats, hand-ported into this fork. -// -// Origin: https://github.com/charlie12345/ROCmFPX - creator of the ROCmFP4 format. -// Ported from: https://github.com/ciru-ai/ROCmFPX - a fork of the above. -// -// Both upstream projects are MIT licensed and based on llama.cpp; upstream authors -// retain their authorship and MIT license credit. See LICENSE. - -#pragma once - -#include <cstdint> - -static __device__ __constant__ const int8_t rocmfpx_fp3_codebook[8] = { - 0, 1, 2, 4, 0, -1, -2, -4, -}; - -static __device__ __constant__ const int8_t rocmfpx_fp6_codebook[64] = { - 0, 1, 2, 3, 4, 5, 6, 7, - 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, - 24, 25, 26, 27, 28, 29, 30, 31, - 0, -1, -2, -3, -4, -5, -6, -7, - -8, -9, -10, -11, -12, -13, -14, -15, - -16, -17, -18, -19, -20, -21, -22, -23, - -24, -25, -26, -27, -28, -29, -30, -31, -}; - -static __device__ __forceinline__ int rocmfpx_pack4_fp3_codes(const uint32_t bits12) { - const uint32_t c0 = (bits12 >> 0) & 7u; - const uint32_t c1 = (bits12 >> 3) & 7u; - const uint32_t c2 = (bits12 >> 6) & 7u; - const uint32_t c3 = (bits12 >> 9) & 7u; - const char4 v = make_char4( - rocmfpx_fp3_codebook[c0], - rocmfpx_fp3_codebook[c1], - rocmfpx_fp3_codebook[c2], - rocmfpx_fp3_codebook[c3]); - return *((const int *) &v); -} - -static __device__ __forceinline__ int rocmfpx_pack4_fp6_codes(const uint32_t bits24) { - const uint32_t c0 = (bits24 >> 0) & 63u; - const uint32_t c1 = (bits24 >> 6) & 63u; - const uint32_t c2 = (bits24 >> 12) & 63u; - const uint32_t c3 = (bits24 >> 18) & 63u; - - const char4 v = make_char4( - rocmfpx_fp6_codebook[c0], - rocmfpx_fp6_codebook[c1], - rocmfpx_fp6_codebook[c2], - rocmfpx_fp6_codebook[c3]); - return *((const int *) &v); -} From d3a8ab54a2f8d9f8c551a68f60cad6463d059bf1 Mon Sep 17 00:00:00 2001 From: Baraxanx <baraxnaxgaming@gmail.com> Date: Mon, 28 Sep 2026 13:01:18 -0400 Subject: [PATCH 202/212] cuda: gate Q4_0_ROCMFP4_FAST MMQ to validated architectures Review scope reduction (halo-box/strix-llama.cpp#40 review): keep the MMQ tile rows only where the path is actually validated - RDNA3.5 (gfx1151, test-backend-ops 78/0) and the Ampere config table (RTX 4090 sm_89). Drop the pascal-dp4a / pascal-older / rdna2 / rdna3 / cdna / rdna4 rows: no hardware to measure there, so those architectures keep today's dequant + BLAS fallback (zero behavior change) instead of an unvalidated tile policy. should_use_mmq now returns false for this type outside the gated set (including Blackwell, which has no rows - native FP4 MMQ is MXFP4/NVFP4-only there), so a missing config row can never reach ggml_cuda_mmq_get_config's abort. --- ggml/src/ggml-cuda/mmq-config-cdna.cuh | 7 ------- ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh | 11 ----------- ggml/src/ggml-cuda/mmq-config-pascal-older.cuh | 11 ----------- ggml/src/ggml-cuda/mmq-config-rdna2.cuh | 11 ----------- ggml/src/ggml-cuda/mmq-config-rdna3.cuh | 11 ----------- ggml/src/ggml-cuda/mmq-config-rdna4.cuh | 12 ------------ ggml/src/ggml-cuda/mmq.cu | 10 +++++++++- 7 files changed, 9 insertions(+), 64 deletions(-) diff --git a/ggml/src/ggml-cuda/mmq-config-cdna.cuh b/ggml/src/ggml-cuda/mmq-config-cdna.cuh index 18d9ac6dc698..4a8d89f72019 100644 --- a/ggml/src/ggml-cuda/mmq-config-cdna.cuh +++ b/ggml/src/ggml-cuda/mmq-config-cdna.cuh @@ -166,19 +166,12 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, true); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_MXFP4, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, true, false); CASE(GGML_TYPE_NVFP4, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, true); CASE(GGML_TYPE_NVFP4, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, true); diff --git a/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh b/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh index 87b76b6c8fc0..83eb7c146e11 100644 --- a/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh +++ b/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh @@ -246,27 +246,16 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh b/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh index 31078d1462b5..2a8dc9e1a93e 100644 --- a/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh +++ b/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh @@ -246,27 +246,16 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-rdna2.cuh b/ggml/src/ggml-cuda/mmq-config-rdna2.cuh index 6cd7ccc17c72..8324d9e1a830 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna2.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna2.cuh @@ -246,27 +246,16 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh index 8af3423d7d68..3a3ef7bd9c09 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh @@ -247,27 +247,16 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 1, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 1, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 1, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq-config-rdna4.cuh b/ggml/src/ggml-cuda/mmq-config-rdna4.cuh index 8ef32188f690..9293d9d55885 100644 --- a/ggml/src/ggml-cuda/mmq-config-rdna4.cuh +++ b/ggml/src/ggml-cuda/mmq-config-rdna4.cuh @@ -261,29 +261,17 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf // --------------------------------------------------------------------------------------------- CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_MXFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); - CASE(GGML_TYPE_Q4_0_ROCMFP4_FAST, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); CASE(GGML_TYPE_NVFP4, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true); diff --git a/ggml/src/ggml-cuda/mmq.cu b/ggml/src/ggml-cuda/mmq.cu index 31e5d3161e6c..c191a4f92e82 100644 --- a/ggml/src/ggml-cuda/mmq.cu +++ b/ggml/src/ggml-cuda/mmq.cu @@ -497,9 +497,17 @@ bool ggml_cuda_should_use_mmq(enum ggml_type type, int cc, int64_t ne11, int64_t // ------------------------------------------------- case GGML_TYPE_MXFP4: case GGML_TYPE_NVFP4: - case GGML_TYPE_Q4_0_ROCMFP4_FAST: mmq_supported = true; break; + case GGML_TYPE_Q4_0_ROCMFP4_FAST: + // MMQ tile rows exist only for RDNA3.5 (gfx1151, validated) and the + // Ampere config table (validated on RTX 4090 / sm_89). Gate to exactly + // those: Blackwell/Rubin have no rows of their own (native FP4 MMQ is + // MXFP4/NVFP4-only there) and would otherwise fall through to the + // Ampere table unvalidated. Everywhere else falls back to dequant + BLAS. + mmq_supported = (ampere_mma_available(cc) && cc < GGML_CUDA_CC_BLACKWELL) || + GGML_CUDA_CC_IS_RDNA3_5(cc); + break; default: mmq_supported = false; break; From a1c935855aa0d605101bc5e85fd8c2f3d5dca841 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Tue, 29 Sep 2026 12:57:38 +0100 Subject: [PATCH 203/212] convert : drop unused imports in qwen4exp Assisted-by: Claude Opus 5.5 --- conversion/qwen4exp.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/conversion/qwen4exp.py b/conversion/qwen4exp.py index e23cf56fa706..7efb5217fd75 100644 --- a/conversion/qwen4exp.py +++ b/conversion/qwen4exp.py @@ -1,7 +1,5 @@ from __future__ import annotations -import json -import logging from typing import Callable, Iterable, cast import torch From acbfbc8100646b55099eaaa41561118f770a6db3 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Tue, 29 Sep 2026 19:42:50 +0100 Subject: [PATCH 204/212] ci : nightly ROCm release builds in the lemonade-sdk/llamacpp-rocm layout Port of lemonade-sdk/llamacpp-rocm's build, test and nightly-alert workflows (MIT). Same targets, same TheRock nightly ROCm, same bundled runtime libraries, same llama-bNNNN-{os}-rocm-{target}-x64.zip assets and bNNNN release numbering. Differences from the original: it builds this repository instead of cloning upstream, falls back to the job token when there is no GH_TOKEN secret, runs the PR trigger only for changes to the pipeline, and gates the self-hosted Strix Halo test jobs behind repository variables so missing runners cannot hold a run in the queue. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../actions/test-llamacpp-build/action.yml | 238 ++++ .github/workflows/build-llamacpp-rocm.yml | 1107 +++++++++++++++++ .github/workflows/nightly-failure-alert.yml | 66 + .github/workflows/test-llamacpp-rocm.yml | 394 ++++++ docs/rocm-releases.md | 42 + scripts/rocm-release/gather_required_libs.py | 57 + 6 files changed, 1904 insertions(+) create mode 100644 .github/actions/test-llamacpp-build/action.yml create mode 100644 .github/workflows/build-llamacpp-rocm.yml create mode 100644 .github/workflows/nightly-failure-alert.yml create mode 100644 .github/workflows/test-llamacpp-rocm.yml create mode 100644 docs/rocm-releases.md create mode 100644 scripts/rocm-release/gather_required_libs.py diff --git a/.github/actions/test-llamacpp-build/action.yml b/.github/actions/test-llamacpp-build/action.yml new file mode 100644 index 000000000000..3152939708c2 --- /dev/null +++ b/.github/actions/test-llamacpp-build/action.yml @@ -0,0 +1,238 @@ +# Same as lemonade-sdk/llamacpp-rocm (MIT). +name: 'Test Llama.cpp Build' +description: 'Download and test llama.cpp build artifacts' + +inputs: + os_type: + description: 'Operating system type (Windows or Linux)' + required: true + gfx_target: + description: 'GPU target (e.g., gfx1151, gfx1150)' + required: true + artifact_name: + description: 'Full artifact name to download' + required: true + +runs: + using: "composite" + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact_name }} + path: llama-binaries + + - name: Download test model (Windows) + if: inputs.os_type == 'Windows' + shell: powershell + run: | + $modelUrl = "https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_0.gguf?download=true" + $modelPath = "Qwen3-0.6B-Q4_0.gguf" + + Write-Host "Downloading test model from: $modelUrl" + curl.exe -L -o $modelPath $modelUrl + + # Verify download + if (Test-Path $modelPath) { + $fileSize = (Get-Item $modelPath).Length + Write-Host "Model downloaded successfully. Size: $fileSize bytes" + } else { + Write-Error "Failed to download model" + exit 1 + } + + - name: Download test model (Linux) + if: inputs.os_type == 'Linux' + shell: bash + run: | + model_url="https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_0.gguf?download=true" + model_path="Qwen3-0.6B-Q4_0.gguf" + + echo "Downloading test model from: $model_url" + curl -L -o "$model_path" "$model_url" + + # Verify download + if [ -f "$model_path" ]; then + file_size=$(stat -c%s "$model_path") + echo "Model downloaded successfully. Size: $file_size bytes" + else + echo "Failed to download model" + exit 1 + fi + + - name: Set up library path (Linux) + if: inputs.os_type == 'Linux' + shell: bash + run: | + # Add the llama-binaries directory to LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH=$(pwd)/llama-binaries:$LD_LIBRARY_PATH" >> $GITHUB_ENV + + - name: Run llama-cli test (Windows) + if: inputs.os_type == 'Windows' + shell: powershell + run: | + $llamaCliPath = "llama-binaries\llama-cli.exe" + $modelPath = "Qwen3-0.6B-Q4_0.gguf" + + # Verify llama-cli exists + if (-not (Test-Path $llamaCliPath)) { + Write-Error "llama-cli.exe not found at: $llamaCliPath" + Write-Host "Available files in llama-binaries:" + Get-ChildItem -Recurse "llama-binaries" + exit 1 + } + + Write-Host "Running llama-cli test for ${{ inputs.gfx_target }}..." + Write-Host "Command: $llamaCliPath -m `"$modelPath`" -ngl 99 -p `"I believe the meaning of life is`" -st -v" + + # Run the command and capture output + try { + $process = Start-Process -FilePath $llamaCliPath -ArgumentList @("-m", "`"$modelPath`"", "-ngl", "99", "-p", "`"I believe the meaning of life is`"", "-st", "-v") -NoNewWindow -Wait -PassThru -RedirectStandardOutput "stdout.txt" -RedirectStandardError "stderr.txt" + + # Wait for the process to complete + $process.WaitForExit() + + # Read both stdout and stderr + $stdout = Get-Content "stdout.txt" -Raw -ErrorAction SilentlyContinue + $stderr = Get-Content "stderr.txt" -Raw -ErrorAction SilentlyContinue + + # Always print the output (both stdout and stderr) + Write-Host "=== LLAMA-CLI STDOUT ===" + if ($stdout) { + Write-Host $stdout + } else { + Write-Host "(empty)" + } + Write-Host "=== LLAMA-CLI STDERR ===" + if ($stderr) { + Write-Host $stderr + } else { + Write-Host "(empty)" + } + Write-Host "=== END OUTPUT ===" + Write-Host "Process exit code: $($process.ExitCode)" + + # Combine output for pattern matching (stderr often contains important CUDA/GPU info) + $output = $stdout + $stderr + + # Clean up temp files + Remove-Item "stdout.txt" -ErrorAction SilentlyContinue + Remove-Item "stderr.txt" -ErrorAction SilentlyContinue + } + catch { + Write-Error "Failed to start llama-cli process: $_" + Write-Error "This usually indicates a missing dependency (DLL) or permission issue" + # Try to get more details about dependencies + Write-Host "Checking for dependency issues..." + Write-Host "Files in llama-binaries directory:" + Get-ChildItem "llama-binaries" | Format-Table Name, Length + exit 1 + } + + # Check if the process exited successfully + if ($process.ExitCode -ne 0) { + Write-Host "❌ llama-cli exited with error code: $($process.ExitCode)" + Write-Host "This error code typically indicates:" + Write-Host " - Exit code 127: Command or library not found (missing DLL)" + Write-Host " - Exit code 1: General error" + Write-Host " - Other codes: Application-specific errors" + Write-Host "" + Write-Host "Please check the STDERR output above for specific error messages." + exit 1 + } + + # Check for expected outputs + $foundGpuOffload = $output -match "offloaded 29/29 layers to GPU" + $foundThinkTag = $output -match "</think>" + + Write-Host "=== TEST RESULTS ===" + Write-Host "Expected 'offloaded 29/29 layers to GPU': $(if ($foundGpuOffload) { 'FOUND' } else { 'NOT FOUND' })" + Write-Host "Expected '</think>' tag: $(if ($foundThinkTag) { 'FOUND' } else { 'NOT FOUND' })" + + if ($foundGpuOffload -and $foundThinkTag) { + Write-Host "✅ Test PASSED - Both expected outputs found" + } else { + Write-Host "❌ Test FAILED - Missing expected outputs" + exit 1 + } + + - name: Run llama-cli test (Linux) + if: inputs.os_type == 'Linux' + shell: bash + run: | + llama_cli_path="./llama-binaries/llama-cli" + model_path="Qwen3-0.6B-Q4_0.gguf" + output_file="llama_output.txt" + + # Make llama-cli executable + chmod +x "$llama_cli_path" + + # Verify llama-cli exists + if [ ! -f "$llama_cli_path" ]; then + echo "llama-cli not found at: $llama_cli_path" + echo "Available files in llama-binaries:" + find llama-binaries -type f + exit 1 + fi + + echo "Running llama-cli test for ${{ inputs.gfx_target }}..." + echo "Command: $llama_cli_path -m \"$model_path\" -ngl 99 -p \"I believe the meaning of life is\" -st -v" + + # Run the command and capture output to file (more robust than variable) + set +e # Don't exit on error, we want to capture the output and check exit code manually + "$llama_cli_path" -m "$model_path" -ngl 99 -p "I believe the meaning of life is" -st -v > "$output_file" 2>&1 + exit_code=$? + set -e # Re-enable exit on error + + echo "=== LLAMA-CLI OUTPUT ===" + if [ -f "$output_file" ] && [ -s "$output_file" ]; then + cat "$output_file" + else + echo "(empty)" + fi + echo "=== END OUTPUT ===" + echo "Process exit code: $exit_code" + + # Check if the process exited successfully + if [ $exit_code -ne 0 ]; then + echo "❌ llama-cli exited with error code: $exit_code" + echo "This error code typically indicates:" + echo " - Exit code 127: Command or library not found (missing shared library)" + echo " - Exit code 1: General error" + echo " - Other codes: Application-specific errors" + echo "" + echo "Checking for missing library dependencies..." + ldd "$llama_cli_path" || echo "ldd command failed" + echo "" + echo "Please check the output above for specific error messages." + rm -f "$output_file" + exit 1 + fi + + # Check for expected outputs using grep -F (fixed strings, no regex interpretation) + # This handles special characters like angle brackets more reliably + found_gpu_offload=false + found_think_tag=false + + if grep -F "offloaded 29/29 layers to GPU" "$output_file" > /dev/null 2>&1; then + found_gpu_offload=true + fi + + if grep -F "</think>" "$output_file" > /dev/null 2>&1; then + found_think_tag=true + fi + + echo "=== TEST RESULTS ===" + echo "Expected 'offloaded 29/29 layers to GPU': $(if [ "$found_gpu_offload" = true ]; then echo 'FOUND'; else echo 'NOT FOUND'; fi)" + echo "Expected '</think>' tag: $(if [ "$found_think_tag" = true ]; then echo 'FOUND'; else echo 'NOT FOUND'; fi)" + + # Clean up output file + rm -f "$output_file" + + if [ "$found_gpu_offload" = true ] && [ "$found_think_tag" = true ]; then + echo "✅ Test PASSED - Both expected outputs found" + else + echo "❌ Test FAILED - Missing expected outputs" + exit 1 + fi + diff --git a/.github/workflows/build-llamacpp-rocm.yml b/.github/workflows/build-llamacpp-rocm.yml new file mode 100644 index 000000000000..cba4cfa1c5f4 --- /dev/null +++ b/.github/workflows/build-llamacpp-rocm.yml @@ -0,0 +1,1107 @@ +# Nightly ROCm release builds of this fork, same layout as lemonade-sdk/llamacpp-rocm +# (https://github.com/lemonade-sdk/llamacpp-rocm, MIT) so the zips drop into the same tooling. +# Differences: builds this repository instead of cloning upstream llama.cpp, uses the job token when no +# GH_TOKEN secret exists, and runs the self-hosted Strix Halo tests only when the runners are registered. +name: Build Llama.cpp + ROCm +on: + workflow_dispatch: + inputs: + operating_systems: + description: 'Operating systems to build for (comma-separated: windows,ubuntu)' + required: false + default: 'windows,ubuntu' + gfx_target: + description: 'AMD GPU targets (comma-separated)' + required: false + default: 'gfx1151,gfx1150,gfx120X,gfx110X,gfx103X,gfx90a,gfx908' + rocm_version: + description: 'ROCm version to use (e.g., 7.13.0a20260318) or "latest" to auto-detect' + required: false + default: 'latest' + llamacpp_version: + description: 'Branch or tag of this repository to build, or "latest" for master' + required: false + default: 'latest' + create_release: + description: 'Create a GitHub release after successful build' + required: false + default: true + type: boolean + + # Trigger on pushes to pull requests that change the release pipeline + pull_request: + types: [opened, synchronize, reopened] + paths: + - '.github/workflows/build-llamacpp-rocm.yml' + - '.github/actions/test-llamacpp-build/**' + - 'scripts/rocm-release/**' + + # Trigger on a schedule to build nightly releases + schedule: + # Runs at 1:00 PM UTC, which is 5:00 AM PST (UTC-8) + # This is two hours after the ROCm nightly tarball is triggered + - cron: '0 13 * * *' + +env: + OPERATING_SYSTEMS: ${{ github.event.inputs.operating_systems || 'windows,ubuntu' }} + GFX_TARGETS: ${{ github.event.inputs.gfx_target || 'gfx1151,gfx1150,gfx120X,gfx110X,gfx103X,gfx90a,gfx908' }} + ROCM_VERSION: ${{ github.event.inputs.rocm_version || 'latest' }} + LLAMACPP_VERSION: ${{ github.event.inputs.llamacpp_version || 'latest' }} + +jobs: + prepare-matrix: + runs-on: ubuntu-22.04 + outputs: + windows_matrix: ${{ steps.set-matrix.outputs.windows_matrix }} + ubuntu_matrix: ${{ steps.set-matrix.outputs.ubuntu_matrix }} + should_build_windows: ${{ steps.set-matrix.outputs.should_build_windows }} + should_build_ubuntu: ${{ steps.set-matrix.outputs.should_build_ubuntu }} + steps: + - name: Set matrix + id: set-matrix + run: | + targets="${{ env.GFX_TARGETS }}" + operating_systems="${{ env.OPERATING_SYSTEMS }}" + + echo "Input targets: $targets" + echo "Input operating systems: $operating_systems" + + # Convert targets to JSON array + matrix_targets=$(echo "$targets" \ + | tr ',' '\n' \ + | sed 's/^ *//;s/ *$//' \ + | sed 's/^"//;s/"$//' \ + | jq -R . \ + | jq -s '{gfx_target: .}' \ + | jq -c) + + # Check which operating systems to build + should_build_windows="false" + should_build_ubuntu="false" + + if [[ "$operating_systems" == *"windows"* ]]; then + should_build_windows="true" + echo "windows_matrix=$matrix_targets" >> $GITHUB_OUTPUT + fi + + if [[ "$operating_systems" == *"ubuntu"* ]]; then + should_build_ubuntu="true" + echo "ubuntu_matrix=$matrix_targets" >> $GITHUB_OUTPUT + fi + + echo "should_build_windows=$should_build_windows" >> $GITHUB_OUTPUT + echo "should_build_ubuntu=$should_build_ubuntu" >> $GITHUB_OUTPUT + + echo "Windows build: $should_build_windows" + echo "Ubuntu build: $should_build_ubuntu" + echo "Generated matrix: $matrix_targets" + + build-windows: + # Pinned to windows-2022 (VS 2022 / MSVC 14.4x). The windows-latest image moved to + # VS 2026 (MSVC 14.51) on 2026-06-08, whose <cmath> declares isgreater/isless/etc. as + # constexpr (implicitly __host__ __device__), which collides with ROCm clang's + # __device__-only HIP math wrappers and breaks the HIP build. See llama.cpp#22570. + runs-on: windows-2022 + needs: prepare-matrix + if: needs.prepare-matrix.outputs.should_build_windows == 'true' + strategy: + matrix: ${{fromJson(needs.prepare-matrix.outputs.windows_matrix)}} + fail-fast: false + outputs: + rocm_version: ${{ steps.set-outputs.outputs.rocm_version }} + llamacpp_commit_hash: ${{ steps.set-outputs.outputs.llamacpp_commit_hash }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Clean up existing directories (safety precaution) + run: | + # Remove existing llama.cpp directory if it exists + if (Test-Path "llama.cpp") { + Write-Host "Removing existing llama.cpp directory..." + Remove-Item -Recurse -Force "llama.cpp" + } + + # Remove existing C:\opt\rocm directory if it exists + if (Test-Path "C:\opt\rocm") { + Write-Host "Removing existing C:\opt\rocm directory..." + Remove-Item -Recurse -Force "C:\opt\rocm" + } + + # Remove any existing ROCm tarball + if (Test-Path "rocm.tar.gz") { + Write-Host "Removing existing rocm.tar.gz..." + Remove-Item -Force "rocm.tar.gz" + } + + Write-Host "Cleanup completed successfully" + + - name: Install Visual Studio Build Tools + run: | + # Download and install Visual Studio Build Tools + $vsInstallerUrl = "https://aka.ms/vs/17/release/vs_buildtools.exe" + $vsInstallerPath = "$env:TEMP\vs_buildtools.exe" + + Write-Host "Downloading Visual Studio Build Tools..." + Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath + + Write-Host "Installing Visual Studio Build Tools..." + Start-Process -FilePath $vsInstallerPath -ArgumentList "--quiet", "--wait", "--norestart", "--add", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "--add", "Microsoft.VisualStudio.Component.VC.CMake.Project", "--add", "Microsoft.VisualStudio.Component.VC.ATL", "--add", "Microsoft.VisualStudio.Component.Windows11SDK.22621" -Wait + + # Clean up installer + Remove-Item $vsInstallerPath -Force + + - name: Install build dependencies + run: | + Write-Host "Installing build dependencies using manual methods..." + + # Install Ninja + Write-Host "Installing Ninja..." + $ninjaUrl = "https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-win.zip" + $ninjaPath = "$env:TEMP\ninja-win.zip" + $ninjaDir = "C:\ninja" + New-Item -ItemType Directory -Force -Path $ninjaDir + Invoke-WebRequest -Uri $ninjaUrl -OutFile $ninjaPath + Expand-Archive -Path $ninjaPath -DestinationPath $ninjaDir -Force + + # Install Strawberry Perl + Write-Host "Installing Strawberry Perl..." + $perlUrl = "http://strawberryperl.com/download/5.32.1.1/strawberry-perl-5.32.1.1-64bit.msi" + $perlPath = "$env:TEMP\strawberry-perl-5.32.1.1-64bit.msi" + Invoke-WebRequest -Uri $perlUrl -OutFile $perlPath + Start-Process msiexec.exe -ArgumentList "/i $perlPath /quiet /norestart" -Wait + + # Verify installations + $env:PATH = "C:\ninja;C:\Strawberry\perl\bin;$env:PATH" + Write-Host "Verifying installations..." + ninja --version + perl --version + + Write-Host "Manual installation of build dependencies completed" + + - name: Download ROCm nightly tarball + run: | + # Determine ROCm version to use + $rocmVersion = "${{ env.ROCM_VERSION }}" + $currentTarget = "${{ matrix.gfx_target }}" + + # Add appropriate suffixes for different GPU targets + $s3Target = $currentTarget + if ($currentTarget -eq "gfx103X" -or $currentTarget -eq "gfx110X" -or $currentTarget -eq "gfx120X") { + $s3Target = "$currentTarget-all" + Write-Host "Using target with -all suffix: $s3Target" + } + + # TheRock now publishes nightlies to the ROCm core tarball index. That + # index is a static HTML page with an embedded JSON `files` array (name + + # mtime), so we parse that instead of the retired S3 XML bucket listing. + $baseUrl = "https://nightly.repo.amd.com/rocm/core/tarball" + if ($rocmVersion -eq "latest") { + Write-Host "Auto-detecting latest ROCm version for target: $currentTarget" + $indexHtml = (Invoke-WebRequest "$baseUrl/" -UseBasicParsing).Content + $filesMatch = [regex]::Match($indexHtml, 'const files = (\[.*?\]);', [System.Text.RegularExpressions.RegexOptions]::Singleline) + if (-not $filesMatch.Success) { + Write-Error "Failed to parse file index from $baseUrl/" + exit 1 + } + + # Pick the newest by the YYYYMMDD build date embedded in the filename. + # Assign before piping so the parsed array enumerates element-by-element. + $allFiles = $filesMatch.Groups[1].Value | ConvertFrom-Json + $prefix = "therock-dist-windows-$s3Target-" + # Anchor the match so the version comes immediately after the prefix. + # This excludes variants like "...-all-tests-<version>.tar.gz" that + # TheRock also publishes and share the same prefix/date. + $versionPattern = "^$([regex]::Escape($prefix))\d+\.\d+\.\d+(a|rc)\d+\.tar\.gz$" + $latest = $allFiles | + Where-Object { $_.name -match $versionPattern } | + Sort-Object { [regex]::Match($_.name, '(\d{8})\.tar\.gz$').Groups[1].Value } | + Select-Object -Last 1 + if (-not $latest) { + Write-Error "No tarball found for prefix '$prefix' at $baseUrl/" + exit 1 + } + $latestFile = $latest.name + Write-Host "Found latest file: $latestFile" + + # Extract version from the filename for environment variable + if ($latestFile -match "therock-dist-windows-$s3Target-(\d+\.\d+\.\d+(?:a|rc)\d+)\.tar\.gz") { + $rocmVersion = $matches[1] + Write-Host "Detected latest ROCm version: $rocmVersion" + } else { + Write-Error "Failed to extract ROCm version from latest file: $latestFile" + Write-Error "Expected pattern: therock-dist-windows-$s3Target-<version>.tar.gz" + exit 1 + } + + $rocmUrl = "$baseUrl/$latestFile" + } else { + # For specific versions, construct the URL directly + $rocmUrl = "$baseUrl/therock-dist-windows-$s3Target-$rocmVersion.tar.gz" + } + + # Store the version for use in other steps + echo "DETECTED_ROCM_VERSION=$rocmVersion" >> $env:GITHUB_ENV + + Write-Host "Downloading ROCm from: $rocmUrl" + Invoke-WebRequest -Uri $rocmUrl -OutFile "rocm.tar.gz" + + - name: Extract ROCm to C:\opt\rocm + run: | + # Create directory if it doesn't exist + New-Item -ItemType Directory -Force -Path "C:\opt\rocm" + + # Extract the tarball + tar -xzf rocm.tar.gz -C C:\opt\rocm --strip-components=1 + + - name: Clone llama.cpp + run: | + $llamacppVersion = "${{ env.LLAMACPP_VERSION }}" + if ($llamacppVersion -eq "latest") { + Write-Host "Cloning master branch (latest) with blobless clone" + git clone --filter=blob:none --single-branch --branch master https://github.com/${{ github.repository }}.git llama.cpp + } else { + Write-Host "Cloning llama.cpp version: $llamacppVersion with blobless clone" + git clone --filter=blob:none --single-branch --branch $llamacppVersion https://github.com/${{ github.repository }}.git llama.cpp + } + cd llama.cpp + + # Get commit hash (5 digits) and store it as environment variable + $commitHash = git rev-parse --short=5 HEAD + echo "LLAMACPP_COMMIT_HASH=$commitHash" >> $env:GITHUB_ENV + Write-Host "llama.cpp commit hash (5 digits): $commitHash" + + # Show current commit info + Write-Host "Current llama.cpp commit:" + git log --oneline -1 + + - name: Build Llama.cpp + ROCm + shell: cmd + run: | + + REM Map GPU targets + set "current_target=${{ matrix.gfx_target }}" + echo Input target: %current_target% + + if "%current_target%"=="gfx110X" ( + set "mapped_target=gfx1100;gfx1101;gfx1102;gfx1103" + ) else if "%current_target%"=="gfx103X" ( + set "mapped_target=gfx1030;gfx1031;gfx1032;gfx1034" + ) else if "%current_target%"=="gfx1151" ( + set "mapped_target=gfx1151" + ) else if "%current_target%"=="gfx1150" ( + set "mapped_target=gfx1150" + ) else if "%current_target%"=="gfx120X" ( + set "mapped_target=gfx1200;gfx1201" + ) else ( + set "mapped_target=%current_target%" + ) + echo Mapped target: %mapped_target% + + REM Set up environment variables and PATH + set HIP_PATH=C:\opt\rocm + set HIP_PLATFORM=amd + set PATH=%HIP_PATH%\lib\llvm\bin;%HIP_PATH%\bin;%PATH% + + REM Set up x64 Native Tools Command Prompt environment + call "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath > vs_path.txt + set /p VS_PATH=<vs_path.txt + call "%VS_PATH%\VC\Auxiliary\Build\vcvars64.bat" + + REM Create build directory + cd llama.cpp + mkdir build + cd build + + REM Configure the project + cmake .. -G Ninja ^ + -DCMAKE_C_COMPILER="C:\opt\rocm\lib\llvm\bin\clang.exe" ^ + -DCMAKE_CXX_COMPILER="C:\opt\rocm\lib\llvm\bin\clang++.exe" ^ + -DCMAKE_CXX_FLAGS="-IC:\opt\rocm\include" ^ + -DCMAKE_CROSSCOMPILING=ON ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DGPU_TARGETS="%mapped_target%" ^ + -DBUILD_SHARED_LIBS=ON ^ + -DLLAMA_BUILD_TESTS=OFF ^ + -DGGML_HIP=ON ^ + -DGGML_OPENMP=OFF ^ + -DGGML_CUDA_FORCE_CUBLAS=OFF ^ + -DGGML_RPC=ON ^ + -DGGML_HIP_ROCWMMA_FATTN=OFF ^ + -DLLAMA_BUILD_BORINGSSL=ON ^ + -DGGML_NATIVE=OFF ^ + -DGGML_STATIC=OFF ^ + -DCMAKE_SYSTEM_NAME=Windows + + REM Build the project + cmake --build . -j %NUMBER_OF_PROCESSORS% + + - name: Copy ROCm core DLLs to build directory + run: | + $rocmVersion = if ($env:DETECTED_ROCM_VERSION) { $env:DETECTED_ROCM_VERSION } else { $env:ROCM_VERSION } + $buildBinPath = "llama.cpp\build\bin" + $rocmBinPath = "C:\opt\rocm\bin" + + Write-Host "Copying ROCm core DLL files to build directory..." + Write-Host "Source: $rocmBinPath" + Write-Host "Destination: $buildBinPath" + + if (Test-Path $rocmBinPath) { + # Copy files matching patterns and specific names. + $filesToCopy = @( + "amdhip64_*.dll", + "rocm_kpack.dll", + "amd_comgr*.dll", + "libhipblas.dll", + "rocblas.dll", + "rocsolver.dll", + "hipblaslt.dll", + "libhipblaslt.dll", + "hipblas.dll", + "origami.dll", + "libtensilelite-host.dll" + ) + + foreach ($pattern in $filesToCopy) { + $matchingFiles = Get-ChildItem -Path $rocmBinPath -Name $pattern -ErrorAction SilentlyContinue + if ($matchingFiles) { + foreach ($file in $matchingFiles) { + $sourcePath = Join-Path $rocmBinPath $file + $destPath = Join-Path $buildBinPath $file + Copy-Item $sourcePath $destPath + Write-Host "Copied: $file" + } + } else { + Write-Host "Warning: No files found matching pattern: $pattern" + } + } + + # Copy the rocblas\library folder and all its contents + $rocblasLibPath = Join-Path $rocmBinPath "rocblas\library" + if (Test-Path $rocblasLibPath) { + Write-Host "Copying rocblas\library folder and all contents..." + $destRocblasPath = Join-Path $buildBinPath "rocblas\library" + Copy-Item -Path $rocblasLibPath -Destination $destRocblasPath -Recurse -Force + Write-Host "Copied: rocblas\library folder with all contents" + + # List the contents of the copied rocblas\library folder + Write-Host "Contents of rocblas\library:" + Get-ChildItem $destRocblasPath -Recurse | Select-Object FullName, Length | Format-Table -AutoSize + } else { + Write-Host "Warning: rocblas\library folder not found at: $rocblasLibPath" + } + + # Copy the hipblaslt\library folder and all its contents + $hipblasltLibPath = Join-Path $rocmBinPath "hipblaslt\library" + if (Test-Path $hipblasltLibPath) { + Write-Host "Copying hipblaslt\library folder and all contents..." + $destHipblasltPath = Join-Path $buildBinPath "hipblaslt\library" + Copy-Item -Path $hipblasltLibPath -Destination $destHipblasltPath -Recurse -Force + Write-Host "Copied: hipblaslt\library folder with all contents" + + # List the contents of the copied hipblaslt\library folder + Write-Host "Contents of hipblaslt\library:" + Get-ChildItem $destHipblasltPath -Recurse | Select-Object FullName, Length | Format-Table -AutoSize + } else { + Write-Host "Warning: rocblas\library folder not found at: $rocblasLibPath" + } + + Write-Host "ROCm core files successfully copied to build directory" + } else { + Write-Error "ROCm bin directory not found: $rocmBinPath" + exit 1 + } + + - name: List build artifacts (including ROCm files) + run: | + cd llama.cpp\build\bin + Write-Host "Final build artifacts (including ROCm core files):" + Get-ChildItem -Recurse | Format-Table Name, Length, LastWriteTime + + - name: Upload build artifacts + id: upload-artifacts + uses: actions/upload-artifact@v4 + with: + name: llama-windows-rocm-${{ matrix.gfx_target }}-x64 + path: llama.cpp/build/bin/ + retention-days: 30 + + - name: Set job outputs + id: set-outputs + run: | + Write-Host "Setting job outputs..." + Write-Host "Current target: ${{ matrix.gfx_target }}" + Write-Host "DETECTED_ROCM_VERSION: $env:DETECTED_ROCM_VERSION" + Write-Host "ROCM_VERSION: $env:ROCM_VERSION" + Write-Host "LLAMACPP_COMMIT_HASH: $env:LLAMACPP_COMMIT_HASH" + + $rocmVersion = if ($env:DETECTED_ROCM_VERSION) { $env:DETECTED_ROCM_VERSION } else { $env:ROCM_VERSION } + echo "rocm_version=$rocmVersion" >> $env:GITHUB_OUTPUT + echo "llamacpp_commit_hash=$env:LLAMACPP_COMMIT_HASH" >> $env:GITHUB_OUTPUT + + Write-Host "Final rocm_version: $rocmVersion" + Write-Host "Final llamacpp_commit_hash: $env:LLAMACPP_COMMIT_HASH" + + - name: Create release summary + run: | + $summary = @" + # Llama.cpp + ROCm Build Summary (Windows - ${{ matrix.gfx_target }}) + + ## Build Configuration + - **GPU Target**: ${{ matrix.gfx_target }} + - **ROCm Version**: ${{ env.DETECTED_ROCM_VERSION || env.ROCM_VERSION }} + - **llama.cpp Commit Hash**: ${{ env.LLAMACPP_COMMIT_HASH }} + - **Build Type**: Release + - **Platform**: Windows + + ## Build Artifacts + The compiled binaries are available in the build artifacts. + + "@ + + Set-Content -Path "build-summary-windows-${{ matrix.gfx_target }}.md" -Value $summary + + - name: Upload build summary + id: upload-summary + uses: actions/upload-artifact@v4 + with: + name: build-summary-windows-${{ matrix.gfx_target }} + path: build-summary-windows-${{ matrix.gfx_target }}.md + retention-days: 30 + + - name: Clean up build artifacts and temporary files + if: always() + run: | + Write-Host "Starting cleanup process..." + + # Remove ROCm tarball + if (Test-Path "rocm.tar.gz") { + Write-Host "Removing rocm.tar.gz..." + Remove-Item -Force "rocm.tar.gz" + } + + # Remove ROCm installation directory + if (Test-Path "C:\opt\rocm") { + Write-Host "Removing C:\opt\rocm directory..." + Remove-Item -Recurse -Force "C:\opt\rocm" + } + # Remove llama.cpp source directory (keeping only the build artifacts) + if (Test-Path "llama.cpp") { + Write-Host "Removing llama.cpp source directory..." + Remove-Item -Recurse -Force "llama.cpp" + } + + # Clean up any temporary build files + if (Test-Path "build-summary-windows-${{ matrix.gfx_target }}.md") { + Write-Host "Removing build-summary-windows-${{ matrix.gfx_target }}.md..." + Remove-Item -Force "build-summary-windows-${{ matrix.gfx_target }}.md" + } + + Write-Host "Cleanup completed successfully" + + build-ubuntu: + runs-on: ubuntu-22.04 + needs: prepare-matrix + if: needs.prepare-matrix.outputs.should_build_ubuntu == 'true' + strategy: + matrix: ${{fromJson(needs.prepare-matrix.outputs.ubuntu_matrix)}} + fail-fast: false + outputs: + rocm_version: ${{ steps.set-outputs.outputs.rocm_version }} + llamacpp_commit_hash: ${{ steps.set-outputs.outputs.llamacpp_commit_hash }} + + + steps: + - name: Free disk space + # Remove unused runner files to free up disk space + run: curl -fsSL https://raw.githubusercontent.com/kou/arrow/e49d8ae15583ceff03237571569099a6ad62be32/ci/scripts/util_free_space.sh | bash + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Clean up existing directories (safety precaution) + run: | + # Remove existing llama.cpp directory if it exists + if [ -d "llama.cpp" ]; then + echo "Removing existing llama.cpp directory..." + rm -rf llama.cpp + fi + + # Remove existing /opt/rocm directory if it exists + if [ -d "/opt/rocm" ]; then + echo "Removing existing /opt/rocm directory..." + sudo rm -rf /opt/rocm + fi + + # Remove any existing ROCm tarball + if [ -f "rocm.tar.gz" ]; then + echo "Removing existing rocm.tar.gz..." + rm -f rocm.tar.gz + fi + + echo "Cleanup completed successfully" + + - name: Install build dependencies + run: | + echo "Installing build dependencies..." + sudo apt update + sudo apt install -y cmake ninja-build unzip curl + + # Verify installations + echo "Verifying installations..." + cmake --version + ninja --version + echo "Build dependencies installation completed" + + - name: Download and extract ROCm directly to /opt/rocm + run: | + # Determine ROCm version to use + rocm_version="${{ env.ROCM_VERSION }}" + current_target="${{ matrix.gfx_target }}" + + # Add appropriate suffixes for different GPU targets + s3_target="$current_target" + if [[ "$current_target" = "gfx103X" || "$current_target" = "gfx110X" || "$current_target" = "gfx120X" ]]; then + s3_target="${current_target}-all" + echo "Using target with -all suffix: $s3_target" + fi + + # TheRock now publishes nightlies to the ROCm core tarball index. That + # index is a static HTML page with an embedded JSON `files` array (name + + # mtime), so we parse that instead of the retired S3 XML bucket listing. + base_url="https://nightly.repo.amd.com/rocm/core/tarball" + if [ "$rocm_version" = "latest" ]; then + echo "Auto-detecting latest ROCm version for target: $current_target" + prefix="therock-dist-linux-${s3_target}-" + + files_json=$(curl -s "$base_url/" | tr '\n' ' ' | grep -oP 'const files = \K\[.*?\](?=\s*;)') + if [ -z "$files_json" ]; then + echo "Failed to parse file index from $base_url/" + exit 1 + fi + + # Pick the newest by the YYYYMMDD build date embedded in the filename. + # Anchor the match so the version comes immediately after the prefix. + # This excludes variants like "...-all-tests-<version>.tar.gz" that + # TheRock also publishes and share the same prefix/date. + latest_file=$(echo "$files_json" | jq -r --arg p "$prefix" \ + '[.[] | select(.name | test("^" + $p + "[0-9]+\\.[0-9]+\\.[0-9]+(a|rc)[0-9]+\\.tar\\.gz$"))] | sort_by(.name | capture("(?<d>[0-9]{8})\\.tar\\.gz$").d) | last | .name // empty') + if [ -z "$latest_file" ]; then + echo "No tarball found for prefix '$prefix' at $base_url/" + exit 1 + fi + echo "Found latest file: $latest_file" + + # Extract version from the filename for environment variable + if [[ "$latest_file" =~ therock-dist-linux-${s3_target}-([0-9]+\.[0-9]+\.[0-9]+(a|rc)[0-9]+)\.tar\.gz ]]; then + rocm_version="${BASH_REMATCH[1]}" + echo "Detected latest ROCm version: $rocm_version" + else + echo "Failed to extract ROCm version from latest file: $latest_file" + echo "Expected pattern: therock-dist-linux-${s3_target}-<version>.tar.gz" + exit 1 + fi + + rocm_url="$base_url/$latest_file" + else + # For specific versions, construct the URL directly + rocm_url="$base_url/therock-dist-linux-${s3_target}-${rocm_version}.tar.gz" + fi + + # Store the version for use in other steps + echo "DETECTED_ROCM_VERSION=$rocm_version" >> $GITHUB_ENV + + echo "Streaming ROCm from: $rocm_url directly to extraction" + + # Create directory if it doesn't exist + sudo mkdir -p /opt/rocm + + # Stream download directly into tar extraction (no intermediate file) + curl -sL "$rocm_url" | sudo tar --use-compress-program=gzip -xf - -C /opt/rocm --strip-components=1 + + - name: Set ROCm environment variables + run: | + echo "Setting ROCm environment variables..." + + # Set environment variables for this step and subsequent steps + echo "HIP_PATH=/opt/rocm" >> $GITHUB_ENV + echo "ROCM_PATH=/opt/rocm" >> $GITHUB_ENV + echo "HIP_PLATFORM=amd" >> $GITHUB_ENV + echo "HIP_CLANG_PATH=/opt/rocm/llvm/bin" >> $GITHUB_ENV + echo "HIP_INCLUDE_PATH=/opt/rocm/include" >> $GITHUB_ENV + echo "HIP_LIB_PATH=/opt/rocm/lib" >> $GITHUB_ENV + echo "HIP_DEVICE_LIB_PATH=/opt/rocm/lib/llvm/amdgcn/bitcode" >> $GITHUB_ENV + + # Update PATH + echo "/opt/rocm/bin:/opt/rocm/llvm/bin:$PATH" >> $GITHUB_PATH + + # Set library paths + echo "LD_LIBRARY_PATH=/opt/rocm/lib:/opt/rocm/lib64:/opt/rocm/llvm/lib:${LD_LIBRARY_PATH:-}" >> $GITHUB_ENV + echo "LIBRARY_PATH=/opt/rocm/lib:/opt/rocm/lib64:${LIBRARY_PATH:-}" >> $GITHUB_ENV + echo "CPATH=/opt/rocm/include:${CPATH:-}" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=/opt/rocm/lib/pkgconfig:${PKG_CONFIG_PATH:-}" >> $GITHUB_ENV + + echo "ROCm environment variables set successfully" + + + - name: Clone llama.cpp + run: | + llamacpp_version="${{ env.LLAMACPP_VERSION }}" + if [ "$llamacpp_version" = "latest" ]; then + echo "Cloning master branch (latest) with blobless clone" + git clone --filter=blob:none --single-branch --branch master https://github.com/${{ github.repository }}.git llama.cpp + else + echo "Cloning llama.cpp version: $llamacpp_version with blobless clone" + git clone --filter=blob:none --single-branch --branch "$llamacpp_version" https://github.com/${{ github.repository }}.git llama.cpp + fi + cd llama.cpp + + # Get commit hash (5 digits) and store it as environment variable + commit_hash=$(git rev-parse --short=5 HEAD) + echo "LLAMACPP_COMMIT_HASH=$commit_hash" >> $GITHUB_ENV + echo "llama.cpp commit hash (5 digits): $commit_hash" + + # Show current commit info + echo "Current llama.cpp commit:" + git log --oneline -1 + + + - name: Build Llama.cpp + ROCm + run: | + # Map GPU targets + current_target="${{ matrix.gfx_target }}" + echo "Input target: $current_target" + + if [ "$current_target" = "gfx110X" ]; then + mapped_target="gfx1100;gfx1101;gfx1102;gfx1103" + elif [ "$current_target" = "gfx103X" ]; then + mapped_target="gfx1030;gfx1031;gfx1032;gfx1034" + elif [ "$current_target" = "gfx1151" ]; then + mapped_target="gfx1151" + elif [ "$current_target" = "gfx1150" ]; then + mapped_target="gfx1150" + elif [ "$current_target" = "gfx120X" ]; then + mapped_target="gfx1200;gfx1201" + else + mapped_target="$current_target" + fi + echo "Mapped target: $mapped_target" + + # Create build directory + cd llama.cpp + mkdir build + cd build + + # Configure the project + cmake .. -G Ninja \ + -DCMAKE_C_COMPILER=/opt/rocm/llvm/bin/clang \ + -DCMAKE_CXX_COMPILER=/opt/rocm/llvm/bin/clang++ \ + -DCMAKE_CXX_FLAGS="-I/opt/rocm/include" \ + -DCMAKE_CROSSCOMPILING=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DGPU_TARGETS="$mapped_target" \ + -DBUILD_SHARED_LIBS=ON \ + -DLLAMA_BUILD_TESTS=OFF \ + -DGGML_HIP=ON \ + -DGGML_OPENMP=OFF \ + -DGGML_CUDA_FORCE_CUBLAS=OFF \ + -DGGML_RPC=ON \ + -DGGML_HIP_ROCWMMA_FATTN=OFF \ + -DLLAMA_BUILD_BORINGSSL=ON \ + -DGGML_NATIVE=OFF \ + -DGGML_STATIC=OFF \ + -DCMAKE_SYSTEM_NAME=Linux + + # Build the project + cmake --build . -j $(nproc) + + - name: Copy ROCm core libs to build directory + run: | + build_bin_path="llama.cpp/build/bin" + rocm_bin_path="/opt/rocm/bin" + + # Copy the rocblas/library folder and all its contents + rocblas_lib_path="/opt/rocm/lib/rocblas/library" + if [ -d "$rocblas_lib_path" ]; then + echo "Copying rocblas/library folder and all contents..." + dest_rocblas_path="$build_bin_path/rocblas/library" + mkdir -p "$(dirname "$dest_rocblas_path")" + cp -r "$rocblas_lib_path" "$(dirname "$dest_rocblas_path")/" + echo "Copied: rocblas/library folder with all contents" + + # List the contents of the copied rocblas/library folder + echo "Contents of rocblas/library:" + find "$dest_rocblas_path" -type f -exec ls -la {} \; | head -20 + else + echo "Warning: rocblas/library folder not found at: $rocblas_lib_path" + fi + + # Copy the hipblaslt/library folder and all its contents + hipblaslt_lib_path="/opt/rocm/lib/hipblaslt/library" + if [ -d "$hipblaslt_lib_path" ]; then + echo "Copying hipblaslt/library folder and all contents..." + dest_hipblaslt_path="$build_bin_path/hipblaslt/library" + mkdir -p "$(dirname "$dest_hipblaslt_path")" + cp -r "$hipblaslt_lib_path" "$(dirname "$dest_hipblaslt_path")/" + echo "Copied: hipblaslt/library folder with all contents" + + # List the contents of the copied hipblaslt/library folder + echo "Contents of hipblaslt/library:" + find "$dest_hipblaslt_path" -type f -exec ls -la {} \; | head -20 + else + echo "Warning: hipblaslt/library folder not found at: $hipblaslt_lib_path" + fi + + # Copy required ROCm libraries to build directory + # If artifacts from ROCm or Llama.cpp change, you may need to update this list + # To get a new list of all libraries, run: + # scripts/rocm-release/gather_required_libs.py --rocm-dir /opt/rocm --dest-dir llama.cpp/build/bin + echo "Copying required ROCm libraries to build directory..." + cp -v /opt/rocm/lib/libhipblas.so* "$build_bin_path/" 2>/dev/null || echo "libhipblas.so* not found" + cp -v /opt/rocm/lib/librocblas.so* "$build_bin_path/" 2>/dev/null || echo "librocblas.so* not found" + cp -v /opt/rocm/lib/libamdhip64.so* "$build_bin_path/" 2>/dev/null || echo "libamdhip64.so* not found" + cp -v /opt/rocm/lib/librocsolver.so* "$build_bin_path/" 2>/dev/null || echo "librocsolver.so* not found" + cp -v /opt/rocm/lib/libroctx64.so* "$build_bin_path/" 2>/dev/null || echo "libroctx64.so* not found" + cp -v /opt/rocm/lib/libhipblaslt.so* "$build_bin_path/" 2>/dev/null || echo "libhipblaslt.so* not found" + cp -v /opt/rocm/lib/libtensilelite-host.so* "$build_bin_path/" 2>/dev/null || echo "libtensilelite-host.so* not found" + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_liblzma.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_liblzma.so* not found" + cp -v /opt/rocm/lib/librocprofiler-register.so* "$build_bin_path/" 2>/dev/null || echo "librocprofiler-register.so* not found" + cp -v /opt/rocm/lib/libamd_comgr.so* "$build_bin_path/" 2>/dev/null || echo "libamd_comgr.so* not found" + cp -v /opt/rocm/lib/libamd_comgr_loader.so* "$build_bin_path/" 2>/dev/null || echo "libamd_comgr_loader.so* not found" + cp -v /opt/rocm/lib/libhsa-runtime64.so* "$build_bin_path/" 2>/dev/null || echo "libhsa-runtime64.so* not found" + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_numa.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_numa.so* not found" + cp -v /opt/rocm/lib/librocroller.so* "$build_bin_path/" 2>/dev/null || echo "librocroller.so* not found" + cp -v /opt/rocm/lib/liborigami.so* "$build_bin_path/" 2>/dev/null || echo "liborigami.so* not found" + cp -v /opt/rocm/lib/librocm_kpack.so* "$build_bin_path/" 2>/dev/null || echo "librocm_kpack.so* not found" + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_z.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_z.so* not found" + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_zstd.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_zstd.so* not found" + cp -v /opt/rocm/lib/llvm/lib/libLLVM.so* "$build_bin_path/" 2>/dev/null || echo "libLLVM.so* not found" + cp -v /opt/rocm/lib/llvm/lib/libclang-cpp.so* "$build_bin_path/" 2>/dev/null || echo "libclang-cpp.so* not found" + cp -v /opt/rocm/lib/llvm/lib/libomp.so* "$build_bin_path/" 2>/dev/null || echo "libomp.so* not found" + + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_elf.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_elf.so* not found" + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_drm.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_drm.so* not found" + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_drm_amdgpu.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_drm_amdgpu.so* not found" + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_bz2.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_bz2.so* not found" + + echo "Finished copying required ROCm libraries" + + - name: Set RPATH for portable distribution + run: | + sudo apt-get install -y patchelf + cd llama.cpp/build/bin + # Set RPATH to $ORIGIN so all libraries (including the comgr stub loader) find deps locally + for file in *.so* llama-*; do + [ -f "$file" ] && [ ! -L "$file" ] && patchelf --set-rpath '$ORIGIN' "$file" 2>/dev/null || true + done + + - name: List build artifacts (including ROCm files) + run: | + cd llama.cpp/build/bin + echo "Final build artifacts (including ROCm library files):" + ls -la + + - name: Upload build artifacts + id: upload-artifacts + uses: actions/upload-artifact@v4 + with: + name: llama-ubuntu-rocm-${{ matrix.gfx_target }}-x64 + path: llama.cpp/build/bin/ + retention-days: 30 + + - name: Set job outputs + id: set-outputs + run: | + echo "Setting job outputs..." + echo "Current target: ${{ matrix.gfx_target }}" + echo "DETECTED_ROCM_VERSION: ${DETECTED_ROCM_VERSION}" + echo "ROCM_VERSION: ${{ env.ROCM_VERSION }}" + echo "LLAMACPP_COMMIT_HASH: ${LLAMACPP_COMMIT_HASH}" + + rocm_version="${DETECTED_ROCM_VERSION:-${{ env.ROCM_VERSION }}}" + echo "rocm_version=$rocm_version" >> $GITHUB_OUTPUT + echo "llamacpp_commit_hash=${LLAMACPP_COMMIT_HASH}" >> $GITHUB_OUTPUT + + echo "Final rocm_version: $rocm_version" + echo "Final llamacpp_commit_hash: ${LLAMACPP_COMMIT_HASH}" + + - name: Create release summary + run: | + cat > "build-summary-ubuntu-${{ matrix.gfx_target }}.md" << 'EOF' + # Llama.cpp + ROCm Build Summary (Ubuntu - ${{ matrix.gfx_target }}) + + ## Build Configuration + - **GPU Target**: ${{ matrix.gfx_target }} + - **ROCm Version**: ${{ env.DETECTED_ROCM_VERSION || env.ROCM_VERSION }} + - **llama.cpp Commit Hash**: ${{ env.LLAMACPP_COMMIT_HASH }} + - **Build Type**: Release + - **Platform**: Ubuntu + + ## Build Artifacts + The compiled binaries are available in the build artifacts. + EOF + + - name: Upload build summary + id: upload-summary + uses: actions/upload-artifact@v4 + with: + name: build-summary-ubuntu-${{ matrix.gfx_target }} + path: build-summary-ubuntu-${{ matrix.gfx_target }}.md + retention-days: 30 + + - name: Clean up build artifacts and temporary files + if: always() + run: | + echo "Starting cleanup process..." + + # Remove ROCm tarball + if [ -f "rocm.tar.gz" ]; then + echo "Removing rocm.tar.gz..." + rm -f rocm.tar.gz + fi + + # Remove ROCm installation directory + if [ -d "/opt/rocm" ]; then + echo "Removing /opt/rocm directory..." + sudo rm -rf /opt/rocm + fi + + # Remove llama.cpp source directory (keeping only the build artifacts) + if [ -d "llama.cpp" ]; then + echo "Removing llama.cpp source directory..." + rm -rf llama.cpp + fi + + # Clean up any temporary build files + if [ -f "build-summary-ubuntu-${{ matrix.gfx_target }}.md" ]; then + echo "Removing build-summary-ubuntu-${{ matrix.gfx_target }}.md..." + rm -f "build-summary-ubuntu-${{ matrix.gfx_target }}.md" + fi + + echo "Cleanup completed successfully" + + test-stx-halo: + runs-on: ${{ matrix.runner }} + needs: [prepare-matrix, build-windows, build-ubuntu] + if: | + vars.STX_HALO_RUNNERS == 'true' && + (needs.build-windows.result == 'success' || needs.build-ubuntu.result == 'success') && + contains(github.event.inputs.gfx_target || 'gfx1151,gfx1150,gfx120X,gfx110X', 'gfx1151') + strategy: + matrix: + include: + - os: Windows + gfx_target: gfx1151 + artifact_prefix: windows + runner: ["stx-halo", "Windows"] + - os: Linux + gfx_target: gfx1151 + artifact_prefix: ubuntu + runner: ["stx-halo", "Linux"] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Test llama.cpp build + uses: ./.github/actions/test-llamacpp-build + with: + os_type: ${{ matrix.os }} + gfx_target: ${{ matrix.gfx_target }} + artifact_name: llama-${{ matrix.artifact_prefix }}-rocm-${{ matrix.gfx_target }}-x64 + + test-stx: + runs-on: ${{ matrix.runner }} + needs: [prepare-matrix, build-windows, build-ubuntu] + if: | + vars.STX_RUNNERS == 'true' && + (needs.build-windows.result == 'success' || needs.build-ubuntu.result == 'success') && + contains(github.event.inputs.gfx_target || 'gfx1151,gfx1150,gfx120X,gfx110X', 'gfx1150') + strategy: + matrix: + include: + - os: Windows + gfx_target: gfx1150 + artifact_prefix: windows + runner: ["stx", "Windows"] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Test llama.cpp build + uses: ./.github/actions/test-llamacpp-build + with: + os_type: ${{ matrix.os }} + gfx_target: ${{ matrix.gfx_target }} + artifact_name: llama-${{ matrix.artifact_prefix }}-rocm-${{ matrix.gfx_target }}-x64 + + create-release: + needs: [prepare-matrix, build-windows, build-ubuntu, test-stx-halo, test-stx] + runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write + if: | + always() && + (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped') && + (needs.build-ubuntu.result == 'success' || needs.build-ubuntu.result == 'skipped') && + (needs.build-windows.result == 'success' || needs.build-ubuntu.result == 'success') && + (needs.test-stx-halo.result == 'success' || needs.test-stx-halo.result == 'skipped') && + (needs.test-stx.result == 'success' || needs.test-stx.result == 'skipped') && + github.event_name != 'pull_request' && + (github.event_name == 'workflow_dispatch' && + (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == null) || + github.event_name == 'schedule') + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + path: ./all-artifacts + + - name: Generate release tag + id: generate-tag + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + # Get all existing release tags that match the pattern b#### + existing_tags=$(gh release list --limit 1000 --json tagName --jq '.[].tagName' | grep -E '^b[0-9]{4}$' | sort -V || echo "") + + if [ -z "$existing_tags" ]; then + # No existing sequential tags found, start with b1000 + next_number=1000 + echo "No existing sequential release tags found, starting with b1000" + else + # Find the highest existing number + highest_tag=$(echo "$existing_tags" | tail -n 1) + highest_number=$(echo "$highest_tag" | sed 's/^b//') + next_number=$((highest_number + 1)) + echo "Highest existing tag: $highest_tag (number: $highest_number)" + echo "Next number will be: $next_number" + fi + + # Format as b#### (4-digit number with leading zeros) + TAG=$(printf "b%04d" $next_number) + + echo "tag=${TAG}" >> $GITHUB_OUTPUT + echo "Generated release tag: ${TAG}" + + - name: Check if tag already exists + id: check-tag + run: | + TAG="${{ steps.generate-tag.outputs.tag }}" + + # Check if the tag already exists + if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then + echo "Tag $TAG already exists, skipping release creation" + echo "tag_exists=true" >> $GITHUB_OUTPUT + else + echo "Tag $TAG does not exist, proceeding with release creation" + echo "tag_exists=false" >> $GITHUB_OUTPUT + fi + + - name: Create archives for all target artifacts + if: steps.check-tag.outputs.tag_exists == 'false' + run: | + # Parse targets and operating systems from environment + targets="${{ env.GFX_TARGETS }}" + operating_systems="${{ env.OPERATING_SYSTEMS }}" + TAG="${{ steps.generate-tag.outputs.tag }}" + + echo "Processing targets: $targets" + echo "Processing operating systems: $operating_systems" + echo "Using release tag: $TAG" + + # Create individual archives for each target and OS combination + IFS=',' read -ra TARGET_ARRAY <<< "$targets" + IFS=',' read -ra OS_ARRAY <<< "$operating_systems" + + for os in "${OS_ARRAY[@]}"; do + os=$(echo "$os" | xargs) # trim whitespace + for target in "${TARGET_ARRAY[@]}"; do + target=$(echo "$target" | xargs) # trim whitespace + + echo "Processing OS: $os, target: $target" + + # Use artifact name to find the directory + artifact_name="llama-${os}-rocm-${target}-x64" + artifact_dir="./all-artifacts/${artifact_name}" + + # Create final archive with release tag + final_archive_name="llama-${TAG}-${os}-rocm-${target}-x64" + + if [ -d "$artifact_dir" ]; then + echo "Creating archive: ${final_archive_name}.zip" + cd "$artifact_dir" + zip -r "../../${final_archive_name}.zip" * + cd ../../ + else + echo "Warning: Artifact directory not found: $artifact_dir" + ls -la ./all-artifacts/ + fi + done + done + + echo "Created archives:" + ls -la *.zip + + - name: Create Release + if: steps.check-tag.outputs.tag_exists == 'false' + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + TAG="${{ steps.generate-tag.outputs.tag }}" + # Get ROCm version from either Windows or Ubuntu build (whichever succeeded) + ROCM_VERSION="${{ needs.build-windows.outputs.rocm_version || needs.build-ubuntu.outputs.rocm_version }}" + # Get llama.cpp commit hash from either Windows or Ubuntu build (whichever succeeded) + LLAMACPP_COMMIT_HASH="${{ needs.build-windows.outputs.llamacpp_commit_hash || needs.build-ubuntu.outputs.llamacpp_commit_hash }}" + targets="${{ env.GFX_TARGETS }}" + operating_systems="${{ env.OPERATING_SYSTEMS }}" + + echo "Creating release with tag: $TAG" + echo "GPU Targets: $targets" + echo "Operating Systems: $operating_systems" + echo "ROCm Version: $ROCM_VERSION" + echo "Llama.cpp Commit: $LLAMACPP_COMMIT_HASH" + + # Verify archives exist + ls -la *.zip + + # Prepare upload files list + upload_files="" + IFS=',' read -ra TARGET_ARRAY <<< "$targets" + IFS=',' read -ra OS_ARRAY <<< "$operating_systems" + + for os in "${OS_ARRAY[@]}"; do + os=$(echo "$os" | xargs) # trim whitespace + for target in "${TARGET_ARRAY[@]}"; do + target=$(echo "$target" | xargs) # trim whitespace + + final_archive_name="llama-${TAG}-${os}-rocm-${target}-x64" + if [ -f "${final_archive_name}.zip" ]; then + upload_files="${upload_files} ${final_archive_name}.zip" + fi + done + done + + echo "Files to upload: $upload_files" + + # Create release with GitHub CLI + gh release create "$TAG" \ + --title "$TAG" \ + --notes "**Build Number**: $TAG + **Operating System(s)**: $operating_systems + **GPU Target(s)**: $targets + **ROCm Version**: $ROCM_VERSION + **Llama.cpp Commit Hash**: $LLAMACPP_COMMIT_HASH + **Build Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC') + + This release includes compiled llama.cpp binaries with ROCm support for multiple GPU targets and operating systems, with all essential ROCm runtime libraries included." \ + $upload_files diff --git a/.github/workflows/nightly-failure-alert.yml b/.github/workflows/nightly-failure-alert.yml new file mode 100644 index 000000000000..3b4ab66736a3 --- /dev/null +++ b/.github/workflows/nightly-failure-alert.yml @@ -0,0 +1,66 @@ +# Same as lemonade-sdk/llamacpp-rocm (MIT). +name: Nightly Failure Alert +on: + workflow_run: + workflows: ["Build Llama.cpp + ROCm"] + types: [completed] + +env: + # GitHub user(s) to alert, space-separated + ALERT_USERS: dzannotti + ALERT_LABEL: nightly-failure + +jobs: + alert: + if: | + github.event.workflow_run.event == 'schedule' && + github.event.workflow_run.conclusion != 'success' + runs-on: ubuntu-22.04 + permissions: + issues: write + actions: read + steps: + - name: Open or update failure issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RUN_ID: ${{ github.event.workflow_run.id }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + CONCLUSION: ${{ github.event.workflow_run.conclusion }} + RUN_DATE: ${{ github.event.workflow_run.run_started_at }} + run: | + failed_jobs=$(gh api --paginate "repos/$GH_REPO/actions/runs/$RUN_ID/jobs?per_page=100" \ + --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | "- [\(.name)](\(.html_url)) — \(.conclusion)"') + [ -z "$failed_jobs" ] && failed_jobs="- (no individual job failures reported)" + + mentions=$(for u in $ALERT_USERS; do printf '@%s ' "$u"; done) + assignees=$(echo $ALERT_USERS | tr ' ' ',') + + body=$(cat <<EOF + $mentions the nightly llama.cpp + ROCm build **${CONCLUSION}** (started ${RUN_DATE}). No release was published. + + **Run:** $RUN_URL + + **Failed jobs:** + $failed_jobs + EOF + ) + + gh label create "$ALERT_LABEL" --color B60205 \ + --description "Scheduled nightly build failed" --force + + existing=$(gh issue list --label "$ALERT_LABEL" --state open --limit 1 --json number --jq '.[0].number // empty') + + if [ -n "$existing" ]; then + echo "Commenting on existing issue #$existing" + gh issue comment "$existing" --body "$body" + else + echo "Opening new issue" + gh issue create \ + --title "Nightly build failed ($(date -u -d "$RUN_DATE" '+%Y-%m-%d'))" \ + --label "$ALERT_LABEL" \ + --assignee "$assignees" \ + --body "$body + + Close this issue once the nightly is fixed. Further failures while it is open are added as comments." + fi diff --git a/.github/workflows/test-llamacpp-rocm.yml b/.github/workflows/test-llamacpp-rocm.yml new file mode 100644 index 000000000000..f8b303277e68 --- /dev/null +++ b/.github/workflows/test-llamacpp-rocm.yml @@ -0,0 +1,394 @@ +# Same as lemonade-sdk/llamacpp-rocm (MIT). Needs self-hosted runners labelled stx-halo + Windows/Linux. +name: Test Llama.cpp + ROCm +on: + workflow_dispatch: + inputs: + operating_systems: + description: 'Operating systems to test on (comma-separated: windows,ubuntu)' + required: false + default: 'windows,ubuntu' + gfx_target: + description: 'AMD GPU targets (comma-separated)' + required: false + default: 'gfx1151' + release_tag: + description: 'Release tag to test (e.g., b1001) or "latest" for the most recent release' + required: false + default: 'latest' + +env: + OPERATING_SYSTEMS: ${{ github.event.inputs.operating_systems || 'windows,ubuntu' }} + GFX_TARGETS: ${{ github.event.inputs.gfx_target || 'gfx1151' }} + RELEASE_TAG: ${{ github.event.inputs.release_tag || 'latest' }} + +jobs: + prepare-matrix: + runs-on: ubuntu-22.04 + outputs: + windows_matrix: ${{ steps.set-matrix.outputs.windows_matrix }} + ubuntu_matrix: ${{ steps.set-matrix.outputs.ubuntu_matrix }} + should_test_windows: ${{ steps.set-matrix.outputs.should_test_windows }} + should_test_ubuntu: ${{ steps.set-matrix.outputs.should_test_ubuntu }} + latest_release_tag: ${{ steps.get-release.outputs.latest_release_tag }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get release tag + id: get-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + release_input="${{ env.RELEASE_TAG }}" + + if [ "$release_input" = "latest" ]; then + # Get the latest release tag using GitHub REST API + release_tag=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/${{ github.repository }}/releases/latest" \ + | jq -r '.tag_name') + echo "Using latest release: $release_tag" + else + # Use the specified release tag + release_tag="$release_input" + echo "Using specified release: $release_tag" + + # Verify the release exists using GitHub REST API + response_code=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/${{ github.repository }}/releases/tags/$release_tag") + + if [ "$response_code" != "200" ]; then + echo "Error: Release '$release_tag' does not exist (HTTP $response_code)" + exit 1 + fi + fi + + echo "latest_release_tag=$release_tag" >> $GITHUB_OUTPUT + echo "Final release tag: $release_tag" + + - name: Set matrix + id: set-matrix + run: | + targets="${{ env.GFX_TARGETS }}" + operating_systems="${{ env.OPERATING_SYSTEMS }}" + + echo "Input targets: $targets" + echo "Input operating systems: $operating_systems" + # Convert targets to JSON array + matrix_targets=$(echo "$targets" \ + | tr ',' '\n' \ + | sed 's/^ *//;s/ *$//' \ + | sed 's/^"//;s/"$//' \ + | jq -R . \ + | jq -s '{gfx_target: .}' \ + | jq -c) + # Check which operating systems to test + should_test_windows="false" + should_test_ubuntu="false" + + if [[ "$operating_systems" == *"windows"* ]]; then + should_test_windows="true" + echo "windows_matrix=$matrix_targets" >> $GITHUB_OUTPUT + fi + + if [[ "$operating_systems" == *"ubuntu"* ]]; then + should_test_ubuntu="true" + echo "ubuntu_matrix=$matrix_targets" >> $GITHUB_OUTPUT + fi + + echo "should_test_windows=$should_test_windows" >> $GITHUB_OUTPUT + echo "should_test_ubuntu=$should_test_ubuntu" >> $GITHUB_OUTPUT + + echo "Windows test: $should_test_windows" + echo "Ubuntu test: $should_test_ubuntu" + echo "Generated matrix: $matrix_targets" + test-windows: + runs-on: [stx-halo, Windows] + needs: prepare-matrix + if: vars.STX_HALO_RUNNERS == 'true' && needs.prepare-matrix.outputs.should_test_windows == 'true' + strategy: + matrix: ${{fromJson(needs.prepare-matrix.outputs.windows_matrix)}} + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download release artifacts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $releaseTag = "${{ needs.prepare-matrix.outputs.latest_release_tag }}" + $target = "${{ matrix.gfx_target }}" + $assetName = "llama-${releaseTag}-windows-rocm-${target}-x64.zip" + $repo = "${{ github.repository }}" + + Write-Host "Downloading release asset: $assetName" + Write-Host "From release: $releaseTag" + + # Get release information using GitHub REST API + $headers = @{ "Authorization" = "token $env:GITHUB_TOKEN" } + $releaseUrl = "https://api.github.com/repos/$repo/releases/tags/$releaseTag" + + try { + $release = Invoke-RestMethod -Uri $releaseUrl -Headers $headers + $asset = $release.assets | Where-Object { $_.name -eq $assetName } + + if (-not $asset) { + Write-Error "Asset '$assetName' not found in release '$releaseTag'" + exit 1 + } + + Write-Host "Found asset: $($asset.name) (Size: $($asset.size) bytes)" + + # Download the specific asset + Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $assetName -Headers $headers + Write-Host "Downloaded $assetName successfully" + } + catch { + Write-Error "Failed to download release asset: $_" + exit 1 + } + + # Extract the downloaded zip file + Write-Host "Extracting $assetName..." + Expand-Archive -Path $assetName -DestinationPath "llama-binaries" -Force + + # List contents + Write-Host "Contents of llama-binaries:" + Get-ChildItem -Recurse "llama-binaries" | Format-Table Name, Length + + - name: Download test model + run: | + $modelUrl = "https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_0.gguf?download=true" + $modelPath = "Qwen3-0.6B-Q4_0.gguf" + + Write-Host "Downloading test model from: $modelUrl" + curl.exe -L -o $modelPath $modelUrl + + # Verify download + if (Test-Path $modelPath) { + $fileSize = (Get-Item $modelPath).Length + Write-Host "Model downloaded successfully. Size: $fileSize bytes" + } else { + Write-Error "Failed to download model" + exit 1 + } + + - name: Run llama-cli test + run: | + $llamaCliPath = "llama-binaries\llama-cli.exe" + $modelPath = "Qwen3-0.6B-Q4_0.gguf" + + # Verify llama-cli exists + if (-not (Test-Path $llamaCliPath)) { + Write-Error "llama-cli.exe not found at: $llamaCliPath" + Write-Host "Available files in llama-binaries:" + Get-ChildItem -Recurse "llama-binaries" + exit 1 + } + + Write-Host "Running llama-cli test..." + Write-Host "Command: $llamaCliPath -m `"$modelPath`" -ngl 99 -p `"I believe the meaning of life is`" -st" + + # Run the command and capture output + try { + $process = Start-Process -FilePath $llamaCliPath -ArgumentList @("-m", "`"$modelPath`"", "-ngl", "99", "-p", "`"I believe the meaning of life is`"", "-st") -NoNewWindow -Wait -PassThru -RedirectStandardOutput "stdout.txt" -RedirectStandardError "stderr.txt" + + # Wait for the process to complete + $process.WaitForExit() + + # Read both stdout and stderr + $stdout = Get-Content "stdout.txt" -Raw -ErrorAction SilentlyContinue + $stderr = Get-Content "stderr.txt" -Raw -ErrorAction SilentlyContinue + + # Combine output (stderr often contains important CUDA/GPU info) + $output = $stdout + $stderr + + # Clean up temp files + Remove-Item "stdout.txt" -ErrorAction SilentlyContinue + Remove-Item "stderr.txt" -ErrorAction SilentlyContinue + + Write-Host "Process exit code: $($process.ExitCode)" + } + catch { + Write-Error "Failed to run llama-cli: $_" + exit 1 + } + + Write-Host "=== LLAMA-CLI OUTPUT ===" + Write-Host $output + Write-Host "=== END OUTPUT ===" + + # Check if the process exited successfully + if ($process.ExitCode -ne 0) { + Write-Host "❌ llama-cli exited with error code: $($process.ExitCode)" + exit 1 + } + + # Check for expected outputs + $foundGpuOffload = $output -match "offloaded 29/29 layers to GPU" + $foundThinkTag = $output -match "</think>" + + Write-Host "=== TEST RESULTS ===" + Write-Host "Expected 'offloaded 29/29 layers to GPU': $(if ($foundGpuOffload) { 'FOUND' } else { 'NOT FOUND' })" + Write-Host "Expected '</think>' tag: $(if ($foundThinkTag) { 'FOUND' } else { 'NOT FOUND' })" + + if ($foundGpuOffload -and $foundThinkTag) { + Write-Host "✅ Test PASSED - Both expected outputs found" + } else { + Write-Host "❌ Test FAILED - Missing expected outputs" + exit 1 + } + test-ubuntu: + runs-on: [stx-halo, Linux] + needs: prepare-matrix + if: vars.STX_HALO_RUNNERS == 'true' && needs.prepare-matrix.outputs.should_test_ubuntu == 'true' + strategy: + matrix: ${{fromJson(needs.prepare-matrix.outputs.ubuntu_matrix)}} + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install jq + run: | + if ! command -v jq &> /dev/null; then + echo "Installing jq..." + # Download and install jq binary directly + mkdir -p ~/bin + curl -L -o ~/bin/jq https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64 + chmod +x ~/bin/jq + export PATH="$HOME/bin:$PATH" + echo "PATH=$HOME/bin:$PATH" >> $GITHUB_ENV + echo "jq installed to ~/bin/jq" + else + echo "jq is already installed" + fi + + - name: Download release artifacts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + release_tag="${{ needs.prepare-matrix.outputs.latest_release_tag }}" + target="${{ matrix.gfx_target }}" + asset_name="llama-${release_tag}-ubuntu-rocm-${target}-x64.zip" + repo="${{ github.repository }}" + + echo "Downloading release asset: $asset_name" + echo "From release: $release_tag" + + # Get release information using GitHub REST API + release_url="https://api.github.com/repos/$repo/releases/tags/$release_tag" + + # Get release data and extract asset download URL + release_data=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "$release_url") + + if [ $? -ne 0 ]; then + echo "Error: Failed to fetch release information" + exit 1 + fi + + # Extract the browser_download_url for the specific asset + download_url=$(echo "$release_data" | jq -r ".assets[] | select(.name == \"$asset_name\") | .browser_download_url") + + if [ "$download_url" = "null" ] || [ -z "$download_url" ]; then + echo "Error: Asset '$asset_name' not found in release '$release_tag'" + echo "Available assets:" + echo "$release_data" | jq -r '.assets[].name' + exit 1 + fi + + asset_size=$(echo "$release_data" | jq -r ".assets[] | select(.name == \"$asset_name\") | .size") + echo "Found asset: $asset_name (Size: $asset_size bytes)" + + # Download the specific asset + curl -L -H "Authorization: token $GITHUB_TOKEN" -o "$asset_name" "$download_url" + + if [ $? -eq 0 ]; then + echo "Downloaded $asset_name successfully" + else + echo "Error: Failed to download $asset_name" + exit 1 + fi + + # Extract the downloaded zip file + echo "Extracting $asset_name..." + unzip -q "$asset_name" -d llama-binaries + + # List contents + echo "Contents of llama-binaries:" + find llama-binaries -type f -exec ls -la {} \; + + - name: Download test model + run: | + model_url="https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_0.gguf?download=true" + model_path="Qwen3-0.6B-Q4_0.gguf" + + echo "Downloading test model from: $model_url" + curl -L -o "$model_path" "$model_url" + + # Verify download + if [ -f "$model_path" ]; then + file_size=$(stat -c%s "$model_path") + echo "Model downloaded successfully. Size: $file_size bytes" + else + echo "Failed to download model" + exit 1 + fi + + - name: Set up library path + run: | + # Add the llama-binaries directory to LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH=$(pwd)/llama-binaries:$LD_LIBRARY_PATH" >> $GITHUB_ENV + + - name: Run llama-cli test + run: | + llama_cli_path="./llama-binaries/llama-cli" + model_path="Qwen3-0.6B-Q4_0.gguf" + + # Make llama-cli executable + chmod +x "$llama_cli_path" + + # Verify llama-cli exists + if [ ! -f "$llama_cli_path" ]; then + echo "llama-cli not found at: $llama_cli_path" + echo "Available files in llama-binaries:" + find llama-binaries -type f + exit 1 + fi + + echo "Running llama-cli test..." + echo "Command: $llama_cli_path -m \"$model_path\" -ngl 99 -p \"I believe the meaning of life is\" -st" + + # Run the command and capture output + output=$("$llama_cli_path" -m "$model_path" -ngl 99 -p "I believe the meaning of life is" -st 2>&1) + + echo "=== LLAMA-CLI OUTPUT ===" + echo "$output" + echo "=== END OUTPUT ===" + + # Check for expected outputs + if echo "$output" | grep -q "offloaded 29/29 layers to GPU"; then + found_gpu_offload=true + else + found_gpu_offload=false + fi + + if echo "$output" | grep -q "</think>"; then + found_think_tag=true + else + found_think_tag=false + fi + + echo "=== TEST RESULTS ===" + echo "Expected 'offloaded 29/29 layers to GPU': $(if [ "$found_gpu_offload" = true ]; then echo 'FOUND'; else echo 'NOT FOUND'; fi)" + echo "Expected '</think>' tag: $(if [ "$found_think_tag" = true ]; then echo 'FOUND'; else echo 'NOT FOUND'; fi)" + + if [ "$found_gpu_offload" = true ] && [ "$found_think_tag" = true ]; then + echo "✅ Test PASSED - Both expected outputs found" + else + echo "❌ Test FAILED - Missing expected outputs" + exit 1 + fi \ No newline at end of file diff --git a/docs/rocm-releases.md b/docs/rocm-releases.md new file mode 100644 index 000000000000..a12c55f82284 --- /dev/null +++ b/docs/rocm-releases.md @@ -0,0 +1,42 @@ +# ROCm release builds + +`.github/workflows/build-llamacpp-rocm.yml` publishes prebuilt ROCm binaries of this fork. It copies the +pipeline of [lemonade-sdk/llamacpp-rocm](https://github.com/lemonade-sdk/llamacpp-rocm) (MIT) and uses the same +artifact layout, so tools that consume their releases can consume these ones too. + +## What a release contains + +- One zip per OS and GPU target: `llama-bNNNN-{windows,ubuntu}-rocm-{target}-x64.zip`. +- Targets: `gfx1151`, `gfx1150`, `gfx120X`, `gfx110X`, `gfx103X`, `gfx90a`, `gfx908`. +- Each zip holds the llama.cpp binaries and shared libraries, built with `GGML_HIP=ON`, `GGML_RPC=ON` and + `BUILD_SHARED_LIBS=ON`. +- Each zip also holds the ROCm runtime libraries it needs (hipBLAS, rocBLAS, hipBLASLt and their kernel + libraries). On Linux, RPATH is set to `$ORIGIN`, so the zip runs without a system ROCm install. +- ROCm comes from the newest TheRock nightly tarball on `nightly.repo.amd.com` unless one is pinned. +- Releases are tagged `b1000`, `b1001` and so on, counting up. This numbering does not collide with the `b1xxxx` + tags carried over from upstream llama.cpp. +- The release notes record the build number, targets, ROCm version, source commit (5 characters) and build date. + +## When it runs + +- Nightly at 13:00 UTC, two hours after TheRock publishes its nightly. This builds `master` and publishes a release. +- On demand with `workflow_dispatch`. Inputs: OS list, targets, ROCm version, branch or tag, and whether to + publish. +- On pull requests that change the pipeline itself. These runs build but do not publish. + +If a nightly run fails, `nightly-failure-alert.yml` opens or updates an issue labelled `nightly-failure`. + +## Hardware tests + +`test-stx-halo` and `test-stx` run the built zip on real hardware. They are skipped unless the repository +variables `STX_HALO_RUNNERS` and `STX_RUNNERS` are set to `true`, because jobs whose self-hosted runners are +missing sit in the queue until they time out. To enable them: + +1. Register runners with the labels `stx-halo` + `Windows` / `Linux` (and `stx` + `Windows` for gfx1150). +2. Set the matching variable to `true`. + +`test-llamacpp-rocm.yml` tests an already published release on the same runners. + +## Tokens + +The workflow uses the `GH_TOKEN` secret when it exists. Otherwise it uses the job token with `contents: write`. diff --git a/scripts/rocm-release/gather_required_libs.py b/scripts/rocm-release/gather_required_libs.py new file mode 100644 index 000000000000..2a7d09585c1b --- /dev/null +++ b/scripts/rocm-release/gather_required_libs.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# From lemonade-sdk/llamacpp-rocm (MIT): lists the ROCm libraries a build links against, see build-llamacpp-rocm.yml + +import subprocess +import os +import shutil +import argparse + + +def find_lib_in_rocm(libname, rocm_dir): + # Simple linear search for libname inside rocm_dir + for root, _, files in os.walk(rocm_dir): + if libname in files: + return os.path.join(root, libname) + + # Exit if the library is not found in rocm_dir + raise RuntimeError(f"Could not find {libname} in {rocm_dir}") + + +def main(): + parser = argparse.ArgumentParser( + description="Gather required libraries for llama-server" + ) + parser.add_argument( + "--rocm-dir", + default="/opt/rocm", + help="Path to ROCm installation directory (default: /opt/rocm)", + ) + parser.add_argument( + "--dest-dir", + default=os.path.expanduser("~/llama.cpp/build/bin"), + help="Destination directory for libraries and binary (default: ~/llama.cpp/build/bin)", + ) + + args = parser.parse_args() + + rocm_dir = args.rocm_dir + dest_dir = args.dest_dir + binary = os.path.join(dest_dir, "llama-server") + + # Create the destination directory and run the binary + os.makedirs(dest_dir, exist_ok=True) + result = subprocess.run([binary], capture_output=True, text=True) + print(f"Error Found: {result.stderr}") + + # Copy the missing libraries to the destination directory + while "error while loading shared libraries" in result.stderr: + so_file = result.stderr.split("shared libraries: ")[1].split(": ")[0] + so_file_path = find_lib_in_rocm(so_file, rocm_dir) + shutil.copy2(so_file_path, dest_dir) + print(f"Copied {so_file_path} -> {dest_dir}") + result = subprocess.run([binary], capture_output=True, text=True) + print(f"Error Found: {result.stderr}") + + +if __name__ == "__main__": + main() From 7e4f9161a528d8eb0ea450929feda026228ca176 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Tue, 29 Sep 2026 19:58:29 +0100 Subject: [PATCH 205/212] ci : release every merge to master, Linux zips by default Release on push to master instead of a nightly cron and build the merged commit. Default to ubuntu; Windows remains available through workflow_dispatch. Strip the trailing whitespace the editorconfig check rejects. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../actions/test-llamacpp-build/action.yml | 66 ++--- .github/workflows/build-llamacpp-rocm.yml | 267 +++++++++--------- .github/workflows/nightly-failure-alert.yml | 8 +- .github/workflows/test-llamacpp-rocm.yml | 124 ++++---- docs/rocm-releases.md | 12 +- 5 files changed, 244 insertions(+), 233 deletions(-) diff --git a/.github/actions/test-llamacpp-build/action.yml b/.github/actions/test-llamacpp-build/action.yml index 3152939708c2..e3cc124ceb53 100644 --- a/.github/actions/test-llamacpp-build/action.yml +++ b/.github/actions/test-llamacpp-build/action.yml @@ -21,17 +21,17 @@ runs: with: name: ${{ inputs.artifact_name }} path: llama-binaries - + - name: Download test model (Windows) if: inputs.os_type == 'Windows' shell: powershell run: | $modelUrl = "https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_0.gguf?download=true" $modelPath = "Qwen3-0.6B-Q4_0.gguf" - + Write-Host "Downloading test model from: $modelUrl" curl.exe -L -o $modelPath $modelUrl - + # Verify download if (Test-Path $modelPath) { $fileSize = (Get-Item $modelPath).Length @@ -40,17 +40,17 @@ runs: Write-Error "Failed to download model" exit 1 } - + - name: Download test model (Linux) if: inputs.os_type == 'Linux' shell: bash run: | model_url="https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_0.gguf?download=true" model_path="Qwen3-0.6B-Q4_0.gguf" - + echo "Downloading test model from: $model_url" curl -L -o "$model_path" "$model_url" - + # Verify download if [ -f "$model_path" ]; then file_size=$(stat -c%s "$model_path") @@ -59,21 +59,21 @@ runs: echo "Failed to download model" exit 1 fi - + - name: Set up library path (Linux) if: inputs.os_type == 'Linux' shell: bash run: | # Add the llama-binaries directory to LD_LIBRARY_PATH echo "LD_LIBRARY_PATH=$(pwd)/llama-binaries:$LD_LIBRARY_PATH" >> $GITHUB_ENV - + - name: Run llama-cli test (Windows) if: inputs.os_type == 'Windows' shell: powershell run: | $llamaCliPath = "llama-binaries\llama-cli.exe" $modelPath = "Qwen3-0.6B-Q4_0.gguf" - + # Verify llama-cli exists if (-not (Test-Path $llamaCliPath)) { Write-Error "llama-cli.exe not found at: $llamaCliPath" @@ -81,21 +81,21 @@ runs: Get-ChildItem -Recurse "llama-binaries" exit 1 } - + Write-Host "Running llama-cli test for ${{ inputs.gfx_target }}..." Write-Host "Command: $llamaCliPath -m `"$modelPath`" -ngl 99 -p `"I believe the meaning of life is`" -st -v" - + # Run the command and capture output try { $process = Start-Process -FilePath $llamaCliPath -ArgumentList @("-m", "`"$modelPath`"", "-ngl", "99", "-p", "`"I believe the meaning of life is`"", "-st", "-v") -NoNewWindow -Wait -PassThru -RedirectStandardOutput "stdout.txt" -RedirectStandardError "stderr.txt" - + # Wait for the process to complete $process.WaitForExit() - + # Read both stdout and stderr $stdout = Get-Content "stdout.txt" -Raw -ErrorAction SilentlyContinue $stderr = Get-Content "stderr.txt" -Raw -ErrorAction SilentlyContinue - + # Always print the output (both stdout and stderr) Write-Host "=== LLAMA-CLI STDOUT ===" if ($stdout) { @@ -111,10 +111,10 @@ runs: } Write-Host "=== END OUTPUT ===" Write-Host "Process exit code: $($process.ExitCode)" - + # Combine output for pattern matching (stderr often contains important CUDA/GPU info) $output = $stdout + $stderr - + # Clean up temp files Remove-Item "stdout.txt" -ErrorAction SilentlyContinue Remove-Item "stderr.txt" -ErrorAction SilentlyContinue @@ -128,7 +128,7 @@ runs: Get-ChildItem "llama-binaries" | Format-Table Name, Length exit 1 } - + # Check if the process exited successfully if ($process.ExitCode -ne 0) { Write-Host "❌ llama-cli exited with error code: $($process.ExitCode)" @@ -140,22 +140,22 @@ runs: Write-Host "Please check the STDERR output above for specific error messages." exit 1 } - + # Check for expected outputs $foundGpuOffload = $output -match "offloaded 29/29 layers to GPU" $foundThinkTag = $output -match "</think>" - + Write-Host "=== TEST RESULTS ===" Write-Host "Expected 'offloaded 29/29 layers to GPU': $(if ($foundGpuOffload) { 'FOUND' } else { 'NOT FOUND' })" Write-Host "Expected '</think>' tag: $(if ($foundThinkTag) { 'FOUND' } else { 'NOT FOUND' })" - + if ($foundGpuOffload -and $foundThinkTag) { Write-Host "✅ Test PASSED - Both expected outputs found" } else { Write-Host "❌ Test FAILED - Missing expected outputs" exit 1 } - + - name: Run llama-cli test (Linux) if: inputs.os_type == 'Linux' shell: bash @@ -163,10 +163,10 @@ runs: llama_cli_path="./llama-binaries/llama-cli" model_path="Qwen3-0.6B-Q4_0.gguf" output_file="llama_output.txt" - + # Make llama-cli executable chmod +x "$llama_cli_path" - + # Verify llama-cli exists if [ ! -f "$llama_cli_path" ]; then echo "llama-cli not found at: $llama_cli_path" @@ -174,16 +174,16 @@ runs: find llama-binaries -type f exit 1 fi - + echo "Running llama-cli test for ${{ inputs.gfx_target }}..." echo "Command: $llama_cli_path -m \"$model_path\" -ngl 99 -p \"I believe the meaning of life is\" -st -v" - + # Run the command and capture output to file (more robust than variable) set +e # Don't exit on error, we want to capture the output and check exit code manually "$llama_cli_path" -m "$model_path" -ngl 99 -p "I believe the meaning of life is" -st -v > "$output_file" 2>&1 exit_code=$? set -e # Re-enable exit on error - + echo "=== LLAMA-CLI OUTPUT ===" if [ -f "$output_file" ] && [ -s "$output_file" ]; then cat "$output_file" @@ -192,7 +192,7 @@ runs: fi echo "=== END OUTPUT ===" echo "Process exit code: $exit_code" - + # Check if the process exited successfully if [ $exit_code -ne 0 ]; then echo "❌ llama-cli exited with error code: $exit_code" @@ -208,27 +208,27 @@ runs: rm -f "$output_file" exit 1 fi - + # Check for expected outputs using grep -F (fixed strings, no regex interpretation) # This handles special characters like angle brackets more reliably found_gpu_offload=false found_think_tag=false - + if grep -F "offloaded 29/29 layers to GPU" "$output_file" > /dev/null 2>&1; then found_gpu_offload=true fi - + if grep -F "</think>" "$output_file" > /dev/null 2>&1; then found_think_tag=true fi - + echo "=== TEST RESULTS ===" echo "Expected 'offloaded 29/29 layers to GPU': $(if [ "$found_gpu_offload" = true ]; then echo 'FOUND'; else echo 'NOT FOUND'; fi)" echo "Expected '</think>' tag: $(if [ "$found_think_tag" = true ]; then echo 'FOUND'; else echo 'NOT FOUND'; fi)" - + # Clean up output file rm -f "$output_file" - + if [ "$found_gpu_offload" = true ] && [ "$found_think_tag" = true ]; then echo "✅ Test PASSED - Both expected outputs found" else diff --git a/.github/workflows/build-llamacpp-rocm.yml b/.github/workflows/build-llamacpp-rocm.yml index cba4cfa1c5f4..ed08088fb9dd 100644 --- a/.github/workflows/build-llamacpp-rocm.yml +++ b/.github/workflows/build-llamacpp-rocm.yml @@ -1,7 +1,9 @@ # Nightly ROCm release builds of this fork, same layout as lemonade-sdk/llamacpp-rocm # (https://github.com/lemonade-sdk/llamacpp-rocm, MIT) so the zips drop into the same tooling. -# Differences: builds this repository instead of cloning upstream llama.cpp, uses the job token when no -# GH_TOKEN secret exists, and runs the self-hosted Strix Halo tests only when the runners are registered. +# Differences: builds this repository instead of cloning upstream llama.cpp, releases on every merge to +# master instead of nightly, builds Linux only by default (Windows still selectable by dispatch), uses the +# job token when no GH_TOKEN secret exists, and runs the self-hosted Strix Halo tests only when the runners +# are registered. name: Build Llama.cpp + ROCm on: workflow_dispatch: @@ -9,7 +11,7 @@ on: operating_systems: description: 'Operating systems to build for (comma-separated: windows,ubuntu)' required: false - default: 'windows,ubuntu' + default: 'ubuntu' gfx_target: description: 'AMD GPU targets (comma-separated)' required: false @@ -36,17 +38,22 @@ on: - '.github/actions/test-llamacpp-build/**' - 'scripts/rocm-release/**' - # Trigger on a schedule to build nightly releases - schedule: - # Runs at 1:00 PM UTC, which is 5:00 AM PST (UTC-8) - # This is two hours after the ROCm nightly tarball is triggered - - cron: '0 13 * * *' + # Release every merge to master + push: + branches: [master] + +# a newer merge supersedes a release that is still building +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true env: - OPERATING_SYSTEMS: ${{ github.event.inputs.operating_systems || 'windows,ubuntu' }} + OPERATING_SYSTEMS: ${{ github.event.inputs.operating_systems || 'ubuntu' }} GFX_TARGETS: ${{ github.event.inputs.gfx_target || 'gfx1151,gfx1150,gfx120X,gfx110X,gfx103X,gfx90a,gfx908' }} ROCM_VERSION: ${{ github.event.inputs.rocm_version || 'latest' }} LLAMACPP_VERSION: ${{ github.event.inputs.llamacpp_version || 'latest' }} + # on a push, build exactly the merged commit + BUILD_SHA: ${{ github.event_name == 'push' && github.sha || '' }} jobs: prepare-matrix: @@ -62,7 +69,7 @@ jobs: run: | targets="${{ env.GFX_TARGETS }}" operating_systems="${{ env.OPERATING_SYSTEMS }}" - + echo "Input targets: $targets" echo "Input operating systems: $operating_systems" @@ -78,20 +85,20 @@ jobs: # Check which operating systems to build should_build_windows="false" should_build_ubuntu="false" - + if [[ "$operating_systems" == *"windows"* ]]; then should_build_windows="true" echo "windows_matrix=$matrix_targets" >> $GITHUB_OUTPUT fi - + if [[ "$operating_systems" == *"ubuntu"* ]]; then should_build_ubuntu="true" echo "ubuntu_matrix=$matrix_targets" >> $GITHUB_OUTPUT fi - + echo "should_build_windows=$should_build_windows" >> $GITHUB_OUTPUT echo "should_build_ubuntu=$should_build_ubuntu" >> $GITHUB_OUTPUT - + echo "Windows build: $should_build_windows" echo "Ubuntu build: $should_build_ubuntu" echo "Generated matrix: $matrix_targets" @@ -114,7 +121,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Clean up existing directories (safety precaution) run: | # Remove existing llama.cpp directory if it exists @@ -122,19 +129,19 @@ jobs: Write-Host "Removing existing llama.cpp directory..." Remove-Item -Recurse -Force "llama.cpp" } - + # Remove existing C:\opt\rocm directory if it exists if (Test-Path "C:\opt\rocm") { Write-Host "Removing existing C:\opt\rocm directory..." Remove-Item -Recurse -Force "C:\opt\rocm" } - + # Remove any existing ROCm tarball if (Test-Path "rocm.tar.gz") { Write-Host "Removing existing rocm.tar.gz..." Remove-Item -Force "rocm.tar.gz" } - + Write-Host "Cleanup completed successfully" - name: Install Visual Studio Build Tools @@ -142,20 +149,20 @@ jobs: # Download and install Visual Studio Build Tools $vsInstallerUrl = "https://aka.ms/vs/17/release/vs_buildtools.exe" $vsInstallerPath = "$env:TEMP\vs_buildtools.exe" - + Write-Host "Downloading Visual Studio Build Tools..." Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath - + Write-Host "Installing Visual Studio Build Tools..." Start-Process -FilePath $vsInstallerPath -ArgumentList "--quiet", "--wait", "--norestart", "--add", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "--add", "Microsoft.VisualStudio.Component.VC.CMake.Project", "--add", "Microsoft.VisualStudio.Component.VC.ATL", "--add", "Microsoft.VisualStudio.Component.Windows11SDK.22621" -Wait - + # Clean up installer Remove-Item $vsInstallerPath -Force - name: Install build dependencies run: | Write-Host "Installing build dependencies using manual methods..." - + # Install Ninja Write-Host "Installing Ninja..." $ninjaUrl = "https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-win.zip" @@ -164,14 +171,14 @@ jobs: New-Item -ItemType Directory -Force -Path $ninjaDir Invoke-WebRequest -Uri $ninjaUrl -OutFile $ninjaPath Expand-Archive -Path $ninjaPath -DestinationPath $ninjaDir -Force - + # Install Strawberry Perl Write-Host "Installing Strawberry Perl..." $perlUrl = "http://strawberryperl.com/download/5.32.1.1/strawberry-perl-5.32.1.1-64bit.msi" $perlPath = "$env:TEMP\strawberry-perl-5.32.1.1-64bit.msi" Invoke-WebRequest -Uri $perlUrl -OutFile $perlPath Start-Process msiexec.exe -ArgumentList "/i $perlPath /quiet /norestart" -Wait - + # Verify installations $env:PATH = "C:\ninja;C:\Strawberry\perl\bin;$env:PATH" Write-Host "Verifying installations..." @@ -179,20 +186,20 @@ jobs: perl --version Write-Host "Manual installation of build dependencies completed" - + - name: Download ROCm nightly tarball run: | # Determine ROCm version to use $rocmVersion = "${{ env.ROCM_VERSION }}" $currentTarget = "${{ matrix.gfx_target }}" - + # Add appropriate suffixes for different GPU targets $s3Target = $currentTarget if ($currentTarget -eq "gfx103X" -or $currentTarget -eq "gfx110X" -or $currentTarget -eq "gfx120X") { $s3Target = "$currentTarget-all" Write-Host "Using target with -all suffix: $s3Target" } - + # TheRock now publishes nightlies to the ROCm core tarball index. That # index is a static HTML page with an embedded JSON `files` array (name + # mtime), so we parse that instead of the retired S3 XML bucket listing. @@ -240,21 +247,21 @@ jobs: # For specific versions, construct the URL directly $rocmUrl = "$baseUrl/therock-dist-windows-$s3Target-$rocmVersion.tar.gz" } - + # Store the version for use in other steps echo "DETECTED_ROCM_VERSION=$rocmVersion" >> $env:GITHUB_ENV - + Write-Host "Downloading ROCm from: $rocmUrl" Invoke-WebRequest -Uri $rocmUrl -OutFile "rocm.tar.gz" - + - name: Extract ROCm to C:\opt\rocm run: | # Create directory if it doesn't exist New-Item -ItemType Directory -Force -Path "C:\opt\rocm" - + # Extract the tarball tar -xzf rocm.tar.gz -C C:\opt\rocm --strip-components=1 - + - name: Clone llama.cpp run: | $llamacppVersion = "${{ env.LLAMACPP_VERSION }}" @@ -266,12 +273,13 @@ jobs: git clone --filter=blob:none --single-branch --branch $llamacppVersion https://github.com/${{ github.repository }}.git llama.cpp } cd llama.cpp - + if ("${{ env.BUILD_SHA }}" -ne "") { git checkout --detach "${{ env.BUILD_SHA }}" } + # Get commit hash (5 digits) and store it as environment variable $commitHash = git rev-parse --short=5 HEAD echo "LLAMACPP_COMMIT_HASH=$commitHash" >> $env:GITHUB_ENV Write-Host "llama.cpp commit hash (5 digits): $commitHash" - + # Show current commit info Write-Host "Current llama.cpp commit:" git log --oneline -1 @@ -283,7 +291,7 @@ jobs: REM Map GPU targets set "current_target=${{ matrix.gfx_target }}" echo Input target: %current_target% - + if "%current_target%"=="gfx110X" ( set "mapped_target=gfx1100;gfx1101;gfx1102;gfx1103" ) else if "%current_target%"=="gfx103X" ( @@ -336,23 +344,23 @@ jobs: REM Build the project cmake --build . -j %NUMBER_OF_PROCESSORS% - + - name: Copy ROCm core DLLs to build directory run: | $rocmVersion = if ($env:DETECTED_ROCM_VERSION) { $env:DETECTED_ROCM_VERSION } else { $env:ROCM_VERSION } $buildBinPath = "llama.cpp\build\bin" $rocmBinPath = "C:\opt\rocm\bin" - + Write-Host "Copying ROCm core DLL files to build directory..." Write-Host "Source: $rocmBinPath" Write-Host "Destination: $buildBinPath" - + if (Test-Path $rocmBinPath) { # Copy files matching patterns and specific names. $filesToCopy = @( "amdhip64_*.dll", "rocm_kpack.dll", - "amd_comgr*.dll", + "amd_comgr*.dll", "libhipblas.dll", "rocblas.dll", "rocsolver.dll", @@ -362,7 +370,7 @@ jobs: "origami.dll", "libtensilelite-host.dll" ) - + foreach ($pattern in $filesToCopy) { $matchingFiles = Get-ChildItem -Path $rocmBinPath -Name $pattern -ErrorAction SilentlyContinue if ($matchingFiles) { @@ -376,7 +384,7 @@ jobs: Write-Host "Warning: No files found matching pattern: $pattern" } } - + # Copy the rocblas\library folder and all its contents $rocblasLibPath = Join-Path $rocmBinPath "rocblas\library" if (Test-Path $rocblasLibPath) { @@ -384,7 +392,7 @@ jobs: $destRocblasPath = Join-Path $buildBinPath "rocblas\library" Copy-Item -Path $rocblasLibPath -Destination $destRocblasPath -Recurse -Force Write-Host "Copied: rocblas\library folder with all contents" - + # List the contents of the copied rocblas\library folder Write-Host "Contents of rocblas\library:" Get-ChildItem $destRocblasPath -Recurse | Select-Object FullName, Length | Format-Table -AutoSize @@ -406,19 +414,19 @@ jobs: } else { Write-Host "Warning: rocblas\library folder not found at: $rocblasLibPath" } - + Write-Host "ROCm core files successfully copied to build directory" } else { Write-Error "ROCm bin directory not found: $rocmBinPath" exit 1 } - + - name: List build artifacts (including ROCm files) run: | cd llama.cpp\build\bin Write-Host "Final build artifacts (including ROCm core files):" Get-ChildItem -Recurse | Format-Table Name, Length, LastWriteTime - + - name: Upload build artifacts id: upload-artifacts uses: actions/upload-artifact@v4 @@ -426,7 +434,7 @@ jobs: name: llama-windows-rocm-${{ matrix.gfx_target }}-x64 path: llama.cpp/build/bin/ retention-days: 30 - + - name: Set job outputs id: set-outputs run: | @@ -435,33 +443,33 @@ jobs: Write-Host "DETECTED_ROCM_VERSION: $env:DETECTED_ROCM_VERSION" Write-Host "ROCM_VERSION: $env:ROCM_VERSION" Write-Host "LLAMACPP_COMMIT_HASH: $env:LLAMACPP_COMMIT_HASH" - + $rocmVersion = if ($env:DETECTED_ROCM_VERSION) { $env:DETECTED_ROCM_VERSION } else { $env:ROCM_VERSION } echo "rocm_version=$rocmVersion" >> $env:GITHUB_OUTPUT echo "llamacpp_commit_hash=$env:LLAMACPP_COMMIT_HASH" >> $env:GITHUB_OUTPUT - + Write-Host "Final rocm_version: $rocmVersion" Write-Host "Final llamacpp_commit_hash: $env:LLAMACPP_COMMIT_HASH" - + - name: Create release summary run: | $summary = @" # Llama.cpp + ROCm Build Summary (Windows - ${{ matrix.gfx_target }}) - + ## Build Configuration - **GPU Target**: ${{ matrix.gfx_target }} - **ROCm Version**: ${{ env.DETECTED_ROCM_VERSION || env.ROCM_VERSION }} - **llama.cpp Commit Hash**: ${{ env.LLAMACPP_COMMIT_HASH }} - **Build Type**: Release - **Platform**: Windows - + ## Build Artifacts The compiled binaries are available in the build artifacts. "@ - + Set-Content -Path "build-summary-windows-${{ matrix.gfx_target }}.md" -Value $summary - + - name: Upload build summary id: upload-summary uses: actions/upload-artifact@v4 @@ -469,18 +477,18 @@ jobs: name: build-summary-windows-${{ matrix.gfx_target }} path: build-summary-windows-${{ matrix.gfx_target }}.md retention-days: 30 - + - name: Clean up build artifacts and temporary files if: always() run: | Write-Host "Starting cleanup process..." - + # Remove ROCm tarball if (Test-Path "rocm.tar.gz") { Write-Host "Removing rocm.tar.gz..." Remove-Item -Force "rocm.tar.gz" } - + # Remove ROCm installation directory if (Test-Path "C:\opt\rocm") { Write-Host "Removing C:\opt\rocm directory..." @@ -491,13 +499,13 @@ jobs: Write-Host "Removing llama.cpp source directory..." Remove-Item -Recurse -Force "llama.cpp" } - + # Clean up any temporary build files if (Test-Path "build-summary-windows-${{ matrix.gfx_target }}.md") { Write-Host "Removing build-summary-windows-${{ matrix.gfx_target }}.md..." Remove-Item -Force "build-summary-windows-${{ matrix.gfx_target }}.md" } - + Write-Host "Cleanup completed successfully" build-ubuntu: @@ -516,10 +524,10 @@ jobs: - name: Free disk space # Remove unused runner files to free up disk space run: curl -fsSL https://raw.githubusercontent.com/kou/arrow/e49d8ae15583ceff03237571569099a6ad62be32/ci/scripts/util_free_space.sh | bash - + - name: Checkout repository uses: actions/checkout@v4 - + - name: Clean up existing directories (safety precaution) run: | # Remove existing llama.cpp directory if it exists @@ -527,19 +535,19 @@ jobs: echo "Removing existing llama.cpp directory..." rm -rf llama.cpp fi - + # Remove existing /opt/rocm directory if it exists if [ -d "/opt/rocm" ]; then echo "Removing existing /opt/rocm directory..." sudo rm -rf /opt/rocm fi - + # Remove any existing ROCm tarball if [ -f "rocm.tar.gz" ]; then echo "Removing existing rocm.tar.gz..." rm -f rocm.tar.gz fi - + echo "Cleanup completed successfully" - name: Install build dependencies @@ -547,26 +555,26 @@ jobs: echo "Installing build dependencies..." sudo apt update sudo apt install -y cmake ninja-build unzip curl - + # Verify installations echo "Verifying installations..." cmake --version ninja --version echo "Build dependencies installation completed" - + - name: Download and extract ROCm directly to /opt/rocm run: | # Determine ROCm version to use rocm_version="${{ env.ROCM_VERSION }}" current_target="${{ matrix.gfx_target }}" - + # Add appropriate suffixes for different GPU targets s3_target="$current_target" if [[ "$current_target" = "gfx103X" || "$current_target" = "gfx110X" || "$current_target" = "gfx120X" ]]; then s3_target="${current_target}-all" echo "Using target with -all suffix: $s3_target" fi - + # TheRock now publishes nightlies to the ROCm core tarball index. That # index is a static HTML page with an embedded JSON `files` array (name + # mtime), so we parse that instead of the retired S3 XML bucket listing. @@ -608,22 +616,22 @@ jobs: # For specific versions, construct the URL directly rocm_url="$base_url/therock-dist-linux-${s3_target}-${rocm_version}.tar.gz" fi - + # Store the version for use in other steps echo "DETECTED_ROCM_VERSION=$rocm_version" >> $GITHUB_ENV - + echo "Streaming ROCm from: $rocm_url directly to extraction" - + # Create directory if it doesn't exist sudo mkdir -p /opt/rocm - + # Stream download directly into tar extraction (no intermediate file) curl -sL "$rocm_url" | sudo tar --use-compress-program=gzip -xf - -C /opt/rocm --strip-components=1 - + - name: Set ROCm environment variables run: | echo "Setting ROCm environment variables..." - + # Set environment variables for this step and subsequent steps echo "HIP_PATH=/opt/rocm" >> $GITHUB_ENV echo "ROCM_PATH=/opt/rocm" >> $GITHUB_ENV @@ -632,19 +640,19 @@ jobs: echo "HIP_INCLUDE_PATH=/opt/rocm/include" >> $GITHUB_ENV echo "HIP_LIB_PATH=/opt/rocm/lib" >> $GITHUB_ENV echo "HIP_DEVICE_LIB_PATH=/opt/rocm/lib/llvm/amdgcn/bitcode" >> $GITHUB_ENV - + # Update PATH echo "/opt/rocm/bin:/opt/rocm/llvm/bin:$PATH" >> $GITHUB_PATH - + # Set library paths echo "LD_LIBRARY_PATH=/opt/rocm/lib:/opt/rocm/lib64:/opt/rocm/llvm/lib:${LD_LIBRARY_PATH:-}" >> $GITHUB_ENV echo "LIBRARY_PATH=/opt/rocm/lib:/opt/rocm/lib64:${LIBRARY_PATH:-}" >> $GITHUB_ENV echo "CPATH=/opt/rocm/include:${CPATH:-}" >> $GITHUB_ENV echo "PKG_CONFIG_PATH=/opt/rocm/lib/pkgconfig:${PKG_CONFIG_PATH:-}" >> $GITHUB_ENV - + echo "ROCm environment variables set successfully" - + - name: Clone llama.cpp run: | llamacpp_version="${{ env.LLAMACPP_VERSION }}" @@ -656,23 +664,24 @@ jobs: git clone --filter=blob:none --single-branch --branch "$llamacpp_version" https://github.com/${{ github.repository }}.git llama.cpp fi cd llama.cpp - + if [ -n "${{ env.BUILD_SHA }}" ]; then git checkout --detach "${{ env.BUILD_SHA }}"; fi + # Get commit hash (5 digits) and store it as environment variable commit_hash=$(git rev-parse --short=5 HEAD) echo "LLAMACPP_COMMIT_HASH=$commit_hash" >> $GITHUB_ENV echo "llama.cpp commit hash (5 digits): $commit_hash" - + # Show current commit info echo "Current llama.cpp commit:" git log --oneline -1 - + - name: Build Llama.cpp + ROCm run: | # Map GPU targets current_target="${{ matrix.gfx_target }}" echo "Input target: $current_target" - + if [ "$current_target" = "gfx110X" ]; then mapped_target="gfx1100;gfx1101;gfx1102;gfx1103" elif [ "$current_target" = "gfx103X" ]; then @@ -715,12 +724,12 @@ jobs: # Build the project cmake --build . -j $(nproc) - + - name: Copy ROCm core libs to build directory run: | build_bin_path="llama.cpp/build/bin" rocm_bin_path="/opt/rocm/bin" - + # Copy the rocblas/library folder and all its contents rocblas_lib_path="/opt/rocm/lib/rocblas/library" if [ -d "$rocblas_lib_path" ]; then @@ -729,7 +738,7 @@ jobs: mkdir -p "$(dirname "$dest_rocblas_path")" cp -r "$rocblas_lib_path" "$(dirname "$dest_rocblas_path")/" echo "Copied: rocblas/library folder with all contents" - + # List the contents of the copied rocblas/library folder echo "Contents of rocblas/library:" find "$dest_rocblas_path" -type f -exec ls -la {} \; | head -20 @@ -752,7 +761,7 @@ jobs: else echo "Warning: hipblaslt/library folder not found at: $hipblaslt_lib_path" fi - + # Copy required ROCm libraries to build directory # If artifacts from ROCm or Llama.cpp change, you may need to update this list # To get a new list of all libraries, run: @@ -779,7 +788,7 @@ jobs: cp -v /opt/rocm/lib/llvm/lib/libLLVM.so* "$build_bin_path/" 2>/dev/null || echo "libLLVM.so* not found" cp -v /opt/rocm/lib/llvm/lib/libclang-cpp.so* "$build_bin_path/" 2>/dev/null || echo "libclang-cpp.so* not found" cp -v /opt/rocm/lib/llvm/lib/libomp.so* "$build_bin_path/" 2>/dev/null || echo "libomp.so* not found" - + cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_elf.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_elf.so* not found" cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_drm.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_drm.so* not found" cp -v /opt/rocm/lib/rocm_sysdeps/lib/librocm_sysdeps_drm_amdgpu.so* "$build_bin_path/" 2>/dev/null || echo "librocm_sysdeps_drm_amdgpu.so* not found" @@ -801,7 +810,7 @@ jobs: cd llama.cpp/build/bin echo "Final build artifacts (including ROCm library files):" ls -la - + - name: Upload build artifacts id: upload-artifacts uses: actions/upload-artifact@v4 @@ -809,7 +818,7 @@ jobs: name: llama-ubuntu-rocm-${{ matrix.gfx_target }}-x64 path: llama.cpp/build/bin/ retention-days: 30 - + - name: Set job outputs id: set-outputs run: | @@ -818,30 +827,30 @@ jobs: echo "DETECTED_ROCM_VERSION: ${DETECTED_ROCM_VERSION}" echo "ROCM_VERSION: ${{ env.ROCM_VERSION }}" echo "LLAMACPP_COMMIT_HASH: ${LLAMACPP_COMMIT_HASH}" - + rocm_version="${DETECTED_ROCM_VERSION:-${{ env.ROCM_VERSION }}}" echo "rocm_version=$rocm_version" >> $GITHUB_OUTPUT echo "llamacpp_commit_hash=${LLAMACPP_COMMIT_HASH}" >> $GITHUB_OUTPUT - + echo "Final rocm_version: $rocm_version" echo "Final llamacpp_commit_hash: ${LLAMACPP_COMMIT_HASH}" - + - name: Create release summary run: | cat > "build-summary-ubuntu-${{ matrix.gfx_target }}.md" << 'EOF' # Llama.cpp + ROCm Build Summary (Ubuntu - ${{ matrix.gfx_target }}) - + ## Build Configuration - **GPU Target**: ${{ matrix.gfx_target }} - **ROCm Version**: ${{ env.DETECTED_ROCM_VERSION || env.ROCM_VERSION }} - **llama.cpp Commit Hash**: ${{ env.LLAMACPP_COMMIT_HASH }} - **Build Type**: Release - **Platform**: Ubuntu - + ## Build Artifacts The compiled binaries are available in the build artifacts. EOF - + - name: Upload build summary id: upload-summary uses: actions/upload-artifact@v4 @@ -849,36 +858,36 @@ jobs: name: build-summary-ubuntu-${{ matrix.gfx_target }} path: build-summary-ubuntu-${{ matrix.gfx_target }}.md retention-days: 30 - + - name: Clean up build artifacts and temporary files if: always() run: | echo "Starting cleanup process..." - + # Remove ROCm tarball if [ -f "rocm.tar.gz" ]; then echo "Removing rocm.tar.gz..." rm -f rocm.tar.gz fi - + # Remove ROCm installation directory if [ -d "/opt/rocm" ]; then echo "Removing /opt/rocm directory..." sudo rm -rf /opt/rocm fi - + # Remove llama.cpp source directory (keeping only the build artifacts) if [ -d "llama.cpp" ]; then echo "Removing llama.cpp source directory..." rm -rf llama.cpp fi - + # Clean up any temporary build files if [ -f "build-summary-ubuntu-${{ matrix.gfx_target }}.md" ]; then echo "Removing build-summary-ubuntu-${{ matrix.gfx_target }}.md..." rm -f "build-summary-ubuntu-${{ matrix.gfx_target }}.md" fi - + echo "Cleanup completed successfully" test-stx-halo: @@ -904,7 +913,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Test llama.cpp build uses: ./.github/actions/test-llamacpp-build with: @@ -931,7 +940,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Test llama.cpp build uses: ./.github/actions/test-llamacpp-build with: @@ -953,19 +962,19 @@ jobs: (needs.test-stx-halo.result == 'success' || needs.test-stx-halo.result == 'skipped') && (needs.test-stx.result == 'success' || needs.test-stx.result == 'skipped') && github.event_name != 'pull_request' && - (github.event_name == 'workflow_dispatch' && + (github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == null) || - github.event_name == 'schedule') - + github.event_name == 'push') + steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Download all build artifacts uses: actions/download-artifact@v4 with: path: ./all-artifacts - + - name: Generate release tag id: generate-tag env: @@ -973,7 +982,7 @@ jobs: run: | # Get all existing release tags that match the pattern b#### existing_tags=$(gh release list --limit 1000 --json tagName --jq '.[].tagName' | grep -E '^b[0-9]{4}$' | sort -V || echo "") - + if [ -z "$existing_tags" ]; then # No existing sequential tags found, start with b1000 next_number=1000 @@ -986,18 +995,18 @@ jobs: echo "Highest existing tag: $highest_tag (number: $highest_number)" echo "Next number will be: $next_number" fi - + # Format as b#### (4-digit number with leading zeros) TAG=$(printf "b%04d" $next_number) - + echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "Generated release tag: ${TAG}" - + - name: Check if tag already exists id: check-tag run: | TAG="${{ steps.generate-tag.outputs.tag }}" - + # Check if the tag already exists if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then echo "Tag $TAG already exists, skipping release creation" @@ -1006,7 +1015,7 @@ jobs: echo "Tag $TAG does not exist, proceeding with release creation" echo "tag_exists=false" >> $GITHUB_OUTPUT fi - + - name: Create archives for all target artifacts if: steps.check-tag.outputs.tag_exists == 'false' run: | @@ -1014,29 +1023,29 @@ jobs: targets="${{ env.GFX_TARGETS }}" operating_systems="${{ env.OPERATING_SYSTEMS }}" TAG="${{ steps.generate-tag.outputs.tag }}" - + echo "Processing targets: $targets" echo "Processing operating systems: $operating_systems" echo "Using release tag: $TAG" - + # Create individual archives for each target and OS combination IFS=',' read -ra TARGET_ARRAY <<< "$targets" IFS=',' read -ra OS_ARRAY <<< "$operating_systems" - + for os in "${OS_ARRAY[@]}"; do os=$(echo "$os" | xargs) # trim whitespace for target in "${TARGET_ARRAY[@]}"; do target=$(echo "$target" | xargs) # trim whitespace - + echo "Processing OS: $os, target: $target" - + # Use artifact name to find the directory artifact_name="llama-${os}-rocm-${target}-x64" artifact_dir="./all-artifacts/${artifact_name}" - + # Create final archive with release tag final_archive_name="llama-${TAG}-${os}-rocm-${target}-x64" - + if [ -d "$artifact_dir" ]; then echo "Creating archive: ${final_archive_name}.zip" cd "$artifact_dir" @@ -1048,10 +1057,10 @@ jobs: fi done done - + echo "Created archives:" ls -la *.zip - + - name: Create Release if: steps.check-tag.outputs.tag_exists == 'false' env: @@ -1064,35 +1073,35 @@ jobs: LLAMACPP_COMMIT_HASH="${{ needs.build-windows.outputs.llamacpp_commit_hash || needs.build-ubuntu.outputs.llamacpp_commit_hash }}" targets="${{ env.GFX_TARGETS }}" operating_systems="${{ env.OPERATING_SYSTEMS }}" - + echo "Creating release with tag: $TAG" echo "GPU Targets: $targets" echo "Operating Systems: $operating_systems" echo "ROCm Version: $ROCM_VERSION" echo "Llama.cpp Commit: $LLAMACPP_COMMIT_HASH" - + # Verify archives exist ls -la *.zip - + # Prepare upload files list upload_files="" IFS=',' read -ra TARGET_ARRAY <<< "$targets" IFS=',' read -ra OS_ARRAY <<< "$operating_systems" - + for os in "${OS_ARRAY[@]}"; do os=$(echo "$os" | xargs) # trim whitespace for target in "${TARGET_ARRAY[@]}"; do target=$(echo "$target" | xargs) # trim whitespace - + final_archive_name="llama-${TAG}-${os}-rocm-${target}-x64" if [ -f "${final_archive_name}.zip" ]; then upload_files="${upload_files} ${final_archive_name}.zip" fi done done - + echo "Files to upload: $upload_files" - + # Create release with GitHub CLI gh release create "$TAG" \ --title "$TAG" \ diff --git a/.github/workflows/nightly-failure-alert.yml b/.github/workflows/nightly-failure-alert.yml index 3b4ab66736a3..e26dc76a3ed3 100644 --- a/.github/workflows/nightly-failure-alert.yml +++ b/.github/workflows/nightly-failure-alert.yml @@ -13,7 +13,7 @@ env: jobs: alert: if: | - github.event.workflow_run.event == 'schedule' && + github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion != 'success' runs-on: ubuntu-22.04 permissions: @@ -37,7 +37,7 @@ jobs: assignees=$(echo $ALERT_USERS | tr ' ' ',') body=$(cat <<EOF - $mentions the nightly llama.cpp + ROCm build **${CONCLUSION}** (started ${RUN_DATE}). No release was published. + $mentions the master llama.cpp + ROCm release build **${CONCLUSION}** (started ${RUN_DATE}). No release was published. **Run:** $RUN_URL @@ -57,10 +57,10 @@ jobs: else echo "Opening new issue" gh issue create \ - --title "Nightly build failed ($(date -u -d "$RUN_DATE" '+%Y-%m-%d'))" \ + --title "Release build failed ($(date -u -d "$RUN_DATE" '+%Y-%m-%d'))" \ --label "$ALERT_LABEL" \ --assignee "$assignees" \ --body "$body - Close this issue once the nightly is fixed. Further failures while it is open are added as comments." + Close this issue once the release build is fixed. Further failures while it is open are added as comments." fi diff --git a/.github/workflows/test-llamacpp-rocm.yml b/.github/workflows/test-llamacpp-rocm.yml index f8b303277e68..8f0f2662cdd9 100644 --- a/.github/workflows/test-llamacpp-rocm.yml +++ b/.github/workflows/test-llamacpp-rocm.yml @@ -40,7 +40,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | release_input="${{ env.RELEASE_TAG }}" - + if [ "$release_input" = "latest" ]; then # Get the latest release tag using GitHub REST API release_tag=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ @@ -51,27 +51,27 @@ jobs: # Use the specified release tag release_tag="$release_input" echo "Using specified release: $release_tag" - + # Verify the release exists using GitHub REST API response_code=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/repos/${{ github.repository }}/releases/tags/$release_tag") - + if [ "$response_code" != "200" ]; then echo "Error: Release '$release_tag' does not exist (HTTP $response_code)" exit 1 fi fi - + echo "latest_release_tag=$release_tag" >> $GITHUB_OUTPUT echo "Final release tag: $release_tag" - + - name: Set matrix id: set-matrix run: | targets="${{ env.GFX_TARGETS }}" operating_systems="${{ env.OPERATING_SYSTEMS }}" - + echo "Input targets: $targets" echo "Input operating systems: $operating_systems" # Convert targets to JSON array @@ -85,20 +85,20 @@ jobs: # Check which operating systems to test should_test_windows="false" should_test_ubuntu="false" - + if [[ "$operating_systems" == *"windows"* ]]; then should_test_windows="true" echo "windows_matrix=$matrix_targets" >> $GITHUB_OUTPUT fi - + if [[ "$operating_systems" == *"ubuntu"* ]]; then should_test_ubuntu="true" echo "ubuntu_matrix=$matrix_targets" >> $GITHUB_OUTPUT fi - + echo "should_test_windows=$should_test_windows" >> $GITHUB_OUTPUT echo "should_test_ubuntu=$should_test_ubuntu" >> $GITHUB_OUTPUT - + echo "Windows test: $should_test_windows" echo "Ubuntu test: $should_test_ubuntu" echo "Generated matrix: $matrix_targets" @@ -122,25 +122,25 @@ jobs: $target = "${{ matrix.gfx_target }}" $assetName = "llama-${releaseTag}-windows-rocm-${target}-x64.zip" $repo = "${{ github.repository }}" - + Write-Host "Downloading release asset: $assetName" Write-Host "From release: $releaseTag" - + # Get release information using GitHub REST API $headers = @{ "Authorization" = "token $env:GITHUB_TOKEN" } $releaseUrl = "https://api.github.com/repos/$repo/releases/tags/$releaseTag" - + try { $release = Invoke-RestMethod -Uri $releaseUrl -Headers $headers $asset = $release.assets | Where-Object { $_.name -eq $assetName } - + if (-not $asset) { Write-Error "Asset '$assetName' not found in release '$releaseTag'" exit 1 } - + Write-Host "Found asset: $($asset.name) (Size: $($asset.size) bytes)" - + # Download the specific asset Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $assetName -Headers $headers Write-Host "Downloaded $assetName successfully" @@ -149,23 +149,23 @@ jobs: Write-Error "Failed to download release asset: $_" exit 1 } - + # Extract the downloaded zip file Write-Host "Extracting $assetName..." Expand-Archive -Path $assetName -DestinationPath "llama-binaries" -Force - + # List contents Write-Host "Contents of llama-binaries:" Get-ChildItem -Recurse "llama-binaries" | Format-Table Name, Length - + - name: Download test model run: | $modelUrl = "https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_0.gguf?download=true" $modelPath = "Qwen3-0.6B-Q4_0.gguf" - + Write-Host "Downloading test model from: $modelUrl" curl.exe -L -o $modelPath $modelUrl - + # Verify download if (Test-Path $modelPath) { $fileSize = (Get-Item $modelPath).Length @@ -174,12 +174,12 @@ jobs: Write-Error "Failed to download model" exit 1 } - + - name: Run llama-cli test run: | $llamaCliPath = "llama-binaries\llama-cli.exe" $modelPath = "Qwen3-0.6B-Q4_0.gguf" - + # Verify llama-cli exists if (-not (Test-Path $llamaCliPath)) { Write-Error "llama-cli.exe not found at: $llamaCliPath" @@ -187,53 +187,53 @@ jobs: Get-ChildItem -Recurse "llama-binaries" exit 1 } - + Write-Host "Running llama-cli test..." Write-Host "Command: $llamaCliPath -m `"$modelPath`" -ngl 99 -p `"I believe the meaning of life is`" -st" - + # Run the command and capture output try { $process = Start-Process -FilePath $llamaCliPath -ArgumentList @("-m", "`"$modelPath`"", "-ngl", "99", "-p", "`"I believe the meaning of life is`"", "-st") -NoNewWindow -Wait -PassThru -RedirectStandardOutput "stdout.txt" -RedirectStandardError "stderr.txt" - + # Wait for the process to complete $process.WaitForExit() - + # Read both stdout and stderr $stdout = Get-Content "stdout.txt" -Raw -ErrorAction SilentlyContinue $stderr = Get-Content "stderr.txt" -Raw -ErrorAction SilentlyContinue - + # Combine output (stderr often contains important CUDA/GPU info) $output = $stdout + $stderr - + # Clean up temp files Remove-Item "stdout.txt" -ErrorAction SilentlyContinue Remove-Item "stderr.txt" -ErrorAction SilentlyContinue - + Write-Host "Process exit code: $($process.ExitCode)" } catch { Write-Error "Failed to run llama-cli: $_" exit 1 } - + Write-Host "=== LLAMA-CLI OUTPUT ===" Write-Host $output Write-Host "=== END OUTPUT ===" - + # Check if the process exited successfully if ($process.ExitCode -ne 0) { Write-Host "❌ llama-cli exited with error code: $($process.ExitCode)" exit 1 } - + # Check for expected outputs $foundGpuOffload = $output -match "offloaded 29/29 layers to GPU" $foundThinkTag = $output -match "</think>" - + Write-Host "=== TEST RESULTS ===" Write-Host "Expected 'offloaded 29/29 layers to GPU': $(if ($foundGpuOffload) { 'FOUND' } else { 'NOT FOUND' })" Write-Host "Expected '</think>' tag: $(if ($foundThinkTag) { 'FOUND' } else { 'NOT FOUND' })" - + if ($foundGpuOffload -and $foundThinkTag) { Write-Host "✅ Test PASSED - Both expected outputs found" } else { @@ -266,7 +266,7 @@ jobs: else echo "jq is already installed" fi - + - name: Download release artifacts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -275,60 +275,60 @@ jobs: target="${{ matrix.gfx_target }}" asset_name="llama-${release_tag}-ubuntu-rocm-${target}-x64.zip" repo="${{ github.repository }}" - + echo "Downloading release asset: $asset_name" echo "From release: $release_tag" - + # Get release information using GitHub REST API release_url="https://api.github.com/repos/$repo/releases/tags/$release_tag" - + # Get release data and extract asset download URL release_data=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "$release_url") - + if [ $? -ne 0 ]; then echo "Error: Failed to fetch release information" exit 1 fi - + # Extract the browser_download_url for the specific asset download_url=$(echo "$release_data" | jq -r ".assets[] | select(.name == \"$asset_name\") | .browser_download_url") - + if [ "$download_url" = "null" ] || [ -z "$download_url" ]; then echo "Error: Asset '$asset_name' not found in release '$release_tag'" echo "Available assets:" echo "$release_data" | jq -r '.assets[].name' exit 1 fi - + asset_size=$(echo "$release_data" | jq -r ".assets[] | select(.name == \"$asset_name\") | .size") echo "Found asset: $asset_name (Size: $asset_size bytes)" - + # Download the specific asset curl -L -H "Authorization: token $GITHUB_TOKEN" -o "$asset_name" "$download_url" - + if [ $? -eq 0 ]; then echo "Downloaded $asset_name successfully" else echo "Error: Failed to download $asset_name" exit 1 fi - + # Extract the downloaded zip file echo "Extracting $asset_name..." unzip -q "$asset_name" -d llama-binaries - + # List contents echo "Contents of llama-binaries:" find llama-binaries -type f -exec ls -la {} \; - + - name: Download test model run: | model_url="https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_0.gguf?download=true" model_path="Qwen3-0.6B-Q4_0.gguf" - + echo "Downloading test model from: $model_url" curl -L -o "$model_path" "$model_url" - + # Verify download if [ -f "$model_path" ]; then file_size=$(stat -c%s "$model_path") @@ -337,20 +337,20 @@ jobs: echo "Failed to download model" exit 1 fi - + - name: Set up library path run: | # Add the llama-binaries directory to LD_LIBRARY_PATH echo "LD_LIBRARY_PATH=$(pwd)/llama-binaries:$LD_LIBRARY_PATH" >> $GITHUB_ENV - + - name: Run llama-cli test run: | llama_cli_path="./llama-binaries/llama-cli" model_path="Qwen3-0.6B-Q4_0.gguf" - + # Make llama-cli executable chmod +x "$llama_cli_path" - + # Verify llama-cli exists if [ ! -f "$llama_cli_path" ]; then echo "llama-cli not found at: $llama_cli_path" @@ -358,37 +358,37 @@ jobs: find llama-binaries -type f exit 1 fi - + echo "Running llama-cli test..." echo "Command: $llama_cli_path -m \"$model_path\" -ngl 99 -p \"I believe the meaning of life is\" -st" - + # Run the command and capture output output=$("$llama_cli_path" -m "$model_path" -ngl 99 -p "I believe the meaning of life is" -st 2>&1) - + echo "=== LLAMA-CLI OUTPUT ===" echo "$output" echo "=== END OUTPUT ===" - + # Check for expected outputs if echo "$output" | grep -q "offloaded 29/29 layers to GPU"; then found_gpu_offload=true else found_gpu_offload=false fi - + if echo "$output" | grep -q "</think>"; then found_think_tag=true else found_think_tag=false fi - + echo "=== TEST RESULTS ===" echo "Expected 'offloaded 29/29 layers to GPU': $(if [ "$found_gpu_offload" = true ]; then echo 'FOUND'; else echo 'NOT FOUND'; fi)" echo "Expected '</think>' tag: $(if [ "$found_think_tag" = true ]; then echo 'FOUND'; else echo 'NOT FOUND'; fi)" - + if [ "$found_gpu_offload" = true ] && [ "$found_think_tag" = true ]; then echo "✅ Test PASSED - Both expected outputs found" else echo "❌ Test FAILED - Missing expected outputs" exit 1 - fi \ No newline at end of file + fi diff --git a/docs/rocm-releases.md b/docs/rocm-releases.md index a12c55f82284..6b9807452772 100644 --- a/docs/rocm-releases.md +++ b/docs/rocm-releases.md @@ -6,7 +6,8 @@ artifact layout, so tools that consume their releases can consume these ones too ## What a release contains -- One zip per OS and GPU target: `llama-bNNNN-{windows,ubuntu}-rocm-{target}-x64.zip`. +- One zip per GPU target: `llama-bNNNN-ubuntu-rocm-{target}-x64.zip`. Windows zips + (`llama-bNNNN-windows-rocm-{target}-x64.zip`) are built only on a manual run that asks for them. - Targets: `gfx1151`, `gfx1150`, `gfx120X`, `gfx110X`, `gfx103X`, `gfx90a`, `gfx908`. - Each zip holds the llama.cpp binaries and shared libraries, built with `GGML_HIP=ON`, `GGML_RPC=ON` and `BUILD_SHARED_LIBS=ON`. @@ -19,12 +20,13 @@ artifact layout, so tools that consume their releases can consume these ones too ## When it runs -- Nightly at 13:00 UTC, two hours after TheRock publishes its nightly. This builds `master` and publishes a release. -- On demand with `workflow_dispatch`. Inputs: OS list, targets, ROCm version, branch or tag, and whether to - publish. +- On every merge to `master`. This builds the merged commit and publishes a release. A newer merge cancels a + release that is still building. +- On demand with `workflow_dispatch`. Inputs: OS list (`ubuntu` by default, `windows,ubuntu` for both), targets, + ROCm version, branch or tag, and whether to publish. - On pull requests that change the pipeline itself. These runs build but do not publish. -If a nightly run fails, `nightly-failure-alert.yml` opens or updates an issue labelled `nightly-failure`. +If a release build for a merge fails, `nightly-failure-alert.yml` opens or updates an issue labelled `nightly-failure`. ## Hardware tests From ea79e754bf32d43f0da749a063de728e08284746 Mon Sep 17 00:00:00 2001 From: Daniele Zannotti <d.zannotti@me.com> Date: Tue, 29 Sep 2026 20:13:27 +0100 Subject: [PATCH 206/212] ci : add a Linux Vulkan zip to each release build-ubuntu-vulkan builds the same commit with GGML_VULKAN=ON, portable CPU variants and $ORIGIN RPATH, and create-release adds llama-bNNNN-ubuntu-vulkan-x64.zip to the release. The vulkan dispatch input turns it off. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .github/workflows/build-llamacpp-rocm.yml | 75 ++++++++++++++++++++++- docs/rocm-releases.md | 8 ++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-llamacpp-rocm.yml b/.github/workflows/build-llamacpp-rocm.yml index ed08088fb9dd..4606bb8e0db9 100644 --- a/.github/workflows/build-llamacpp-rocm.yml +++ b/.github/workflows/build-llamacpp-rocm.yml @@ -24,6 +24,11 @@ on: description: 'Branch or tag of this repository to build, or "latest" for master' required: false default: 'latest' + vulkan: + description: 'Also build the Linux Vulkan zip' + required: false + default: true + type: boolean create_release: description: 'Create a GitHub release after successful build' required: false @@ -890,6 +895,61 @@ jobs: echo "Cleanup completed successfully" + build-ubuntu-vulkan: + # Linux Vulkan build of the same commit, packaged next to the ROCm zips in the same release. + # Portable: CPU variants and backends are loaded at runtime, RPATH is $ORIGIN, and the system + # Vulkan loader and driver (Mesa RADV) are used. + runs-on: ubuntu-22.04 + if: github.event.inputs.vulkan != 'false' + outputs: + llamacpp_commit_hash: ${{ steps.set-outputs.outputs.llamacpp_commit_hash }} + + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + ref: ${{ env.BUILD_SHA != '' && env.BUILD_SHA || (env.LLAMACPP_VERSION == 'latest' && 'master' || env.LLAMACPP_VERSION) }} + path: llama.cpp + + - name: Install build dependencies + run: | + wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - + sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-jammy.list https://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list + sudo apt-get update -y + sudo apt-get install -y build-essential cmake ninja-build vulkan-sdk zip + + - name: Build Llama.cpp + Vulkan + run: | + cd llama.cpp + echo "LLAMACPP_COMMIT_HASH=$(git rev-parse --short=5 HEAD)" >> $GITHUB_ENV + git log --oneline -1 + cmake -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_RPATH='$ORIGIN' \ + -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \ + -DBUILD_SHARED_LIBS=ON \ + -DLLAMA_BUILD_TESTS=OFF \ + -DGGML_VULKAN=ON \ + -DGGML_RPC=ON \ + -DGGML_NATIVE=OFF \ + -DGGML_BACKEND_DL=ON \ + -DGGML_CPU_ALL_VARIANTS=ON \ + -DLLAMA_BUILD_BORINGSSL=ON + cmake --build build -j $(nproc) + cp LICENSE build/bin/ + ls -la build/bin + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: llama-ubuntu-vulkan-x64 + path: llama.cpp/build/bin/ + retention-days: 30 + + - name: Set job outputs + id: set-outputs + run: echo "llamacpp_commit_hash=${LLAMACPP_COMMIT_HASH}" >> $GITHUB_OUTPUT + test-stx-halo: runs-on: ${{ matrix.runner }} needs: [prepare-matrix, build-windows, build-ubuntu] @@ -949,7 +1009,7 @@ jobs: artifact_name: llama-${{ matrix.artifact_prefix }}-rocm-${{ matrix.gfx_target }}-x64 create-release: - needs: [prepare-matrix, build-windows, build-ubuntu, test-stx-halo, test-stx] + needs: [prepare-matrix, build-windows, build-ubuntu, build-ubuntu-vulkan, test-stx-halo, test-stx] runs-on: ubuntu-22.04 permissions: contents: write @@ -958,6 +1018,7 @@ jobs: always() && (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped') && (needs.build-ubuntu.result == 'success' || needs.build-ubuntu.result == 'skipped') && + (needs.build-ubuntu-vulkan.result == 'success' || needs.build-ubuntu-vulkan.result == 'skipped') && (needs.build-windows.result == 'success' || needs.build-ubuntu.result == 'success') && (needs.test-stx-halo.result == 'success' || needs.test-stx-halo.result == 'skipped') && (needs.test-stx.result == 'success' || needs.test-stx.result == 'skipped') && @@ -1058,6 +1119,11 @@ jobs: done done + if [ -d "./all-artifacts/llama-ubuntu-vulkan-x64" ]; then + echo "Creating archive: llama-${TAG}-ubuntu-vulkan-x64.zip" + (cd ./all-artifacts/llama-ubuntu-vulkan-x64 && zip -r "../../llama-${TAG}-ubuntu-vulkan-x64.zip" *) + fi + echo "Created archives:" ls -la *.zip @@ -1100,6 +1166,12 @@ jobs: done done + vulkan_line="" + if [ -f "llama-${TAG}-ubuntu-vulkan-x64.zip" ]; then + upload_files="${upload_files} llama-${TAG}-ubuntu-vulkan-x64.zip" + vulkan_line="**Vulkan**: ubuntu x64" + fi + echo "Files to upload: $upload_files" # Create release with GitHub CLI @@ -1109,6 +1181,7 @@ jobs: **Operating System(s)**: $operating_systems **GPU Target(s)**: $targets **ROCm Version**: $ROCM_VERSION + $vulkan_line **Llama.cpp Commit Hash**: $LLAMACPP_COMMIT_HASH **Build Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC') diff --git a/docs/rocm-releases.md b/docs/rocm-releases.md index 6b9807452772..d26230a1848c 100644 --- a/docs/rocm-releases.md +++ b/docs/rocm-releases.md @@ -1,4 +1,4 @@ -# ROCm release builds +# ROCm and Vulkan release builds `.github/workflows/build-llamacpp-rocm.yml` publishes prebuilt ROCm binaries of this fork. It copies the pipeline of [lemonade-sdk/llamacpp-rocm](https://github.com/lemonade-sdk/llamacpp-rocm) (MIT) and uses the same @@ -9,7 +9,11 @@ artifact layout, so tools that consume their releases can consume these ones too - One zip per GPU target: `llama-bNNNN-ubuntu-rocm-{target}-x64.zip`. Windows zips (`llama-bNNNN-windows-rocm-{target}-x64.zip`) are built only on a manual run that asks for them. - Targets: `gfx1151`, `gfx1150`, `gfx120X`, `gfx110X`, `gfx103X`, `gfx90a`, `gfx908`. -- Each zip holds the llama.cpp binaries and shared libraries, built with `GGML_HIP=ON`, `GGML_RPC=ON` and +- One Linux Vulkan zip per release: `llama-bNNNN-ubuntu-vulkan-x64.zip`. It is built from the same commit with + `GGML_VULKAN=ON`, loads the CPU variants and backends at runtime (`GGML_BACKEND_DL`, + `GGML_CPU_ALL_VARIANTS`), sets RPATH to `$ORIGIN`, and uses the system Vulkan loader and driver + (Mesa RADV on Strix Halo). A manual run can turn it off with the `vulkan` input. +- Each ROCm zip holds the llama.cpp binaries and shared libraries, built with `GGML_HIP=ON`, `GGML_RPC=ON` and `BUILD_SHARED_LIBS=ON`. - Each zip also holds the ROCm runtime libraries it needs (hipBLAS, rocBLAS, hipBLASLt and their kernel libraries). On Linux, RPATH is set to `$ORIGIN`, so the zip runs without a system ROCm install. From 8388e3f841f5cff73d87169cbc37d3c81980a95f Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:47:44 +0200 Subject: [PATCH 207/212] hip: optimize Qwen3.8-Flash-Next prefill on Strix Halo (#91) --- ggml/src/ggml-cuda/gated_delta_net.cu | 667 +++++++++++++++++- ggml/src/ggml-cuda/gated_delta_net.cuh | 16 + ggml/src/ggml-cuda/ggml-cuda.cu | 530 +++++++++++++- ggml/src/ggml-cuda/idx-relu-sum.cu | 15 +- ggml/src/ggml-cuda/idx-relu-sum.cuh | 6 + ggml/src/ggml-cuda/mmb-quant.cuh | 38 +- ggml/src/ggml-cuda/mmb.cu | 537 ++++++++++++-- ggml/src/ggml-cuda/mmb.cuh | 7 + ggml/src/ggml-cuda/moe-weighted-reduction.cu | 96 +++ ggml/src/ggml-cuda/moe-weighted-reduction.cuh | 9 + ggml/src/ggml-cuda/qsa-prefill.cu | 137 +++- ggml/src/ggml-cuda/top-k.cu | 191 +++++ ggml/src/ggml-cuda/unary.cu | 35 + ggml/src/ggml-cuda/unary.cuh | 4 + src/llama-ple-disk.cpp | 132 +++- src/models/qwen4exp.cpp | 84 ++- tests/test-backend-ops.cpp | 393 ++++++++++- 17 files changed, 2780 insertions(+), 117 deletions(-) diff --git a/ggml/src/ggml-cuda/gated_delta_net.cu b/ggml/src/ggml-cuda/gated_delta_net.cu index ff2ac52745ea..00069ec2df2f 100644 --- a/ggml/src/ggml-cuda/gated_delta_net.cu +++ b/ggml/src/ggml-cuda/gated_delta_net.cu @@ -2,6 +2,9 @@ #include "ggml-cuda/common.cuh" #include "unary.cuh" +#include <mutex> +#include <unordered_map> + // Warp reduction without the LDS crossbar: on RDNA the generic warp_reduce_sum compiles to 5 dependent // ds_bpermute round trips (~100+ cycles each), which dominates the per-token latency of the recurrence. @@ -499,6 +502,604 @@ gated_delta_net_tiled_cuda(const float * q, } } + +// --------------------------------------------------------------------------------------------- +// Chunked Gated DeltaNet prefill (RDNA3.5, S_v = 128, non-KDA, final state only, n_seqs == 1). +// Per chunk of CL tokens (G = in-chunk cumsum of g, D[r][s] = exp(G_r - G_s)): +// A[r][s] = beta_r (k_r.k_s) D[r][s] (s < r), T' = (I + A)^-1 diag(beta) +// U = T' (V - diag(e^G) K S0) (all delta-rule corrections, one product) +// O = scale diag(e^G) Q S0 + P U, P[r][s] = scale (q_r.k_s) D[r][s] (s <= r) +// S1 = e^{G_L} S0 + K^T diag(e^{G_L - G}) U +// gdn_chunk_prep_kernel: fully parallel, one block per (chunk, k-head) for its H/Hk value heads. Produces the chunk's +// q/k (optionally through the fused causal conv + SiLU and the fused L2 norm) and v as fp16 rows, KK^T / QK^T on +// WMMA, T by a barrier-free wave-level forward substitution, and writes T', P and the gate factors. +// gdn_chunk_scan_kernel: one block (8 waves) per value head, sequential over chunks; wave w owns 16 state columns +// in fp32 WMMA accumulators. The chunk's q/k/T'/P are staged once per block in LDS (K^T transposed on the way in) +// and the next chunk is prefetched into registers. Optionally applies the gated RMS norm of the output (fused). +// WMMA operands are fp16 with fp32 accumulation: not bit-identical to the sequential kernels (NMSE ~2e-7). +// GGML_GDN_CHUNK=0 disables the path (and every fusion that depends on it), GGML_GDN_CHUNK_MIN sets the token threshold. +#if defined(GGML_USE_HIP) && defined(RDNA3_5) +#define GDNC_ENABLED 1 +#endif +#ifndef GDNC_SCAN_WAVES +#define GDNC_SCAN_WAVES 6 +#endif +typedef _Float16 gdnc_v16h __attribute__((ext_vector_type(16))); +typedef float gdnc_v8f __attribute__((ext_vector_type(8))); + +#if defined(GDNC_ENABLED) +#define GDNC_WMMA(a, b, c) __builtin_amdgcn_wmma_f32_16x16x16_f16_w32((a), (b), (c)) +// A/B operand fragment from a row-major fp16 matrix: lane holds row (lane & 15), 16 consecutive k +static __device__ __forceinline__ gdnc_v16h gdnc_frag(const _Float16 * base, const int stride, const int row0, const int k0, const int lane) { + const _Float16 * p = base + (row0 + (lane & 15)) * stride + k0; + gdnc_v16h f; + const uint4 a = *(const uint4 *) p, b = *(const uint4 *) (p + 8); + __builtin_memcpy(&f, &a, 16); __builtin_memcpy(((char *) &f) + 16, &b, 16); + return f; +} +// same, from 16 consecutive fp32 values +static __device__ __forceinline__ gdnc_v16h gdnc_frag_f32(const float * p) { + gdnc_v16h f; +#pragma unroll + for (int j = 0; j < 4; ++j) { + const float4 x = ((const float4 *) p)[j]; + f[4 * j + 0] = (_Float16) x.x; f[4 * j + 1] = (_Float16) x.y; + f[4 * j + 2] = (_Float16) x.z; f[4 * j + 3] = (_Float16) x.w; + } + return f; +} +// accumulator tile (lane: column lane&15, rows 2e + (lane>>4)) -> B fragment (lane: column lane&15, rows 0..15) +static __device__ __forceinline__ gdnc_v16h gdnc_acc_to_frag(const gdnc_v8f & acc, const int lane) { + gdnc_v16h f; + const bool hi = lane >= 16; +#pragma unroll + for (int e = 0; e < 8; ++e) { + const float own = acc[e]; + const float oth = __int_as_float(__builtin_amdgcn_permlanex16(__float_as_int(own), __float_as_int(own), 0x76543210, 0xFEDCBA98, true, false)); + f[2 * e] = (_Float16) (hi ? oth : own); + f[2 * e + 1] = (_Float16) (hi ? own : oth); + } + return f; +} +#endif + +// inputs of the chunk kernels: the q/k/v views of the GDN op, or (fused conv) the raw conv input x [T, C] with the +// conv history [C, 3] and taps [C, 4]: q/k/v element (t, c) is then silu(sum_j w[c][j] * in(t - 3 + j, c)), the +// ssm_conv_f32 + SiLU arithmetic of gdn_conv_direct_kernel +struct gdnc_in { + const float * q; const float * k; const float * v; + int64_t sq1, sq2, sv1, sv2; + const float * x; const float * st; const float * cw; // x == nullptr: read the views + int64_t C, qc, kc, vc; // x row stride, channel of element 0 of the q / k / v views +}; + +#if defined(GDNC_ENABLED) +// channels c..c+3 of token t through the causal conv + SiLU; wt[ch][j] = tap j of channel c + ch +static __device__ __forceinline__ float4 gdnc_conv4(const gdnc_in & in, const float (&wt)[4][4], const int64_t t, const int64_t c) { + float s[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; +#pragma unroll + for (int j = 0; j < 4; ++j) { + const int64_t u = t - 3 + j; + float xv[4]; + if (u >= 0) { + const float4 v4 = *(const float4 *) (in.x + u * in.C + c); + xv[0] = v4.x; xv[1] = v4.y; xv[2] = v4.z; xv[3] = v4.w; + } else { +#pragma unroll + for (int ch = 0; ch < 4; ++ch) xv[ch] = in.st[(c + ch) * 3 + (u + 3)]; + } +#pragma unroll + for (int ch = 0; ch < 4; ++ch) s[ch] = fmaf(xv[ch], wt[ch][j], s[ch]); + } + float4 o; + o.x = ggml_cuda_op_silu_single(s[0] + 0.0f); o.y = ggml_cuda_op_silu_single(s[1] + 0.0f); + o.z = ggml_cuda_op_silu_single(s[2] + 0.0f); o.w = ggml_cuda_op_silu_single(s[3] + 0.0f); + return o; +} +static __device__ __forceinline__ void gdnc_load_taps(const gdnc_in & in, const int64_t c, float (&wt)[4][4]) { +#pragma unroll + for (int ch = 0; ch < 4; ++ch) { + const float4 w4 = *(const float4 *) (in.cw + (c + ch) * 4); + wt[ch][0] = w4.x; wt[ch][1] = w4.y; wt[ch][2] = w4.z; wt[ch][3] = w4.w; + } +} +static __device__ __forceinline__ uint2 gdnc_pack4h(const float4 v) { + const _Float16 h[4] = { (_Float16) v.x, (_Float16) v.y, (_Float16) v.z, (_Float16) v.w }; + uint2 u; + __builtin_memcpy(&u, h, 8); + return u; +} +#endif + +// K1: one block per (chunk, k-head), handling the H/Hk value heads that share it. Produces the (normalized) q/k and v +// of the chunk as fp16 rows (Qh/Kh [Hk, T, 128], Vh [H, T, 128]) for the scan, plus T', P and the gate factors. +template <int CL> +__global__ void __launch_bounds__(256) +gdn_chunk_prep_kernel(const gdnc_in in, const float * __restrict__ g, + const float * __restrict__ beta, _Float16 * __restrict__ Tb, _Float16 * __restrict__ Pb, float2 * __restrict__ Gb, + _Float16 * __restrict__ Qh, _Float16 * __restrict__ Kh, _Float16 * __restrict__ Vh, + const int H, const int Hk, const int n_tokens, const int nch, + const int64_t sb1, const int64_t sb2, const float scale, + const float qk_eps, const float qk_mul) { +#if defined(GDNC_ENABLED) + constexpr int SK = 136; + constexpr int NT = CL / 16; + constexpr int NTRI = NT * (NT + 1) / 2; + constexpr int MR = CL / 32; + constexpr int CPW = CL / 8; + constexpr int NQK = CL * 32 / 256; + constexpr int RMAX = 4; + __shared__ __align__(16) _Float16 Ks[CL * SK]; + __shared__ __align__(16) _Float16 Qs[CL * SK]; + __shared__ float KK[CL][CL + 1]; + __shared__ float QK[CL][CL + 1]; + __shared__ float A[CL][CL + 1]; + __shared__ float Gs[RMAX][CL], bs[RMAX][CL]; + const int hk = blockIdx.x, ch = blockIdx.y, tid = threadIdx.x, lane = tid & 31, wave = tid >> 5; + const int cl = lane & 15, rh = lane >> 4; + const int R = H / Hk; + const int t0 = ch * CL, Lc = min(CL, n_tokens - t0); + const int i4 = (tid & 31) * 4; // this thread's 4 channels of every row it handles (row = (idx >> 5)) + + { + float4 kv[NQK], qv[NQK]; + if (in.x) { + float wq[4][4], wk[4][4]; + gdnc_load_taps(in, in.qc + (int64_t) hk * in.sq1 + i4, wq); + gdnc_load_taps(in, in.kc + (int64_t) hk * in.sq1 + i4, wk); +#pragma unroll + for (int j = 0; j < NQK; ++j) { + const int r = (tid + 256 * j) >> 5; + const int64_t t = t0 + min(r, Lc - 1); + qv[j] = gdnc_conv4(in, wq, t, in.qc + (int64_t) hk * in.sq1 + i4); + kv[j] = gdnc_conv4(in, wk, t, in.kc + (int64_t) hk * in.sq1 + i4); + } + } else { +#pragma unroll + for (int j = 0; j < NQK; ++j) { + const int r = (tid + 256 * j) >> 5; + const int64_t o = (int64_t) (t0 + min(r, Lc - 1)) * in.sq2 + (int64_t) hk * in.sq1 + i4; + kv[j] = *(const float4 *) (in.k + o); + qv[j] = *(const float4 *) (in.q + o); + } + } + if (qk_eps >= 0.0f) { + // fused q/k L2 norm: a row's 128 values are the float4s of one wave (row = idx >> 5) +#pragma unroll + for (int j = 0; j < NQK; ++j) { + const float sk = gdn_warp_reduce_sum<32>(kv[j].x * kv[j].x + kv[j].y * kv[j].y + kv[j].z * kv[j].z + kv[j].w * kv[j].w); + const float sq = gdn_warp_reduce_sum<32>(qv[j].x * qv[j].x + qv[j].y * qv[j].y + qv[j].z * qv[j].z + qv[j].w * qv[j].w); + const float fk = qk_mul * rsqrtf(sk + qk_eps), fq = qk_mul * rsqrtf(sq + qk_eps); + kv[j].x *= fk; kv[j].y *= fk; kv[j].z *= fk; kv[j].w *= fk; + qv[j].x *= fq; qv[j].y *= fq; qv[j].z *= fq; qv[j].w *= fq; + } + } +#pragma unroll + for (int j = 0; j < NQK; ++j) { + const int r = (tid + 256 * j) >> 5; + const bool ok = r < Lc; + const float4 z4 = make_float4(0.0f, 0.0f, 0.0f, 0.0f); + const uint2 kh = gdnc_pack4h(ok ? kv[j] : z4), qh = gdnc_pack4h(ok ? qv[j] : z4); + *(uint2 *) (Ks + r * SK + i4) = kh; + *(uint2 *) (Qs + r * SK + i4) = qh; + if (ok) { + const int64_t o = ((int64_t) hk * n_tokens + t0 + r) * 128 + i4; + *(uint2 *) (Kh + o) = kh; + *(uint2 *) (Qh + o) = qh; + } + } + // v of the R value heads of this k-head -> Vh (fp16) + for (int jh = 0; jh < R; ++jh) { + const int h = hk + jh * Hk; + float wv[4][4]; + if (in.x) gdnc_load_taps(in, in.vc + (int64_t) h * in.sv1 + i4, wv); +#pragma unroll + for (int j = 0; j < NQK; ++j) { + const int r = (tid + 256 * j) >> 5; + if (r >= Lc) continue; + const int64_t t = t0 + r; + const float4 vv = in.x ? gdnc_conv4(in, wv, t, in.vc + (int64_t) h * in.sv1 + i4) + : *(const float4 *) (in.v + t * in.sv2 + (int64_t) h * in.sv1 + i4); + *(uint2 *) (Vh + ((int64_t) h * n_tokens + t) * 128 + i4) = gdnc_pack4h(vv); + } + } + } + for (int idx = tid; idx < R * CL; idx += 256) { + const int j = idx / CL, r = idx % CL, h = hk + j * Hk; + const int64_t o = (int64_t) (t0 + r) * sb2 + (int64_t) h * sb1; + Gs[j][r] = r < Lc ? g[o] : 0.0f; + bs[j][r] = r < Lc ? beta[o] : 0.0f; + } + __syncthreads(); + if (tid < R) { + float gv[CL]; +#pragma unroll + for (int r = 0; r < CL; ++r) gv[r] = Gs[tid][r]; +#pragma unroll + for (int r = 1; r < CL; ++r) gv[r] += gv[r - 1]; +#pragma unroll + for (int r = 0; r < CL; ++r) Gs[tid][r] = gv[r]; + } + // raw lower-triangular tiles of K K^T and Q K^T (shared by all value heads of this k-head) + for (int job = wave; job < 2 * NTRI; job += 8) { + const bool isq = job >= NTRI; + int jj = isq ? job - NTRI : job, rt = 0; + while (jj >= rt + 1) { jj -= rt + 1; ++rt; } + const int st = jj; + const _Float16 * As = isq ? Qs : Ks; + gdnc_v8f acc = {}; +#pragma unroll + for (int kk = 0; kk < 128; kk += 16) { + acc = GDNC_WMMA(gdnc_frag(As, SK, rt * 16, kk, lane), gdnc_frag(Ks, SK, st * 16, kk, lane), acc); + } + float (*D)[CL + 1] = isq ? QK : KK; +#pragma unroll + for (int e = 0; e < 8; ++e) D[rt * 16 + 2 * e + rh][st * 16 + cl] = acc[e]; + } + __syncthreads(); + + for (int j = 0; j < R; ++j) { + const int h = hk + j * Hk; + _Float16 * Pp = Pb + ((int64_t) h * nch + ch) * CL * CL; + for (int idx = tid; idx < CL * CL; idx += 256) { + const int r = idx / CL, s = idx % CL; + const float d = s <= r ? expf(Gs[j][r] - Gs[j][s]) : 0.0f; + A[r][s] = s < r ? bs[j][r] * KK[r][s] * d : 0.0f; + Pp[idx] = (_Float16) (s <= r ? scale * QK[r][s] * d : 0.0f); + } + if (tid < CL) { + Gb[((int64_t) h * nch + ch) * CL + tid] = make_float2(expf(Gs[j][tid]), expf(Gs[j][CL - 1] - Gs[j][tid])); + } + __syncthreads(); + // T = (I + A)^-1 column by column, barrier-free: lane owns rows lane + 32 m, the wave CPW columns + { + const int j0 = wave * CPW; + float x[MR][CPW]; +#pragma unroll + for (int m = 0; m < MR; ++m) +#pragma unroll + for (int c = 0; c < CPW; ++c) x[m][c] = (lane + 32 * m == j0 + c) ? 1.0f : 0.0f; +#pragma unroll + for (int qq = 0; qq < CL - 1; ++qq) { + if (qq < j0) continue; + float a[MR]; +#pragma unroll + for (int m = 0; m < MR; ++m) a[m] = A[lane + 32 * m][qq]; +#pragma unroll + for (int c = 0; c < CPW; ++c) { + const float xq = __int_as_float(__builtin_amdgcn_readlane(__float_as_int(x[qq / 32][c]), qq % 32)); +#pragma unroll + for (int m = 0; m < MR; ++m) x[m][c] = fmaf(-a[m], xq, x[m][c]); + } + } + _Float16 * Tp = Tb + ((int64_t) h * nch + ch) * CL * CL; +#pragma unroll + for (int m = 0; m < MR; ++m) { + const int r = lane + 32 * m; + _Float16 tmp[CPW]; +#pragma unroll + for (int c = 0; c < CPW; ++c) tmp[c] = (_Float16) (x[m][c] * bs[j][j0 + c]); + __builtin_memcpy(Tp + r * CL + j0, tmp, sizeof(tmp)); + } + } + __syncthreads(); + } +#else + NO_DEVICE_CODE; +#endif +} + +// K2: one block (8 waves) per value head, wave w owns state columns [16 w, 16 w + 16). The chunk's q/k/T'/P are +// staged once per block in LDS (K^T transposed on the way in); the next chunk is prefetched into registers. +template <int CL> +__global__ void __launch_bounds__(256, GDNC_SCAN_WAVES) +gdn_chunk_scan_kernel(const _Float16 * __restrict__ Qh, const _Float16 * __restrict__ Kh, const _Float16 * __restrict__ Vh, + const float * __restrict__ curr_state, float * __restrict__ dst, float * __restrict__ state, + const _Float16 * __restrict__ Tb, const _Float16 * __restrict__ Pb, const float2 * __restrict__ Gb, + const int H, const int Hk, const int n_tokens, const int nch, const float scale, + const float * __restrict__ on_w, const float * __restrict__ on_z, float * __restrict__ on_out, uint16_t * __restrict__ on_out16, + const float on_eps) { +#if defined(GDNC_ENABLED) + constexpr int NT = CL / 16; + constexpr int SQ = 136; + constexpr int ST = CL + 8; + constexpr int NQK = CL * 32 / 256; // 4-half groups of q (and of k) per thread per chunk + constexpr int NTP = 2 * CL * CL / 8 / 256; // uint4 of T'/P per thread per chunk + static_assert(NTP * 256 == 2 * CL * CL / 8, "T'/P staging must divide evenly"); + __shared__ __align__(16) _Float16 Qs[CL * SQ]; + __shared__ __align__(16) _Float16 Ks[CL * SQ]; + __shared__ __align__(16) _Float16 KTs[128 * ST]; + __shared__ __align__(16) _Float16 Ts[CL * ST]; + __shared__ __align__(16) _Float16 Ps[CL * ST]; + __shared__ float2 Gs[CL]; + __shared__ float red[CL][9]; // fused gated norm: per-row partial sums of squares of the 8 waves + const int h = blockIdx.x, tid = threadIdx.x, lane = tid & 31, wave = tid >> 5; + const int hk = h % Hk; + const int cl = lane & 15, rh = lane >> 4, col = wave * 16 + cl; + + const float * s_in = curr_state + (int64_t) h * 128 * 128; + gdnc_v8f S[8]; +#pragma unroll + for (int it = 0; it < 8; ++it) +#pragma unroll + for (int e = 0; e < 8; ++e) S[it][e] = s_in[col * 128 + it * 16 + 2 * e + rh]; + + const _Float16 * kh = Kh + (int64_t) hk * n_tokens * 128; // prep's fp16 rows: normalized q / k, v + const _Float16 * qh = Qh + (int64_t) hk * n_tokens * 128; + const _Float16 * vh = Vh + (int64_t) h * n_tokens * 128 + col; + + uint2 pk[NQK], pq[NQK]; + uint4 ptp[NTP]; + float2 pg = make_float2(0.0f, 0.0f); + float pv[NT][8]; + float pz[NT][8]; // fused output norm: the gate z of this lane's rows / column, prefetched with v + const float * zh = on_z ? on_z + (int64_t) h * 128 + col : nullptr; + auto prefetch = [&](const int ch) { + const int t0 = ch * CL; +#pragma unroll + for (int j = 0; j < NQK; ++j) { + const int idx = tid + 256 * j, r = idx >> 5, i = (idx & 31) * 4; + const int64_t o = (int64_t) min(t0 + r, n_tokens - 1) * 128 + i; + pk[j] = *(const uint2 *) (kh + o); + pq[j] = *(const uint2 *) (qh + o); + } + const _Float16 * Tp = Tb + ((int64_t) h * nch + ch) * CL * CL; + const _Float16 * Pp = Pb + ((int64_t) h * nch + ch) * CL * CL; +#pragma unroll + for (int j = 0; j < NTP; ++j) { + // 2 CL^2 / 8 is a multiple of 256 for CL = 32 / 64: no guard (a divergent guard forces vmcnt(0) at the join) + const int idx = tid + 256 * j; + const int w = idx % (CL * CL / 8); + ptp[j] = *(const uint4 *) ((idx < CL * CL / 8 ? Tp : Pp) + w * 8); + } + pg = Gb[((int64_t) h * nch + ch) * CL + (tid % CL)]; + }; + // v / z of a chunk: issued right after the previous chunk consumed its own (one register copy each) + auto prefetch_v = [&](const int ch) { + const int t0 = ch * CL; +#pragma unroll + for (int rt = 0; rt < NT; ++rt) +#pragma unroll + for (int e = 0; e < 8; ++e) pv[rt][e] = (float) vh[(int64_t) min(t0 + rt * 16 + 2 * e + rh, n_tokens - 1) * 128]; + }; + auto prefetch_z = [&](const int ch) { + const int t0 = ch * CL; +#pragma unroll + for (int rt = 0; rt < NT; ++rt) +#pragma unroll + for (int e = 0; e < 8; ++e) pz[rt][e] = zh[(int64_t) min(t0 + rt * 16 + 2 * e + rh, n_tokens - 1) * H * 128]; + }; + prefetch(0); + prefetch_v(0); + if (zh) prefetch_z(0); + + for (int ch = 0; ch < nch; ++ch) { + const int t0 = ch * CL; + __syncthreads(); +#pragma unroll + for (int j = 0; j < NQK; ++j) { + const int idx = tid + 256 * j, r = idx >> 5, i = (idx & 31) * 4; + *(uint2 *) (Ks + r * SQ + i) = pk[j]; + *(uint2 *) (Qs + r * SQ + i) = pq[j]; + _Float16 kk4[4]; + __builtin_memcpy(kk4, &pk[j], 8); + KTs[(i + 0) * ST + r] = kk4[0]; KTs[(i + 1) * ST + r] = kk4[1]; + KTs[(i + 2) * ST + r] = kk4[2]; KTs[(i + 3) * ST + r] = kk4[3]; + } +#pragma unroll + for (int j = 0; j < NTP; ++j) { + const int idx = tid + 256 * j; + const int w = idx % (CL * CL / 8), r = w / (CL / 8), c8 = (w % (CL / 8)) * 8; + *(uint4 *) ((idx < CL * CL / 8 ? Ts : Ps) + r * ST + c8) = ptp[j]; + } + if (tid < CL) Gs[tid] = pg; + __syncthreads(); + // unconditional (clamped): a branch here makes the compiler drain the prefetch before the compute + prefetch(min(ch + 1, nch - 1)); + + // K S0, Q S0 + gdnc_v8f ks[NT], qs[NT]; +#pragma unroll + for (int rt = 0; rt < NT; ++rt) { ks[rt] = gdnc_v8f{}; qs[rt] = gdnc_v8f{}; } +#pragma unroll + for (int it = 0; it < 8; ++it) { + const gdnc_v16h sb = gdnc_acc_to_frag(S[it], lane); +#pragma unroll + for (int rt = 0; rt < NT; ++rt) { + ks[rt] = GDNC_WMMA(gdnc_frag(Ks, SQ, rt * 16, it * 16, lane), sb, ks[rt]); + qs[rt] = GDNC_WMMA(gdnc_frag(Qs, SQ, rt * 16, it * 16, lane), sb, qs[rt]); + } + } + // U = T' (V - diag(e^G) K S0) + gdnc_v16h YB[NT]; +#pragma unroll + for (int rt = 0; rt < NT; ++rt) { + gdnc_v8f y; +#pragma unroll + for (int e = 0; e < 8; ++e) y[e] = fmaf(-Gs[rt * 16 + 2 * e + rh].x, ks[rt][e], pv[rt][e]); + YB[rt] = gdnc_acc_to_frag(y, lane); + } + prefetch_v(min(ch + 1, nch - 1)); + gdnc_v8f U[NT]; + gdnc_v16h UB[NT]; +#pragma unroll + for (int rt = 0; rt < NT; ++rt) { + gdnc_v8f u = {}; +#pragma unroll + for (int st = 0; st <= rt; ++st) u = GDNC_WMMA(gdnc_frag(Ts, ST, rt * 16, st * 16, lane), YB[st], u); + U[rt] = u; + UB[rt] = gdnc_acc_to_frag(u, lane); + } + // S1 = e^{G_L} S0 + K^T diag(e^{G_L - G}) U + gdnc_v16h UB2[NT]; +#pragma unroll + for (int st = 0; st < NT; ++st) { + gdnc_v8f u; +#pragma unroll + for (int e = 0; e < 8; ++e) u[e] = U[st][e] * Gs[st * 16 + 2 * e + rh].y; + UB2[st] = gdnc_acc_to_frag(u, lane); + } + const float eGL = Gs[CL - 1].x; +#pragma unroll + for (int it = 0; it < 8; ++it) { + gdnc_v8f acc; +#pragma unroll + for (int e = 0; e < 8; ++e) acc[e] = S[it][e] * eGL; +#pragma unroll + for (int st = 0; st < NT; ++st) acc = GDNC_WMMA(gdnc_frag(KTs, ST, it * 16, st * 16, lane), UB2[st], acc); + S[it] = acc; + } + // O = scale diag(e^G) Q S0 + P U + gdnc_v8f O[NT]; +#pragma unroll + for (int rt = 0; rt < NT; ++rt) { + gdnc_v8f o; +#pragma unroll + for (int e = 0; e < 8; ++e) o[e] = qs[rt][e] * (Gs[rt * 16 + 2 * e + rh].x * scale); +#pragma unroll + for (int st = 0; st <= rt; ++st) o = GDNC_WMMA(gdnc_frag(Ps, ST, rt * 16, st * 16, lane), UB[st], o); + O[rt] = o; + } + if (on_z == nullptr) { +#pragma unroll + for (int rt = 0; rt < NT; ++rt) +#pragma unroll + for (int e = 0; e < 8; ++e) { + const int t = t0 + rt * 16 + 2 * e + rh; + if (t < n_tokens) dst[((int64_t) t * H + h) * 128 + col] = O[rt][e]; + } + } else { + // fused gated RMS norm (rms_rows_f32<true>): v = rsqrt(mean(o^2) + eps) * o * w * sigmoid(z) +#pragma unroll + for (int rt = 0; rt < NT; ++rt) +#pragma unroll + for (int e = 0; e < 8; ++e) { + float p = O[rt][e] * O[rt][e]; + p += __shfl_xor(p, 1, 32); p += __shfl_xor(p, 2, 32); p += __shfl_xor(p, 4, 32); p += __shfl_xor(p, 8, 32); + if (cl == 0) red[rt * 16 + 2 * e + rh][wave] = p; + } + __syncthreads(); + const float wc = on_w[col]; +#pragma unroll + for (int rt = 0; rt < NT; ++rt) +#pragma unroll + for (int e = 0; e < 8; ++e) { + const int r = rt * 16 + 2 * e + rh, t = t0 + r; + float ss = 0.0f; +#pragma unroll + for (int w8 = 0; w8 < 8; ++w8) ss += red[r][w8]; + if (t < n_tokens) { + const int64_t oi = ((int64_t) t * H + h) * 128 + col; + const float sc = rsqrtf(ss / 128.0f + on_eps); + const float v = (sc * O[rt][e] * wc) * (1.0f / (1.0f + expf(-pz[rt][e]))); + if (on_out) on_out[oi] = v; + if (on_out16) on_out16[oi] = ggml_cuda_f32_to_bf16_rne(v); + } + } + prefetch_z(min(ch + 1, nch - 1)); + } + } + + float * s_out = state + (int64_t) h * 128 * 128; +#pragma unroll + for (int it = 0; it < 8; ++it) +#pragma unroll + for (int e = 0; e < 8; ++e) s_out[col * 128 + it * 16 + 2 * e + rh] = S[it][e]; +#else + NO_DEVICE_CODE; +#endif +} + +static bool gdn_chunk_enabled() { + static const int v = getenv("GGML_GDN_CHUNK") ? atoi(getenv("GGML_GDN_CHUNK")) : 1; + return v != 0; +} + +static int gdn_chunk_min_tokens() { + static const int v = getenv("GGML_GDN_CHUNK_MIN") ? atoi(getenv("GGML_GDN_CHUNK_MIN")) : 256; + return v; +} + +// the dispatch condition of the chunked prefill path (single source of truth for the op and the q/k-norm fusion) +static bool gdn_chunk_eligible_raw(const int cc, const bool kda, const bool keep_rs, const int64_t S_v, const int64_t H, + const int64_t Hk, const int64_t n_tokens, const int64_t n_seqs, const int64_t sq1, const int64_t sq2, + const void * q_d, const void * k_d) { +#if defined(GGML_USE_HIP) + const bool aligned = sq1 % 4 == 0 && sq2 % 4 == 0 && ((uintptr_t) q_d) % 16 == 0 && ((uintptr_t) k_d) % 16 == 0; + return !kda && !keep_rs && GGML_CUDA_CC_IS_RDNA3_5(cc) && S_v == 128 && n_seqs == 1 && n_tokens >= gdn_chunk_min_tokens() && + aligned && Hk > 0 && H % Hk == 0 && H / Hk <= 4 && gdn_chunk_enabled(); +#else + GGML_UNUSED(cc); GGML_UNUSED(kda); GGML_UNUSED(keep_rs); GGML_UNUSED(S_v); GGML_UNUSED(H); GGML_UNUSED(Hk); + GGML_UNUSED(n_tokens); GGML_UNUSED(n_seqs); GGML_UNUSED(sq1); GGML_UNUSED(sq2); GGML_UNUSED(q_d); GGML_UNUSED(k_d); + return false; +#endif +} + +bool ggml_cuda_gdn_chunk_eligible(const ggml_tensor * gdn, const ggml_tensor * q, const ggml_tensor * k) { + const ggml_tensor * v = gdn->src[2]; + if (gdn->op != GGML_OP_GATED_DELTA_NET || q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F32 || + !ggml_are_same_shape(q, k) || !ggml_are_same_stride(q, k) || !ggml_is_contiguous_rows(q) || q->ne[1] != gdn->src[0]->ne[1]) { + return false; + } + const int cc = ggml_cuda_info().devices[ggml_cuda_get_device()].cc; + const bool kda = gdn->src[3]->ne[0] == v->ne[0]; + const bool keep_rs = ggml_get_op_params_i32(gdn, 0) > 1; + return gdn_chunk_eligible_raw(cc, kda, keep_rs, v->ne[0], v->ne[1], q->ne[1], v->ne[2], v->ne[3], + (int64_t) (q->nb[1] / sizeof(float)), (int64_t) (q->nb[2] / sizeof(float)), q->data, k->data); +} + +struct gdn_qk_norm_ovr { + const ggml_tensor * q; + const ggml_tensor * k; + float eps; + float mul; +}; +static std::mutex g_gdn_qk_mtx; +static std::unordered_map<const ggml_tensor *, gdn_qk_norm_ovr> g_gdn_qk; + +struct gdn_conv_ovr { + const float * x; + const float * st; + const float * cw; + int64_t C, qc, kc, vc; +}; +static std::unordered_map<const ggml_tensor *, gdn_conv_ovr> g_gdn_conv; + +void ggml_cuda_gdn_set_conv(const ggml_tensor * gdn, const float * x, const float * st, const float * cw, + int64_t C, int64_t qc, int64_t kc, int64_t vc) { + std::lock_guard<std::mutex> lk(g_gdn_qk_mtx); + g_gdn_conv[gdn] = { x, st, cw, C, qc, kc, vc }; +} + +struct gdn_out_norm_ovr { + const float * w; + const float * z; + float * out; + uint16_t * out16; + float eps; +}; +static std::unordered_map<const ggml_tensor *, gdn_out_norm_ovr> g_gdn_on; + +void ggml_cuda_gdn_set_out_norm(const ggml_tensor * gdn, const float * w, const float * z, float * out, uint16_t * out16, float eps) { + std::lock_guard<std::mutex> lk(g_gdn_qk_mtx); + g_gdn_on[gdn] = { w, z, out, out16, eps }; +} + +bool ggml_cuda_gdn_chunk_eligible_node(const ggml_tensor * gdn) { + const ggml_tensor * q = gdn->src[0], * k = gdn->src[1]; + { + std::lock_guard<std::mutex> lk(g_gdn_qk_mtx); + auto it = g_gdn_qk.find(gdn); + if (it != g_gdn_qk.end()) { q = it->second.q; k = it->second.k; } + } + return ggml_cuda_gdn_chunk_eligible(gdn, q, k); +} + +void ggml_cuda_gdn_set_qk_norm(const ggml_tensor * gdn, const ggml_tensor * q_raw, const ggml_tensor * k_raw, float eps, float mul) { + std::lock_guard<std::mutex> lk(g_gdn_qk_mtx); + g_gdn_qk[gdn] = { q_raw, k_raw, eps, mul }; +} + template <bool KDA, bool keep_rs_t> static void launch_gated_delta_net( const float * q_d, const float * k_d, const float * v_d, @@ -595,8 +1196,36 @@ static void launch_gated_delta_net( static void ggml_cuda_op_gated_delta_net_impl( ggml_backend_cuda_context & ctx, ggml_tensor * dst, const ggml_cuda_gated_delta_net_fused_cache * cache) { - ggml_tensor * src_q = dst->src[0]; - ggml_tensor * src_k = dst->src[1]; + // q/k-norm fusion: the normalized q/k were never computed; read the raw views and normalize in the chunk kernels + gdn_qk_norm_ovr qk_ovr = { nullptr, nullptr, -1.0f, 1.0f }; + { + std::lock_guard<std::mutex> lk(g_gdn_qk_mtx); + auto it = g_gdn_qk.find(dst); + if (it != g_gdn_qk.end()) { + qk_ovr = it->second; + g_gdn_qk.erase(it); + } + } + gdn_out_norm_ovr on_ovr = { nullptr, nullptr, nullptr, nullptr, 0.0f }; + { + std::lock_guard<std::mutex> lk(g_gdn_qk_mtx); + auto it = g_gdn_on.find(dst); + if (it != g_gdn_on.end()) { + on_ovr = it->second; + g_gdn_on.erase(it); + } + } + gdn_conv_ovr cv_ovr = { nullptr, nullptr, nullptr, 0, 0, 0, 0 }; + { + std::lock_guard<std::mutex> lk(g_gdn_qk_mtx); + auto it = g_gdn_conv.find(dst); + if (it != g_gdn_conv.end()) { + cv_ovr = it->second; + g_gdn_conv.erase(it); + } + } + ggml_tensor * src_q = qk_ovr.q ? (ggml_tensor *) qk_ovr.q : dst->src[0]; + ggml_tensor * src_k = qk_ovr.k ? (ggml_tensor *) qk_ovr.k : dst->src[1]; ggml_tensor * src_v = dst->src[2]; ggml_tensor * src_g = dst->src[3]; ggml_tensor * src_beta = dst->src[4]; @@ -667,6 +1296,40 @@ static void ggml_cuda_op_gated_delta_net_impl( state_slot_stride = cache->slot_stride; } +#if defined(GGML_USE_HIP) + { + const int dev = ggml_cuda_get_device(); + const int cc = ggml_cuda_info().devices[dev].cc; + const float qk_eps = qk_ovr.q ? qk_ovr.eps : -1.0f; + const float qk_mul = qk_ovr.q ? qk_ovr.mul : 1.0f; + const bool eligible = gdn_chunk_eligible_raw(cc, kda, keep_rs, S_v, H, neqk1, n_tokens, n_seqs, sq1, sq2, q_d, k_d); + if ((qk_ovr.q || on_ovr.z || cv_ovr.x) && !eligible) { + GGML_ABORT("gated_delta_net: conv / q/k / output norms were fused for %s but the chunked path is not taken", dst->name); + } + if (eligible) { + gdnc_in in = { q_d, k_d, v_d, sq1, sq2, sv1, sv2, cv_ovr.x, cv_ovr.st, cv_ovr.cw, cv_ovr.C, cv_ovr.qc, cv_ovr.kc, cv_ovr.vc }; + auto run = [&](auto CLc) { + constexpr int CL = decltype(CLc)::value; + const int nch = (int) ((n_tokens + CL - 1) / CL); + ggml_cuda_pool_alloc<_Float16> Tb(ctx.pool(), (size_t) H * nch * CL * CL); + ggml_cuda_pool_alloc<_Float16> Pb(ctx.pool(), (size_t) H * nch * CL * CL); + ggml_cuda_pool_alloc<float2> Gb(ctx.pool(), (size_t) H * nch * CL); + ggml_cuda_pool_alloc<_Float16> Qh(ctx.pool(), (size_t) neqk1 * n_tokens * 128); + ggml_cuda_pool_alloc<_Float16> Kh(ctx.pool(), (size_t) neqk1 * n_tokens * 128); + ggml_cuda_pool_alloc<_Float16> Vh(ctx.pool(), (size_t) H * n_tokens * 128); + gdn_chunk_prep_kernel<CL><<<dim3(neqk1, nch), 256, 0, stream>>>(in, g_d, b_d, + Tb.get(), Pb.get(), Gb.get(), Qh.get(), Kh.get(), Vh.get(), (int) H, (int) neqk1, (int) n_tokens, nch, + sb1, sb2, scale, qk_eps, qk_mul); + gdn_chunk_scan_kernel<CL><<<dim3(H), 256, 0, stream>>>(Qh.get(), Kh.get(), Vh.get(), s_d, dst_d, state_d, + Tb.get(), Pb.get(), Gb.get(), (int) H, (int) neqk1, (int) n_tokens, nch, scale, + on_ovr.w, on_ovr.z, on_ovr.out, on_ovr.out16, on_ovr.eps); + CUDA_CHECK(cudaGetLastError()); + }; + run(std::integral_constant<int, 32>{}); + return; + } + } +#endif if (kda) { if (keep_rs) { launch_gated_delta_net<true, true>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d, diff --git a/ggml/src/ggml-cuda/gated_delta_net.cuh b/ggml/src/ggml-cuda/gated_delta_net.cuh index e2b41cf1c47f..045bbccb1237 100644 --- a/ggml/src/ggml-cuda/gated_delta_net.cuh +++ b/ggml/src/ggml-cuda/gated_delta_net.cuh @@ -9,6 +9,22 @@ struct ggml_cuda_gated_delta_net_fused_cache { void ggml_cuda_op_gated_delta_net(ggml_backend_cuda_context & ctx, ggml_tensor * dst); +// Chunked prefill (RDNA3.5): the q/k L2 norms that precede GATED_DELTA_NET (RMS_NORM(x, eps/n) -> SCALE(1/sqrt(n)), +// i.e. x * mul * rsqrt(sum(x^2) + eps) per 128-row) are folded into the chunk kernels. ggml_cuda_gdn_chunk_eligible +// is the exact dispatch condition of the chunked path for this GDN node with these q/k tensors; set_qk_norm registers +// the raw q/k views for the next evaluation of that node (consumed by it). +bool ggml_cuda_gdn_chunk_eligible(const ggml_tensor * gdn, const ggml_tensor * q, const ggml_tensor * k); +void ggml_cuda_gdn_set_qk_norm(const ggml_tensor * gdn, const ggml_tensor * q_raw, const ggml_tensor * k_raw, float eps, float mul); +// chunked prefill: the gated RMS norm of the output (RMS_NORM -> MUL(w) -> MUL(sigmoid(z))) written by the scan kernel +// straight into out (+ optional BF16 copy out16); z / out laid out like the GDN output rows [S_v, H, T] +void ggml_cuda_gdn_set_out_norm(const ggml_tensor * gdn, const float * w, const float * z, float * out, uint16_t * out16, float eps); +// chunked prefill: the causal conv + SiLU producing q/k/v is computed by the chunk kernels from the raw conv input +// x [T, C] (row stride C floats), history st [C, 3] and taps cw [C, 4]; qc/kc/vc = channel of element 0 of each view +void ggml_cuda_gdn_set_conv(const ggml_tensor * gdn, const float * x, const float * st, const float * cw, + int64_t C, int64_t qc, int64_t kc, int64_t vc); +// ggml_cuda_gdn_chunk_eligible with the q/k views registered by set_qk_norm (if any) +bool ggml_cuda_gdn_chunk_eligible_node(const ggml_tensor * gdn); + // same op, but writes the snapshot(s) into the cache instead of dst (see ggml_cuda_try_gdn_cache_fusion) void ggml_cuda_op_gated_delta_net_fused_cache(ggml_backend_cuda_context & ctx, ggml_tensor * dst, ggml_cuda_gated_delta_net_fused_cache cache); diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index d3f9870db762..0acd2fa713ed 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -60,6 +60,7 @@ #include "ggml-cuda/wkv.cuh" #include "ggml-cuda/gla.cuh" #include "ggml-cuda/gated_delta_net.cuh" +#include <unordered_map> #include "ggml-cuda/hyperconn.cuh" #include "ggml-cuda/norm-gated.cuh" #include "ggml-cuda/idx-relu-sum.cuh" @@ -3898,6 +3899,155 @@ static int ggml_cuda_match_idx_relu_sum(const ggml_cgraph * g, int i, ggml_cuda_ if (overlap) { return 0; } } a.score = src; a.dst = (ggml_tensor *) prev; a.heads = (int) H; + a.vis_tails = nullptr; a.vis_starts = nullptr; + for (auto & l : a.vis_leafs) { l = nullptr; } + if (getenv("GGML_CUDA_DISABLE_IDX_VIS") != nullptr || ns != 1) return count; + + auto vis_reject = [&]() { + for (auto & l : a.vis_leafs) { l = nullptr; } + return count; + }; + // qwen4exp_apply_compact_visibility with one sequence, emitted right after the head sum: + // starts = CPY(VIEW(limits) I32 [nb]) F32; tails = RESHAPE(CPY(VIEW(limits) I32 [nt]) F32, 1, nt) + // ADD(sum, LOG(STEP(SUB(REPEAT(tails, sum), starts)))) + // Every node between the head sum and the final ADD must belong to that chain, so skipping them is safe. + const int last = k - 1; + int fin = -1; + for (int j = last + 1; j < g->n_nodes && j <= last + 14; ++j) { + const ggml_tensor * n = g->nodes[j]; + if (n->op == GGML_OP_ADD && n->src[0] == prev) { fin = j; break; } + } + if (fin < 0) return vis_reject(); + const ggml_tensor * add = g->nodes[fin]; + const ggml_tensor * lg = add->src[1]; + auto is_unary = [](const ggml_tensor * t, ggml_unary_op op) { + return t && t->op == GGML_OP_UNARY && ggml_get_unary_op(t) == op && t->type == GGML_TYPE_F32 && ggml_is_contiguous(t); + }; + if (add->type != GGML_TYPE_F32 || !ggml_is_contiguous(add) || !ggml_are_same_shape(add, prev)) return vis_reject(); + if (!lg || lg->op != GGML_OP_LOG || lg->type != GGML_TYPE_F32 || !ggml_is_contiguous(lg) || !ggml_are_same_shape(lg, prev)) return vis_reject(); + const ggml_tensor * st = lg->src[0]; + if (!is_unary(st, GGML_UNARY_OP_STEP) || !ggml_are_same_shape(st, prev)) return vis_reject(); + const ggml_tensor * sb = st->src[0]; + if (!sb || sb->op != GGML_OP_SUB || sb->type != GGML_TYPE_F32 || !ggml_is_contiguous(sb) || !ggml_are_same_shape(sb, prev)) return vis_reject(); + const ggml_tensor * rp = sb->src[0]; + const ggml_tensor * c1 = sb->src[1]; + if (!rp || rp->op != GGML_OP_REPEAT || rp->type != GGML_TYPE_F32 || !ggml_is_contiguous(rp) || !ggml_are_same_shape(rp, prev)) return vis_reject(); + auto is_i32_cast = [](const ggml_tensor * c, int64_t n) { + return c && c->op == GGML_OP_CPY && c->type == GGML_TYPE_F32 && ggml_is_contiguous(c) && ggml_nelements(c) == n && + c->src[0] && c->src[0]->type == GGML_TYPE_I32 && ggml_is_contiguous(c->src[0]) && + ggml_nelements(c->src[0]) == n; + }; + // starts: [nb] broadcast over the queries + if (!is_i32_cast(c1, nb) || c1->ne[0] != nb) return vis_reject(); + const ggml_tensor * tr = rp->src[0]; + const ggml_tensor * c2 = tr; + if (tr && tr->op == GGML_OP_RESHAPE) { c2 = tr->src[0]; } + if (!tr || tr->ne[0] != 1 || tr->ne[1] != nt || ggml_nelements(tr) != nt || !is_i32_cast(c2, nt)) return vis_reject(); + + // the chain, and nothing else, sits between the head sum and the final ADD + const ggml_tensor * chain[10] = { c1->src[0], c1, c2->src[0], c2, tr, rp, sb, st, lg, add }; + // The limits views and their I32->F32 casts read a graph input, which ggml_can_fuse_subgraph_ext rejects as an + // external view source (and the cast self-reference would skew its use count), so they are checked here: each + // must be read only by its successor in the chain. The fused kernel reads the limits input directly. The tails + // RESHAPE is a view of its cast and is checked the same way. When the scheduler copies the limits views to the + // device as split inputs, the casts read those copies (leafs) and the original VIEW nodes stay in the range + // unused; a view computes nothing, so skipping one is harmless whoever reads it. + const ggml_tensor * leafs[5] = { c1->src[0], c1, c2->src[0], c2, tr }; + const ggml_tensor * users[5] = { c1, sb, c2, tr, rp }; + int indices2[32]; ggml_op ops2[32]; + int count2 = 0; + int n_manual = 0; + bool seen[5] = {}; + for (int j = i; j <= fin; ++j) { + const ggml_tensor * n = g->nodes[j]; + if (j > last) { + int leaf = -1; + for (int l = 0; l < 5; ++l) { if (leafs[l] == n) leaf = l; } + bool member = leaf >= 0; + for (const ggml_tensor * c : chain) { member = member || c == n; } + if (!member) { + if (n->op != GGML_OP_VIEW && n->op != GGML_OP_RESHAPE && n->op != GGML_OP_PERMUTE && + n->op != GGML_OP_TRANSPOSE && n->op != GGML_OP_NONE) return vis_reject(); + if (n_manual >= 16) return vis_reject(); + a.vis_leafs[n_manual++] = n; + continue; + } + if (leaf >= 0) { + if (n->flags & GGML_TENSOR_FLAG_OUTPUT) return vis_reject(); + if (ggml_node_get_use_count(g, j) != 1 + (n->src[1] == n ? 1 : 0)) return vis_reject(); + int refs = 0; + for (int s2 = 0; s2 < GGML_MAX_SRC; ++s2) { if (users[leaf]->src[s2] == n) ++refs; } + if (refs != 1) return vis_reject(); + if (n_manual >= 16) return vis_reject(); + a.vis_leafs[n_manual++] = n; + seen[leaf] = true; + continue; + } + } + if (count2 >= 32) return vis_reject(); + indices2[count2] = j; ops2[count2] = n->op; ++count2; + } + // the casts and the tails RESHAPE must be nodes of the skipped range; the limits views may instead be leafs + // (device copies of split inputs), which are ready before the range starts + if (!seen[1] || !seen[3] || !seen[4] || tr == c2) return vis_reject(); + for (int l : { 0, 2 }) { + if (!seen[l]) { + for (int j = i; j < g->n_nodes; ++j) { if (g->nodes[j] == leafs[l]) return vis_reject(); } + } + } + if (!ggml_can_fuse_subgraph_ext(g, indices2, count2, ops2, &fin, 1)) { + for (auto & l : a.vis_leafs) { l = nullptr; } + return vis_reject(); + } + count2 = fin - i + 1; // nodes skipped: every node from the RELU to the final ADD + // the fused kernel reads the scores and the limits while it writes the result + for (const ggml_tensor * in : { src, (const ggml_tensor *) c1->src[0], (const ggml_tensor *) c2->src[0] }) { + if (in->data && add->data) { + const uintptr_t av = (uintptr_t) in->data, bv = (uintptr_t) add->data; + const bool overlap = av <= bv ? bv - av < ggml_nbytes(in) : av - bv < ggml_nbytes(add); + if (overlap) { return vis_reject(); } + } + } + a.dst = (ggml_tensor *) add; + a.vis_starts = c1->src[0]; + a.vis_tails = c2->src[0]; + return count2; +} + +// MUL_MAT(keys F32, q) -> RESHAPE [M, 4, nq] -> RELU/head sum (+ visibility): the scorer writes the summed scores +static int ggml_cuda_match_idx_gemm(ggml_backend_cuda_context & ctx, const ggml_cgraph * g, int i, ggml_cuda_idx_relu_sum_args & a) { + if (getenv("GGML_CUDA_DISABLE_IDX_GEMM") != nullptr || i + 3 >= g->n_nodes) return 0; + const ggml_tensor * mm = g->nodes[i]; + if (mm->op != GGML_OP_MUL_MAT || mm->src[2] != nullptr || mm->type != GGML_TYPE_F32 || !ggml_is_contiguous(mm)) return 0; + const ggml_tensor * rs = g->nodes[i + 1]; + if (rs->op != GGML_OP_RESHAPE || rs->src[0] != mm || rs->ne[0] != mm->ne[0] || rs->ne[1] != 4 || rs->ne[3] != 1 || + rs->ne[1] * rs->ne[2] != mm->ne[1] * mm->ne[2] * mm->ne[3]) return 0; + const ggml_tensor * relu = g->nodes[i + 2]; + if (relu->op != GGML_OP_UNARY || ggml_get_unary_op(relu) != GGML_UNARY_OP_RELU || relu->src[0] != rs) return 0; + const int c = ggml_cuda_match_idx_relu_sum(g, i + 2, a); + if (c <= 0 || a.score != rs || a.heads != 4) return 0; + if (!ggml_cuda_mmb_idx_score_supported(ctx, mm->src[0], mm->src[1], mm, a.heads)) return 0; + const int count = c + 2; + const int output = i + count - 1; + if (g->nodes[output] != a.dst) return 0; + int indices[32]; ggml_op ops[32]; + int n = 0; + for (int j = i; j <= output; ++j) { + const ggml_tensor * node = g->nodes[j]; + bool checked = false; + for (const ggml_tensor * l : a.vis_leafs) { checked = checked || (l && l == node); } + if (checked) continue; + if (n >= 32) return 0; + indices[n] = j; ops[n] = node->op; ++n; + } + if (!ggml_can_fuse_subgraph_ext(g, indices, n, ops, &output, 1)) return 0; + for (const ggml_tensor * in : { (const ggml_tensor *) mm->src[0], (const ggml_tensor *) mm->src[1], a.vis_tails, a.vis_starts }) { + if (in && in->data && a.dst->data) { + const uintptr_t av = (uintptr_t) in->data, bv = (uintptr_t) a.dst->data; + const bool overlap = av <= bv ? bv - av < ggml_nbytes(in) : av - bv < ggml_nbytes(a.dst); + if (overlap) { return 0; } + } + } return count; } @@ -4290,6 +4440,82 @@ static uint16_t * ggml_cuda_bf16_for_mmb_reader(ggml_backend_cuda_context & ctx, return nullptr; } +// qwen4exp chunked Gated DeltaNet prefill: the q/k L2 norms RMS_NORM(x, eps/n) -> SCALE(s) (x2) at node i whose only +// reader is a chunk-eligible GATED_DELTA_NET. Returns the index of the last matched node (-1: no match) and the GDN index, +// the raw q/k views, eps (n * eps/n) and the factor s*sqrt(n) of x * s*sqrt(n) * rsqrt(sum(x^2) + eps). +static int ggml_cuda_gdn_qk_norm_match(const ggml_cgraph * cgraph, const int i, int & ig, const ggml_tensor *& q_raw, + const ggml_tensor *& k_raw, float & eps, float & mul) { + const ggml_tensor * node = cgraph->nodes[i]; + if (node->op != GGML_OP_RMS_NORM) { + return -1; + } + auto next_compute = [&](int k) { + while (k < cgraph->n_nodes && ggml_cuda_is_view_or_noop(cgraph->nodes[k])) { + k++; + } + return k; + }; + const int iq_s = next_compute(i + 1); + const int ik_n = iq_s < cgraph->n_nodes ? next_compute(iq_s + 1) : cgraph->n_nodes; + const int ik_s = ik_n < cgraph->n_nodes ? next_compute(ik_n + 1) : cgraph->n_nodes; + if (ik_s >= cgraph->n_nodes) { + return -1; + } + const ggml_tensor * qs = cgraph->nodes[iq_s]; + const ggml_tensor * kn = cgraph->nodes[ik_n]; + const ggml_tensor * ks = cgraph->nodes[ik_s]; + q_raw = node->src[0]; + k_raw = kn->src[0]; + bool ok = qs->op == GGML_OP_SCALE && qs->src[0] == node && kn->op == GGML_OP_RMS_NORM && ks->op == GGML_OP_SCALE && + ks->src[0] == kn && q_raw && k_raw && node->ne[0] == 128 && q_raw->type == GGML_TYPE_F32 && + node->type == GGML_TYPE_F32 && qs->type == GGML_TYPE_F32 && ks->type == GGML_TYPE_F32 && + ggml_are_same_shape(q_raw, k_raw) && + ggml_get_op_params_f32(node, 0) == ggml_get_op_params_f32(kn, 0) && + ggml_get_op_params_f32(qs, 0) == ggml_get_op_params_f32(ks, 0) && + ggml_get_op_params_f32(qs, 1) == 0.0f && ggml_get_op_params_f32(ks, 1) == 0.0f; + ig = -1; + for (int k = ik_s + 1; ok && k < cgraph->n_nodes && k <= ik_s + 24; ++k) { + const ggml_tensor * g = cgraph->nodes[k]; + if (g->op == GGML_OP_GATED_DELTA_NET && g->src[0] == qs && g->src[1] == ks) { + ig = k; + break; + } + } + ok = ok && ig > 0 && + ggml_node_get_use_count(cgraph, i) == 1 && ggml_node_get_use_count(cgraph, iq_s) == 1 && + ggml_node_get_use_count(cgraph, ik_n) == 1 && ggml_node_get_use_count(cgraph, ik_s) == 1 && + ((node->flags | qs->flags | kn->flags | ks->flags) & GGML_TENSOR_FLAG_OUTPUT) == 0 && + ggml_cuda_gdn_chunk_eligible(cgraph->nodes[ig], q_raw, k_raw); + if (!ok) { + return -1; + } + const float n = (float) node->ne[0]; + eps = ggml_get_op_params_f32(node, 0) * n; + mul = ggml_get_op_params_f32(qs, 0) * sqrtf(n); + return ik_s; +} + +// shared-expert merge after the MoE weighted reduction `red`: SIGMOID(gate [1, T]) -> MUL(shexp, .) -> ADD(red, .). +// One predicate for the fusion in ggml_cuda_try_fuse and the reorder in graph_optimize, so nodes are only moved +// when the fusion can fire (use counts are checked by each caller). +static bool ggml_cuda_moe_sgma_enabled() { + static const bool on = getenv("GGML_CUDA_DISABLE_MOE_SGMA") == nullptr && getenv("GGML_CUDA_DISABLE_SGMA") == nullptr && + !(getenv("GGML_CUDA_DISABLE_FUSION") != nullptr && std::atoi(getenv("GGML_CUDA_DISABLE_FUSION"))); + return on; +} + +static bool ggml_cuda_moe_sgma_tail(const ggml_cuda_moe_weighted_reduction_match & m, const ggml_tensor * sig, + const ggml_tensor * mul, const ggml_tensor * add) { + const ggml_tensor * red = m.dst; + return sig->op == GGML_OP_UNARY && ggml_get_unary_op(sig) == GGML_UNARY_OP_SIGMOID && sig->ne[0] == 1 && + mul->op == GGML_OP_MUL && mul->src[1] == sig && add->op == GGML_OP_ADD && + ((add->src[0] == mul && add->src[1] == red) || (add->src[1] == mul && add->src[0] == red)) && + ggml_are_same_shape(mul->src[0], add) && ggml_are_same_shape(red, add) && ggml_nrows(add) == ggml_nelements(sig) && + (sig->flags & GGML_TENSOR_FLAG_OUTPUT) == 0 && (mul->flags & GGML_TENSOR_FLAG_OUTPUT) == 0 && + (red->flags & GGML_TENSOR_FLAG_OUTPUT) == 0 && + ggml_cuda_moe_weighted_reduction_sgma_supported(m.experts, m.expert_scale, m.weights, sig->src[0], mul->src[0], add); +} + static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph * cgraph, int i) { static bool disable_fusion = getenv("GGML_CUDA_DISABLE_FUSION") != nullptr && std::atoi(getenv("GGML_CUDA_DISABLE_FUSION")); @@ -4306,6 +4532,131 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph ggml_tensor * node = cgraph->nodes[i]; + // chains already computed by an earlier fused kernel (the chunked GDN writes its gated output norm itself) + static thread_local std::unordered_map<const ggml_tensor *, int> s_precomputed; + if (!s_precomputed.empty()) { + auto it = s_precomputed.find(node); + if (it != s_precomputed.end()) { + const int count = it->second; + s_precomputed.erase(it); + return count > 0 ? count : GGML_CUDA_FUSED_SELF; + } + } + + if (node->op == GGML_OP_MUL_MAT && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + ggml_cuda_idx_relu_sum_args args; + const int count = ggml_cuda_match_idx_gemm(*cuda_ctx, cgraph, i, args); + if (count > 0) { + ggml_cuda_mmb_idx_score(*cuda_ctx, node->src[0], node->src[1], args.dst, + args.vis_tails ? (const int32_t *) args.vis_tails->data : nullptr, + args.vis_starts ? (const int32_t *) args.vis_starts->data : nullptr); + return count - 1; + } + } + + // prefill shared-expert merge: SIGMOID(gate [1, T]) -> MUL(shexp [n, T], gate) -> ADD(moe_out, .) in one kernel + if (node->op == GGML_OP_UNARY && ggml_get_unary_op(node) == GGML_UNARY_OP_SIGMOID && i + 2 < cgraph->n_nodes && + node->type == GGML_TYPE_F32 && node->ne[0] == 1 && ggml_nrows(node) >= 2 && getenv("GGML_CUDA_DISABLE_SGMA") == nullptr) { + ggml_tensor * mul = cgraph->nodes[i + 1]; + ggml_tensor * add = cgraph->nodes[i + 2]; + const ggml_tensor * gate = node->src[0]; + if (mul->op == GGML_OP_MUL && add->op == GGML_OP_ADD && mul->src[1] == node && (add->src[0] == mul || add->src[1] == mul)) { + const ggml_tensor * src = mul->src[0]; + const ggml_tensor * other = add->src[0] == mul ? add->src[1] : add->src[0]; + auto aliases_ok = [&](const ggml_tensor * t) { // disjoint from out, or the same elements in place + const uintptr_t a0 = (uintptr_t) t->data, a1 = a0 + ggml_nbytes(t), b0 = (uintptr_t) add->data, b1 = b0 + ggml_nbytes(add); + return !(a0 < b1 && b0 < a1) || (t->data == add->data && ggml_are_same_layout(t, add)); + }; + if (gate->type == GGML_TYPE_F32 && ggml_is_contiguous(gate) && src->type == GGML_TYPE_F32 && other->type == GGML_TYPE_F32 && + add->type == GGML_TYPE_F32 && mul->type == GGML_TYPE_F32 && ggml_is_contiguous(src) && ggml_is_contiguous(other) && + ggml_is_contiguous(add) && ggml_are_same_shape(src, add) && ggml_are_same_shape(other, add) && + ggml_are_same_shape(mul, add) && ggml_nrows(add) == ggml_nelements(node) && other != mul && + !ggml_cuda_mmb_is_bf16_only(*cuda_ctx, add) && aliases_ok(src) && aliases_ok(other) && aliases_ok(gate) && + ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_UNARY, GGML_OP_MUL, GGML_OP_ADD }, { i + 2 })) { + ggml_cuda_op_sigmoid_gate_mul_add(*cuda_ctx, gate, src, other, add); + return 2; + } + } + } + + // qwen4exp GDN: ssm_beta / ssm_alpha, two narrow F32-weight GEMMs on the same activations, in one pass over them. + // The second node's output is written early, so it must not overlap anything the nodes in between touch. + if (node->op == GGML_OP_MUL_MAT && node->src[0]->type == GGML_TYPE_F32 && node->src[0]->ne[1] <= 64 && node->src[1]->type == GGML_TYPE_F32 && + node->src[1]->ne[1] * node->src[1]->ne[2] >= 512 && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc) && + getenv("GGML_CUDA_DISABLE_F32_DUAL") == nullptr && ggml_cuda_mmb_supported_mm(*cuda_ctx, node->src[0], node->src[1], node)) { + for (int j = i + 1; j < cgraph->n_nodes && j <= i + 8; ++j) { + ggml_tensor * n2 = cgraph->nodes[j]; + if (!(n2->op == GGML_OP_MUL_MAT && n2->src[1] == node->src[1] && n2->src[0]->type == GGML_TYPE_F32 && + n2->src[0]->ne[0] == node->src[0]->ne[0] && n2->src[0]->ne[1] <= 64 && n2->src[2] == nullptr && + ggml_cuda_mmb_supported_mm(*cuda_ctx, n2->src[0], n2->src[1], n2))) { + continue; + } + auto overlaps = [](const ggml_tensor * a, const ggml_tensor * b) { + if (!a || !b || !a->data || !b->data) return false; + const uintptr_t a0 = (uintptr_t) a->data, a1 = a0 + ggml_nbytes(a), b0 = (uintptr_t) b->data, b1 = b0 + ggml_nbytes(b); + return a0 < b1 && b0 < a1; + }; + bool ok = !overlaps(n2, node) && !overlaps(n2, node->src[1]); + for (int k = i + 1; ok && k < j; ++k) { + const ggml_tensor * t = cgraph->nodes[k]; + ok = !overlaps(n2, t); + for (int sidx = 0; ok && sidx < GGML_MAX_SRC && t->src[sidx]; ++sidx) ok = !overlaps(n2, t->src[sidx]); + } + if (ok && ggml_cuda_mmb_f32_dual(*cuda_ctx, node->src[0], n2->src[0], node->src[1], node, n2)) { + s_precomputed[n2] = 0; + return GGML_CUDA_FUSED_SELF; + } + break; + } + } + + // chunked GDN prefill: RMS_NORM(view of the GDN output) -> MUL(w) -> MUL(sigmoid(z)) is applied by the scan kernel + // as its epilogue (rms_rows_f32<true> math), so the pre-norm output never round-trips through memory + if (node->op == GGML_OP_GATED_DELTA_NET && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc) && + getenv("GGML_CUDA_DISABLE_GDN_OUTNORM") == nullptr) { + const ggml_tensor * v = node->src[2]; + int ix = -1; + for (int j = i + 1; j < cgraph->n_nodes && j <= i + 16; ++j) { + ggml_tensor * r = cgraph->nodes[j]; + if (r->op == GGML_OP_VIEW && r->view_src == node && r->view_offs == 0) { + ix = j; + } + if (r->op != GGML_OP_RMS_NORM) { + continue; + } + const ggml_tensor * x = r->src[0]; + if (!(x && x->view_src == node && x->view_offs == 0)) { + break; + } + ggml_cuda_norm_gated_match m; + const int sk = ggml_cuda_norm_gated_match_at(cgraph, j, m); + const int64_t S = v->ne[0], H = v->ne[1], T = v->ne[2]; + const bool ok = sk > 0 && !m.staged && m.pre < 0 && m.x == x && m.z && S == 128 && v->ne[3] == 1 && + x->ne[0] == S && x->ne[1] == H && x->ne[2] == T && x->nb[0] == 4 && x->nb[1] == (size_t) S * 4 && + x->nb[2] == (size_t) S * H * 4 && ggml_nelements(m.dst) == S * H * T && ggml_is_contiguous(m.dst) && + ggml_nelements(m.z) == S * H * T && ggml_is_contiguous(m.z) && ggml_nelements(m.w) == S && + (ix < 0 || ggml_node_get_use_count(cgraph, ix) == 1) && + (m.dst->flags & GGML_TENSOR_FLAG_OUTPUT) == 0 && + ggml_cuda_gdn_chunk_eligible_node(node); + // the norm output is written during the scan, earlier than in the unfused schedule: it must not overlap the GDN + // tensor (output rows + state tail) and may alias z only element for element + auto overlap = [](const ggml_tensor * a, const ggml_tensor * b) { + const uintptr_t a0 = (uintptr_t) a->data, a1 = a0 + ggml_nbytes(a), b0 = (uintptr_t) b->data, b1 = b0 + ggml_nbytes(b); + return a->data && b->data && a0 < b1 && b0 < a1; + }; + const bool alias_ok = ok && !overlap(m.dst, node) && + (!overlap(m.dst, m.z) || (m.dst->data == m.z->data && ggml_nbytes(m.dst) == ggml_nbytes(m.z))); + if (ok && alias_ok) { + uint16_t * dst16 = ggml_cuda_bf16_for_mmb_reader(*cuda_ctx, cgraph, j + sk, m.dst); + // every reader takes the BF16 copy: skip the F32 store + float * dst32 = (dst16 && ggml_cuda_mmb_is_bf16_only(*cuda_ctx, m.dst)) ? nullptr : (float *) m.dst->data; + ggml_cuda_gdn_set_out_norm(node, (const float *) m.w->data, (const float *) m.z->data, dst32, dst16, m.eps); + s_precomputed[r] = sk; + } + break; + } + } + // routed gate/up + SwiGLU on the BF16 WMMA path from 512 tokens: ahead of the MMQ-based expert fusions below, // which yield to MMB whenever it supports the GEMM if (node->op == GGML_OP_MUL_MAT_ID && i + 2 < cgraph->n_nodes && cgraph->nodes[i + 1]->op == GGML_OP_MUL_MAT_ID && @@ -4344,6 +4695,46 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph return 1; // the concat and the no-op view after it } if (node->op == GGML_OP_SSM_CONV && ggml_cuda_gdn_conv_match_at_conv(cgraph, i, gm)) { + // chunked GDN prefill: if the SiLU output is read only through the q/k/v views of one chunk-eligible + // GATED_DELTA_NET (q/k through the fused L2 norms), the chunk kernels compute the conv themselves + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc) && getenv("GGML_CUDA_DISABLE_GDN_CONV") == nullptr && + getenv("GGML_CUDA_DISABLE_GDN_QKNORM") == nullptr && i + 1 < cgraph->n_nodes && cgraph->nodes[i + 1] == gm.out) { + const ggml_tensor * out = gm.out; + int i_rms = -1; + for (int k = i + 2; k < cgraph->n_nodes && k <= i + 24; ++k) { + const ggml_tensor * t = cgraph->nodes[k]; + if (t->op == GGML_OP_RMS_NORM && t->src[0] && t->src[0]->view_src == out) { i_rms = k; break; } + } + int ig = -1; const ggml_tensor * q_raw = nullptr; const ggml_tensor * k_raw = nullptr; float eps = 0.0f, mul = 1.0f; + const int last = i_rms > 0 ? ggml_cuda_gdn_qk_norm_match(cgraph, i_rms, ig, q_raw, k_raw, eps, mul) : -1; + const ggml_tensor * gdn = last > 0 ? cgraph->nodes[ig] : nullptr; + const ggml_tensor * v_raw = gdn ? gdn->src[2] : nullptr; + bool ok = gdn && v_raw && q_raw->view_src == out && k_raw->view_src == out && v_raw->view_src == out && + (out->flags & GGML_TENSOR_FLAG_OUTPUT) == 0 && ggml_node_get_use_count(cgraph, i + 1) == 3 && + out->ne[0] == gm.C && ggml_is_contiguous(out) && gm.x->ne[0] == gm.C && + q_raw->nb[2] == (size_t) gm.C * 4 && v_raw->nb[2] == (size_t) gm.C * 4 && v_raw->nb[1] == 128 * 4 && + q_raw->view_offs % 16 == 0 && k_raw->view_offs % 16 == 0 && v_raw->view_offs % 16 == 0 && gm.C % 4 == 0; + // the three views are the only readers of the SiLU output, and each view has exactly one reader + for (int k = i + 2; ok && k < cgraph->n_nodes; ++k) { + const ggml_tensor * t = cgraph->nodes[k]; + for (int sidx = 0; ok && sidx < GGML_MAX_SRC && t->src[sidx]; ++sidx) { + const ggml_tensor * sv = t->src[sidx]; + if (sv == out) { + ok = t == q_raw || t == k_raw || t == v_raw; + } else if (sv->view_src == out) { + ok = (sv == q_raw && t == cgraph->nodes[i_rms]) || (sv == k_raw && t->op == GGML_OP_RMS_NORM && t->src[0] == k_raw) || + (sv == v_raw && t == gdn); + } + } + } + if (ok) { + ggml_cuda_gdn_set_qk_norm(gdn, q_raw, k_raw, eps, mul); + ggml_cuda_gdn_set_conv(gdn, (const float *) gm.x->data, (const float *) gm.state->data, (const float *) gm.w->data, + gm.C, (int64_t) (q_raw->view_offs / 4), (int64_t) (k_raw->view_offs / 4), (int64_t) (v_raw->view_offs / 4)); + s_precomputed[cgraph->nodes[i_rms]] = last - i_rms; + return 1; + } + } ggml_cuda_gdn_conv_direct(*cuda_ctx, gm); return 1; } @@ -4424,6 +4815,17 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } } + // qwen4exp chunked Gated DeltaNet prefill: q/k L2 norms folded into the chunk kernels (ggml_cuda_gdn_qk_norm_match) + if (node->op == GGML_OP_RMS_NORM && GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc) && + getenv("GGML_CUDA_DISABLE_GDN_QKNORM") == nullptr) { + int ig; const ggml_tensor * q_raw; const ggml_tensor * k_raw; float eps, mul; + const int last = ggml_cuda_gdn_qk_norm_match(cgraph, i, ig, q_raw, k_raw, eps, mul); + if (last > 0) { + ggml_cuda_gdn_set_qk_norm(cgraph->nodes[ig], q_raw, k_raw, eps, mul); + return last - i; + } + } + if (node->op == GGML_OP_L2_NORM && i + 2 < cgraph->n_nodes && GGML_CUDA_CC_IS_AMD(ggml_cuda_info().devices[cuda_ctx->device].cc)) { ggml_tensor * k_view = cgraph->nodes[i + 1]; @@ -4518,6 +4920,20 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph } } } + // prefill shared-expert merge right after: SIGMOID(gate [1, T]) -> MUL(shexp, .) -> ADD(reduction, .), the reduction + // and the product used only there -> one kernel (bit-identical to reduction + sigmoid_gate_mul_add_f32) + if (ggml_cuda_moe_sgma_enabled() && !merge && i + count + 2 < cgraph->n_nodes && ggml_node_has_n_uses(cgraph, i + count - 1, 1)) { + ggml_tensor * sig = cgraph->nodes[i + count], * mul = cgraph->nodes[i + count + 1], * add = cgraph->nodes[i + count + 2]; + if (ggml_cuda_moe_sgma_tail(match, sig, mul, add) && + ggml_node_has_n_uses(cgraph, i + count, 1) && ggml_node_has_n_uses(cgraph, i + count + 1, 1)) { + const int out_idx = i + count + 2; + if (ggml_cuda_check_fusion_memory_ranges(cgraph, i, count + 3, &out_idx, 1) && + ggml_cuda_op_moe_weighted_reduction_sgma(*cuda_ctx, match.experts, match.expert_scale, match.weights, + sig->src[0], mul->src[0], add)) { + return count + 2; + } + } + } const int output_idx = i + count - 1; if (ggml_cuda_check_fusion_memory_ranges(cgraph, i, count, &output_idx, 1)) { ggml_cuda_op_moe_weighted_reduction( @@ -6205,6 +6621,54 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph const void * key = cgraph->n_nodes ? cgraph->nodes[0] : nullptr; if (cuda_ctx->mmb_after_compute || cuda_ctx->mmb_first_split == nullptr || key == cuda_ctx->mmb_first_split) { ggml_cuda_mmb_marks_clear(*cuda_ctx); cuda_ctx->mmb_graph_sigs.clear(); cuda_ctx->mmb_res16_pending.clear(); cuda_ctx->mmb_first_split = key; cuda_ctx->mmb_after_compute = false; } } + // MoE prefill with a sigmoid-gated shared expert (qwen4exp, and any model with the same pattern): the weighted + // reduction is followed by the shared-expert GEMMs and only then by SIGMOID(gate) -> MUL(shexp, .) -> ADD(moe_out, .). + // Move the reduction nodes down to just before the SIGMOID so the fusion computes reduction + merge in one kernel. + // Only done when that fusion can fire (same predicate) and no node in between reads a reduction node or writes + // (in place / through a view) memory the reduction reads. Runs before allocation, so buffer lifetimes follow the new + // order. GGML_CUDA_DISABLE_MOE_SGMA=1 or GGML_CUDA_DISABLE_FUSION=1 keeps the original order. + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc) && ggml_cuda_moe_sgma_enabled()) { + auto root = [](const ggml_tensor * t) { return t->view_src ? t->view_src : t; }; + // readers of t in this graph (the use-count table is not guaranteed on a split's graph view) + auto n_uses = [&](const ggml_tensor * t) { + int c = 0; + for (int n = 0; n < cgraph->n_nodes; ++n) { + for (int s = 0; s < GGML_MAX_SRC && cgraph->nodes[n]->src[s]; ++s) { + c += cgraph->nodes[n]->src[s] == t; + } + } + return c; + }; + for (int i = 0; i < cgraph->n_nodes; ++i) { + if (cgraph->nodes[i]->op != GGML_OP_MUL) continue; + ggml_cuda_moe_weighted_reduction_match mm; + if (!ggml_cuda_match_moe_weighted_reduction(cgraph, i, mm) || ggml_nrows(mm.dst) < 512) continue; + const int cnt = mm.node_count; + int k = -1; + for (int n = i + cnt; n + 2 < cgraph->n_nodes && n <= i + cnt + 24; ++n) { + const ggml_tensor * sig = cgraph->nodes[n], * mul = cgraph->nodes[n + 1], * add = cgraph->nodes[n + 2]; + if (ggml_cuda_moe_sgma_tail(mm, sig, mul, add)) { k = n; break; } + } + if (k <= i + cnt) continue; // not found, or already adjacent + if (n_uses(mm.dst) != 1 || n_uses(cgraph->nodes[k]) != 1 || n_uses(cgraph->nodes[k + 1]) != 1) continue; + bool indep = true; + for (int n = i + cnt; n < k && indep; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + for (int r = i; r < i + cnt && indep; ++r) { + const ggml_tensor * red = cgraph->nodes[r]; + for (int s = 0; s < GGML_MAX_SRC && t->src[s] && indep; ++s) { // t reads a reduction node + indep = t->src[s] != red && root(t->src[s]) != red; + } + for (int s = 0; s < GGML_MAX_SRC && red->src[s] && indep; ++s) { // t writes what the reduction reads + indep = !(t->view_src && root(red->src[s]) == t->view_src) && red->src[s] != t; + } + } + } + if (!indep) continue; + std::rotate(cgraph->nodes + i, cgraph->nodes + i + cnt, cgraph->nodes + k); // [between][reduction] SIGMOID MUL ADD + i = k + 2; + } + } { // HC16: mark tensors whose readers all take the BF16 copy, so their producers skip the F32 store if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { const int ws = ggml_cuda_info().devices[cuda_ctx->device].warp_size; @@ -6352,6 +6816,25 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph } if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, d)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, d); } + for (int i = 0; i < cgraph->n_nodes; ++i) { // gated norm of a GDN output read only by MMB GEMMs (ssm_out): the + ggml_cuda_norm_gated_match m; // chunked scan epilogue then stores only the BF16 copy + if (ggml_cuda_norm_gated_match_at(cgraph, i, m) <= 0 || !m.dst || !m.x) continue; + const ggml_tensor * xs = m.x->view_src; + if (!xs || xs->op != GGML_OP_GATED_DELTA_NET) continue; + const ggml_tensor * d = m.dst; + if (ggml_nrows(d) < 512) continue; + bool ok = true; int nread = 0; + for (int n = i + 1; n < cgraph->n_nodes && ok; ++n) { + const ggml_tensor * t = cgraph->nodes[n]; + if (!reads(t, d)) continue; + ++nread; + if (t->op == GGML_OP_VIEW || t->op == GGML_OP_RESHAPE) continue; + if (t->op == GGML_OP_MUL_MAT && t->src[0]->type != GGML_TYPE_F32 && + ggml_cuda_mmb_supported_mm(*cuda_ctx, t->src[0], t->src[1], t)) continue; + ok = false; + } + if (ok && nread > 0 && ggml_cuda_marks_readers_local(cgraph, d)) ggml_cuda_mmb_mark_bf16_only(*cuda_ctx, d); + } for (int i = 0; i < cgraph->n_nodes; ++i) { // the HC gate GEMM feeding only the fused mix keeps its BF16 epilogue const ggml_tensor * t = cgraph->nodes[i]; if (t->op != GGML_OP_MUL_MAT || t->src[0]->ne[0] != 320 || t->src[0]->ne[1] != 10240 || !ggml_cuda_mmb_supported_mm(*cuda_ctx, t->src[0], t->src[1], t)) continue; @@ -6396,7 +6879,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph auto reads = [](const ggml_tensor * t, const ggml_tensor * x) { for (int s = 0; s < GGML_MAX_SRC && t->src[s]; ++s) if (t->src[s] == x || t->src[s]->view_src == x) return true; return false; }; for (int i = 0; i < cgraph->n_nodes; ++i) { const ggml_tensor * t = cgraph->nodes[i]; - if (t->op == GGML_OP_MUL_MAT && (t->src[0]->type == GGML_TYPE_IQ4_NL || t->src[0]->type == GGML_TYPE_Q6_K) && ggml_cuda_mmb_supported_mm(*cuda_ctx, t->src[0], t->src[1], t)) ggml_cuda_mmb_shadow_prepare(*cuda_ctx, t->src[0]); + if (t->op == GGML_OP_MUL_MAT && (t->src[0]->type == GGML_TYPE_IQ4_NL || t->src[0]->type == GGML_TYPE_Q6_K || t->src[0]->type == GGML_TYPE_Q8_0) && ggml_cuda_mmb_supported_mm(*cuda_ctx, t->src[0], t->src[1], t)) ggml_cuda_mmb_shadow_prepare(*cuda_ctx, t->src[0]); } for (int i = 0; i < cgraph->n_nodes; ++i) { const ggml_tensor * glu = cgraph->nodes[i]; @@ -6472,12 +6955,27 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph } if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc)) { + for (int i = 0; i < cgraph->n_nodes; ++i) { + if (cgraph->nodes[i]->op != GGML_OP_MUL_MAT) continue; + ggml_cuda_idx_relu_sum_args args; + if (ggml_cuda_match_idx_gemm(*cuda_ctx, cgraph, i, args) > 0) { + const ggml_tensor * mm = cgraph->nodes[i]; + for (const ggml_tensor * in : { (const ggml_tensor *) mm->src[0], (const ggml_tensor *) mm->src[1], args.vis_tails, args.vis_starts }) { + if (!in) { continue; } + auto * root = const_cast<ggml_tensor *>(in->view_src ? in->view_src : in); + params->add_alloc_dep(params->user_data, root, args.dst); + } + } + } for (int i = 0; i < cgraph->n_nodes; ++i) { if (cgraph->nodes[i]->op != GGML_OP_UNARY) continue; ggml_cuda_idx_relu_sum_args args; if (ggml_cuda_match_idx_relu_sum(cgraph, i, args) > 0) { - auto * root = const_cast<ggml_tensor *>(args.score->view_src ? args.score->view_src : args.score); - params->add_alloc_dep(params->user_data, root, args.dst); + for (const ggml_tensor * in : { args.score, args.vis_tails, args.vis_starts }) { + if (!in) { continue; } + auto * root = const_cast<ggml_tensor *>(in->view_src ? in->view_src : in); + params->add_alloc_dep(params->user_data, root, args.dst); + } } } } @@ -6508,6 +7006,32 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph inputs[0] = pm.x; inputs[1] = pm.state; out = pm.out; } else if (ggml_cuda_gdn_conv_match_at_concat(cgraph, i, gm)) { inputs[0] = gm.x; inputs[1] = gm.state; out = gm.out; + + // The chunked GDN can elide the conv/SiLU and read x, history and taps at the GDN node instead. + // Keeping them only through the SiLU allows the scheduler to reuse x (and corrupt q/k/v) before + // gdn_chunk_prep_kernel reads it. Restrict the longer lifetime to the fused prefill pattern. + if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[cuda_ctx->device].cc) && + getenv("GGML_CUDA_DISABLE_GDN_CONV") == nullptr && + getenv("GGML_CUDA_DISABLE_GDN_QKNORM") == nullptr) { + int i_rms = -1; + for (int j = gm.conv_idx + 2; j < cgraph->n_nodes && j <= gm.conv_idx + 24; ++j) { + const ggml_tensor * t = cgraph->nodes[j]; + if (t->op == GGML_OP_RMS_NORM && t->src[0] && t->src[0]->view_src == gm.out) { + i_rms = j; + break; + } + } + int ig = -1; + const ggml_tensor * q_raw = nullptr, * k_raw = nullptr; + float eps = 0.0f, mul = 1.0f; + if (i_rms >= 0 && ggml_cuda_gdn_qk_norm_match(cgraph, i_rms, ig, q_raw, k_raw, eps, mul) > 0 && + ig >= 0 && cgraph->nodes[ig]->src[2] && cgraph->nodes[ig]->src[2]->view_src == gm.out) { + for (const ggml_tensor * input : {gm.x, gm.state, gm.w}) { + auto * root = const_cast<ggml_tensor *>(input->view_src ? input->view_src : input); + params->add_alloc_dep(params->user_data, root, cgraph->nodes[ig]); + } + } + } } for (const ggml_tensor * input : inputs) { if (input) { diff --git a/ggml/src/ggml-cuda/idx-relu-sum.cu b/ggml/src/ggml-cuda/idx-relu-sum.cu index 6cd833bcbfc3..8eef67d14b4e 100644 --- a/ggml/src/ggml-cuda/idx-relu-sum.cu +++ b/ggml/src/ggml-cuda/idx-relu-sum.cu @@ -1,8 +1,11 @@ #include "idx-relu-sum.cuh" // Preserve the head addition order of the RELU/CONT/ADD graph. +// With tails/starts it also applies the compact QSA visibility exactly as CPY(I32->F32), REPEAT, SUB, STEP, LOG +// and ADD compute it, so the result is bit-identical to the unfused graph. static __global__ void idx_relu_sum_f32(const float * __restrict__ src, float * __restrict__ dst, - const int n_blocks, const int heads) { + const int n_blocks, const int heads, + const int32_t * __restrict__ tails, const int32_t * __restrict__ starts) { const int b = blockIdx.x * blockDim.x + threadIdx.x; if (b >= n_blocks) return; const int64_t tt = blockIdx.y; // token within stream, streams stacked @@ -11,6 +14,11 @@ static __global__ void idx_relu_sum_f32(const float * __restrict__ src, float * for (int h = 1; h < heads; ++h) { acc = acc + fmaxf(p[(int64_t) h * n_blocks], 0.0f); } + if (tails) { + const float d = (float) tails[tt] - (float) starts[b]; + const float step = d > 0.0f; + acc = acc + logf(step); + } dst[tt * (int64_t) n_blocks + b] = acc; } @@ -20,6 +28,9 @@ void ggml_cuda_op_idx_relu_sum(ggml_backend_cuda_context & ctx, const ggml_cuda_ const int64_t rows = s->ne[2] * s->ne[3]; constexpr int threads = 256; const dim3 grid((n_blocks + threads - 1) / threads, (unsigned) rows, 1); - idx_relu_sum_f32<<<grid, threads, 0, ctx.stream()>>>((const float *) s->data, (float *) args.dst->data, n_blocks, args.heads); + const int32_t * tails = args.vis_tails ? (const int32_t *) args.vis_tails->data : nullptr; + const int32_t * starts = args.vis_starts ? (const int32_t *) args.vis_starts->data : nullptr; + idx_relu_sum_f32<<<grid, threads, 0, ctx.stream()>>>((const float *) s->data, (float *) args.dst->data, n_blocks, args.heads, + tails, starts); CUDA_CHECK(cudaGetLastError()); } diff --git a/ggml/src/ggml-cuda/idx-relu-sum.cuh b/ggml/src/ggml-cuda/idx-relu-sum.cuh index 04f902bac221..b796dbf254f5 100644 --- a/ggml/src/ggml-cuda/idx-relu-sum.cuh +++ b/ggml/src/ggml-cuda/idx-relu-sum.cuh @@ -4,5 +4,11 @@ struct ggml_cuda_idx_relu_sum_args { const ggml_tensor * score = nullptr; ggml_tensor * dst = nullptr; // [n_blocks, n_tps, n_stream] int heads = 0; + // optional fused compact visibility (single sequence): dst += log(step(float(tails[t]) - float(starts[b]))) + const ggml_tensor * vis_tails = nullptr; // I32 [n_tps] contiguous view of the QSA limits + const ggml_tensor * vis_starts = nullptr; // I32 [n_blocks] contiguous view of the QSA limits + // nodes of the visibility chain that the matcher checked itself (views/casts of the limits input); a caller that + // widens the fusion must leave them out of ggml_can_fuse_subgraph_ext + const ggml_tensor * vis_leafs[16] = {}; }; void ggml_cuda_op_idx_relu_sum(ggml_backend_cuda_context & ctx, const ggml_cuda_idx_relu_sum_args & args); diff --git a/ggml/src/ggml-cuda/mmb-quant.cuh b/ggml/src/ggml-cuda/mmb-quant.cuh index 6bb8812630d4..ba0bce882f11 100644 --- a/ggml/src/ggml-cuda/mmb-quant.cuh +++ b/ggml/src/ggml-cuda/mmb-quant.cuh @@ -160,14 +160,30 @@ __device__ __forceinline__ void mmb_decode_slice(const void * row, const int k0, for (int tid = lane; tid < 32; tid += 8) dequantize_iq3_xxs<float>(row, k0 / QK, out, tid); } else if constexpr (TYPE == GGML_TYPE_IQ3_S) { - const mmb_quant_slice out{dst, k0 % QK}; + // lane -> (il = lane>>1, ib = ib0 + (lane&1)): 8 consecutive weights, one 16-byte LDS store. + // Same arithmetic as dequantize_iq3_s (d * grid * sign in fp32, then RNE bf16) -> bit-identical. + const block_iq3_s * x = (const block_iq3_s *) row + k0 / QK; + const int ib0 = (k0 % QK) / 32, sub = lane & 1, il = lane >> 1, ib = ib0 + sub; + const uint16_t q2 = *(const uint16_t *)(x->qs + 8 * ib + 2 * il); + const int qh = x->qh[ib]; + const uint32_t g1 = iq3s_grid[(q2 & 0xff) | ((qh << (8 - 2 * il)) & 256)]; + const uint32_t g2 = iq3s_grid[(q2 >> 8) | ((qh << (7 - 2 * il)) & 256)]; + const float d = (float) x->d * (1 + 2 * ((x->scales[ib / 2] >> 4 * (ib % 2)) & 0xf)); + const int signs = x->signs[4 * ib + il]; + float v[8]; #pragma unroll - for (int tid = lane; tid < 32; tid += 8) dequantize_iq3_s<float>(row, k0 / QK, out, tid); + for (int j = 0; j < 4; ++j) { + v[j + 0] = d * (float)((g1 >> (8 * j)) & 0xff) * (signs & (1 << (j + 0)) ? -1.f : 1.f); + v[j + 4] = d * (float)((g2 >> (8 * j)) & 0xff) * (signs & (1 << (j + 4)) ? -1.f : 1.f); + } + uint4 o; o.x = mmb_pack2(v[0], v[1]); o.y = mmb_pack2(v[2], v[3]); o.z = mmb_pack2(v[4], v[5]); o.w = mmb_pack2(v[6], v[7]); + *(uint4 *)(dst + 32 * sub + 8 * il) = o; } else if constexpr (TYPE == GGML_TYPE_IQ4_XS) { + // decode only the two 32-blocks of this 64-wide K slice (tid = il*8 + ib): 1 call/lane instead of 4 const mmb_quant_slice out{dst, k0 % QK}; -#pragma unroll - for (int tid = lane; tid < 32; tid += 8) dequantize_iq4_xs<float>(row, k0 / QK, out, tid); + const int ib0 = (k0 % QK) / 32; + dequantize_iq4_xs<float>(row, k0 / QK, out, (lane >> 1) * 8 + ib0 + (lane & 1)); } else if constexpr (TYPE == GGML_TYPE_IQ4_NL) { #pragma unroll @@ -289,7 +305,8 @@ static void mmb_dispatch_quant(ggml_type type, Fn fn) { } } -__device__ __forceinline__ void mmb_dq_q4k_slice(uint4 q0, uint4 q1, uint4 meta, int slice, uint32_t * out) { +template <bool HALF = false> +__device__ __forceinline__ void mmb_dq_q4k_slice(uint4 q0, uint4 q1, uint4 meta, int slice, uint32_t * out, int half = 0) { const float d = mmb_h2f((uint16_t) meta.x), dm = mmb_h2f((uint16_t)(meta.x >> 16)); auto byte = [&](int i) { const uint32_t word = i < 4 ? meta.y : i < 8 ? meta.z : meta.w; return (word >> (8 * (i & 3))) & 255; }; auto scale_min = [&](int i, float & ds, float & ms) { @@ -301,12 +318,13 @@ __device__ __forceinline__ void mmb_dq_q4k_slice(uint4 q0, uint4 q1, uint4 meta, scale_min(2 * slice, d0, m0); scale_min(2 * slice + 1, d1, m1); const uint32_t words[8] = {q0.x, q0.y, q0.z, q0.w, q1.x, q1.y, q1.z, q1.w}; #pragma unroll - for (int j = 0; j < 8; ++j) { + for (int j = 0; j < (HALF ? 4 : 8); ++j) { + const int wj = HALF ? j + 4 * half : j; const uint32_t q = words[j]; - out[2*j] = mmb_pack2(d0 * (q & 15) - m0, d0 * ((q >> 8) & 15) - m0); - out[2*j + 1] = mmb_pack2(d0 * ((q >> 16) & 15) - m0, d0 * ((q >> 24) & 15) - m0); - out[16 + 2*j] = mmb_pack2(d1 * ((q >> 4) & 15) - m1, d1 * ((q >> 12) & 15) - m1); - out[16 + 2*j + 1] = mmb_pack2(d1 * ((q >> 20) & 15) - m1, d1 * (q >> 28) - m1); + out[2*wj] = mmb_pack2(d0 * (q & 15) - m0, d0 * ((q >> 8) & 15) - m0); + out[2*wj + 1] = mmb_pack2(d0 * ((q >> 16) & 15) - m0, d0 * ((q >> 24) & 15) - m0); + out[16 + 2*wj] = mmb_pack2(d1 * ((q >> 4) & 15) - m1, d1 * ((q >> 12) & 15) - m1); + out[16 + 2*wj + 1] = mmb_pack2(d1 * ((q >> 20) & 15) - m1, d1 * (q >> 28) - m1); } } diff --git a/ggml/src/ggml-cuda/mmb.cu b/ggml/src/ggml-cuda/mmb.cu index 25a6b04b606b..ee1be4240fa1 100644 --- a/ggml/src/ggml-cuda/mmb.cu +++ b/ggml/src/ggml-cuda/mmb.cu @@ -15,7 +15,13 @@ typedef float v8f __attribute__((ext_vector_type(8))); constexpr int MMB_BK = 64, MMB_NT = 256, MMB_LDS_STRIDE = MMB_BK + 8; __device__ __forceinline__ uint16_t mmb_f2bf(float f) { uint32_t u = __float_as_uint(f); u += 0x7fffu + ((u >> 16) & 1u); return (uint16_t)(u >> 16); } -__device__ __forceinline__ uint32_t mmb_pack2(float a, float b) { return (uint32_t)mmb_f2bf(a) | ((uint32_t)mmb_f2bf(b) << 16); } +// same RNE as mmb_f2bf on both halves, the two high halves joined by one v_perm_b32 (no mov_b16 + and_or). Unguarded +// like the other __builtin_amdgcn_perm uses in this file: device code here is AMD-only. +__device__ __forceinline__ uint32_t mmb_pack2(float a, float b) { + uint32_t ua = __float_as_uint(a), ub = __float_as_uint(b); + ua += 0x7fffu + ((ua >> 16) & 1u); ub += 0x7fffu + ((ub >> 16) & 1u); + return __builtin_amdgcn_perm(ub, ua, 0x07060302u); +} __constant__ int8_t mmb_kv_iq4nl[16] = {-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113}; __device__ __forceinline__ float mmb_h2f(uint16_t h) { return (float) __builtin_bit_cast(_Float16, h); } @@ -260,6 +266,7 @@ hc_gate_mix_kernel(const uint8_t * __restrict__ W, const uint16_t * __restrict__ const size_t wrow_bytes = mmb_row_bytes<WTYPE>(K); constexpr int B_ITEMS = (BN * 8) / MMB_NT; uint4 a0 = make_uint4(0,0,0,0), a1 = make_uint4(0,0,0,0); uint32_t a2 = 0; uint4 bst[B_ITEMS]; int brow[B_ITEMS]; + uint4 aw[WTYPE == 2 ? 8 : 1]; const uint8_t * arow = W; if (tid < BM) { const int c = tid / CH, i = tid - c * CH; arow = W + (size_t)(c * E + e0 + i) * wrow_bytes; } #pragma unroll @@ -268,12 +275,18 @@ hc_gate_mix_kernel(const uint8_t * __restrict__ W, const uint16_t * __restrict__ auto load_regs = [&](const int ks) { weight_ks = ks; if constexpr (WTYPE == 0) if (tid < BM) { const uint8_t * p = arow + (size_t)ks * 36; a0 = *(const uint4 *)(p); a1 = *(const uint4 *)(p + 16); a2 = *(const uint32_t *)(p + 32); } + if constexpr (WTYPE == 2) if (tid < BM) { const uint4 * p = (const uint4 *)(arow + (size_t)ks * MMB_BK * 2); +#pragma unroll + for (int q = 0; q < 8; ++q) aw[q] = p[q]; } #pragma unroll for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; const int off = (c & 7) * 8; bst[i] = (brow[i] >= 0) ? *(const uint4 *)(Lo + (size_t)brow[i] * K + ks * MMB_BK + off) : make_uint4(0,0,0,0); } }; auto store_lds = [&]() { if constexpr (WTYPE == 0) { if (tid < BM) mmb_dq_row36(a0, a1, a2, (uint32_t *)(As + tid * MMB_LDS_STRIDE)); } + else if constexpr (WTYPE == 2) { if (tid < BM) { +#pragma unroll + for (int q = 0; q < 8; ++q) *(uint4 *)(As + tid * MMB_LDS_STRIDE + q * 8) = aw[q]; } } else { constexpr int TYPE = WTYPE == 1 ? GGML_TYPE_Q8_0 : WTYPE - 32; for (int row = tid / 8; row < BM; row += MMB_NT / 8) { @@ -359,7 +372,7 @@ mmb_routed_kernel(const uint8_t * __restrict__ W, const size_t expert_bytes, con #endif } -template <int BM, int BN, int WTM, int WTN, int WTYPE, bool TAIL, typename XRowFn, typename DRowFn> +template <int BM, int BN, int WTM, int WTN, int WTYPE, bool TAIL, bool Q4K_HALF, typename XRowFn, typename DRowFn> __device__ __forceinline__ void mmb_tile_gemm_glu(const uint8_t * __restrict__ Wg, const uint8_t * __restrict__ Wu, const size_t wrow_bytes, const int a_rows, const uint16_t * __restrict__ Xh, const int K, XRowFn xrow, float * __restrict__ D, uint16_t * __restrict__ Dh, const bool store_f32, const int M, DRowFn drow, const int m0, const int n_cols, uint16_t * Ag, uint16_t * Au, uint16_t * Bs) { @@ -374,12 +387,36 @@ __device__ __forceinline__ void mmb_tile_gemm_glu(const uint8_t * __restrict__ W int brow[B_ITEMS]; #pragma unroll for (int i = 0; i < B_ITEMS; ++i) { const int c = tid + i * MMB_NT; brow[i] = xrow(c >> 3); } + // IQ3_S: prefetch the per-lane block fields into registers during the WMMA loop (8 lanes per row, 8 weights per lane), + // grid table in LDS; decode in store_lds. Same fp32 arithmetic as dequantize_iq3_s -> bit-identical. + constexpr bool IQ3 = WTYPE == 32 + GGML_TYPE_IQ3_S; + constexpr int Q3R = IQ3 ? (BM * 8) / MMB_NT : 1; + uint32_t q3g[Q3R][2], q3u[Q3R][2]; + __shared__ uint32_t q3grid[IQ3 ? 512 : 1]; + if constexpr (IQ3) { for (int i = tid; i < 512; i += MMB_NT) q3grid[i] = iq3s_grid[i]; } int weight_ks = 0; auto load_regs = [&](const int ks) { weight_ks = ks; + if constexpr (IQ3) { + const int l8 = tid & 7, sub = l8 & 1, il = l8 >> 1, ib = ((ks * MMB_BK) % QK_K) / 32 + sub; +#pragma unroll + for (int r = 0; r < Q3R; ++r) { + const int row = (tid >> 3) + r * (MMB_NT / 8); + if (row < a_rows) { +#pragma unroll + for (int h = 0; h < 2; ++h) { + const block_iq3_s * x = (const block_iq3_s *)((h ? Wu : Wg) + (size_t)row * wrow_bytes) + (ks * MMB_BK) / QK_K; + const uint32_t q2 = *(const uint16_t *)(x->qs + 8 * ib + 2 * il); + const uint32_t w0 = q2 | ((uint32_t)x->qh[ib] << 16) | ((uint32_t)x->signs[4 * ib + il] << 24); + const uint32_t w1 = (uint32_t)*(const uint16_t *)&x->d | ((uint32_t)((x->scales[ib / 2] >> 4 * (ib % 2)) & 0xf) << 16); + if (h) { q3u[r][0] = w0; q3u[r][1] = w1; } else { q3g[r][0] = w0; q3g[r][1] = w1; } + } + } else { q3g[r][0] = q3g[r][1] = q3u[r][0] = q3u[r][1] = 0u; } // d = 0 -> zero rows (never stored) + } + } #pragma unroll for (int i = 0; i < A_ITEMS; ++i) { - const int row = tid + i * MMB_NT; + const int row = Q4K_HALF ? (tid + i * MMB_NT) / 2 : tid + i * MMB_NT; if constexpr (WTYPE == 0) { if (row < BM && row < a_rows) { const uint8_t * pg = Wg + (size_t)row * wrow_bytes + (size_t)ks * 36; @@ -392,9 +429,11 @@ __device__ __forceinline__ void mmb_tile_gemm_glu(const uint8_t * __restrict__ W const uint8_t * pg = Wg + (size_t)row * wrow_bytes + (size_t)(ks / 4) * sizeof(block_q4_K); const uint8_t * pu = Wu + (size_t)row * wrow_bytes + (size_t)(ks / 4) * sizeof(block_q4_K); gm[i] = *(const uint4 *)pg; um[i] = *(const uint4 *)pu; - const int offset = 16 + (ks & 3) * 32; - g0[i] = *(const uint4 *)(pg + offset); g1[i] = *(const uint4 *)(pg + offset + 16); - u0[i] = *(const uint4 *)(pu + offset); u1[i] = *(const uint4 *)(pu + offset + 16); + const int offset = 16 + (ks & 3) * 32 + (Q4K_HALF ? (tid & 1) * 16 : 0); + g0[i] = *(const uint4 *)(pg + offset); u0[i] = *(const uint4 *)(pu + offset); + if constexpr (!Q4K_HALF) { + g1[i] = *(const uint4 *)(pg + offset + 16); u1[i] = *(const uint4 *)(pu + offset + 16); + } } else { gm[i] = um[i] = g0[i] = g1[i] = u0[i] = u1[i] = make_uint4(0,0,0,0); } } } @@ -405,7 +444,33 @@ __device__ __forceinline__ void mmb_tile_gemm_glu(const uint8_t * __restrict__ W } }; auto store_lds = [&]() { - if constexpr (WTYPE != 0 && WTYPE != 32 + GGML_TYPE_Q4_K) { + if constexpr (IQ3) { + const int l8 = tid & 7, sub = l8 & 1, il = l8 >> 1; +#pragma unroll + for (int r = 0; r < Q3R; ++r) { + const int row = (tid >> 3) + r * (MMB_NT / 8); +#pragma unroll + for (int h = 0; h < 2; ++h) { + // branchless (padding rows decode to zero); d * grid is exact in fp32, rounded to bf16 (RNE) and packed + // with one v_perm per pair; the signs are applied after packing as an XOR of the bf16 sign bits + // (RNE is sign-symmetric: bf16(-v) == bf16(v) ^ 0x8000), bit-identical to dequantize_iq3_s + const uint32_t w0 = h ? q3u[r][0] : q3g[r][0], w1 = h ? q3u[r][1] : q3g[r][1]; + const uint32_t qh = (w0 >> 16) & 0xff, sg = w0 >> 24; + const uint32_t g1 = q3grid[(w0 & 0xff) | ((qh << (8 - 2 * il)) & 256)]; + const uint32_t g2 = q3grid[((w0 >> 8) & 0xff) | ((qh << (7 - 2 * il)) & 256)]; + const float d = __half2float(__ushort_as_half((unsigned short)(w1 & 0xffff))) * (1 + 2 * (int)(w1 >> 16)); + auto rb = [](const float x) { const uint32_t u = __float_as_uint(x); return u + 0x7fffu + ((u >> 16) & 1u); }; + auto pk = [&](const uint32_t g, const int j) { + return __builtin_amdgcn_perm(rb(d * (float)((g >> (8 * j + 8)) & 0xff)), rb(d * (float)((g >> (8 * j)) & 0xff)), 0x07060302u); }; + uint4 o; + o.x = pk(g1, 0) ^ ((sg & 1u) << 15 | (sg & 2u) << 30); + o.y = pk(g1, 2) ^ ((sg & 4u) << 13 | (sg & 8u) << 28); + o.z = pk(g2, 0) ^ ((sg & 16u) << 11 | (sg & 32u) << 26); + o.w = pk(g2, 2) ^ ((sg & 64u) << 9 | (sg & 128u) << 24); + *(uint4 *)((h ? Au : Ag) + row * MMB_LDS_STRIDE + 32 * sub + 8 * il) = o; + } + } + } else if constexpr (WTYPE != 0 && WTYPE != 32 + GGML_TYPE_Q4_K) { constexpr int LOAD_TYPE = WTYPE == 1 ? 32 + GGML_TYPE_Q8_0 : WTYPE; mmb_load_quant_tile<LOAD_TYPE, BM, MMB_LDS_STRIDE>(Wg, wrow_bytes, a_rows, weight_ks, Ag); mmb_load_quant_tile<LOAD_TYPE, BM, MMB_LDS_STRIDE>(Wu, wrow_bytes, a_rows, weight_ks, Au); @@ -413,14 +478,14 @@ __device__ __forceinline__ void mmb_tile_gemm_glu(const uint8_t * __restrict__ W #pragma unroll for (int i = 0; i < A_ITEMS; ++i) { - const int row = tid + i * MMB_NT; + const int row = Q4K_HALF ? (tid + i * MMB_NT) / 2 : tid + i * MMB_NT; if (row < BM) { if constexpr (WTYPE == 0) { mmb_dq_row36(g0[i], g1[i], g2[i], (uint32_t *)(Ag + row * MMB_LDS_STRIDE)); mmb_dq_row36(u0[i], u1[i], u2[i], (uint32_t *)(Au + row * MMB_LDS_STRIDE)); } else if constexpr (WTYPE == 32 + GGML_TYPE_Q4_K) { - mmb_dq_q4k_slice(g0[i], g1[i], gm[i], weight_ks & 3, (uint32_t *)(Ag + row * MMB_LDS_STRIDE)); - mmb_dq_q4k_slice(u0[i], u1[i], um[i], weight_ks & 3, (uint32_t *)(Au + row * MMB_LDS_STRIDE)); + mmb_dq_q4k_slice<Q4K_HALF>(g0[i], Q4K_HALF ? g0[i] : g1[i], gm[i], weight_ks & 3, (uint32_t *)(Ag + row * MMB_LDS_STRIDE), tid & 1); + mmb_dq_q4k_slice<Q4K_HALF>(u0[i], Q4K_HALF ? u0[i] : u1[i], um[i], weight_ks & 3, (uint32_t *)(Au + row * MMB_LDS_STRIDE), tid & 1); } } } @@ -438,7 +503,7 @@ __device__ __forceinline__ void mmb_tile_gemm_glu(const uint8_t * __restrict__ W #pragma unroll for (int j = 0; j < TN; ++j) jact[j] = !TAIL || (wn * WTN + j * 16) < n_cols; const int nks = K / MMB_BK; - load_regs(0); store_lds(); __syncthreads(); + load_regs(0); if constexpr (IQ3) __syncthreads(); store_lds(); __syncthreads(); for (int ks = 0; ks < nks; ++ks) { if (ks + 1 < nks) load_regs(ks + 1); #pragma unroll @@ -480,7 +545,7 @@ __device__ __forceinline__ void mmb_tile_gemm_glu(const uint8_t * __restrict__ W } } -template <int BM, int BN, int WTM, int WTN, int WTYPE = 0> +template <int BM, int BN, int WTM, int WTN, int WTYPE = 0, bool Q4K_HALF = false> __global__ void __launch_bounds__(MMB_NT, 2) mmb_routed_glu_kernel(const uint8_t * __restrict__ Wg, const uint8_t * __restrict__ Wu, const size_t expert_bytes, const uint16_t * __restrict__ Xh, float * __restrict__ D, uint16_t * __restrict__ Dh, const bool store_f32, @@ -498,7 +563,7 @@ mmb_routed_glu_kernel(const uint8_t * __restrict__ Wg, const uint8_t * __restric const int r0 = bounds[e] + jt * BN, cnt = bounds[e + 1] - r0; const int m0 = blockIdx.x * BM; const size_t wrow_bytes = mmb_row_bytes<WTYPE>(K); - mmb_tile_gemm_glu<BM, BN, WTM, WTN, WTYPE, true>(Wg + (size_t)e * expert_bytes + (size_t)m0 * wrow_bytes, Wu + (size_t)e * expert_bytes + (size_t)m0 * wrow_bytes, wrow_bytes, M - m0, Xh, K, + mmb_tile_gemm_glu<BM, BN, WTM, WTN, WTYPE, true, Q4K_HALF>(Wg + (size_t)e * expert_bytes + (size_t)m0 * wrow_bytes, Wu + (size_t)e * expert_bytes + (size_t)m0 * wrow_bytes, wrow_bytes, M - m0, Xh, K, [&](int i) { return (i < cnt) ? ids_src[r0 + i] : -1; }, D, Dh, store_f32, M, [&](int i) { return (i < cnt) ? ids_dst[r0 + i] : -1; }, m0, cnt, Ag, Au, Bs); #endif } @@ -507,9 +572,15 @@ mmb_routed_glu_kernel(const uint8_t * __restrict__ Wg, const uint8_t * __restric __device__ __forceinline__ void mmb_split2(float x, uint16_t & hi, uint16_t & lo) { hi = __builtin_bit_cast(uint16_t, (_Float16) x); lo = __builtin_bit_cast(uint16_t, (_Float16) (x - (float) __builtin_bit_cast(_Float16, hi))); } -template <int BM, int BN, int WTM, int WTN, bool TWO, bool XSPLIT> +// EPI == 1 (qwen4exp QSA indexer scorer): the T columns are (query, head) pairs with 4 heads, and D is [M, T/4] with +// D[q*M + m] = ((relu(h0) + relu(h1)) + relu(h2)) + relu(h3) (+ log(step(float(tails[q]) - float(starts[m]))) when +// tails != nullptr): the exact expression the separate GEMM, idx_relu_sum and visibility chain evaluate. +template <int BM, int BN, int WTM, int WTN, bool TWO, bool XSPLIT, int EPI = 0> __global__ void __launch_bounds__(MMB_NT, 2) -mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, float * __restrict__ D, const int M, const int K, const int T) { +mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, float * __restrict__ D, const int M, const int K, const int T, + const float * __restrict__ W2 = nullptr, float * __restrict__ D2 = nullptr, const int M1 = 0, + const int32_t * __restrict__ tails = nullptr, const int32_t * __restrict__ starts = nullptr) { + // W2 != nullptr: two GEMMs on the same X, rows [0, M1) from W -> D (ld M1) and rows [M1, M) from W2 -> D2 (ld M - M1) #if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) // WMMA (wave32, 16x16x16 bf16/f16 into f32) exists only on RDNA3; the host gate keeps other devices off this path NO_DEVICE_CODE; @@ -527,18 +598,40 @@ mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, #pragma unroll for (int e = 0; e < 8; ++e) acc[i][j][e] = 0.f; constexpr int A_CH = BM * BKs / 4, B_CH = BN * BKs / 4; - for (int k0 = 0; k0 < K; k0 += BKs) { - for (int idx = tid; idx < A_CH; idx += MMB_NT) { const int row = idx >> 3, c4 = (idx & 7) * 4; const int m = m0 + row; - float4 v = make_float4(0.f,0.f,0.f,0.f); if (m < M) v = *(const float4 *)(W + (size_t) m * K + k0 + c4); + // one register stage: the next K slice is fetched while the current one is multiplied (unguarded loads with + // clamped rows, zeroed at the LDS store: a divergent guard would make the compiler drain them at the join) + constexpr int A_IT = (A_CH + MMB_NT - 1) / MMB_NT, B_IT = (B_CH + MMB_NT - 1) / MMB_NT; + static_assert(A_CH % MMB_NT == 0 && B_CH % MMB_NT == 0, "f32split staging assumes whole passes"); + float4 ra[A_IT], rb[B_IT]; + auto gload = [&](const int k0) { +#pragma unroll + for (int i = 0; i < A_IT; ++i) { const int idx = tid + i * MMB_NT, row = idx >> 3, c4 = (idx & 7) * 4; + const int gr = min(m0 + row, M - 1); + const float * wr = (W2 && gr >= M1) ? W2 + (size_t) (gr - M1) * K : W + (size_t) gr * K; + ra[i] = *(const float4 *)(wr + k0 + c4); } +#pragma unroll + for (int i = 0; i < B_IT; ++i) { const int idx = tid + i * MMB_NT, row = idx >> 3, c4 = (idx & 7) * 4; + rb[i] = *(const float4 *)(X + (size_t) min(t0 + row, T - 1) * K + k0 + c4); } + }; + auto lstore = [&]() { +#pragma unroll + for (int i = 0; i < A_IT; ++i) { const int idx = tid + i * MMB_NT, row = idx >> 3, c4 = (idx & 7) * 4; + const float4 v = m0 + row < M ? ra[i] : make_float4(0.f,0.f,0.f,0.f); uint16_t h[4], l[4]; mmb_split2(v.x,h[0],l[0]); mmb_split2(v.y,h[1],l[1]); mmb_split2(v.z,h[2],l[2]); mmb_split2(v.w,h[3],l[3]); *(uint2 *)(Ah + row * LS + c4) = make_uint2((uint32_t)h[0] | ((uint32_t)h[1] << 16), (uint32_t)h[2] | ((uint32_t)h[3] << 16)); if constexpr (!TWO) *(uint2 *)(Al + row * LS + c4) = make_uint2((uint32_t)l[0] | ((uint32_t)l[1] << 16), (uint32_t)l[2] | ((uint32_t)l[3] << 16)); } - for (int idx = tid; idx < B_CH; idx += MMB_NT) { const int row = idx >> 3, c4 = (idx & 7) * 4; const int t = t0 + row; - float4 v = make_float4(0.f,0.f,0.f,0.f); if (t < T) v = *(const float4 *)(X + (size_t) t * K + k0 + c4); +#pragma unroll + for (int i = 0; i < B_IT; ++i) { const int idx = tid + i * MMB_NT, row = idx >> 3, c4 = (idx & 7) * 4; + const float4 v = t0 + row < T ? rb[i] : make_float4(0.f,0.f,0.f,0.f); uint16_t h[4], l[4]; mmb_split2(v.x,h[0],l[0]); mmb_split2(v.y,h[1],l[1]); mmb_split2(v.z,h[2],l[2]); mmb_split2(v.w,h[3],l[3]); *(uint2 *)(Bh + row * LS + c4) = make_uint2((uint32_t)h[0] | ((uint32_t)h[1] << 16), (uint32_t)h[2] | ((uint32_t)h[3] << 16)); if constexpr (XSPLIT) *(uint2 *)(Bl + row * LS + c4) = make_uint2((uint32_t)l[0] | ((uint32_t)l[1] << 16), (uint32_t)l[2] | ((uint32_t)l[3] << 16)); } + }; + gload(0); + for (int k0 = 0; k0 < K; k0 += BKs) { + lstore(); __syncthreads(); + gload(min(k0 + BKs, K - BKs)); const int r = lane & 15; #pragma unroll for (int kk = 0; kk < BKs; kk += 16) { @@ -563,12 +656,173 @@ mmb_f32split_kernel(const float * __restrict__ W, const float * __restrict__ X, __syncthreads(); } const int cm = lane & 15, cn = lane >> 4; + if constexpr (EPI == 1) { + // lane holds t = base + 4p + cn (e = 2p) and base + 4p + 2 + cn (e = 2p + 1): heads cn and 2 + cn of query + // base/4 + p; lane ^ 16 holds the other two heads of the same (m, query). The shuffles run on every lane. + const int Q = T >> 2; +#pragma unroll + for (int i = 0; i < TM; ++i) { const int m = m0 + wm * WTM + i * 16 + cm; + const float sm = (tails && m < M) ? (float) starts[m] : 0.0f; +#pragma unroll + for (int j = 0; j < TN; ++j) { + float o[8]; +#pragma unroll + for (int e = 0; e < 8; ++e) o[e] = __shfl_xor(acc[i][j][e], 16, 32); +#pragma unroll + for (int p = 0; p < 4; ++p) { + const float h0 = cn == 0 ? acc[i][j][2 * p] : o[2 * p]; + const float h1 = cn == 0 ? o[2 * p] : acc[i][j][2 * p]; + const float h2 = cn == 0 ? acc[i][j][2 * p + 1] : o[2 * p + 1]; + const float h3 = cn == 0 ? o[2 * p + 1] : acc[i][j][2 * p + 1]; + float s = fmaxf(h0, 0.0f); + s = s + fmaxf(h1, 0.0f); + s = s + fmaxf(h2, 0.0f); + s = s + fmaxf(h3, 0.0f); + const int q = ((t0 + wn * WTN + j * 16) >> 2) + p; + if ((p >> 1) == cn && m < M && q < Q) { + if (tails) { + const float d = (float) tails[q] - sm; + const float step = d > 0.0f; + s = s + logf(step); + } + D[(size_t) q * M + m] = s; + } + } + } + } + return; + } #pragma unroll for (int i = 0; i < TM; ++i) { const int m = m0 + wm * WTM + i * 16 + cm; if (m >= M) continue; #pragma unroll for (int j = 0; j < TN; ++j) #pragma unroll - for (int e = 0; e < 8; ++e) { const int t = t0 + wn * WTN + j * 16 + 2 * e + cn; if (t < T) D[(size_t) t * M + m] = acc[i][j][e]; } } + for (int e = 0; e < 8; ++e) { const int t = t0 + wn * WTN + j * 16 + 2 * e + cn; if (t < T) { + if (W2) { if (m < M1) D[(size_t) t * M1 + m] = acc[i][j][e]; else D2[(size_t) t * (M - M1) + (m - M1)] = acc[i][j][e]; } + else D[(size_t) t * M + m] = acc[i][j][e]; } } } +#endif +} + + +// qwen4exp QSA indexer scorer with the keys stationary. mmb_f32split_kernel<..., EPI=1> re-reads its key tile for every +// 128-column tile of the 2048 (query, head) columns, which spills the ~32 MB of pooled F32 keys per layer (250k depth) +// out of the Infinity Cache. Here each workgroup rounds its BM key rows to F16 into LDS once and walks all T columns, +// staging each 128-column chunk of the queries (pre-rounded to F16 once per call) in LDS. Every output tile issues the +// same WMMAs on the same F16 fragments in the same K order as the split kernel (TWO, !XSPLIT: hi parts only), and the +// epilogue computes the same values, so the result matches it bit for bit. +__global__ void mmb_f32_to_f16_kernel(const float * __restrict__ x, uint16_t * __restrict__ y, const int64_t n) { + const int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x; + if (i < n) y[i] = __builtin_bit_cast(uint16_t, (_Float16) x[i]); +} + +template <int BM, int BN, int WAVES_M> +__global__ void __launch_bounds__(MMB_NT, 1) +mmb_idx_score_kernel(const float * __restrict__ W, const uint16_t * __restrict__ Xh, float * __restrict__ D, + const int M, const int K, const int T, const int32_t * __restrict__ tails, const int32_t * __restrict__ starts) { +#if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) + NO_DEVICE_CODE; +#else + constexpr int KMAX = 128, LS = KMAX + 8; + constexpr int WAVES_N = MMB_NT / 32 / WAVES_M, WTM = BM / WAVES_M, WTN = BN / WAVES_N, TM = WTM / 16, TN = WTN / 16; + static_assert(WAVES_M * WAVES_N * 32 == MMB_NT && TM >= 1 && TN >= 1 && WTM % 16 == 0 && WTN % 16 == 0, "tile must use every wave"); + __shared__ __align__(16) uint16_t Ah[BM * LS]; + __shared__ __align__(16) uint16_t Bh[BN * LS]; + const int tid = threadIdx.x, lane = tid & 31, wave = tid >> 5, wm = wave % WAVES_M, wn = wave / WAVES_M; + const int m0 = blockIdx.x * BM; + const int K4 = K / 4, K8 = K / 8; + + // keys -> F16 (the hi part of mmb_split2), rows past M are zero + for (int idx = tid; idx < BM * K4; idx += MMB_NT) { + const int row = idx / K4, c4 = (idx % K4) * 4; + const float4 v = m0 + row < M ? *(const float4 *)(W + (size_t) (m0 + row) * K + c4) : make_float4(0.f, 0.f, 0.f, 0.f); + const uint32_t h0 = __builtin_bit_cast(uint16_t, (_Float16) v.x), h1 = __builtin_bit_cast(uint16_t, (_Float16) v.y); + const uint32_t h2 = __builtin_bit_cast(uint16_t, (_Float16) v.z), h3 = __builtin_bit_cast(uint16_t, (_Float16) v.w); + *(uint2 *)(Ah + row * LS + c4) = make_uint2(h0 | (h1 << 16), h2 | (h3 << 16)); + } + + // query chunks: BN rows x K halfs, 8 halfs per load; columns past T are zero + constexpr int B_IT = BN * KMAX / 8 / MMB_NT; + uint4 rb[B_IT]; + const int b_loads = BN * K8; + auto gload = [&](const int t0) { +#pragma unroll + for (int it = 0; it < B_IT; ++it) { + const int idx = tid + it * MMB_NT; + const int row = idx / K8, c8 = (idx % K8) * 8; + rb[it] = (idx < b_loads && t0 + row < T) ? *(const uint4 *)(Xh + (size_t) (t0 + row) * K + c8) : make_uint4(0, 0, 0, 0); + } + }; + auto lstore = [&]() { +#pragma unroll + for (int it = 0; it < B_IT; ++it) { + const int idx = tid + it * MMB_NT; + if (idx < b_loads) { const int row = idx / K8, c8 = (idx % K8) * 8; *(uint4 *)(Bh + row * LS + c8) = rb[it]; } + } + }; + + const int r = lane & 15, cm = lane & 15, cn = lane >> 4; + const int Q = T >> 2; + float sm[TM]; +#pragma unroll + for (int i = 0; i < TM; ++i) { const int m = m0 + wm * WTM + i * 16 + cm; sm[i] = (tails && m < M) ? (float) starts[m] : 0.0f; } + + gload(0); + for (int t0 = 0; t0 < T; t0 += BN) { + __syncthreads(); // the previous chunk is consumed (and the keys are stored on the first pass) + lstore(); + __syncthreads(); + if (t0 + BN < T) gload(t0 + BN); + v8f acc[TM][TN]; +#pragma unroll + for (int i = 0; i < TM; ++i) +#pragma unroll + for (int j = 0; j < TN; ++j) +#pragma unroll + for (int e = 0; e < 8; ++e) acc[i][j][e] = 0.f; + for (int k0 = 0; k0 < K; k0 += 16) { + v16s ah[TM], bh[TN]; +#pragma unroll + for (int i = 0; i < TM; ++i) { const int off = (wm * WTM + i * 16 + r) * LS + k0; + ah[i] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Ah + off), *(const uint4 *)(Ah + off + 8)}); } +#pragma unroll + for (int j = 0; j < TN; ++j) { const int off = (wn * WTN + j * 16 + r) * LS + k0; + bh[j] = __builtin_bit_cast(v16s, (uint4[2]){*(const uint4 *)(Bh + off), *(const uint4 *)(Bh + off + 8)}); } +#pragma unroll + for (int i = 0; i < TM; ++i) +#pragma unroll + for (int j = 0; j < TN; ++j) acc[i][j] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(bh[j], ah[i], acc[i][j]); + } + // epilogue of mmb_f32split_kernel<..., EPI=1> +#pragma unroll + for (int i = 0; i < TM; ++i) { const int m = m0 + wm * WTM + i * 16 + cm; +#pragma unroll + for (int j = 0; j < TN; ++j) { + float o[8]; +#pragma unroll + for (int e = 0; e < 8; ++e) o[e] = __shfl_xor(acc[i][j][e], 16, 32); +#pragma unroll + for (int p = 0; p < 4; ++p) { + const float h0 = cn == 0 ? acc[i][j][2 * p] : o[2 * p]; + const float h1 = cn == 0 ? o[2 * p] : acc[i][j][2 * p]; + const float h2 = cn == 0 ? acc[i][j][2 * p + 1] : o[2 * p + 1]; + const float h3 = cn == 0 ? o[2 * p + 1] : acc[i][j][2 * p + 1]; + float s = fmaxf(h0, 0.0f); + s = s + fmaxf(h1, 0.0f); + s = s + fmaxf(h2, 0.0f); + s = s + fmaxf(h3, 0.0f); + const int q = ((t0 + wn * WTN + j * 16) >> 2) + p; + if ((p >> 1) == cn && m < M && q < Q) { + if (tails) { + // log(step(d)) is exactly 0 or -inf, and s >= +0, so this is s + logf(step) + const float d = (float) tails[q] - sm[i]; + s = d > 0.0f ? s + 0.0f : -INFINITY; + } + D[(size_t) q * M + m] = s; + } + } + } + } + } #endif } @@ -671,6 +925,27 @@ __global__ void mmb_dq_q6k_bf16_kernel(const uint8_t * __restrict__ W, uint16_t } } +__global__ void mmb_dq_q8_0_bf16_kernel(const uint8_t * __restrict__ W, uint16_t * __restrict__ out, const size_t nblocks) { + const size_t b = (size_t) blockIdx.x * blockDim.x + threadIdx.x; + if (b >= nblocks) return; + const uint8_t * p = W + b * 34; + const float d = mmb_h2f((uint16_t)(p[0] | (p[1] << 8))); + uint32_t * o = (uint32_t *) (out + b * 32); +#pragma unroll + for (int j = 0; j < 16; ++j) o[j] = mmb_pack2(d * (float)(int8_t) p[2 + 2 * j], d * (float)(int8_t) p[3 + 2 * j]); +} + +// HC Q8_0 matrices (hc_*_down [10240 -> 320], hc_*_up [320 -> 10240]) get a BF16 shadow: their GEMMs re-decode the +// whole matrix per token tile. MMB_SHADOW_Q8=0 disables. +static bool mmb_is_hc_q8(const ggml_tensor * w) { + static const bool on = getenv("MMB_SHADOW_Q8") ? atoi(getenv("MMB_SHADOW_Q8")) != 0 : true; + // HC down [10240 -> 320] reads its weights once per token tile: Q8_0 (68 B / 64 weights) beats the BF16 shadow + // (128 B) there, so only the HC up / gate weights get a shadow unless MMB_HCD_SHADOW=1 + static const bool down = getenv("MMB_HCD_SHADOW") ? atoi(getenv("MMB_HCD_SHADOW")) != 0 : false; + if (!on || !w || w->type != GGML_TYPE_Q8_0 || w->op != GGML_OP_NONE || !w->data || !w->buffer || w->ne[2] != 1 || w->ne[3] != 1 || !ggml_is_contiguous(w)) return false; + return (down && w->ne[0] >= 4096 && w->ne[1] <= 384) || (w->ne[0] <= 384 && w->ne[1] >= 4096); +} + __global__ void mmb_dq_iq4nl_bf16_kernel(const uint8_t * __restrict__ W, uint16_t * __restrict__ out, const size_t nblocks) { const size_t b = (size_t) blockIdx.x * blockDim.x + threadIdx.x; if (b >= nblocks) return; @@ -717,7 +992,7 @@ int mmb_tall_mode(){ return 2; } bool mmb_tall() { return mmb_tall_mode() != 0; } bool mmb_gatemix_flag() { return true; } bool mmb_down16_flag() { return true; } -bool mmb_glu() { return true; } +bool mmb_glu() { static const bool v = getenv("MMB_GLU") ? atoi(getenv("MMB_GLU")) != 0 : true; return v; } } // namespace @@ -801,18 +1076,78 @@ bool ggml_cuda_mmb_supported_mmid(ggml_backend_cuda_context & ctx, const ggml_te return true; } +// geometry ggml_cuda_mul_mat_mmb picks for an F32 weight with M > 64; the fused scorer only takes 0 and 5 +static int mmb_f32_tile_for(int M) { + static const int FT_env = getenv("MMB_F32_TILE") ? atoi(getenv("MMB_F32_TILE")) : -1; + return FT_env >= 0 ? FT_env : (M <= 1024 ? 5 : 0); +} + +bool ggml_cuda_mmb_idx_score_supported(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, + const ggml_tensor * mm, int heads) { + if (heads != 4 || src0->type != GGML_TYPE_F32 || !ggml_cuda_mmb_supported_mm(ctx, src0, src1, mm)) return false; + const int64_t M = src0->ne[1], T = src1->ne[1] * src1->ne[2] * src1->ne[3]; + if (M <= 64 || T % 4 != 0 || src1->ne[2] != 1 || src1->ne[3] != 1) return false; + const int FT = mmb_f32_tile_for((int) M); + return FT == 0 || FT == 5; +} + +void ggml_cuda_mmb_idx_score(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst, + const int32_t * tails, const int32_t * starts) { + cudaStream_t stream = ctx.stream(); + const int K = (int) src0->ne[0], M = (int) src0->ne[1]; + const int T = (int) (src1->ne[1] * src1->ne[2] * src1->ne[3]); + const float * W = (const float *) src0->data, * X = (const float *) src1->data; float * D = (float *) dst->data; + // keys-stationary scorer for the wide case (MMB_IDX_SCORE_V2=0 keeps the split kernel; same result) + static const bool v2 = getenv("MMB_IDX_SCORE_V2") == nullptr || atoi(getenv("MMB_IDX_SCORE_V2")) != 0; + if (v2 && mmb_f32_tile_for(M) == 0 && K <= 128 && K % 16 == 0) { + ggml_cuda_pool_alloc<uint16_t> xh(ctx.pool(), (size_t) T * K); + const int64_t n = (int64_t) T * K; + mmb_f32_to_f16_kernel<<<(unsigned) ((n + 255) / 256), 256, 0, stream>>>(X, xh.get(), n); + // 64 key rows x 64 query columns per step (2 x 4 waves): fastest of the shapes tried on gfx1151 at 250k depth + mmb_idx_score_kernel<64, 64, 2><<<(M + 63) / 64, MMB_NT, 0, stream>>>(W, xh.get(), D, M, K, T, tails, starts); + CUDA_CHECK(cudaGetLastError()); + return; + } + if (mmb_f32_tile_for(M) == 5) { + dim3 g((M + 31) / 32, (T + 63) / 64); + mmb_f32split_kernel<32, 64, 16, 16, true, false, 1><<<g, MMB_NT, 0, stream>>>(W, X, D, M, K, T, nullptr, nullptr, 0, tails, starts); + } else { + dim3 g((M + 127) / 128, (T + 127) / 128); + mmb_f32split_kernel<128, 128, 32, 64, true, false, 1><<<g, MMB_NT, 0, stream>>>(W, X, D, M, K, T, nullptr, nullptr, 0, tails, starts); + } + CUDA_CHECK(cudaGetLastError()); +} + void ggml_cuda_mul_mat_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) { cudaStream_t stream = ctx.stream(); const int K = (int) src0->ne[0], M = (int) src0->ne[1]; const int T = (int) (src1->ne[1] * src1->ne[2] * src1->ne[3]); if (src0->type == GGML_TYPE_F32) { const float * W = (const float *) src0->data, * X = (const float *) src1->data; float * D = (float *) dst->data; - if (M <= 64) { + // MMB_F32_TILE narrows the M>64 F32 GEMM (e.g. the M=512 MoE router) for more blocks per ubatch chunk. + // 0 keeps master's 128x128 tile. Valid geometries satisfy (8/WAVES_M)*WTN == BN (the kernel's static_assert). + // Default: 5 (32x64) for M <= 1024 (the M=512 router: 87 -> 77 ms per pp4096 on gfx1151, about -0.4% GPU time), + // 128x128 above, where the narrow tile would reload the activations ~3x more per output. Every geometry keeps the + // per-output K order, so the output is the same. + static const int FT_env = getenv("MMB_F32_TILE") ? atoi(getenv("MMB_F32_TILE")) : -1; + const int FT = FT_env >= 0 ? FT_env : (M <= 1024 ? 5 : 0); + // MMB_F32_SMALL: M <= 64 (ssm_alpha / ssm_beta [2560 -> 48]) launches only T/128 blocks at 64x128 (32 at 4096 tokens + // on 40 CUs); narrower token tiles give 2-4x the blocks with the same per-output K order (bit-identical) + static const int FS = getenv("MMB_F32_SMALL") ? atoi(getenv("MMB_F32_SMALL")) : 2; + if (M <= 64 && FS == 2) { + dim3 grid((M + 63) / 64, (T + 31) / 32); + mmb_f32split_kernel<64, 32, 16, 16, true, false><<<grid, MMB_NT, 0, stream>>>(W, X, D, M, K, T); + } else if (M <= 64 && FS == 1) { + dim3 grid((M + 63) / 64, (T + 63) / 64); + mmb_f32split_kernel<64, 64, 16, 32, true, false><<<grid, MMB_NT, 0, stream>>>(W, X, D, M, K, T); + } else if (M <= 64) { dim3 grid((M + 63) / 64, (T + 127) / 128); mmb_f32split_kernel<64, 128, 16, 64, true, false><<<grid, MMB_NT, 0, stream>>>(W, X, D, M, K, T); - } else { - dim3 grid((M + 127) / 128, (T + 127) / 128); - mmb_f32split_kernel<128, 128, 32, 64, true, false><<<grid, MMB_NT, 0, stream>>>(W, X, D, M, K, T); + } else if (FT == 1) { dim3 g((M + 63) / 64, (T + 127) / 128); mmb_f32split_kernel< 64, 128, 32, 32, true, false><<<g, MMB_NT, 0, stream>>>(W, X, D, M, K, T); } + else if (FT == 4) { dim3 g((M + 31) / 32, (T + 127) / 128); mmb_f32split_kernel< 32, 128, 32, 16, true, false><<<g, MMB_NT, 0, stream>>>(W, X, D, M, K, T); } + else if (FT == 5) { dim3 g((M + 31) / 32, (T + 63) / 64); mmb_f32split_kernel< 32, 64, 16, 16, true, false><<<g, MMB_NT, 0, stream>>>(W, X, D, M, K, T); } + else if (FT == 6) { dim3 g((M + 63) / 64, (T + 63) / 64); mmb_f32split_kernel< 64, 64, 32, 16, true, false><<<g, MMB_NT, 0, stream>>>(W, X, D, M, K, T); } + else { dim3 g((M + 127) / 128,(T + 127) / 128); mmb_f32split_kernel<128, 128, 32, 64, true, false><<<g, MMB_NT, 0, stream>>>(W, X, D, M, K, T); } CUDA_CHECK(cudaGetLastError()); return; } @@ -825,6 +1160,28 @@ void ggml_cuda_mul_mat_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * CUDA_CHECK(cudaGetLastError()); return; } + // Q8_0 HC down [10240 -> 320]: at 128x128 tiles only 3x(T/128) blocks with a K=10240 loop (occupancy-bound). + // Narrower tiles keep the per-output K accumulation order (bit-identical) but launch 3-10x more blocks. + // MMB_HCD_TILE: 0 = generic path, 1 <64,128,32,32>, 2 <32,128,32,16>, 3 <64,64,32,16>, 4 <32,64,16,16>, 5 <64,32,16,16> + const uint16_t * hc_shadow = mmb_is_hc_q8(src0) ? mmb_shadow_lookup(ctx, src0) : nullptr; + static const int HCD = getenv("MMB_HCD_TILE") ? atoi(getenv("MMB_HCD_TILE")) : 3; + if (HCD && src0->type == GGML_TYPE_Q8_0 && M <= 384 && K >= 4096 && T >= 512) { + const bool hbf = ggml_cuda_mmb_blk16() && ggml_cuda_mmb_is_bf16_only(ctx, dst) && (M & 7) == 0; + uint16_t * Dh2 = hbf ? (uint16_t *) dst->data : nullptr; const bool sf = !hbf; + auto go = [&](auto tag, const uint8_t * WP) { + constexpr int WT = decltype(tag)::value; + if (HCD == 1) { dim3 g((M + 63) / 64, (T + 127) / 128); mmb_dense_kernel<64, 128, 32, 32, WT><<<g, MMB_NT, 0, stream>>>(WP, xhp, D, Dh2, sf, M, K, T); } + else if (HCD == 2) { dim3 g((M + 31) / 32, (T + 127) / 128); mmb_dense_kernel<32, 128, 32, 16, WT><<<g, MMB_NT, 0, stream>>>(WP, xhp, D, Dh2, sf, M, K, T); } + else if (HCD == 3) { dim3 g((M + 63) / 64, (T + 63) / 64); mmb_dense_kernel<64, 64, 32, 16, WT><<<g, MMB_NT, 0, stream>>>(WP, xhp, D, Dh2, sf, M, K, T); } + else if (HCD == 4) { dim3 g((M + 31) / 32, (T + 63) / 64); mmb_dense_kernel<32, 64, 16, 16, WT><<<g, MMB_NT, 0, stream>>>(WP, xhp, D, Dh2, sf, M, K, T); } + else if (HCD == 6) { dim3 g(1, (T + 31) / 32); mmb_dense_kernel<384, 32, 96, 16, WT><<<g, MMB_NT, 0, stream>>>(WP, xhp, D, Dh2, sf, M, K, T); } + else { dim3 g((M + 63) / 64, (T + 31) / 32); mmb_dense_kernel<64, 32, 16, 16, WT><<<g, MMB_NT, 0, stream>>>(WP, xhp, D, Dh2, sf, M, K, T); } + }; + if (hc_shadow) go(std::integral_constant<int, 2>{}, (const uint8_t *) hc_shadow); + else go(std::integral_constant<int, 1>{}, W); + CUDA_CHECK(cudaGetLastError()); + return; + } const uint16_t * shadow_pre = ((src0->type == GGML_TYPE_IQ4_NL && mmb_shadow()) || src0->type == GGML_TYPE_Q6_K) ? mmb_shadow_lookup(ctx, src0) : nullptr; const bool big = (M >= 6144 && K >= 2560) || (shadow_pre && K >= 2560 && T >= 4096); uint16_t * Dh = (mmb_hc16() && K == 320 && M == 10240) ? ggml_cuda_mmb_slot_reserve(ctx, 1, dst, (size_t) T * M) : nullptr; @@ -834,27 +1191,53 @@ void ggml_cuda_mul_mat_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * } dim3 grid((M + 127) / 128, big ? (T + 255) / 256 : (T + 127) / 128); const uint16_t * shadow = shadow_pre; + // MMB_DENSE_TILE narrows the shared-expert / dense GEMM tiles for more blocks per ubatch chunk. +#define MMB_SMALL_DENSE(WT, WP) do { \ + static const int DT_ = getenv("MMB_DENSE_TILE") ? atoi(getenv("MMB_DENSE_TILE")) : 0; \ + if (DT_ == 1) { dim3 gd((M + 63) / 64, (T + 127) / 128); mmb_dense_kernel<64, 128, 32, 32, WT><<<gd, MMB_NT, 0, stream>>>(WP, xhp, D, Dh, store_f32, M, K, T); } \ + else if (DT_ == 2) { dim3 gd((M + 31) / 32, (T + 127) / 128); mmb_dense_kernel<32, 128, 32, 16, WT><<<gd, MMB_NT, 0, stream>>>(WP, xhp, D, Dh, store_f32, M, K, T); } \ + else if (DT_ == 3) { dim3 gd((M + 63) / 64, (T + 63) / 64); mmb_dense_kernel<64, 64, 32, 16, WT><<<gd, MMB_NT, 0, stream>>>(WP, xhp, D, Dh, store_f32, M, K, T); } \ + else { mmb_dense_kernel<128, 128, 32, 64, WT><<<grid, MMB_NT, 0, stream>>>(WP, xhp, D, Dh, store_f32, M, K, T); } \ +} while (0) if (shadow) { if (big) mmb_dense_kernel<128, 256, 64, 64, 2><<<grid, MMB_NT, 0, stream>>>((const uint8_t *) shadow, xhp, D, Dh, store_f32, M, K, T); - else mmb_dense_kernel<128, 128, 32, 64, 2><<<grid, MMB_NT, 0, stream>>>((const uint8_t *) shadow, xhp, D, Dh, store_f32, M, K, T); + else { MMB_SMALL_DENSE(2, (const uint8_t *) shadow); } } else if (src0->type == GGML_TYPE_IQ4_NL) { if (big) mmb_dense_kernel<128, 256, 64, 64, 0><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); - else mmb_dense_kernel<128, 128, 32, 64, 0><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + else { MMB_SMALL_DENSE(0, W); } } else if (src0->type == GGML_TYPE_Q8_0) { if (big) mmb_dense_kernel<128, 256, 64, 64, 1><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); - else mmb_dense_kernel<128, 128, 32, 64, 1><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + else { MMB_SMALL_DENSE(1, W); } } else if (mmb_quant_type(src0->type)) { mmb_dispatch_quant(src0->type, [&](auto tag) { constexpr int WT = decltype(tag)::value; if (big) mmb_dense_kernel<128, 256, 64, 64, WT><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); - else mmb_dense_kernel<128, 128, 32, 64, WT><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + else { MMB_SMALL_DENSE(WT, W); } }); } else { if (big) mmb_dense_kernel<128, 256, 64, 64, 2><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); - else mmb_dense_kernel<128, 128, 32, 64, 2><<<grid, MMB_NT, 0, stream>>>(W, xhp, D, Dh, store_f32, M, K, T); + else { MMB_SMALL_DENSE(2, W); } + } + CUDA_CHECK(cudaGetLastError()); +} + +// two F32-weight GEMMs [K -> M1] and [K -> M2] (M1 + M2 <= 128) on the same F32 activations, one pass over X +bool ggml_cuda_mmb_f32_dual(ggml_backend_cuda_context & ctx, const ggml_tensor * w1, const ggml_tensor * w2, const ggml_tensor * x, + ggml_tensor * d1, ggml_tensor * d2) { + const int K = (int) w1->ne[0], M1 = (int) w1->ne[1], M2 = (int) w2->ne[1]; + const int T = (int) (x->ne[1] * x->ne[2] * x->ne[3]); + if (w1->type != GGML_TYPE_F32 || w2->type != GGML_TYPE_F32 || x->type != GGML_TYPE_F32 || w2->ne[0] != K || x->ne[0] != K || + M1 + M2 > 128 || M1 % 16 != 0 || K % 32 != 0 || !ggml_is_contiguous(w1) || !ggml_is_contiguous(w2) || !ggml_is_contiguous(x) || + !ggml_is_contiguous(d1) || !ggml_is_contiguous(d2) || ggml_nelements(d1) != (int64_t) M1 * T || ggml_nelements(d2) != (int64_t) M2 * T) { + return false; } + const int M = M1 + M2; + dim3 grid((M + 63) / 64, (T + 31) / 32); + mmb_f32split_kernel<64, 32, 16, 16, true, false><<<grid, MMB_NT, 0, ctx.stream()>>>((const float *) w1->data, (const float *) x->data, + (float *) d1->data, M, K, T, (const float *) w2->data, (float *) d2->data, M1); CUDA_CHECK(cudaGetLastError()); + return true; } bool ggml_cuda_mmb_gatemix() { return mmb_gatemix_flag(); } @@ -874,6 +1257,10 @@ bool ggml_cuda_hc_gate_mix(ggml_backend_cuda_context & ctx, const ggml_tensor * uint16_t * outh = ggml_cuda_mmb_slot_reserve(ctx, 3, dst, (size_t) T * E); const bool store_f32 = !(outh && ggml_cuda_mmb_is_bf16_only(ctx, dst)); dim3 grid(E / 32, (T + 127) / 128); + const uint16_t * hc_shadow = mmb_is_hc_q8(w) ? mmb_shadow_lookup(ctx, w) : nullptr; + if (hc_shadow) { + hc_gate_mix_kernel<4, 2><<<grid, MMB_NT, 0, stream>>>((const uint8_t *) hc_shadow, lo16, xn16, (float *) dst->data, outh, store_f32, E, K, T, scale, bias); + } else mmb_dispatch_quant(w->type, [&](auto tag) { constexpr int WT = decltype(tag)::value; hc_gate_mix_kernel<4, WT><<<grid, MMB_NT, 0, stream>>>((const uint8_t *) w->data, lo16, xn16, (float *) dst->data, outh, store_f32, E, K, T, scale, bias); @@ -901,7 +1288,12 @@ void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), bounds.get(), E, T, n_used, ne11, si1, sis1, /*write_inverse=*/false, stream); } - constexpr int BN_SMALL = 32, THRESH = 128; + constexpr int BN_SMALL = 32; + // THRESH=32 (was 128): more MoE experts use the wide BN tile during prefill. + // Measured on qwen3.8-flash-next / gfx1151, -p 2048 -d 0,12000,32000,64000: + // +3.6% d0, +4.7% d12k, +4.0% d32k, +1.9% d64k vs THRESH=128; byte-identical + // output, test-backend-ops 29643/29643. Env MMB_THRESH overrides. + static const int THRESH = getenv("MMB_THRESH") ? atoi(getenv("MMB_THRESH")) : 32; const int nbig_max = n_rows / BN + E + 1; const int nsmall_max = E * ((THRESH + BN_SMALL - 1) / BN_SMALL) + 1; ggml_cuda_pool_alloc<uint32_t> desc_big(ctx.pool(), nbig_max); @@ -911,11 +1303,18 @@ void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor const uint8_t * W = (const uint8_t *) src0->data; float * D = (float *) dst->data; const size_t eb = (size_t) src0->nb[2]; uint16_t * Dh = (mmb_down16_flag() && ggml_cuda_mmb_is_bf16_only(ctx, dst)) ? (uint16_t *) dst->data : nullptr; const bool store_f32 = Dh == nullptr; + static const int RT = getenv("MMB_ROUTED_TILE") ? atoi(getenv("MMB_ROUTED_TILE")) : 0; dim3 gbig((M + 127) / 128, nbig_max), gsmall((M + 127) / 128, nsmall_max); mmb_dispatch_quant(src0->type, [&](auto tag) { constexpr int WT = decltype(tag)::value; + if (RT == 1) { + const dim3 gb((M + 63) / 64, nbig_max), gs((M + 63) / 64, nsmall_max); + mmb_routed_kernel<64, BN, 32, 32, WT><<<gb, MMB_NT, 0, stream>>>(W, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); + mmb_routed_kernel<64, BN_SMALL, 16, 16, WT><<<gs, MMB_NT, 0, stream>>>(W, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + } else { mmb_routed_kernel<128, BN, 32, 64, WT><<<gbig, MMB_NT, 0, stream>>>(W, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); mmb_routed_kernel<128, BN_SMALL, 32, 16, WT><<<gsmall, MMB_NT, 0, stream>>>(W, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + } }); CUDA_CHECK(cudaGetLastError()); } @@ -937,7 +1336,7 @@ void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_te const int K = (int) gw->ne[0], M = (int) gw->ne[1], E = (int) gw->ne[2]; const int ne11 = (int) src1->ne[1], T = (int) src1->ne[2], n_used = (int) ids->ne[0]; const int n_rows_x = ne11 * T, n_rows = n_used * T; - constexpr int BN = 128; + static const int BN_sel = getenv("MMB_BN") ? atoi(getenv("MMB_BN")) : 128; const uint16_t * xhp = mmb_bf16_activation(ctx, src1, (size_t) n_rows_x * K, stream); ggml_cuda_pool_alloc<int32_t> ids_src1(ctx.pool(), n_rows); ggml_cuda_pool_alloc<int32_t> ids_dst(ctx.pool(), n_rows); @@ -949,20 +1348,67 @@ void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_te ggml_cuda_launch_mm_ids_helper((const int32_t *) ids->data, ids_src1.get(), ids_dst.get(), bounds.get(), E, T, n_used, ne11, si1, sis1, /*write_inverse=*/false, stream); } - constexpr int BN_SMALL = 32, THRESH = 128; - const int nbig_max = n_rows / BN + E + 1; + constexpr int BN_SMALL = 32; + // THRESH=32 (was 128): more MoE experts use the wide BN tile during prefill. + // Measured on qwen3.8-flash-next / gfx1151, -p 2048 -d 0,12000,32000,64000: + // +3.6% d0, +4.7% d12k, +4.0% d32k, +1.9% d64k vs THRESH=128; byte-identical + // output, test-backend-ops 29643/29643. Env MMB_THRESH overrides. + static const int THRESH = getenv("MMB_THRESH") ? atoi(getenv("MMB_THRESH")) : 32; + const int BN_rt = BN_sel; + // actual tile counts for the chosen BN; pool sized for the smallest BN (largest count) so any BN_sel fits + const int nbig_max = n_rows / BN_rt + E + 1; + const int nbig_cap = n_rows / 64 + E + 1; const int nsmall_max = E * ((THRESH + BN_SMALL - 1) / BN_SMALL) + 1; - ggml_cuda_pool_alloc<uint32_t> desc_big(ctx.pool(), nbig_max); + ggml_cuda_pool_alloc<uint32_t> desc_big(ctx.pool(), nbig_cap); ggml_cuda_pool_alloc<uint32_t> desc_small(ctx.pool(), nsmall_max); - mmb_build_desc2<<<1, 1024, 0, stream>>>(bounds.get(), desc_big.get(), desc_small.get(), E, nbig_max, nsmall_max, BN, BN_SMALL, THRESH); + mmb_build_desc2<<<1, 1024, 0, stream>>>(bounds.get(), desc_big.get(), desc_small.get(), E, nbig_cap, nsmall_max, BN_rt, BN_SMALL, THRESH); uint16_t * Dh = ggml_cuda_mmb_slot_reserve(ctx, 2, glu, (size_t) n_rows * M); const bool store_f32 = !ggml_cuda_mmb_is_bf16_only(ctx, glu); const uint8_t * Wg = (const uint8_t *) gw->data, * Wu = (const uint8_t *) uw->data; float * D = (float *) glu->data; const size_t eb = (size_t) gw->nb[2]; - dim3 gbig((M + 63) / 64, nbig_max), gsmall((M + 63) / 64, nsmall_max); + // MMB_BM_BIG / MMB_BM_SMALL widen the M tile (n_ff_exp rows per block) to halve the M-tile count and the + // B-tile reloads at small rows/expert. BM=128 needs a matching (WTM,WTN) to keep the 8 waves full. + static const int BM_BIG = getenv("MMB_BM_BIG") ? atoi(getenv("MMB_BM_BIG")) : 64; + static const int BM_SMALL = getenv("MMB_BM_SMALL") ? atoi(getenv("MMB_BM_SMALL")) : 64; + // gfx1151 / ROCm 10, Qwen3.8 Q4_K pp4096: routed GLU 510 -> 476 ms in a GPU trace. + // Retest if quant decode or lane layout changes; MMB_Q4K_HALF=0 restores one lane per row. + static const bool q4k_half = getenv("MMB_Q4K_HALF") ? atoi(getenv("MMB_Q4K_HALF")) != 0 : true; mmb_dispatch_quant(gw->type, [&](auto tag) { constexpr int WT = decltype(tag)::value; - mmb_routed_glu_kernel<64, BN, 32, 32, WT><<<gbig, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); - mmb_routed_glu_kernel<64, BN_SMALL, 16, 16, WT><<<gsmall, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + if (BM_BIG == 128 && BN_sel != 256) { + const dim3 gbig((M + 127) / 128, nbig_max); + mmb_routed_glu_kernel<128, 128, 64, 32, WT><<<gbig, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); + } else { + auto launch_big = [&](auto bn_tag) { + constexpr int BN = decltype(bn_tag)::value; + const dim3 gbig((M + 63) / 64, nbig_max); + if constexpr (WT == 32 + GGML_TYPE_Q4_K && BN == 128) { + if (q4k_half) { + mmb_routed_glu_kernel<64, BN, 32, BN / 4, WT, true><<<gbig, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); + } else { + mmb_routed_glu_kernel<64, BN, 32, BN / 4, WT><<<gbig, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); + } + } else { + mmb_routed_glu_kernel<64, BN, 32, BN / 4, WT><<<gbig, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_big.get(), M, K); + } + }; + if (BN_sel == 256) launch_big(std::integral_constant<int,256>{}); + else launch_big(std::integral_constant<int,128>{}); + } + if (BM_SMALL == 128) { + const dim3 gsmall((M + 127) / 128, nsmall_max); + mmb_routed_glu_kernel<128, BN_SMALL, 32, 16, WT><<<gsmall, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + } else { + const dim3 gsmall((M + 63) / 64, nsmall_max); + if constexpr (WT == 32 + GGML_TYPE_Q4_K) { + if (q4k_half) { + mmb_routed_glu_kernel<64, BN_SMALL, 16, 16, WT, true><<<gsmall, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + } else { + mmb_routed_glu_kernel<64, BN_SMALL, 16, 16, WT><<<gsmall, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + } + } else { + mmb_routed_glu_kernel<64, BN_SMALL, 16, 16, WT><<<gsmall, MMB_NT, 0, stream>>>(Wg, Wu, eb, xhp, D, Dh, store_f32, ids_src1.get(), ids_dst.get(), bounds.get(), desc_small.get(), M, K); + } + } }); CUDA_CHECK(cudaGetLastError()); } @@ -970,6 +1416,17 @@ void ggml_cuda_mul_mat_id_mmb_glu(ggml_backend_cuda_context & ctx, const ggml_te // Called from graph_optimize (outside stream capture): create the shadow for an eligible IQ4_NL dense weight. void ggml_cuda_mmb_shadow_prepare(ggml_backend_cuda_context & ctx, const ggml_tensor * w) { if (!w) return; + if (mmb_is_hc_q8(w)) { + if (mmb_state(ctx).shadow.count(w->data) > 0) return; + const size_t n = (size_t) w->ne[0] * w->ne[1], bytes = n * 2; + if (mmb_state(ctx).shadow_bytes + bytes > mmb_shadow_cap()) return; + uint16_t * buf = nullptr; + if (cudaMalloc((void **) &buf, bytes) != cudaSuccess) { GGML_LOG_WARN("MMB_SHADOW alloc failed (%zu bytes)\n", bytes); return; } + mmb_dq_q8_0_bf16_kernel<<<(unsigned) ((n / 32 + 255) / 256), 256, 0, ctx.stream()>>>((const uint8_t *) w->data, buf, n / 32); + CUDA_CHECK(cudaGetLastError()); + mmb_state(ctx).shadow[w->data] = buf; mmb_state(ctx).shadow_bytes += bytes; + return; + } if (mmb_is_resident_q6k(w)) { if (!mmb_shadow_q6k() || mmb_state(ctx).shadow.count(w->data) > 0) return; const size_t n = (size_t) w->ne[0] * w->ne[1], bytes = n * 2; diff --git a/ggml/src/ggml-cuda/mmb.cuh b/ggml/src/ggml-cuda/mmb.cuh index 5e547eef4cda..473b942f6b92 100644 --- a/ggml/src/ggml-cuda/mmb.cuh +++ b/ggml/src/ggml-cuda/mmb.cuh @@ -5,6 +5,11 @@ bool ggml_cuda_mmb_supported_mm (ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst); bool ggml_cuda_mmb_supported_mmid(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, const ggml_tensor * dst); void ggml_cuda_mul_mat_mmb (ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst); +// qwen4exp QSA indexer scorer: F32 GEMM + relu + 4-head sum (+ compact visibility) in one kernel, dst [M, T/4] +bool ggml_cuda_mmb_idx_score_supported(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, + const ggml_tensor * mm, int heads); +void ggml_cuda_mmb_idx_score(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst, + const int32_t * tails, const int32_t * starts); void ggml_cuda_mul_mat_id_mmb(ggml_backend_cuda_context & ctx, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * ids, ggml_tensor * dst); void ggml_cuda_mmb_begin_graph(ggml_backend_cuda_context & ctx); // producers that can emit a BF16 copy of an F32 output register it here; returns the BF16 buffer to fill (n elements) @@ -19,6 +24,8 @@ size_t ggml_cuda_mmb_marks_count(ggml_backend_cuda_context & ctx); void ggml_cuda_mmb_mark_bf16_only(ggml_backend_cuda_context & ctx, const ggml_tensor * t); bool ggml_cuda_mmb_is_bf16_only(ggml_backend_cuda_context & ctx, const ggml_tensor * t); bool ggml_cuda_mmb_gatemix(); +// two F32-weight GEMMs (M1 + M2 <= 128 rows) on the same F32 activations in one pass (qwen4exp ssm_beta + ssm_alpha) +bool ggml_cuda_mmb_f32_dual(ggml_backend_cuda_context & ctx, const ggml_tensor * w1, const ggml_tensor * w2, const ggml_tensor * x, ggml_tensor * d1, ggml_tensor * d2); bool ggml_cuda_mmb_down16(); bool ggml_cuda_mmb_res16(); bool ggml_cuda_mmb_blk16(); diff --git a/ggml/src/ggml-cuda/moe-weighted-reduction.cu b/ggml/src/ggml-cuda/moe-weighted-reduction.cu index a826c70bfcf6..95c75603daaf 100644 --- a/ggml/src/ggml-cuda/moe-weighted-reduction.cu +++ b/ggml/src/ggml-cuda/moe-weighted-reduction.cu @@ -1,6 +1,7 @@ #include "moe-weighted-reduction.cuh" #include "mmb.cuh" #include <cstdlib> +#include <algorithm> __device__ __forceinline__ float moe_bf2f(const uint16_t h) { return __uint_as_float(((uint32_t) h) << 16); } @@ -210,3 +211,98 @@ void ggml_cuda_op_moe_weighted_reduction(ggml_backend_cuda_context & ctx, (float *) dst->data, n_embd, n_tokens, (int) n_expert_used, stream); CUDA_CHECK(cudaGetLastError()); } + +// prefill: reduction + shared-expert merge in one pass, out = sum_k (e_k * scale_k) * w_k + shexp * sigmoid(gate[t]). +// The sum is the moe_weighted_reduction_*_v4 expression; the merge is sigmoid_gate_mul_add_f32's (product and sum rounded +// separately, sigmoid as op_sigmoid) -> bit-identical to the two kernels, without the F32 round trip of the reduction. +#if defined(GGML_USE_HIP) +static __device__ __forceinline__ float moe_mul_rn(const float a, const float b) { float r; asm("v_mul_f32_e32 %0, %1, %2" : "=v"(r) : "v"(a), "v"(b)); return r; } +static __device__ __forceinline__ float moe_add_rn(const float a, const float b) { float r; asm("v_add_f32_e32 %0, %1, %2" : "=v"(r) : "v"(a), "v"(b)); return r; } +#else +static __device__ __forceinline__ float moe_mul_rn(const float a, const float b) { return __fmul_rn(a, b); } +static __device__ __forceinline__ float moe_add_rn(const float a, const float b) { return __fadd_rn(a, b); } +#endif +template <bool EIN16> +static __global__ void moe_weighted_reduction_sgma_v4(const void * __restrict__ experts_v, + const float * __restrict__ expert_scale, + const float * __restrict__ weights, + const float * __restrict__ gate, + const float * __restrict__ shexp, + float * __restrict__ dst, + const int64_t n_embd, const int n_expert_used) { + const int64_t token = blockIdx.x; + const int64_t col4 = ((int64_t) blockIdx.y * blockDim.x + threadIdx.x) * 4; + if (col4 >= n_embd) return; + auto ld = [&](const uint64_t row) -> float4 { + if constexpr (EIN16) { + const ushort4 h = *(const ushort4 *)((const uint16_t *) experts_v + row * n_embd + col4); + return make_float4(moe_bf2f(h.x), moe_bf2f(h.y), moe_bf2f(h.z), moe_bf2f(h.w)); + } else { + return *(const float4 *)((const float *) experts_v + row * n_embd + col4); + } + }; + const uint64_t first_row = (uint64_t) token * n_expert_used; + const float first_scale = expert_scale != nullptr ? expert_scale[first_row] : 1.0f; + const float w0 = weights[first_row]; + const float4 e0 = ld(first_row); + float4 sum; sum.x = (e0.x * first_scale) * w0; sum.y = (e0.y * first_scale) * w0; sum.z = (e0.z * first_scale) * w0; sum.w = (e0.w * first_scale) * w0; + for (int expert = 1; expert < n_expert_used; ++expert) { + const uint64_t row = first_row + expert; + const float scale = expert_scale != nullptr ? expert_scale[row] : 1.0f; + const float w = weights[row]; + const float4 e = ld(row); + sum.x += (e.x * scale) * w; sum.y += (e.y * scale) * w; sum.z += (e.z * scale) * w; sum.w += (e.w * scale) * w; + } + const float g = 1.0f / (1.0f + expf(-gate[token])); + const float4 a = *(const float4 *)(shexp + token * n_embd + col4); + const float4 o = make_float4(moe_add_rn(sum.x, moe_mul_rn(a.x, g)), moe_add_rn(sum.y, moe_mul_rn(a.y, g)), + moe_add_rn(sum.z, moe_mul_rn(a.z, g)), moe_add_rn(sum.w, moe_mul_rn(a.w, g))); + *(float4 *)(dst + token * n_embd + col4) = o; +} + +bool ggml_cuda_moe_weighted_reduction_sgma_supported(const ggml_tensor * experts, const ggml_tensor * expert_scale, + const ggml_tensor * weights, const ggml_tensor * gate, const ggml_tensor * shexp, const ggml_tensor * dst) { + const int64_t n_embd = experts->ne[0]; + const int64_t n_tokens = experts->ne[2] * experts->ne[3]; + return n_embd % 4 == 0 && experts->type == GGML_TYPE_F32 && weights->type == GGML_TYPE_F32 && shexp->type == GGML_TYPE_F32 && + gate->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32 && + (expert_scale == nullptr || (expert_scale->type == GGML_TYPE_F32 && ggml_is_contiguous(expert_scale))) && + ggml_is_contiguous(experts) && ggml_is_contiguous(weights) && ggml_is_contiguous(shexp) && ggml_is_contiguous(gate) && + ggml_is_contiguous(dst) && ggml_nelements(gate) == n_tokens && ggml_nelements(shexp) == n_embd * n_tokens && + ggml_nelements(dst) == n_embd * n_tokens; +} + +bool ggml_cuda_op_moe_weighted_reduction_sgma(ggml_backend_cuda_context & ctx, const ggml_tensor * experts, + const ggml_tensor * expert_scale, const ggml_tensor * weights, const ggml_tensor * gate, const ggml_tensor * shexp, + ggml_tensor * dst) { + const int64_t n_embd = experts->ne[0]; + const int64_t n_expert_used = experts->ne[1]; + const int64_t n_tokens = experts->ne[2] * experts->ne[3]; + if (!ggml_cuda_moe_weighted_reduction_sgma_supported(experts, expert_scale, weights, gate, shexp, dst) || + ((uintptr_t) shexp->data % 16) != 0 || ((uintptr_t) dst->data % 16) != 0 || ((uintptr_t) experts->data % 16) != 0) { + return false; + } + // a BF16-only sum stays on the unfused path, which writes it; this kernel only stores F32 + if (ggml_cuda_mmb_blk16() && ggml_cuda_mmb_is_bf16_only(ctx, dst)) { + return false; + } + // same condition as the unfused reduction's BF16-input path + const bool ein16 = ggml_cuda_mmb_down16() && ggml_cuda_mmb_is_bf16_only(ctx, experts); + // one block per token when its n_embd/4 lanes fit a block (640 for n_embd 2560: no idle lanes, where 256 left 128 of + // 768 idle), else 256. MOE_SGMA_THREADS overrides, rounded to whole warps within [32, 1024]. + static const int env_threads = getenv("MOE_SGMA_THREADS") ? atoi(getenv("MOE_SGMA_THREADS")) : 0; + const int64_t lanes = n_embd / 4; + int threads = (lanes <= 1024 && lanes % 32 == 0) ? (int) lanes : 256; + if (env_threads > 0) { + threads = std::min(1024, std::max(32, (env_threads + 31) / 32 * 32)); + } + const dim3 blocks(n_tokens, (n_embd / 4 + threads - 1) / threads, 1); + const float * sc = expert_scale ? (const float *) expert_scale->data : nullptr; + cudaStream_t stream = ctx.stream(); +#define MOE_SGMA_LAUNCH(A) moe_weighted_reduction_sgma_v4<A><<<blocks, threads, 0, stream>>>(experts->data, sc, \ + (const float *) weights->data, (const float *) gate->data, (const float *) shexp->data, (float *) dst->data, n_embd, (int) n_expert_used) + if (ein16) { MOE_SGMA_LAUNCH(true); } else { MOE_SGMA_LAUNCH(false); } +#undef MOE_SGMA_LAUNCH + CUDA_CHECK(cudaGetLastError()); + return true; +} diff --git a/ggml/src/ggml-cuda/moe-weighted-reduction.cuh b/ggml/src/ggml-cuda/moe-weighted-reduction.cuh index 894c4b1a35ed..cff331ba80e3 100644 --- a/ggml/src/ggml-cuda/moe-weighted-reduction.cuh +++ b/ggml/src/ggml-cuda/moe-weighted-reduction.cuh @@ -6,3 +6,12 @@ void ggml_cuda_op_moe_weighted_reduction(ggml_backend_cuda_context & ctx, const ggml_tensor * weights, ggml_tensor * dst, const ggml_tensor * merge = nullptr); + +// types/shapes/contiguity the fused kernel below needs (no data pointers, so graph_optimize can ask before allocation) +bool ggml_cuda_moe_weighted_reduction_sgma_supported(const ggml_tensor * experts, const ggml_tensor * expert_scale, + const ggml_tensor * weights, const ggml_tensor * gate, const ggml_tensor * shexp, const ggml_tensor * dst); + +// reduction + SIGMOID(gate [1, T]) -> MUL(shexp, .) -> ADD(reduction, .) in one kernel; false if the shapes/types don't fit +bool ggml_cuda_op_moe_weighted_reduction_sgma(ggml_backend_cuda_context & ctx, const ggml_tensor * experts, + const ggml_tensor * expert_scale, const ggml_tensor * weights, const ggml_tensor * gate, const ggml_tensor * shexp, + ggml_tensor * dst); diff --git a/ggml/src/ggml-cuda/qsa-prefill.cu b/ggml/src/ggml-cuda/qsa-prefill.cu index 860878949f37..2d7b32d387e2 100644 --- a/ggml/src/ggml-cuda/qsa-prefill.cu +++ b/ggml/src/ggml-cuda/qsa-prefill.cu @@ -19,7 +19,7 @@ static __device__ __forceinline__ float qsa3_h2f(const uint16_t h) { return (flo #define QSA3_SENT 0x7FFFFFFF __global__ __launch_bounds__(256) void qsa3_rows_kernel( const int * __restrict__ ids, const size_t i1, const int ns, const int nk, int * __restrict__ srow, int * __restrict__ sflag, - const char * mask, size_t m1) { + const char * mask, size_t m1, const int np2) { extern __shared__ int ent[]; __shared__ int unsorted; const int q = blockIdx.x, tid = threadIdx.x; @@ -43,11 +43,32 @@ __global__ __launch_bounds__(256) void qsa3_rows_kernel( __syncthreads(); if (unsorted) { int * dst = srow + (size_t) q * ns; - for (int j = tid; j < ns; j += 256) { - const int e = ent[j]; - int rank = 0; - for (int k = 0; k < ns; ++k) { const int f = ent[k]; rank += (f < e) || (f == e && k < j); } - dst[rank] = e; + if (np2 == 0) { + for (int j = tid; j < ns; j += 256) { + const int e = ent[j]; + int rank = 0; + for (int k = 0; k < ns; ++k) { const int f = ent[k]; rank += (f < e) || (f == e && k < j); } + dst[rank] = e; + } + } else { + // bitonic sort of the row padded to np2 with the sentinel (sorts last): O(n log^2 n) instead of the O(n^2) + // rank sort; equal entries are equal values, so the sorted row is the same as the rank sort's + for (int j = ns + tid; j < np2; j += 256) { ent[j] = QSA3_SENT; } + __syncthreads(); + for (int k = 2; k <= np2; k <<= 1) { + for (int jj = k >> 1; jj > 0; jj >>= 1) { + for (int i = tid; i < np2; i += 256) { + const int ixj = i ^ jj; + if (ixj > i) { + const int a = ent[i], b = ent[ixj]; + const bool asc = (i & k) == 0; + if ((a > b) == asc) { ent[i] = b; ent[ixj] = a; } + } + } + __syncthreads(); + } + } + for (int j = tid; j < ns; j += 256) { dst[j] = ent[j]; } } } if (tid == 0) { sflag[q] = unsorted; } @@ -175,11 +196,12 @@ __global__ __launch_bounds__(QSA3_MERGE_LANES) void qsa3_merge_kernel( struct qsa3_layout { size_t q1, q2, o1, o2, m1; + size_t knb1, vnb1, knb2, vnb2; // raw f16 cache strides (DIRECT reads only) int nk, n_q, gqa, cap; float scale; }; -__global__ __launch_bounds__(256) void qsa3_attn_kernel( +template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( const float * __restrict__ q, const uint16_t * __restrict__ pk, const uint16_t * __restrict__ pv, const uint16_t * __restrict__ mask, const uint16_t * __restrict__ ublk, const uint16_t * __restrict__ umask, const int * __restrict__ ucount, float * __restrict__ out, const qsa3_layout s) { @@ -194,8 +216,17 @@ __global__ __launch_bounds__(256) void qsa3_attn_kernel( __shared__ float l_s[48]; const size_t nblk = (size_t) s.nk / 4; - const uint16_t * pkg = pk + (size_t) kvh * nblk * 1024; - const uint16_t * pvg = pv + (size_t) kvh * nblk * 1024; + const uint16_t * pkg = nullptr; + const uint16_t * pvg = nullptr; + const char * kbase = nullptr; + const char * vbase = nullptr; + if constexpr (DIRECT) { + kbase = reinterpret_cast<const char *>(pk) + (size_t) kvh * s.knb2; + vbase = reinterpret_cast<const char *>(pv) + (size_t) kvh * s.vnb2; + } else { + pkg = pk + (size_t) kvh * nblk * 1024; + pvg = pv + (size_t) kvh * nblk * 1024; + } const uint16_t * gblk = ublk + (size_t) g * s.cap; const uint16_t * gmsk = umask + (size_t) g * s.cap; const int nchunks = ucount[g] >> 2; @@ -258,12 +289,23 @@ __global__ __launch_bounds__(256) void qsa3_attn_kernel( return; } const int kb = blk_of(b01, b23, j); - const uint16_t * krow = pkg + (size_t) kb * 1024 + (r & 3) * 16 + w * 128; + if constexpr (DIRECT) { + // the packed 16-half chunk at (key, w, t) is dims (2w+t)*16 .. +15 of that key's row + const int key = 4*kb + (r & 3); + const char * p = kbase + (size_t)key * s.knb1 + (size_t)((2*w) * 16) * 2; +#pragma unroll + for (int t = 0; t < 2; ++t) { + const char * q = p + (size_t)t * 32; + kf[t] = __builtin_bit_cast(v16s, (uint4[2]){*reinterpret_cast<const uint4 *>(q), *reinterpret_cast<const uint4 *>(q + 16)}); + } + } else { + const uint16_t * krow = pkg + (size_t) kb * 1024 + (r & 3) * 16 + w * 128; #pragma unroll - for (int t = 0; t < 2; ++t) { - const uint4 lo = *reinterpret_cast<const uint4 *>(krow + t*64); - const uint4 hi4 = *reinterpret_cast<const uint4 *>(krow + t*64 + 8); - kf[t] = __builtin_bit_cast(v16s, (uint4[2]){lo, hi4}); + for (int t = 0; t < 2; ++t) { + const uint4 lo = *reinterpret_cast<const uint4 *>(krow + t*64); + const uint4 hi4 = *reinterpret_cast<const uint4 *>(krow + t*64 + 8); + kf[t] = __builtin_bit_cast(v16s, (uint4[2]){lo, hi4}); + } } }; @@ -295,11 +337,27 @@ __global__ __launch_bounds__(256) void qsa3_attn_kernel( #pragma unroll for (int t = 0; t < 2; ++t) { const int d = 32*w + 16*t + r; - const uint2 v0 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb0 * 1024 + d * 4), m01 & 0xffffu); - const uint2 v1 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb1 * 1024 + d * 4), m01 >> 16); - const uint2 v2 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb2 * 1024 + d * 4), m23 & 0xffffu); - const uint2 v3 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb3 * 1024 + d * 4), m23 >> 16); - vf[t] = __builtin_bit_cast(v16s, (uint2[4]){v0, v1, v2, v3}); + if constexpr (DIRECT) { + // the packed uint2 holds the four keys of block kb at dim d; gather them from the rows + const char * col = vbase + (size_t)d * 2; + auto kv4 = [&](const int kb) -> uint2 { + const char * b = col + (size_t)(4*kb) * s.vnb1; + const uint32_t a0 = *reinterpret_cast<const uint16_t *>(b); + const uint32_t a1 = *reinterpret_cast<const uint16_t *>(b + s.vnb1); + const uint32_t a2 = *reinterpret_cast<const uint16_t *>(b + 2*s.vnb1); + const uint32_t a3 = *reinterpret_cast<const uint16_t *>(b + 3*s.vnb1); + return make_uint2(a0 | (a1 << 16), a2 | (a3 << 16)); + }; + vf[t] = __builtin_bit_cast(v16s, (uint2[4]){ + mask_values(kv4(kb0), m01 & 0xffffu), mask_values(kv4(kb1), m01 >> 16), + mask_values(kv4(kb2), m23 & 0xffffu), mask_values(kv4(kb3), m23 >> 16)}); + } else { + const uint2 v0 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb0 * 1024 + d * 4), m01 & 0xffffu); + const uint2 v1 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb1 * 1024 + d * 4), m01 >> 16); + const uint2 v2 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb2 * 1024 + d * 4), m23 & 0xffffu); + const uint2 v3 = mask_values(*reinterpret_cast<const uint2 *>(pvg + (size_t) kb3 * 1024 + d * 4), m23 >> 16); + vf[t] = __builtin_bit_cast(v16s, (uint2[4]){v0, v1, v2, v3}); + } } } uint32_t nb01 = 0, nb23 = 0, nm01 = 0, nm23 = 0; @@ -462,12 +520,18 @@ void ggml_cuda_flash_attn_ext_qsa_prefill(ggml_backend_cuda_context & ctx, ggml_ const auto * q=dst->src[0], * k=dst->src[1], * v=dst->src[2], * m=dst->src[3], * ids=dst->src[5]; float scale; memcpy(&scale, dst->op_params, 4); const int n_q = (int) q->ne[1], ns = (int) ids->ne[0], nk = (int) k->ne[1]; - ggml_cuda_pool_alloc<uint16_t> packed_k(ctx.pool(), ggml_nelements(k)); - ggml_cuda_pool_alloc<uint16_t> packed_v(ctx.pool(), ggml_nelements(v)); - const uint32_t count_k=ggml_nelements(k)/8, count_v=ggml_nelements(v)/4; - qsa_pack_k<<<(count_k+255)/256,256,0,ctx.stream()>>>((const char *)k->data,packed_k.get(),k->nb[1],k->nb[2],nk,count_k); - qsa_pack_v<<<(count_v+255)/256,256,0,ctx.stream()>>>((const char *)v->data,packed_v.get(),v->nb[1],v->nb[2],nk,count_v); - CUDA_CHECK(cudaGetLastError()); + // QSA_DIRECT: -1/auto picks direct f16 reads when the attention is small against the cache + // (the pack is O(n_kv) per ubatch), 0 always packs, 1 always reads direct. Both paths are bit-identical. + static const int qsa_direct_cfg = getenv("QSA_DIRECT") ? atoi(getenv("QSA_DIRECT")) : -1; + const bool qsa_direct = qsa_direct_cfg == 1 || (qsa_direct_cfg < 0 && (size_t) n_q * 32 < (size_t) nk); + ggml_cuda_pool_alloc<uint16_t> packed_k(ctx.pool(), qsa_direct ? 0 : ggml_nelements(k)); + ggml_cuda_pool_alloc<uint16_t> packed_v(ctx.pool(), qsa_direct ? 0 : ggml_nelements(v)); + if (!qsa_direct) { + const uint32_t count_k=ggml_nelements(k)/8, count_v=ggml_nelements(v)/4; + qsa_pack_k<<<(count_k+255)/256,256,0,ctx.stream()>>>((const char *)k->data,packed_k.get(),k->nb[1],k->nb[2],nk,count_k); + qsa_pack_v<<<(count_v+255)/256,256,0,ctx.stream()>>>((const char *)v->data,packed_v.get(),v->nb[1],v->nb[2],nk,count_v); + CUDA_CHECK(cudaGetLastError()); + } const int ngroups = (n_q + QSA3_G - 1) / QSA3_G; const int cap = (QSA3_G * ns + 3) & ~3; ggml_cuda_pool_alloc<uint16_t> ublk(ctx.pool(), (size_t) ngroups * cap); @@ -476,19 +540,30 @@ void ggml_cuda_flash_attn_ext_qsa_prefill(ggml_backend_cuda_context & ctx, ggml_ ggml_cuda_pool_alloc<int> srow(ctx.pool(), (size_t) n_q * ns); ggml_cuda_pool_alloc<int> sflag(ctx.pool(), (size_t) n_q); { - const ggml_cuda_kernel_launch_params launch(dim3(n_q), dim3(256), (size_t) ns * sizeof(int), ctx.stream()); - ggml_cuda_kernel_launch(qsa3_rows_kernel, launch, (const int *) ids->data, ids->nb[1], ns, nk, srow.get(), sflag.get(), m ? (const char *)m->data : nullptr, m ? m->nb[1] : 0); + // bitonic row sort over the next power of two (GGML_QSA_RANKSORT=1: the O(n^2) rank sort) + static const bool ranksort = getenv("GGML_QSA_RANKSORT") && atoi(getenv("GGML_QSA_RANKSORT")) != 0; + int np2 = 1; while (np2 < ns) np2 <<= 1; + if (ranksort || (size_t) np2 * sizeof(int) > 60 * 1024) np2 = 0; + const ggml_cuda_kernel_launch_params launch(dim3(n_q), dim3(256), (size_t) (np2 ? np2 : ns) * sizeof(int), ctx.stream()); + ggml_cuda_kernel_launch(qsa3_rows_kernel, launch, (const int *) ids->data, ids->nb[1], ns, nk, srow.get(), sflag.get(), m ? (const char *)m->data : nullptr, m ? m->nb[1] : 0, np2); CUDA_CHECK(cudaGetLastError()); const ggml_cuda_kernel_launch_params launch2(dim3(ngroups), dim3(QSA3_MERGE_LANES), (size_t) QSA3_G * ns * sizeof(int), ctx.stream()); ggml_cuda_kernel_launch(qsa3_merge_kernel, launch2, (const int *) ids->data, ids->nb[1], n_q, ns, nk, (const int *) srow.get(), (const int *) sflag.get(), ublk.get(), umask.get(), ucount.get(), cap); CUDA_CHECK(cudaGetLastError()); } - qsa3_layout layout{q->nb[1], q->nb[2], dst->nb[1], dst->nb[2], m ? m->nb[1] : 0, nk, n_q, 12, cap, scale}; + qsa3_layout layout{q->nb[1], q->nb[2], dst->nb[1], dst->nb[2], m ? m->nb[1] : 0, + k->nb[1], v->nb[1], k->nb[2], v->nb[2], nk, n_q, 12, cap, scale}; const ggml_cuda_kernel_launch_params launch(dim3(ngroups, k->ne[2]), dim3(256), 0, ctx.stream()); - ggml_cuda_kernel_launch(qsa3_attn_kernel, launch, (const float *) q->data, packed_k.get(), packed_v.get(), - m ? (const uint16_t *) m->data : nullptr, (const uint16_t *) ublk.get(), (const uint16_t *) umask.get(), - (const int *) ucount.get(), (float *) dst->data, layout); + if (qsa_direct) { + ggml_cuda_kernel_launch(qsa3_attn_kernel<true>, launch, (const float *) q->data, (const uint16_t *) k->data, (const uint16_t *) v->data, + m ? (const uint16_t *) m->data : nullptr, (const uint16_t *) ublk.get(), (const uint16_t *) umask.get(), + (const int *) ucount.get(), (float *) dst->data, layout); + } else { + ggml_cuda_kernel_launch(qsa3_attn_kernel<false>, launch, (const float *) q->data, packed_k.get(), packed_v.get(), + m ? (const uint16_t *) m->data : nullptr, (const uint16_t *) ublk.get(), (const uint16_t *) umask.get(), + (const int *) ucount.get(), (float *) dst->data, layout); + } CUDA_CHECK(cudaGetLastError()); } diff --git a/ggml/src/ggml-cuda/top-k.cu b/ggml/src/ggml-cuda/top-k.cu index 97eea70e13b0..4843f15af71f 100644 --- a/ggml/src/ggml-cuda/top-k.cu +++ b/ggml/src/ggml-cuda/top-k.cu @@ -264,6 +264,190 @@ static void top_k_radix_cuda( } } +// One workgroup per row (RDNA3.5 prefill, e.g. the qwen4exp QSA block selection: 512 of ~62k blocks per query). +// The radix path above reads every row ~5.5 times (four 8-bit histogram passes, a gather and a tie gather); here a +// 12-bit LDS histogram locates the threshold bucket, a second pass writes the keys above it and compacts the bucket +// into LDS, and the remaining 20 bits are resolved on the compacted candidates. A bucket larger than the LDS buffer +// falls back to further histogram passes over the row. The selected set is the same as top_k_radix_cuda with +// stable_ties: every key above the k-th largest, then the lowest-column keys equal to it. Like that path, the order +// of the keys above the threshold is unspecified; the equal keys follow them in column order. +#define TOPK_WG_THREADS 1024 +#define TOPK_WG_CAP 4096 + +// bin b of hist (NBINS bins, descending search) with sum(hist[> b]) < rank <= sum(hist[>= b]); returns b and the +// rank left inside it. Every thread gets the result. +template<int NBINS> +static __device__ __forceinline__ void top_k_wg_find(const int * hist, int rank, int * red, int & bin, int & rank_in) { + constexpr int PER = NBINS / TOPK_WG_THREADS; + const int tid = threadIdx.x; + // thread t owns bins [NBINS - (t+1)*PER, NBINS - t*PER): thread 0 the highest + int own = 0; +#pragma unroll + for (int i = 0; i < PER; ++i) { own += hist[NBINS - 1 - (tid * PER + i)]; } + red[tid] = own; + __syncthreads(); + // inclusive scan over threads (Hillis-Steele) + for (int off = 1; off < TOPK_WG_THREADS; off <<= 1) { + const int v = tid >= off ? red[tid - off] : 0; + __syncthreads(); + red[tid] += v; + __syncthreads(); + } + const int incl = red[tid], excl = incl - own; + __shared__ int s_bin, s_rank; + if (excl < rank && rank <= incl) { + int r = rank - excl, b = NBINS - 1 - tid * PER; + for (int i = 0; i < PER; ++i, --b) { + const int h = hist[b]; + if (r <= h) break; + r -= h; + } + s_bin = b; s_rank = r; + } + __syncthreads(); + bin = s_bin; rank_in = s_rank; + __syncthreads(); +} + +static __global__ void __launch_bounds__(TOPK_WG_THREADS) top_k_wg_kernel( + const float * __restrict__ src, int * __restrict__ dst, const int ncols, const int k) { + const int row = blockIdx.x, tid = threadIdx.x; + const float * x = src + (size_t) row * ncols; + int * out = dst + (size_t) row * k; + + __shared__ int hist[4096]; + __shared__ int red[TOPK_WG_THREADS]; + __shared__ uint32_t ckey[TOPK_WG_CAP]; + __shared__ int ccol[TOPK_WG_CAP]; + __shared__ int n_out, n_cand; + + for (int i = tid; i < 4096; i += TOPK_WG_THREADS) hist[i] = 0; + if (tid == 0) { n_out = 0; n_cand = 0; } + __syncthreads(); + + // pass 1: top 12 bits (four independent loads in flight per thread) + { + int c = tid; + for (; c + 3 * TOPK_WG_THREADS < ncols; c += 4 * TOPK_WG_THREADS) { + const float v0 = x[c], v1 = x[c + TOPK_WG_THREADS], v2 = x[c + 2 * TOPK_WG_THREADS], v3 = x[c + 3 * TOPK_WG_THREADS]; + atomicAdd(&hist[top_k_float_to_ordered(v0) >> 20], 1); + atomicAdd(&hist[top_k_float_to_ordered(v1) >> 20], 1); + atomicAdd(&hist[top_k_float_to_ordered(v2) >> 20], 1); + atomicAdd(&hist[top_k_float_to_ordered(v3) >> 20], 1); + } + for (; c < ncols; c += TOPK_WG_THREADS) { + atomicAdd(&hist[top_k_float_to_ordered(x[c]) >> 20], 1); + } + } + __syncthreads(); + int b12, rank; + top_k_wg_find<4096>(hist, k, red, b12, rank); + const int bucket = hist[b12]; + uint32_t prefix = (uint32_t) b12 << 20, pmask = 0xFFF00000u; + __syncthreads(); + + // pass 2: keys above the bucket go out, the bucket is compacted when it fits + const bool fits = bucket <= TOPK_WG_CAP; + auto place = [&](const int c, const float v) { + const uint32_t key = top_k_float_to_ordered(v); + const uint32_t top = key >> 20; + if (top > (uint32_t) b12) { + out[atomicAdd(&n_out, 1)] = c; + } else if (fits && top == (uint32_t) b12) { + const int p = atomicAdd(&n_cand, 1); + ckey[p] = key; ccol[p] = c; + } + }; + { + int c = tid; + for (; c + 3 * TOPK_WG_THREADS < ncols; c += 4 * TOPK_WG_THREADS) { + const float v0 = x[c], v1 = x[c + TOPK_WG_THREADS], v2 = x[c + 2 * TOPK_WG_THREADS], v3 = x[c + 3 * TOPK_WG_THREADS]; + place(c, v0); place(c + TOPK_WG_THREADS, v1); place(c + 2 * TOPK_WG_THREADS, v2); place(c + 3 * TOPK_WG_THREADS, v3); + } + for (; c < ncols; c += TOPK_WG_THREADS) place(c, x[c]); + } + __syncthreads(); + + // remaining 20 bits: 10 + 10, on the candidates or on the row + const int ncand = fits ? n_cand : 0; + for (int shift = 10; shift >= 0; shift -= 10) { + for (int i = tid; i < 1024; i += TOPK_WG_THREADS) hist[i] = 0; + __syncthreads(); + if (fits) { + for (int i = tid; i < ncand; i += TOPK_WG_THREADS) { + const uint32_t key = ckey[i]; + if ((key & pmask) == prefix) atomicAdd(&hist[(key >> shift) & 1023], 1); + } + } else { + for (int c = tid; c < ncols; c += TOPK_WG_THREADS) { + const uint32_t key = top_k_float_to_ordered(x[c]); + if ((key & pmask) == prefix) atomicAdd(&hist[(key >> shift) & 1023], 1); + } + } + __syncthreads(); + int b10, r2; + top_k_wg_find<1024>(hist, rank, red, b10, r2); + rank = r2; + prefix |= (uint32_t) b10 << shift; + pmask |= 1023u << shift; + __syncthreads(); + } + const uint32_t thr = prefix; // the k-th largest key; `rank` keys equal to it are taken + + // keys above the threshold inside the bucket + if (fits) { + for (int i = tid; i < ncand; i += TOPK_WG_THREADS) { + if (ckey[i] > thr) out[atomicAdd(&n_out, 1)] = ccol[i]; + } + } else { + for (int c = tid; c < ncols; c += TOPK_WG_THREADS) { + const uint32_t key = top_k_float_to_ordered(x[c]); + if ((key >> 20) == (uint32_t) b12 && key > thr) out[atomicAdd(&n_out, 1)] = c; + } + } + __syncthreads(); + const int base = n_out; // == k - rank + + // equal keys, lowest columns first + if (fits) { + // gather the equal columns, then pick the `rank` smallest by counting (each is unique) + __shared__ int n_eq; + if (tid == 0) n_eq = 0; + __syncthreads(); + // the histogram is free now and holds up to TOPK_WG_CAP (>= ncand) columns + for (int i = tid; i < ncand; i += TOPK_WG_THREADS) { + if (ckey[i] == thr) { const int p = atomicAdd(&n_eq, 1); hist[p] = ccol[i]; } + } + __syncthreads(); + const int ne = n_eq; + for (int i = tid; i < ne; i += TOPK_WG_THREADS) { + const int col = hist[i]; + int less = 0; + for (int j = 0; j < ne; ++j) less += hist[j] < col; + if (less < rank) out[base + less] = col; + } + } else { + const int lane = tid % warpSize, warp = tid / warpSize; + constexpr int NW = TOPK_WG_THREADS / 32; + int taken = 0; + for (int c0 = 0; c0 < ncols && taken < rank; c0 += TOPK_WG_THREADS) { + const int c = c0 + tid; + const bool eq = c < ncols && top_k_float_to_ordered(x[c]) == thr; + const unsigned long long m = __ballot(eq); + if (lane == 0) red[warp] = __popcll(m); + __syncthreads(); + int before = taken; + for (int w = 0; w < NW; ++w) { if (w < warp) before += red[w]; } + int total = 0; + for (int w = 0; w < NW; ++w) total += red[w]; + const int pos = before + __popcll(m & ((1ULL << lane) - 1)); + if (eq && pos < rank) out[base + pos] = c; + taken += total; + __syncthreads(); + } + } +} + #endif // defined(GGML_USE_HIP) void ggml_cuda_op_top_k(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { @@ -287,6 +471,13 @@ void ggml_cuda_op_top_k(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { const bool use_radix = ncols >= 8192 || (ncols >= 4096 && nrows >= 128) || (ncols >= 2048 && nrows >= 512); if (GGML_CUDA_CC_IS_RDNA3_5(ggml_cuda_info().devices[ctx.device].cc) && use_radix && ncols <= INT_MAX && nrows > 1 && nrows <= INT_MAX && k <= INT_MAX) { + // GGML_CUDA_TOPK_WG=0 keeps the multi-pass radix kernels (same selected set) + static const bool wg = getenv("GGML_CUDA_TOPK_WG") == nullptr || atoi(getenv("GGML_CUDA_TOPK_WG")) != 0; + if (wg && k <= ncols && nrows <= INT_MAX) { + top_k_wg_kernel<<<(unsigned) nrows, TOPK_WG_THREADS, 0, stream>>>(src0_d, dst_d, (int) ncols, (int) k); + CUDA_CHECK(cudaGetLastError()); + return; + } top_k_radix_cuda(pool, src0_d, dst_d, ncols, nrows, k, stream, true); return; } diff --git a/ggml/src/ggml-cuda/unary.cu b/ggml/src/ggml-cuda/unary.cu index adc42022701f..3cdbc5709419 100644 --- a/ggml/src/ggml-cuda/unary.cu +++ b/ggml/src/ggml-cuda/unary.cu @@ -791,3 +791,38 @@ void ggml_cuda_op_scale_unary(ggml_backend_cuda_context & ctx, ggml_tensor * sca GGML_ABORT("unsupported unary op for fused scale+unary"); } } + +/* prefill shared-expert merge: out = other + src * sigmoid(gate[t]) with one gate value per row (token); the product + and the sum are rounded separately (no fma contraction), as the unfused MUL and ADD nodes -> bit-identical */ +#if defined(GGML_USE_HIP) +static __device__ __forceinline__ float sgma_mul_rn(const float a, const float b) { float r; asm("v_mul_f32_e32 %0, %1, %2" : "=v"(r) : "v"(a), "v"(b)); return r; } +static __device__ __forceinline__ float sgma_add_rn(const float a, const float b) { float r; asm("v_add_f32_e32 %0, %1, %2" : "=v"(r) : "v"(a), "v"(b)); return r; } +#else +static __device__ __forceinline__ float sgma_mul_rn(const float a, const float b) { return __fmul_rn(a, b); } +static __device__ __forceinline__ float sgma_add_rn(const float a, const float b) { return __fadd_rn(a, b); } +#endif +static __global__ void sigmoid_gate_mul_add_f32(const float * __restrict__ src, const float * __restrict__ other, + const float * __restrict__ gate, float * __restrict__ out, const int64_t ncols, const int64_t n) { + const int64_t i = ((int64_t) blockIdx.x * blockDim.x + threadIdx.x) * 4; + if (i >= n) { + return; + } + if (i + 4 <= n && (ncols % 4) == 0) { + const float g = op_sigmoid(gate[i / ncols]); + const float4 a = *(const float4 *) (src + i), b = *(const float4 *) (other + i); + *(float4 *) (out + i) = make_float4(sgma_add_rn(b.x, sgma_mul_rn(a.x, g)), sgma_add_rn(b.y, sgma_mul_rn(a.y, g)), + sgma_add_rn(b.z, sgma_mul_rn(a.z, g)), sgma_add_rn(b.w, sgma_mul_rn(a.w, g))); + } else { + for (int64_t j = i; j < n && j < i + 4; ++j) { + out[j] = sgma_add_rn(other[j], sgma_mul_rn(src[j], op_sigmoid(gate[j / ncols]))); + } + } +} + +void ggml_cuda_op_sigmoid_gate_mul_add(ggml_backend_cuda_context & ctx, const ggml_tensor * gate, const ggml_tensor * src, + const ggml_tensor * other, ggml_tensor * out) { + const int64_t n = ggml_nelements(out), ncols = out->ne[0]; + sigmoid_gate_mul_add_f32<<<(unsigned) ((n / 4 + 255) / 256 + 1), 256, 0, ctx.stream()>>>((const float *) src->data, + (const float *) other->data, (const float *) gate->data, (float *) out->data, ncols, n); + CUDA_CHECK(cudaGetLastError()); +} diff --git a/ggml/src/ggml-cuda/unary.cuh b/ggml/src/ggml-cuda/unary.cuh index dddcabc5bed5..07192f7975aa 100644 --- a/ggml/src/ggml-cuda/unary.cuh +++ b/ggml/src/ggml-cuda/unary.cuh @@ -128,3 +128,7 @@ __device__ __forceinline__ float ggml_cuda_op_swiglu_clamp_single(float gate, fl // fused GGML_OP_SCALE + GGML_UNARY_OP_{SILU,SIGMOID}: dst = op(scale*x + bias) void ggml_cuda_op_scale_unary(ggml_backend_cuda_context & ctx, ggml_tensor * scale_node, ggml_tensor * unary_node, uint16_t * dst16 = nullptr); + +// out = other + src * sigmoid(gate[row]) (gate: one value per row) +void ggml_cuda_op_sigmoid_gate_mul_add(ggml_backend_cuda_context & ctx, const ggml_tensor * gate, const ggml_tensor * src, + const ggml_tensor * other, ggml_tensor * out); diff --git a/src/llama-ple-disk.cpp b/src/llama-ple-disk.cpp index e21ab0bd1f2b..da374f9d9df5 100644 --- a/src/llama-ple-disk.cpp +++ b/src/llama-ple-disk.cpp @@ -6,6 +6,8 @@ #include <atomic> #include <chrono> #include <condition_variable> +#include <cstdlib> +#include <cstdio> #include <cstring> #include <mutex> #include <stdexcept> @@ -16,6 +18,7 @@ #if !defined(_WIN32) #include <cerrno> #include <fcntl.h> +#include <sys/mman.h> #include <unistd.h> #endif @@ -43,6 +46,7 @@ struct llama_ple_disk::impl { std::vector<uint8_t> raw; // [distinct rows, rs] std::vector<std::pair<int32_t, uint32_t>> misses; // (row, index into raw) float * dst = nullptr; // destination of the gather in progress + const uint8_t * src_full = nullptr; // preloaded table while a gather reads from it uint8_t * bounce0 = nullptr; // calling-thread bounce buffer std::mutex mtx; // one gather at a time; a model is shared by every context built on it @@ -62,6 +66,14 @@ struct llama_ple_disk::impl { std::atomic<size_t> next{0}; bool stop = false; + // LLAMA_PLE_PRELOAD=1: the whole table is read once into anonymous (huge-page) RAM by background threads; once + // complete, gathers dequantize straight from it (no syscalls, no cold reads) + uint8_t * full_map = nullptr; // mmap base (page aligned, covers the aligned span of the table) + size_t full_len = 0; + const uint8_t * full_rows = nullptr; // row 0 of the table inside full_map + std::atomic<bool> full_ready{false}; + std::thread full_loader; + uint64_t st_calls = 0, st_rows = 0, st_uniq = 0, st_hits = 0, st_reads = 0, st_bytes = 0; double st_ms = 0; @@ -114,7 +126,13 @@ struct llama_ple_disk::impl { } n_threads = std::max<int32_t>(1, p.n_threads); + if (const char * e = getenv("LLAMA_PLE_THREADS")) { + n_threads = std::max(1, atoi(e)); + } + if (getenv("LLAMA_PLE_PRELOAD") && atoi(getenv("LLAMA_PLE_PRELOAD")) != 0) { + start_preload(); + } if (p.cache_bytes >= rs) { size_t n = p.cache_bytes / rs; n_slots = 1; @@ -130,6 +148,12 @@ struct llama_ple_disk::impl { ~impl() { #if !defined(_WIN32) + if (full_loader.joinable()) { + full_loader.join(); + } + if (full_map) { + munmap(full_map, full_len); + } { std::lock_guard<std::mutex> lk(pm); stop = true; @@ -146,6 +170,89 @@ struct llama_ple_disk::impl { } #if !defined(_WIN32) + void start_preload() { + const size_t pg = 4096; + const off_t a0 = (off_t) offs & ~(off_t) (pg - 1); + const size_t need = (size_t) ((off_t) offs - a0) + (size_t) nrows * rs; + full_len = ((need + (2u << 20) - 1) / (2u << 20)) * (2u << 20); + void * m = mmap(nullptr, full_len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + if (m == MAP_FAILED) { + LLAMA_LOG_WARN("llama_ple_disk: preload mmap of %zu bytes failed (%s)\n", full_len, strerror(errno)); + full_len = 0; + return; + } + full_map = (uint8_t *) m; +#if defined(MADV_HUGEPAGE) + madvise(full_map, full_len, MADV_HUGEPAGE); // 2 MiB pages: 27 GB of 4 KiB pages would fragment the GTT pool +#endif + full_rows = full_map + ((off_t) offs - a0); + const std::string path = fname; + full_loader = std::thread([this, path, a0, need]() { + const auto t0 = std::chrono::steady_clock::now(); + int dfd = -1; +#if defined(O_DIRECT) + dfd = open(path.c_str(), O_RDONLY | O_DIRECT | O_CLOEXEC); // keeps the 27 GB out of the page cache +#endif + const bool dio = dfd >= 0; + if (!dio) { + dfd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + } + if (dfd < 0) { + LLAMA_LOG_WARN("llama_ple_disk: preload open failed (%s)\n", strerror(errno)); + return; + } + const size_t chunk = 64u << 20; + const size_t n_chunks = (need + chunk - 1) / chunk; + std::atomic<size_t> next_chunk{0}; + std::atomic<bool> failed{false}; + auto reader = [&]() { + for (;;) { + const size_t c = next_chunk.fetch_add(1); + if (c >= n_chunks || failed) { + break; + } + size_t len = std::min(chunk, need - c * chunk); + if (dio) { + len = ((len + 4095) / 4096) * 4096; + } + size_t got = 0; + while (got < len) { + const ssize_t r = pread(dfd, full_map + c * chunk + got, len - got, a0 + (off_t) (c * chunk + got)); + if (r < 0 && errno == EINTR) { + continue; + } + if (r <= 0) { + break; + } + got += (size_t) r; + } + if (got < std::min(chunk, need - c * chunk)) { + failed = true; + } + } + }; + std::vector<std::thread> rt; + for (int t = 0; t < 8; ++t) { + rt.emplace_back(reader); + } + for (auto & t : rt) { + t.join(); + } + close(dfd); + if (failed) { + LLAMA_LOG_WARN("llama_ple_disk: preload of %s failed, staying on pread\n", path.c_str()); + return; + } + full_ready = true; + LLAMA_LOG_INFO("llama_ple_disk: preloaded %.2f GiB in %.1f s (%s)\n", need / (1024.0 * 1024.0 * 1024.0), + std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count(), dio ? "direct" : "buffered"); + if (getenv("LLAMA_PLE_STATS")) { + fprintf(stderr, "ple_preload: done %.2f GiB in %.1f s\n", need / (1024.0 * 1024.0 * 1024.0), + std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count()); + } + }); + } + size_t bounce_size() const { return ((rs + block - 1) / block) * block + 2 * block; } @@ -196,7 +303,7 @@ struct llama_ple_disk::impl { // dequantize distinct row u into the first position that asked for it, then copy it to the others void dequant_row(size_t u) const { - const uint8_t * src = raw.data() + u * rs; + const uint8_t * src = src_full ? src_full + (size_t) order[runs[u]].first * rs : raw.data() + u * rs; float * first = dst + (size_t) order[runs[u]].second * ne0; if (to_float) { to_float(src, first, ne0); @@ -304,6 +411,20 @@ struct llama_ple_disk::impl { const size_t n_uniq = runs.size(); runs.push_back((uint32_t) n); + if (full_ready.load(std::memory_order_acquire)) { + const auto t_dq0 = std::chrono::steady_clock::now(); + dst = out; src_full = full_rows; + run_job(job_kind::dequant, n_uniq, 64, 256); + dst = nullptr; src_full = nullptr; + st_calls += 1; st_rows += n; st_uniq += n_uniq; st_hits += n_uniq; + const auto t_end = std::chrono::steady_clock::now(); + st_ms += std::chrono::duration<double, std::milli>(t_end - t0).count(); + if (getenv("LLAMA_PLE_STATS")) { + fprintf(stderr, "ple_gather: rows %zu uniq %zu preloaded | sort %.1f ms dequant %.1f ms\n", n, n_uniq, + std::chrono::duration<double, std::milli>(t_dq0 - t0).count(), std::chrono::duration<double, std::milli>(t_end - t_dq0).count()); + } + return; + } raw.resize(n_uniq * rs); misses.clear(); for (size_t u = 0; u < n_uniq; ++u) { @@ -318,6 +439,7 @@ struct llama_ple_disk::impl { misses.emplace_back(row, (uint32_t) u); } + const auto t_read0 = std::chrono::steady_clock::now(); if (!misses.empty()) { run_job(job_kind::read, misses.size(), 1, 2); if (n_slots) { @@ -329,9 +451,17 @@ struct llama_ple_disk::impl { } } + const auto t_dq0 = std::chrono::steady_clock::now(); dst = out; run_job(job_kind::dequant, n_uniq, 64, 256); dst = nullptr; + static const bool stats = getenv("LLAMA_PLE_STATS") != nullptr; + if (stats) { + const auto t_end = std::chrono::steady_clock::now(); + auto ms = [](auto a, auto b) { return std::chrono::duration<double, std::milli>(b - a).count(); }; + fprintf(stderr, "ple_gather: rows %zu uniq %zu hits %zu reads %zu | sort %.1f ms read %.1f ms dequant %.1f ms total %.1f ms (%d threads)\n", + n, n_uniq, n_uniq - misses.size(), misses.size(), ms(t0, t_read0), ms(t_read0, t_dq0), ms(t_dq0, t_end), ms(t0, t_end), n_threads); + } st_calls += 1; st_rows += n; diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index e4b99fd7feab..ef999883b00b 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -241,6 +241,7 @@ void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) { if (ple_on_disk && ple_w) { llama_ple_disk::params dp; dp.direct_io = false; // page-cached, so qwen4exp_ple_prefetch's readahead advice can land + if (const char * e = getenv("LLAMA_PLE_DIRECT")) { dp.direct_io = atoi(e) != 0; } ple_disk = std::make_shared<llama_ple_disk>(ml.fnames.at(ple_w->idx), ple_w->offs, ple_w->tensor->type, ple_w->tensor->ne[0], ple_rows, dp); @@ -1901,36 +1902,69 @@ void llm_graph_input_ple::set_input(const llama_ubatch * ubatch) { const auto * traits = ggml_get_type_traits(tab->type); + GGML_ASSERT(tab->type == GGML_TYPE_F32 || traits->to_float != nullptr); + embd_buf.resize(idx.size() * (size_t) dim); + + // The GPU idles while this runs (65536 rows for a 4096-token ubatch), so it is split over host threads: + // phase 1 queues every page read at once (memory-mapped table, random rows: one madvise per row instead of + // taking the faults one after another, ~0.2 ms each from NVMe), phase 2 dequantizes. Each row goes through + // the same to_float as before, so the result is identical. LLAMA_PLE_HOST_THREADS=1 is the serial loop. + static const int n_thr_cfg = [] { + const char * e = getenv("LLAMA_PLE_HOST_THREADS"); + const int hw = (int) std::max(1u, std::thread::hardware_concurrency()); + return e ? std::max(1, atoi(e)) : std::min(32, hw); + }(); + const size_t n_rows = idx.size(); + const int n_thr = (int) std::max<size_t>(1, std::min<size_t>((size_t) n_thr_cfg, n_rows / 1024)); + auto advise = [&](size_t k0, size_t k1) { #if defined(__linux__) || defined(__APPLE__) - // memory-mapped table, random rows: queue every page read at once instead of taking the - // faults one after another (~0.2 ms each from NVMe) while dequantizing below - { const long page = sysconf(_SC_PAGESIZE); - if (page > 0) { - const uintptr_t base = (uintptr_t) tab->data; - const uintptr_t mask = ~((uintptr_t) page - 1); - uintptr_t prev_page = 0; - for (const int32_t r : idx) { - const uintptr_t a0 = (base + (size_t) r * row_size) & mask; - const uintptr_t a1 = base + (size_t) r * row_size + row_size; - if (a0 != prev_page) { - madvise((void *) a0, a1 - a0, MADV_WILLNEED); - prev_page = a0; - } + if (page <= 0) { + return; + } + const uintptr_t base = (uintptr_t) tab->data; + const uintptr_t mask = ~((uintptr_t) page - 1); + uintptr_t prev_page = 0; + for (size_t k = k0; k < k1; ++k) { + const int32_t r = idx[k]; + const uintptr_t a0 = (base + (size_t) r * row_size) & mask; + const uintptr_t a1 = base + (size_t) r * row_size + row_size; + if (a0 != prev_page) { + madvise((void *) a0, a1 - a0, MADV_WILLNEED); + prev_page = a0; } } - } +#else + GGML_UNUSED(k0); GGML_UNUSED(k1); #endif - - embd_buf.resize(idx.size() * (size_t) dim); - for (size_t k = 0; k < idx.size(); ++k) { - const char * src = (const char *) tab->data + (size_t) idx[k] * row_size; - float * dst = embd_buf.data() + k * (size_t) dim; - if (tab->type == GGML_TYPE_F32) { - memcpy(dst, src, dim * sizeof(float)); - } else { - GGML_ASSERT(traits->to_float != nullptr); - traits->to_float(src, dst, dim); + }; + auto dequant = [&](size_t k0, size_t k1) { + for (size_t k = k0; k < k1; ++k) { + const char * src = (const char *) tab->data + (size_t) idx[k] * row_size; + float * dst = embd_buf.data() + k * (size_t) dim; + if (tab->type == GGML_TYPE_F32) { + memcpy(dst, src, dim * sizeof(float)); + } else { + traits->to_float(src, dst, dim); + } + } + }; + if (n_thr <= 1) { + advise(0, n_rows); + dequant(0, n_rows); + } else { + const size_t chunk = (n_rows + n_thr - 1) / n_thr; + for (int phase = 0; phase < 2; ++phase) { + std::vector<std::thread> workers; + workers.reserve(n_thr - 1); + for (int t = 1; t < n_thr; ++t) { + const size_t k0 = std::min(n_rows, (size_t) t * chunk), k1 = std::min(n_rows, k0 + chunk); + workers.emplace_back([&, k0, k1, phase] { if (phase == 0) { advise(k0, k1); } else { dequant(k0, k1); } }); + } + if (phase == 0) { advise(0, std::min(n_rows, chunk)); } else { dequant(0, std::min(n_rows, chunk)); } + for (auto & w : workers) { + w.join(); + } } } ggml_backend_tensor_set(embd, embd_buf.data(), 0, embd_buf.size() * sizeof(float)); diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp index 68402258accc..c9f4f827a619 100644 --- a/tests/test-backend-ops.cpp +++ b/tests/test-backend-ops.cpp @@ -3998,10 +3998,28 @@ struct test_indexer_head_sum : public test_case { const int blocks, heads, tokens, streams; const bool view, escape; const int alias; - test_indexer_head_sum(int blocks, int heads, int tokens, int streams=1, bool view=false, bool escape=false, int alias=0) - : blocks(blocks), heads(heads), tokens(tokens), streams(streams), view(view), escape(escape), alias(alias) {} + const int vis; // 1: compact visibility chain after the sum, 2: same with the log output also escaping + ggml_tensor * limits = nullptr; + test_indexer_head_sum(int blocks, int heads, int tokens, int streams=1, bool view=false, bool escape=false, int alias=0, int vis=0) + : blocks(blocks), heads(heads), tokens(tokens), streams(streams), view(view), escape(escape), alias(alias), vis(vis) {} std::string op_desc(ggml_tensor *) override { return "INDEXER_HEAD_SUM"; } - std::string vars() override { return VARS_TO_STR7(blocks, heads, tokens, streams, view, escape, alias); } + std::string vars() override { return VARS_TO_STR8(blocks, heads, tokens, streams, view, escape, alias, vis); } + void initialize_tensors(ggml_context * ctx) override { + std::mt19937 rng(1234); + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != nullptr; t = ggml_get_next_tensor(ctx, t)) { + if (t == limits) { + // block starts (some INT32_MAX: not a complete block), then tails, then row ids (0) + const int extra = 7; + std::vector<int32_t> v(ggml_nelements(t), 0); + std::uniform_int_distribution<int32_t> d(0, 4 * blocks); + for (int b = 0; b < blocks + extra; ++b) { v[b] = b % 97 == 5 ? INT32_MAX : 4 * b + (b % 3); } + for (int q = 0; q < tokens; ++q) { v[blocks + extra + q] = q % 13 == 0 ? 4 * (q * blocks / tokens) : d(rng); } + ggml_backend_tensor_set(t, v.data(), 0, ggml_nbytes(t)); + } else { + init_tensor_uniform(t); + } + } + } bool run_whole_graph() override { return true; } double max_nmse_err() override { return 0.0; } ggml_tensor * build_graph(ggml_context * ctx) override { @@ -4016,6 +4034,72 @@ struct test_indexer_head_sum : public test_case { } if (escape) sum=ggml_add(ctx, sum, first); if (alias) { sum->view_src=backing;sum->view_offs=alias==2 ? sizeof(float) : 0; } + if (vis) { + // as qwen4exp_apply_compact_visibility with n_seq == 1; the limits hold more blocks than the scorer sees + const int extra = 7; + limits = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, blocks + extra + 2 * tokens); + ggml_set_input(limits); + auto * starts = ggml_cast(ctx, ggml_view_1d(ctx, limits, blocks, 0), GGML_TYPE_F32); + auto * tails = ggml_cast(ctx, ggml_view_1d(ctx, limits, tokens, (blocks + extra) * sizeof(int32_t)), GGML_TYPE_F32); + tails = ggml_reshape_2d(ctx, tails, 1, tokens); + auto * lg = ggml_log(ctx, ggml_step(ctx, ggml_sub(ctx, ggml_repeat(ctx, tails, sum), starts))); + sum = ggml_add(ctx, sum, lg); + if (vis == 2) sum = ggml_add(ctx, sum, lg); + } + return sum; + } +}; + +// qwen4exp QSA indexer scorer: MUL_MAT(pooled keys F32 [128, blocks], q [128, 4*queries]) -> RESHAPE -> RELU -> head sum +// (-> compact visibility). On RDNA3.5 the whole chain is one fused GEMM unless GGML_CUDA_DISABLE_IDX_GEMM is set. +struct test_indexer_score : public test_case { + const int blocks, queries; + const bool vis, bounded; + ggml_tensor * limits = nullptr; + test_indexer_score(int blocks, int queries, bool vis, bool bounded = false) + : blocks(blocks), queries(queries), vis(vis), bounded(bounded) {} + std::string op_desc(ggml_tensor *) override { return "INDEXER_SCORE"; } + std::string vars() override { return VARS_TO_STR4(blocks, queries, vis, bounded); } + bool run_whole_graph() override { return true; } + double max_nmse_err() override { return 1e-5; } // WMMA rounds the operands to f16 + void initialize_tensors(ggml_context * ctx) override { + std::mt19937 rng(4321); + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != nullptr; t = ggml_get_next_tensor(ctx, t)) { + if (t == limits) { + const int extra = 5; + std::vector<int32_t> v(ggml_nelements(t), 0); + std::uniform_int_distribution<int32_t> d(0, 4 * blocks); + for (int b = 0; b < blocks + extra; ++b) { v[b] = b % 89 == 3 ? INT32_MAX : 4 * b; } + for (int q = 0; q < queries; ++q) { v[blocks + extra + q] = d(rng); } + ggml_backend_tensor_set(t, v.data(), 0, ggml_nbytes(t)); + } else { + init_tensor_uniform(t); + } + } + } + ggml_tensor * build_graph(ggml_context * ctx) override { + const int dim = 128, heads = 4; + auto * keys = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, dim, bounded ? blocks + 32 : blocks); + auto * k = bounded ? ggml_view_2d(ctx, keys, dim, blocks, keys->nb[1], 0) : keys; + auto * qall = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, dim, heads * (queries + (bounded ? 8 : 0))); + auto * q = bounded ? ggml_view_2d(ctx, qall, dim, heads * queries, qall->nb[1], 8 * heads * qall->nb[1]) : qall; + auto * score = ggml_mul_mat(ctx, k, q); + score = ggml_reshape_4d(ctx, score, blocks, heads, queries, 1); + score = ggml_relu(ctx, score); + ggml_tensor * sum = nullptr; + for (int h = 0; h < heads; ++h) { + auto * slice = ggml_view_3d(ctx, score, blocks, queries, 1, score->nb[2], score->nb[3], h * score->nb[1]); + sum = sum ? ggml_add(ctx, sum, slice) : ggml_cont(ctx, slice); + } + if (vis) { + const int extra = 5; + limits = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, blocks + extra + 2 * queries); + ggml_set_input(limits); + auto * starts = ggml_cast(ctx, ggml_view_1d(ctx, limits, blocks, 0), GGML_TYPE_F32); + auto * tails = ggml_cast(ctx, ggml_view_1d(ctx, limits, queries, (blocks + extra) * sizeof(int32_t)), GGML_TYPE_F32); + tails = ggml_reshape_2d(ctx, tails, 1, queries); + sum = ggml_add(ctx, sum, ggml_log(ctx, ggml_step(ctx, ggml_sub(ctx, ggml_repeat(ctx, tails, sum), starts)))); + } return sum; } }; @@ -5080,6 +5164,12 @@ struct test_gated_delta_net : public test_case { return VARS_TO_STR9(type, head_count, head_size, n_seq_tokens, n_seqs, v_repeat, permuted, kda, K); } + // the chunked prefill path (RDNA3.5: S_v = 128, >= 256 tokens, one sequence, final state only) runs its matrix + // products on fp16 WMMA operands (NMSE ~2e-7 vs the fp32 reference) instead of the exact fp32 recurrence + double max_nmse_err() override { + return (head_size == 128 && n_seq_tokens >= 256 && n_seqs == 1 && !kda && K == 1) ? 1e-6 : 1e-7; + } + test_gated_delta_net(ggml_type type = GGML_TYPE_F32, int64_t head_count = 4, int64_t head_size = 16, int64_t n_seq_tokens = 1, int64_t n_seqs = 1, int v_repeat = 1, bool permuted = false, bool kda = false, int64_t K = 1) @@ -5133,6 +5223,70 @@ struct test_gated_delta_net : public test_case { } }; +// The chunked GDN can calculate q/k/v convolution at the GDN node, after the normal conv/SiLU nodes. +// The activation x is produced by a preceding op and must remain allocated until that later read. +struct test_gdn_conv_prefill : public test_case { + const int n_tokens; + const int n_k_heads; + const int n_v_heads; + test_gdn_conv_prefill(int n_tokens, int n_k_heads, int n_v_heads) + : n_tokens(n_tokens), n_k_heads(n_k_heads), n_v_heads(n_v_heads) {} + std::string op_desc(ggml_tensor *) override { return "GDN_CONV_PREFILL"; } + std::string vars() override { return VARS_TO_STR3(n_tokens, n_k_heads, n_v_heads); } + bool run_whole_graph() override { return true; } + bool use_scheduler_allocation() override { return true; } + double max_nmse_err() override { return 2e-5; } + + void initialize_tensors(ggml_context * ctx) override { + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t; t = ggml_get_next_tensor(ctx, t)) { + if (t->op != GGML_OP_NONE) continue; + if (strcmp(t->name, "gate") == 0) init_tensor_uniform(t, -3.0f, -0.5f); + else if (strcmp(t->name, "beta") == 0) init_tensor_uniform(t, 0.0f, 0.5f); + else if (strcmp(t->name, "history") == 0 || strcmp(t->name, "state") == 0) + init_tensor_uniform(t, -0.01f, 0.01f); + else init_tensor_uniform(t, -0.25f, 0.25f); + } + } + + ggml_tensor * build_graph(ggml_context * ctx) override { + const int C = 128 * (2 * n_k_heads + n_v_heads); + ggml_tensor * x_src = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, C, n_tokens); + ggml_set_name(x_src, "x_src"); + ggml_tensor * x = ggml_scale(ctx, x_src, 0.5f); // an allocatable intermediate, not a permanent input + ggml_tensor * history = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 3, C); + ggml_set_name(history, "history"); + ggml_tensor * w = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 4, C); + ggml_set_name(w, "taps"); + ggml_tensor * cc = ggml_concat(ctx, history, ggml_transpose(ctx, x), 0); + ggml_tensor * conv = ggml_ssm_conv(ctx, ggml_reshape_3d(ctx, cc, n_tokens + 3, C, 1), w); + ggml_tensor * silu = ggml_silu(ctx, conv); + if (gf) ggml_build_forward_expand(gf, silu); + + // An independent full-size allocation between conv and GDN exercises scheduler allocation and fusion order. + ggml_tensor * scratch = ggml_scale(ctx, x_src, 0.25f); + if (gf) ggml_build_forward_expand(gf, scratch); + + auto view = [&](int heads, int offset) { + return ggml_view_4d(ctx, silu, 128, heads, n_tokens, 1, + 128 * sizeof(float), (size_t) C * sizeof(float), + (size_t) C * n_tokens * sizeof(float), (size_t) offset * sizeof(float)); + }; + ggml_tensor * q = view(n_k_heads, 0); + ggml_tensor * k = view(n_k_heads, 128 * n_k_heads); + ggml_tensor * v = view(n_v_heads, 256 * n_k_heads); + const float eps = 1e-6f; + q = ggml_scale(ctx, ggml_rms_norm(ctx, q, eps / 128), 1.0f / sqrtf(128.0f)); + k = ggml_scale(ctx, ggml_rms_norm(ctx, k, eps / 128), 1.0f / sqrtf(128.0f)); + ggml_tensor * g = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, 1, n_v_heads, n_tokens, 1); + ggml_set_name(g, "gate"); + ggml_tensor * beta = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, 1, n_v_heads, n_tokens, 1); + ggml_set_name(beta, "beta"); + ggml_tensor * state = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, 128, 128, n_v_heads, 1); + ggml_set_name(state, "state"); + return ggml_gated_delta_net(ctx, q, k, v, g, beta, state, 1); + } +}; + // GGML_OP_GATED_DELTA_NET + GGML_OP_CPY (recurrent cache fusion) struct test_gated_delta_net_cache_fusion : public test_case { const ggml_type type; @@ -5909,6 +6063,54 @@ struct test_swiglu_iq3_mmvq : public test_case { } }; +// Qwen3.8-Flash-Next prefill MoE (512 experts, top-10) with a skewed routing that reproduces the measured +// tokens-per-expert histogram of a 4096-token ubatch. The selected quant covers routed gate/up or down. +struct test_moe_prefill : public test_case { + const bool glu; const int64_t n; + const ggml_type quant; + std::string op_desc(ggml_tensor * t) override { GGML_UNUSED(t); return "MOE_PREFILL"; } + std::string vars() override { return VARS_TO_STR3(glu, n, quant); } + bool run_whole_graph() override { return true; } + double max_nmse_err() override { return 5e-4; } + uint64_t op_flops(ggml_tensor * t) override { GGML_UNUSED(t); return (uint64_t) 2 * 2560 * 640 * 10 * n * (glu ? 2 : 1); } + test_moe_prefill(bool glu, int64_t n, ggml_type type = GGML_TYPE_COUNT) + : glu(glu), n(n), quant(type == GGML_TYPE_COUNT ? (glu ? GGML_TYPE_IQ3_S : GGML_TYPE_IQ4_NL) : type) {} + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * ids_all = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, 512, n); + ggml_set_name(ids_all, "ids"); + ggml_tensor * ids = ggml_view_2d(ctx, ids_all, 10, n, ids_all->nb[1], 0); + if (glu) { + ggml_tensor * gw = ggml_new_tensor_3d(ctx, quant, 2560, 640, 512); + ggml_tensor * uw = ggml_new_tensor_3d(ctx, quant, 2560, 640, 512); + ggml_tensor * x = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 2560, 1, n); + return ggml_swiglu_split(ctx, ggml_mul_mat_id(ctx, gw, x, ids), ggml_mul_mat_id(ctx, uw, x, ids)); + } + ggml_tensor * dw = ggml_new_tensor_3d(ctx, quant, 640, 2560, 512); + ggml_tensor * x = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 640, 10, n); + return ggml_mul_mat_id(ctx, dw, x, ids); + } + void initialize_tensors(ggml_context * ctx) override { + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { + if (t->view_src) continue; + if (t->type != GGML_TYPE_I32) { init_tensor_uniform(t); continue; } + // counts per expert from the histogram {0:105, 1-15:134, 16-31:61, 32-63:72, 64-127:69, 128-255:39, 256-511:18, >=512:14} + const int bins[8][3] = {{0,0,105},{1,15,134},{16,31,61},{32,63,72},{64,127,69},{128,255,39},{256,511,18},{512,1480,14}}; + std::vector<int> cnt; + for (auto & b : bins) for (int i = 0; i < b[2]; ++i) cnt.push_back(b[0] + (b[2] > 1 ? (b[1] - b[0]) * i / (b[2] - 1) : 0)); + const int64_t total = 10 * n; int64_t sum = 0; for (int c : cnt) sum += c; + // scale to 10*n rows, then fix the rounding on the largest expert + for (int & c : cnt) c = (int) ((int64_t) c * total / sum); + sum = 0; for (int c : cnt) sum += c; cnt[511] += (int) (total - sum); + GGML_ASSERT(cnt[511] <= n); + std::vector<int> perm(512); for (int i = 0; i < 512; ++i) perm[i] = (i * 197) % 512; // spread hot experts + std::vector<int> slots; for (int e = 0; e < 512; ++e) for (int i = 0; i < cnt[e]; ++i) slots.push_back(perm[e]); + std::vector<int32_t> data(512 * n, 0); + for (int64_t tok = 0; tok < n; ++tok) for (int j = 0; j < 10; ++j) data[tok * 512 + j] = slots[tok + j * n]; + ggml_backend_tensor_set(t, data.data(), 0, data.size() * sizeof(int32_t)); + } + } +}; + // GGML_OP_MUL_MAT_ID + GGML_OP_ADD or GGML_OP_MUL struct test_mul_mat_id_fusion : public test_case { const ggml_type type_a; @@ -7853,6 +8055,137 @@ struct test_moe_weighted_reduction : public test_case { } }; +// MoE weighted reduction followed by the sigmoid-gated shared-expert merge (qwen4exp, Qwen3-Next, Qwen3.5-MoE): +// out = reduction + shexp * sigmoid(gate_logit). The CUDA backend fuses the reduction and the merge into one kernel. +// between = true puts the shared-expert GEMMs between them, as the model builders do; graph_optimize then moves the +// reduction down (RDNA3.5, >= 512 tokens), so allocation goes through the scheduler and buffer reuse follows the +// new order. hazard = true adds an in-between node that writes the experts in place after the reduction read them: +// the reorder must then keep the original order (checked on the node order, since both backends compute the same graph). +struct test_moe_shexp_merge : public test_case { + const ggml_type type_down; // F32: experts are an input; else produced by MUL_MAT_ID with these weights + const int64_t n_embd; + const int64_t n_expert_used; + const int64_t n_tokens; + const bool with_expert_scale; + const bool between; + const bool hazard; + const int n_mats = 16; + const int64_t k = 256; + ggml_tensor * writer_node = nullptr; + + test_moe_shexp_merge(ggml_type type_down, int64_t n_embd, int64_t n_expert_used, int64_t n_tokens, + bool with_expert_scale, bool between, bool hazard = false) + : type_down(type_down), n_embd(n_embd), n_expert_used(n_expert_used), n_tokens(n_tokens), + with_expert_scale(with_expert_scale), between(between), hazard(hazard) {} + + std::string vars() override { + return VARS_TO_STR7(type_down, n_embd, n_expert_used, n_tokens, with_expert_scale, between, hazard); + } + std::string op_desc(ggml_tensor * t) override { GGML_UNUSED(t); return "MOE_SHEXP_MERGE"; } + bool run_whole_graph() override { return true; } + bool use_scheduler_allocation() override { return between; } + double max_nmse_err() override { return type_down == GGML_TYPE_F32 ? 1e-6 : 5e-4; } + + void initialize_tensors(ggml_context * ctx) override { + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t; t = ggml_get_next_tensor(ctx, t)) { + if (t->op == GGML_OP_NONE && t->type != GGML_TYPE_I32) { + init_tensor_uniform(t); + } + } + init_mul_mat_id_ids(ctx, n_mats); + } + + double err(const float * a, const float * b, size_t n) override { + // both backends run the (possibly reordered) graph, so check the order itself: every reader of the memory the + // writer overwrites must still run before it (the reorder may move nodes that only read unaffected tensors) + if (hazard && gf && writer_node) { + const ggml_tensor * target = writer_node->view_src; + int i_writer = -1; + for (int i = 0; i < ggml_graph_n_nodes(gf); ++i) { + i_writer = ggml_graph_node(gf, i) == writer_node ? i : i_writer; + } + for (int i = i_writer + 1; i < ggml_graph_n_nodes(gf); ++i) { + const ggml_tensor * t = ggml_graph_node(gf, i); + for (int j = 0; j < GGML_MAX_SRC && t->src[j]; ++j) { + const ggml_tensor * r = t->src[j]->view_src ? t->src[j]->view_src : t->src[j]; + if (r == target) { + printf("%s reads %s after the in-place writer ", t->name, target->name); + return 1e9; + } + } + } + } + return nmse(a, b, n); + } + + ggml_tensor * build_graph(ggml_context * ctx) override { + ggml_tensor * x = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, k, n_tokens); + ggml_set_name(x, "x"); + + ggml_tensor * experts; + if (type_down == GGML_TYPE_F32) { + experts = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, n_embd, n_expert_used, n_tokens); + } else { + ggml_tensor * down = ggml_new_tensor_3d(ctx, type_down, k, n_embd, n_mats); + ggml_set_name(down, "down"); + ggml_tensor * ids = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, n_mats, n_tokens); + ids = ggml_view_2d(ctx, ids, n_expert_used, n_tokens, ids->nb[1], 0); + ggml_tensor * h = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, k, n_expert_used, n_tokens); + ggml_set_name(h, "h"); + experts = ggml_mul_mat_id(ctx, down, h, ids); + } + ggml_set_name(experts, "experts"); + ggml_tensor * weights = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 1, n_expert_used, n_tokens); + ggml_set_name(weights, "weights"); + + ggml_tensor * scaled = experts; + if (with_expert_scale) { + ggml_tensor * expert_scale = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 1, n_expert_used, n_tokens); + ggml_set_name(expert_scale, "expert_scale"); + scaled = ggml_mul(ctx, experts, expert_scale); + } + ggml_tensor * weighted = ggml_mul(ctx, scaled, weights); + ggml_set_name(weighted, "weighted_experts"); + + ggml_tensor * red = nullptr; + for (int64_t e = 0; e < n_expert_used; ++e) { + ggml_tensor * v = ggml_view_2d(ctx, weighted, n_embd, n_tokens, weighted->nb[2], e * weighted->nb[1]); + if (mode == MODE_TEST) { ggml_build_forward_expand(gf, v); } + red = red ? ggml_add(ctx, red, v) : v; + if (mode == MODE_TEST && e > 0) { ggml_build_forward_expand(gf, red); } + } + ggml_set_name(red, "moe_out"); + + // shared expert and its gate logit + ggml_tensor * w_sh = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, k, n_embd); + ggml_set_name(w_sh, "w_shexp"); + ggml_tensor * w_g = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, k, 1); + ggml_set_name(w_g, "w_shexp_gate"); + ggml_tensor * shexp, * logit; + if (between) { + shexp = ggml_mul_mat(ctx, w_sh, x); + if (mode == MODE_TEST) { ggml_build_forward_expand(gf, shexp); } + if (hazard) { + // overwrites the experts after the reduction read them (in place, a view of the experts) + writer_node = type_down == GGML_TYPE_F32 ? ggml_scale_inplace(ctx, experts, 0.5f) + : ggml_scale_inplace(ctx, weights, 0.5f); + ggml_set_name(writer_node, "writer"); + if (mode == MODE_TEST) { ggml_build_forward_expand(gf, writer_node); } + } + logit = ggml_mul_mat(ctx, w_g, x); + } else { + // precomputed, so SIGMOID -> MUL -> ADD directly follow the reduction + shexp = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, n_embd, n_tokens); + logit = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 1, n_tokens); + } + ggml_set_name(shexp, "shexp"); + ggml_tensor * gated = ggml_mul(ctx, shexp, ggml_sigmoid(ctx, logit)); + ggml_tensor * out = ggml_add(ctx, red, gated); + ggml_set_name(out, "out"); + return out; + } +}; + struct test_mul_mat_vec_fusion : public test_case { const ggml_type type; const ggml_glu_op glu_op; @@ -10605,6 +10938,16 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_indexer_head_sum(128,4,64,2,false,true)); for (int alias : {1,2}) test_cases.emplace_back(new test_indexer_head_sum(128,4,64,2,false,false,alias)); test_cases.emplace_back(new test_indexer_head_sum(10112,4,64)); + for (int blocks : {65, 1000, 1025, 5000}) { + for (bool vis : {false, true}) test_cases.emplace_back(new test_indexer_score(blocks, 512, vis)); + } + test_cases.emplace_back(new test_indexer_score(3001, 77, true)); + test_cases.emplace_back(new test_indexer_score(3001, 512, true, true)); + // with the compact visibility chain (fused on RDNA3.5 unless GGML_CUDA_DISABLE_IDX_VIS) + for (int blocks : {64, 1000, 10112}) test_cases.emplace_back(new test_indexer_head_sum(blocks,4,512,1,false,false,0,1)); + test_cases.emplace_back(new test_indexer_head_sum(1000,4,77,1,false,false,0,1)); + test_cases.emplace_back(new test_indexer_head_sum(1000,4,512,1,false,false,0,2)); + test_cases.emplace_back(new test_indexer_head_sum(1000,4,64,2,false,false,0,1)); for (int cols : {31, 32, 64, 128, 256, 288}) { for (int rows : {4095, 4096, 4097}) { @@ -11940,6 +12283,12 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { } test_cases.emplace_back(new test_mul_mat_id_fusion(GGML_TYPE_F16, GGML_TYPE_F32, 16, 16, false, 32, 32, 32, 3)); test_cases.emplace_back(new test_mul_mat_id_fusion(GGML_TYPE_Q8_0, GGML_TYPE_F32, 8, 4, false, 512, 129, 256, 2)); + test_cases.emplace_back(new test_moe_prefill(true, 512)); + test_cases.emplace_back(new test_moe_prefill(false, 512)); + test_cases.emplace_back(new test_moe_prefill(true, 511, GGML_TYPE_Q4_K)); + test_cases.emplace_back(new test_moe_prefill(true, 512, GGML_TYPE_Q4_K)); + test_cases.emplace_back(new test_moe_prefill(true, 513, GGML_TYPE_Q4_K)); + test_cases.emplace_back(new test_moe_prefill(false, 512, GGML_TYPE_Q5_1)); test_cases.emplace_back(new test_mul_mat_pair()); // gpt-oss issue with Vulkan mmq_id @@ -12342,6 +12691,12 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { } } + // qwen4exp QSA block selection: 512 queries, 512 of ~31k / ~62k blocks (one workgroup per row on RDNA3.5) + for (int64_t n : {31250, 62500, 65536}) { + test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {n, 512, 1, 1}, 512)); + test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {n, 64, 1, 1}, 512, 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)); @@ -12815,6 +13170,17 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { test_cases.emplace_back(new test_moe_weighted_reduction(2048, 15, 40, false, true)); test_cases.emplace_back(new test_moe_weighted_reduction(2048, 16, 32, false, true)); + // reduction + sigmoid-gated shared-expert merge: adjacent (fusion only) and with the shared-expert GEMMs in between + // (graph_optimize reorder at >= 512 tokens); n_embd 2560 / 2048 / 4608 cover 640, 512 and the 256-thread fallback + test_cases.emplace_back(new test_moe_shexp_merge(GGML_TYPE_F32, 64, 8, 33, false, false)); + test_cases.emplace_back(new test_moe_shexp_merge(GGML_TYPE_F32, 2560, 10, 64, true, false)); + test_cases.emplace_back(new test_moe_shexp_merge(GGML_TYPE_F32, 2048, 8, 512, false, true)); + test_cases.emplace_back(new test_moe_shexp_merge(GGML_TYPE_F32, 4608, 8, 512, true, true)); + test_cases.emplace_back(new test_moe_shexp_merge(GGML_TYPE_IQ4_NL, 2560, 10, 512, true, true)); + test_cases.emplace_back(new test_moe_shexp_merge(GGML_TYPE_Q8_0, 2048, 8, 512, false, true)); + test_cases.emplace_back(new test_moe_shexp_merge(GGML_TYPE_F32, 2560, 10, 512, true, true, true)); + test_cases.emplace_back(new test_moe_shexp_merge(GGML_TYPE_IQ4_NL, 2560, 10, 512, true, true, true)); + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 1, 1)); test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 16, 1, 1)); test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 16, 1, 1, 1, true, true)); @@ -12848,6 +13214,13 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { for (int n_tokens : {15, 16, 17, 64, 65, 2048}) { test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 16, 128, n_tokens, 1, 1, false, true)); } + // chunked prefill path (RDNA3.5): qwen3.8-style 16 k-heads x 3 = 48 v-heads + for (int n_tokens : {256, 300, 4096}) { + test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 16, 128, n_tokens, 1, 3)); + } + // Convolution/SiLU elided into the chunked GDN, allocated through the scheduler. + test_cases.emplace_back(new test_gdn_conv_prefill(256, 2, 2)); + test_cases.emplace_back(new test_gdn_conv_prefill(512, 2, 4)); // Multiple sequences must remain on the generic fallback. test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 16, 128, 64, 2, 1, false, true)); test_cases.emplace_back(new test_gated_delta_net(GGML_TYPE_F32, 32, 128, 15, 1, 1, false, true)); @@ -13030,6 +13403,12 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() { static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() { std::vector<std::unique_ptr<test_case>> test_cases; + if (getenv("GGML_PERF_MOE")) { + test_cases.emplace_back(new test_moe_prefill(true, 4096)); + test_cases.emplace_back(new test_moe_prefill(false, 4096)); + return test_cases; + } + for (ggml_type type : {GGML_TYPE_F32, GGML_TYPE_F16}) { for (int width : {90, 128}) { @@ -13514,6 +13893,14 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() { test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, nrows, 1, 1}, 2048)); } } + // qwen4exp QSA indexer scorer at 128k / 250k depth (fused GEMM + head sum + visibility on RDNA3.5) + for (int blocks : {31250, 62500}) { + test_cases.emplace_back(new test_indexer_score(blocks, 512, true)); + } + // qwen4exp QSA block selection at 128k / 250k depth: 512 queries, 512 blocks each + for (int64_t cols : {31250, 62500}) { + test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, 512, 1, 1}, 512)); + } // backend sampler: one row of the vocab (llama-sampler.cpp top_k) for (auto k : {20, 40}) { test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {151936, 1, 1, 1}, k)); From 12643af640857e5a1bec10e146b84fef8179b372 Mon Sep 17 00:00:00 2001 From: ARC <arc@empyreal.works> Date: Wed, 30 Sep 2026 01:01:28 +0200 Subject: [PATCH 208/212] ggml : backport persistent view initialization after buffer-size splits (#37) Co-authored-by: Yoshinao Kadowaki <nuts.kado@gmail.com> --- ggml/src/ggml-alloc.c | 41 +++++++++++++++++++++++++---------------- tests/test-alloc.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/ggml/src/ggml-alloc.c b/ggml/src/ggml-alloc.c index 5fd670fd0c43..4c7d18e8ec02 100644 --- a/ggml/src/ggml-alloc.c +++ b/ggml/src/ggml-alloc.c @@ -1169,6 +1169,20 @@ static void free_buffers(ggml_backend_buffer_t ** buffers, const size_t * n_buff free(*buffers); } +static bool alloc_ctx_tensors_finalize_views(struct ggml_context * ctx) { + for (struct ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) { + if (t->view_src != NULL && t->buffer == NULL) { + enum ggml_status status = ggml_backend_view_init(t); + if (status != GGML_STATUS_SUCCESS) { + GGML_LOG_ERROR("%s: failed to initialize view tensor %s\n", __func__, t->name); + return false; + } + } + } + + return true; +} + static bool alloc_tensor_range(struct ggml_context * ctx, struct ggml_tensor * first, struct ggml_tensor * last, ggml_backend_buffer_type_t buft, size_t size, @@ -1187,24 +1201,14 @@ static bool alloc_tensor_range(struct ggml_context * ctx, struct ggml_tallocr tallocr = ggml_tallocr_new(buffer); for (struct ggml_tensor * t = first; t != last; t = ggml_get_next_tensor(ctx, t)) { - enum ggml_status status = GGML_STATUS_SUCCESS; - if (t->data == NULL) { - if (t->view_src == NULL) { - status = ggml_tallocr_alloc(&tallocr, t); - } else if (t->buffer == NULL) { - status = ggml_backend_view_init(t); - } - } else { - if (t->view_src != NULL && t->buffer == NULL) { - // view of a pre-allocated tensor - status = ggml_backend_view_init(t); + if (t->view_src == NULL && t->data == NULL) { + enum ggml_status status = ggml_tallocr_alloc(&tallocr, t); + if (status != GGML_STATUS_SUCCESS) { + GGML_LOG_ERROR("%s: failed to allocate tensor %s\n", __func__, t->name); + free_buffers(buffers, n_buffers); + return false; } } - if (status != GGML_STATUS_SUCCESS) { - GGML_LOG_ERROR("%s: failed to initialize tensor %s\n", __func__, t->name); - free_buffers(buffers, n_buffers); - return false; - } } return true; @@ -1262,6 +1266,11 @@ static ggml_backend_buffer_t ggml_backend_alloc_ctx_tensors_from_buft_impl( return NULL; } + if (!alloc_ctx_tensors_finalize_views(ctx)) { + free_buffers(&buffers, &n_buffers); + return NULL; + } + ggml_backend_buffer_t buffer; if (n_buffers == 1) { buffer = buffers[0]; diff --git a/tests/test-alloc.cpp b/tests/test-alloc.cpp index 219eb5a4661c..0a53eac0f410 100644 --- a/tests/test-alloc.cpp +++ b/tests/test-alloc.cpp @@ -685,6 +685,31 @@ static void test_pinned_view_root_no_inplace() { GGML_ASSERT(!memory_overlap(root, child)); } +// Parent nbytes > max_size leaves cur_buf_size > max_size. A following view +// then triggers a split with this_size == 0, so the view-only residual range +// skips alloc_tensor_range unless views are finalized after all splits. +static void test_view_init_after_max_size_split() { + const size_t max_size = 16; + dummy_backend backend = dummy_backend_init(max_size); + auto [ctx, graph, ctx_ptr] = make_context(); + (void) graph; + + ggml_tensor * parent = make_input_with_size(ctx, 24); // 6 x f32, > max_size + ggml_tensor * view0 = ggml_view_1d(ctx, parent, 2, 0); + ggml_tensor * view1 = ggml_view_1d(ctx, parent, 2, 2 * sizeof(float)); + assign_names(ctx); + + ggml_backend_buffer_ptr buf(ggml_backend_alloc_ctx_tensors_from_buft(ctx, &backend.buffer_type)); + GGML_ASSERT(buf); + + for (ggml_tensor * t = ggml_get_first_tensor(ctx); t; t = ggml_get_next_tensor(ctx, t)) { + GGML_ASSERT(t->buffer != nullptr); + GGML_ASSERT(t->data != nullptr); + } + GGML_ASSERT(view0->view_src == parent); + GGML_ASSERT(view1->view_src == parent); +} + static void run(const char * name, void (*f)()) { printf("%s ", name); fflush(stdout); @@ -709,5 +734,6 @@ int main() { run("test_graph_optimize_alloc_dep", test_graph_optimize_alloc_dep); run("test_pinned_no_inplace", test_pinned_no_inplace); run("test_pinned_view_root_no_inplace", test_pinned_view_root_no_inplace); + run("test_view_init_after_max_size_split", test_view_init_after_max_size_split); return 0; } From 40cc375d834ec81647969f7809d6481b98d409a7 Mon Sep 17 00:00:00 2001 From: Baraxanx <baraxnaxgaming@gmail.com> Date: Tue, 29 Sep 2026 21:25:50 -0400 Subject: [PATCH 209/212] cuda: return 0 for invalid ROCmFP4 scale bytes in the HIP decoder The arithmetic UE4M3 half-scale decode (non-LUT branch) accepted bytes 0x7f-0xff and produced garbage scales, while the CPU decoder (rocmfp4.c) returns 0 for them. Loader validation of those bytes only runs with --check-tensors, so a crafted GGUF decoded differently on GPU and CPU. Guard x > 0x7e -> 0 in both branches to match CPU. Verified bit-identical to the CPU LUT decoder for all 256 scale byte values; nvcc sm_89 compile clean. No behavior change for valid bytes, so existing test-backend-ops results stand. Assisted-by: Hermes Agent (Qwen3.8-Flash-Next) --- ggml/rocmfp4/rocmfp4_hip_scale.cuh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ggml/rocmfp4/rocmfp4_hip_scale.cuh b/ggml/rocmfp4/rocmfp4_hip_scale.cuh index a783be2e9493..b69ac0d87f85 100644 --- a/ggml/rocmfp4/rocmfp4_hip_scale.cuh +++ b/ggml/rocmfp4/rocmfp4_hip_scale.cuh @@ -97,12 +97,15 @@ static __device__ __forceinline__ float rocmfp4_u32_as_f32(uint32_t bits) { #endif } -// ROCmFP4 validates scale bytes before backend execution, so HIP/ROCm hot -// paths can decode finite unsigned E4M3 half-scales directly without the -// generic FP8 NaN handling used by other formats. +// Decode invalid scale bytes (> 0x7e) as 0, matching the CPU decoder in +// rocmfp4.c. Loader validation of these bytes only runs with --check-tensors, +// so kernels must not trust them. static __device__ __forceinline__ float rocmfp4_ue4m3_to_fp32_half_finite(uint8_t x) { + if (x > 0x7e) { + return 0.0f; + } #if defined(GGML_USE_HIP) && GGML_ROCMFP4_USE_SCALE_LUT - return x <= 0x7e ? rocmfp4_scale_ue4m3_half_lut[x] : 0.0f; + return rocmfp4_scale_ue4m3_half_lut[x]; #else const int exp = (x >> 3) & 0xF; const int man = x & 0x7; From f6ade7dc76c400067c2918b4132fede4198615cf Mon Sep 17 00:00:00 2001 From: Piotr Wilkin <ilintar@gmail.com> Date: Thu, 1 Oct 2026 00:32:48 +0200 Subject: [PATCH 210/212] cuda : fix read past the end of the top-k MoE group in the alloc-dependency pass Pass the inclusive final node index instead of the exclusive group end. This keeps allocation dependencies in the graph when the group is terminal. Assisted-by: Codex --- ggml/src/ggml-cuda/ggml-cuda.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu index e7d24daf1fcb..fa7d0dcd5bbf 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -6921,7 +6921,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph static const bool disable_fusion = getenv("GGML_CUDA_DISABLE_FUSION") != nullptr && std::atoi(getenv("GGML_CUDA_DISABLE_FUSION")); auto add_alloc_deps = [&](size_t start, size_t last_node) { - + // last_node is the inclusive index of the final group node. for (size_t i = start; i < last_node; ++i) { params->add_alloc_dep(params->user_data, cgraph->nodes[i], cgraph->nodes[last_node]); @@ -7136,7 +7136,7 @@ static void ggml_backend_cuda_graph_optimize(ggml_backend_t backend, ggml_cgraph if (ggml_can_fuse_subgraph(cgraph, i, ops.size(), ops.data(), out_nodes, 2) && ggml_cuda_should_use_topk_moe(node, logits, weights, ids)) { - add_alloc_deps(i, i + ops.size()); + add_alloc_deps(i, i + ops.size() - 1); i += ops.size() - 1; } } From 4048e03c5ad98f91dc9d07e5171e06afe76c881c Mon Sep 17 00:00:00 2001 From: Gaetan Puleo <12990773+gaetan-puleo@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:43:49 +0200 Subject: [PATCH 211/212] hip: gate the MMB matmul path by quant type, not architecture PR #75 gated the BF16 WMMA matmul path (mmb) to LLM_ARCH_QWEN4EXP only, which fixed real prefill regressions on some models but also removed the path from models where it is a large win (e.g. Qwen3.8-27B Q8_0). Only Q8_0 and IQ4_NL have a dedicated fast decoder in mmb_dispatch_quant (the special int constants 1 and 0); every other quant falls back to the generic decode. So the benefit tracks the weight quant type, not the arch. This keeps the path opt-in for all architectures but restricts mmb_quant_type() to the quants that actually have a fast decoder, which removes the regressions PR #75 addressed while keeping the Q8_0 wins. Strix Halo (gfx1151), pp2048, depth 0, -b 2048 -ub 2048 (llama-bench): model / quant mmb off mmb all this Gemma 4 26B Q8_0 2082 2452 2316 Qwen3.8-27B Q8_0 213 499 453 Qwen3.8-27B IQ3_S 378 301 382 Signal-3.8-27B Q4_K_XL 411 276 411 --- ggml/src/ggml-cuda/mmb-quant.cuh | 1 + src/llama-context.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-cuda/mmb-quant.cuh b/ggml/src/ggml-cuda/mmb-quant.cuh index ba0bce882f11..8780d246b381 100644 --- a/ggml/src/ggml-cuda/mmb-quant.cuh +++ b/ggml/src/ggml-cuda/mmb-quant.cuh @@ -246,6 +246,7 @@ __device__ __forceinline__ void mmb_load_quant_tile(const uint8_t * weights, siz } static bool mmb_quant_type(ggml_type type) { + if (type != GGML_TYPE_Q8_0 && type != GGML_TYPE_IQ4_NL) return false; switch (type) { case GGML_TYPE_Q1_0: case GGML_TYPE_Q2_0: diff --git a/src/llama-context.cpp b/src/llama-context.cpp index 0d289ba03d6b..9968fbc4c28c 100644 --- a/src/llama-context.cpp +++ b/src/llama-context.cpp @@ -381,7 +381,7 @@ llama_context::llama_context( ggml_backend_reg_t reg = dev ? ggml_backend_dev_backend_reg(dev) : nullptr; auto * set_mmb_fn = reg ? (void (*)(ggml_backend_t, bool)) ggml_backend_reg_get_proc_address(reg, "ggml_backend_cuda_set_mmb_enabled") : nullptr; if (set_mmb_fn) { - set_mmb_fn(backend.get(), model.arch == LLM_ARCH_QWEN4EXP); + set_mmb_fn(backend.get(), true); } } From 86bd254adf3193e862b2cf80b9aeb3c9301b0e1d Mon Sep 17 00:00:00 2001 From: klucsik <klucsik@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:50:22 +0000 Subject: [PATCH 212/212] cuda : widen QSA selected-key block ids to uint32 and lift the depth bound to 1M The maskless selected-key route was capped at 262140 keys because qsa_prefill stores whole-block ids as uint16 and uses 0xFFFF as the padding sentinel: 65535 real blocks * ratio 4 = 262140, and at n_kv = 262144 the 65536th block id collides with the sentinel. A maskless ubatch above 512 tokens at any depth past 262140 has no kernel that accepts it, so fattn aborts the instance (#85, and the repeated aborts at k [256,265984,2,1] under -c 524288). Store the union's block ids as uint32 with 0xFFFFFFFF as the sentinel and raise the bound to GGML_QSA_MAX_KEYS = 1048576, the max context the Qwen3.8-Flash-Next card supports with RoPE/YaRN. This replaces the earlier cap-the-gate approach proposed in review on this PR: the fatal band disappears entirely and per-slot context can go past 262140 without falling back to the masked route. - qsa-prefill.cu: ublk becomes uint32 (umask stays uint16: 4 bits per query slot for QSA3_G=4 queries over ratio=4 keys). The descriptor load now reads one uint4 per chunk instead of a uint2 of two packed id-halves; cap is always a multiple of 4, so (g*cap + 4c)*4 stays 16B-aligned. blk_of() tests the sentinel per element; pad slots still resolve to block 0 with no mask bits and are discarded exactly as before. - qsa-decode.cu: depth bound follows the same constant. Its ids are gathered directly (no block packing); the per-row unique table is sized by ids->ne[0] (<= 2560), not by nk, so depth was never structurally limited. - common.cuh: one shared GGML_QSA_MAX_KEYS so the two kernel gates cannot drift apart again; qwen4exp.cpp mirrors it (llama/ must not include ggml-cuda). - qwen4exp.cpp: QSA_MASKLESS_MAX_KV = 1048576 replaces the bogus 16777216, which was checked against neither kernel bound. Verified on the host only: the CPU build of libllama/ggml compiles, test-qsa-prefix and test-qsa-fast pass, and a host harness reproducing the writer/reader lines proves the old and new encodings agree for every id < 65535, that the old one mis-serves 65535/65536/66495 (sentinel aliasing and truncation) where the new one is exact, that padding still resolves to a discarded block 0, and 20000 randomised lists round-trip. NOT run on gfx1151: no ROCm/HIP toolchain here, so the kernel itself is unbuilt and unmeasured. Expect +16 MiB of pool per ubatch at ids=2051, n_q=4096 (uint32 ublk doubles that buffer). Assisted-by: Piclaw (Qwen3.8-Flash) --- ggml/src/ggml-cuda/common.cuh | 7 ++++ ggml/src/ggml-cuda/qsa-decode.cu | 2 +- ggml/src/ggml-cuda/qsa-prefill.cu | 56 +++++++++++++++++-------------- src/models/qwen4exp.cpp | 10 +++++- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index 0e1e888f6bf3..af1b61cfa306 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -93,6 +93,13 @@ #define GGML_CUDA_CC_IS_RDNA2(cc) (cc >= GGML_CUDA_CC_RDNA2 && cc < GGML_CUDA_CC_RDNA3) #define GGML_CUDA_CC_IS_RDNA3_0(cc) (cc >= GGML_CUDA_CC_RDNA3 && cc < GGML_CUDA_CC_RDNA3_5) #define GGML_CUDA_CC_IS_RDNA3_5(cc) (cc >= GGML_CUDA_CC_RDNA3_5 && cc < GGML_CUDA_CC_RDNA4) + +// max selected-key depth the QSA prefill/decode kernels serve. The selected list addresses whole blocks of 4 keys, +// and the union stores block ids as uint32 with 0xFFFFFFFF reserved as the padding sentinel, so the space is not +// the limit: this bound matches the largest context the Q3.8-Flash-Next card supports with RoPE/YaRN scaling. +// The model-side maskless gate (qwen4exp.cpp) and both kernel support checks must agree with it exactly, so that +// any n_kv above the bound takes the masked route instead of reaching a kernel that refuses the op (GGML_ABORT). +#define GGML_QSA_MAX_KEYS 1048576 #define GGML_CUDA_CC_IS_RDNA3(cc) (GGML_CUDA_CC_IS_RDNA3_0(cc) || GGML_CUDA_CC_IS_RDNA3_5(cc)) #define GGML_CUDA_CC_IS_RDNA4(cc) (cc >= GGML_CUDA_CC_RDNA4) #define GGML_CUDA_CC_IS_GCN_APU(cc) ((cc) == GGML_CUDA_CC_GFX909 || (cc) == GGML_CUDA_CC_GFX90C) diff --git a/ggml/src/ggml-cuda/qsa-decode.cu b/ggml/src/ggml-cuda/qsa-decode.cu index ba83164230c4..1215357aaf02 100644 --- a/ggml/src/ggml-cuda/qsa-decode.cu +++ b/ggml/src/ggml-cuda/qsa-decode.cu @@ -89,7 +89,7 @@ bool ggml_cuda_flash_attn_ext_qsa_decode_supported(ggml_backend_cuda_context & c if (bias != 0.0f || cap != 0.0f || q->type != GGML_TYPE_F32 || k->type != GGML_TYPE_F16 || v->type != GGML_TYPE_F16 || dst->type != GGML_TYPE_F32 || ids->type != GGML_TYPE_I32 || q->ne[0] != 256 || k->ne[0] != 256 || v->ne[0] != 256 || q->ne[1] < 1 || q->ne[1] > QSA_DECODE_MAX_QUERIES || - k->ne[1] < 1 || k->ne[1] > 262144 || v->ne[1] != k->ne[1] || + k->ne[1] < 1 || k->ne[1] > GGML_QSA_MAX_KEYS || v->ne[1] != k->ne[1] || k->ne[2] < 1 || q->ne[2] % k->ne[2] || v->ne[2] != k->ne[2] || q->ne[3] != 1 || k->ne[3] != 1 || v->ne[3] != 1 || q->nb[0] != 4 || k->nb[0] != 2 || v->nb[0] != 2 || ids->nb[0] != 4 || diff --git a/ggml/src/ggml-cuda/qsa-prefill.cu b/ggml/src/ggml-cuda/qsa-prefill.cu index 2d7b32d387e2..2bf6289eba91 100644 --- a/ggml/src/ggml-cuda/qsa-prefill.cu +++ b/ggml/src/ggml-cuda/qsa-prefill.cu @@ -17,6 +17,10 @@ static __device__ __forceinline__ float qsa3_h2f(const uint16_t h) { return (flo // union builder, kernel A: per query row, check that the (validity-transformed) row is non-decreasing; if not, rank-sort it into srow[q] and set sflag[q]. Sentinel 0x7FFFFFFF for invalid (-1 / >= nk) entries. #define QSA3_SENT 0x7FFFFFFF + +// padding sentinel in the union's block-id list. The list is uint32 so the id space is not what limits the selected +// depth (uint16 did: 65535 real blocks * ratio 4 = 262140 keys, which collided with the 0xFFFF padding sentinel). +#define QSA3_BLK_SENT 0xFFFFFFFFu __global__ __launch_bounds__(256) void qsa3_rows_kernel( const int * __restrict__ ids, const size_t i1, const int ns, const int nk, int * __restrict__ srow, int * __restrict__ sflag, const char * mask, size_t m1, const int np2) { @@ -83,7 +87,7 @@ static __device__ __forceinline__ int qsa3_get(const int * __restrict__ p, const __global__ __launch_bounds__(QSA3_MERGE_LANES) void qsa3_merge_kernel( const int * __restrict__ ids, const size_t i1, const int n_q, const int ns, const int nk, const int * __restrict__ srow, const int * __restrict__ sflag, - uint16_t * __restrict__ ublk, uint16_t * __restrict__ umask, int * __restrict__ ucount, const int cap) { + uint32_t * __restrict__ ublk, uint16_t * __restrict__ umask, int * __restrict__ ucount, const int cap) { extern __shared__ int rows_s[]; __shared__ int scan_s[QSA3_MERGE_LANES]; const int lane = threadIdx.x; @@ -114,7 +118,7 @@ __global__ __launch_bounds__(QSA3_MERGE_LANES) void qsa3_merge_kernel( for (int qi = 0; qi < QSA3_G; ++qi) { if (cnt[qi] > 0) { kmax = max(kmax, qsa3_get(rp[qi], cnt[qi] - 1, nk, raw[qi])); if (ref < 0) { ref = qi; } } } - uint16_t * ob = ublk + (size_t) g * cap, * om = umask + (size_t) g * cap; + uint32_t * ob = ublk + (size_t) g * cap; uint16_t * om = umask + (size_t) g * cap; if (kmax < 0) { if (lane == 0) { ucount[g] = 0; } return; } const int rc = cnt[ref]; const int lo = lane == 0 ? 0 : (qsa3_get(rp[ref], (int) (((long long) rc * lane) / QSA3_MERGE_LANES), nk, raw[ref]) & ~3); @@ -184,12 +188,12 @@ __global__ __launch_bounds__(QSA3_MERGE_LANES) void qsa3_merge_kernel( p[qi] = pn; head[qi] = pn < pe[qi] ? rr[pn] : QSA3_SENT; } } - ob[o] = (uint16_t) b; om[o] = (uint16_t) mk; ++o; + ob[o] = (uint32_t) b; om[o] = (uint16_t) mk; ++o; } } if (lane == QSA3_MERGE_LANES - 1) { int t = total; - while (t & 3) { ob[t] = 0xFFFFu; om[t] = 0; ++t; } + while (t & 3) { ob[t] = QSA3_BLK_SENT; om[t] = 0; ++t; } ucount[g] = t; } } @@ -203,7 +207,7 @@ struct qsa3_layout { template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( const float * __restrict__ q, const uint16_t * __restrict__ pk, const uint16_t * __restrict__ pv, - const uint16_t * __restrict__ mask, const uint16_t * __restrict__ ublk, const uint16_t * __restrict__ umask, + const uint16_t * __restrict__ mask, const uint32_t * __restrict__ ublk, const uint16_t * __restrict__ umask, const int * __restrict__ ucount, float * __restrict__ out, const qsa3_layout s) { #if defined(__HIP_DEVICE_COMPILE__) && !defined(RDNA3) NO_DEVICE_CODE; @@ -227,7 +231,7 @@ template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( pkg = pk + (size_t) kvh * nblk * 1024; pvg = pv + (size_t) kvh * nblk * 1024; } - const uint16_t * gblk = ublk + (size_t) g * s.cap; + const uint32_t * gblk = ublk + (size_t) g * s.cap; const uint16_t * gmsk = umask + (size_t) g * s.cap; const int nchunks = ucount[g] >> 2; @@ -271,16 +275,16 @@ template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( const uint16_t * maskq = (mask && w < 3 && own_query < s.n_q) ? reinterpret_cast<const uint16_t *>(reinterpret_cast<const char *>(mask) + (size_t) own_query * s.m1) : nullptr; - auto load_desc = [&](const int c, uint32_t & b01, uint32_t & b23, uint32_t & m01, uint32_t & m23) { - const uint2 bb = *reinterpret_cast<const uint2 *>(gblk + 4*c); + auto load_desc = [&](const int c, uint32_t * b, uint32_t & m01, uint32_t & m23) { + const uint4 bb = *reinterpret_cast<const uint4 *>(gblk + 4*c); const uint2 mm = *reinterpret_cast<const uint2 *>(gmsk + 4*c); - b01 = bb.x; b23 = bb.y; m01 = mm.x; m23 = mm.y; + b[0] = bb.x; b[1] = bb.y; b[2] = bb.z; b[3] = bb.w; m01 = mm.x; m23 = mm.y; }; - auto blk_of = [&](const uint32_t b01, const uint32_t b23, const int j) -> int { - const uint32_t v = (j < 2 ? b01 : b23) >> (16 * (j & 1)) & 0xFFFFu; - return v == 0xFFFFu ? 0 : (int) v; + // a padded slot reads block 0 but carries no mask bits, so its contribution is discarded downstream + auto blk_of = [&](const uint32_t * b, const int j) -> int { + return b[j] == QSA3_BLK_SENT ? 0 : (int) b[j]; }; - auto load_k = [&](const uint32_t b01, const uint32_t b23, const uint32_t m01, const uint32_t m23, v16s * kf) { + auto load_k = [&](const uint32_t * b, const uint32_t m01, const uint32_t m23, v16s * kf) { const int j = r >> 2; const uint32_t mk = ((j < 2 ? m01 : m23) >> (16 * (j & 1))) & 0xffffu; if (((mk >> (r & 3)) & 0x1111u) == 0) { @@ -288,7 +292,7 @@ template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( kf[1] = v16s{}; return; } - const int kb = blk_of(b01, b23, j); + const int kb = blk_of(b, j); if constexpr (DIRECT) { // the packed 16-half chunk at (key, w, t) is dims (2w+t)*16 .. +15 of that key's row const int key = 4*kb + (r & 3); @@ -318,9 +322,10 @@ template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( return make_uint2(v.x & lo, v.y & hi); }; - uint32_t b01 = 0, b23 = 0, m01 = 0, m23 = 0; + uint32_t b[4] = { 0, 0, 0, 0 }; + uint32_t m01 = 0, m23 = 0; v16s kf[2]; - if (nchunks > 0) { load_desc(0, b01, b23, m01, m23); load_k(b01, b23, m01, m23, kf); } + if (nchunks > 0) { load_desc(0, b, m01, m23); load_k(b, m01, m23, kf); } for (int c = 0; c < nchunks; ++c) { v8f sc[3]; @@ -333,7 +338,7 @@ template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( } v16s vf[2]; { - const int kb0 = blk_of(b01, b23, 0), kb1 = blk_of(b01, b23, 1), kb2 = blk_of(b01, b23, 2), kb3 = blk_of(b01, b23, 3); + const int kb0 = blk_of(b, 0), kb1 = blk_of(b, 1), kb2 = blk_of(b, 2), kb3 = blk_of(b, 3); #pragma unroll for (int t = 0; t < 2; ++t) { const int d = 32*w + 16*t + r; @@ -360,9 +365,10 @@ template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( } } } - uint32_t nb01 = 0, nb23 = 0, nm01 = 0, nm23 = 0; + uint32_t nb[4] = { 0, 0, 0, 0 }; + uint32_t nm01 = 0, nm23 = 0; v16s kfn[2]; - if (c + 1 < nchunks) { load_desc(c + 1, nb01, nb23, nm01, nm23); load_k(nb01, nb23, nm01, nm23, kfn); } + if (c + 1 < nchunks) { load_desc(c + 1, nb, nm01, nm23); load_k(nb, nm01, nm23, kfn); } else { kfn[0] = kf[0]; kfn[1] = kf[1]; } #pragma unroll for (int i = 0; i < 3; ++i) { @@ -391,7 +397,7 @@ template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( const int j = e >> 1, cc = 2*(e & 1) + hi; const uint32_t mk = ((j < 2 ? m01 : m23) >> (16 * (j & 1))) & 0xFFFFu; const bool kv = (mk >> (own_qi * 4 + cc)) & 1u; - if (maskq && kv) { scf[e] += qsa3_h2f(maskq[4 * blk_of(b01, b23, j) + cc]); } + if (maskq && kv) { scf[e] += qsa3_h2f(maskq[4 * blk_of(b, j) + cc]); } if (!kv) { scf[e] = -INFINITY; } } float mloc = scf[0]; @@ -447,7 +453,7 @@ template<bool DIRECT> __global__ __launch_bounds__(256) void qsa3_attn_kernel( for (int t = 0; t < 2; ++t) { O[i][t] = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(vf[t], pf, O[i][t]); } } kf[0] = kfn[0]; kf[1] = kfn[1]; - b01 = nb01; b23 = nb23; m01 = nm01; m23 = nm23; + b[0] = nb[0]; b[1] = nb[1]; b[2] = nb[2]; b[3] = nb[3]; m01 = nm01; m23 = nm23; } if (w < 3 && hi == 0) { l_s[own_row] = l; } @@ -486,7 +492,7 @@ bool ggml_cuda_flash_attn_ext_qsa_prefill_supported(ggml_backend_cuda_context & return bias==0 && softcap==0 && q->type==GGML_TYPE_F32 && k->type==GGML_TYPE_F16 && v->type==GGML_TYPE_F16 && dst->type==GGML_TYPE_F32 && ids->type==GGML_TYPE_I32 && q->ne[0]==256 && k->ne[0]==256 && v->ne[0]==256 && q->ne[1]>=128 && q->ne[1]<=INT_MAX && - k->ne[1]>0 && k->ne[1]<=262140 && k->ne[1]%4==0 && v->ne[1]==k->ne[1] && + k->ne[1]>0 && k->ne[1]<=GGML_QSA_MAX_KEYS && k->ne[1]%4==0 && v->ne[1]==k->ne[1] && k->ne[2]>0 && k->ne[2]<=65535 && q->ne[2]==12*k->ne[2] && v->ne[2]==k->ne[2] && q->ne[3]==1 && k->ne[3]==1 && v->ne[3]==1 && ggml_nelements(k)/4<=INT_MAX && q->nb[0]==4 && k->nb[0]==2 && v->nb[0]==2 && ids->nb[0]==4 && @@ -534,7 +540,7 @@ void ggml_cuda_flash_attn_ext_qsa_prefill(ggml_backend_cuda_context & ctx, ggml_ } const int ngroups = (n_q + QSA3_G - 1) / QSA3_G; const int cap = (QSA3_G * ns + 3) & ~3; - ggml_cuda_pool_alloc<uint16_t> ublk(ctx.pool(), (size_t) ngroups * cap); + ggml_cuda_pool_alloc<uint32_t> ublk(ctx.pool(), (size_t) ngroups * cap); ggml_cuda_pool_alloc<uint16_t> umask(ctx.pool(), (size_t) ngroups * cap); ggml_cuda_pool_alloc<int> ucount(ctx.pool(), (size_t) ngroups); ggml_cuda_pool_alloc<int> srow(ctx.pool(), (size_t) n_q * ns); @@ -557,11 +563,11 @@ void ggml_cuda_flash_attn_ext_qsa_prefill(ggml_backend_cuda_context & ctx, ggml_ const ggml_cuda_kernel_launch_params launch(dim3(ngroups, k->ne[2]), dim3(256), 0, ctx.stream()); if (qsa_direct) { ggml_cuda_kernel_launch(qsa3_attn_kernel<true>, launch, (const float *) q->data, (const uint16_t *) k->data, (const uint16_t *) v->data, - m ? (const uint16_t *) m->data : nullptr, (const uint16_t *) ublk.get(), (const uint16_t *) umask.get(), + m ? (const uint16_t *) m->data : nullptr, (const uint32_t *) ublk.get(), (const uint16_t *) umask.get(), (const int *) ucount.get(), (float *) dst->data, layout); } else { ggml_cuda_kernel_launch(qsa3_attn_kernel<false>, launch, (const float *) q->data, packed_k.get(), packed_v.get(), - m ? (const uint16_t *) m->data : nullptr, (const uint16_t *) ublk.get(), (const uint16_t *) umask.get(), + m ? (const uint16_t *) m->data : nullptr, (const uint32_t *) ublk.get(), (const uint16_t *) umask.get(), (const int *) ucount.get(), (float *) dst->data, layout); } CUDA_CHECK(cudaGetLastError()); diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp index d071c5281acb..8c15f88e7d0f 100644 --- a/src/models/qwen4exp.cpp +++ b/src/models/qwen4exp.cpp @@ -829,11 +829,19 @@ static int64_t qwen4exp_query_strip(int64_t n_tokens, int64_t n_stream) { // attention kernels understand that layout (F16 K/V, head 256, single stream; qsa_decode takes 1..512 queries, // qsa_prefill larger ubatches). They exist only in the HIP backend for RDNA3.5; the gate does not check the device, so // on any other backend the resulting maskless op is not taken by a QSA kernel. +// +// The depth must also fit the kernel that will run the op. Both selected-key kernels take GGML_QSA_MAX_KEYS +// (common.cuh): qsa_prefill indexes whole blocks of ratio keys in a uint32 union list whose 0xFFFFFFFF slot is a +// padding sentinel, qsa_decode gathers keys directly. The model side cannot see the backend, so it mirrors the +// bound; the kernels' own support checks are what actually decide, and they must agree with this constant. +static constexpr int64_t QSA_MASKLESS_MAX_KV = 1048576; // == GGML_QSA_MAX_KEYS, kept as a literal: llama/ must not depend on ggml-cuda headers +static_assert(QSA_MASKLESS_MAX_KV%4 == 0, "qsa_prefill requires a multiple-of-4 key count at its depth bound"); + static bool qwen4exp_use_block_selection(bool blk_bias, int64_t n_stream, int64_t ratio, int64_t n_kv, const llama_ubatch & ubatch, const llama_cparams & cparams, const llama_hparams & hparams, ggml_type type_k, ggml_type type_v) { return blk_bias && cparams.causal_attn && n_stream==1 && ratio>1 && hparams.indexer_top_k%ratio==0 && - n_kv>hparams.indexer_top_k+ratio-1 && n_kv<=16777216 && ubatch.token && + n_kv>hparams.indexer_top_k+ratio-1 && n_kv<=QSA_MASKLESS_MAX_KV && ubatch.token && cparams.flash_attn && cparams.offload_kqv && hparams.f_max_alibi_bias==0.0f && !hparams.attn_soft_cap && hparams.n_embd_head_k()==256 && hparams.n_embd_head_v()==256 && type_k==GGML_TYPE_F16 && type_v==GGML_TYPE_F16;