From 6a1a922d269908a29cbd4b49c27e6a8e7fd10fae Mon Sep 17 00:00:00 2001 From: Niklas Wenzel Date: Sat, 5 Sep 2026 12:19:47 +0200 Subject: [PATCH 01/31] metal : fix memory leak in early return (#28399) --- ggml/src/ggml-metal/ggml-metal-context.m | 1 + 1 file changed, 1 insertion(+) diff --git a/ggml/src/ggml-metal/ggml-metal-context.m b/ggml/src/ggml-metal/ggml-metal-context.m index e1129db30..6cdc4006b 100644 --- a/ggml/src/ggml-metal/ggml-metal-context.m +++ b/ggml/src/ggml-metal/ggml-metal-context.m @@ -111,6 +111,7 @@ ggml_metal_t ggml_metal_init(ggml_metal_device_t dev) { id queue = ggml_metal_device_get_queue(dev); if (queue == nil) { GGML_LOG_ERROR("%s: error: failed to create command queue\n", __func__); + free(res); return NULL; } From 74a7c897f049c17e7080423aa2111776eff6ebbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20G=C3=A4=C3=9Fler?= Date: Sat, 5 Sep 2026 22:42:35 +0200 Subject: [PATCH 02/31] Github: limit blank issues to maintainers (#28435) --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0d246533c..570e83e77 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: Got an idea? url: https://github.com/ggml-org/llama.cpp/discussions/categories/ideas From 971595d6697f53b215d02a8381f8b5af142a4d86 Mon Sep 17 00:00:00 2001 From: Niklas Wenzel Date: Sun, 6 Sep 2026 07:37:01 +0200 Subject: [PATCH 03/31] metal : add remaining fa-vec tunings for M2 Max (#28458) --- ggml/src/ggml-metal/ggml-metal-tuning.cpp | 147 ++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/ggml/src/ggml-metal/ggml-metal-tuning.cpp b/ggml/src/ggml-metal/ggml-metal-tuning.cpp index 8cdc55a0a..2323269c4 100644 --- a/ggml/src/ggml-metal/ggml-metal-tuning.cpp +++ b/ggml/src/ggml-metal/ggml-metal-tuning.cpp @@ -1248,6 +1248,153 @@ constexpr fa_vec_entry_t fa_vec_tuned_table[] = { { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_F16, 512, 512, 2, 0 }, { 4, 4 } }, { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_F16, 512, 512, 2, 1 }, { 4, 1 } }, { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_F16, 512, 512, 2, 2 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 32, 32, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 32, 32, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 32, 32, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 32, 32, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 32, 32, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 64, 64, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 96, 96, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 96, 96, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 96, 96, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 32, 32, 1, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 32, 32, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 32, 32, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 32, 32, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 64, 64, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 64, 64, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 64, 64, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 64, 64, 2, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 64, 64, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 96, 96, 2, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 96, 96, 3, 3 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 128, 128, 1, 4 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 128, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 192, 192, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 192, 192, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 192, 192, 2, 3 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 192, 128, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q4_1, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 32, 32, 1, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 32, 32, 2, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 96, 96, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 96, 96, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 192, 1, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 192, 1, 4 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 192, 2, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 192, 3, 2 }, { 1, 2 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 128, 2, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 128, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 192, 128, 3, 3 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 256, 256, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 256, 256, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 256, 256, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 256, 256, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_0, 576, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 32, 32, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 32, 32, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 32, 32, 1, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 32, 32, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 32, 32, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 64, 64, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 64, 64, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 64, 64, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 64, 64, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 64, 64, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 96, 96, -1, 1 }, { 4, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 96, 96, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 96, 96, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 96, 96, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 128, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 128, 128, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 128, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 128, 128, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 192, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 192, -1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 192, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 192, 2, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 192, 3, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 128, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 128, -1, 1 }, { 4, 2 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 128, 1, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 128, 1, 2 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 128, 1, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 128, 2, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 128, 3, 1 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 192, 128, 3, 4 }, { 2, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 256, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 256, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 320, 256, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 320, 256, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 512, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 512, 512, -1, 1 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 576, 512, -1, 0 }, { 1, 4 } }, + { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q5_1, 576, 512, -1, 1 }, { 1, 4 } }, { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 2, 4 } }, { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q8_0, 32, 32, 1, 2 }, { 1, 4 } }, { { GGML_METAL_DEVICE_M2_MAX, GGML_TYPE_Q8_0, 32, 32, 2, 2 }, { 4, 4 } }, From c457e3bf7fa88a7ccfa31f1dbf358ac1ebd90e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Gallou=C3=ABt?= Date: Sun, 6 Sep 2026 07:49:39 +0200 Subject: [PATCH 04/31] ui : embed assets directly with CMake (#28445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the build-time C++ helper and external gzip dependency, simplifying cross-compilation. Keep the generated C++ in templates for readability and preserve fully embedded UI assets. Signed-off-by: Adrien Gallouët --- scripts/ui-assets.cmake | 260 +++++++++++++++++++++++++----- tools/server/CMakeLists.txt | 2 + tools/ui/CMakeLists.txt | 62 +------- tools/ui/embed.cpp | 308 ------------------------------------ tools/ui/ui.cpp.in | 36 +++++ tools/ui/ui.h.in | 21 +++ 6 files changed, 281 insertions(+), 408 deletions(-) delete mode 100644 tools/ui/embed.cpp create mode 100644 tools/ui/ui.cpp.in create mode 100644 tools/ui/ui.h.in diff --git a/scripts/ui-assets.cmake b/scripts/ui-assets.cmake index 0c1c4de55..402f95bd4 100644 --- a/scripts/ui-assets.cmake +++ b/scripts/ui-assets.cmake @@ -15,7 +15,6 @@ set(HF_BUCKET "" CACHE STRING "Hugging Face bucket name") set(HF_VERSION "" CACHE STRING "Version to download (empty = resolve from git)") set(HF_ENABLED "" CACHE STRING "Whether to allow HF Bucket download (ON/OFF)") set(BUILD_UI "" CACHE STRING "Build UI via npm (ON/OFF)") -set(LLAMA_UI_EMBED "" CACHE STRING "Path to llama-ui-embed helper") set(LLAMA_UI_GZIP "" CACHE STRING "Apply gzip compress to assets to save bandwidth") set(DIST_DIR "${UI_BINARY_DIR}/dist") @@ -25,6 +24,223 @@ set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp") set(UI_CPP "${UI_BINARY_DIR}/ui.cpp") set(UI_H "${UI_BINARY_DIR}/ui.h") +function(mime_from_ext name out_var) + string(FIND "${name}" "." ext REVERSE) + if(ext GREATER -1) + string(SUBSTRING "${name}" ${ext} -1 ext_full) + string(SUBSTRING "${ext_full}" 1 -1 ext_str) + else() + set(ext_str "") + endif() + if(ext_str STREQUAL "html") + set(m "text/html; charset=utf-8") + elseif(ext_str STREQUAL "css") + set(m "text/css") + elseif(ext_str STREQUAL "js") + set(m "application/javascript") + elseif(ext_str STREQUAL "json") + set(m "application/json") + elseif(ext_str STREQUAL "webmanifest") + set(m "application/manifest+json") + elseif(ext_str STREQUAL "svg") + set(m "image/svg+xml") + elseif(ext_str STREQUAL "png") + set(m "image/png") + elseif(ext_str STREQUAL "jpg" OR ext_str STREQUAL "jpeg") + set(m "image/jpeg") + elseif(ext_str STREQUAL "ico") + set(m "image/x-icon") + elseif(ext_str STREQUAL "woff") + set(m "font/woff") + elseif(ext_str STREQUAL "woff2") + set(m "font/woff2") + else() + set(m "application/octet-stream") + endif() + set(${out_var} "${m}" PARENT_SCOPE) +endfunction() + +# Fail when a dist tree is present but is missing files the UI needs at +# runtime; catches truncated/stale asset trees early with a useful message. +function(ui_validate_assets files in_dir) + list(LENGTH files n_assets) + if(n_assets EQUAL 0) + return() + endif() + + set(found_index FALSE) + set(found_manifest FALSE) + set(found_sw FALSE) + set(found_build_json FALSE) + set(found_version_json FALSE) + set(found_bundle_js FALSE) + set(found_bundle_css FALSE) + set(found_workbox_js FALSE) + + foreach(f ${files}) + get_filename_component(base "${f}" NAME) + if(base STREQUAL "index.html") + set(found_index TRUE) + elseif(base STREQUAL "manifest.webmanifest") + set(found_manifest TRUE) + elseif(base STREQUAL "sw.js") + set(found_sw TRUE) + elseif(base STREQUAL "build.json") + set(found_build_json TRUE) + elseif(base STREQUAL "version.json") + set(found_version_json TRUE) + elseif(base MATCHES "^bundle.*\\.js$") + set(found_bundle_js TRUE) + elseif(base MATCHES "^bundle.*\\.css$") + set(found_bundle_css TRUE) + elseif(base MATCHES "^workbox.*\\.js$") + set(found_workbox_js TRUE) + endif() + endforeach() + + set(missing "") + if(NOT found_index) + list(APPEND missing "index.html") + endif() + if(NOT found_manifest) + list(APPEND missing "manifest.webmanifest") + endif() + if(NOT found_sw) + list(APPEND missing "sw.js") + endif() + if(NOT found_build_json) + list(APPEND missing "build.json") + endif() + if(NOT found_version_json) + list(APPEND missing "version.json") + endif() + if(NOT found_bundle_js) + list(APPEND missing "bundle[hash].js") + endif() + if(NOT found_bundle_css) + list(APPEND missing "bundle[hash].css") + endif() + if(NOT found_workbox_js) + list(APPEND missing "workbox[hash].js") + endif() + + if(missing) + set(listing "") + foreach(f ${files}) + string(APPEND listing " ${f}\n") + endforeach() + set(missing_list "") + foreach(m ${missing}) + string(APPEND missing_list " ${m}\n") + endforeach() + message(FATAL_ERROR + "UI: current asset files:\n${listing}" + "UI: missing required asset(s):\n${missing_list}" + "UI: hint: try cleaning your build directory: ${in_dir}") + endif() +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()). +function(emit_files dist_dir) + set(embed_dir "${dist_dir}") + set(use_gzip FALSE) + + if(EXISTS "${dist_dir}/index.html") + if(EXISTS "${dist_dir}/_gzip") + # a _gzip tree inside dist_dir can only be a leftover from an + # older version of this script that staged it there + file(REMOVE_RECURSE "${dist_dir}/_gzip") + message(STATUS "UI: removed stale gzip tree ${dist_dir}/_gzip") + 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() + 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}) + get_filename_component(asset_path "${dist_dir}/${f}" REALPATH) + get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY) + file(MAKE_DIRECTORY "${dst_dir}") + file(ARCHIVE_CREATE + OUTPUT "${gzip_dir}/${f}" + PATHS "${asset_path}" + FORMAT raw + COMPRESSION GZip + ) + endforeach() + message(STATUS "UI: gzip compression applied (${gzip_dir})") + set(embed_dir "${gzip_dir}") + set(use_gzip TRUE) + 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. + set(ASSET_ARRAYS "") + set(ASSET_TABLE "") + set(idx 0) + + foreach(f IN LISTS assets) + file(READ "${embed_dir}/${f}" hex HEX) + if(hex STREQUAL "") + message(FATAL_ERROR "UI: empty file: ${embed_dir}/${f}") + endif() + + string(REGEX REPLACE "(..)" "0x\\1," bytes "${hex}") + file(SHA256 "${embed_dir}/${f}" etag) + mime_from_ext("${f}" mime) + + string(APPEND ASSET_ARRAYS + "static const unsigned char asset_${idx}[] = {${bytes}};\n") + + string(APPEND ASSET_TABLE + " { \"${f}\", asset_${idx}, sizeof(asset_${idx}), \"\\\"${etag}\\\"\", \"${mime}\" },\n") + + math(EXPR idx "${idx} + 1") + endforeach() + + set(LLAMA_UI_HAS_ASSETS 0) + if(n_assets GREATER 0) + set(LLAMA_UI_HAS_ASSETS 1) + endif() + set(N_ASSETS "${n_assets}") + set(USE_GZIP false) + if(use_gzip) + 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) + message(STATUS "UI: embedded ${n_assets} assets") +endfunction() + function(npm_build_should_skip out_var) set(${out_var} FALSE PARENT_SCOPE) @@ -250,48 +466,6 @@ function(hf_download version out_var out_resolved) endforeach() endfunction() -function(emit_files dist_dir) - # If gzip is requested, compress every asset into a parallel _gzip/ tree - # the structure stays the same; for ex: /abc/def --> /_gzip/abc/def - # embed.cpp will check for _gzip and will pick it up - if(LLAMA_UI_GZIP AND EXISTS "${dist_dir}/index.html") - find_program(GZIP_EXECUTABLE gzip) - if(NOT GZIP_EXECUTABLE) - message(WARNING "UI: LLAMA_UI_GZIP requested but gzip not found, embedding uncompressed") - else() - set(gzip_dir "${dist_dir}/_gzip") - file(REMOVE_RECURSE "${gzip_dir}") - file(GLOB_RECURSE all_files RELATIVE "${dist_dir}" "${dist_dir}/*") - foreach(f ${all_files}) - get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY) - file(MAKE_DIRECTORY "${dst_dir}") - execute_process( - COMMAND "${GZIP_EXECUTABLE}" -c "${dist_dir}/${f}" - OUTPUT_FILE "${gzip_dir}/${f}" - RESULT_VARIABLE gz_rc - ) - if(NOT gz_rc EQUAL 0) - message(FATAL_ERROR "UI: gzip failed for ${f}") - endif() - endforeach() - message(STATUS "UI: gzip compression applied (${gzip_dir})") - endif() - endif() - - set(args "${UI_CPP}" "${UI_H}") - if(EXISTS "${dist_dir}/index.html") - list(APPEND args "${dist_dir}") - endif() - - execute_process( - COMMAND "${LLAMA_UI_EMBED}" ${args} - RESULT_VARIABLE rc - ) - if(NOT rc EQUAL 0) - message(FATAL_ERROR "UI: llama-ui-embed failed (${rc})") - endif() -endfunction() - # --------------------------------------------------------------------------- # 1. Priority 1: pre-built assets supplied in tools/ui/dist # --------------------------------------------------------------------------- diff --git a/tools/server/CMakeLists.txt b/tools/server/CMakeLists.txt index 280bd9e19..43c245633 100644 --- a/tools/server/CMakeLists.txt +++ b/tools/server/CMakeLists.txt @@ -50,6 +50,8 @@ target_include_directories(${TARGET} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(${TARGET} PRIVATE ../mtmd ${CMAKE_SOURCE_DIR}) target_link_libraries(${TARGET} PUBLIC server-context llama-ui cpp-httplib ${CMAKE_THREAD_LIBS_INIT}) +add_dependencies(${TARGET} llama-ui-assets) + if(LLAMA_TOOLS_INSTALL) install(TARGETS ${TARGET} LIBRARY) endif() diff --git a/tools/ui/CMakeLists.txt b/tools/ui/CMakeLists.txt index 208b46a5c..79ffe9fc1 100644 --- a/tools/ui/CMakeLists.txt +++ b/tools/ui/CMakeLists.txt @@ -36,60 +36,11 @@ endif() set(UI_CPP "${CMAKE_CURRENT_BINARY_DIR}/ui.cpp") set(UI_H "${CMAKE_CURRENT_BINARY_DIR}/ui.h") -if(CMAKE_CROSSCOMPILING) - find_program(HOST_CXX_COMPILER NAMES g++ clang++ NO_CMAKE_FIND_ROOT_PATH) - if(NOT HOST_CXX_COMPILER) - message(FATAL_ERROR "UI: no host C++ compiler (g++/clang++) found to build llama-ui-embed; set -DHOST_CXX_COMPILER=") - endif() - message(STATUS "UI: building llama-ui-embed with host compiler ${HOST_CXX_COMPILER}") - - if(CMAKE_HOST_WIN32) - set(LLAMA_UI_EMBED_EXE "${CMAKE_CURRENT_BINARY_DIR}/llama-ui-embed-host.exe") - else() - set(LLAMA_UI_EMBED_EXE "${CMAKE_CURRENT_BINARY_DIR}/llama-ui-embed-host") - 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" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/embed.cpp" - COMMENT "Building llama-ui-embed (host)" - VERBATIM - ) - - # phony target to tie it into the dependency graph - add_custom_target(llama-ui-embed DEPENDS "${LLAMA_UI_EMBED_EXE}") -else() - # exclude llama-ui-embed from sanitizer flags, - # it's a build-time-only tool, no need to instrument it - # this is to fix TSan "memory layout is incompatible" error on CI - get_directory_property(_llama_ui_dir_co COMPILE_OPTIONS) - get_directory_property(_llama_ui_dir_ll LINK_LIBRARIES) - set(_llama_ui_embed_co ${_llama_ui_dir_co}) - set(_llama_ui_embed_ll ${_llama_ui_dir_ll}) - list(FILTER _llama_ui_embed_co EXCLUDE REGEX ".*-fsanitize=.*") - list(FILTER _llama_ui_embed_ll EXCLUDE REGEX ".*-fsanitize=.*") - set_directory_properties(PROPERTIES - COMPILE_OPTIONS "${_llama_ui_embed_co}" - LINK_LIBRARIES "${_llama_ui_embed_ll}") - - add_executable(llama-ui-embed embed.cpp) - target_compile_features(llama-ui-embed PRIVATE cxx_std_17) - set_target_properties(llama-ui-embed PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" - ) - set(LLAMA_UI_EMBED_EXE "$") - - # restore so the llama-ui library below keeps sanitizer instrumentation - set_directory_properties(PROPERTIES - COMPILE_OPTIONS "${_llama_ui_dir_co}" - LINK_LIBRARIES "${_llama_ui_dir_ll}") -endif() - -# Run the provisioning script every build so source changes in tools/ui/ are -# always picked up. The script uses copy_if_different for ui.cpp/ui.h, so the -# library only recompiles when contents actually change. +# Provision assets and generate ui.cpp/ui.h natively in CMake at build time. +# The generated sources are compiled by the regular target toolchain; no +# build-time host executable is needed (works in any cross-compile setup). +# The script uses copy_if_different semantics, so the library below only +# recompiles when the generated contents actually change. add_custom_target(llama-ui-assets ALL BYPRODUCTS ${UI_CPP} ${UI_H} COMMAND ${CMAKE_COMMAND} @@ -101,15 +52,12 @@ add_custom_target(llama-ui-assets ALL "-DHF_VERSION=${HF_UI_VERSION}" "-DHF_ENABLED=${LLAMA_USE_PREBUILT_UI}" "-DBUILD_UI=${LLAMA_BUILD_UI}" - "-DLLAMA_UI_EMBED=${LLAMA_UI_EMBED_EXE}" "-DLLAMA_UI_GZIP=${LLAMA_UI_GZIP}" -P "${PROJECT_SOURCE_DIR}/scripts/ui-assets.cmake" COMMENT "Provisioning UI assets" VERBATIM ) -add_dependencies(llama-ui-assets llama-ui-embed) - set_source_files_properties(${UI_CPP} ${UI_H} PROPERTIES GENERATED TRUE) add_library(${TARGET} STATIC ${UI_CPP} ${UI_H}) diff --git a/tools/ui/embed.cpp b/tools/ui/embed.cpp deleted file mode 100644 index b76c9047f..000000000 --- a/tools/ui/embed.cpp +++ /dev/null @@ -1,308 +0,0 @@ -// llama-ui-embed: generate ui.cpp / ui.h that embed UI assets as C arrays. -// -// Usage: -// llama-ui-embed [] -// -// Recursively embeds every regular file under . -// Asset names are relative paths from (e.g. "_app/immutable/bundle.HASH.js"). -// Without , emits an empty asset table. - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - - -static const char * mime_from_ext(const std::string & name) { - auto ext = name.rfind('.'); - if (ext == std::string::npos) return "application/octet-stream"; - std::string e = name.substr(ext + 1); - if (e == "html") return "text/html; charset=utf-8"; - if (e == "css") return "text/css"; - if (e == "js") return "application/javascript"; - if (e == "json") return "application/json"; - if (e == "webmanifest") return "application/manifest+json"; - if (e == "svg") return "image/svg+xml"; - if (e == "png") return "image/png"; - if (e == "jpg" || - e == "jpeg") return "image/jpeg"; - if (e == "ico") return "image/x-icon"; - if (e == "woff") return "font/woff"; - if (e == "woff2") return "font/woff2"; - return "application/octet-stream"; -} - -// Computes FNV-1a hash of the data -static uint64_t fnv_hash(const uint8_t * data, size_t len) { - const uint64_t fnv_prime = 0x100000001b3ULL; - uint64_t hash = 0xcbf29ce484222325ULL; - - for (size_t i = 0; i < len; ++i) { - hash ^= data[i]; - hash *= fnv_prime; - } - return hash; -} - -static bool read_file(const std::filesystem::path & path, std::vector & out) { - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f) { - fprintf(stderr, "embed: cannot open %s\n", path.string().c_str()); - return false; - } - const auto sz = f.tellg(); - if (sz < 0) { - return false; - } - f.seekg(0); - out.resize(static_cast(sz)); - if (sz > 0 && !f.read(reinterpret_cast(out.data()), sz)) { - return false; - } - return true; -} - -static void append_bytes_hex(std::string & out, const std::vector & bytes) { - static const char hex[] = "0123456789abcdef"; - out.reserve(out.size() + bytes.size() * 5); - for (unsigned char b : bytes) { - out += '0'; - out += 'x'; - out += hex[b >> 4]; - out += hex[b & 0xf]; - out += ','; - } -} - -static bool write_if_different(const std::string & path, const std::string & content) { - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (f) { - const auto sz = f.tellg(); - if (sz >= 0 && static_cast(sz) == content.size()) { - std::string existing(static_cast(sz), '\0'); - f.seekg(0); - if (sz == 0 || f.read(existing.data(), sz)) { - if (existing == content) { - return true; - } - } - } - } - - std::ofstream out(path, std::ios::binary | std::ios::trunc); - if (!out) { - fprintf(stderr, "embed: cannot write %s\n", path.c_str()); - return false; - } - if (!content.empty()) { - out.write(content.data(), static_cast(content.size())); - } - bool ok = out.good(); - if (ok) { - printf("embed: write output file %s\n", path.c_str()); - } - return ok; -} - -static std::string path_basename(const std::string & name) { - const size_t p = name.rfind('/'); - return p == std::string::npos ? name : name.substr(p + 1); -} -static bool str_starts_with(const std::string & s, const char * prefix) { - const size_t n = strlen(prefix); - return s.size() >= n && s.compare(0, n, prefix) == 0; -} -static bool str_ends_with(const std::string & s, const char * suffix) { - const size_t n = strlen(suffix); - return s.size() >= n && s.compare(s.size() - n, n, suffix) == 0; -} - -static std::string fmt(const char * pattern, ...) { - char tmp[512]; - va_list ap; - va_start(ap, pattern); - const int n = vsnprintf(tmp, sizeof(tmp), pattern, ap); - va_end(ap); - return (n > 0) ? std::string(tmp, static_cast(n)) : std::string(); -} - -struct asset_entry { - std::string name; - std::filesystem::path path; -}; - -int main(int argc, char ** argv) { - if (argc < 3 || argc > 4) { - fprintf(stderr, "usage: %s []\n", argv[0]); - return 1; - } - - const std::string out_cpp = argv[1]; - const std::string out_h = argv[2]; - const std::string asset_dir = (argc >= 4) ? argv[3] : std::string(); - - const bool use_gzip = !asset_dir.empty() && std::filesystem::exists(asset_dir + "/_gzip"); - const std::string in_dir = use_gzip ? (asset_dir + "/_gzip") : asset_dir; - - std::vector assets; - if (!in_dir.empty()) { - const std::filesystem::path dir = in_dir; - - std::error_code ec; - std::filesystem::recursive_directory_iterator it(dir, ec); - if (ec) { - fprintf(stderr, "embed: cannot iterate %s: %s\n", argv[3], ec.message().c_str()); - return 1; - } - for (const auto & entry : it) { - if (!entry.is_regular_file()) { - continue; - } - // name is the relative path from dir, with forward slashes - const std::string name = entry.path().lexically_relative(dir).generic_string(); - assets.push_back({ name, entry.path() }); - } - - // directory iteration order is unspecified; sort for reproducible output - std::sort(assets.begin(), assets.end(), - [](const asset_entry & a, const asset_entry & b) { return a.name < b.name; }); - } - - const int n_assets = static_cast(assets.size()); - - if (n_assets > 0) { - using match_fn = std::function; - auto exact = [](const char * name) -> match_fn { - return [name](const std::string & base) { return base == name; }; - }; - - struct required_check { const char * label; match_fn match; bool found; }; - required_check checks[] = { - { "index.html", exact("index.html"), false }, - { "manifest.webmanifest", exact("manifest.webmanifest"), false }, - { "sw.js", exact("sw.js"), false }, - { "build.json", exact("build.json"), false }, - { "version.json", exact("version.json"), false }, - { "bundle[hash].js", [](const std::string & b) { - return str_starts_with(b, "bundle") && str_ends_with(b, ".js"); - }, false }, - { "bundle[hash].css", [](const std::string & b) { - return str_starts_with(b, "bundle") && str_ends_with(b, ".css"); - }, false }, - { "workbox[hash].js", [](const std::string & b) { - return str_starts_with(b, "workbox") && str_ends_with(b, ".js"); - }, false }, - }; - - for (const auto & a : assets) { - const std::string base = path_basename(a.name); - for (auto & c : checks) { - if (!c.found) { c.found = c.match(base); } - } - } - - std::vector missing; - for (const auto & c : checks) { - if (!c.found) { missing.push_back(c.label); } - } - if (!missing.empty()) { - fprintf(stderr, "\ncurrent asset files:\n"); - for (const auto & a : assets) { - fprintf(stderr, " %s\n", a.name.c_str()); - } - fprintf(stderr, "missing required asset(s):\n"); - for (const char * m : missing) { - fprintf(stderr, " %s\n", m); - } - fprintf(stderr, "hint: try cleaning your build directory: %s\n", in_dir.c_str()); - return 1; - } - } - - std::string h; - h += "#pragma once\n\n#include \n#include \n\n"; - if (n_assets > 0) { - h += "#define LLAMA_UI_HAS_ASSETS 1\n\n"; - } - h += - "struct llama_ui_asset {\n" - " std::string name;\n" - " const unsigned char * data;\n" - " std::size_t size;\n" - " std::string etag;\n" - " std::string type;\n" - "};\n\n" - "const llama_ui_asset * llama_ui_find_asset(const std::string & name);\n" - "bool llama_ui_use_gzip();\n"; - h += fmt("const std::array & llama_ui_get_assets();\n", n_assets); - - std::string cpp; - cpp += "#include \"ui.h\"\n\n"; - - if (n_assets > 0) { - for (int i = 0; i < n_assets; i++) { - std::vector bytes; - if (!read_file(assets[i].path, bytes)) { - return 1; - } - if (bytes.empty()) { - fprintf(stderr, "embed: empty file: %s\n", assets[i].path.generic_string().c_str()); - return 1; - } - cpp += fmt("static const unsigned char asset_%d_data[] = {", i); - append_bytes_hex(cpp, bytes); - - // note: this is a simple hash for cache busting, not a cryptographic hash; fnv is enough here - const auto hash = fnv_hash(bytes.data(), bytes.size()); - - cpp += fmt("};\nstatic const std::size_t asset_%d_size = %zu;\n", - i, bytes.size()); - cpp += fmt("static const char asset_%d_etag[] = \"\\\"0x%016" PRIx64 "\\\"\";\n\n", - i, hash); - } - - cpp += fmt("static const std::array g_assets = {{\n", n_assets); - for (int i = 0; i < n_assets; i++) { - const std::string & name = assets[i].name; - cpp += fmt(" { \"%s\", asset_%d_data, asset_%d_size, asset_%d_etag, \"%s\" },\n", - name.c_str(), i, i, i, mime_from_ext(name)); - } - cpp += "}};\n\n"; - - cpp += - "const llama_ui_asset * llama_ui_find_asset(const std::string & name) {\n" - " for (const auto & a : g_assets) {\n" - " if (a.name == name) {\n" - " return &a;\n" - " }\n" - " }\n" - " return nullptr;\n" - "}\n"; - cpp += fmt("const std::array & llama_ui_get_assets() {\n", n_assets); - cpp += " return g_assets;\n" - "}\n"; - } else { - cpp += - "const llama_ui_asset * llama_ui_find_asset(const std::string &) {\n" - " return nullptr;\n" - "}\n" - "const std::array & llama_ui_get_assets() {\n" - " static const std::array empty{};\n" - " return empty;\n" - "}\n"; - } - cpp += fmt("bool llama_ui_use_gzip() { return %s; }\n", use_gzip ? "true" : "false"); - - bool ok = true; - ok = write_if_different(out_h, h) && ok; - ok = write_if_different(out_cpp, cpp) && ok; - return ok ? 0 : 1; -} diff --git a/tools/ui/ui.cpp.in b/tools/ui/ui.cpp.in new file mode 100644 index 000000000..7f91ef2a2 --- /dev/null +++ b/tools/ui/ui.cpp.in @@ -0,0 +1,36 @@ +// Generated by scripts/ui-assets.cmake - do not edit. + +#include "ui.h" + +@ASSET_ARRAYS@ +#if defined(LLAMA_UI_HAS_ASSETS) +static const std::array g_assets = {{ +@ASSET_TABLE@ +}}; +#endif + +const llama_ui_asset * llama_ui_find_asset(const std::string & name) { +#if defined(LLAMA_UI_HAS_ASSETS) + for (const auto & a : g_assets) { + if (a.name == name) { + return &a; + } + } +#else + (void) name; +#endif + return nullptr; +} + +const std::array & llama_ui_get_assets() { +#if defined(LLAMA_UI_HAS_ASSETS) + return g_assets; +#else + static const std::array empty{}; + return empty; +#endif +} + +bool llama_ui_use_gzip() { + return @USE_GZIP@; +} diff --git a/tools/ui/ui.h.in b/tools/ui/ui.h.in new file mode 100644 index 000000000..4555b0dd5 --- /dev/null +++ b/tools/ui/ui.h.in @@ -0,0 +1,21 @@ +// Generated by scripts/ui-assets.cmake - do not edit. + +#pragma once + +#include +#include + +// Defined as 1 only when assets were embedded (tools/server checks defined()). +#cmakedefine LLAMA_UI_HAS_ASSETS 1 + +struct llama_ui_asset { + std::string name; + const unsigned char * data; + std::size_t size; + std::string etag; + std::string type; +}; + +const llama_ui_asset * llama_ui_find_asset(const std::string & name); +bool llama_ui_use_gzip(); +const std::array & llama_ui_get_assets(); From 7620399f58aebfd2196b74021f9581bcf7218cb9 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sun, 6 Sep 2026 08:21:22 +0200 Subject: [PATCH 05/31] common: add --log-jsonl (#28437) * common: add --log-jsonl * rename unknown to none --- common/arg.cpp | 8 ++++++++ common/log.cpp | 40 +++++++++++++++++++++++++++++++++++++- common/log.h | 1 + tools/cli/README.md | 3 ++- tools/completion/README.md | 3 ++- tools/server/README.md | 3 ++- 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index 2669cacd6..015196ca1 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -3901,6 +3901,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex common_log_set_file(common_log_main(), value.c_str()); } ).set_env("LLAMA_ARG_LOG_FILE")); + add_opt(common_arg( + {"--log-jsonl"}, + {"--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); + } + ).set_env("LLAMA_ARG_LOG_JSONL")); add_opt(common_arg( {"--log-prompts-dir"}, "PATH", "Log prompts to directory (auto-created if not present; only used for debugging, default: disabled)", diff --git a/common/log.cpp b/common/log.cpp index 0f0cb7902..42951190c 100644 --- a/common/log.cpp +++ b/common/log.cpp @@ -1,5 +1,6 @@ #include "common.h" #include "log.h" +#include "json.h" #include #include @@ -66,6 +67,17 @@ static const char* g_col[] = { "", }; +static const char * level_str(enum ggml_log_level level) { + switch (level) { + case GGML_LOG_LEVEL_DEBUG: return "debug"; + case GGML_LOG_LEVEL_INFO: return "info"; + case GGML_LOG_LEVEL_WARN: return "warn"; + case GGML_LOG_LEVEL_ERROR: return "error"; + case GGML_LOG_LEVEL_CONT: return "cont"; + default: return "none"; + } +} + struct common_log_entry { enum ggml_log_level level {GGML_LOG_LEVEL_INFO}; @@ -74,6 +86,7 @@ struct common_log_entry { int64_t timestamp { 0 }; bool is_end { false }; // signals the worker thread to stop bool prefix { false }; + bool jsonl { false }; common_log_entry(size_t size = 256) : msg(size) { } @@ -88,11 +101,23 @@ struct common_log_entry { fcur = stdout; - if (level != GGML_LOG_LEVEL_NONE) { + if (level != GGML_LOG_LEVEL_NONE && !jsonl) { fcur = stderr; } } + if (jsonl) { + common_json obj = { + {"type", "log"}, + {"time", timestamp}, + {"level", level_str(level)}, + {"msg", msg.data()}, + }; + fprintf(fcur, "%s\n", obj.dump_safe().c_str()); + fflush(fcur); + return; + } + if (level != GGML_LOG_LEVEL_NONE && level != GGML_LOG_LEVEL_CONT && prefix) { if (timestamp) { // [M.s.ms.us] @@ -131,6 +156,7 @@ struct common_log { file = nullptr; prefix = false; timestamps = false; + jsonl = false; running = false; t_start = t_us(); @@ -158,6 +184,7 @@ private: bool prefix; bool timestamps; + bool jsonl; bool running; int64_t t_start; @@ -246,6 +273,7 @@ public: entry.is_end = false; entry.level = level; entry.prefix = prefix; + entry.jsonl = jsonl; entry.timestamp = 0; if (timestamps) { entry.timestamp = t_us() - t_start; @@ -360,6 +388,12 @@ public: this->timestamps = timestamps; } + + void set_jsonl(bool jsonl) { + std::lock_guard lock(mtx); + + this->jsonl = jsonl; + } }; // @@ -433,6 +467,10 @@ 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 f03358252..37f4de92b 100644 --- a/common/log.h +++ b/common/log.h @@ -91,6 +91,7 @@ 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 diff --git a/tools/cli/README.md b/tools/cli/README.md index b874d0207..b667d341d 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -90,6 +90,7 @@ | `-hft, --hf-token TOKEN` | Hugging Face access token (default: value from HF_TOKEN environment variable)
(env: HF_TOKEN) | | `--log-disable` | Log disable | | `--log-file FNAME` | Log to file
(env: LLAMA_ARG_LOG_FILE) | +| `--log-jsonl, --no-log-jsonl` | Log as JSONL (one JSON object per line) to stdout, this also disables colored logging (default: disabled)
(env: LLAMA_ARG_LOG_JSONL) | | `--log-colors [on\|off\|auto]` | Set colored logging ('on', 'off', or 'auto', default: 'auto')
'auto' enables colors when output is to a terminal
(env: LLAMA_ARG_LOG_COLORS) | | `-v, --verbose, --log-verbose` | Set verbosity level to infinity (i.e. log all messages, useful for debugging) | | `--offline` | Offline mode: forces use of cache, prevents network access
(env: LLAMA_ARG_OFFLINE) | @@ -178,7 +179,7 @@ | `--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 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-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) | +| `--reasoning-preserve, --no-reasoning-preserve` | preserve reasoning trace in the full history, not just the last assistant message (default: enabled)
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) | | `--skip-chat-parsing, --no-skip-chat-parsing` | force a pure content parser, even if a Jinja template is specified; model will output everything in the content section, including any reasoning and/or tool calls (default: disabled)
(env: LLAMA_ARG_SKIP_CHAT_PARSING) | diff --git a/tools/completion/README.md b/tools/completion/README.md index 145be77e3..702a1c4c2 100644 --- a/tools/completion/README.md +++ b/tools/completion/README.md @@ -173,6 +173,7 @@ llama-completion.exe -m models\gemma-1.1-7b-it.Q4_K_M.gguf --ignore-eos -n -1 | `-hft, --hf-token TOKEN` | Hugging Face access token (default: value from HF_TOKEN environment variable)
(env: HF_TOKEN) | | `--log-disable` | Log disable | | `--log-file FNAME` | Log to file
(env: LLAMA_ARG_LOG_FILE) | +| `--log-jsonl, --no-log-jsonl` | Log as JSONL (one JSON object per line) to stdout, this also disables colored logging (default: disabled)
(env: LLAMA_ARG_LOG_JSONL) | | `--log-colors [on\|off\|auto]` | Set colored logging ('on', 'off', or 'auto', default: 'auto')
'auto' enables colors when output is to a terminal
(env: LLAMA_ARG_LOG_COLORS) | | `-v, --verbose, --log-verbose` | Set verbosity level to infinity (i.e. log all messages, useful for debugging) | | `--offline` | Offline mode: forces use of cache, prevents network access
(env: LLAMA_ARG_OFFLINE) | @@ -256,7 +257,7 @@ llama-completion.exe -m models\gemma-1.1-7b-it.Q4_K_M.gguf --ignore-eos -n -1 | `--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 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-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) | +| `--reasoning-preserve, --no-reasoning-preserve` | preserve reasoning trace in the full history, not just the last assistant message (default: enabled)
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) | | `--skip-chat-parsing, --no-skip-chat-parsing` | force a pure content parser, even if a Jinja template is specified; model will output everything in the content section, including any reasoning and/or tool calls (default: disabled)
(env: LLAMA_ARG_SKIP_CHAT_PARSING) | diff --git a/tools/server/README.md b/tools/server/README.md index c6e907ba9..952d31e75 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -107,6 +107,7 @@ For the full list of features, please refer to [server's changelog](https://gith | `-hft, --hf-token TOKEN` | Hugging Face access token (default: value from HF_TOKEN environment variable)
(env: HF_TOKEN) | | `--log-disable` | Log disable | | `--log-file FNAME` | Log to file
(env: LLAMA_ARG_LOG_FILE) | +| `--log-jsonl, --no-log-jsonl` | Log as JSONL (one JSON object per line) to stdout, this also disables colored logging (default: disabled)
(env: LLAMA_ARG_LOG_JSONL) | | `--log-colors [on\|off\|auto]` | Set colored logging ('on', 'off', or 'auto', default: 'auto')
'auto' enables colors when output is to a terminal
(env: LLAMA_ARG_LOG_COLORS) | | `-v, --verbose, --log-verbose` | Set verbosity level to infinity (i.e. log all messages, useful for debugging) | | `--offline` | Offline mode: forces use of cache, prevents network access
(env: LLAMA_ARG_OFFLINE) | @@ -236,7 +237,7 @@ For the full list of features, please refer to [server's changelog](https://gith | `--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 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-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) | +| `--reasoning-preserve, --no-reasoning-preserve` | preserve reasoning trace in the full history, not just the last assistant message (default: enabled)
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) | | `--skip-chat-parsing, --no-skip-chat-parsing` | force a pure content parser, even if a Jinja template is specified; model will output everything in the content section, including any reasoning and/or tool calls (default: disabled)
(env: LLAMA_ARG_SKIP_CHAT_PARSING) | From 0afb805b19e26c719a466145761faabad3af1a74 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Sun, 6 Sep 2026 10:52:40 +0200 Subject: [PATCH 06/31] ui: Improve Chat Messages rendering performance (#28460) * ui : update active conversation fields in place updateCurrentNode, applyConversationUpdate, updateConversationTimestamp and the pin toggle replaced the whole activeConversation object, so its identity changed on every send, tool result and rename. ChatMessages tracks that identity to refresh sibling info, so each replacement triggered a full refetch of every message in the conversation. Write the changed fields instead, mirroring updateMessageAtIndex. Assisted-by: pi:zai-org/GLM-5.3 * ui : reuse the conversation load read for sibling info Opening a conversation read every message from the database twice: once in loadConversation for the active path, once in ChatMessages for the sibling map. Hand the freshly read array over once so the chat screen builds sibling info from it, and set the conversation and its messages in one sync block so effects never see the new conversation paired with the previous one's messages. Assisted-by: pi:zai-org/GLM-5.3 * ui : memoize leaf walks in sibling map build buildSiblingInfoMap resolves each sibling's leaf by walking the last-child chain, once per sibling per message, so the walk repeats along the same chains for every message in the conversation ( O(messages^2) on long chats ). Memoize leaf resolution per build with path compression so each edge is walked once. Assisted-by: pi:zai-org/GLM-5.3 * ui : skip sibling refetch for in-place message edits refreshAllMessages refetches every message of the conversation just to rebuild sibling info, but preserve-responses and non-branching assistant edits never create branches, so the sibling map stays valid. Refresh only after actions that branch (editWithBranching kept) or delete. Assisted-by: pi:zai-org/GLM-5.3 * ui : drop unused currentResponse reactive writes Nothing reads chatStore.currentResponse, but setChatStreaming reassigned it on every streamed chunk, so each token paid a reactive write and string assignment for nothing. Remove the field and the clearUIState wrapper that only reset it. Assisted-by: pi:zai-org/GLM-5.3 * ui : reuse completed agentic turn sections during streaming deriveAgenticSections runs in a $derived invalidated per streamed chunk, but re-derived every turn of the session each time, so per-chunk cost grew with session length. Cache completed turns keyed by their assistant message plus reference checks on every field that feeds derivation; only the streaming turn recomputes. Cache hits return the same section objects, so tool block props stay stable and skip their per-chunk re-derive. Assisted-by: pi:zai-org/GLM-5.3 * ui : share markdown block infrastructure Every markdown block duplicated shared work: a full copy of the hljs theme CSS per instance, and the remark/rehype plugin chain rebuilt on every processMarkdown call ( once per block at mount, again per coalesced chunk while streaming ). Use the single theme style element already maintained by SyntaxHighlightedCode, and build pipelines once - shared process-wide for attachment-less blocks, cached by attachments identity otherwise. Assisted-by: pi:zai-org/GLM-5.3 * ui : measure assistant layout only for the last message Every assistant message ran getComputedStyle, getBoundingClientRect and a ResizeObserver over the previous user bubble at mount, even off-screen ones, forcing a layout pass per message while a long conversation renders. The measured vars only feed the :last-child min-height rule, so gate the effect on isLastAssistantMessage; one measurement and one observer remain, and the effect re-runs when the last message changes. Assisted-by: pi:zai-org/GLM-5.3 * ui : trim whole-blob scans in tool block headers Tool block headers parsed their entire blobs at mount, even collapsed, and most tool results and args are large plain text or embedded file content: skip JSON.parse unless the blob starts with a JSON container, prefilter search-result extraction with a Title:/URL: substring check, and match the end-anchored exit-code marker against only the tail of exec outputs. Assisted-by: pi:zai-org/GLM-5.3 * ui : parse write_file and edit_file titles without the content blob Both block headers parsed the full args JSON at mount, even collapsed, and write_file and edit_file args embed the whole file content or edit strings, so every block paid a full-blob JSON parse just to read the path. Split the meta into a title tier that extracts the path with a targeted key match (full parse only as fallback) and a body tier that keeps the full parse; Svelte deriveds are lazy, and the body snippet renders only while the block is expanded, so collapsed blocks no longer parse args. Assisted-by: pi:zai-org/GLM-5.3 * ui : mount chat messages lazily near the viewport Every message row mounted its full component tree on load, so the cycle collector, GC and layout invalidation kept walking every live object and DOM node even for rows the user never scrolls to - which dominated the profile of long conversations. Wrap each row in a placeholder with an IntersectionObserver ( two viewport heights of runway ) that swaps in the real ChatMessage when the row approaches the viewport; the row shell keeps the content-visibility sizing, and rows stay mounted once realized. Rows targeted by the pending-edit flow mount eagerly. Assisted-by: pi:zai-org/GLM-5.3 * ui : smooth the chat navigation animations Slide the centered new-chat form to the bottom edge with a transform instead of a bottom offset - layout-property transitions need the main thread every frame and stutter while a long conversation loads, while transform transitions run on the compositor. Fade the message list in with a CSS animation keyed to the conversation id, disabled under prefers-reduced-motion. Assisted-by: pi:zai-org/GLM-5.3 * ui : follow the svelte runes guidance in chat message code Two effects detected changes with manual previous-value refs and reset flags. The permission request carries object identity, so its dismissal is now a derived comparing the dismissed request; the continue request is a bare boolean, so its dismissal only shrinks to a reset while no request is pending. Also drop a dead if (browser) guard in the markdown theme loader - effects never run on the server. Assisted-by: pi:zai-org/GLM-5.3 * test : pin the chat perf invariants in the unit suite Cover the fixes whose silent regression would be stale or wrong UI rather than a crash: the turn-section cache must reuse unchanged turns yet recompute on every field it compares; the sibling map must resolve the same leaves after the leaf-walk memoization; the active conversation must keep its identity through field updates; and the blob gates ( exec tail window, plain-text result gate, search prefilter ) must keep accepting what they gate. Only the risky invariants are pinned - no coverage for coverage's sake. Assisted-by: pi:zai-org/GLM-5.3 * refactor : address review remarks Name the tool-arg string-field pattern, move the file tools' path field aliases and the JSON container gates into lib/constants, and export the write_file / edit_file meta types from $lib/types instead of the parser modules. Assisted-by: pi:zai-org/GLM-5.3 --- tools/ui/src/app.d.ts | 1 - .../ChatMessage/ChatMessage.svelte | 24 +-- .../ChatMessageAssistant.svelte | 5 +- .../ChatMessageToolCallBlock.svelte | 22 ++- .../ChatMessageToolCallBlockEditFile.svelte | 14 +- .../ChatMessageToolCallBlockWriteFile.svelte | 10 +- .../ChatMessageToolCall/parsers/_shared.ts | 40 +++++ .../ChatMessageToolCall/parsers/edit-file.ts | 62 +++++--- .../parsers/run-javascript.ts | 20 ++- .../ChatMessageToolCall/parsers/write-file.ts | 56 +++++-- .../ChatMessageAgenticContent.svelte | 45 +++--- .../app/chat/ChatMessages/ChatMessages.svelte | 137 ++++++++++++------ .../chat/ChatMessages/LazyChatMessage.svelte | 105 ++++++++++++++ .../app/chat/ChatScreen/ChatScreen.svelte | 9 +- .../MarkdownContent/MarkdownContent.svelte | 98 ++----------- .../MarkdownContent/markdown-processor.ts | 112 ++++++++++++++ tools/ui/src/lib/constants/index.ts | 1 + .../lib/constants/tool-call-args.constants.ts | 23 +++ tools/ui/src/lib/stores/chat/index.svelte.ts | 11 -- .../lib/stores/conversations/index.svelte.ts | 62 +++++--- tools/ui/src/lib/types/index.ts | 11 +- tools/ui/src/lib/types/tools.d.ts | 47 ++++++ tools/ui/src/lib/utils/agentic.ts | 89 +++++++++++- tools/ui/src/lib/utils/branching.ts | 30 +++- tools/ui/src/lib/utils/index.ts | 3 +- .../src/lib/utils/parse-exec-shell-error.ts | 8 +- .../src/lib/utils/parse-exec-shell-status.ts | 7 +- tools/ui/src/lib/utils/search-results.ts | 16 +- tools/ui/src/lib/utils/tool-call-meta.ts | 10 +- tools/ui/src/routes/(chat)/+page.svelte | 3 +- tools/ui/tests/unit/agentic-sections.test.ts | 111 ++++++++++++++ tools/ui/tests/unit/branching.test.ts | 95 ++++++++++++ .../ui/tests/unit/conversations-store.test.ts | 90 ++++++++++++ .../unit/parse-exec-shell-status.test.ts | 18 +++ tools/ui/tests/unit/search-results.test.ts | 27 +++- tools/ui/tests/unit/tool-call-meta.test.ts | 12 ++ tools/ui/tests/unit/tool-calls.test.ts | 116 ++++++++++++++- 37 files changed, 1260 insertions(+), 290 deletions(-) create mode 100644 tools/ui/src/lib/components/app/chat/ChatMessages/LazyChatMessage.svelte create mode 100644 tools/ui/src/lib/components/app/content/MarkdownContent/markdown-processor.ts create mode 100644 tools/ui/src/lib/constants/tool-call-args.constants.ts create mode 100644 tools/ui/tests/unit/branching.test.ts create mode 100644 tools/ui/tests/unit/conversations-store.test.ts diff --git a/tools/ui/src/app.d.ts b/tools/ui/src/app.d.ts index 5309dce8f..639a16df2 100644 --- a/tools/ui/src/app.d.ts +++ b/tools/ui/src/app.d.ts @@ -137,7 +137,6 @@ declare global { declare global { interface Window { - idxThemeStyle?: number; idxCodeBlock?: number; // File System Access API - not in the DOM lib and unavailable in some browsers diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte index fa2a50bc5..46d05338b 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte @@ -404,7 +404,7 @@ } -
+
{#if message.role === MessageRole.SYSTEM} {:else if mcpPromptExtra} @@ -425,25 +425,3 @@ /> {/if}
- - diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte index a2c742f0f..dac55caff 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte @@ -82,8 +82,11 @@ let lastUserMessageHeight = $state(0); let assistantMarginTop = $state(0); + // The measured CSS vars feed the :last-child min-height rule only, so only + // the last assistant message needs them. Reading isLastAssistantMessage + // here also re-runs the effect when this message stops being the last. $effect(() => { - if (!assistantEl) return; + if (!assistantEl || !isLastAssistantMessage) return; assistantMarginTop = Math.round(parseFloat(getComputedStyle(assistantEl).marginTop)); diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte index a604a97e3..cc2b4a562 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte @@ -13,7 +13,12 @@ import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte'; import { BuiltInTool } from '$lib/enums'; import type { AgenticSection, DatabaseMessageExtra } from '$lib/types'; - import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils'; + import { + extractSearchQuery, + extractSearchResults, + isWebSearchToolName, + looksLikeSearchResult + } from '$lib/utils'; interface Props { section: AgenticSection; @@ -26,11 +31,16 @@ let { attachments, isExecuting, isStreaming, onToggle, open, section }: Props = $props(); - const searchResults = $derived(extractSearchResults(section.toolResult)); - const searchQuery = $derived(extractSearchQuery(section.toolArgs)); - const isSearchCall = $derived( - searchResults.length > 0 || (searchQuery.length > 0 && isWebSearchToolName(section.toolName)) - ); + // Runs for every tool block on mount, before the body renders: the cheap + // content prefilter and the tool-name allow-list come first so blobs from + // exec/file tools are never line-split or JSON-parsed here + const isSearchCall = $derived.by(() => { + if (looksLikeSearchResult(section.toolResult)) { + return extractSearchResults(section.toolResult).length > 0; + } + + return isWebSearchToolName(section.toolName) && extractSearchQuery(section.toolArgs).length > 0; + }); {#if isSearchCall} diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte index 2067e4268..22ffc256b 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte @@ -1,5 +1,5 @@ @@ -45,11 +49,11 @@ {meta.errorMessage}
- {:else if meta && meta.edits.length > 0} + {:else if meta && editFileBody && editFileBody.edits.length > 0} {#each editDiffs as diffLines, ei (ei)}
- Edit {ei + 1} of {meta.edits.length} + Edit {ei + 1} of {editFileBody.edits.length}
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte index 178c479d9..cafa5280b 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte @@ -1,5 +1,5 @@ @@ -45,7 +49,7 @@
{:else if meta} | null { } } +// Compiled per key on first use; the key set is tiny and fixed. +const toolArgStringRegexes = new Map(); + +/** + * Extract a string field from a JSON tool-args blob without parsing the + * whole document. write_file and edit_file args embed full file contents, + * yet the block title needs only the path; a targeted key match plus a + * JSON.parse of the captured string literal alone keeps title rendering + * O(path) instead of O(blob). Returns undefined when the key is missing + * or its value is not a string; callers fall back to the full parse. + */ +export function extractToolArgString( + toolArgs: string, + keys: readonly string[] +): string | undefined { + for (const key of keys) { + let pattern = toolArgStringRegexes.get(key); + + if (!pattern) { + pattern = new RegExp(TOOL_ARG_STRING_FIELD_PATTERN_TEMPLATE.replace('{key}', key)); + toolArgStringRegexes.set(key, pattern); + } + + const match = pattern.exec(toolArgs); + + if (!match) continue; + + try { + const value: unknown = JSON.parse(`"${match[1]}"`); + + if (typeof value === 'string') return value; + } catch { + // fall through to the next key; the full parse is the fallback + } + } + + return undefined; +} + /** * Parse a section's toolArgs against an expected tool name. Returns * `null` when: diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts index 9ed6f92bc..d711466cb 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts @@ -3,26 +3,12 @@ // rendering), plus the result blob for `result` / `edits_applied` / // `error` fields. -import { parseToolArgs } from './_shared'; -import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants'; +import { extractToolArgString, parseToolArgs } from './_shared'; +import { FILE_PATH_SEPARATOR_REGEX, TOOL_ARG_PATH_KEYS } from '$lib/constants'; import { BuiltInTool } from '$lib/enums'; -import type { AgenticSection } from '$lib/types'; +import type { AgenticSection, EditFileEdit, EditFileMeta, EditFileTitleMeta } from '$lib/types'; import { tryParseToolResultObject } from '$lib/utils'; -export type EditFileEdit = { - oldText: string; - newText: string; -}; - -export type EditFileMeta = { - fileName: string; - filePath: string; - edits: EditFileEdit[]; - resultMessage?: string; - editsApplied?: number; - errorMessage?: string; -}; - export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null { const args = parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true }); @@ -79,3 +65,45 @@ export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null resultMessage }; } + +/** + * Title-tier meta for edit_file blocks: everything the header and status + * pill render, obtained without parsing the embedded edit strings. The path + * comes from a targeted key extraction; the full parse runs only as a + * fallback for arg shapes the extraction can't see. + */ +export function parseEditFileTitleMeta(section: AgenticSection): EditFileTitleMeta | null { + if (section.toolName !== BuiltInTool.SERVER_EDIT_FILE || !section.toolArgs) return null; + + let rawPath: string | undefined = extractToolArgString(section.toolArgs, TOOL_ARG_PATH_KEYS); + + if (!rawPath) { + const args = parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true }); + const fallbackPath = args?.path ?? args?.file_path ?? args?.filePath; + + if (typeof fallbackPath === 'string' && fallbackPath) rawPath = fallbackPath; + } + + if (!rawPath) return null; + + const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath; + const resultObj = tryParseToolResultObject(section.toolResult); + + let resultMessage: string | undefined; + let editsApplied: number | undefined; + let errorMessage: string | undefined; + + if (typeof resultObj?.error === 'string') { + errorMessage = resultObj.error; + } else if (resultObj) { + if (typeof resultObj.result === 'string') { + resultMessage = resultObj.result; + } + + if (Number.isFinite(Number(resultObj.edits_applied))) { + editsApplied = Number(resultObj.edits_applied); + } + } + + return { editsApplied, errorMessage, fileName, filePath: rawPath, resultMessage }; +} diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts index 440a1f5d6..bd97cd2fe 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts @@ -6,6 +6,7 @@ // are handled. import { parseToolArgs } from './_shared'; +import { JSON_ARRAY_OPEN, JSON_OBJECT_OPEN } from '$lib/constants'; import { BuiltInTool } from '$lib/enums'; import type { AgenticSection } from '$lib/types'; @@ -38,14 +39,21 @@ export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMe // do we scan raw lines for the `Error:` prefix. let parsedObject: Record | null = null; - try { - const parsed: unknown = JSON.parse(toolResultString); + // Successful sandbox output is a JSON array, errors are objects; plain + // text (huge console logs) fails the parse below anyway, so only try + // when the blob starts with a JSON container + const trimmedResult = toolResultString.trimStart(); - if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { - parsedObject = parsed as Record; + if (trimmedResult[0] === JSON_OBJECT_OPEN || trimmedResult[0] === JSON_ARRAY_OPEN) { + try { + const parsed: unknown = JSON.parse(trimmedResult); + + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + parsedObject = parsed as Record; + } + } catch { + parsedObject = null; } - } catch { - parsedObject = null; } if (typeof parsedObject?.error === 'string') { diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts index 5b9bf9f88..4a8e1a9c9 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts @@ -3,22 +3,12 @@ // finishes) and surfaces `bytes`, `result`, and `error` from the // result blob. -import { parseToolArgs } from './_shared'; -import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants'; +import { extractToolArgString, parseToolArgs } from './_shared'; +import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX, TOOL_ARG_PATH_KEYS } from '$lib/constants'; import { BuiltInTool } from '$lib/enums'; -import type { AgenticSection } from '$lib/types'; +import type { AgenticSection, WriteFileMeta, WriteFileTitleMeta } from '$lib/types'; import { getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils'; -export type WriteFileMeta = { - fileName: string; - filePath: string; - language: string; - content: string; - bytesWritten?: number; - resultMessage?: string; - errorMessage?: string; -}; - export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null { const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true }); @@ -51,3 +41,43 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul resultMessage }; } + +/** + * Title-tier meta for write_file blocks: everything the header and status + * pill render, obtained without parsing the embedded file content. The path + * comes from a targeted key extraction; the full parse runs only as a + * fallback for arg shapes the extraction can't see. + */ +export function parseWriteFileTitleMeta(section: AgenticSection): WriteFileTitleMeta | null { + if (section.toolName !== BuiltInTool.SERVER_WRITE_FILE || !section.toolArgs) return null; + + let rawPath: string | undefined = extractToolArgString(section.toolArgs, TOOL_ARG_PATH_KEYS); + + if (!rawPath) { + const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true }); + const fallbackPath = args?.path ?? args?.file_path ?? args?.filePath; + + if (typeof fallbackPath === 'string' && fallbackPath) rawPath = fallbackPath; + } + + if (!rawPath) return null; + + const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath; + const language = + getFileTypeByExtension(rawPath)?.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '') ?? + CODE_BLOCK.DEFAULT_LANGUAGE; + const resultObj = tryParseToolResultObject(section.toolResult); + const bytesWritten = + resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined; + const resultMessage = typeof resultObj?.result === 'string' ? resultObj.result : undefined; + const errorMessage = typeof resultObj?.error === 'string' ? resultObj.error : undefined; + + return { + bytesWritten, + errorMessage, + fileName, + filePath: rawPath, + language, + resultMessage + }; +} diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 5137e261f..ea9428e07 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -46,49 +46,44 @@ isLastAssistantMessage ? !!agenticStore.getLastError(message.convId) : false ); - let permissionDismissed = $state(false); - const pendingPermission = $derived( isStreaming && isLastAssistantMessage ? agenticStore.getPendingPermissionRequest(message.convId) : null ); - let prevPendingRef: typeof pendingPermission = null; - $effect(() => { - if (pendingPermission !== prevPendingRef) { - prevPendingRef = pendingPermission; + // dismissal applies to the request object, so the next request ( new + // identity ) shows the card again without any reset bookkeeping + let dismissedPermission: typeof pendingPermission = $state(null); - if (pendingPermission) { - permissionDismissed = false; - } - } - }); + const visiblePermission = $derived( + pendingPermission && dismissedPermission !== pendingPermission ? pendingPermission : null + ); function handlePermission(decision: ToolPermissionDecision) { - permissionDismissed = true; + dismissedPermission = pendingPermission; agenticStore.resolvePermission(message.convId, decision); } - let continueDismissed = $state(false); - const pendingContinue = $derived( isStreaming && isLastAssistantMessage ? agenticStore.getPendingContinueRequest(message.convId) : false ); - let prevContinueRef = false; - $effect(() => { - if (pendingContinue !== prevContinueRef) { - prevContinueRef = pendingContinue; + let continueDismissed = $state(false); - if (pendingContinue) { - continueDismissed = false; - } + // the continue request is a plain boolean, so there is no identity to + // compare against; clear the dismissal whenever no request is pending so + // the next one starts from a clean state + $effect(() => { + if (!pendingContinue) { + continueDismissed = false; } }); + const showContinue = $derived(Boolean(pendingContinue) && !continueDismissed); + function handleContinue(shouldContinue: boolean) { continueDismissed = true; agenticStore.resolveContinue(message.convId, shouldContinue); @@ -238,15 +233,15 @@ {/each} {/if} - {#if pendingPermission && !permissionDismissed} + {#if visiblePermission} {/if} - {#if pendingContinue && !continueDismissed} + {#if showContinue} {/if}
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte index 4750a9f7c..0078225c0 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte @@ -1,5 +1,6 @@ -
- {#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)} - - {/each} - - {#if conversationsStore.activeConversation && agenticStore.getPendingSteeringMessageContent(conversationsStore.activeConversation!.id)} - {@const convId = conversationsStore.activeConversation!.id} - {@const pendingContent = agenticStore.getPendingSteeringMessageContent(convId)} - - {#if pendingContent} - agenticStore.clearSteeringMessage(convId)} - onEdit={(newContent, extras) => - agenticStore.injectSteeringMessage(convId, newContent, extras)} - onSendImmediately={() => chatStore.abortCurrentFlow(convId)} + +{#key conversationsStore.activeConversation?.id ?? 'new'} +
+ {#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)} + - {/if} - {:else if conversationsStore.activeConversation && chatStore.getPendingMessageContent(conversationsStore.activeConversation!.id)} - {@const convId = conversationsStore.activeConversation!.id} - {@const pendingContent = chatStore.getPendingMessageContent(convId)} + {/each} - {#if pendingContent} - chatStore.clearPendingMessage(convId)} - onEdit={(newContent, extras) => chatStore.injectPendingMessage(convId, newContent, extras)} - onSendImmediately={() => chatStore.abortCurrentFlow(convId)} - /> + {#if conversationsStore.activeConversation && agenticStore.getPendingSteeringMessageContent(conversationsStore.activeConversation!.id)} + {@const convId = conversationsStore.activeConversation!.id} + {@const pendingContent = agenticStore.getPendingSteeringMessageContent(convId)} + + {#if pendingContent} + agenticStore.clearSteeringMessage(convId)} + onEdit={(newContent, extras) => + agenticStore.injectSteeringMessage(convId, newContent, extras)} + onSendImmediately={() => chatStore.abortCurrentFlow(convId)} + /> + {/if} + {:else if conversationsStore.activeConversation && chatStore.getPendingMessageContent(conversationsStore.activeConversation!.id)} + {@const convId = conversationsStore.activeConversation!.id} + {@const pendingContent = chatStore.getPendingMessageContent(convId)} + + {#if pendingContent} + chatStore.clearPendingMessage(convId)} + onEdit={(newContent, extras) => + chatStore.injectPendingMessage(convId, newContent, extras)} + onSendImmediately={() => chatStore.abortCurrentFlow(convId)} + /> + {/if} {/if} - {/if} -
+
+{/key} + + diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/LazyChatMessage.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/LazyChatMessage.svelte new file mode 100644 index 000000000..f9667bbbb --- /dev/null +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/LazyChatMessage.svelte @@ -0,0 +1,105 @@ + + +
+ {#if mounted} + + {/if} +
+ + diff --git a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte index 3ad3f2468..6cea95d0d 100644 --- a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte @@ -315,13 +315,18 @@
bottomed move with transform, not bottom: + // layout-property transitions need the main thread every frame and + // stutter while a long conversation loads; transform transitions + // run on the compositor and stay smooth + 'pointer-events-none md:sticky fixed mt-auto transition-transform duration-200', deviceStore.isStandalone ? 'bottom-6 right-4 left-4' : deviceStore.isIOSSafari ? 'bottom-1 left-2 right-2' : 'bottom-2 right-2 left-2', - isEmpty ? 'md:bottom-[calc(50dvh-7rem)] 2xl:bottom-[calc(50dvh-4rem)]' : 'md:bottom-4' + 'md:bottom-4', + isEmpty ? 'md:translate-y-[calc(-50dvh+8rem)] 2xl:translate-y-[calc(-50dvh+5rem)]' : '' ]} > diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte b/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte index 87b41bd00..c217a769a 100644 --- a/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte +++ b/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte @@ -1,23 +1,12 @@ diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-processor.ts b/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-processor.ts new file mode 100644 index 000000000..e973a6a4b --- /dev/null +++ b/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-processor.ts @@ -0,0 +1,112 @@ +// Shared remark/rehype pipeline factory for MarkdownContent. +// +// The frozen plugin chain is expensive to build ( ~15 plugin instances ), +// and MarkdownContent used to rebuild it on every processMarkdown call: +// once per block at mount, and again on every coalesced chunk while +// streaming. Pipelines without attachments are shared process-wide per +// math flag; attachment-bearing pipelines are cached by the attachments +// array identity, which changes whenever extras are updated. + +import { rehypeEnhanceCodeBlocks } from './plugins/rehype/enhance-code-blocks'; +import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links'; +import { rehypeEnhanceMermaidBlocks } from './plugins/rehype/enhance-mermaid-blocks'; +import { rehypeEnhanceSvgBlocks } from './plugins/rehype/enhance-svg-blocks'; +import { rehypeFileBadge } from './plugins/rehype/file-badge'; +import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre'; +import { rehypeRtlSupport } from './plugins/rehype/rehype-rtl-support'; +import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images'; +import { rehypeSvgPre } from './plugins/rehype/svg-pre'; +import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer'; +import { remarkLiteralHtml } from './plugins/remark/literal-html'; +import { FileTypeText } from '$lib/enums/files.enums'; +import type { DatabaseMessageExtra } from '$lib/types/database'; +import type { Root as HastRoot } from 'hast'; +import { all as lowlightAll } from 'lowlight'; +import type { Root as MdastRoot } from 'mdast'; +import rehypeHighlight from 'rehype-highlight'; +import rehypeKatex from 'rehype-katex'; +import rehypeStringify from 'rehype-stringify'; +import { remark } from 'remark'; +import remarkBreaks from 'remark-breaks'; +import remarkGfm from 'remark-gfm'; +import remarkMath from 'remark-math'; +import remarkRehype from 'remark-rehype'; + +export interface MarkdownProcessor { + parse(markdown: string): MdastRoot; + run(tree: MdastRoot): Promise; + stringify(tree: HastRoot): string; +} + +export interface MarkdownProcessorOptions { + attachments?: DatabaseMessageExtra[]; + disableMath?: boolean; +} + +const sharedPipelines = new Map(); +const attachmentPipelines = new WeakMap(); + +function buildPipeline({ + attachments, + disableMath = false +}: MarkdownProcessorOptions): MarkdownProcessor { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let proc: any = remark().use(remarkGfm); // GitHub Flavored Markdown + + if (!disableMath) { + proc = proc.use(remarkMath); // Parse $inline$ and $$block$$ math + } + + proc = proc + .use(remarkBreaks) // Convert line breaks to
+ // Treat raw HTML as literal text with preserved indentation + .use(remarkLiteralHtml) + .use(remarkRehype); // Convert Markdown AST to rehype + + if (!disableMath) { + proc = proc.use(rehypeKatex); // Render math using KaTeX + } + + const pipeline = proc + .use(rehypeHighlight, { + aliases: { [FileTypeText.XML]: [FileTypeText.SVELTE, FileTypeText.VUE] }, + languages: lowlightAll + }) // Add syntax highlighting + .use(rehypeRestoreTableHtml) // Restore limited HTML (e.g.
,
    ) inside Markdown tables + .use(rehypeEnhanceLinks) // Add target="_blank" to links + .use(rehypeFileBadge) // Render file:// anchors as inline badge chips + .use(rehypeMermaidPre) // Convert mermaid blocks to
    +		.use(rehypeSvgPre) // Convert svg blocks to 
    +		.use(rehypeEnhanceCodeBlocks) // Wrap code blocks with header and actions
    +		.use(rehypeEnhanceMermaidBlocks) // Wrap mermaid blocks with header and actions
    +		.use(rehypeEnhanceSvgBlocks) // Wrap svg blocks with header and actions
    +		.use(rehypeResolveAttachmentImages, { attachments })
    +		.use(rehypeRtlSupport) // Add bidirectional text support
    +		.use(rehypeStringify, { allowDangerousHtml: true }); // Convert to HTML string
    +
    +	return pipeline as MarkdownProcessor;
    +}
    +
    +export function getMarkdownProcessor(options: MarkdownProcessorOptions): MarkdownProcessor {
    +	if (options.attachments && options.attachments.length > 0) {
    +		let cached = attachmentPipelines.get(options.attachments);
    +
    +		if (!cached) {
    +			cached = buildPipeline(options);
    +			attachmentPipelines.set(options.attachments, cached);
    +		}
    +
    +		return cached;
    +	}
    +
    +	const key = String(Boolean(options.disableMath));
    +
    +	let cached = sharedPipelines.get(key);
    +
    +	if (!cached) {
    +		cached = buildPipeline(options);
    +		sharedPipelines.set(key, cached);
    +	}
    +
    +	return cached;
    +}
    diff --git a/tools/ui/src/lib/constants/index.ts b/tools/ui/src/lib/constants/index.ts
    index e3241373e..d93ae6429 100644
    --- a/tools/ui/src/lib/constants/index.ts
    +++ b/tools/ui/src/lib/constants/index.ts
    @@ -16,6 +16,7 @@ export * from './context-gauge-popup.constants';
     export * from './conversation-import.constants';
     export * from './binary-detection.constants';
     export * from './content-detection.constants';
    +export * from './tool-call-args.constants';
     export * from './tool-ui.constants';
     export * from './cache.constants';
     export * from './chat-form.constants';
    diff --git a/tools/ui/src/lib/constants/tool-call-args.constants.ts b/tools/ui/src/lib/constants/tool-call-args.constants.ts
    new file mode 100644
    index 000000000..e74260be2
    --- /dev/null
    +++ b/tools/ui/src/lib/constants/tool-call-args.constants.ts
    @@ -0,0 +1,23 @@
    +// Tool-args and tool-result parsing helpers: the file tools' path field
    +// aliases, the JSON container gates for result blobs, and the targeted
    +// string-field pattern used for cheap title-tier extraction.
    +
    +/**
    + * Field aliases the file tools accept for the path argument. Tool contracts
    + * drifted over time: some models emit `file_path` / `filePath`.
    + */
    +export const TOOL_ARG_PATH_KEYS: readonly string[] = ['path', 'file_path', 'filePath'];
    +
    +/** Opening character of a JSON object; only an object root can carry fields. */
    +export const JSON_OBJECT_OPEN = '{';
    +
    +/** Opening character of a JSON array; successful sandbox output is one. */
    +export const JSON_ARRAY_OPEN = '[';
    +
    +/**
    + * Matches `"": ""` in a JSON args blob ( whitespace between
    + * tokens allowed ), capturing the raw string literal so only that literal
    + * gets decoded; escaped quotes stay inside the value group. `{key}` is
    + * replaced with the field name before use.
    + */
    +export const TOOL_ARG_STRING_FIELD_PATTERN_TEMPLATE = '"{key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"';
    diff --git a/tools/ui/src/lib/stores/chat/index.svelte.ts b/tools/ui/src/lib/stores/chat/index.svelte.ts
    index 296c2cca5..4bdcc6845 100644
    --- a/tools/ui/src/lib/stores/chat/index.svelte.ts
    +++ b/tools/ui/src/lib/stores/chat/index.svelte.ts
    @@ -55,7 +55,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
     		string,
     		{ response: string; messageId: string; model?: string | null }
     	>();
    -	currentResponse = $state('');
     	errorDialogState = $state(null);
     	// true while the active conversation has a local pipe (send, attach or resume-wait)
     	isLoading = $derived(this.activity.isLocal(conversationsStore.activeConversation?.id ?? ''));
    @@ -256,8 +255,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
     		}
     
     		this.chatStreamingStates.delete(convId);
    -
    -		if (convId === conversationsStore.activeConversation?.id) this.currentResponse = '';
     	}
     	clearEditMode(): void {
     		this.isEditModeActive = false;
    @@ -272,11 +269,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
     		this.pendingMessages.delete(convId);
     	}
     
    -	/** Reset per-view state when (re)mounting the empty chat screen. */
    -	clearUIState(): void {
    -		this.currentResponse = '';
    -	}
    -
     	consumePendingDraft(): { message: string; files: ChatUploadedFile[] } | null {
     		if (!this.pendingDraftMessage && this.pendingDraftFiles.length === 0) return null;
     
    @@ -766,8 +758,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
     			model: model ?? this.chatStreamingStates.get(convId)?.model,
     			response
     		});
    -
    -		if (convId === conversationsStore.activeConversation?.id) this.currentResponse = response;
     	}
     
     	setEditModeActive(handler: (files: File[]) => void): void {
    @@ -1244,7 +1234,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
     	syncLoadingStateForChat(convId: string): void {
     		const s = this.chatStreamingStates.get(convId);
     
    -		this.currentResponse = s?.response || '';
     		this.processing.setActiveConversation(convId);
     
     		// Sync streaming content to activeMessages so UI displays current content
    diff --git a/tools/ui/src/lib/stores/conversations/index.svelte.ts b/tools/ui/src/lib/stores/conversations/index.svelte.ts
    index df5b1ecef..c4fea2e4e 100644
    --- a/tools/ui/src/lib/stores/conversations/index.svelte.ts
    +++ b/tools/ui/src/lib/stores/conversations/index.svelte.ts
    @@ -52,6 +52,13 @@ class ConversationsStore implements ConversationsPreferencesHost {
     	/** In-flight init run; shared by concurrent callers, reset on failure to allow retry */
     	private initPromise: Promise | null = null;
     
    +	/**
    +	 * Messages loadConversation just read, handed off once so the chat
    +	 * screen can reuse them for sibling info instead of re-fetching the
    +	 * whole conversation a second time.
    +	 */
    +	private lastLoadedMessages: { convId: string; messages: DatabaseMessage[] } | null = null;
    +
     	/**
     	 * Memo of the last findMessageIndex() lookup. Streaming calls it once per
     	 * chunk for the same message, so a validated cache hit keeps that O(1)
    @@ -88,7 +95,13 @@ class ConversationsStore implements ConversationsPreferencesHost {
     		}
     
     		if (this.activeConversation?.id === id) {
    -			this.activeConversation = { ...this.activeConversation, ...updates };
    +			// field-wise, not object replacement: effects that track the active
    +			// conversation identity would otherwise refire on every rename or pin
    +			const target = this.activeConversation as unknown as Record;
    +
    +			for (const [key, value] of Object.entries(updates)) {
    +				if (target[key] !== value) target[key] = value;
    +			}
     		}
     	}
     
    @@ -202,11 +215,8 @@ class ConversationsStore implements ConversationsPreferencesHost {
     			const updates = await DatabaseService.bulkToggleConversationPins(convIds);
     			const activeId = this.activeConversation?.id;
     
    -			if (activeId && updates.has(activeId)) {
    -				this.activeConversation = {
    -					...this.activeConversation!,
    -					pinned: updates.get(activeId)!
    -				};
    +			if (this.activeConversation && activeId && updates.has(activeId)) {
    +				this.activeConversation.pinned = updates.get(activeId)!;
     			}
     
     			for (let i = 0; i < this.conversations.length; i++) {
    @@ -236,6 +246,17 @@ class ConversationsStore implements ConversationsPreferencesHost {
     		this.preferences.resetPending();
     	}
     
    +	/** One-shot handoff of the messages the last loadConversation read. */
    +	consumeLastLoadedMessages(convId: string): DatabaseMessage[] | null {
    +		if (this.lastLoadedMessages?.convId !== convId) return null;
    +
    +		const messages = this.lastLoadedMessages.messages;
    +
    +		this.lastLoadedMessages = null;
    +
    +		return messages;
    +	}
    +
     	/**
     	 * Creates a new conversation and navigates to it
     	 * @param name - Optional name for the conversation
    @@ -509,22 +530,15 @@ class ConversationsStore implements ConversationsPreferencesHost {
     			// it doesn't belong to this conversation.
     			this.preferences.pendingCwd = null;
     
    +			const allMessages = await DatabaseService.getConversationMessages(convId);
    +
    +			// set conversation and messages in one sync block so effects never see
    +			// the new conversation with the previous conversation's messages
    +			this.lastLoadedMessages = { convId, messages: allMessages };
     			this.activeConversation = conversation;
    -
    -			if (conversation.currNode) {
    -				const allMessages = await DatabaseService.getConversationMessages(convId);
    -				const filteredMessages = filterByLeafNodeId(
    -					allMessages,
    -					conversation.currNode,
    -					false
    -				) as DatabaseMessage[];
    -
    -				this.activeMessages = filteredMessages;
    -			} else {
    -				const messages = await DatabaseService.getConversationMessages(convId);
    -
    -				this.activeMessages = messages;
    -			}
    +			this.activeMessages = conversation.currNode
    +				? (filterByLeafNodeId(allMessages, conversation.currNode, false) as DatabaseMessage[])
    +				: allMessages;
     
     			return true;
     		} catch (error) {
    @@ -558,7 +572,7 @@ class ConversationsStore implements ConversationsPreferencesHost {
     		const currentLeafNodeId = findLeafNode(allMessages, siblingId);
     
     		await DatabaseService.updateCurrentNode(this.activeConversation.id, currentLeafNodeId);
    -		this.activeConversation = { ...this.activeConversation, currNode: currentLeafNodeId };
    +		this.activeConversation.currNode = currentLeafNodeId;
     		await this.refreshActiveMessages();
     
     		if (rootMessage && this.activeMessages.length > 0) {
    @@ -694,7 +708,7 @@ class ConversationsStore implements ConversationsPreferencesHost {
     		}
     
     		if (this.activeConversation?.id === targetId) {
    -			this.activeConversation = { ...this.activeConversation, lastModified: now };
    +			this.activeConversation.lastModified = now;
     		}
     
     		DatabaseService.updateConversation(targetId, { lastModified: now }).catch((error) =>
    @@ -710,7 +724,7 @@ class ConversationsStore implements ConversationsPreferencesHost {
     		if (!this.activeConversation) return;
     
     		await DatabaseService.updateCurrentNode(this.activeConversation.id, nodeId);
    -		this.activeConversation = { ...this.activeConversation, currNode: nodeId };
    +		this.activeConversation.currNode = nodeId;
     	}
     
     	/**
    diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts
    index d91c2811a..333c1bd3c 100644
    --- a/tools/ui/src/lib/types/index.ts
    +++ b/tools/ui/src/lib/types/index.ts
    @@ -209,7 +209,16 @@ export type {
     export type { DesktopIconStripItem } from './navigation';
     
     // Tools types
    -export type { ToolEntry, ToolGroup, ToolUiEntry } from './tools';
    +export type {
    +	EditFileEdit,
    +	EditFileMeta,
    +	EditFileTitleMeta,
    +	ToolEntry,
    +	ToolGroup,
    +	ToolUiEntry,
    +	WriteFileMeta,
    +	WriteFileTitleMeta
    +} from './tools';
     
     // Reasoning
     export type { ReasoningEffortLevel } from './reasoning';
    diff --git a/tools/ui/src/lib/types/tools.d.ts b/tools/ui/src/lib/types/tools.d.ts
    index edcec65c7..fa8963bd1 100644
    --- a/tools/ui/src/lib/types/tools.d.ts
    +++ b/tools/ui/src/lib/types/tools.d.ts
    @@ -31,3 +31,50 @@ export interface ToolGroup {
     	serverId?: string;
     	tools: ToolEntry[];
     }
    +
    +export interface WriteFileMeta {
    +	fileName: string;
    +	filePath: string;
    +	language: string;
    +	content: string;
    +	bytesWritten?: number;
    +	resultMessage?: string;
    +	errorMessage?: string;
    +}
    +
    +/** Everything the write_file block title and status pill show; the full meta
    + *  ( with the embedded file content ) stays body-only so collapsed blocks
    + *  never parse the content blob. */
    +export interface WriteFileTitleMeta {
    +	fileName: string;
    +	filePath: string;
    +	language: string;
    +	bytesWritten?: number;
    +	resultMessage?: string;
    +	errorMessage?: string;
    +}
    +
    +export interface EditFileEdit {
    +	oldText: string;
    +	newText: string;
    +}
    +
    +export interface EditFileMeta {
    +	fileName: string;
    +	filePath: string;
    +	edits: EditFileEdit[];
    +	resultMessage?: string;
    +	editsApplied?: number;
    +	errorMessage?: string;
    +}
    +
    +/** Everything the edit_file block title and status pill show; the full meta
    + *  ( with the embedded edit strings ) stays body-only so collapsed blocks
    + *  never parse the args blob. */
    +export interface EditFileTitleMeta {
    +	fileName: string;
    +	filePath: string;
    +	resultMessage?: string;
    +	editsApplied?: number;
    +	errorMessage?: string;
    +}
    diff --git a/tools/ui/src/lib/utils/agentic.ts b/tools/ui/src/lib/utils/agentic.ts
    index cd150c5ef..28b3f43ee 100644
    --- a/tools/ui/src/lib/utils/agentic.ts
    +++ b/tools/ui/src/lib/utils/agentic.ts
    @@ -109,6 +109,89 @@ function deriveSingleTurnSections(
     	return sections;
     }
     
    +interface TurnSectionsCacheEntry {
    +	content: string | undefined;
    +	extra: DatabaseMessageExtra[] | undefined;
    +	reasoningContent: string | undefined;
    +	toolCalls: string | undefined;
    +	toolMessageContents: (string | undefined)[];
    +	toolMessageExtras: (DatabaseMessageExtra[] | undefined)[];
    +	toolMessages: DatabaseMessage[];
    +	sections: AgenticSection[];
    +}
    +
    +const turnSectionsCache = new WeakMap();
    +
    +function isTurnCacheValid(
    +	entry: TurnSectionsCacheEntry,
    +	message: DatabaseMessage,
    +	toolMessages: DatabaseMessage[]
    +): boolean {
    +	if (
    +		entry.content !== message.content ||
    +		entry.reasoningContent !== message.reasoningContent ||
    +		entry.toolCalls !== message.toolCalls ||
    +		entry.extra !== message.extra
    +	) {
    +		return false;
    +	}
    +
    +	if (entry.toolMessages.length !== toolMessages.length) return false;
    +
    +	for (let i = 0; i < toolMessages.length; i++) {
    +		if (entry.toolMessages[i] !== toolMessages[i]) return false;
    +
    +		if (entry.toolMessageContents[i] !== toolMessages[i].content) return false;
    +
    +		if (entry.toolMessageExtras[i] !== toolMessages[i].extra) return false;
    +	}
    +
    +	return true;
    +}
    +
    +/**
    + * deriveSingleTurnSections with structural reuse for completed turns.
    + *
    + * deriveAgenticSections runs in a $derived invalidated per streamed chunk, but
    + * only the last turn actually changes. Messages mutate in place and are never
    + * replaced, so a WeakMap keyed by the turn's assistant message plus reference
    + * checks on every field deriveSingleTurnSections reads detects any change. A
    + * cache hit also returns the same section objects, keeping downstream props
    + * stable so tool blocks skip their per-chunk re-derive. The streaming turn
    + * recomputes uncached on every chunk.
    + */
    +function deriveTurnSections(
    +	message: DatabaseMessage,
    +	toolMessages: DatabaseMessage[],
    +	streamingToolCalls: ApiChatCompletionToolCall[],
    +	isStreaming: boolean
    +): AgenticSection[] {
    +	if (isStreaming || streamingToolCalls.length > 0) {
    +		return deriveSingleTurnSections(message, toolMessages, streamingToolCalls, isStreaming);
    +	}
    +
    +	const cached = turnSectionsCache.get(message);
    +
    +	if (cached && isTurnCacheValid(cached, message, toolMessages)) {
    +		return cached.sections;
    +	}
    +
    +	const sections = deriveSingleTurnSections(message, toolMessages, [], false);
    +
    +	turnSectionsCache.set(message, {
    +		content: message.content,
    +		extra: message.extra,
    +		reasoningContent: message.reasoningContent,
    +		sections,
    +		toolCalls: message.toolCalls,
    +		toolMessageContents: toolMessages.map((tm) => tm.content),
    +		toolMessageExtras: toolMessages.map((tm) => tm.extra),
    +		toolMessages
    +	});
    +
    +	return sections;
    +}
    +
     /**
      * Derives display sections from structured message data.
      *
    @@ -132,13 +215,13 @@ export function deriveAgenticSections(
     	const hasAssistantContinuations = toolMessages.some((m) => m.role === MessageRole.ASSISTANT);
     
     	if (!hasAssistantContinuations) {
    -		return deriveSingleTurnSections(message, toolMessages, streamingToolCalls, isStreaming);
    +		return deriveTurnSections(message, toolMessages, streamingToolCalls, isStreaming);
     	}
     
     	const sections: AgenticSection[] = [];
     	const firstTurnToolMsgs = collectToolMessages(toolMessages, 0);
     
    -	sections.push(...deriveSingleTurnSections(message, firstTurnToolMsgs));
    +	sections.push(...deriveTurnSections(message, firstTurnToolMsgs, [], false));
     
     	let i = firstTurnToolMsgs.length;
     
    @@ -150,7 +233,7 @@ export function deriveAgenticSections(
     			const isLastTurn = i + 1 + turnToolMsgs.length >= toolMessages.length;
     
     			sections.push(
    -				...deriveSingleTurnSections(
    +				...deriveTurnSections(
     					msg,
     					turnToolMsgs,
     					isLastTurn ? streamingToolCalls : [],
    diff --git a/tools/ui/src/lib/utils/branching.ts b/tools/ui/src/lib/utils/branching.ts
    index 6c2c895cb..43d33d424 100644
    --- a/tools/ui/src/lib/utils/branching.ts
    +++ b/tools/ui/src/lib/utils/branching.ts
    @@ -105,18 +105,34 @@ export function filterByLeafNodeId(
      */
     function findLeafNodeInMap(
     	nodeMap: ReadonlyMap,
    -	messageId: string
    +	messageId: string,
    +	leafCache?: Map
     ): string {
    +	const path: string[] = [];
    +
     	let currentNode: DatabaseMessage | undefined = nodeMap.get(messageId);
     
     	while (currentNode && currentNode.children.length > 0) {
     		// Follow the last child (most recent branch)
    +		const cached = leafCache?.get(currentNode.id);
    +
    +		if (cached !== undefined) {
    +			for (const id of path) leafCache?.set(id, cached);
    +
    +			return cached;
    +		}
    +
    +		path.push(currentNode.id);
     		const lastChildId = currentNode.children[currentNode.children.length - 1];
     
     		currentNode = nodeMap.get(lastChildId);
     	}
     
    -	return currentNode?.id ?? messageId;
    +	const leafId = currentNode?.id ?? messageId;
    +
    +	for (const id of path) leafCache?.set(id, leafId);
    +
    +	return leafId;
     }
     
     /**
    @@ -176,7 +192,8 @@ export function findDescendantMessages(
      */
     export function getMessageSiblings(
     	nodeMap: ReadonlyMap,
    -	messageId: string
    +	messageId: string,
    +	leafCache?: Map
     ): ChatMessageSiblingInfo | null {
     	const message = nodeMap.get(messageId);
     
    @@ -212,7 +229,7 @@ export function getMessageSiblings(
     	// Convert sibling message IDs to their corresponding leaf node IDs
     	// This allows navigation between different conversation branches
     	const siblingLeafIds = siblingIds.map((siblingId: string) =>
    -		findLeafNodeInMap(nodeMap, siblingId)
    +		findLeafNodeInMap(nodeMap, siblingId, leafCache)
     	);
     	// Find current message's position among siblings
     	const currentIndex = siblingIds.indexOf(messageId);
    @@ -236,9 +253,12 @@ export function buildSiblingInfoMap(
     ): Map {
     	const nodeMap = new Map(messages.map((msg) => [msg.id, msg] as const));
     	const siblingMap = new Map();
    +	// Leaf walks repeat along the same child chains for every message; memoize
    +	// them per build so each edge is walked once instead of O(messages^2)
    +	const leafCache = new Map();
     
     	for (const msg of messages) {
    -		const info = getMessageSiblings(nodeMap, msg.id);
    +		const info = getMessageSiblings(nodeMap, msg.id, leafCache);
     
     		if (info) {
     			siblingMap.set(msg.id, info);
    diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts
    index 079cdc871..721618c48 100644
    --- a/tools/ui/src/lib/utils/index.ts
    +++ b/tools/ui/src/lib/utils/index.ts
    @@ -285,7 +285,8 @@ export {
     	extractSearchResults,
     	extractSearchQuery,
     	faviconForUrl,
    -	isWebSearchToolName
    +	isWebSearchToolName,
    +	looksLikeSearchResult
     } from './search-results';
     
     // Cache utilities
    diff --git a/tools/ui/src/lib/utils/parse-exec-shell-error.ts b/tools/ui/src/lib/utils/parse-exec-shell-error.ts
    index 42d2ee254..a7b2eb5c8 100644
    --- a/tools/ui/src/lib/utils/parse-exec-shell-error.ts
    +++ b/tools/ui/src/lib/utils/parse-exec-shell-error.ts
    @@ -3,8 +3,14 @@ export function parseExecShellCommandError(
     ): string | undefined {
     	if (!toolResultString) return undefined;
     
    +	// Exec results are usually large plain-text stdout; only a JSON object
    +	// root can carry an error field, so skip the parse otherwise
    +	const trimmed = toolResultString.trimStart();
    +
    +	if (trimmed[0] !== '{') return undefined;
    +
     	try {
    -		const parsed: unknown = JSON.parse(toolResultString);
    +		const parsed: unknown = JSON.parse(trimmed);
     
     		if (
     			parsed &&
    diff --git a/tools/ui/src/lib/utils/parse-exec-shell-status.ts b/tools/ui/src/lib/utils/parse-exec-shell-status.ts
    index 1f7ec557e..71dd110bd 100644
    --- a/tools/ui/src/lib/utils/parse-exec-shell-status.ts
    +++ b/tools/ui/src/lib/utils/parse-exec-shell-status.ts
    @@ -15,15 +15,18 @@ export interface ExecShellExitStatus {
     }
     
     // Anchor to the absolute end so intermediate "[exit code: N]" string content
    -// (e.g. a shell echo) doesn't false-positive.
    +// (e.g. a shell echo) doesn't false-positive. The marker is at most ~50 chars
    +// with the timed-out suffix, so matching a tail slice keeps the cost constant
    +// for megabyte exec outputs instead of scanning the whole blob.
     const EXIT_CODE_TAIL_REGEX = /\[exit code: (-?\d+)\](?: \[exit due to timed out\])?\s*$/;
    +const EXIT_CODE_TAIL_SCAN = 128;
     
     export function parseExecShellCommandExitStatus(
     	toolResultString: string | undefined
     ): ExecShellExitStatus | undefined {
     	if (!toolResultString) return undefined;
     
    -	const match = toolResultString.match(EXIT_CODE_TAIL_REGEX);
    +	const match = toolResultString.slice(-EXIT_CODE_TAIL_SCAN).match(EXIT_CODE_TAIL_REGEX);
     
     	if (!match) return undefined;
     
    diff --git a/tools/ui/src/lib/utils/search-results.ts b/tools/ui/src/lib/utils/search-results.ts
    index facf7766d..0fe861d94 100644
    --- a/tools/ui/src/lib/utils/search-results.ts
    +++ b/tools/ui/src/lib/utils/search-results.ts
    @@ -156,6 +156,20 @@ function parseChunk(chunk: string): SearchResult | null {
     	return result;
     }
     
    +const EMPTY_SEARCH_RESULTS: SearchResult[] = [];
    +
    +/**
    + * Cheap prefilter for the wire format: a parseable result needs both a
    + * `Title:` and a `URL:` field line, so a blob missing either substring can
    + * never yield a result. Two substring scans cost far less than the
    + * line-split parse for the megabyte tool results exec and file tools emit.
    + */
    +export function looksLikeSearchResult(text: string | undefined | null): boolean {
    +	if (!text) return false;
    +
    +	return text.includes('Title:') && text.includes('URL:');
    +}
    +
     /** Bounded cache for extractSearchResults results. */
     const SEARCH_RESULTS_CACHE_MAX_SIZE = 32;
     const searchResultsCache = new Map();
    @@ -168,7 +182,7 @@ const searchResultsCache = new Map();
      * tool result strings.
      */
     export function extractSearchResults(text: string | undefined | null): SearchResult[] {
    -	if (!text) return [];
    +	if (!text || !looksLikeSearchResult(text)) return EMPTY_SEARCH_RESULTS;
     
     	const cached = searchResultsCache.get(text);
     
    diff --git a/tools/ui/src/lib/utils/tool-call-meta.ts b/tools/ui/src/lib/utils/tool-call-meta.ts
    index b64bca786..2c035446d 100644
    --- a/tools/ui/src/lib/utils/tool-call-meta.ts
    +++ b/tools/ui/src/lib/utils/tool-call-meta.ts
    @@ -4,6 +4,8 @@
     // Each tool needs to surface fields like `error`, `result`, `bytes`,
     // `edits_applied` without repeating the try/JSON.parse/object guard inline.
     
    +import { JSON_OBJECT_OPEN } from '$lib/constants';
    +
     /**
      * Parse a tool-result blob into a JSON object, or `null` if it isn't
      * one. Returns null for:
    @@ -16,8 +18,14 @@ export function tryParseToolResultObject(
     ): Record | null {
     	if (!toolResultString) return null;
     
    +	// Tool results are usually large plain text (file contents, stdout); only
    +	// a JSON object root can carry fields, so skip the parse otherwise
    +	const trimmed = toolResultString.trimStart();
    +
    +	if (trimmed[0] !== JSON_OBJECT_OPEN) return null;
    +
     	try {
    -		const parsed: unknown = JSON.parse(toolResultString);
    +		const parsed: unknown = JSON.parse(trimmed);
     
     		if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
     			return parsed as Record;
    diff --git a/tools/ui/src/routes/(chat)/+page.svelte b/tools/ui/src/routes/(chat)/+page.svelte
    index 08a6b11ad..53975d7b3 100644
    --- a/tools/ui/src/routes/(chat)/+page.svelte
    +++ b/tools/ui/src/routes/(chat)/+page.svelte
    @@ -3,7 +3,7 @@
     	import { page } from '$app/state';
     	import { DialogModelNotAvailable } from '$lib/components/app';
     	import { APP_NAME, URL_PARAMS } from '$lib/constants';
    -	import { chatStore, conversationsStore, modelsStore, serverStore } from '$lib/stores';
    +	import { conversationsStore, modelsStore, serverStore } from '$lib/stores';
     	import { onMount } from 'svelte';
     
     	let qParam = $derived(page.url.searchParams.get(URL_PARAMS.QUERY));
    @@ -77,7 +77,6 @@
     		}
     
     		conversationsStore.clearActiveConversation();
    -		chatStore.clearUIState();
     
     		await modelsStore.fetch();
     
    diff --git a/tools/ui/tests/unit/agentic-sections.test.ts b/tools/ui/tests/unit/agentic-sections.test.ts
    index 4096a1710..fdb3b2217 100644
    --- a/tools/ui/tests/unit/agentic-sections.test.ts
    +++ b/tools/ui/tests/unit/agentic-sections.test.ts
    @@ -290,3 +290,114 @@ describe('hasAgenticContent', () => {
     		expect(hasAgenticContent(msg)).toBe(false);
     	});
     });
    +
    +// The turn-section cache: completed turns are immutable, so repeated
    +// derivations return the same section objects - which is what keeps tool
    +// block props stable while another turn streams. Every field the cache
    +// compares must invalidate it; a miss here renders stale content.
    +
    +describe('completed turn section reuse', () => {
    +	const toolCallsJson = JSON.stringify([
    +		{ function: { arguments: '{"path":"/a"}', name: 'test' }, id: 'call_1', type: 'function' }
    +	]);
    +
    +	function makeSession() {
    +		return {
    +			anchor: makeAssistant({
    +				content: 'answer',
    +				reasoningContent: 'thinking',
    +				toolCalls: toolCallsJson
    +			}),
    +			tools: [makeToolMsg({ content: 'tool result', extra: [{ type: 'file' } as never] })]
    +		};
    +	}
    +
    +	it('returns the same section objects for unchanged inputs', () => {
    +		const { anchor, tools } = makeSession();
    +		const first = deriveAgenticSections(anchor, tools, [], false);
    +		const second = deriveAgenticSections(anchor, tools, [], false);
    +
    +		expect(second[0]).toBe(first[0]);
    +		expect(second[1]).toBe(first[1]);
    +	});
    +
    +	it('recomputes when the assistant content changes', () => {
    +		const { anchor, tools } = makeSession();
    +		const first = deriveAgenticSections(anchor, tools, [], false);
    +
    +		anchor.content = 'edited';
    +		const second = deriveAgenticSections(anchor, tools, [], false);
    +
    +		expect(second).not.toBe(first);
    +		expect(second.some((s) => s.type === AgenticSectionType.TEXT && s.content === 'edited')).toBe(
    +			true
    +		);
    +	});
    +
    +	it('recomputes when reasoning content changes', () => {
    +		const { anchor, tools } = makeSession();
    +		const first = deriveAgenticSections(anchor, tools, [], false);
    +
    +		anchor.reasoningContent = 'new thinking';
    +		const second = deriveAgenticSections(anchor, tools, [], false);
    +
    +		expect(second).not.toBe(first);
    +	});
    +
    +	it('recomputes when toolCalls change', () => {
    +		const { anchor, tools } = makeSession();
    +		const first = deriveAgenticSections(anchor, tools, [], false);
    +
    +		anchor.toolCalls = '[]';
    +		const second = deriveAgenticSections(anchor, tools, [], false);
    +
    +		expect(second).not.toBe(first);
    +	});
    +
    +	it('recomputes when a tool result or its extras change', () => {
    +		const { anchor, tools } = makeSession();
    +		const first = deriveAgenticSections(anchor, tools, [], false);
    +
    +		tools[0].content = 'new tool result';
    +		expect(deriveAgenticSections(anchor, tools, [], false)).not.toBe(first);
    +
    +		const firstAfterContent = deriveAgenticSections(anchor, tools, [], false);
    +
    +		tools[0].extra = [{ type: 'image' } as never];
    +		expect(deriveAgenticSections(anchor, tools, [], false)).not.toBe(firstAfterContent);
    +	});
    +
    +	it('never reuses the streaming turn', () => {
    +		const { anchor, tools } = makeSession();
    +		const first = deriveAgenticSections(anchor, tools, [], true);
    +		const second = deriveAgenticSections(anchor, tools, [], true);
    +
    +		expect(second).not.toBe(first);
    +	});
    +
    +	it('keeps completed turns stable while the last turn streams', () => {
    +		const anchor = makeAssistant({
    +			content: 'turn one',
    +			id: 'ast-1',
    +			toolCalls: JSON.stringify([
    +				{ function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
    +			])
    +		});
    +		const continuation = makeAssistant({ content: 'turn two', id: 'ast-2' });
    +		const tools = [
    +			makeToolMsg({ content: 'r1', id: 'tool-1', toolCallId: 'call_1' }),
    +			continuation,
    +			makeToolMsg({ content: 'r2', id: 'tool-2', toolCallId: 'call_2' })
    +		];
    +		const first = deriveAgenticSections(anchor, tools, [], true);
    +		const second = deriveAgenticSections(anchor, tools, [], true);
    +
    +		// turn one is complete: identical section objects across derivations
    +		expect(second.slice(0, 2)).toEqual(first.slice(0, 2));
    +		expect(second[0]).toBe(first[0]);
    +		expect(second[1]).toBe(first[1]);
    +
    +		// the streaming last turn recomputed: fresh section objects
    +		expect(second[second.length - 1]).not.toBe(first[first.length - 1]);
    +	});
    +});
    diff --git a/tools/ui/tests/unit/branching.test.ts b/tools/ui/tests/unit/branching.test.ts
    new file mode 100644
    index 000000000..8a752ae2f
    --- /dev/null
    +++ b/tools/ui/tests/unit/branching.test.ts
    @@ -0,0 +1,95 @@
    +// Sibling-info correctness for buildSiblingInfoMap, including the memoized
    +// leaf resolution. A wrong leaf id here breaks branch navigation, so the
    +// deep-chain and multi-branch cases below pin the resolution down.
    +
    +import { MessageRole, MessageType } from '$lib/enums';
    +import type { DatabaseMessage } from '$lib/types/database';
    +import { buildSiblingInfoMap, findLeafNode } from '$lib/utils/branching';
    +import { describe, expect, it } from 'vitest';
    +
    +function msg(id: string, parent: string | null, children: string[] = []): DatabaseMessage {
    +	return {
    +		children,
    +		content: '',
    +		convId: 'c1',
    +		id,
    +		parent,
    +		role: MessageRole.USER,
    +		timestamp: 0,
    +		type: MessageType.TEXT
    +	} as DatabaseMessage;
    +}
    +
    +/** root -> m1 -> ... -> m depth, each node with a single child. */
    +function linearChain(depth: number): DatabaseMessage[] {
    +	const messages = [msg('m0', null, ['m1'])];
    +
    +	for (let i = 1; i <= depth; i++) {
    +		messages.push(msg(`m${i}`, `m${i - 1}`, i < depth ? [`m${i + 1}`] : []));
    +	}
    +
    +	return messages;
    +}
    +
    +describe('buildSiblingInfoMap', () => {
    +	it('resolves the deepest leaf for every node of a long single chain', () => {
    +		const messages = linearChain(50);
    +		const map = buildSiblingInfoMap(messages);
    +		const leafId = messages[messages.length - 1].id;
    +
    +		// every non-root message of the chain is an only child, and its
    +		// navigation target is the chain's deepest leaf
    +		for (const m of messages.slice(1)) {
    +			const info = map.get(m.id);
    +
    +			expect(info?.totalSiblings).toBe(1);
    +			expect(info?.siblingIds).toEqual([leafId]);
    +		}
    +	});
    +
    +	it('reports sibling position and leaf targets on a branched tree', () => {
    +		// m0 -> m1, m4 ; m1 -> m2 ; m2 -> m3, m6 ; m4 -> m5
    +		const root = msg('m0', null, ['m1', 'm4']);
    +		const m1 = msg('m1', 'm0', ['m2']);
    +		const m2 = msg('m2', 'm1', ['m3', 'm6']);
    +		const m3 = msg('m3', 'm2');
    +		const m4 = msg('m4', 'm0', ['m5']);
    +		const m5 = msg('m5', 'm4');
    +		const m6 = msg('m6', 'm2');
    +		const map = buildSiblingInfoMap([root, m1, m2, m3, m4, m5, m6]);
    +
    +		// m1 and m4 share the root as parent; their nav targets are the
    +		// leaves of their subtrees ( m6 for the first branch, m5 for the second )
    +		expect(map.get(m1.id)).toMatchObject({
    +			currentIndex: 0,
    +			siblingIds: [m6.id, m5.id],
    +			totalSiblings: 2
    +		});
    +		expect(map.get(m4.id)).toMatchObject({
    +			currentIndex: 1,
    +			siblingIds: [m6.id, m5.id],
    +			totalSiblings: 2
    +		});
    +
    +		// m3 and m6 are siblings under m2; both are leaves
    +		expect(map.get(m3.id)?.siblingIds).toEqual([m3.id, m6.id]);
    +		expect(map.get(m6.id)?.currentIndex).toBe(1);
    +
    +		// the root has no parent and reports itself
    +		expect(map.get(root.id)).toMatchObject({
    +			currentIndex: 0,
    +			siblingIds: [root.id],
    +			totalSiblings: 1
    +		});
    +	});
    +
    +	it('agrees with findLeafNode for arbitrary nodes', () => {
    +		const messages = linearChain(20);
    +		const leafId = messages[messages.length - 1].id;
    +
    +		// every node of the chain resolves to the deepest leaf
    +		for (const m of messages) {
    +			expect(findLeafNode(messages, m.id), `leaf of ${m.id}`).toBe(leafId);
    +		}
    +	});
    +});
    diff --git a/tools/ui/tests/unit/conversations-store.test.ts b/tools/ui/tests/unit/conversations-store.test.ts
    new file mode 100644
    index 000000000..e06546597
    --- /dev/null
    +++ b/tools/ui/tests/unit/conversations-store.test.ts
    @@ -0,0 +1,90 @@
    +// Field updates to the active conversation must keep the object identity
    +// stable: effects that track the identity ( the chat screen's sibling-info
    +// refresh ) refire on every identity change, which used to trigger a full
    +// message refetch on every send and tool result.
    +
    +import { beforeEach, describe, expect, it, vi } from 'vitest';
    +
    +vi.mock('$lib/services/database.service', () => ({
    +	DatabaseService: {
    +		getConversation: vi.fn(),
    +		getConversationMessages: vi.fn(),
    +		updateConversation: vi.fn(),
    +		updateCurrentNode: vi.fn()
    +	}
    +}));
    +
    +import { DatabaseService } from '$lib/services/database.service';
    +import { conversationsStore } from '$lib/stores/conversations/index.svelte';
    +import type { DatabaseConversation, DatabaseMessage } from '$lib/types/database';
    +
    +const getConversationMock = vi.mocked(DatabaseService.getConversation);
    +const getMessagesMock = vi.mocked(DatabaseService.getConversationMessages);
    +const updateCurrentNodeMock = vi.mocked(DatabaseService.updateCurrentNode);
    +
    +function makeConversation(overrides: Partial = {}): DatabaseConversation {
    +	return {
    +		currNode: 'node-1',
    +		id: 'conv-1',
    +		lastModified: 1000,
    +		name: 'conversation',
    +		...overrides
    +	};
    +}
    +
    +async function loadActive(conversation: DatabaseConversation, messages: DatabaseMessage[]) {
    +	getConversationMock.mockResolvedValue(conversation);
    +	getMessagesMock.mockResolvedValue(messages);
    +
    +	expect(await conversationsStore.loadConversation(conversation.id)).toBe(true);
    +}
    +
    +beforeEach(() => {
    +	getConversationMock.mockReset();
    +	getMessagesMock.mockReset();
    +	updateCurrentNodeMock.mockReset();
    +	updateCurrentNodeMock.mockResolvedValue(undefined);
    +	vi.mocked(DatabaseService.updateConversation).mockReset();
    +	vi.mocked(DatabaseService.updateConversation).mockResolvedValue(undefined);
    +});
    +
    +describe('active conversation identity', () => {
    +	it('hands the load read off exactly once', async () => {
    +		await loadActive(makeConversation(), []);
    +
    +		expect(conversationsStore.consumeLastLoadedMessages('conv-1')).toEqual([]);
    +		// a second consume is a miss: branch actions must fall back to a refetch
    +		expect(conversationsStore.consumeLastLoadedMessages('conv-1')).toBeNull();
    +	});
    +
    +	it('writes currNode in place on updateCurrentNode', async () => {
    +		await loadActive(makeConversation(), []);
    +		const before = conversationsStore.activeConversation;
    +
    +		await conversationsStore.updateCurrentNode('node-2');
    +
    +		expect(conversationsStore.activeConversation).toBe(before);
    +		expect(conversationsStore.activeConversation?.currNode).toBe('node-2');
    +	});
    +
    +	it('writes renamed and pinned fields in place on applyConversationUpdate', async () => {
    +		await loadActive(makeConversation(), []);
    +		const before = conversationsStore.activeConversation;
    +
    +		conversationsStore.applyConversationUpdate('conv-1', { name: 'renamed', pinned: true });
    +
    +		expect(conversationsStore.activeConversation).toBe(before);
    +		expect(conversationsStore.activeConversation?.name).toBe('renamed');
    +		expect(conversationsStore.activeConversation?.pinned).toBe(true);
    +	});
    +
    +	it('writes lastModified in place on updateConversationTimestamp', async () => {
    +		await loadActive(makeConversation(), []);
    +		const before = conversationsStore.activeConversation;
    +
    +		conversationsStore.updateConversationTimestamp('conv-1');
    +
    +		expect(conversationsStore.activeConversation).toBe(before);
    +		expect(conversationsStore.activeConversation?.lastModified).toBeGreaterThan(1000);
    +	});
    +});
    diff --git a/tools/ui/tests/unit/parse-exec-shell-status.test.ts b/tools/ui/tests/unit/parse-exec-shell-status.test.ts
    index ed499d078..7e22bf9ee 100644
    --- a/tools/ui/tests/unit/parse-exec-shell-status.test.ts
    +++ b/tools/ui/tests/unit/parse-exec-shell-status.test.ts
    @@ -71,3 +71,21 @@ describe('isExitCodeSummaryLine', () => {
     		expect(isExitCodeSummaryLine('[exit code: 7]', undefined)).toBe(false);
     	});
     });
    +
    +describe('parseExecShellCommandExitStatus tail scan', () => {
    +	it('finds the marker at the end of a blob larger than the tail window', () => {
    +		// the parser matches only the last ~128 chars; a marker past that
    +		// window must still parse, and an earlier fake must not match
    +		const blob = `${'the shell prints [exit code: 1] mid-stream\n'.repeat(2000)}[exit code: 0]`;
    +		const status = parseExecShellCommandExitStatus(blob);
    +
    +		expect(status?.code).toBe(0);
    +		expect(status?.timedOut).toBe(false);
    +	});
    +
    +	it('keeps rejecting markers that are not at the absolute end', () => {
    +		const blob = `${'stdout\n'.repeat(2000)}[exit code: 0]\nsome trailing log line`;
    +
    +		expect(parseExecShellCommandExitStatus(blob)).toBeUndefined();
    +	});
    +});
    diff --git a/tools/ui/tests/unit/search-results.test.ts b/tools/ui/tests/unit/search-results.test.ts
    index c168dec25..561ab935a 100644
    --- a/tools/ui/tests/unit/search-results.test.ts
    +++ b/tools/ui/tests/unit/search-results.test.ts
    @@ -2,7 +2,8 @@ import {
     	extractSearchQuery,
     	extractSearchResults,
     	faviconForUrl,
    -	isWebSearchToolName
    +	isWebSearchToolName,
    +	looksLikeSearchResult
     } from '$lib/utils/search-results';
     import { describe, expect, it } from 'vitest';
     
    @@ -119,3 +120,27 @@ describe('isWebSearchToolName', () => {
     		expect(isWebSearchToolName('exec_shell_command')).toBe(false);
     	});
     });
    +
    +describe('extractSearchResults prefilter', () => {
    +	it('returns the shared empty array for blobs without the wire format', () => {
    +		// exec/file tool results never carry Title:/URL: field lines; the
    +		// cheap prefilter must skip the line-split parse for them
    +		const stdout = `${'make[1]: entering directory\n'.repeat(5000)}`;
    +
    +		expect(extractSearchResults(stdout)).toEqual([]);
    +	});
    +
    +	it('returns an empty result when only one required field is present', () => {
    +		expect(extractSearchResults('URL: https://example.com')).toEqual([]);
    +		expect(extractSearchResults('Title: only a title')).toEqual([]);
    +	});
    +});
    +
    +describe('looksLikeSearchResult', () => {
    +	it('requires both Title and URL field markers', () => {
    +		expect(looksLikeSearchResult('Title: a\nURL: https://b')).toBe(true);
    +		expect(looksLikeSearchResult('URL: https://b')).toBe(false);
    +		expect(looksLikeSearchResult('plain stdout')).toBe(false);
    +		expect(looksLikeSearchResult(undefined)).toBe(false);
    +	});
    +});
    diff --git a/tools/ui/tests/unit/tool-call-meta.test.ts b/tools/ui/tests/unit/tool-call-meta.test.ts
    index bb28e3830..f94d2279f 100644
    --- a/tools/ui/tests/unit/tool-call-meta.test.ts
    +++ b/tools/ui/tests/unit/tool-call-meta.test.ts
    @@ -28,3 +28,15 @@ describe('tryParseToolResultObject', () => {
     		expect(tryParseToolResultObject('{bad')).toBeNull();
     	});
     });
    +
    +describe('tryParseToolResultObject gating', () => {
    +	it('parses JSON objects that start after leading whitespace', () => {
    +		expect(tryParseToolResultObject('\n  {"result":"ok"}')).toEqual({ result: 'ok' });
    +	});
    +
    +	it('skips the parse for large plain-text results', () => {
    +		// most tool results are file contents or stdout; the gate avoids a
    +		// doomed JSON.parse over the whole blob
    +		expect(tryParseToolResultObject(`${'stdout line\n'.repeat(2000)}`)).toBeNull();
    +	});
    +});
    diff --git a/tools/ui/tests/unit/tool-calls.test.ts b/tools/ui/tests/unit/tool-calls.test.ts
    index f84a2405e..a2274f9d2 100644
    --- a/tools/ui/tests/unit/tool-calls.test.ts
    +++ b/tools/ui/tests/unit/tool-calls.test.ts
    @@ -1,5 +1,8 @@
     import { parseToolArgs } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared';
    -import { parseEditFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file';
    +import {
    +	parseEditFileMeta,
    +	parseEditFileTitleMeta
    +} from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file';
     import { parseExecShellCommandMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command';
     import { parseFileGlobSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search';
     import { parseGrepSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search';
    @@ -7,10 +10,10 @@ import { parseReadFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMes
     import { parseRunJavascriptMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript';
     import {
     	parseWriteFileMeta,
    -	type WriteFileMeta
    +	parseWriteFileTitleMeta
     } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file';
     import { AgenticSectionType, BuiltInTool } from '$lib/enums';
    -import type { AgenticSection } from '$lib/types';
    +import type { AgenticSection, WriteFileMeta } from '$lib/types';
     import { abbreviateHome, formatCwdMessage, lastPathSegment, parseCwdMessage } from '$lib/utils';
     import { describe, expect, it } from 'vitest';
     
    @@ -223,6 +226,113 @@ describe('parseWriteFileMeta', () => {
     	});
     });
     
    +describe('parseWriteFileTitleMeta', () => {
    +	it('matches the full meta for path, language and result fields', () => {
    +		const args = JSON.stringify({ content: 'x'.repeat(50_000), path: '/foo.ts' });
    +		const toolResult = '{"result":"wrote","bytes":42}';
    +		const section = makeSection(
    +			{ toolArgs: args, toolName: BuiltInTool.SERVER_WRITE_FILE, toolResult },
    +			BuiltInTool.SERVER_WRITE_FILE
    +		);
    +		const full = parseWriteFileMeta(section);
    +		const title = parseWriteFileTitleMeta(section);
    +
    +		expect(title?.filePath).toBe(full?.filePath);
    +		expect(title?.fileName).toBe(full?.fileName);
    +		expect(title?.language).toBe(full?.language);
    +		expect(title?.bytesWritten).toBe(full?.bytesWritten);
    +		expect(title?.resultMessage).toBe(full?.resultMessage);
    +		expect(title?.errorMessage).toBe(full?.errorMessage);
    +	});
    +
    +	it('extracts a path with escaped characters without parsing the content blob', () => {
    +		const section = makeSection(
    +			{
    +				toolArgs: '{"path":"/a\\nb\\"c/d.ts","content":"x"}',
    +				toolName: BuiltInTool.SERVER_WRITE_FILE
    +			},
    +			BuiltInTool.SERVER_WRITE_FILE
    +		);
    +
    +		expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/a\nb"c/d.ts');
    +	});
    +
    +	it('falls back to the full parse for args the extractor can not see', () => {
    +		const section = makeSection(
    +			{
    +				// key written with an escaped unicode escape sequence in the name
    +				toolArgs: '{"\\u0070ath":"/foo.ts","content":"x"}',
    +				toolName: BuiltInTool.SERVER_WRITE_FILE
    +			},
    +			BuiltInTool.SERVER_WRITE_FILE
    +		);
    +
    +		expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/foo.ts');
    +	});
    +
    +	it('accepts partial args like the full parser', () => {
    +		const section = makeSection(
    +			{ toolArgs: '{"path":"/foo.t', toolName: BuiltInTool.SERVER_WRITE_FILE },
    +			BuiltInTool.SERVER_WRITE_FILE
    +		);
    +
    +		expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/foo.t');
    +	});
    +
    +	it('returns null for sections with a different tool name', () => {
    +		expect(
    +			parseWriteFileTitleMeta(
    +				makeSection({
    +					toolArgs: '{"path":"/x","content":"y"}',
    +					toolName: BuiltInTool.SERVER_READ_FILE
    +				})
    +			)
    +		).toBeNull();
    +	});
    +});
    +
    +describe('parseEditFileTitleMeta', () => {
    +	it('matches the full meta for path and result fields', () => {
    +		const section = makeSection(
    +			{
    +				toolArgs: '{"path":"/foo.ts","edits":[{"old_text":"a","new_text":"b"}]}' + ' '.repeat(0),
    +				toolName: BuiltInTool.SERVER_EDIT_FILE,
    +				toolResult: '{"result":"ok","edits_applied":1}'
    +			},
    +			BuiltInTool.SERVER_EDIT_FILE
    +		);
    +		const full = parseEditFileMeta(section);
    +		const title = parseEditFileTitleMeta(section);
    +
    +		expect(title?.filePath).toBe(full?.filePath);
    +		expect(title?.fileName).toBe(full?.fileName);
    +		expect(title?.editsApplied).toBe(full?.editsApplied);
    +		expect(title?.resultMessage).toBe(full?.resultMessage);
    +		expect(title?.errorMessage).toBe(full?.errorMessage);
    +	});
    +
    +	it('surfaces errorMessage from the result blob without parsing args', () => {
    +		const section = makeSection(
    +			{
    +				toolArgs: '{"path":"/foo.ts","edits":[]}',
    +				toolName: BuiltInTool.SERVER_EDIT_FILE,
    +				toolResult: '{"error":"permission denied"}'
    +			},
    +			BuiltInTool.SERVER_EDIT_FILE
    +		);
    +
    +		expect(parseEditFileTitleMeta(section)?.errorMessage).toBe('permission denied');
    +	});
    +
    +	it('returns null when args have no path-like field', () => {
    +		expect(
    +			parseEditFileTitleMeta(
    +				makeSection({ toolArgs: '{"edits":[]}', toolName: BuiltInTool.SERVER_EDIT_FILE })
    +			)
    +		).toBeNull();
    +	});
    +});
    +
     describe('parseEditFileMeta', () => {
     	it('parses edits array and applies editsApplied from the result', () => {
     		const section = makeSection(
    
    From 9e0e220594af405a62835dc3a27495729fd8506b Mon Sep 17 00:00:00 2001
    From: Aldehir Rojas 
    Date: Sun, 6 Sep 2026 03:59:10 -0500
    Subject: [PATCH 07/31] grammar : fix max repetition threshold (#28469)
    
    ---
     src/llama-grammar.cpp | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/llama-grammar.cpp b/src/llama-grammar.cpp
    index f14215ac7..6aa03c766 100644
    --- a/src/llama-grammar.cpp
    +++ b/src/llama-grammar.cpp
    @@ -492,7 +492,7 @@ const char * llama_grammar_parser::parse_sequence(
                 total_rules = min_times;
             }
     
    -        if (n_prev_rules * total_rules >= MAX_REPETITION_THRESHOLD) {
    +        if (n_prev_rules * total_rules > MAX_REPETITION_THRESHOLD) {
                 throw std::runtime_error("number of rules that are going to be repeated multiplied by the new repetition exceeds sane defaults, please reduce the number of repetitions or rule complexity");
             }
     
    
    From 73a43d1f69345aee8bb186ef4b3172cef892f2e5 Mon Sep 17 00:00:00 2001
    From: Aman Gupta 
    Date: Sun, 6 Sep 2026 19:45:01 +0800
    Subject: [PATCH 08/31] cuda: fixes races in mmid and mmf (#28475)
    
    ---
     ggml/src/ggml-cuda/common.cuh    |  6 ++++++
     ggml/src/ggml-cuda/fattn-vec.cuh |  4 +---
     ggml/src/ggml-cuda/mmf.cuh       | 19 +++++++++++++++++++
     ggml/src/ggml-cuda/mmid.cu       |  1 +
     4 files changed, 27 insertions(+), 3 deletions(-)
    
    diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh
    index 9918c0394..ed0ea60bd 100644
    --- a/ggml/src/ggml-cuda/common.cuh
    +++ b/ggml/src/ggml-cuda/common.cuh
    @@ -121,6 +121,12 @@
     #    define GGML_CUDA_USE_PDL
     #endif  // !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA) && (CUDART_VERSION >= 12030 || (!(defined(_MSC_VER) && !defined(__clang__)) && CUDART_VERSION >= 11080))
     
    +static __device__ __forceinline__ void ggml_cuda_syncwarp() {
    +#ifndef GGML_USE_HIP
    +    __syncwarp();
    +#endif // GGML_USE_HIP
    +}
    +
     static __device__ __forceinline__ void ggml_cuda_pdl_sync() {
     #if defined(GGML_CUDA_USE_PDL) && defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= GGML_CUDA_CC_HOPPER
         cudaGridDependencySynchronize();
    diff --git a/ggml/src/ggml-cuda/fattn-vec.cuh b/ggml/src/ggml-cuda/fattn-vec.cuh
    index 519b36b9f..57a285565 100644
    --- a/ggml/src/ggml-cuda/fattn-vec.cuh
    +++ b/ggml/src/ggml-cuda/fattn-vec.cuh
    @@ -317,9 +317,7 @@ static __global__ void flash_attn_ext_vec(
     #endif // V_DOT2_F32_F16_AVAILABLE
             }
     
    -#ifndef GGML_USE_HIP
    -        __syncwarp();
    -#endif // GGML_USE_HIP
    +        ggml_cuda_syncwarp();
     
     #pragma unroll
             for (int k0 = 0; k0 < WARP_SIZE; k0 += V_cols_per_iter) {
    diff --git a/ggml/src/ggml-cuda/mmf.cuh b/ggml/src/ggml-cuda/mmf.cuh
    index d55cc1ec7..879a86527 100644
    --- a/ggml/src/ggml-cuda/mmf.cuh
    +++ b/ggml/src/ggml-cuda/mmf.cuh
    @@ -143,6 +143,7 @@ static __global__ void mul_mat_f(
                 if (threadIdx.x == 0) {
                     slot_map[j] = -1;
                 }
    +            ggml_cuda_syncwarp();
     
                 if (col_base + j >= ncols_dst_total) {
                     continue;
    @@ -171,10 +172,12 @@ static __global__ void mul_mat_f(
             tile_A A[ntA][warp_size / tile_A::J];
     #pragma unroll
             for (int itA = 0; itA < ntA; ++itA) {
    +            ggml_cuda_syncwarp();
     #pragma unroll
                 for (int i = 0; i < tile_A::I; ++i) {
                     tile_xy[i*tile_k_padded + threadIdx.x] = x[(itA*tile_A::I + i)*stride_row  + col];
                 }
    +            ggml_cuda_syncwarp();
     #pragma unroll
                 for (int k0 = 0; k0 < warp_size; k0 += tile_A::J) {
                     load_ldmatrix(A[itA][k0/tile_A::J], tile_xy + k0, tile_k_padded);
    @@ -183,6 +186,7 @@ static __global__ void mul_mat_f(
     
     #pragma unroll
             for (int itB = 0; itB < ntB; ++itB) {
    +            ggml_cuda_syncwarp();
                 if constexpr (std::is_same_v) {
     #pragma unroll
                     for (int j0 = 0; j0 < tile_B::I; ++j0) {
    @@ -212,6 +216,7 @@ static __global__ void mul_mat_f(
                 } else {
                     static_assert(std::is_same_v, "unsupported type");
                 }
    +            ggml_cuda_syncwarp();
     #pragma unroll
                 for (int k0 = 0; k0 < warp_size; k0 += tile_B::J) {
                     tile_B B;
    @@ -229,6 +234,8 @@ static __global__ void mul_mat_f(
     
         if (nwarps > 1) {
             __syncthreads();
    +    } else {
    +        ggml_cuda_syncwarp();
         }
     #pragma unroll
         for (int itB = 0; itB < ntB; ++itB) {
    @@ -245,6 +252,8 @@ static __global__ void mul_mat_f(
     
         if (nwarps > 1) {
             __syncthreads();
    +    } else {
    +        ggml_cuda_syncwarp();
         }
     
     #pragma unroll
    @@ -382,10 +391,12 @@ static __global__ void mul_mat_f_ids(
             tile_A A[ntA][warp_size / tile_A::J];
     #pragma unroll
             for (int itA = 0; itA < ntA; ++itA) {
    +            ggml_cuda_syncwarp();
     #pragma unroll
                 for (int i = 0; i < tile_A::I; ++i) {
                     tile_xy[i*tile_k_padded + threadIdx.x] = x[(itA*tile_A::I + i)*stride_row  + col];
                 }
    +            ggml_cuda_syncwarp();
     #pragma unroll
                 for (int k0 = 0; k0 < warp_size; k0 += tile_A::J) {
                     load_ldmatrix(A[itA][k0/tile_A::J], tile_xy + k0, tile_k_padded);
    @@ -419,6 +430,7 @@ static __global__ void mul_mat_f_ids(
                 int next_buf = 1;
     #pragma unroll
                 for (int itB = 0; itB < ntB; ++itB) {
    +                ggml_cuda_syncwarp();
     #pragma unroll
                     for (int j0 = 0; j0 < tile_B::I; ++j0) {
                         tile_xy[j0*tile_k_padded + threadIdx.x] = vals_buf[curr_buf][j0];
    @@ -428,6 +440,7 @@ static __global__ void mul_mat_f_ids(
                         gather_tile(itB + 1, vals_buf[next_buf]);
                     }
     
    +                ggml_cuda_syncwarp();
     #pragma unroll
                     for (int k0 = 0; k0 < warp_size; k0 += tile_B::J) {
                         tile_B B;
    @@ -472,6 +485,7 @@ static __global__ void mul_mat_f_ids(
                 int next_buf = 1;
     #pragma unroll
                 for (int itB = 0; itB < ntB; ++itB) {
    +                ggml_cuda_syncwarp();
     #pragma unroll
                     for (int j0 = 0; j0 < tile_B::I; ++j0) {
                         const float2 tmp = vals_buf[curr_buf][j0];
    @@ -482,6 +496,7 @@ static __global__ void mul_mat_f_ids(
                         gather_tile(itB + 1, vals_buf[next_buf]);
                     }
     
    +                ggml_cuda_syncwarp();
     #pragma unroll
                     for (int k0 = 0; k0 < warp_size; k0 += tile_B::J) {
                         tile_B B;
    @@ -507,6 +522,8 @@ static __global__ void mul_mat_f_ids(
     
         if (nwarps > 1) {
             __syncthreads();
    +    } else {
    +        ggml_cuda_syncwarp();
         }
     #pragma unroll
         for (int itB = 0; itB < ntB; ++itB) {
    @@ -523,6 +540,8 @@ static __global__ void mul_mat_f_ids(
     
         if (nwarps > 1) {
             __syncthreads();
    +    } else {
    +        ggml_cuda_syncwarp();
         }
     
     #pragma unroll
    diff --git a/ggml/src/ggml-cuda/mmid.cu b/ggml/src/ggml-cuda/mmid.cu
    index ed0851dcf..0b222e63a 100644
    --- a/ggml/src/ggml-cuda/mmid.cu
    +++ b/ggml/src/ggml-cuda/mmid.cu
    @@ -101,6 +101,7 @@ static __global__ void mm_ids_helper(
             }
         }
         nex_prev = warp_reduce_sum(nex_prev);
    +    ggml_cuda_syncwarp();
     
         for (int itc = threadIdx.x; itc < it_compact; itc += warp_size) {
             const mm_ids_helper_store store_it = store[itc];
    
    From d03efa5d5369a9cba63bec1ae712a2f6e686a1d6 Mon Sep 17 00:00:00 2001
    From: lhez 
    Date: Sun, 6 Sep 2026 08:33:08 -0700
    Subject: [PATCH 09/31] opencl: properly choose weights pack for q4_K, q5_K
     mul_mat (#28402)
    
    ---
     ggml/src/ggml-opencl/ggml-opencl.cpp | 8 +++++++-
     1 file changed, 7 insertions(+), 1 deletion(-)
    
    diff --git a/ggml/src/ggml-opencl/ggml-opencl.cpp b/ggml/src/ggml-opencl/ggml-opencl.cpp
    index ad4a995ab..d737aea12 100644
    --- a/ggml/src/ggml-opencl/ggml-opencl.cpp
    +++ b/ggml/src/ggml-opencl/ggml-opencl.cpp
    @@ -8304,8 +8304,10 @@ inline bool enable_adreno_trans_weight_q5_K(const ggml_backend_opencl_context *b
         const size_t elem_num = ggml_nelements(tensor);
         const size_t q_img_width = elem_num / 8;
         const size_t qh_img_width = elem_num / 16;
    +    const bool shape_ok = tensor->ne[0] % 32 == 0 && tensor->ne[1] % 4 == 0 &&
    +                          tensor->ne[2] == 1 && tensor->ne[3] == 1;
     
    -    return q_img_width <= backend_ctx->image_max_buffer_size &&
    +    return shape_ok && q_img_width <= backend_ctx->image_max_buffer_size &&
                qh_img_width <= backend_ctx->image_max_buffer_size;
     }
     
    @@ -8328,6 +8330,10 @@ static inline bool flat_large_m_enabled() {
     }
     
     static inline bool use_flat_gemv_for_large_m_q4_K(const ggml_backend_opencl_context *backend_ctx, const ggml_tensor *tensor) {
    +    if (tensor->ne[1] % 4 != 0 && tensor->ne[2] == 1 && tensor->ne[3] == 1) {
    +        return true;
    +    }
    +
         if (!flat_large_m_enabled()) {
             return false;
         }
    
    From 3ad1ba7336986d98592d3e28cafd1a406715351f Mon Sep 17 00:00:00 2001
    From: KnightYao 
    Date: Sun, 6 Sep 2026 23:43:58 +0800
    Subject: [PATCH 10/31] [Model] Support for Spark2_5ForCausalLM  implementation
     (#27868)
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    * Add Spark3 Model
    * rename spark3 -> spark2_5
    
    Co-authored-by: Sigbjørn Skjæret 
    Co-authored-by: dongjiang 
    ---
     conversion/__init__.py          |   1 +
     conversion/base.py              |   3 +
     conversion/spark2_5.py          |  65 ++++++++++++++
     convert_hf_to_gguf_update.py    |   1 +
     docs/autoparser.md              |   1 +
     gguf-py/gguf/constants.py       |  15 ++++
     models/templates/README.md      |   2 +
     models/templates/Spark2.5.jinja | 110 ++++++++++++++++++++++++
     src/llama-arch.cpp              |   1 +
     src/llama-arch.h                |   1 +
     src/llama-model-saver.cpp       |   1 +
     src/llama-model.cpp             |   3 +
     src/llama-vocab.cpp             |  12 +++
     src/llama-vocab.h               |   1 +
     src/models/models.h             |  13 +++
     src/models/spark2-5.cpp         | 146 ++++++++++++++++++++++++++++++++
     tests/test-chat.cpp             |  94 ++++++++++++++++++++
     tests/test-llama-archs.cpp      |   2 +-
     18 files changed, 471 insertions(+), 1 deletion(-)
     create mode 100644 conversion/spark2_5.py
     create mode 100644 models/templates/Spark2.5.jinja
     create mode 100644 src/models/spark2-5.cpp
    
    diff --git a/conversion/__init__.py b/conversion/__init__.py
    index 94d6a49fb..4d58bcd10 100644
    --- a/conversion/__init__.py
    +++ b/conversion/__init__.py
    @@ -255,6 +255,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
         "SeedOssForCausalLM": "olmo",
         "SmallThinkerForCausalLM": "smallthinker",
         "SmolLM3ForCausalLM": "llama",
    +    "Spark2_5ForCausalLM": "spark2_5",
         "SolarOpenForCausalLM": "glm",
         "StableLMEpochForCausalLM": "stablelm",
         "StableLmForCausalLM": "stablelm",
    diff --git a/conversion/base.py b/conversion/base.py
    index c1ecf1c65..dc1083ead 100644
    --- a/conversion/base.py
    +++ b/conversion/base.py
    @@ -1543,6 +1543,9 @@ class TextModel(ModelBase):
             if chkhsh == "9e454714343b69b99b71795c1d27a68c2a1d15dab111f4d353109f966af29da7":
                 # ref: https://huggingface.co/LiquidAI/LFM2.5-8B-A1B
                 res = "lfm2"
    +        if chkhsh == "0a766d034107bc736a3f2dc4968fd62e54a3570f1454443e0c5a4cc6bd7941ed":
    +            # ref: https://huggingface.co/XHToken/Spark-X2.5-1.7B
    +            res = "spark2_5"
             if chkhsh == "0ef9807a4087ebef797fc749390439009c3b9eda9ad1a097abbe738f486c01e5":
                 # ref: https://huggingface.co/meta-llama/Meta-Llama-3-8B
                 res = "llama-bpe"
    diff --git a/conversion/spark2_5.py b/conversion/spark2_5.py
    new file mode 100644
    index 000000000..44a0bd262
    --- /dev/null
    +++ b/conversion/spark2_5.py
    @@ -0,0 +1,65 @@
    +from __future__ import annotations
    +
    +from collections.abc import Iterable
    +from typing import TYPE_CHECKING
    +
    +if TYPE_CHECKING:
    +    from torch import Tensor
    +
    +from .base import ModelBase, TextModel, gguf
    +
    +
    +@ModelBase.register("Spark2_5ForCausalLM")
    +@ModelBase.example("XHToken/Spark-X2.5-1.7B")
    +class Spark2_5Model(TextModel):
    +    model_arch = gguf.MODEL_ARCH.SPARK2_5
    +
    +    def set_gguf_parameters(self) -> None:
    +        super().set_gguf_parameters()
    +
    +        hparams = self.hparams
    +        layer_types = hparams["layer_types"]
    +        if len(layer_types) != self.block_count:
    +            raise ValueError(
    +                f"Spark2_5 layer_types length {len(layer_types)} != num_hidden_layers {self.block_count}"
    +            )
    +        if any(layer_type not in ("sliding_attention", "full_attention") for layer_type in layer_types):
    +            raise ValueError(f"Spark2_5 has unsupported layer_types: {layer_types}")
    +        if hparams.get("gate_attn_act_mode") != "sigmoid" or hparams.get("headwise_attn_output_gate") is not True:
    +            raise ValueError("Spark2_5 conversion requires head-wise sigmoid attention gates")
    +        if hparams.get("hidden_act") != "gelu":
    +            raise ValueError(f"Spark2_5 conversion requires GELU, got {hparams.get('hidden_act')!r}")
    +
    +        self.gguf_writer.add_vocab_size(hparams["vocab_size"])
    +        self.gguf_writer.add_sliding_window(hparams["sliding_window"])
    +        self.gguf_writer.add_sliding_window_pattern(
    +            [layer_type == "sliding_attention" for layer_type in layer_types]
    +        )
    +
    +        head_dim = hparams["head_dim"]
    +        full_rope = self.rope_parameters["full_attention"]
    +        swa_rope = self.rope_parameters["sliding_attention"]
    +        self.gguf_writer.add_rope_dimension_count(
    +            int(head_dim * float(full_rope["partial_rotary_factor"]))
    +        )
    +        self.gguf_writer.add_rope_dimension_count_swa(
    +            int(head_dim * float(swa_rope["partial_rotary_factor"]))
    +        )
    +
    +    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
    +        if name.endswith(".self_attn.q_k_v_proj.weight"):
    +            if bid is None:
    +                raise ValueError(f"Spark2_5 fused QKV tensor has no block id: {name}")
    +            yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_QKV, bid), data_torch
    +            return
    +
    +        if name.endswith(".self_attn.g_proj.weight"):
    +            if bid is None:
    +                raise ValueError(f"Spark2_5 attention gate tensor has no block id: {name}")
    +            expected = self.hparams["num_attention_heads"]
    +            if data_torch.shape[0] != expected:
    +                raise ValueError(
    +                    f"Spark2_5 layer {bid} attention gate width {data_torch.shape[0]} != head count {expected}"
    +                )
    +
    +        yield from super().modify_tensors(data_torch, name, bid)
    diff --git a/convert_hf_to_gguf_update.py b/convert_hf_to_gguf_update.py
    index c4141afa6..6af74cd87 100755
    --- a/convert_hf_to_gguf_update.py
    +++ b/convert_hf_to_gguf_update.py
    @@ -191,6 +191,7 @@ pre_computed_hashes = [
         {"name": "gpt-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/evilfreelancer/ruGPT3XL", "chkhsh": "0fe1cf6eda062318a1af7270f3331a85c539a01778ff948e24388e949c5282f4"},
         # lfm2 variants
         {"name": "lfm2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LiquidAI/LFM2.5-8B-A1B", "chkhsh": "9e454714343b69b99b71795c1d27a68c2a1d15dab111f4d353109f966af29da7"},
    +    {"name": "spark2_5", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/XHToken/Spark-X2.5-1.7B", "chkhsh": "0a766d034107bc736a3f2dc4968fd62e54a3570f1454443e0c5a4cc6bd7941ed"},
     ]
     
     
    diff --git a/docs/autoparser.md b/docs/autoparser.md
    index b5e32621d..2a7ea00b4 100644
    --- a/docs/autoparser.md
    +++ b/docs/autoparser.md
    @@ -514,6 +514,7 @@ The following templates have active tests in `tests/test-chat.cpp`:
     | Mistral Small 3.2 | JSON_NATIVE | `[TOOL_CALLS]func[ARGS]{...}` with call ID |
     | Devstral | JSON_NATIVE | `[TOOL_CALLS]func[ARGS]{...}` without call ID |
     | StepFun 3.5 Flash | TAG_WITH_TAGGED | `` format |
    +| Spark2.5 | TAG_WITH_TAGGED | `name......` format |
     
     ## Adding Support for New Templates
     
    diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py
    index 399d31f1d..486f3586d 100644
    --- a/gguf-py/gguf/constants.py
    +++ b/gguf-py/gguf/constants.py
    @@ -619,6 +619,7 @@ class MODEL_ARCH(IntEnum):
         PADDLEOCR        = auto()
         MIMO2            = auto()
         STEP35           = auto()
    +    SPARK2_5           = auto()
         LLAMA_EMBED      = auto()
         MAINCODER        = auto()
         KIMI_LINEAR      = auto()
    @@ -1373,6 +1374,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
         MODEL_ARCH.PADDLEOCR:        "paddleocr",
         MODEL_ARCH.MIMO2:            "mimo2",
         MODEL_ARCH.STEP35:           "step35",
    +    MODEL_ARCH.SPARK2_5:         "spark2_5",
         MODEL_ARCH.LLAMA_EMBED:      "llama-embed",
         MODEL_ARCH.MAINCODER:        "maincoder",
         MODEL_ARCH.KIMI_LINEAR:      "kimi-linear",
    @@ -5231,6 +5233,19 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD,
             MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM,
         ],
    +    MODEL_ARCH.SPARK2_5: [
    +        MODEL_TENSOR.TOKEN_EMBD,
    +        MODEL_TENSOR.OUTPUT_NORM,
    +        MODEL_TENSOR.OUTPUT,
    +        MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
    +        MODEL_TENSOR.ATTN_GATE,
    +        MODEL_TENSOR.ATTN_OUT,
    +        MODEL_TENSOR.FFN_NORM,
    +        MODEL_TENSOR.FFN_GATE,
    +        MODEL_TENSOR.FFN_DOWN,
    +        MODEL_TENSOR.FFN_UP,
    +    ],
         MODEL_ARCH.LLAMA_EMBED: [
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT_NORM,
    diff --git a/models/templates/README.md b/models/templates/README.md
    index 3a649b8f4..022a5e278 100644
    --- a/models/templates/README.md
    +++ b/models/templates/README.md
    @@ -23,4 +23,6 @@ These templates can be updated with the following commands:
     ./scripts/get_chat_template.py Qwen/Qwen3-0.6B                               > models/templates/Qwen-Qwen3-0.6B.jinja
     ./scripts/get_chat_template.py zai-org/GLM-4.5                               > models/templates/zai-org-GLM-4.5.jinja
     ./scripts/get_chat_template.py deepseek-ai/DeepSeek-V3.1                     > models/templates/deepseek-ai-DeepSeek-V3.1.jinja
    +./scripts/get_chat_template.py XHToken/Spark-X2.5-1.7B                       > models/templates/Spark2.5.jinja
    +./scripts/get_chat_template.py XHToken/Spark-X2.5-4B                         > models/templates/Spark2.5.jinja
     ```
    diff --git a/models/templates/Spark2.5.jinja b/models/templates/Spark2.5.jinja
    new file mode 100644
    index 000000000..54aa34ff2
    --- /dev/null
    +++ b/models/templates/Spark2.5.jinja
    @@ -0,0 +1,110 @@
    +{%- if not messages %}
    +    {{- raise_exception('No messages provided.') }}
    +{%- endif %}
    +
    +{%- set enable_thinking = enable_thinking | default(true) %}
    +
    +{#- Render a string or a list of text blocks. -#}
    +{%- macro render_content(content, context_name) %}
    +    {%- if content is string %}
    +        {{- content }}
    +    {%- elif content is none or content is undefined %}
    +        {{- '' }}
    +    {%- elif content is iterable and content is not mapping %}
    +        {%- for block in content %}
    +            {%- if block.type == 'text' %}
    +                {{- block.text }}
    +            {%- else %}
    +                {{- raise_exception('Unsupported ' ~ context_name ~ ' content block type: ' ~ (block.type | string)) }}
    +            {%- endif %}
    +        {%- endfor %}
    +    {%- else %}
    +        {{- raise_exception(context_name ~ ' content must be a string or a list of text blocks') }}
    +    {%- endif %}
    +{%- endmacro %}
    +
    +{#- Default system prompt. -#}
    +{%- set default_system = 'you are a helpful assistant.' %}
    +
    +{#- The first message-level system is placed in the initial system block. -#}
    +{%- set ns = namespace(initial_system='') %}
    +{%- if messages[0].role == 'system' %}
    +    {%- set ns.initial_system = render_content(messages[0].content, 'system') %}
    +{%- endif %}
    +
    +{#- System block. -#}
    +{{- '<|start▁of▁sentence|><|System|>' + '\n' + default_system }}
    +{%- if tools %}
    +    {{- '## Tools' + '\n' + 'You have access to the following functions:' + '\n' + '' }}
    +    {%- for tool in tools %}
    +        {{- '\n' + tool.function | tojson }}
    +    {%- endfor %}
    +    {{- '\n' + '' }}
    +{%- endif %}
    +{%- if ns.initial_system %}
    +    {{- '\n\n' + ns.initial_system }}
    +{%- endif %}
    +{{- '<|end▁of▁sentence|>' }}
    +
    +{#- Conversation turns. -#}
    +{%- for message in messages %}
    +    {%- if message.role == 'system' %}
    +        {#- The first system message was consumed by the initial block. -#}
    +        {%- if not loop.first %}
    +            {{- '<|start▁of▁sentence|><|System|>\n' + render_content(message.content, 'system') + '<|end▁of▁sentence|>' }}
    +        {%- endif %}
    +    {%- elif message.role == 'user' %}
    +        {{- '<|start▁of▁sentence|><|User|>' + render_content(message.content, 'user') + '<|end▁of▁sentence|>' }}
    +    {%- elif message.role == 'assistant' %}
    +        {%- set assistant_content = render_content(message.content, 'assistant') %}
    +        {%- if message.reasoning_content is defined and message.reasoning_content %}
    +            {%- set reasoning_content = message.reasoning_content %}
    +        {%- else %}
    +            {%- set reasoning_content = '' %}
    +        {%- endif %}
    +        {{- '<|start▁of▁sentence|><|Bot|>' }}
    +        {%- if reasoning_content %}
    +            {{- '' + reasoning_content + '' }}
    +        {%- else %}
    +            {{- '' }}
    +        {%- endif %}
    +        {%- if assistant_content %}
    +            {{- assistant_content }}
    +        {%- endif %}
    +        {%- if message.tool_calls is defined and message.tool_calls is not none %}
    +            {%- for tool_call in message.tool_calls %}
    +                {%- if tool_call.function.arguments is not mapping %}
    +                    {{- raise_exception('tool_call.function.arguments must be a dictionary; normalize JSON strings before apply_chat_template') }}
    +                {%- endif %}
    +                {%- set args = tool_call.function.arguments %}
    +                {{- '' + tool_call.function.name }}
    +                {%- for k, v in args.items() %}
    +                    {{- '' ~ k ~ '' ~ (v if v is string else v | tojson) ~ '' }}
    +                {%- endfor %}
    +                {{- '' }}
    +            {%- endfor %}
    +        {%- endif %}
    +        {{- '<|end▁of▁sentence|>' }}
    +    {%- elif message.role == 'tool' %}
    +        {%- if loop.previtem is undefined or loop.previtem.role != 'tool' %}
    +            {{- '<|start▁of▁sentence|><|Tool|>' }}
    +        {%- endif %}
    +        {{- '' ~ message.content ~ '' }}
    +        {%- if loop.nextitem is undefined or loop.nextitem.role != 'tool' %}
    +            {{- '<|end▁of▁sentence|>' }}
    +        {%- endif %}
    +    {%- else %}
    +        {{- raise_exception('Unsupported message role: ' ~ message.role) }}
    +    {%- endif %}
    +{%- endfor %}
    +
    +{#- Generation prompt. -#}
    +{%- if add_generation_prompt %}
    +    {{- '<|start▁of▁sentence|><|Bot|>' }}
    +    {%- if enable_thinking is defined and enable_thinking %}
    +        {{- '' }}
    +    {%- endif %}
    +    {%- if enable_thinking is defined and not enable_thinking %}
    +        {{- '' }}
    +    {%- endif %}
    +{%- endif %}
    diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp
    index d06be641a..15f651919 100644
    --- a/src/llama-arch.cpp
    +++ b/src/llama-arch.cpp
    @@ -146,6 +146,7 @@ static const std::map LLM_ARCH_NAMES = {
         { LLM_ARCH_PADDLEOCR,        "paddleocr"        },
         { LLM_ARCH_MIMO2,            "mimo2"            },
         { LLM_ARCH_STEP35,           "step35"           },
    +    { LLM_ARCH_SPARK2_5,         "spark2_5"         },
         { LLM_ARCH_LLAMA_EMBED,      "llama-embed"      },
         { LLM_ARCH_MAINCODER,        "maincoder"        },
         { LLM_ARCH_KIMI_LINEAR,      "kimi-linear"      },
    diff --git a/src/llama-arch.h b/src/llama-arch.h
    index 62dfa5d81..f1d173a57 100644
    --- a/src/llama-arch.h
    +++ b/src/llama-arch.h
    @@ -147,6 +147,7 @@ enum llm_arch {
         LLM_ARCH_PADDLEOCR,
         LLM_ARCH_MIMO2,
         LLM_ARCH_STEP35,
    +    LLM_ARCH_SPARK2_5,
         LLM_ARCH_LLAMA_EMBED,
         LLM_ARCH_MAINCODER,
         LLM_ARCH_KIMI_LINEAR,
    diff --git a/src/llama-model-saver.cpp b/src/llama-model-saver.cpp
    index df2a46d93..66f8bdec3 100644
    --- a/src/llama-model-saver.cpp
    +++ b/src/llama-model-saver.cpp
    @@ -27,6 +27,7 @@ bool llama_model_saver_supports_arch(llm_arch arch) {
             case LLM_ARCH_APERTUS:
             case LLM_ARCH_MIMO2:
             case LLM_ARCH_STEP35:
    +        case LLM_ARCH_SPARK2_5:
             case LLM_ARCH_MUSE_GLIMMER:
             case LLM_ARCH_MELLUM:
             case LLM_ARCH_LAGUNA:
    diff --git a/src/llama-model.cpp b/src/llama-model.cpp
    index b837e2765..0e0036781 100644
    --- a/src/llama-model.cpp
    +++ b/src/llama-model.cpp
    @@ -338,6 +338,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
                 return new llama_model_kimi_k3(params);
             case LLM_ARCH_STEP35:
                 return new llama_model_step35(params);
    +        case LLM_ARCH_SPARK2_5:
    +            return new llama_model_spark2_5(params);
             default:
                 throw std::runtime_error(std::string("unsupported model architecture: '") + llm_arch_name(arch) + "'");
         }
    @@ -2999,6 +3001,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
             case LLM_ARCH_QWEN3NEXT:
             case LLM_ARCH_MIMO2:
             case LLM_ARCH_STEP35:
    +        case LLM_ARCH_SPARK2_5:
             case LLM_ARCH_TALKIE:
             case LLM_ARCH_MELLUM:
                 return LLAMA_ROPE_TYPE_NEOX;
    diff --git a/src/llama-vocab.cpp b/src/llama-vocab.cpp
    index c0c34cdd8..a69801f08 100644
    --- a/src/llama-vocab.cpp
    +++ b/src/llama-vocab.cpp
    @@ -325,6 +325,14 @@ struct llm_tokenizer_bpe : llm_tokenizer {
                         "[!\"#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~][A-Za-z]+|[^\r\n\\p{L}\\p{P}\\p{S}]?[\\p{L}\\p{M}]+| ?[\\p{P}\\p{S}]+[\r\n]*|\\s*[\r\n]+|\\s+(?!\\S)|\\s+",
                     };
                     break;
    +            case LLAMA_VOCAB_PRE_TYPE_SPARK2_5:
    +                regex_exprs = {
    +                    "\\p{N}{1,3}",
    +                    "[一-龥぀-ゟ゠-ヿ]+",
    +                    "[!\"#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~][A-Za-z]+|[^\r\n\\p{L}\\p{P}\\p{S}]?[\\p{L}\\p{M}]+| ?[\\p{P}\\p{S}]+|[\r\n]|\\s+(?!\\S)|\\s+",
    +                    "\\p{N}",
    +                };
    +                break;
                 case LLAMA_VOCAB_PRE_TYPE_YOUTU:
                     regex_exprs = {
                         "[가-힣ㄱ-ㆎ]+|[!…“”‘’—:;,、-〿︰-﹏]+|[ㄅ-ㄯ]+|[一-龥぀-ゟ゠-ヿ]+",
    @@ -2170,6 +2178,10 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
                         tokenizer_pre == "deepseek-v3") {
                     pre_type = LLAMA_VOCAB_PRE_TYPE_DEEPSEEK3_LLM;
                     clean_spaces = false;
    +            } else if (
    +                    tokenizer_pre == "spark2_5") {
    +                pre_type = LLAMA_VOCAB_PRE_TYPE_SPARK2_5;
    +                clean_spaces = false;
                 } else if (
                         tokenizer_pre == "youtu") {
                     pre_type = LLAMA_VOCAB_PRE_TYPE_YOUTU;
    diff --git a/src/llama-vocab.h b/src/llama-vocab.h
    index e02ea78ff..65293c026 100644
    --- a/src/llama-vocab.h
    +++ b/src/llama-vocab.h
    @@ -66,6 +66,7 @@ enum llama_vocab_pre_type {
         LLAMA_VOCAB_PRE_TYPE_MELLUM2           = 55,
         LLAMA_VOCAB_PRE_TYPE_LAGUNA            = 56,
         LLAMA_VOCAB_PRE_TYPE_HY_V4             = 57,
    +    LLAMA_VOCAB_PRE_TYPE_SPARK2_5          = 58,
     };
     
     struct LLM_KV;
    diff --git a/src/models/models.h b/src/models/models.h
    index 93a6b3494..50e9a235c 100644
    --- a/src/models/models.h
    +++ b/src/models/models.h
    @@ -2606,3 +2606,16 @@ struct llama_model_step35 : public llama_model_base {
     
         std::unique_ptr build_arch_graph(const llm_graph_params & params) const override;
     };
    +
    +
    +struct llama_model_spark2_5 : public llama_model_base {
    +    llama_model_spark2_5(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 build_arch_graph(const llm_graph_params & params) const override;
    +};
    diff --git a/src/models/spark2-5.cpp b/src/models/spark2-5.cpp
    new file mode 100644
    index 000000000..107448777
    --- /dev/null
    +++ b/src/models/spark2-5.cpp
    @@ -0,0 +1,146 @@
    +#include "models.h"
    +
    +void llama_model_spark2_5::load_arch_hparams(llama_model_loader & ml) {
    +    ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
    +    ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa);
    +
    +    hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;
    +    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);
    +
    +    switch (hparams.n_layer()) {
    +        case 28: type = LLM_TYPE_1_7B; break;
    +        default: type = LLM_TYPE_UNKNOWN;
    +    }
    +}
    +
    +void llama_model_spark2_5::load_arch_tensors(llama_model_loader &) {
    +    LLAMA_LOAD_LOCALS;
    +
    +    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}, TENSOR_NOT_REQUIRED);
    +    if (output == nullptr) {
    +        output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
    +    }
    +
    +    for (int i = 0; i < n_layer; ++i) {
    +        auto & layer = layers[i];
    +
    +        const int64_t n_head_i = hparams.n_head(i);
    +        const int64_t n_head_kv_i = hparams.n_head_kv(i);
    +        const int64_t n_embd_q = hparams.n_embd_head_k(i) * n_head_i;
    +        const int64_t n_embd_k = hparams.n_embd_head_k(i) * n_head_kv_i;
    +        const int64_t n_embd_v = hparams.n_embd_head_v(i) * n_head_kv_i;
    +
    +        layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
    +        create_tensor_qkv(layer, i, n_embd, n_embd_q, n_embd_k, n_embd_v, 0);
    +        layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", i), {n_embd, n_head_i}, 0);
    +        layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_q, n_embd}, 0);
    +
    +        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_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
    +        layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), {n_ff, n_embd}, 0);
    +    }
    +}
    +
    +std::unique_ptr llama_model_spark2_5::build_arch_graph(const llm_graph_params & params) const {
    +    return std::make_unique(*this, params);
    +}
    +
    +llama_model_spark2_5::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_v();
    +
    +    GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
    +    GGML_ASSERT(hparams.swa_type == LLAMA_SWA_TYPE_STANDARD);
    +
    +    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();
    +
    +    const float kq_scale = 1.0f / sqrtf(float(n_embd_head));
    +
    +    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);
    +
    +        const int64_t n_head_i = hparams.n_head(il);
    +        const int64_t n_head_kv_i = hparams.n_head_kv(il);
    +        const int64_t n_rot_i = hparams.n_rot(il);
    +        const float freq_base_i = model.get_rope_freq_base(cparams, il);
    +        const float freq_scale_i = model.get_rope_freq_scale(cparams, il);
    +
    +        ggml_tensor * attn_inp = cur;
    +        auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur, n_embd_head, n_head_i, n_head_kv_i, il);
    +
    +        Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr,
    +                n_rot_i, rope_type, n_ctx_orig, freq_base_i, freq_scale_i,
    +                ext_factor, attn_factor, beta_fast, beta_slow);
    +        Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr,
    +                n_rot_i, rope_type, n_ctx_orig, freq_base_i, freq_scale_i,
    +                ext_factor, attn_factor, beta_fast, beta_slow);
    +        cb(Qcur, "Qcur_rope", il);
    +        cb(Kcur, "Kcur_rope", il);
    +
    +        cur = build_attn(inp_attn,
    +                nullptr, nullptr, nullptr,
    +                Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);
    +        cb(cur, "attn_out", il);
    +
    +        ggml_tensor * gate = build_lora_mm(model.layers[il].wqkv_gate, attn_inp);
    +        gate = ggml_sigmoid(ctx0, gate);
    +        cb(gate, "attn_gate", il);
    +
    +        const int64_t n_tokens_i = cur->ne[1];
    +        cur = ggml_reshape_3d(ctx0, cur, n_embd_head, n_head_i, n_tokens_i);
    +        gate = ggml_reshape_3d(ctx0, gate, 1, n_head_i, n_tokens_i);
    +        cur = ggml_mul(ctx0, cur, gate);
    +        cur = ggml_reshape_2d(ctx0, cur, n_embd_head * n_head_i, n_tokens_i);
    +        cb(cur, "attn_gated", il);
    +
    +        cur = build_lora_mm(model.layers[il].wo, cur, model.layers[il].wo_s);
    +        cb(cur, "attn_out_proj", 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_ffn(cur,
    +                model.layers[il].ffn_up, nullptr, nullptr,
    +                model.layers[il].ffn_gate, nullptr, nullptr,
    +                model.layers[il].ffn_down, nullptr, nullptr,
    +                nullptr,
    +                LLM_FFN_GELU, LLM_FFN_PAR, il);
    +        cb(cur, "ffn_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);
    +    cb(cur, "result_output", -1);
    +    res->t_logits = cur;
    +
    +    ggml_build_forward_expand(gf, cur);
    +}
    diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp
    index 7918f0ffc..f27c91e4d 100644
    --- a/tests/test-chat.cpp
    +++ b/tests/test-chat.cpp
    @@ -4405,6 +4405,100 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
                 .run();
         }
     
    +    // Spark2.5 uses tagged arguments with forced-open thinking.
    +    {
    +        auto tst = peg_tester("models/templates/Spark2.5.jinja", detailed_debug);
    +
    +        tst.test("Hello, world!\nWhat's up?")
    +            .enable_thinking(false)
    +            .expect(message_assist)
    +            .expect_reconstruction()
    +            .run();
    +
    +        tst.test("I'm\nthinkingHello, world!\nWhat's up?")
    +            .enable_thinking(true)
    +            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
    +            .expect(message_assist_thoughts)
    +            .expect_reconstruction()
    +            .run();
    +
    +        tst.test(
    +               "special_function"
    +               "arg11"
    +               "")
    +            .enable_thinking(false)
    +            .tools({ special_function_tool })
    +            .expect(message_assist_call)
    +            .expect_reconstruction()
    +            .run();
    +
    +        tst.test(
    +               "I'm\nthinking"
    +               "special_function"
    +               "arg11"
    +               "")
    +            .enable_thinking(true)
    +            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
    +            .tools({ special_function_tool })
    +            .expect(message_assist_call_thoughts)
    +            .expect_reconstruction()
    +            .run();
    +
    +        tst.test(
    +               "special_function"
    +               "arg11"
    +               ""
    +               "special_function_with_opt"
    +               "arg11"
    +               "arg22"
    +               "")
    +            .enable_thinking(false)
    +            .parallel_tool_calls(true)
    +            .tools({ special_function_tool, special_function_tool_with_optional_param })
    +            .expect_tool_calls({
    +                { "special_function", R"({"arg1": 1})", {} },
    +                { "special_function_with_opt", R"({"arg1": 1, "arg2": 2})", {} },
    +            })
    +            .expect_reconstruction()
    +            .run();
    +
    +        tst.test(
    +               "Preparing updates."
    +               "magic_int"
    +               "ref42"
    +               "name上海"
    +               ""
    +               "amount"
    +               "orig2.5"
    +               ""
    +               "toggle"
    +               "enabledtrue"
    +               ""
    +               "set_config"
    +               "config{\"source\": \"spark\", \"options\": {\"strict\": true}}"
    +               ""
    +               "nested_args"
    +               "tags[\"alpha\", \"测试\"]"
    +               "entries[{\"id\": 1, \"label\": \"first\"}, {\"id\": 2, \"label\": \"第二\"}]"
    +               ""
    +               "empty_args"
    +               "")
    +            .enable_thinking(false)
    +            .parallel_tool_calls(true)
    +            .tools({ magic_int_tool, amount_tool, toggle_tool, config_tool, nested_args_tool, empty_args_tool })
    +            .expect_content("Preparing updates.")
    +            .expect_tool_calls({
    +                { "magic_int", R"({"ref": 42, "name": "上海"})", {} },
    +                { "amount", R"({"orig": 2.5})", {} },
    +                { "toggle", R"({"enabled": true})", {} },
    +                { "set_config", R"({"config": {"source": "spark", "options": {"strict": true}}})", {} },
    +                { "nested_args", R"({"tags": ["alpha", "测试"], "entries": [{"id": 1, "label": "first"}, {"id": 2, "label": "第二"}]})", {} },
    +                { "empty_args", "{}", {} },
    +            })
    +            .expect_reconstruction()
    +            .run();
    +    }
    +
         // Verify the throw path produces a readable error message, not std::out_of_range.
         // #20424 introduced effective_input = generation_prompt + input, but the throw
         // uses input.substr(result.end) where result.end is in effective_input space.
    diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp
    index 0f3d1c79a..dbed9846f 100644
    --- a/tests/test-llama-archs.cpp
    +++ b/tests/test-llama-archs.cpp
    @@ -237,7 +237,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
             ms.add_kv(LLM_KV_ROPE_FREQ_BASE_SWA,              10000.0f);
             // 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 ||
    +    } 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) {
             std::vector pattern;
             pattern.reserve(n_layer);
    
    From 5fdfa6282936576d2f352d4b97f397a109f207a6 Mon Sep 17 00:00:00 2001
    From: Daniel Han 
    Date: Sun, 6 Sep 2026 09:46:21 -0700
    Subject: [PATCH 11/31] models : fix GDN normalization from `max` to `rsqrt`
     (#28068)
    
    * models: use flash-linear-attention's l2norm for gated delta net q/k
    
    The GDN q/k normalization is defined by flash-linear-attention as
    
        l2norm(x) = x * rsqrt(sum(x*x) + eps)
    
    with eps inside the root. Every GDN call site in the tree uses ggml_l2_norm
    instead, which is x / max(sqrt(sum(x*x)), eps), i.e.
    torch.nn.functional.normalize - its CUDA kernel cites that page.
    
    The clamp never engages at these magnitudes, so in practice llama.cpp
    normalizes with no epsilon at all where the reference has one inside the
    root.
    
    transformers made the same substitution when it first added Qwen3-Next and
    corrected it three days later in huggingface/transformers#40842, 'Fix the
    misalignment between the l2norm in GDN of Qwen3-Next and the implementation
    in the FLA library'. vLLM and SGLang vendor FLA rather than reimplementing
    it, so neither ever had the clamp.
    
    eps keeps coming from the checkpoint, exactly as every call site already
    passed it. The references hardcode 1e-6 for this norm; that is a separate
    question and the two agree on every GDN checkpoint in the wild.
    
    ggml_l2_norm itself is correct and unchanged, as is rwkv7-base, its original
    caller, which passes normalize's own default eps of 1e-12.
    
    No new ggml op: rms_norm already carries eps inside the root, so
    rms_norm(x, eps/n) * (1/sqrt(n)) is exactly x * rsqrt(sum(x*x) + eps).
    
    * Update src/models/models.h
    
    Co-authored-by: Georgi Gerganov 
    
    ---------
    
    Co-authored-by: Georgi Gerganov 
    ---
     src/models/bailingmoe3.cpp | 4 ++--
     src/models/kimi-k3.cpp     | 6 +++---
     src/models/kimi-linear.cpp | 5 +++--
     src/models/models.h        | 7 +++++++
     src/models/qwen35.cpp      | 5 +++--
     src/models/qwen35moe.cpp   | 5 +++--
     src/models/qwen3next.cpp   | 5 +++--
     src/models/qwen4exp.cpp    | 5 +++--
     8 files changed, 27 insertions(+), 15 deletions(-)
    
    diff --git a/src/models/bailingmoe3.cpp b/src/models/bailingmoe3.cpp
    index 1f2592cfa..e208c7d5a 100644
    --- a/src/models/bailingmoe3.cpp
    +++ b/src/models/bailingmoe3.cpp
    @@ -280,8 +280,8 @@ llama_model_bailingmoe3::graph::graph(const llama_model & model, const llm_graph
                 ggml_tensor * beta = ggml_mul_mat(ctx0, layer.ssm_beta, cur);
                 beta = ggml_sigmoid(ctx0, ggml_reshape_4d(ctx0, beta, 1, n_head, n_seq_tokens, n_seqs));
     
    -            q = ggml_l2_norm(ctx0, q, hparams.f_norm_rms_eps);
    -            k = ggml_l2_norm(ctx0, k, hparams.f_norm_rms_eps);
    +            q = build_gdn_l2_norm(ctx0, q, hparams.f_norm_rms_eps);
    +            k = build_gdn_l2_norm(ctx0, k, hparams.f_norm_rms_eps);
     
                 ggml_tensor * states_all = mctx_cur->get_s_l(il);
                 ggml_tensor * state = build_rs(inp_rs, states_all, hparams.n_embd_s(), n_seqs);
    diff --git a/src/models/kimi-k3.cpp b/src/models/kimi-k3.cpp
    index b061093eb..b7604cbf2 100644
    --- a/src/models/kimi-k3.cpp
    +++ b/src/models/kimi-k3.cpp
    @@ -441,9 +441,9 @@ ggml_tensor * llama_model_kimi_k3::graph::build_kda_layer(
         ggml_tensor * state = build_rs(inp_rs, ssm_states_all, hparams.n_embd_s(), n_seqs);
         state = ggml_reshape_4d(ctx0, state, head_dim, head_dim, n_head_kda, n_seqs);
     
    -    const float eps = hparams.f_norm_rms_eps;
    -    Qcur = ggml_l2_norm(ctx0, Qcur, eps);
    -    Kcur = ggml_l2_norm(ctx0, Kcur, eps);
    +    const float eps_norm = hparams.f_norm_rms_eps;
    +    Qcur = build_gdn_l2_norm(ctx0, Qcur, eps_norm);
    +    Kcur = build_gdn_l2_norm(ctx0, Kcur, eps_norm);
     
         auto attn_out = build_delta_net(Qcur, Kcur, Vcur, g1, beta, state, il);
     
    diff --git a/src/models/kimi-linear.cpp b/src/models/kimi-linear.cpp
    index 601d1d9be..f391f5f50 100644
    --- a/src/models/kimi-linear.cpp
    +++ b/src/models/kimi-linear.cpp
    @@ -331,10 +331,11 @@ llama_model_kimi_linear::graph::graph(const llama_model & model, const llm_graph
                 ggml_tensor * state = build_rs(inp_rs, ssm_states_all, hparams.n_embd_s(), n_seqs);
                 state = ggml_reshape_4d(ctx0, state, head_dim, head_dim, n_head, n_seqs);
     
    +
                 const float eps_norm = hparams.f_norm_rms_eps;
     
    -            Qcur = ggml_l2_norm(ctx0, Qcur, eps_norm);
    -            Kcur = ggml_l2_norm(ctx0, Kcur, eps_norm);
    +            Qcur = build_gdn_l2_norm(ctx0, Qcur, eps_norm);
    +            Kcur = build_gdn_l2_norm(ctx0, Kcur, eps_norm);
     
                 // Choose between build_delta_net_chunking and build_delta_net_recurrent based on n_tokens
                 auto attn_out = build_delta_net(Qcur, Kcur, Vcur, g1, beta, state, il);
    diff --git a/src/models/models.h b/src/models/models.h
    index 50e9a235c..87195fddd 100644
    --- a/src/models/models.h
    +++ b/src/models/models.h
    @@ -10,6 +10,13 @@
     
     class llama_memory_hybrid_idx_context;
     
    +// ref: https://github.com/ggml-org/llama.cpp/pull/28068
    +static inline ggml_tensor * build_gdn_l2_norm(ggml_context * ctx, ggml_tensor * x, float eps) {
    +    const float n = x->ne[0];
    +
    +    return ggml_scale(ctx, ggml_rms_norm(ctx, x, eps/n), 1.0f/sqrtf(n));
    +}
    +
     //
     // base classes
     //
    diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp
    index 0b9210981..478f9ebea 100644
    --- a/src/models/qwen35.cpp
    +++ b/src/models/qwen35.cpp
    @@ -423,10 +423,11 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn_linear(
         cb(k_conv, "k_conv", il);
         cb(v_conv, "v_conv", il);
     
    +
         const float eps_norm = hparams.f_norm_rms_eps;
     
    -    q_conv = ggml_l2_norm(ctx0, q_conv, eps_norm);
    -    k_conv = ggml_l2_norm(ctx0, k_conv, eps_norm);
    +    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
    +    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);
     
         //q_conv = ggml_cont_4d(ctx0, q_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
         //k_conv = ggml_cont_4d(ctx0, k_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
    diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp
    index ed4083f12..488c7d357 100644
    --- a/src/models/qwen35moe.cpp
    +++ b/src/models/qwen35moe.cpp
    @@ -447,10 +447,11 @@ ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn_linear(
         cb(k_conv, "k_conv", il);
         cb(v_conv, "v_conv", il);
     
    +
         const float eps_norm = hparams.f_norm_rms_eps;
     
    -    q_conv = ggml_l2_norm(ctx0, q_conv, eps_norm);
    -    k_conv = ggml_l2_norm(ctx0, k_conv, eps_norm);
    +    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
    +    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);
     
         //q_conv = ggml_cont_4d(ctx0, q_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
         //k_conv = ggml_cont_4d(ctx0, k_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
    diff --git a/src/models/qwen3next.cpp b/src/models/qwen3next.cpp
    index eb823b8ea..222c0acf0 100644
    --- a/src/models/qwen3next.cpp
    +++ b/src/models/qwen3next.cpp
    @@ -503,10 +503,11 @@ ggml_tensor * llama_model_qwen3next::graph::build_layer_attn_linear(
         cb(k_conv, "k_conv", il);
         cb(v_conv, "v_conv", il);
     
    +
         const float eps_norm = hparams.f_norm_rms_eps;
     
    -    q_conv = ggml_l2_norm(ctx0, q_conv, eps_norm);
    -    k_conv = ggml_l2_norm(ctx0, k_conv, eps_norm);
    +    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
    +    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);
     
         //q_conv = ggml_cont_4d(ctx0, q_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
         //k_conv = ggml_cont_4d(ctx0, k_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
    diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp
    index 1484c9b07..8ace95f73 100644
    --- a/src/models/qwen4exp.cpp
    +++ b/src/models/qwen4exp.cpp
    @@ -936,10 +936,11 @@ ggml_tensor * llama_model_qwen4exp::graph::build_layer_attn_linear(
         cb(k_conv, "k_conv", il);
         cb(v_conv, "v_conv", il);
     
    +
         const float eps_norm = hparams.f_norm_rms_eps;
     
    -    q_conv = ggml_l2_norm(ctx0, q_conv, eps_norm);
    -    k_conv = ggml_l2_norm(ctx0, k_conv, eps_norm);
    +    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
    +    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);
     
         // repeat to match shapes when head keys != value keys; unneeded with the fused GDN
         if (num_k_heads != num_v_heads && (!cparams.fused_gdn_ar || !cparams.fused_gdn_ch)) {
    
    From 465e49b9cea78a68b9c244ffb48d0ee24a82873d Mon Sep 17 00:00:00 2001
    From: PikaPikachu 
    Date: Mon, 7 Sep 2026 00:47:05 +0800
    Subject: [PATCH 12/31] convert : add `--fuse-qkv` flag to fuse Q/K/V into QKV
     during HF-to-GGUF conversion (#22780)
    
    ---
     conversion/base.py          |  92 +++++++++++++++++++++++++++++-
     convert_hf_to_gguf.py       |   5 ++
     gguf-py/gguf/constants.py   |  80 ++++++++++++++++++++++++++
     src/llama-graph.cpp         | 110 ++++++++++++++++++++++++++----------
     src/llama-graph.h           |  13 +++++
     src/llama-model.cpp         |   6 ++
     src/models/deepseek2.cpp    |  13 +----
     src/models/deepseek2ocr.cpp |   4 +-
     src/models/gemma3n.cpp      |   9 ++-
     src/models/gemma4.cpp       |  40 ++++++++++---
     src/models/jais2.cpp        |   8 +--
     src/models/kimi-linear.cpp  |  19 +++++--
     src/models/llada.cpp        |   7 +--
     src/models/minimax-m2.cpp   |  11 ++--
     src/models/olmo2.cpp        |  11 ++--
     src/models/olmoe.cpp        |  11 ++--
     src/models/qwen35.cpp       |  22 ++++----
     src/models/qwen35moe.cpp    |  22 ++++----
     src/models/qwen3next.cpp    |  22 ++++----
     src/models/step35.cpp       |  16 ++++--
     20 files changed, 396 insertions(+), 125 deletions(-)
    
    diff --git a/conversion/base.py b/conversion/base.py
    index dc1083ead..d2d80be36 100644
    --- a/conversion/base.py
    +++ b/conversion/base.py
    @@ -130,7 +130,8 @@ class ModelBase:
                      sentence_transformers_dense_modules: bool = False,
                      target_model_dir: Path | None = None,
                      fuse_gate_up_exps: bool = False,
    -                 fp8_as_q8: bool = False):
    +                 fp8_as_q8: bool = False,
    +                 fuse_qkv: bool = False):
             if type(self) is ModelBase or \
                     type(self) is TextModel or \
                     type(self) is MmprojModel:
    @@ -153,6 +154,15 @@ class ModelBase:
             self.fuse_gate_up_exps = fuse_gate_up_exps
             self._gate_exp_buffer: dict[int, Tensor] = {}
             self._up_exp_buffer: dict[int, Tensor] = {}
    +        self.fuse_qkv = fuse_qkv
    +        self._q_buffer: dict[int, Tensor] = {}
    +        self._k_buffer: dict[int, Tensor] = {}
    +        self._v_buffer: dict[int, Tensor] = {}
    +        self._q_bias_buffer: dict[int, Tensor] = {}
    +        self._k_bias_buffer: dict[int, Tensor] = {}
    +        self._v_bias_buffer: dict[int, Tensor] = {}
    +        self._fusable_qkv_weight_layers: set[int] = set()
    +        self._fusable_qkv_bias_layers: set[int] = set()
             self.hparams = ModelBase.load_hparams(self.dir_model, self.is_mistral_format) if hparams is None else hparams
             self.model_tensors = self.index_tensors(remote_hf_model_id=remote_hf_model_id)
             self.metadata_override = metadata_override
    @@ -617,6 +627,43 @@ class ModelBase:
                 raise ValueError(f"Can not map tensor {name!r}")
             return new_name
     
    +    def prepare_qkv_fusion(self) -> None:
    +        self._fusable_qkv_weight_layers.clear()
    +        self._fusable_qkv_bias_layers.clear()
    +        if not self.fuse_qkv or gguf.MODEL_TENSOR.ATTN_QKV not in gguf.MODEL_TENSORS[self.model_arch]:
    +            return
    +
    +        qkv_types = {
    +            gguf.MODEL_TENSOR.ATTN_Q,
    +            gguf.MODEL_TENSOR.ATTN_K,
    +            gguf.MODEL_TENSOR.ATTN_V,
    +        }
    +        weights: dict[int, set[gguf.MODEL_TENSOR]] = {}
    +        biases: dict[int, set[gguf.MODEL_TENSOR]] = {}
    +
    +        for name in self.model_tensors:
    +            mapped = self.tensor_map.get_type_and_name(name, try_suffixes=(".weight", ".bias"))
    +            if mapped is None:
    +                continue
    +            tensor_type, new_name = mapped
    +            if tensor_type not in qkv_types:
    +                continue
    +
    +            bid = next((int(part) for part in new_name.split(".") if part.isdecimal()), None)
    +            if bid is None:
    +                continue
    +            if new_name.endswith(".weight"):
    +                weights.setdefault(bid, set()).add(tensor_type)
    +            elif new_name.endswith(".bias"):
    +                biases.setdefault(bid, set()).add(tensor_type)
    +
    +        for bid, weight_types in weights.items():
    +            bias_types = biases.get(bid, set())
    +            if weight_types == qkv_types and (not bias_types or bias_types == qkv_types):
    +                self._fusable_qkv_weight_layers.add(bid)
    +                if bias_types:
    +                    self._fusable_qkv_bias_layers.add(bid)
    +
         def set_gguf_parameters(self):
             raise NotImplementedError("set_gguf_parameters() must be implemented in subclasses")
     
    @@ -645,6 +692,40 @@ class ModelBase:
                    self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.FFN_UP_EXP, bid):
                     return []
     
    +        # Handle Q/K/V tensor fusion if enabled
    +        qkv_bid = next((int(part) for part in new_name.split(".") if part.isdecimal()), None) if self.fuse_qkv else None
    +        if qkv_bid is not None:
    +            is_bias = new_name.endswith('.bias')
    +            suffix = '.bias' if is_bias else '.weight'
    +            fusable_layers = self._fusable_qkv_bias_layers if is_bias else self._fusable_qkv_weight_layers
    +            if qkv_bid not in fusable_layers:
    +                return [(new_name, data_torch)]
    +
    +            buf_q = self._q_bias_buffer if is_bias else self._q_buffer
    +            buf_k = self._k_bias_buffer if is_bias else self._k_buffer
    +            buf_v = self._v_bias_buffer if is_bias else self._v_buffer
    +
    +            if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_Q, qkv_bid, suffix):
    +                buf_q[qkv_bid] = data_torch
    +            elif self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_K, qkv_bid, suffix):
    +                buf_k[qkv_bid] = data_torch
    +            elif self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_V, qkv_bid, suffix):
    +                buf_v[qkv_bid] = data_torch
    +
    +            if qkv_bid in buf_q and qkv_bid in buf_k and qkv_bid in buf_v:
    +                q_data = buf_q.pop(qkv_bid)
    +                k_data = buf_k.pop(qkv_bid)
    +                v_data = buf_v.pop(qkv_bid)
    +                fused_data = torch.cat([q_data, k_data, v_data], dim=0)
    +                fused_name = self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_QKV, qkv_bid, suffix=suffix)
    +                logger.info(f"Fused Q, K, V {suffix[1:]} into QKV for layer {qkv_bid}")
    +                return [(fused_name, fused_data)]
    +
    +            if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_Q, qkv_bid, suffix) or \
    +               self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_K, qkv_bid, suffix) or \
    +               self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.ATTN_V, qkv_bid, suffix):
    +                return []
    +
             return [(new_name, data_torch)]
     
         def tensor_force_quant(self, name: str, new_name: str, bid: int | None, n_dims: int) -> gguf.GGMLQuantizationType | bool:
    @@ -899,6 +980,8 @@ class ModelBase:
     
             self.dequant_model()
     
    +        self.prepare_qkv_fusion()
    +
             # Handle empty tensor_map for models with block_count=0 (like MobileNetV5)
             if self.tensor_map.mapping:
                 max_name_len = max(len(s) for _, s in self.tensor_map.mapping.values()) + len(".weight,")
    @@ -1027,6 +1110,13 @@ class ModelBase:
     
                     self.gguf_writer.add_tensor(new_name, data, raw_dtype=data_qtype)
     
    +        qkv_buffers = (
    +            self._q_buffer, self._k_buffer, self._v_buffer,
    +            self._q_bias_buffer, self._k_bias_buffer, self._v_bias_buffer,
    +        )
    +        if any(qkv_buffers):
    +            raise ValueError("QKV fusion did not consume all buffered tensors")
    +
         def set_type(self):
             self.gguf_writer.add_type(gguf.GGUFType.MODEL)
     
    diff --git a/convert_hf_to_gguf.py b/convert_hf_to_gguf.py
    index 78ad26c65..e09616b19 100755
    --- a/convert_hf_to_gguf.py
    +++ b/convert_hf_to_gguf.py
    @@ -157,6 +157,10 @@ def parse_args() -> argparse.Namespace:
             help="Store tensors dequantized from FP8 as Q8_0 instead of BF16/F16.",
         )
     
    +    parser.add_argument(
    +        "--fuse-qkv", action="store_true",
    +        help="Fuse separate Q, K, V weight tensors into a single QKV tensor.",
    +    )
         parser.add_argument(
             "--target-model-dir", type=str, default=None,
             help=(
    @@ -290,6 +294,7 @@ def main() -> None:
                                          target_model_dir=Path(args.target_model_dir) if args.target_model_dir else None,
                                          fuse_gate_up_exps=args.fuse_gate_up_exps,
                                          fp8_as_q8=args.fp8_as_q8,
    +                                     fuse_qkv=args.fuse_qkv,
                                          )
     
             if args.vocab_only:
    diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py
    index 486f3586d..d51e459dd 100644
    --- a/gguf-py/gguf/constants.py
    +++ b/gguf-py/gguf/constants.py
    @@ -2296,6 +2296,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2316,6 +2317,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2339,6 +2341,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2359,6 +2362,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2404,6 +2408,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2506,6 +2511,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_TYPES,
             MODEL_TENSOR.ATTN_NORM_2,
             MODEL_TENSOR.ATTN_OUT_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -2534,6 +2540,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2563,6 +2570,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2575,6 +2583,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2602,6 +2611,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2633,6 +2643,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2648,6 +2659,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2663,6 +2675,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2677,6 +2690,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2691,6 +2705,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -2711,6 +2726,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -2727,6 +2743,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -2782,6 +2799,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -2798,6 +2816,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -2938,6 +2957,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3071,6 +3091,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3086,6 +3107,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3104,6 +3126,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.ROPE_FACTORS_LONG,
             MODEL_TENSOR.ROPE_FACTORS_SHORT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3141,6 +3164,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3153,6 +3177,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
         MODEL_ARCH.GEMMA2: [
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3169,6 +3194,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.OUTPUT_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -3187,6 +3213,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.OUTPUT_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -3223,6 +3250,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.OUTPUT_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -3278,6 +3306,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.DENSE_2_OUT,
             MODEL_TENSOR.DENSE_3_OUT,
             MODEL_TENSOR.OUTPUT_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -3298,6 +3327,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3461,6 +3491,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3490,6 +3521,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3504,6 +3536,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3518,6 +3551,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3569,6 +3603,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
         MODEL_ARCH.OLMO: [
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3581,6 +3616,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3596,6 +3632,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
         MODEL_ARCH.SEED_OSS: [
             MODEL_TENSOR.TOKEN_EMBD,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3612,6 +3649,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_OUT,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3662,6 +3700,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3683,6 +3722,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3745,6 +3785,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_A,
             MODEL_TENSOR.ATTN_Q_B,
    @@ -3867,6 +3908,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -3943,6 +3985,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
             MODEL_TENSOR.ATTN_POST_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4088,6 +4131,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4102,6 +4146,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4123,6 +4168,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.SSM_D,
             MODEL_TENSOR.SSM_NORM,
             MODEL_TENSOR.SSM_OUT,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4142,6 +4188,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.SSM_D,
             MODEL_TENSOR.SSM_NORM,
             MODEL_TENSOR.SSM_OUT,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4172,6 +4219,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4187,6 +4235,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -4212,6 +4261,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -4243,6 +4293,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4257,6 +4308,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4282,6 +4334,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.SSM_D,
             MODEL_TENSOR.SSM_NORM,
             MODEL_TENSOR.SSM_OUT,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4345,6 +4398,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -4384,6 +4438,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4475,6 +4530,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -4538,6 +4594,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4553,6 +4610,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
             MODEL_TENSOR.ATTN_POST_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4604,6 +4662,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4618,6 +4677,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4635,6 +4695,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.ATTN_NORM,
     
             # Attention components
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,         # Query projection
             MODEL_TENSOR.ATTN_K,         # Key projection
             MODEL_TENSOR.ATTN_V,         # Value projection
    @@ -4667,6 +4728,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -4687,6 +4749,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -4703,6 +4766,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -4793,6 +4857,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4809,6 +4874,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
             MODEL_TENSOR.ATTN_POST_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4832,6 +4898,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.ATTN_NORM, # operator_norm
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4852,6 +4919,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.ATTN_NORM, # operator_norm
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4867,6 +4935,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4886,6 +4955,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4903,6 +4973,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_OUT,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -4920,6 +4991,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -4958,6 +5030,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -5021,6 +5094,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -5038,6 +5112,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -5053,6 +5128,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -5206,6 +5282,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -5252,6 +5329,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ROPE_FREQS,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    @@ -5271,6 +5349,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_Q_NORM,
             MODEL_TENSOR.ATTN_K,
    @@ -5287,6 +5366,7 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
             MODEL_TENSOR.OUTPUT_NORM,
             MODEL_TENSOR.OUTPUT,
             MODEL_TENSOR.ATTN_NORM,
    +        MODEL_TENSOR.ATTN_QKV,
             MODEL_TENSOR.ATTN_Q,
             MODEL_TENSOR.ATTN_K,
             MODEL_TENSOR.ATTN_V,
    diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp
    index 8ea441f44..4cbd5fe21 100644
    --- a/src/llama-graph.cpp
    +++ b/src/llama-graph.cpp
    @@ -1623,8 +1623,26 @@ llm_graph_qkv llm_graph_context::build_qkv(
                       int64_t   n_head,
                       int64_t   n_head_kv,
                           int   il) const {
    -    const int64_t n_embd_q  = n_embd_head * n_head;
    -    const int64_t n_embd_kv = n_embd_head * n_head_kv;
    +    return build_qkv(layer, cur,
    +            n_embd_head, n_head,
    +            n_embd_head, n_head_kv,
    +            n_embd_head, n_head_kv,
    +            il);
    +}
    +
    +llm_graph_qkv llm_graph_context::build_qkv(
    +        const llama_layer & layer,
    +              ggml_tensor * cur,
    +                  int64_t   n_embd_head_q,
    +                  int64_t   n_head_q,
    +                  int64_t   n_embd_head_k,
    +                  int64_t   n_head_k,
    +                  int64_t   n_embd_head_v,
    +                  int64_t   n_head_v,
    +                      int   il,
    +                     bool   reshape) const {
    +    const int64_t n_embd_q = n_embd_head_q * n_head_q;
    +    const int64_t n_embd_k = n_embd_head_k * n_head_k;
     
         ggml_tensor * Qcur, * Kcur, * Vcur;
     
    @@ -1635,59 +1653,93 @@ llm_graph_qkv llm_graph_context::build_qkv(
             if (layer.wqkv_b) {
                 qkv = ggml_add(ctx0, qkv, layer.wqkv_b);
                 cb(qkv, "wqkv_b", il);
    +        } else if (layer.wq_b && layer.wk_b && layer.wv_b) {
    +            // Fused weights may coexist with separate Q/K/V biases in legacy or custom GGUFs.
    +            ggml_tensor * qkv_b = ggml_concat(ctx0, ggml_concat(ctx0, layer.wq_b, layer.wk_b, 0), layer.wv_b, 0);
    +            qkv = ggml_add(ctx0, qkv, qkv_b);
    +            cb(qkv, "wqkv_b", il);
             }
    -        if (hparams.f_clamp_kqv > 0.0f) {
    +        if (reshape && hparams.f_clamp_kqv > 0.0f) {
                 qkv = ggml_clamp(ctx0, qkv, -hparams.f_clamp_kqv, hparams.f_clamp_kqv);
                 cb(qkv, "wqkv_clamped", il);
             }
    -        Qcur = ggml_view_3d(ctx0, qkv, n_embd_head, n_head,    n_tokens,
    -            ggml_row_size(qkv->type, n_embd_head), qkv->nb[1], 0);
    -        Kcur = ggml_view_3d(ctx0, qkv, n_embd_head, n_head_kv, n_tokens,
    -            ggml_row_size(qkv->type, n_embd_head), qkv->nb[1],
    -            ggml_row_size(qkv->type, n_embd_q));
    -        Vcur = ggml_view_3d(ctx0, qkv, n_embd_head, n_head_kv, n_tokens,
    -            ggml_row_size(qkv->type, n_embd_head), qkv->nb[1],
    -            ggml_row_size(qkv->type, n_embd_q + n_embd_kv));
    +        if (reshape) {
    +            Qcur = ggml_view_3d(ctx0, qkv, n_embd_head_q, n_head_q, n_tokens,
    +                ggml_row_size(qkv->type, n_embd_head_q), qkv->nb[1], 0);
    +            Kcur = ggml_view_3d(ctx0, qkv, n_embd_head_k, n_head_k, n_tokens,
    +                ggml_row_size(qkv->type, n_embd_head_k), qkv->nb[1],
    +                ggml_row_size(qkv->type, n_embd_q));
    +            Vcur = ggml_view_3d(ctx0, qkv, n_embd_head_v, n_head_v, n_tokens,
    +                ggml_row_size(qkv->type, n_embd_head_v), qkv->nb[1],
    +                ggml_row_size(qkv->type, n_embd_q + n_embd_k));
    +        } else {
    +            Qcur = ggml_view_2d(ctx0, qkv, n_embd_q, n_tokens, qkv->nb[1], 0);
    +            Kcur = ggml_view_2d(ctx0, qkv, n_embd_k, n_tokens, qkv->nb[1],
    +                ggml_row_size(qkv->type, n_embd_q));
    +            Vcur = ggml_view_2d(ctx0, qkv, n_embd_head_v * n_head_v, n_tokens, qkv->nb[1],
    +                ggml_row_size(qkv->type, n_embd_q + n_embd_k));
    +        }
    +        if (!reshape) {
    +            Qcur = ggml_cont(ctx0, Qcur);
    +            Kcur = ggml_cont(ctx0, Kcur);
    +            Vcur = ggml_cont(ctx0, Vcur);
    +        }
         } else {
             // separate Q/K/V path
             Qcur = build_lora_mm(layer.wq, cur, layer.wq_s);
    -        cb(Qcur, "Qcur", il);
    -        if (layer.wq_b) {
    -            Qcur = ggml_add(ctx0, Qcur, layer.wq_b);
    +        if (reshape) {
                 cb(Qcur, "Qcur", il);
             }
    -        if (hparams.f_clamp_kqv > 0.0f) {
    +        if (layer.wq_b) {
    +            Qcur = ggml_add(ctx0, Qcur, layer.wq_b);
    +            if (reshape) {
    +                cb(Qcur, "Qcur", il);
    +            }
    +        }
    +        if (reshape && hparams.f_clamp_kqv > 0.0f) {
                 Qcur = ggml_clamp(ctx0, Qcur, -hparams.f_clamp_kqv, hparams.f_clamp_kqv);
                 cb(Qcur, "Qcur_clamped", il);
             }
             Kcur = build_lora_mm(layer.wk, cur, layer.wk_s);
    -        cb(Kcur, "Kcur", il);
    -        if (layer.wk_b) {
    -            Kcur = ggml_add(ctx0, Kcur, layer.wk_b);
    +        if (reshape) {
                 cb(Kcur, "Kcur", il);
             }
    -        if (hparams.f_clamp_kqv > 0.0f) {
    +        if (layer.wk_b) {
    +            Kcur = ggml_add(ctx0, Kcur, layer.wk_b);
    +            if (reshape) {
    +                cb(Kcur, "Kcur", il);
    +            }
    +        }
    +        if (reshape && hparams.f_clamp_kqv > 0.0f) {
                 Kcur = ggml_clamp(ctx0, Kcur, -hparams.f_clamp_kqv, hparams.f_clamp_kqv);
                 cb(Kcur, "Kcur_clamped", il);
             }
             Vcur = build_lora_mm(layer.wv, cur, layer.wv_s);
    -        cb(Vcur, "Vcur", il);
    -        if (layer.wv_b) {
    -            Vcur = ggml_add(ctx0, Vcur, layer.wv_b);
    +        if (reshape) {
                 cb(Vcur, "Vcur", il);
             }
    -        if (hparams.f_clamp_kqv > 0.0f) {
    +        if (layer.wv_b) {
    +            Vcur = ggml_add(ctx0, Vcur, layer.wv_b);
    +            if (reshape) {
    +                cb(Vcur, "Vcur", il);
    +            }
    +        }
    +        if (reshape && hparams.f_clamp_kqv > 0.0f) {
                 Vcur = ggml_clamp(ctx0, Vcur, -hparams.f_clamp_kqv, hparams.f_clamp_kqv);
                 cb(Vcur, "Vcur_clamped", il);
             }
    -        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);
    -        Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);
    +        if (reshape) {
    +            Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head_q, n_head_q, n_tokens);
    +            Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head_k, n_head_k, n_tokens);
    +            Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head_v, n_head_v, n_tokens);
    +        }
         }
     
    -    cb(Qcur, "Qcur", il);
    -    cb(Kcur, "Kcur", il);
    -    cb(Vcur, "Vcur", il);
    +    if (reshape) {
    +        cb(Qcur, "Qcur", il);
    +        cb(Kcur, "Kcur", il);
    +        cb(Vcur, "Vcur", il);
    +    }
     
         return { Qcur, Kcur, Vcur };
     }
    diff --git a/src/llama-graph.h b/src/llama-graph.h
    index dddfdac7b..b486578c1 100644
    --- a/src/llama-graph.h
    +++ b/src/llama-graph.h
    @@ -1079,6 +1079,19 @@ struct llm_graph_context {
                       int64_t   n_head_kv,
                           int   il) const;
     
    +    // Set reshape to false to return contiguous projections before clamp/reshape.
    +    llm_graph_qkv build_qkv(
    +        const llama_layer & layer,
    +              ggml_tensor * cur,
    +                  int64_t   n_embd_head_q,
    +                  int64_t   n_head_q,
    +                  int64_t   n_embd_head_k,
    +                  int64_t   n_head_k,
    +                  int64_t   n_embd_head_v,
    +                  int64_t   n_head_v,
    +                      int   il,
    +                     bool   reshape = true) const;
    +
         ggml_tensor * build_ffn(
                  ggml_tensor * cur,
                  ggml_tensor * up,
    diff --git a/src/llama-model.cpp b/src/llama-model.cpp
    index 0e0036781..ffedf89e6 100644
    --- a/src/llama-model.cpp
    +++ b/src/llama-model.cpp
    @@ -3236,6 +3236,12 @@ void llama_model_base::create_tensor_qkv(llama_layer & layer, int bid,
         layer.wqkv = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", bid), {n_embd_, n_embd_qkv}, TENSOR_NOT_REQUIRED | TENSOR_SKIP_IF_VIRTUAL);
         if (layer.wqkv) {
             layer.wqkv_b = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "bias", bid), {n_embd_qkv}, TENSOR_NOT_REQUIRED | TENSOR_SKIP_IF_VIRTUAL);
    +        // Fused weights may coexist with separate Q/K/V biases in legacy or custom GGUFs.
    +        if (!layer.wqkv_b) {
    +            layer.wq_b = create_tensor(tn(LLM_TENSOR_ATTN_Q, "bias", bid), {n_embd_q_}, TENSOR_NOT_REQUIRED);
    +            layer.wk_b = create_tensor(tn(LLM_TENSOR_ATTN_K, "bias", bid), {n_embd_k_}, TENSOR_NOT_REQUIRED);
    +            layer.wv_b = create_tensor(tn(LLM_TENSOR_ATTN_V, "bias", bid), {n_embd_v_}, TENSOR_NOT_REQUIRED);
    +        }
         } else {
             layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", bid), {n_embd_, n_embd_q_}, flags);
             layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", bid), {n_embd_, n_embd_k_}, flags);
    diff --git a/src/models/deepseek2.cpp b/src/models/deepseek2.cpp
    index 4628ff4da..deca86527 100644
    --- a/src/models/deepseek2.cpp
    +++ b/src/models/deepseek2.cpp
    @@ -475,21 +475,12 @@ llama_model_deepseek2::graph::graph(const llama_model & model, const llm_graph_p
                 const int ocr_rope_type = GGML_ROPE_TYPE_NEOX;
                 GGML_ASSERT(n_embed_head == n_embd_head_k && n_embed_head == n_embd_head_v);
     
    -            ggml_tensor * Qcur = NULL;
    -            ggml_tensor * Kcur = NULL;
    -            ggml_tensor * Vcur = NULL;
    -
    -            Qcur = ggml_mul_mat(ctx0, model.layers[il].wq, cur);
    -            Kcur = ggml_mul_mat(ctx0, model.layers[il].wk, cur);
    -            Vcur = ggml_mul_mat(ctx0, model.layers[il].wv, cur);
    +            auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
    +                    n_embed_head, n_head, n_head, il);
                 cb(Qcur, "q", il);
                 cb(Kcur, "k", il);
                 cb(Vcur, "v", il);
     
    -            Qcur = ggml_reshape_3d(ctx0, Qcur, n_embed_head, n_head, n_tokens);
    -            Kcur = ggml_reshape_3d(ctx0, Kcur, n_embed_head, n_head, n_tokens);
    -            Vcur = ggml_reshape_3d(ctx0, Vcur, n_embed_head, n_head, n_tokens);
    -
                 GGML_ASSERT(fabs(freq_base - 10000.0) < 1e-4);
                 Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr, n_embed_head, ocr_rope_type, 0, freq_base, 1, 0, 1, 0, 0);
                 Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr, n_embed_head, ocr_rope_type, 0, freq_base, 1, 0, 1, 0, 0);
    diff --git a/src/models/deepseek2ocr.cpp b/src/models/deepseek2ocr.cpp
    index 1c5c452e9..3d630699e 100644
    --- a/src/models/deepseek2ocr.cpp
    +++ b/src/models/deepseek2ocr.cpp
    @@ -40,9 +40,7 @@ void llama_model_deepseek2ocr::load_arch_tensors(llama_model_loader &) {
         for (int i = 0; i < n_layer; ++i) {
             auto & layer = layers[i];
     
    -        layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_embd}, 0);
    -        layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), {n_embd, n_embd}, 0);
    -        layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), {n_embd, n_embd}, 0);
    +        create_tensor_qkv(layer, i, n_embd, n_embd, n_embd, n_embd, 0);
             layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd, n_embd}, 0);
     
             // norm
    diff --git a/src/models/gemma3n.cpp b/src/models/gemma3n.cpp
    index 83eb8250a..ea616db3b 100644
    --- a/src/models/gemma3n.cpp
    +++ b/src/models/gemma3n.cpp
    @@ -176,7 +176,14 @@ llama_model_gemma3n::graph::graph(const llama_model & model, const llm_graph_par
                         hparams.f_attention_scale, il);
             } else {
                 // reuse KV cache of earlier layers
    -            ggml_tensor * Qcur = build_lora_mm(model.layers[il].wq, cur);
    +            ggml_tensor * Qcur;
    +            if (model.layers[il].wqkv) {
    +                ggml_tensor * qkv = build_lora_mm(model.layers[il].wqkv, cur);
    +                const int64_t q_dim = n_embd_head * n_head;
    +                Qcur = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv, q_dim, n_tokens, qkv->nb[1], 0));
    +            } else {
    +                Qcur = build_lora_mm(model.layers[il].wq, cur);
    +            }
                 cb(Qcur, "Qcur", il);
                 Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head, n_tokens);
     
    diff --git a/src/models/gemma4.cpp b/src/models/gemma4.cpp
    index 0cd95742d..388126e26 100644
    --- a/src/models/gemma4.cpp
    +++ b/src/models/gemma4.cpp
    @@ -75,9 +75,13 @@ void llama_model_gemma4::load_arch_tensors(llama_model_loader &) {
             layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
     
             // note: use_alternative_attention (v_proj is optional, if it's not present, use k_proj)
    -        layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q,   "weight", i), {n_embd, n_embd_head * n_head}, 0);
    -        layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K,   "weight", i), {n_embd, n_embd_k}, kv_flags);
    -        layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V,   "weight", i), {n_embd, n_embd_v}, TENSOR_NOT_REQUIRED);
    +        layer.wqkv = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", i),
    +            {n_embd, n_embd_head * n_head + n_embd_k + n_embd_v}, TENSOR_NOT_REQUIRED | TENSOR_SKIP_IF_VIRTUAL);
    +        if (!layer.wqkv) {
    +            layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_embd_head * n_head}, 0);
    +            layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), {n_embd, n_embd_k}, kv_flags);
    +            layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), {n_embd, n_embd_v}, TENSOR_NOT_REQUIRED);
    +        }
             layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head * n_head, n_embd}, 0);
     
             layer.attn_q_norm    = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM,    "weight", i), {n_embd_head}, 0);
    @@ -202,9 +206,17 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para
     
             // Q projection (shared for both non-KV and KV layers)
             // this is to mirror Gemma4Attention in pytorch code
    +        ggml_tensor * qkv_fused = nullptr;
             ggml_tensor * Qcur;
    -        {
    +        if (model.layers[il].wqkv) {
    +            qkv_fused = build_lora_mm(model.layers[il].wqkv, cur, model.layers[il].wqkv_s);
    +            cb(qkv_fused, "wqkv", il);
    +            const int64_t q_dim = n_embd_head * n_head;
    +            Qcur = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv_fused, q_dim, n_tokens, qkv_fused->nb[1], 0));
    +        } else {
                 Qcur = build_lora_mm(model.layers[il].wq, cur, model.layers[il].wq_s);
    +        }
    +        {
                 cb(Qcur, "Qcur", il);
     
                 Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head, n_tokens);
    @@ -219,12 +231,22 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para
     
             // self-attention
             if (hparams.has_kv(il)) {
    -            ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur, model.layers[il].wk_s);
    +            ggml_tensor * Kcur;
    +            ggml_tensor * Vcur;
    +            if (qkv_fused) {
    +                const int64_t q_dim = n_embd_head * n_head;
    +                const int64_t k_dim = n_embd_head * n_head_kv;
    +                const int64_t v_dim = n_embd_head * n_head_kv;
    +                const size_t  esize = ggml_element_size(qkv_fused);
    +                Kcur = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv_fused, k_dim, n_tokens, qkv_fused->nb[1], q_dim * esize));
    +                Vcur = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv_fused, v_dim, n_tokens, qkv_fused->nb[1], (q_dim + k_dim) * esize));
    +            } else {
    +                Kcur = build_lora_mm(model.layers[il].wk, cur, model.layers[il].wk_s);
    +                Vcur = model.layers[il].wv
    +                       ? build_lora_mm(model.layers[il].wv, cur, model.layers[il].wv_s)
    +                       : Kcur; // if v_proj is not present, use Kcur as Vcur
    +            }
                 cb(Kcur, "Kcur", il);
    -
    -            ggml_tensor * Vcur = model.layers[il].wv
    -                                    ? build_lora_mm(model.layers[il].wv, cur, model.layers[il].wv_s)
    -                                    : Kcur; // if v_proj is not present, use Kcur as Vcur
                 cb(Vcur, "Vcur", il);
     
                 Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
    diff --git a/src/models/jais2.cpp b/src/models/jais2.cpp
    index 8610fcc9f..64813b7b6 100644
    --- a/src/models/jais2.cpp
    +++ b/src/models/jais2.cpp
    @@ -29,15 +29,9 @@ void llama_model_jais2::load_arch_tensors(llama_model_loader &) {
             layer.attn_norm   = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
             layer.attn_norm_b = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "bias", i),   {n_embd}, 0);
     
    -        layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_embd_head_k * n_head}, 0);
    -        layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), {n_embd, n_embd_k_gqa}, 0);
    -        layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), {n_embd, n_embd_v_gqa}, 0);
    +        create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_k_gqa, n_embd_v_gqa, 0);
             layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);
     
    -        // attention biases - all have shape n_embd (output dimension of projections)
    -        layer.wq_b = create_tensor(tn(LLM_TENSOR_ATTN_Q, "bias", i), {n_embd}, 0);
    -        layer.wk_b = create_tensor(tn(LLM_TENSOR_ATTN_K, "bias", i), {n_embd}, 0);
    -        layer.wv_b = create_tensor(tn(LLM_TENSOR_ATTN_V, "bias", i), {n_embd}, 0);
             layer.wo_b = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "bias", i), {n_embd}, 0);
     
             layer.ffn_norm   = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
    diff --git a/src/models/kimi-linear.cpp b/src/models/kimi-linear.cpp
    index f391f5f50..b9cf28d85 100644
    --- a/src/models/kimi-linear.cpp
    +++ b/src/models/kimi-linear.cpp
    @@ -195,7 +195,7 @@ static ggml_tensor * causal_conv1d(ggml_cgraph * gf, ggml_context * ctx0, ggml_t
     // Causal Conv1d function for Q,K,V
     // When qkv is 0, it is Q, 1 is K, 2 is V
         // Step 1: Q, K, V projections -> [d_inner, n_tokens]
    -    ggml_tensor * x_proj = ggml_mul_mat(ctx0, proj_w, x);
    +    ggml_tensor * x_proj = proj_w ? ggml_mul_mat(ctx0, proj_w, x) : x;
     
         // Reshape input: {d_inner, n_tokens} -> {d_inner, n_seq_tokens, n_seqs}
         ggml_tensor * x_3d = ggml_reshape_3d(ctx0, x_proj, d_inner, n_seq_tokens, n_seqs);
    @@ -295,9 +295,20 @@ llama_model_kimi_linear::graph::graph(const llama_model & model, const llm_graph
                 ggml_tensor * conv_states_all = mctx_cur->get_r_l(il);
                 cb(conv_states_all, "conv_states_all", il);
                 ggml_tensor * conv_state_all = build_rs(inp_rs, conv_states_all, hparams.n_embd_r(), n_seqs);
    -            ggml_tensor * Qcur = causal_conv1d(gf, ctx0, conv_states_all, conv_state_all, 0, cur, layer.wq, layer.ssm_q_conv, d_conv, head_dim, n_head, n_seq_tokens, n_seqs, n_tokens, kv_head);
    -            ggml_tensor * Kcur = causal_conv1d(gf, ctx0, conv_states_all, conv_state_all, 1, cur, layer.wk, layer.ssm_k_conv, d_conv, head_dim, n_head, n_seq_tokens, n_seqs, n_tokens, kv_head);
    -            ggml_tensor * Vcur = causal_conv1d(gf, ctx0, conv_states_all, conv_state_all, 2, cur, layer.wv, layer.ssm_v_conv, d_conv, head_dim, n_head, n_seq_tokens, n_seqs, n_tokens, kv_head);
    +            ggml_tensor * q_in = cur, * k_in = cur, * v_in = cur;
    +            ggml_tensor * q_w = layer.wq, * k_w = layer.wk, * v_w = layer.wv;
    +            if (layer.wqkv) {
    +                ggml_tensor * qkv = ggml_mul_mat(ctx0, layer.wqkv, cur);
    +                const int64_t d_inner = head_dim * n_head;
    +                const size_t esize = ggml_element_size(qkv);
    +                q_in = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv, d_inner, n_tokens, qkv->nb[1], 0));
    +                k_in = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv, d_inner, n_tokens, qkv->nb[1], d_inner * esize));
    +                v_in = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv, d_inner, n_tokens, qkv->nb[1], 2 * d_inner * esize));
    +                q_w = nullptr; k_w = nullptr; v_w = nullptr;
    +            }
    +            ggml_tensor * Qcur = causal_conv1d(gf, ctx0, conv_states_all, conv_state_all, 0, q_in, q_w, layer.ssm_q_conv, d_conv, head_dim, n_head, n_seq_tokens, n_seqs, n_tokens, kv_head);
    +            ggml_tensor * Kcur = causal_conv1d(gf, ctx0, conv_states_all, conv_state_all, 1, k_in, k_w, layer.ssm_k_conv, d_conv, head_dim, n_head, n_seq_tokens, n_seqs, n_tokens, kv_head);
    +            ggml_tensor * Vcur = causal_conv1d(gf, ctx0, conv_states_all, conv_state_all, 2, v_in, v_w, layer.ssm_v_conv, d_conv, head_dim, n_head, n_seq_tokens, n_seqs, n_tokens, kv_head);
     
                 // g1 = -exp(A_log) * softplus(f_b(f_a(x)) + dt_bias)
                 ggml_tensor * f_a = ggml_mul_mat(ctx0, layer.ssm_f_a, cur);
    diff --git a/src/models/llada.cpp b/src/models/llada.cpp
    index 87d4259f9..ae3d6925c 100644
    --- a/src/models/llada.cpp
    +++ b/src/models/llada.cpp
    @@ -36,12 +36,7 @@ void llama_model_llada::load_arch_tensors(llama_model_loader &) {
     
             layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), { n_embd }, 0);
     
    -        // Use separate Q, K, V projections without bias, matching LLaDALlamaBlock
    -        layer.wq =
    -            create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), { n_embd, n_embd_head_k * n_head }, 0);
    -        layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), { n_embd, n_embd_k_gqa }, 0);
    -        layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), { n_embd, n_embd_v_gqa }, 0);
    -        // No bias for QKV projections as per config: include_bias=false, include_qkv_bias=false
    +        create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_k_gqa, n_embd_v_gqa, 0);
             layer.wo =
                 create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), { n_embd_head_k * n_head, n_embd }, 0);
             layer.wo_b = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "bias", i), { n_embd }, TENSOR_NOT_REQUIRED);
    diff --git a/src/models/minimax-m2.cpp b/src/models/minimax-m2.cpp
    index c2e69bfaa..7a22af036 100644
    --- a/src/models/minimax-m2.cpp
    +++ b/src/models/minimax-m2.cpp
    @@ -71,14 +71,13 @@ llama_model_minimax_m2::graph::graph(const llama_model & model, const llm_graph_
                 cur = build_norm(inpL, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
                 cb(cur, "attn_norm", il);
     
    -            // compute Q and K and RoPE them
    -            ggml_tensor * Qcur = build_lora_mm(model.layers[il].wq, cur);
    +            auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
    +                    n_embd_head, n_head,
    +                    n_embd_head, n_head_kv,
    +                    n_embd_head, n_head_kv,
    +                    il, false);
                 cb(Qcur, "Qcur", il);
    -
    -            ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur);
                 cb(Kcur, "Kcur", il);
    -
    -            ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur);
                 cb(Vcur, "Vcur", il);
     
                 Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, NULL,
    diff --git a/src/models/olmo2.cpp b/src/models/olmo2.cpp
    index cb52cdef7..05b9394b8 100644
    --- a/src/models/olmo2.cpp
    +++ b/src/models/olmo2.cpp
    @@ -93,14 +93,13 @@ llama_model_olmo2::graph::graph(const llama_model & model, const llm_graph
     
             // self_attention
             {
    -            // compute Q and K and RoPE them
    -            ggml_tensor * Qcur = build_lora_mm(model.layers[il].wq, cur);
    +            auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
    +                    n_embd_head, n_head,
    +                    n_embd_head, n_head_kv,
    +                    n_embd_head, n_head_kv,
    +                    il, false);
                 cb(Qcur, "Qcur", il);
    -
    -            ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur);
                 cb(Kcur, "Kcur", il);
    -
    -            ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur);
                 cb(Vcur, "Vcur", il);
     
                 Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, NULL,
    diff --git a/src/models/olmoe.cpp b/src/models/olmoe.cpp
    index 1e2baeb20..11c53f3f4 100644
    --- a/src/models/olmoe.cpp
    +++ b/src/models/olmoe.cpp
    @@ -79,14 +79,13 @@ llama_model_olmoe::graph::graph(const llama_model & model, const llm_graph_param
     
             // self_attention
             {
    -            // compute Q and K and RoPE them
    -            ggml_tensor * Qcur = build_lora_mm(model.layers[il].wq, cur);
    +            auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
    +                    n_embd_head, n_head,
    +                    n_embd_head, n_head_kv,
    +                    n_embd_head, n_head_kv,
    +                    il, false);
                 cb(Qcur, "Qcur", il);
    -
    -            ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur);
                 cb(Kcur, "Kcur", il);
    -
    -            ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur);
                 cb(Vcur, "Vcur", il);
     
                 Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, NULL,
    diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp
    index 478f9ebea..a1e263500 100644
    --- a/src/models/qwen35.cpp
    +++ b/src/models/qwen35.cpp
    @@ -263,8 +263,14 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn(
         // Order: joint QG projection, QG split, Q norm, KV projection, K norm, RoPE, attention
     
         // 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 ]
    +    auto [Qcur_full, Kcur, Vcur] = build_qkv(model.layers[il], cur,
    +            n_embd_head * 2, n_head,
    +            n_embd_head,     n_head_kv,
    +            n_embd_head,     n_head_kv,
    +            il, false);
         cb(Qcur_full, "Qcur_full", il);
    +    cb(Kcur, "Kcur", il);
    +    cb(Vcur, "Vcur", il);
     
         ggml_tensor * Qcur = ggml_view_3d(ctx0, Qcur_full, n_embd_head, n_head, n_tokens,
             ggml_element_size(Qcur_full) * n_embd_head * 2,
    @@ -275,12 +281,6 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn(
         Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, nullptr, LLM_NORM_RMS, il);
         cb(Qcur, "Qcur_normed", il);
     
    -    ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur, model.layers[il].wk_s);
    -    cb(Kcur, "Kcur", il);
    -
    -    ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur, model.layers[il].wv_s);
    -    cb(Vcur, "Vcur", il);
    -
         // Apply K normalization
         Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
         Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, nullptr, LLM_NORM_RMS, il);
    @@ -554,7 +554,11 @@ llama_model_qwen35::graph_mtp::graph_mtp(const llama_model & model, const llm_gr
         cur = build_norm(cur, layer.attn_norm, nullptr, LLM_NORM_RMS, il);
         cb(cur, "mtp_attn_norm", il);
     
    -    ggml_tensor * Qcur_full = build_lora_mm(layer.wq, cur, layer.wq_s);
    +    auto [Qcur_full, Kcur, Vcur] = build_qkv(layer, cur,
    +            n_embd_head * 2, n_head,
    +            n_embd_head,     n_head_kv,
    +            n_embd_head,     n_head_kv,
    +            il, false);
         cb(Qcur_full, "mtp_Qcur_full", il);
     
         ggml_tensor * Qcur = ggml_view_3d(ctx0, Qcur_full,
    @@ -573,12 +577,10 @@ llama_model_qwen35::graph_mtp::graph_mtp(const llama_model & model, const llm_gr
         gate = ggml_cont_2d(ctx0, gate, n_embd_head * n_head, n_tokens);
         cb(gate, "mtp_gate", il);
     
    -    ggml_tensor * Kcur = build_lora_mm(layer.wk, cur, layer.wk_s);
         Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
         Kcur = build_norm(Kcur, layer.attn_k_norm, nullptr, LLM_NORM_RMS, il);
         cb(Kcur, "mtp_Kcur_normed", il);
     
    -    ggml_tensor * Vcur = build_lora_mm(layer.wv, cur, layer.wv_s);
         Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);
         cb(Vcur, "mtp_Vcur", il);
     
    diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp
    index 488c7d357..bdf772625 100644
    --- a/src/models/qwen35moe.cpp
    +++ b/src/models/qwen35moe.cpp
    @@ -287,8 +287,14 @@ ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn(
         // Order: joint QG projection, QG split, Q norm, KV projection, K norm, RoPE, attention
     
         // 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 ]
    +    auto [Qcur_full, Kcur, Vcur] = build_qkv(model.layers[il], cur,
    +            n_embd_head * 2, n_head,
    +            n_embd_head,     n_head_kv,
    +            n_embd_head,     n_head_kv,
    +            il, false);
         cb(Qcur_full, "Qcur_full", il);
    +    cb(Kcur, "Kcur", il);
    +    cb(Vcur, "Vcur", il);
     
         ggml_tensor * Qcur = ggml_view_3d(ctx0, Qcur_full, n_embd_head, n_head, n_tokens,
             ggml_element_size(Qcur_full) * n_embd_head * 2,
    @@ -299,12 +305,6 @@ ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn(
         Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, nullptr, LLM_NORM_RMS, il);
         cb(Qcur, "Qcur_normed", il);
     
    -    ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur, model.layers[il].wk_s);
    -    cb(Kcur, "Kcur", il);
    -
    -    ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur, model.layers[il].wv_s);
    -    cb(Vcur, "Vcur", il);
    -
         // Apply K normalization
         Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
         Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, nullptr, LLM_NORM_RMS, il);
    @@ -618,7 +618,11 @@ llama_model_qwen35moe::graph_mtp::graph_mtp(const llama_model & model, const llm
         cur = build_norm(cur, layer.attn_norm, nullptr, LLM_NORM_RMS, il);
         cb(cur, "mtp_attn_norm", il);
     
    -    ggml_tensor * Qcur_full = build_lora_mm(layer.wq, cur, layer.wq_s);
    +    auto [Qcur_full, Kcur, Vcur] = build_qkv(layer, cur,
    +            n_embd_head * 2, n_head,
    +            n_embd_head,     n_head_kv,
    +            n_embd_head,     n_head_kv,
    +            il, false);
         cb(Qcur_full, "mtp_Qcur_full", il);
     
         ggml_tensor * Qcur = ggml_view_3d(ctx0, Qcur_full,
    @@ -637,12 +641,10 @@ llama_model_qwen35moe::graph_mtp::graph_mtp(const llama_model & model, const llm
         gate = ggml_cont_2d(ctx0, gate, n_embd_head * n_head, n_tokens);
         cb(gate, "mtp_gate", il);
     
    -    ggml_tensor * Kcur = build_lora_mm(layer.wk, cur, layer.wk_s);
         Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
         Kcur = build_norm(Kcur, layer.attn_k_norm, nullptr, LLM_NORM_RMS, il);
         cb(Kcur, "mtp_Kcur_normed", il);
     
    -    ggml_tensor * Vcur = build_lora_mm(layer.wv, cur, layer.wv_s);
         Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);
         cb(Vcur, "mtp_Vcur", il);
     
    diff --git a/src/models/qwen3next.cpp b/src/models/qwen3next.cpp
    index 222c0acf0..b63fc9c6a 100644
    --- a/src/models/qwen3next.cpp
    +++ b/src/models/qwen3next.cpp
    @@ -244,8 +244,14 @@ ggml_tensor * llama_model_qwen3next::graph::build_layer_attn(
         // Order: joint QG projection, QG split, Q norm, KV projection, K norm, RoPE, attention
     
         // 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);
    +    auto [Qcur_full, Kcur, Vcur] = build_qkv(model.layers[il], cur,
    +            n_embd_head * 2, n_head,
    +            n_embd_head,     n_head_kv,
    +            n_embd_head,     n_head_kv,
    +            il, false);
         cb(Qcur_full, "Qcur_full", il);
    +    cb(Kcur, "Kcur", il);
    +    cb(Vcur, "Vcur", il);
     
         Qcur_full = ggml_reshape_4d(ctx0, Qcur_full, n_embd_head * 2, n_head, n_tokens, 1);
     
    @@ -260,12 +266,6 @@ ggml_tensor * llama_model_qwen3next::graph::build_layer_attn(
                          Qcur_full->nb[1], Qcur_full->nb[2], Qcur_full->nb[3], n_embd_head * ggml_element_size(Qcur_full));
         cb(gate, "gate", il);
     
    -    ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur, model.layers[il].wk_s);
    -    cb(Kcur, "Kcur", il);
    -
    -    ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur, model.layers[il].wv_s);
    -    cb(Vcur, "Vcur", il);
    -
         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);
     
    @@ -692,7 +692,11 @@ llama_model_qwen3next::graph_mtp::graph_mtp(const llama_model & model, const llm
         cur = build_norm(cur, layer.attn_norm, nullptr, LLM_NORM_RMS, il);
         cb(cur, "mtp_attn_norm", il);
     
    -    ggml_tensor * Qcur_full = build_lora_mm(layer.wq, cur, layer.wq_s);
    +    auto [Qcur_full, Kcur, Vcur] = build_qkv(layer, cur,
    +            n_embd_head * 2, n_head,
    +            n_embd_head,     n_head_kv,
    +            n_embd_head,     n_head_kv,
    +            il, false);
         cb(Qcur_full, "mtp_Qcur_full", il);
     
         ggml_tensor * Qcur = ggml_view_3d(ctx0, Qcur_full,
    @@ -703,12 +707,10 @@ llama_model_qwen3next::graph_mtp::graph_mtp(const llama_model & model, const llm
         Qcur = build_norm(Qcur, layer.attn_q_norm, nullptr, LLM_NORM_RMS, il);
         cb(Qcur, "mtp_Qcur_normed", il);
     
    -    ggml_tensor * Kcur = build_lora_mm(layer.wk, cur, layer.wk_s);
         Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
         Kcur = build_norm(Kcur, layer.attn_k_norm, nullptr, LLM_NORM_RMS, il);
         cb(Kcur, "mtp_Kcur_normed", il);
     
    -    ggml_tensor * Vcur = build_lora_mm(layer.wv, cur, layer.wv_s);
         Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);
     
         Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr,
    diff --git a/src/models/step35.cpp b/src/models/step35.cpp
    index 53f3179c6..946a36960 100644
    --- a/src/models/step35.cpp
    +++ b/src/models/step35.cpp
    @@ -216,9 +216,11 @@ llama_model_step35::graph::graph(const llama_model & model, const llm_graph_para
             {
                 cur = build_norm(cur, model.layers[il].attn_norm, nullptr, LLM_NORM_RMS, il);
                 cb(cur, "attn_norm", il);
    -            ggml_tensor * Qcur = build_lora_mm(model.layers[il].wq, cur);
    -            ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur);
    -            ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur);
    +            auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
    +                    n_embd_head_k, n_head_l,
    +                    n_embd_head_k, n_head_kv_l,
    +                    n_embd_head_v, n_head_kv_l,
    +                    il, false);
     
                 cb(Qcur, "Qcur", il);
                 cb(Kcur, "Kcur", il);
    @@ -425,9 +427,11 @@ llama_model_step35::graph_mtp::graph_mtp(const llama_model & model, const llm_gr
         cur = build_norm(cur, layer.attn_norm, nullptr, LLM_NORM_RMS, il);
         cb(cur, "mtp_attn_norm", il);
     
    -    ggml_tensor * Qcur = build_lora_mm(layer.wq, cur, layer.wq_s);
    -    ggml_tensor * Kcur = build_lora_mm(layer.wk, cur, layer.wk_s);
    -    ggml_tensor * Vcur = build_lora_mm(layer.wv, cur, layer.wv_s);
    +    auto [Qcur, Kcur, Vcur] = build_qkv(layer, cur,
    +            n_embd_head_k, n_head_l,
    +            n_embd_head_k, n_head_kv_l,
    +            n_embd_head_v, n_head_kv_l,
    +            il, false);
         cb(Qcur, "mtp_Qcur", il);
         cb(Kcur, "mtp_Kcur", il);
         cb(Vcur, "mtp_Vcur", il);
    
    From 8fe90e1fbfc065f17a0b233c9df239423cd24a75 Mon Sep 17 00:00:00 2001
    From: Anjielon 
    Date: Mon, 7 Sep 2026 06:35:30 +0200
    Subject: [PATCH 13/31] vulkan: add TQ1_0 support (mm, mat-vec, mat-vec-id,
     dequant, get_rows) (#27765)
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    * vulkan: add TQ1_0 support (mm, mat-vec, dequant, get_rows)
    
    * vulkan: pack TQ1_0 powers of 3 into a 32-bit constant
    
    Replaces the constant array with a packed 32-bit value (7 bits per entry,
    max 81 < 128) extracted with shift/mask, as suggested in review — avoids a
    constant array that may not be kept in registers.
    
    test-backend-ops on gfx1151: tq1_0 MUL_MAT 11/11, MUL_MAT_ID 6/6,
    GET_ROWS 4/4, unchanged.
    
    * vulkan: address review - shared TQ1_0 decode helpers, fix standalone dequant shader
    
    Review feedback from jeffbolznv, all points:
    
    - Move the packed-pow3 decode into shared helpers in types.glsl
      (tq1_0_byte_of / tq1_0_digit_of / tq1_0_trit) and use them from
      dequant_funcs.glsl, mul_mm_funcs.glsl, dequant_funcs_cm2.glsl and
      dequant_tq1_0.comp instead of repeating the logic. The cm2 path also
      drops its constant array for the packed-constant extraction.
    - Translate all remaining comments to English.
    - dequant_tq1_0.comp: use dequant_head.glsl. The shader previously declared
      its own single-field push constant while the pipeline is created with the
      5-field layout, so p.ne read the wrong field - confirmed broken, as
      suspected in review.
    - Fix wg_denoms for the standalone dequant pipeline: one invocation decodes
      4 elements with local_size 256, so a workgroup covers 256*4 elements, not
      256*16. With the old value the dispatcher launched a quarter of the
      required workgroups.
    
    Verified by temporarily forcing the dequant + f16 matmul path for TQ1_0
    (hack not committed): test-backend-ops MUL_MAT passes through the rewritten
    standalone shader, and the standard MUL_MAT / MUL_MAT_ID / GET_ROWS
    tq1_0 cases still pass on Vulkan (AMD gfx1151).
    
    * vulkan: address review — English comments, shared tq1_0_trit, trim TQ1_0 test cases
    
    - mul_mat_vec_tq1_0.comp: drop leftover non-English comment and the local
      POW3_PACKED constant; all decode sites now call tq1_0_trit() from types.glsl
    - types.glsl / dequant_funcs_cm2.glsl: ASCII-only, drop stale reviewer note
    - test-backend-ops: remove the oversized MUL_MAT_ID case (432 MiB A tensor,
      ~172 GFLOP reference); move the two remaining ones next to the other
      backend-specific mul_mat_id one-offs and document why they are needed
    
    * metal: decline TQ1_0 for GET_ROWS and mat-mul in supports_op
    
    The new TQ1_0 cases in test-backend-ops exposed that the Metal backend
    claimed support for GET_ROWS/MUL_MAT/MUL_MAT_ID with TQ1_0 sources while
    having no such kernels (ggml_metal_library_compile_pipeline aborted on the
    missing kernel_get_rows_tq1_0). Decline the type so the ops fall back to
    the CPU, matching the existing NVFP4 handling on the same lines.
    
    Assisted-by: Claude Fable 5
    
    * vulkan: trim the TQ1_0 comments
    
    Addresses @0cc4m's review: keep only what the code does not already say.
    
    Removed the block-format recaps (the layout is right there in the struct) and
    the step-by-step decode walkthrough. Kept the two facts a reader cannot infer:
    the 8-bit truncation is part of the format, not an optimisation, and the powers
    of 3 are packed into one uint so they do not end up in a constant array that
    may miss the registers.
    
    No functional change.
    
    * vulkan: address review — trim comments, fold Metal check, drop unused _v
    
    Per @0cc4m's review:
    
    - dequant_funcs.glsl, dequant_funcs_cm2.glsl: drop the "see types.glsl"
      pointers — they apply to every quant and say nothing specific.
    - dequant_tq1_0.comp: drop the wg_denoms note. It is a precondition, not
      information.
    - mul_mm_funcs.glsl: same pointer removed.
    - types.glsl: the comment on tq1_0_trit is down to the one fact the code
      cannot show — the 8-bit truncation is part of the format, matching the C
      reference, not an optimisation.
    - dequant_funcs_cm2.glsl: removed dequantFuncTQ1_0_v and its define. You were
      right that it is optional: it wrapped four scalar decodes and vectorised
      nothing, and mul_mm_cm2.comp already guards the path with
      `#if defined(dequantFuncA_v)` (DATA_A_F32 omits it the same way).
    - ggml-metal-device.m: folded TQ1_0 into the existing NVFP4 check instead of a
      separate block, and dropped both comments.
    - test-backend-ops.cpp: the two mul_mat_id cases stay — they cover the
      block-stride loop and the per-expert base offset that k == 256 alone never
      reaches — but the comment is now one line instead of five.
    
    Kept: the one-line labels on the three block regions in mul_mat_vec_tq1_0.comp
    and on tq1_0_byte_of(). Those state the 5-trits-per-byte packing, which the
    loop bounds do not show. Happy to remove them too if you prefer.
    
    Re-verified on AMD gfx1151 (Vulkan), test-backend-ops, 2/2 backends passed:
    MUL_MAT 9 TQ1_0 cases, MUL_MAT_ID 5, GET_ROWS 4 — all OK, no failures.
    The coopmat2 path is unchanged apart from the removed _v define.
    ---
     ggml/src/ggml-metal/ggml-metal-device.m       |  7 +-
     ggml/src/ggml-vulkan/ggml-vulkan.cpp          | 23 +++++
     .../vulkan-shaders/dequant_funcs.glsl         | 15 ++++
     .../vulkan-shaders/dequant_funcs_cm2.glsl     | 15 ++++
     .../vulkan-shaders/dequant_tq1_0.comp         | 28 ++++++
     .../vulkan-shaders/mul_mat_vec_tq1_0.comp     | 85 +++++++++++++++++++
     .../vulkan-shaders/mul_mm_funcs.glsl          | 18 ++++
     .../src/ggml-vulkan/vulkan-shaders/types.glsl | 35 ++++++++
     .../vulkan-shaders/vulkan-shaders-gen.cpp     |  3 +-
     tests/test-backend-ops.cpp                    |  9 +-
     10 files changed, 233 insertions(+), 5 deletions(-)
     create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq1_0.comp
     create mode 100644 ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0.comp
    
    diff --git a/ggml/src/ggml-metal/ggml-metal-device.m b/ggml/src/ggml-metal/ggml-metal-device.m
    index e20a4e891..afd6f5210 100644
    --- a/ggml/src/ggml-metal/ggml-metal-device.m
    +++ b/ggml/src/ggml-metal/ggml-metal-device.m
    @@ -1486,7 +1486,9 @@ static bool ggml_metal_supports_mul_mat_op(
             const struct ggml_tensor * op,
             bool src0_f16_has_mv,
             bool mm_path) {
    -    if (!has_simdgroup_reduction || op->src[0]->type == GGML_TYPE_NVFP4) {
    +    if (!has_simdgroup_reduction ||
    +        op->src[0]->type == GGML_TYPE_NVFP4 ||
    +        op->src[0]->type == GGML_TYPE_TQ1_0) {
             return false;
         }
     
    @@ -1887,7 +1889,8 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te
                     };
                 }
             case GGML_OP_GET_ROWS:
    -            return op->src[0]->type != GGML_TYPE_NVFP4;
    +            return op->src[0]->type != GGML_TYPE_NVFP4 &&
    +                   op->src[0]->type != GGML_TYPE_TQ1_0;
             case GGML_OP_SET_ROWS:
                 {
                     if (op->src[0]->type == GGML_TYPE_F16) {
    diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    index a04a6b27a..efadd3663 100644
    --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    @@ -4727,6 +4727,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
             CREATE_MM2(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(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q2_K], matmul_q2_k_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3)
             CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_TQ2_0], matmul_tq2_0_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3)
    +        CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_TQ1_0], matmul_tq1_0_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3)
             CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q3_K], matmul_q3_k_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3)
             CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q4_K], matmul_q4_k_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3)
             CREATE_MM2(pipeline_dequant_mul_mat_mat_f16[GGML_TYPE_Q5_K], matmul_q5_k_f16, mmq_wg_denoms_k, warptile_mmq_k, vk_mat_mat_push_constants, 3)
    @@ -4768,6 +4769,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
             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_TQ1_0], matmul_id_subgroup_tq1_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)
    @@ -4841,6 +4843,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
     
             CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q2_K], matmul_q2_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, );
             CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ2_0], matmul_tq2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, );
    +        CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ1_0], matmul_tq1_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, );
             CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_K], matmul_q3_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, );
             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, );
    @@ -4886,6 +4889,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
             CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_f32, 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[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id);
             CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_subgroup_tq2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id);
    +        CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0], matmul_id_subgroup_tq1_0_f32, 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[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_f32, 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[GGML_TYPE_Q4_K], matmul_id_subgroup_q4_k_f32, 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[GGML_TYPE_Q5_K], matmul_id_subgroup_q5_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id);
    @@ -4977,6 +4981,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
             CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q8_0], matmul_q8_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
             CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q2_K], matmul_q2_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
             CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ2_0], matmul_tq2_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
    +        CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ1_0], matmul_tq1_0_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
             CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_K], matmul_q3_k_f32, mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
             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, , 0);
             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, , 0);
    @@ -5026,6 +5031,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
                 CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_subgroup_q8_0_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_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K], matmul_id_subgroup_q2_k_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_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_subgroup_tq2_0_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_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0], matmul_id_subgroup_tq1_0_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_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K], matmul_id_subgroup_q3_k_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_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K], matmul_id_subgroup_q4_k_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_Q5_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_K], matmul_id_subgroup_q5_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size);
    @@ -5074,6 +5080,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
                 CREATE_MM2(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0], matmul_id_q8_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM2(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K], matmul_id_q2_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM2(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0], matmul_id_tq2_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
    +            CREATE_MM2(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0], matmul_id_tq1_0_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM2(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K], matmul_id_q3_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM2(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K], matmul_id_q4_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM2(GGML_TYPE_Q5_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_K], matmul_id_q5_k_f32, mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
    @@ -5154,6 +5161,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
     
             CREATE_MM(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q2_K].f32acc, matmul_q2_k_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
             CREATE_MM(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ2_0].f32acc, matmul_tq2_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
    +        CREATE_MM(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat[GGML_TYPE_TQ1_0].f32acc, matmul_tq1_0_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
             CREATE_MM(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q3_K].f32acc, matmul_q3_k_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
             CREATE_MM(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q4_K].f32acc, matmul_q4_k_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
             CREATE_MM(GGML_TYPE_Q5_K, pipeline_dequant_mul_mat_mat[GGML_TYPE_Q5_K].f32acc, matmul_q5_k_f32, , mmq_wg_denoms, warptile_mmq, vk_mat_mat_push_constants, 3, , 0);
    @@ -5202,6 +5210,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
                 CREATE_MM(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0].f32acc, matmul_id_subgroup_q8_0_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_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K].f32acc, matmul_id_subgroup_q2_k_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_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0].f32acc, matmul_id_subgroup_tq2_0_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_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0].f32acc, matmul_id_subgroup_tq1_0_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_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K].f32acc, matmul_id_subgroup_q3_k_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_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K].f32acc, matmul_id_subgroup_q4_k_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_Q5_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_K].f32acc, matmul_id_subgroup_q5_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, mul_mat_subgroup_size);
    @@ -5232,6 +5241,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
                 CREATE_MM(GGML_TYPE_Q8_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q8_0].f32acc, matmul_id_q8_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM(GGML_TYPE_Q2_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q2_K].f32acc, matmul_id_q2_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM(GGML_TYPE_TQ2_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ2_0].f32acc, matmul_id_tq2_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
    +            CREATE_MM(GGML_TYPE_TQ1_0, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_TQ1_0].f32acc, matmul_id_tq1_0_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM(GGML_TYPE_Q3_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q3_K].f32acc, matmul_id_q3_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM(GGML_TYPE_Q4_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q4_K].f32acc, matmul_id_q4_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
                 CREATE_MM(GGML_TYPE_Q5_K, pipeline_dequant_mul_mat_mat_id[GGML_TYPE_Q5_K].f32acc, matmul_id_q5_k_f32, , mmq_wg_denoms, warptile_mmqid, vk_mat_mat_id_push_constants, mul_mat_id_param_count, _id, 0);
    @@ -5341,6 +5351,7 @@ 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_Q8_0][i], "mul_mat_vec_q8_0_f32_f32", arr_dmmv_q8_0_f32_f32_len[reduc], arr_dmmv_q8_0_f32_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size);
                 ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f32_f32[w][GGML_TYPE_Q2_K][i], "mul_mat_vec_q2_k_f32_f32", arr_dmmv_q2_k_f32_f32_len[reduc16], arr_dmmv_q2_k_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_TQ2_0][i], "mul_mat_vec_tq2_0_f32_f32", arr_dmmv_tq2_0_f32_f32_len[reduc16], arr_dmmv_tq2_0_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_TQ1_0][i], "mul_mat_vec_tq1_0_f32_f32", arr_dmmv_tq1_0_f32_f32_len[reduc16], arr_dmmv_tq1_0_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_K][i], "mul_mat_vec_q3_k_f32_f32", arr_dmmv_q3_k_f32_f32_len[reduc16], arr_dmmv_q3_k_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_K][i], "mul_mat_vec_q4_k_f32_f32", arr_dmmv_q4_k_f32_f32_len[reduc16], arr_dmmv_q4_k_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_Q5_K][i], "mul_mat_vec_q5_k_f32_f32", arr_dmmv_q5_k_f32_f32_len[reduc16], arr_dmmv_q5_k_f32_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, i+1}, 1, true, use_subgroups16, force_subgroup_size16);
    @@ -5369,6 +5380,7 @@ 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_Q8_0][i], "mul_mat_vec_q8_0_f16_f32", arr_dmmv_q8_0_f16_f32_len[reduc], arr_dmmv_q8_0_f16_f32_data[reduc], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq, i+1}, 1, true, use_subgroups, force_subgroup_size);
                 ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_f16_f32[w][GGML_TYPE_Q2_K][i], "mul_mat_vec_q2_k_f16_f32", arr_dmmv_q2_k_f16_f32_len[reduc16], arr_dmmv_q2_k_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_TQ2_0][i], "mul_mat_vec_tq2_0_f16_f32", arr_dmmv_tq2_0_f16_f32_len[reduc16], arr_dmmv_tq2_0_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_TQ1_0][i], "mul_mat_vec_tq1_0_f16_f32", arr_dmmv_tq1_0_f16_f32_len[reduc16], arr_dmmv_tq1_0_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_K][i], "mul_mat_vec_q3_k_f16_f32", arr_dmmv_q3_k_f16_f32_len[reduc16], arr_dmmv_q3_k_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_K][i], "mul_mat_vec_q4_k_f16_f32", arr_dmmv_q4_k_f16_f32_len[reduc16], arr_dmmv_q4_k_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, 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_Q5_K][i], "mul_mat_vec_q5_k_f16_f32", arr_dmmv_q5_k_f16_f32_len[reduc16], arr_dmmv_q5_k_f16_f32_data[reduc16], "main", mul_mat_vec_num_bindings, sizeof(vk_mat_vec_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq, i+1}, 1, true, use_subgroups16, force_subgroup_size16);
    @@ -5424,6 +5436,7 @@ 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_Q8_0], "mul_mat_vec_id_q8_0_f32",       arr_dmmv_id_q8_0_f32_f32_len[reduc],    arr_dmmv_id_q8_0_f32_f32_data[reduc],    "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {1*rm_stdq, 1, 1}, {wg_size_subgroup, 1*rm_stdq}, 1, true, use_subgroups, force_subgroup_size);
             ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q2_K], "mul_mat_vec_id_q2_k_f32",       arr_dmmv_id_q2_k_f32_f32_len[reduc16],    arr_dmmv_id_q2_k_f32_f32_data[reduc16],    "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq}, 1, true, use_subgroups16, force_subgroup_size16);
             ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_TQ2_0], "mul_mat_vec_id_tq2_0_f32",     arr_dmmv_id_tq2_0_f32_f32_len[reduc16],   arr_dmmv_id_tq2_0_f32_f32_data[reduc16],   "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq}, 1, true, use_subgroups16, force_subgroup_size16);
    +        ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_TQ1_0], "mul_mat_vec_id_tq1_0_f32",     arr_dmmv_id_tq1_0_f32_f32_len[reduc16],   arr_dmmv_id_tq1_0_f32_f32_data[reduc16],   "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq}, 1, true, use_subgroups16, force_subgroup_size16);
             ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q3_K], "mul_mat_vec_id_q3_k_f32",       arr_dmmv_id_q3_k_f32_f32_len[reduc16],    arr_dmmv_id_q3_k_f32_f32_data[reduc16],    "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq}, 1, true, use_subgroups16, force_subgroup_size16);
             ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q4_K], "mul_mat_vec_id_q4_k_f32",       arr_dmmv_id_q4_k_f32_f32_len[reduc16],    arr_dmmv_id_q4_k_f32_f32_data[reduc16],    "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq}, 1, true, use_subgroups16, force_subgroup_size16);
             ggml_vk_create_pipeline(device, device->pipeline_dequant_mul_mat_vec_id_f32[w][GGML_TYPE_Q5_K], "mul_mat_vec_id_q5_k_f32",       arr_dmmv_id_q5_k_f32_f32_len[reduc16],    arr_dmmv_id_q5_k_f32_f32_data[reduc16],    "main", mul_mat_vec_id_num_bindings, sizeof(vk_mat_vec_id_push_constants), {rm_kq, 1, 1}, {wg_size_subgroup16, rm_kq}, 1, true, use_subgroups16, force_subgroup_size16);
    @@ -5490,6 +5503,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
         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);
         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_TQ1_0], "dequant_tq1_0", dequant_tq1_0_len, dequant_tq1_0_data, "main", 2, 5 * sizeof(uint32_t), {256 * 4, 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);
         ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q4_K], "dequant_q4_k", dequant_q4_k_len, dequant_q4_k_data, "main", 2, 5 * sizeof(uint32_t), {256 * 32, 1, 1}, {}, 1);
         ggml_vk_create_pipeline(device, device->pipeline_dequant[GGML_TYPE_Q5_K], "dequant_q5_k", dequant_q5_k_len, dequant_q5_k_data, "main", 2, 5 * sizeof(uint32_t), {256 * 64, 1, 1}, {}, 1);
    @@ -5519,6 +5533,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
         ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_Q8_0], "get_rows_q8_0", get_rows_q8_0_len, get_rows_q8_0_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1);
         ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_Q2_K], "get_rows_q2_k", get_rows_q2_k_len, get_rows_q2_k_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1);
         ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_TQ2_0], "get_rows_tq2_0", get_rows_tq2_0_len, get_rows_tq2_0_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1);
    +    ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_TQ1_0], "get_rows_tq1_0", get_rows_tq1_0_len, get_rows_tq1_0_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_K], "get_rows_q3_k", get_rows_q3_k_len, get_rows_q3_k_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_K], "get_rows_q4_k", get_rows_q4_k_len, get_rows_q4_k_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1);
         ggml_vk_create_pipeline(device, device->pipeline_get_rows[GGML_TYPE_Q5_K], "get_rows_q5_k", get_rows_q5_k_len, get_rows_q5_k_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1);
    @@ -5548,6 +5563,7 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
         ggml_vk_create_pipeline(device, device->pipeline_get_rows_f32[GGML_TYPE_Q8_0], "get_rows_q8_0_f32", get_rows_q8_0_f32_len, get_rows_q8_0_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_Q2_K], "get_rows_q2_k_f32", get_rows_q2_k_f32_len, get_rows_q2_k_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_TQ2_0], "get_rows_tq2_0_f32", get_rows_tq2_0_f32_len, get_rows_tq2_0_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_TQ1_0], "get_rows_tq1_0_f32", get_rows_tq1_0_f32_len, get_rows_tq1_0_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_K], "get_rows_q3_k_f32", get_rows_q3_k_f32_len, get_rows_q3_k_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_K], "get_rows_q4_k_f32", get_rows_q4_k_f32_len, get_rows_q4_k_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_Q5_K], "get_rows_q5_k_f32", get_rows_q5_k_f32_len, get_rows_q5_k_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {1024, 1, 1}, {}, 1);
    @@ -7787,6 +7803,7 @@ static vk_pipeline ggml_vk_get_to_fp16(ggml_backend_vk_context * ctx, ggml_type
             case GGML_TYPE_MXFP4:
             case GGML_TYPE_NVFP4:
             case GGML_TYPE_TQ2_0:
    +        case GGML_TYPE_TQ1_0:
                 break;
             default:
                 return nullptr;
    @@ -7862,6 +7879,7 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_pipeline(ggml_backend_vk_conte
             case GGML_TYPE_MXFP4:
             case GGML_TYPE_NVFP4:
             case GGML_TYPE_TQ2_0:
    +        case GGML_TYPE_TQ1_0:
                 break;
             default:
                 return nullptr;
    @@ -7932,6 +7950,7 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec(ggml_backend_vk_context *
             case GGML_TYPE_MXFP4:
             case GGML_TYPE_NVFP4:
             case GGML_TYPE_TQ2_0:
    +        case GGML_TYPE_TQ1_0:
                 break;
             default:
                 return nullptr;
    @@ -8026,6 +8045,7 @@ static vk_matmul_pipeline ggml_vk_get_mul_mat_mat_id_pipeline(ggml_backend_vk_co
             case GGML_TYPE_MXFP4:
             case GGML_TYPE_NVFP4:
             case GGML_TYPE_TQ2_0:
    +        case GGML_TYPE_TQ1_0:
                 break;
             default:
                 return nullptr;
    @@ -8099,6 +8119,7 @@ static vk_pipeline ggml_vk_get_dequantize_mul_mat_vec_id(ggml_backend_vk_context
             case GGML_TYPE_MXFP4:
             case GGML_TYPE_NVFP4:
             case GGML_TYPE_TQ2_0:
    +        case GGML_TYPE_TQ1_0:
                 break;
             default:
                 return nullptr;
    @@ -18673,6 +18694,7 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm
                         case GGML_TYPE_MXFP4:
                         case GGML_TYPE_NVFP4:
                         case GGML_TYPE_TQ2_0:
    +                    case GGML_TYPE_TQ1_0:
                             break;
                         default:
                             return false;
    @@ -18779,6 +18801,7 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm
                         case GGML_TYPE_MXFP4:
                         case GGML_TYPE_NVFP4:
                         case GGML_TYPE_TQ2_0:
    +                    case GGML_TYPE_TQ1_0:
                         case GGML_TYPE_I32:
                             return true;
                         default:
    diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl
    index 627932bd3..9df66cb44 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs.glsl
    @@ -608,6 +608,21 @@ vec2 get_dm(uint ib, uint a_offset) {
     }
     #endif
     
    +#if defined(DATA_A_TQ1_0)
    +float tq1_0_val(uint ib, uint e, uint a_offset) {
    +    const uint bidx = tq1_0_byte_of(e);
    +    const uint qbyte = uint(bidx < 48u ? data_a[a_offset + ib].qs[bidx]
    +                                       : data_a[a_offset + ib].qh[bidx - 48u]);
    +    return float(tq1_0_trit(qbyte, tq1_0_digit_of(e))) - 1.0;
    +}
    +vec2 dequantize(uint ib, uint iqs, uint a_offset) {
    +    return vec2(tq1_0_val(ib, iqs, a_offset), tq1_0_val(ib, iqs + 1u, a_offset));
    +}
    +vec2 get_dm(uint ib, uint a_offset) {
    +    return vec2(float(data_a[a_offset + ib].d), 0);
    +}
    +#endif
    +
     #if defined(DATA_A_TQ2_0)
     vec2 dequantize(uint ib, uint iqs, uint a_offset) {
         // elem e -> byte qs[(e/128)*32 + e%32], bits 2*((e%128)/32); w = q - 1 (d applied via get_dm)
    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 46cc69cb2..ef53264a7 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_funcs_cm2.glsl
    @@ -247,6 +247,19 @@ f16vec4 dequantFuncQ8_0_v(const in decodeBufQ8_0 bl, const in uint blockCoords[2
         return f16vec4(vec4(qi) * vec4(float(d)));
     }
     
    +layout(buffer_reference, std430, buffer_reference_align = 2) buffer decodeBufTQ1_0 {
    +   block_tq1_0 block;
    +};
    +
    +float16_t dequantFuncTQ1_0(const in decodeBufTQ1_0 bl, const in uint blockCoords[2], const in uint coordInBlock[2])
    +{
    +    const uint e = coordInBlock[1];
    +    const uint bidx = tq1_0_byte_of(e);
    +    const uint qbyte = uint(bidx < 48u ? bl.block.qs[bidx] : bl.block.qh[bidx - 48u]);
    +    const uint xi = tq1_0_trit(qbyte, tq1_0_digit_of(e));
    +    return bl.block.d * (float16_t(int(xi)) - float16_t(1.0));
    +}
    +
     layout(buffer_reference, std430, buffer_reference_align = 2) buffer decodeBufTQ2_0 {
        block_tq2_0 block;
     };
    @@ -1406,6 +1419,8 @@ f16vec4 dequantFuncNVFP4_v(const in decodeBufNVFP4 bl, const in uint blockCoords
     #elif defined(DATA_A_Q8_0)
     #define dequantFuncA dequantFuncQ8_0
     #define dequantFuncA_v dequantFuncQ8_0_v
    +#elif defined(DATA_A_TQ1_0)
    +#define dequantFuncA dequantFuncTQ1_0
     #elif defined(DATA_A_TQ2_0)
     #define dequantFuncA dequantFuncTQ2_0
     #define dequantFuncA_v dequantFuncTQ2_0_v
    diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq1_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq1_0.comp
    new file mode 100644
    index 000000000..1632e7463
    --- /dev/null
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dequant_tq1_0.comp
    @@ -0,0 +1,28 @@
    +#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_tq1_0 data_a[];};
    +layout (binding = 1) writeonly buffer D {D_TYPE data_b[];};
    +
    +void main() {
    +    const uint i = gl_GlobalInvocationID.x * 4;
    +
    +    if (i >= p.nel) {
    +        return;
    +    }
    +
    +    const uint ib = i / QUANT_K_TQ1_0;
    +    const float d = float(data_a[ib].d);
    +
    +    [[unroll]] for (uint j = 0; j < 4 && (i + j) < p.nel; ++j) {
    +        const uint e = (i + j) % QUANT_K_TQ1_0;
    +        const uint bidx = tq1_0_byte_of(e);
    +        const uint qbyte = uint(bidx < 48u ? data_a[ib].qs[bidx]
    +                                           : data_a[ib].qh[bidx - 48u]);
    +        const uint xi = tq1_0_trit(qbyte, tq1_0_digit_of(e));
    +        data_b[i + j] = D_TYPE(d * (float(xi) - 1.0f));
    +    }
    +}
    diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0.comp b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0.comp
    new file mode 100644
    index 000000000..2c99a268e
    --- /dev/null
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec_tq1_0.comp
    @@ -0,0 +1,85 @@
    +#version 450
    +#extension GL_EXT_shader_explicit_arithmetic_types : require
    +
    +#include "mul_mat_vec_base.glsl"
    +
    +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in;
    +
    +FLOAT_TYPE temp[NUM_COLS][NUM_ROWS];
    +
    +// Walks the packed bytes directly (byte m, digit t) rather than via
    +// tq1_0_byte_of()/tq1_0_digit_of(): one byte per thread, expanded in place.
    +void compute_outputs(const uint32_t first_row, const uint32_t num_rows) {
    +    uint a_offset, b_offset, d_offset;
    +    get_offsets(a_offset, b_offset, d_offset);
    +
    +    const uint num_blocks_per_row = p.ncols / QUANT_K;
    +    const uint tid = gl_LocalInvocationID.x;
    +
    +    [[unroll]] for (uint j = 0; j < NUM_COLS; ++j) {
    +        [[unroll]] for (uint i = 0; i < NUM_ROWS; ++i) {
    +            temp[j][i] = FLOAT_TYPE(0);
    +        }
    +    }
    +
    +    for (uint nrow = 0; nrow < num_rows; ++nrow) {
    +        const uint ib0 = a_offset + (first_row + nrow) * num_blocks_per_row;
    +        for (uint jcol = 0; jcol < NUM_COLS; ++jcol) {
    +            const uint b_base = (jcol * p.batch_stride_b);
    +            for (uint i = tid/8; i < num_blocks_per_row; i += gl_WorkGroupSize.x/8) {
    +                const FLOAT_TYPE d = float(data_a[ib0 + i].d);
    +
    +                // First qs chunk: 32 bytes (5*32 elements)
    +                [[unroll]] for (uint m = tid%8; m < 32; m += 8) {
    +                    const uint q_byte = uint(data_a[ib0 + i].qs[m]);
    +                    [[unroll]] for (uint t = 0; t < 5; ++t) {
    +                        const uint xi = tq1_0_trit(q_byte, t);
    +                        const FLOAT_TYPE dequant_val = FLOAT_TYPE(d * (float(xi) - 1.0f));
    +                        const uint elem = t * 32u + m;
    +                        const uint b_idx = i * QUANT_K + elem;
    +                        temp[jcol][nrow] += dequant_val * FLOAT_TYPE(data_b[b_base + b_offset + b_idx]);
    +                    }
    +                }
    +
    +                // Second qs chunk: 16 bytes (5*16 elements)
    +                [[unroll]] for (uint m = tid%8; m < 16; m += 8) {
    +                    const uint q_byte = uint(data_a[ib0 + i].qs[32u + m]);
    +                    [[unroll]] for (uint t = 0; t < 5; ++t) {
    +                        const uint xi = tq1_0_trit(q_byte, t);
    +                        const FLOAT_TYPE dequant_val = FLOAT_TYPE(d * (float(xi) - 1.0f));
    +                        const uint elem = 160u + t * 16u + m;
    +                        const uint b_idx = i * QUANT_K + elem;
    +                        temp[jcol][nrow] += dequant_val * FLOAT_TYPE(data_b[b_base + b_offset + b_idx]);
    +                    }
    +                }
    +
    +                // qh bytes: 4 bytes (4*4 elements)
    +                [[unroll]] for (uint j = tid%8; j < 4; j += 8) {
    +                    const uint qh_byte = uint(data_a[ib0 + i].qh[j]);
    +                    [[unroll]] for (uint t = 0; t < 4; ++t) {
    +                        const uint xi = tq1_0_trit(qh_byte, t);
    +                        const FLOAT_TYPE dequant_val = FLOAT_TYPE(d * (float(xi) - 1.0f));
    +                        const uint elem = 240u + t * 4u + j;
    +                        const uint b_idx = i * QUANT_K + elem;
    +                        temp[jcol][nrow] += dequant_val * FLOAT_TYPE(data_b[b_base + b_offset + b_idx]);
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    reduce_result(temp, d_offset, first_row, num_rows, tid);
    +}
    +
    +void main() {
    +    const uint first_row = NUM_ROWS * (gl_WorkGroupID.x + gl_NumWorkGroups.x * gl_WorkGroupID.z);
    +
    +    if (first_row + NUM_ROWS <= p.stride_d) {
    +        compute_outputs(first_row, NUM_ROWS);
    +    } else {
    +        if (first_row >= p.stride_d) {
    +            return;
    +        }
    +        compute_outputs(first_row, p.stride_d - first_row);
    +    }
    +}
    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 7d852dced..bdc70af14 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl
    @@ -197,6 +197,24 @@ void load_a_to_shmem(const uint pos_a, const uint row, const uint col, const uin
                 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_TQ1_0)
    +            const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row;
    +
    +            const uint ib  = idx / 128;               // 2 values per idx
    +            const uint iqs = (idx % 128) * 2;         // element 0,2,4..254
    +
    +            const float d = float(data_a[ib].d);
    +            vec2 v;
    +            for (uint kk = 0u; kk < 2u; ++kk) {
    +                const uint e = iqs + kk;
    +                const uint bidx = tq1_0_byte_of(e);
    +                const uint qbyte = uint(bidx < 48u ? data_a[ib].qs[bidx]
    +                                                   : data_a[ib].qh[bidx - 48u]);
    +                v[kk] = d * (float(tq1_0_trit(qbyte, tq1_0_digit_of(e))) - 1.0);
    +            }
    +
    +            const uint k_pair = row * LOAD_VEC_A / 2;
    +            store_a(col, k_pair, FLOAT_TYPEV2(v.xy));
     #elif defined(DATA_A_TQ2_0)
                 const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row;
     
    diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl b/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl
    index adb1bb8b3..a19c7f2f4 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/types.glsl
    @@ -303,6 +303,41 @@ struct block_q2_K_packed32
     #define DATA_A_QUANT_K
     #endif
     
    +#define QUANT_K_TQ1_0 256
    +
    +// TQ1_0: base-3 packed trits, 5 per byte in `qs` (48B) and 4 in `qh` (4B).
    +struct block_tq1_0
    +{
    +    uint8_t qs[(QUANT_K_TQ1_0 - 4 * QUANT_K_TQ1_0 / 64) / 5];
    +    uint8_t qh[QUANT_K_TQ1_0 / 64];
    +    float16_t d;
    +};
    +
    +// Element e in [0,255] -> its packed byte (0..47 qs, 48..51 qh) and digit.
    +uint tq1_0_byte_of(uint e) {
    +    return e < 160u ? (e % 32u)
    +         : e < 240u ? 32u + ((e - 160u) % 16u)
    +         : 48u + ((e - 240u) % 4u);
    +}
    +uint tq1_0_digit_of(uint e) {
    +    return e < 160u ? (e / 32u)
    +         : e < 240u ? ((e - 160u) / 16u)
    +         : ((e - 240u) / 4u);
    +}
    +// The 8-bit truncation below is part of the format, not an optimisation:
    +// the C reference does `uint8_t q = qs[..] * pow3[n]`.
    +uint tq1_0_trit(uint qbyte, uint t) {
    +    const uint POW3_PACKED = (1u << 28) | (3u << 21) | (9u << 14) | (27u << 7) | 81u;
    +    return ((((qbyte * ((POW3_PACKED >> (7u * (4u - t))) & 0x7Fu)) & 255u) * 3u) >> 8);
    +}
    +
    +#if defined(DATA_A_TQ1_0)
    +#define QUANT_K QUANT_K_TQ1_0
    +#define QUANT_R 1
    +#define A_TYPE block_tq1_0
    +#define DATA_A_QUANT_K
    +#endif
    +
     #define QUANT_K_TQ2_0 256
     
     // ternary (BitNet): 2-bit codes, w = (q - 1) * d; qs layout matches q2_K's
    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 27ff68c10..5d4b9c5fa 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp
    @@ -72,6 +72,7 @@ const std::vector type_names = {
         "iq4_nl",
         "mxfp4",
         "nvfp4",
    +    "tq1_0",
         "tq2_0",
         "bf16",
     };
    @@ -734,7 +735,7 @@ void process_shaders() {
         for (const auto& tname : type_names) {
             // mul mat vec
             std::string data_a_key = "DATA_A_" + to_uppercase(tname);
    -        std::string shader = (string_ends_with(tname, "_k") || string_starts_with(tname, "iq1_") || string_starts_with(tname, "iq2_") || string_starts_with(tname, "iq3_") || tname == "tq2_0") ? "mul_mat_vec_" + tname + ".comp" : "mul_mat_vec.comp";
    +        std::string shader = (string_ends_with(tname, "_k") || string_starts_with(tname, "iq1_") || string_starts_with(tname, "iq2_") || string_starts_with(tname, "iq3_") || tname == "tq2_0" || tname == "tq1_0") ? "mul_mat_vec_" + tname + ".comp" : "mul_mat_vec.comp";
     
             string_to_spv("mul_mat_vec_" + tname + "_f32_f32", shader, merge_maps(base_dict, {{data_a_key, "1"}, {"B_TYPE", "float"}, {"B_TYPEV2", "vec2"}, {"B_TYPEV4", "vec4"}, {"D_TYPE", "float"}}));
             string_to_spv("mul_mat_vec_" + tname + "_f16_f32", shader, merge_maps(base_dict, {{data_a_key, "1"}, {"B_TYPE", "float16_t"}, {"B_TYPEV2", "f16vec2"}, {"B_TYPEV4", "f16vec4"}, {"D_TYPE", "float"}}));
    diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp
    index c93e41b2e..31593541f 100644
    --- a/tests/test-backend-ops.cpp
    +++ b/tests/test-backend-ops.cpp
    @@ -8646,7 +8646,7 @@ static const ggml_type all_types[] = {
         GGML_TYPE_Q4_K, GGML_TYPE_Q5_K,
         GGML_TYPE_Q6_K,
         GGML_TYPE_TQ2_0,
    -    // GGML_TYPE_TQ1_0, // TODO: implement for all backends
    +    GGML_TYPE_TQ1_0,
         GGML_TYPE_IQ2_XXS, GGML_TYPE_IQ2_XS, GGML_TYPE_IQ2_S,
         GGML_TYPE_IQ3_XXS, GGML_TYPE_IQ1_S, GGML_TYPE_IQ1_M,
         GGML_TYPE_IQ4_NL, GGML_TYPE_IQ3_S, GGML_TYPE_IQ4_XS,
    @@ -8674,7 +8674,7 @@ static const ggml_type other_types[] = {
         GGML_TYPE_Q5_K,
         GGML_TYPE_Q6_K,
         GGML_TYPE_TQ2_0,
    -    // GGML_TYPE_TQ1_0, // TODO: implement for all backends
    +    GGML_TYPE_TQ1_0,
         GGML_TYPE_IQ2_XS, GGML_TYPE_IQ2_S,
         GGML_TYPE_IQ3_XXS, GGML_TYPE_IQ1_S, GGML_TYPE_IQ1_M,
         GGML_TYPE_IQ4_NL, GGML_TYPE_IQ3_S, GGML_TYPE_IQ4_XS,
    @@ -9815,6 +9815,11 @@ static std::vector> make_test_cases_eval() {
         test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_MXFP4, GGML_TYPE_F32, 32, 2, false, 2880, 32, 2880));
         test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_Q4_0, GGML_TYPE_F32, 32, 2, false, 2880, 32, 2880));
     
    +    // multiple blocks per row: exercises the block-stride loop and the
    +    // per-expert base offset, which k == 256 alone leaves untested
    +    test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 28, 10, false, 1024, 1, 4096));
    +    test_cases.emplace_back(new test_mul_mat_id(GGML_TYPE_TQ1_0, GGML_TYPE_F32, 128, 8, false, 1024, 1, 2048));
    +
         for (ggml_type type_a : all_types) {
             test_cases.emplace_back(new test_mul_mat_id(type_a, GGML_TYPE_F32, 4, 2, false, 64, 16, 3*ggml_blck_size(type_a)));
         }
    
    From 2092353c8b828105f3f47e40e094ed3153ce0531 Mon Sep 17 00:00:00 2001
    From: Daniel Bevenius 
    Date: Mon, 7 Sep 2026 07:23:39 +0200
    Subject: [PATCH 14/31] ci : add container image checking and tagging (wip)
     (#28394)
    
    This commit contains a suggestion for handling container images which
    are currently not semver tagged, they only have build numbers in there
    tags.
    
    The proposed solution here is to first add a check to make sure that
    there are container images built for the build number of the release and
    if not fail the build. The container images are build nightly but they
    can be triggered manually as well.
    If the the container images check passes then the make-release workflow
    will re-tag the images with the semver.
    ---
     .github/workflows/make-release.yml | 24 ++++++++++++++++
     scripts/make-release-checks.sh     | 45 ++++++++++++++++++++++++++++++
     2 files changed, 69 insertions(+)
    
    diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml
    index 40fc86287..6644a80cc 100644
    --- a/.github/workflows/make-release.yml
    +++ b/.github/workflows/make-release.yml
    @@ -19,6 +19,7 @@ env:
     
     permissions:
       contents: write
    +  packages: write
     
     jobs:
       make-release:
    @@ -113,6 +114,29 @@ jobs:
                   data: await fs.readFileSync('./nightly-tag.txt')
                 });
     
    +      - name: Re-tag container images with release version
    +        if: ${{ github.event.inputs.dry_run == 'false' && steps.desc.outputs.nightly_tag != '' }}
    +        env:
    +          GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
    +        run: |
    +          VERSION="${{ steps.checks.outputs.version }}"
    +          NIGHTLY_TAG="${{ steps.desc.outputs.nightly_tag }}"
    +          REPO_OWNER="${GITHUB_REPOSITORY_OWNER,,}"
    +          IMAGE_REPO="ghcr.io/${REPO_OWNER}/${{ github.event.repository.name }}"
    +
    +          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
    +
    +          VARIANTS=("" "-cuda" "-cuda13" "-vulkan" "-rocm" "-intel" "-musa" "-openvino")
    +          TYPES=("full" "light" "server")
    +          for type in "${TYPES[@]}"; do
    +            for variant in "${VARIANTS[@]}"; do
    +              src="${IMAGE_REPO}:${type}${variant}-${NIGHTLY_TAG}"
    +              dst="${IMAGE_REPO}:${type}${variant}-${VERSION}"
    +              echo "Tagging ${src} -> ${dst}"
    +              docker buildx imagetools create --tag "${dst}" "${src}"
    +            done
    +          done
    +
           - name: Dry run summary
             if: ${{ github.event.inputs.dry_run == 'true' }}
             run: |
    diff --git a/scripts/make-release-checks.sh b/scripts/make-release-checks.sh
    index bc575e5a4..32c193745 100755
    --- a/scripts/make-release-checks.sh
    +++ b/scripts/make-release-checks.sh
    @@ -120,6 +120,51 @@ else
         fi
     fi
     
    +echo "Checking container images for commit ${SHA}..."
    +NIGHTLY_TAG="$(git tag --points-at "${SHA}" | grep -E '(^|-)b[0-9]+(-[0-9a-f]{7})?$' | head -n 1 || true)"
    +if [[ -z "${NIGHTLY_TAG}" ]]; then
    +    echo "Warning: no nightly tag points at ${SHA} - skipping container image check"
    +elif [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
    +    echo "Warning: GITHUB_REPOSITORY not set - skipping container image check (local run)"
    +else
    +    CONTAINER_REPO="${GITHUB_REPOSITORY,,}"  # lower-case owner/repo for ghcr.io
    +    GHCR_TOKEN="$(curl -fsSL \
    +        "https://ghcr.io/token?scope=repository:${CONTAINER_REPO}:pull&service=ghcr.io" \
    +        | grep -oP '"token"\s*:\s*"\K[^"]+')"
    +
    +    VARIANTS=("" "-cuda" "-cuda13" "-vulkan" "-rocm" "-intel" "-musa" "-openvino")
    +    TYPES=("full" "light" "server")
    +    CONTAINER_ERR=""
    +    for type in "${TYPES[@]}"; do
    +        for variant in "${VARIANTS[@]}"; do
    +            tag="${type}${variant}-${NIGHTLY_TAG}"
    +            STATUS="$(curl -s -o /dev/null -w "%{http_code}" \
    +                -H "Authorization: Bearer ${GHCR_TOKEN}" \
    +                -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" \
    +                "https://ghcr.io/v2/${CONTAINER_REPO}/manifests/${tag}")"
    +            if [[ "${STATUS}" == "200" ]]; then
    +                echo "  ${tag} - OK"
    +            else
    +                echo "  ${tag} - MISSING"
    +                CONTAINER_ERR+=" ${tag}"
    +            fi
    +        done
    +    done
    +
    +    if [[ -n "${CONTAINER_ERR}" ]]; then
    +        if [[ "$DRY_RUN" == "true" ]]; then
    +            echo "Warning: missing container images for ${NIGHTLY_TAG}:${CONTAINER_ERR} (dry run, continuing)."
    +            CHECKS_PASSED=false
    +        else
    +            echo "Error: missing container images for ${NIGHTLY_TAG}:${CONTAINER_ERR}"
    +            echo "The Docker workflow must complete successfully before making a release."
    +            exit 1
    +        fi
    +    else
    +        echo "All container images found for ${NIGHTLY_TAG} - OK"
    +    fi
    +fi
    +
     if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
         echo "checks_passed=${CHECKS_PASSED}" >> "$GITHUB_OUTPUT"
     fi
    
    From 9ac8c408a33b04396880deba4a9f44470ff12156 Mon Sep 17 00:00:00 2001
    From: Jeff Bolz 
    Date: Mon, 7 Sep 2026 01:08:28 -0500
    Subject: [PATCH 15/31] vulkan: rms_norm fusion opportunities (#28024)
    
    Support RMS_NORM + MUL + ADD (+ MUL) and RMS_NORM + VIEW + SET_ROWS.
    Extend ROPE + VIEW + SET_ROWS to support IMROPE.
    
    Worth around 4% in gemma4 on my system.
    ---
     ggml/src/ggml-vulkan/ggml-vulkan.cpp          | 350 +++++++++++++++---
     .../ggml-vulkan/vulkan-shaders/rms_norm.comp  |  32 +-
     .../vulkan-shaders/rms_norm_partials.comp     |  26 +-
     .../vulkan-shaders/vulkan-shaders-gen.cpp     |   4 +
     tests/test-backend-ops.cpp                    | 148 +++++---
     5 files changed, 454 insertions(+), 106 deletions(-)
    
    diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    index efadd3663..9b47c6c95 100644
    --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    @@ -671,6 +671,11 @@ static constexpr std::initializer_list> topk_qsa_edges {
         { 5, 1, 4 }, // add->src[1]     == reshape
         { 6, 0, 5 }, // top_k->src[0]   == add
     };
    +static constexpr std::initializer_list rms_norm_mul_add_mul_pattern { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ADD, GGML_OP_MUL };
    +static constexpr std::initializer_list rms_norm_mul_add_pattern     { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ADD };
    +static constexpr std::initializer_list rms_norm_mul_rope_view_set_rows_pattern { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ROPE, GGML_OP_VIEW, GGML_OP_SET_ROWS };
    +static constexpr std::initializer_list rms_norm_view_set_rows_pattern { GGML_OP_RMS_NORM, GGML_OP_VIEW, GGML_OP_SET_ROWS };
    +static constexpr std::initializer_list rope_view_set_rows_pattern { GGML_OP_ROPE, GGML_OP_VIEW, GGML_OP_SET_ROWS };
     
     //node #978 (  SOFT_MAX):     ffn_moe_probs-15 (   0K) [Vulka         ] use=2:    ffn_moe_logits-15 (   0K) [Vulka         ]
     //node #979 (   RESHAPE): ffn_moe_probs-15 (re (   0K) [Vulka         ] use=1:     ffn_moe_probs-15 (   0K) [Vulka         ]
    @@ -770,6 +775,16 @@ enum topk_moe_mode {
         TOPK_MOE_COUNT,
     };
     
    +enum rms_norm_mode {
    +    RMS_NORM_MUL,
    +    RMS_NORM_MUL_ADD,
    +    RMS_NORM_MUL_ADD_MUL,
    +    RMS_NORM_MUL_ROPE,
    +    RMS_NORM_MUL_ROPE_VIEW_SET_ROWS,
    +    RMS_NORM_VIEW_SET_ROWS,
    +    RMS_NORM_COUNT,
    +};
    +
     static constexpr std::initializer_list> rope_view_set_rows_edges {
         { 1, 0, 0 }, // view->src[0]     == rope
         { 2, 0, 1 }, // set_rows->src[0] == view
    @@ -782,6 +797,11 @@ static constexpr std::initializer_list> rms_norm_mul_rope_vie
         { 4, 0, 3 }, // set_rows->src[0] == view
     };
     
    +static constexpr std::initializer_list> rms_norm_view_set_rows_edges {
    +    { 1, 0, 0 }, // view->src[0]     == rms_norm
    +    { 2, 0, 1 }, // set_rows->src[0] == view
    +};
    +
     static constexpr std::array lightning_indexer_k_types = {
         GGML_TYPE_F32,
         GGML_TYPE_F16,
    @@ -1002,6 +1022,12 @@ struct vk_device_struct {
         vk_pipeline pipeline_group_norm_f32;
         vk_pipeline pipeline_rms_norm_f32;
         vk_pipeline pipeline_rms_norm_mul_f32;
    +    vk_pipeline pipeline_rms_norm_mul_add_f32;
    +    vk_pipeline pipeline_rms_norm_mul_add_mul_f32;
    +    vk_pipeline pipeline_rms_norm_mul_add_partials_f32;
    +    vk_pipeline pipeline_rms_norm_mul_add_mul_partials_f32;
    +    vk_pipeline pipeline_rms_norm_set_rows_f32_f32;
    +    vk_pipeline pipeline_rms_norm_set_rows_f32_f16;
         vk_pipeline pipeline_rms_norm_partials_f32;
         vk_pipeline pipeline_rms_norm_mul_partials_f32;
         vk_pipeline pipeline_rms_norm_mul_rope_f32_f32;
    @@ -2467,6 +2493,7 @@ struct ggml_backend_vk_context {
         bool fused_topk_moe_scale {};
         // QSA indexer gather+add+top_k fused into one radix-select
         bool fused_topk_qsa {};
    +    rms_norm_mode fused_rms_norm_mode {RMS_NORM_COUNT};
     
         // for GGML_VK_PERF_LOGGER
         std::unique_ptr perf_logger;
    @@ -5609,6 +5636,12 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
     
         ggml_vk_create_pipeline(device, device->pipeline_rms_norm_f32, "rms_norm_f32", rms_norm_f32_len, rms_norm_f32_data, "main", 4, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 0}, 1, true);
         ggml_vk_create_pipeline(device, device->pipeline_rms_norm_mul_f32, "rms_norm_mul_f32", rms_norm_f32_len, rms_norm_f32_data, "main", 4, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 1}, 1, true);
    +    ggml_vk_create_pipeline(device, device->pipeline_rms_norm_mul_add_f32, "rms_norm_mul_add_f32", rms_norm_mul_add_f32_len, rms_norm_mul_add_f32_data, "main", 5, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 1, 0}, 1, true);
    +    ggml_vk_create_pipeline(device, device->pipeline_rms_norm_mul_add_mul_f32, "rms_norm_mul_add_mul_f32", rms_norm_mul_add_f32_len, rms_norm_mul_add_f32_data, "main", 5, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 1, 1}, 1, true);
    +    ggml_vk_create_pipeline(device, device->pipeline_rms_norm_mul_add_partials_f32, "rms_norm_mul_add_partials_f32", rms_norm_mul_add_partials_f32_len, rms_norm_mul_add_partials_f32_data, "main", 6, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 1, 0}, 1, true);
    +    ggml_vk_create_pipeline(device, device->pipeline_rms_norm_mul_add_mul_partials_f32, "rms_norm_mul_add_mul_partials_f32", rms_norm_mul_add_partials_f32_len, rms_norm_mul_add_partials_f32_data, "main", 6, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 1, 1}, 1, true);
    +    ggml_vk_create_pipeline(device, device->pipeline_rms_norm_set_rows_f32_f32, "rms_norm_set_rows_f32_f32", rms_norm_set_rows_f32_f32_len, rms_norm_set_rows_f32_f32_data, "main", 4, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 0}, 1, true);
    +    ggml_vk_create_pipeline(device, device->pipeline_rms_norm_set_rows_f32_f16, "rms_norm_set_rows_f32_f16", rms_norm_set_rows_f32_f16_len, rms_norm_set_rows_f32_f16_data, "main", 4, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 0}, 1, true);
         ggml_vk_create_pipeline(device, device->pipeline_rms_norm_partials_f32, "rms_norm_partials_f32", rms_norm_partials_f32_len, rms_norm_partials_f32_data, "main", 4, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 0}, 1, true);
         ggml_vk_create_pipeline(device, device->pipeline_rms_norm_mul_partials_f32, "rms_norm_mul_partials_f32", rms_norm_partials_f32_len, rms_norm_partials_f32_data, "main", 4, sizeof(vk_op_binary_push_constants), {1, 1, 1}, {0, 1}, 1, true);
     
    @@ -11551,10 +11584,9 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const
         case GGML_OP_RMS_NORM:
             if (src0->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) {
                 if (ctx->do_add_rms_partials) {
    -                return ctx->num_additional_fused_ops > 0 ? ctx->device->pipeline_rms_norm_mul_partials_f32 : ctx->device->pipeline_rms_norm_partials_f32;
    -            } else {
    -                return ctx->num_additional_fused_ops > 0 ? ctx->device->pipeline_rms_norm_mul_f32 : ctx->device->pipeline_rms_norm_f32;
    +                return ctx->fused_rms_norm_mode == RMS_NORM_MUL ? ctx->device->pipeline_rms_norm_mul_partials_f32 : ctx->device->pipeline_rms_norm_partials_f32;
                 }
    +            return ctx->fused_rms_norm_mode == RMS_NORM_MUL ? ctx->device->pipeline_rms_norm_mul_f32 : ctx->device->pipeline_rms_norm_f32;
             }
             return nullptr;
         case GGML_OP_RMS_NORM_BACK:
    @@ -13500,40 +13532,121 @@ static vk_op_rope_push_constants ggml_vk_make_rope_constants(const ggml_tensor *
         return rope;
     }
     
    -static void ggml_vk_rms_norm(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx, float * op_params) {
    -    ggml_tensor * dst;
    -    const ggml_tensor * src0;
    -    const ggml_tensor * src1;
    -
    -    if (ctx->num_additional_fused_ops > 0) {
    -        // fused rms_norm + mul
    -        ggml_tensor *mul = cgraph->nodes[node_idx + 1];
    -        ggml_tensor *other_src = mul->src[0] == cgraph->nodes[node_idx + 0] ? mul->src[1] : mul->src[0];
    -        dst = mul;
    -        src0 = cgraph->nodes[node_idx]->src[0];
    -        src1 = other_src;
    -    } else {
    -        dst = cgraph->nodes[node_idx];
    -        src0 = src1 = dst->src[0];
    -    }
    -
    +static vk_op_binary_push_constants ggml_vk_rms_norm_push_constants(
    +        const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * dst,
    +        float eps, uint32_t num_partials) {
         const uint32_t src0_type_size = ggml_type_size(src0->type);
         const uint32_t src1_type_size = ggml_type_size(src1->type);
         const uint32_t dst_type_size = ggml_type_size(dst->type);
     
    -    uint32_t param3 = ctx->do_add_rms_partials ? ggml_vk_rms_num_partials(ctx, dst) : 0;
    -
    -    vk_op_binary_push_constants bin {
    +    return {
             (uint32_t)ggml_nelements(src0),
             (uint32_t)src0->ne[0], (uint32_t)src0->ne[1], (uint32_t)src0->ne[2],(uint32_t)src0->ne[3], (uint32_t)src0->nb[0] / src0_type_size, (uint32_t)src0->nb[1] / src0_type_size, (uint32_t)src0->nb[2] / src0_type_size, (uint32_t)src0->nb[3] / src0_type_size,
             (uint32_t)src1->ne[0], (uint32_t)src1->ne[1], (uint32_t)src1->ne[2],(uint32_t)src1->ne[3], (uint32_t)src1->nb[0] / src1_type_size, (uint32_t)src1->nb[1] / src1_type_size, (uint32_t)src1->nb[2] / src1_type_size, (uint32_t)src1->nb[3] / src1_type_size,
             (uint32_t) dst->ne[0], (uint32_t) dst->ne[1], (uint32_t) dst->ne[2],(uint32_t) dst->ne[3], (uint32_t) dst->nb[0] /  dst_type_size, (uint32_t) dst->nb[1] /  dst_type_size, (uint32_t) dst->nb[2] /  dst_type_size, (uint32_t) dst->nb[3] /  dst_type_size,
             0,
    -        op_params[0], 0.0f, (int32_t)param3,
    +        eps, 0.0f, (int32_t)num_partials,
         };
    +}
     
    -    // more than one fused op means rms_norm+mul+rope
    -    if (ctx->num_additional_fused_ops > 1) {
    +static void ggml_vk_rms_norm_finish(ggml_backend_vk_context * ctx, const ggml_tensor * src0) {
    +    if (ctx->do_add_rms_partials_offset_calculation) {
    +        ctx->prealloc_size_add_rms_partials_offset += ggml_vk_rms_partials_size(ctx, src0);
    +        ctx->do_add_rms_partials = false;
    +        ctx->do_add_rms_partials_offset_calculation = false;
    +    }
    +}
    +
    +static void ggml_vk_rms_norm(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx, float * op_params) {
    +    ggml_tensor * rms = cgraph->nodes[node_idx];
    +    const ggml_tensor * src0 = rms->src[0];
    +
    +    if (ctx->fused_rms_norm_mode == RMS_NORM_VIEW_SET_ROWS) {
    +        GGML_ASSERT(ctx->num_additional_fused_ops == 2);
    +        ggml_tensor * set_rows = cgraph->nodes[node_idx + 2];
    +        const ggml_tensor * indices = set_rows->src[1];
    +        vk_op_binary_push_constants pc = ggml_vk_rms_norm_push_constants(src0, src0, set_rows, op_params[0], 0);
    +        init_pushconst_tensor_offsets(ctx, pc, src0, src0, nullptr, nullptr, set_rows);
    +
    +        vk_pipeline pipeline = set_rows->type == GGML_TYPE_F16 ?
    +            ctx->device->pipeline_rms_norm_set_rows_f32_f16 : ctx->device->pipeline_rms_norm_set_rows_f32_f32;
    +        ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
    +        ggml_vk_dispatch_pipeline(ctx, subctx, pipeline,
    +            {
    +                ggml_vk_tensor_subbuffer(ctx, src0, true),
    +                ggml_vk_tensor_subbuffer(ctx, src0, true),
    +                ggml_vk_tensor_subbuffer(ctx, set_rows, true),
    +                ggml_vk_tensor_subbuffer(ctx, indices),
    +            }, pc, { (uint32_t)src0->ne[1], (uint32_t)src0->ne[2], (uint32_t)src0->ne[3] });
    +        ggml_vk_rms_norm_finish(ctx, src0);
    +        return;
    +    }
    +
    +    if (ctx->fused_rms_norm_mode == RMS_NORM_MUL_ADD || ctx->fused_rms_norm_mode == RMS_NORM_MUL_ADD_MUL) {
    +        ggml_tensor * mul = cgraph->nodes[node_idx + 1];
    +        ggml_tensor * add = cgraph->nodes[node_idx + 2];
    +        const ggml_tensor * weight = mul->src[0] == rms ? mul->src[1] : mul->src[0];
    +        const ggml_tensor * residual = add->src[0] == mul ? add->src[1] : add->src[0];
    +        const bool do_post_multiply = ctx->fused_rms_norm_mode == RMS_NORM_MUL_ADD_MUL;
    +        GGML_ASSERT(ctx->num_additional_fused_ops == (do_post_multiply ? 3 : 2));
    +        ggml_tensor * dst = do_post_multiply ? cgraph->nodes[node_idx + 3] : add;
    +        const ggml_tensor * post_scale = do_post_multiply ?
    +            (dst->src[0] == add ? dst->src[1] : dst->src[0]) : src0;
    +
    +        const uint32_t num_partials = ctx->do_add_rms_partials ? ggml_vk_rms_num_partials(ctx, dst) : 0;
    +        vk_op_binary_push_constants pc = ggml_vk_rms_norm_push_constants(src0, weight, dst, op_params[0], num_partials);
    +        init_pushconst_tensor_offsets(ctx, pc, src0, weight, residual, post_scale, dst);
    +
    +        vk_pipeline pipeline;
    +        if (ctx->do_add_rms_partials) {
    +            pipeline = do_post_multiply ?
    +                ctx->device->pipeline_rms_norm_mul_add_mul_partials_f32 : ctx->device->pipeline_rms_norm_mul_add_partials_f32;
    +        } else {
    +            pipeline = do_post_multiply ?
    +                ctx->device->pipeline_rms_norm_mul_add_mul_f32 : ctx->device->pipeline_rms_norm_mul_add_f32;
    +        }
    +        ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
    +        if (ctx->do_add_rms_partials) {
    +            ggml_vk_dispatch_pipeline(ctx, subctx, pipeline,
    +                {
    +                    ggml_vk_tensor_subbuffer(ctx, src0, true),
    +                    ggml_vk_tensor_subbuffer(ctx, weight, true),
    +                    ggml_vk_tensor_subbuffer(ctx, dst, true),
    +                    ggml_vk_subbuffer(ctx, ctx->prealloc_add_rms_partials, ctx->prealloc_size_add_rms_partials_offset),
    +                    ggml_vk_tensor_subbuffer(ctx, residual),
    +                    ggml_vk_tensor_subbuffer(ctx, post_scale),
    +                }, pc, { (uint32_t)CEIL_DIV(src0->ne[0], 128), 1, 1 });
    +        } else {
    +            ggml_vk_dispatch_pipeline(ctx, subctx, pipeline,
    +                {
    +                    ggml_vk_tensor_subbuffer(ctx, src0, true),
    +                    ggml_vk_tensor_subbuffer(ctx, weight, true),
    +                    ggml_vk_tensor_subbuffer(ctx, dst, true),
    +                    ggml_vk_tensor_subbuffer(ctx, residual),
    +                    ggml_vk_tensor_subbuffer(ctx, post_scale),
    +                }, pc, { (uint32_t)src0->ne[1], (uint32_t)src0->ne[2], (uint32_t)src0->ne[3] });
    +        }
    +        ggml_vk_rms_norm_finish(ctx, src0);
    +        return;
    +    }
    +
    +    ggml_tensor * dst;
    +    const ggml_tensor * src1;
    +
    +    if (ctx->fused_rms_norm_mode != RMS_NORM_COUNT) {
    +        ggml_tensor * mul = cgraph->nodes[node_idx + 1];
    +        dst = mul;
    +        src1 = mul->src[0] == rms ? mul->src[1] : mul->src[0];
    +    } else {
    +        dst = rms;
    +        src1 = src0;
    +    }
    +
    +    const uint32_t num_partials = ctx->do_add_rms_partials ? ggml_vk_rms_num_partials(ctx, dst) : 0;
    +    vk_op_binary_push_constants bin = ggml_vk_rms_norm_push_constants(src0, src1, dst, op_params[0], num_partials);
    +
    +    if (ctx->fused_rms_norm_mode == RMS_NORM_MUL_ROPE ||
    +        ctx->fused_rms_norm_mode == RMS_NORM_MUL_ROPE_VIEW_SET_ROWS) {
             static constexpr uint32_t max_tensors = 7;
             const ggml_tensor *tensors[max_tensors] {};
     
    @@ -13543,7 +13656,8 @@ static void ggml_vk_rms_norm(ggml_backend_vk_context * ctx, vk_context& subctx,
     
             ggml_tensor *other_src = mul->src[0] == rms ? mul->src[1] : mul->src[0];
     
    -        bool do_set_rows = ctx->num_additional_fused_ops == 4;
    +        bool do_set_rows = ctx->fused_rms_norm_mode == RMS_NORM_MUL_ROPE_VIEW_SET_ROWS;
    +        GGML_ASSERT(ctx->num_additional_fused_ops == (do_set_rows ? 4 : 2));
     
             tensors[0] = rms->src[0];
             tensors[1] = other_src;
    @@ -13610,14 +13724,11 @@ static void ggml_vk_rms_norm(ggml_backend_vk_context * ctx, vk_context& subctx,
                     ggml_vk_subbuffer(ctx, buf[6], offset[6]),
                 }, pc, elements);
         } else {
    +        GGML_ASSERT(ctx->fused_rms_norm_mode == RMS_NORM_MUL || ctx->fused_rms_norm_mode == RMS_NORM_COUNT);
             ggml_vk_op_f32(ctx, subctx, src0, src1, nullptr, nullptr, dst, GGML_OP_RMS_NORM, std::move(bin));
         }
     
    -    if (ctx->do_add_rms_partials_offset_calculation) {
    -        ctx->prealloc_size_add_rms_partials_offset += ggml_vk_rms_partials_size(ctx, src0);
    -        ctx->do_add_rms_partials = false;
    -        ctx->do_add_rms_partials_offset_calculation = false;
    -    }
    +    ggml_vk_rms_norm_finish(ctx, src0);
     }
     
     static void ggml_vk_rms_norm_back(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) {
    @@ -16938,7 +17049,8 @@ static bool ggml_vk_can_fuse(const ggml_backend_vk_context * ctx, const struct g
             return false;
         }
     
    -    if (ops.size() == 2 && ops.begin()[0] == GGML_OP_RMS_NORM && ops.begin()[1] == GGML_OP_MUL) {
    +    if ((ops.size() == 2 || ops.size() == 3 || ops.size() == 4) &&
    +        ops.begin()[0] == GGML_OP_RMS_NORM && ops.begin()[1] == GGML_OP_MUL) {
             // additional constraints specific to this fusion
             const ggml_tensor *rms_norm = cgraph->nodes[node_idx];
             const ggml_tensor *mul = cgraph->nodes[node_idx + 1];
    @@ -16960,6 +17072,43 @@ static bool ggml_vk_can_fuse(const ggml_backend_vk_context * ctx, const struct g
             if (!ggml_is_contiguous_rows(mul->src[0]) || !ggml_is_contiguous_rows(mul->src[1])) {
                 return false;
             }
    +
    +        if (ops.size() >= 3 && ops.begin()[2] == GGML_OP_ADD) {
    +            const ggml_tensor *add = cgraph->nodes[node_idx + 2];
    +            const ggml_tensor *residual = add->src[0] == mul ? add->src[1] : add->src[0];
    +            if (add->src[0] != mul && add->src[1] != mul) {
    +                return false;
    +            }
    +            if (residual->type != GGML_TYPE_F32 || add->type != GGML_TYPE_F32 ||
    +                !ggml_are_same_shape(add, residual) || !ggml_is_contiguous(residual) ||
    +                !ggml_is_contiguous(add) || get_misalign_bytes(ctx, residual) != 0) {
    +                return false;
    +            }
    +
    +            const ggml_tensor *dst = add;
    +            if (ops.size() == 4) {
    +                if (ops.begin()[3] != GGML_OP_MUL) {
    +                    return false;
    +                }
    +
    +                const ggml_tensor *post_mul = cgraph->nodes[node_idx + 3];
    +                const ggml_tensor *scale = post_mul->src[0] == add ? post_mul->src[1] : post_mul->src[0];
    +                if (post_mul->src[0] != add && post_mul->src[1] != add) {
    +                    return false;
    +                }
    +                // The shader reads data_e[0], so the final multiply must use a scalar.
    +                if (scale->type != GGML_TYPE_F32 || post_mul->type != GGML_TYPE_F32 ||
    +                    ggml_nelements(scale) != 1 || !ggml_is_contiguous(post_mul) ||
    +                    get_misalign_bytes(ctx, scale) != 0) {
    +                    return false;
    +                }
    +                dst = post_mul;
    +            }
    +
    +            if (get_misalign_bytes(ctx, dst) != 0) {
    +                return false;
    +            }
    +        }
         }
         auto const &mm_add_ok = [&](const ggml_tensor *mul, const ggml_tensor *add) {
             const ggml_tensor *bias = add->src[0] == mul ? add->src[1] : add->src[0];
    @@ -17341,12 +17490,11 @@ static bool ggml_vk_can_fuse_topk_qsa(ggml_backend_vk_context * ctx, const struc
     
     static bool ggml_vk_can_fuse_rope_set_rows(ggml_backend_vk_context * ctx, const struct ggml_cgraph * cgraph,
                                                int node_idx) {
    -    GGML_UNUSED(ctx);
         const ggml_tensor *rope = cgraph->nodes[node_idx + 0];
         const ggml_tensor *view = cgraph->nodes[node_idx + 1];
         const ggml_tensor *set_rows = cgraph->nodes[node_idx + 2];
     
    -    // ne3 not tested
    +    // The set_rows epilogue uses one index per ne2 slice and does not encode ne3.
         if (rope->src[0]->ne[3] != 1) {
             return false;
         }
    @@ -17355,19 +17503,50 @@ static bool ggml_vk_can_fuse_rope_set_rows(ggml_backend_vk_context * ctx, const
             return false;
         }
     
    -    if (set_rows->src[1]->type != GGML_TYPE_I64) {
    +    // The shader reads each aligned I64 index as a uvec2 and uses its low 32 bits.
    +    if (set_rows->src[1]->type != GGML_TYPE_I64 || !ggml_is_contiguous(set_rows->src[1]) ||
    +        set_rows->nb[0] != ggml_type_size(set_rows->type) || get_misalign_bytes(ctx, set_rows->src[1]) != 0) {
             return false;
         }
     
    -    // The view should flatten two dims of rope into one dim
    +    // SET_ROWS consumes one flattened [ne0*ne1] row for each ne2 slice.
         if (!ggml_is_contiguous(view) ||
    -        view->ne[0] != rope->ne[0] * rope->ne[1]) {
    +        view->ne[0] != rope->ne[0] * rope->ne[1] || view->ne[1] != rope->ne[2] ||
    +        view->ne[2] != 1 || view->ne[3] != 1 ||
    +        ggml_nelements(set_rows->src[1]) != rope->ne[2]) {
             return false;
         }
     
    -    // Only norm/neox/mrope shaders have the fusion code
    +    // Only norm/neox/mrope/imrope shaders have the fusion code
         const int mode = ((const int32_t *) rope->op_params)[2];
    -    if (mode != GGML_ROPE_TYPE_NORMAL && mode != GGML_ROPE_TYPE_NEOX && mode != GGML_ROPE_TYPE_MROPE) {
    +    if (mode != GGML_ROPE_TYPE_NORMAL && mode != GGML_ROPE_TYPE_NEOX &&
    +        mode != GGML_ROPE_TYPE_MROPE && mode != GGML_ROPE_TYPE_IMROPE) {
    +        return false;
    +    }
    +
    +    return true;
    +}
    +
    +static bool ggml_vk_can_fuse_rms_norm_set_rows(ggml_backend_vk_context * ctx, const struct ggml_cgraph * cgraph,
    +                                               int node_idx) {
    +    const ggml_tensor * rms = cgraph->nodes[node_idx];
    +    const ggml_tensor * view = cgraph->nodes[node_idx + 1];
    +    const ggml_tensor * set_rows = cgraph->nodes[node_idx + 2];
    +
    +    // The RMS kernel reads F32 and writes directly to the F32 or F16 SET_ROWS destination.
    +    if (rms->src[0]->type != GGML_TYPE_F32 || rms->type != GGML_TYPE_F32 ||
    +        (set_rows->type != GGML_TYPE_F32 && set_rows->type != GGML_TYPE_F16) ||
    +        set_rows->src[1]->type != GGML_TYPE_I64 || !ggml_is_contiguous(set_rows->src[1]) ||
    +        set_rows->nb[0] != ggml_type_size(set_rows->type) || get_misalign_bytes(ctx, set_rows->src[1]) != 0) {
    +        return false;
    +    }
    +    // As with the ROPE epilogue, each ne2 slice supplies one flattened row and ne3 is not encoded.
    +    if (rms->ne[3] != 1 || !ggml_is_contiguous(rms->src[0]) || !ggml_is_contiguous(view)) {
    +        return false;
    +    }
    +    if (view->ne[0] != rms->ne[0] * rms->ne[1] || view->ne[1] != rms->ne[2] ||
    +        view->ne[2] != 1 || view->ne[3] != 1 ||
    +        ggml_nelements(set_rows->src[1]) != rms->ne[2]) {
             return false;
         }
     
    @@ -17462,7 +17641,6 @@ static bool ggml_vk_tensors_overlap(const ggml_tensor * a, const ggml_tensor * b
     
     static bool ggml_vk_can_fuse_rms_norm_mul_rope(ggml_backend_vk_context * ctx, const struct ggml_cgraph * cgraph,
                                                    int node_idx) {
    -    GGML_UNUSED(ctx);
         const ggml_tensor *rms = cgraph->nodes[node_idx + 0];
         const ggml_tensor *mul = cgraph->nodes[node_idx + 1];
         const ggml_tensor *rope = cgraph->nodes[node_idx + 2];
    @@ -17719,6 +17897,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
             ctx->fused_topk_moe_mode = TOPK_MOE_COUNT;
             ctx->fused_topk_moe_scale = false;
             ctx->fused_topk_qsa = false;
    +        ctx->fused_rms_norm_mode = RMS_NORM_COUNT;
             const char *fusion_string {};
             if (!ctx->device->disable_fusion) {
                 uint32_t num_adds = ggml_vk_fuse_multi_add(ctx, cgraph, i);
    @@ -17753,27 +17932,47 @@ 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_can_fuse_subgraph(cgraph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ROPE, GGML_OP_VIEW, GGML_OP_SET_ROWS }, { i + 4 }) &&
    +            } else if (ggml_can_fuse_subgraph(cgraph, i, rms_norm_mul_rope_view_set_rows_pattern, { 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) &&
                            ggml_vk_can_fuse_rope_set_rows(ctx, cgraph, i + 2)) {
                     ctx->num_additional_fused_ops = 4;
    +                ctx->fused_rms_norm_mode = RMS_NORM_MUL_ROPE_VIEW_SET_ROWS;
                     fusion_string = "RMS_NORM_MUL_ROPE_VIEW_SET_ROWS";
                     op_srcs_fused_elementwise[0] = false;
                     op_srcs_fused_elementwise[1] = false;
                     op_srcs_fused_elementwise[2] = false;
                     op_srcs_fused_elementwise[3] = false;
                     op_srcs_fused_elementwise[4] = false;
    -            } else if (ggml_vk_can_fuse(ctx, cgraph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ROPE })&&
    +            } else if (ggml_vk_can_fuse(ctx, cgraph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ROPE }) &&
                            ggml_vk_can_fuse_rms_norm_mul_rope(ctx, cgraph, i)) {
                     ctx->num_additional_fused_ops = 2;
    +                ctx->fused_rms_norm_mode = RMS_NORM_MUL_ROPE;
                     fusion_string = "RMS_NORM_MUL_ROPE";
                     // rope is approximately elementwise - whole rows are done by a single workgroup and it's row-wise
                     op_srcs_fused_elementwise[0] = false;
                     op_srcs_fused_elementwise[1] = true;
                     op_srcs_fused_elementwise[2] = true;
    +            } else if (ggml_vk_can_fuse(ctx, cgraph, i, rms_norm_mul_add_mul_pattern)) {
    +                ctx->num_additional_fused_ops = 3;
    +                ctx->fused_rms_norm_mode = RMS_NORM_MUL_ADD_MUL;
    +                fusion_string = "RMS_NORM_MUL_ADD_MUL";
    +                std::fill_n(op_srcs_fused_elementwise, 4, true);
    +            } else if (ggml_vk_can_fuse(ctx, cgraph, i, rms_norm_mul_add_pattern)) {
    +                ctx->num_additional_fused_ops = 2;
    +                ctx->fused_rms_norm_mode = RMS_NORM_MUL_ADD;
    +                fusion_string = "RMS_NORM_MUL_ADD";
    +                std::fill_n(op_srcs_fused_elementwise, 3, true);
    +            } else if (ggml_can_fuse_subgraph(cgraph, i, rms_norm_view_set_rows_pattern, { i + 2 }) &&
    +                       ggml_check_edges(cgraph, i, rms_norm_view_set_rows_edges) &&
    +                       ggml_vk_can_fuse_rms_norm_set_rows(ctx, cgraph, i)) {
    +                ctx->num_additional_fused_ops = 2;
    +                ctx->fused_rms_norm_mode = RMS_NORM_VIEW_SET_ROWS;
    +                fusion_string = "RMS_NORM_VIEW_SET_ROWS";
    +                std::fill_n(op_srcs_fused_elementwise, 3, false);
                 } else if (ggml_vk_can_fuse(ctx, cgraph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL })) {
                     ctx->num_additional_fused_ops = 1;
    +                ctx->fused_rms_norm_mode = RMS_NORM_MUL;
                     fusion_string = "RMS_NORM_MUL";
                     // rms_norm is not elementwise, but whole rows must be consumed and the scale factor computed before
                     // they are overwritten, and one workgroup per row. So close enough.
    @@ -17792,7 +17991,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
                     fusion_string = "SSM_CONV_SILU";
                     op_srcs_fused_elementwise[0] = false;
                     op_srcs_fused_elementwise[1] = true;
    -            } else if (ggml_can_fuse_subgraph(cgraph, i, { GGML_OP_ROPE, GGML_OP_VIEW, GGML_OP_SET_ROWS }, { i + 2 }) &&
    +            } else if (ggml_can_fuse_subgraph(cgraph, i, rope_view_set_rows_pattern, { i + 2 }) &&
                            ggml_check_edges(cgraph, i, rope_view_set_rows_edges) &&
                            ggml_vk_can_fuse_rope_set_rows(ctx, cgraph, i)) {
                     ctx->num_additional_fused_ops = 2;
    @@ -17930,6 +18129,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
                     ctx->fused_topk_moe_mode = TOPK_MOE_COUNT;
                     ctx->fused_topk_moe_scale = false;
                     ctx->fused_topk_qsa = false;
    +                ctx->fused_rms_norm_mode = RMS_NORM_COUNT;
                 }
             }
     
    @@ -18130,6 +18330,22 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph *
                 continue;
             }
     
    +        if (keep_pattern(rms_norm_mul_add_mul_pattern)) {
    +            continue;
    +        }
    +        if (keep_pattern(rms_norm_mul_add_pattern)) {
    +            continue;
    +        }
    +        if (keep_pattern(rms_norm_mul_rope_view_set_rows_pattern)) {
    +            continue;
    +        }
    +        if (keep_pattern(rms_norm_view_set_rows_pattern)) {
    +            continue;
    +        }
    +        if (keep_pattern(rope_view_set_rows_pattern)) {
    +            continue;
    +        }
    +
             // First, grab the next unused node.
             current_set.push_back(first_unused);
     
    @@ -18163,7 +18379,12 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph *
                     match_pattern(topk_moe_early_softmax, j) ||
                     match_pattern(topk_moe_late_softmax, j) ||
                     match_pattern(snake_pattern, j) ||
    -                in_qsa_pattern(j)) {
    +                in_qsa_pattern(j) ||
    +                match_pattern(rms_norm_mul_add_mul_pattern, j) ||
    +                match_pattern(rms_norm_mul_add_pattern, j) ||
    +                match_pattern(rms_norm_mul_rope_view_set_rows_pattern, j) ||
    +                match_pattern(rms_norm_view_set_rows_pattern, j) ||
    +                match_pattern(rope_view_set_rows_pattern, j)) {
                     continue;
                 }
                 bool ok = true;
    @@ -18203,30 +18424,41 @@ static void ggml_vk_graph_optimize(ggml_backend_t backend, struct ggml_cgraph *
                             }
                         }
                     }
    -                // Look for ROPE + VIEW + SET_ROWS and make them consecutive
    -                if (graph->nodes[rope_idx]->op == GGML_OP_ROPE) {
    +                // Look for ROPE/RMS_NORM + VIEW + SET_ROWS and make them consecutive
    +                if (graph->nodes[rope_idx]->op == GGML_OP_ROPE || graph->nodes[rope_idx]->op == GGML_OP_RMS_NORM) {
                         int view_idx = -1;
                         int set_rows_idx = -1;
    -                    for (int k = rope_idx+1; k < std::min(rope_idx + 10, graph->n_nodes); ++k) {
    -                        if (view_idx == -1 &&
    -                            graph->nodes[k]->op == GGML_OP_VIEW &&
    -                            graph->nodes[k]->src[0] == graph->nodes[rope_idx]) {
    +                    for (int k = rope_idx + 1; k < std::min(rope_idx + 15, graph->n_nodes); ++k) {
    +                        if (used[k]) {
    +                            continue;
    +                        }
    +                        if (view_idx == -1 && graph->nodes[k]->op == GGML_OP_VIEW && graph->nodes[k]->src[0] == graph->nodes[rope_idx]) {
                                 view_idx = k;
                                 continue;
                             }
    -                        if (view_idx != -1 &&
    -                            set_rows_idx == -1 &&
    -                            graph->nodes[k]->op == GGML_OP_SET_ROWS &&
    -                            graph->nodes[k]->src[0] == graph->nodes[view_idx]) {
    +                        if (view_idx != -1 && graph->nodes[k]->op == GGML_OP_SET_ROWS && graph->nodes[k]->src[0] == graph->nodes[view_idx]) {
                                 set_rows_idx = k;
                                 break;
                             }
                         }
                         if (set_rows_idx != -1) {
    -                        current_set.push_back(view_idx);
    -                        current_set.push_back(set_rows_idx);
    -                        used[view_idx] = true;
    -                        used[set_rows_idx] = true;
    +                        const int node_idxs[] = { rope_idx, view_idx, set_rows_idx };
    +                        const ggml_op ops[] = { graph->nodes[rope_idx]->op, GGML_OP_VIEW, GGML_OP_SET_ROWS };
    +                        bool can_pull = ggml_can_fuse_subgraph_ext(graph, node_idxs, 3, ops, &set_rows_idx, 1);
    +
    +                        for (int c = rope_idx + 1; can_pull && c < set_rows_idx; ++c) {
    +                            if (!used[c] && c != view_idx && !is_empty(graph->nodes[c]) &&
    +                                is_src_of(graph->nodes[set_rows_idx], graph->nodes[c])) {
    +                                can_pull = false;
    +                            }
    +                        }
    +
    +                        if (can_pull) {
    +                            current_set.push_back(view_idx);
    +                            current_set.push_back(set_rows_idx);
    +                            used[view_idx] = true;
    +                            used[set_rows_idx] = true;
    +                        }
                         }
                     }
                     // Look for MUL_MAT_ID + ADD_ID + MUL
    diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/rms_norm.comp b/ggml/src/ggml-vulkan/vulkan-shaders/rms_norm.comp
    index 55b89f19a..ee813842c 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/rms_norm.comp
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/rms_norm.comp
    @@ -27,12 +27,24 @@ layout (binding = 6) readonly buffer R_I {uvec2 rope_data_i[];}; // indices for
     #define GGML_ROPE_TYPE_MROPE  8
     #define GGML_ROPE_TYPE_VISION 24
     
    +#elif RMS_NORM_ADD_FUSION
    +
    +layout (binding = 3) readonly buffer C {float data_c[];};
    +layout (binding = 4) readonly buffer E {float data_e[];};
    +
    +#elif RMS_NORM_SET_ROWS_FUSION
    +
    +layout (binding = 3) readonly buffer I {uvec2 data_i[];};
    +
     #endif
     
     #extension GL_EXT_control_flow_attributes : enable
     #define BLOCK_SIZE 512
     
     layout (constant_id = 1) const bool do_multiply = false;
    +#if RMS_NORM_ADD_FUSION
    +layout (constant_id = 2) const bool do_post_multiply = false;
    +#endif
     
     layout(local_size_x = BLOCK_SIZE, local_size_y = 1, local_size_z = 1) in;
     
    @@ -57,6 +69,8 @@ void rms_norm(uint num_iters) {
     #if RMS_NORM_ROPE_FUSION
         // Per-row offset in shared memory
         uint32_t d_offset = 0;
    +#elif RMS_NORM_SET_ROWS_FUSION
    +    uint32_t d_offset = data_i[channel].x*p.nb21 + row*ncols + get_doffset();
     #else
         uint32_t d_offset = ((samp*nchannels + channel)*nrows + row)*ncols + get_doffset();
     #endif
    @@ -91,14 +105,28 @@ void rms_norm(uint num_iters) {
                     if (col >= ncols) {
                         continue;
                     }
    -                data_d[d_offset + col] = D_TYPE(scale * FLOAT_TYPE(data_a[a_offset + col]) * FLOAT_TYPE(data_b[b_offset + fastmod(col, p.ne10)]));
    +                FLOAT_TYPE value = scale * FLOAT_TYPE(data_a[a_offset + col]) * FLOAT_TYPE(data_b[b_offset + fastmod(col, p.ne10)]);
    +#if RMS_NORM_ADD_FUSION
    +                value += FLOAT_TYPE(data_c[d_offset + col]);
    +                if (do_post_multiply) {
    +                    value *= FLOAT_TYPE(data_e[0]);
    +                }
    +#endif
    +                data_d[d_offset + col] = D_TYPE(value);
                 }
             } else {
                 [[unroll]] for (uint col = tid, idx = 0; idx < num_iters; col += BLOCK_SIZE, ++idx) {
                     if (col >= ncols) {
                         continue;
                     }
    -                data_d[d_offset + col] = D_TYPE(scale * FLOAT_TYPE(data_a[a_offset + col]) * FLOAT_TYPE(data_b[b_offset + col]));
    +                FLOAT_TYPE value = scale * FLOAT_TYPE(data_a[a_offset + col]) * FLOAT_TYPE(data_b[b_offset + col]);
    +#if RMS_NORM_ADD_FUSION
    +                value += FLOAT_TYPE(data_c[d_offset + col]);
    +                if (do_post_multiply) {
    +                    value *= FLOAT_TYPE(data_e[0]);
    +                }
    +#endif
    +                data_d[d_offset + col] = D_TYPE(value);
                 }
             }
         } else {
    diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/rms_norm_partials.comp b/ggml/src/ggml-vulkan/vulkan-shaders/rms_norm_partials.comp
    index 4618b2c7e..cf7ab21f2 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/rms_norm_partials.comp
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/rms_norm_partials.comp
    @@ -10,11 +10,19 @@
     #define BLOCK_SIZE 128
     
     layout (constant_id = 1) const bool do_multiply = false;
    +#if RMS_NORM_ADD_FUSION
    +layout (constant_id = 2) const bool do_post_multiply = false;
    +#endif
     
     layout(local_size_x = BLOCK_SIZE, local_size_y = 1, local_size_z = 1) in;
     
     layout (binding = 3, std430) readonly buffer PartialsBuf {float partial_sums[];};
     
    +#if RMS_NORM_ADD_FUSION
    +layout (binding = 4) readonly buffer C {float data_c[];};
    +layout (binding = 5) readonly buffer E {float data_e[];};
    +#endif
    +
     shared FLOAT_TYPE sumsh[BLOCK_SIZE];
     
     void main() {
    @@ -55,9 +63,23 @@ void main() {
     
         if (do_multiply) {
             if (ncols > p.ne10) {
    -            data_d[d_offset + col] = D_TYPE(scale * FLOAT_TYPE(data_a[a_offset + col]) * FLOAT_TYPE(data_b[b_offset + fastmod(col, p.ne10)]));
    +            FLOAT_TYPE value = scale * FLOAT_TYPE(data_a[a_offset + col]) * FLOAT_TYPE(data_b[b_offset + fastmod(col, p.ne10)]);
    +#if RMS_NORM_ADD_FUSION
    +            value += FLOAT_TYPE(data_c[d_offset + col]);
    +            if (do_post_multiply) {
    +                value *= FLOAT_TYPE(data_e[0]);
    +            }
    +#endif
    +            data_d[d_offset + col] = D_TYPE(value);
             } else {
    -            data_d[d_offset + col] = D_TYPE(scale * FLOAT_TYPE(data_a[a_offset + col]) * FLOAT_TYPE(data_b[b_offset + col]));
    +            FLOAT_TYPE value = scale * FLOAT_TYPE(data_a[a_offset + col]) * FLOAT_TYPE(data_b[b_offset + col]);
    +#if RMS_NORM_ADD_FUSION
    +            value += FLOAT_TYPE(data_c[d_offset + col]);
    +            if (do_post_multiply) {
    +                value *= FLOAT_TYPE(data_e[0]);
    +            }
    +#endif
    +            data_d[d_offset + col] = D_TYPE(value);
             }
         } else {
             data_d[d_offset + col] = D_TYPE(scale * FLOAT_TYPE(data_a[a_offset + col]));
    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 5d4b9c5fa..da0d54ab4 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp
    @@ -806,6 +806,10 @@ void process_shaders() {
         string_to_spv("norm_f32", "norm.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}}));
         string_to_spv("group_norm_f32", "group_norm.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}}));
         string_to_spv("rms_norm_f32", "rms_norm.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}}));
    +    string_to_spv("rms_norm_mul_add_f32", "rms_norm.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"RMS_NORM_ADD_FUSION", "1"}}));
    +    string_to_spv("rms_norm_mul_add_partials_f32", "rms_norm_partials.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"RMS_NORM_ADD_FUSION", "1"}}));
    +    string_to_spv("rms_norm_set_rows_f32_f32", "rms_norm.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"RMS_NORM_SET_ROWS_FUSION", "1"}}));
    +    string_to_spv("rms_norm_set_rows_f32_f16", "rms_norm.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float16_t"}, {"RMS_NORM_SET_ROWS_FUSION", "1"}}));
         string_to_spv("rms_norm_partials_f32", "rms_norm_partials.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}}));
         string_to_spv("rms_norm_mul_rope_f32_f32", "rms_norm.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"ROPE_D_TYPE", "float"}, {"RMS_NORM_ROPE_FUSION", "1"}}));
         string_to_spv("rms_norm_mul_rope_f32_f16", "rms_norm.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"B_TYPE", "float"}, {"D_TYPE", "float"}, {"ROPE_D_TYPE", "float16_t"}, {"RMS_NORM_ROPE_FUSION", "1"}}));
    diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp
    index 31593541f..6a8cfd46d 100644
    --- a/tests/test-backend-ops.cpp
    +++ b/tests/test-backend-ops.cpp
    @@ -2668,13 +2668,16 @@ struct test_rope_set_rows : public test_case {
         }
     };
     
    -// GGML_OP_RMS_NORM + GGML_OP_MUL + GGML_OP_ROPE (+ GGML_OP_VIEW + GGML_OP_SET_ROWS)
    +// GGML_OP_RMS_NORM with optional GGML_OP_MUL, GGML_OP_ROPE, GGML_OP_VIEW and GGML_OP_SET_ROWS
     struct test_rms_norm_mul_rope : public test_case {
         const std::array ne;
         const float eps;
         const bool multi_add; // test a sequence of adds feeding into rms_norm
    +    const bool mul;
    +    const bool rope;
         const bool set_rows;
         const bool broadcast; // multiply by a 1D [ne0] weight, as model norm weights are
    +    const ggml_type set_rows_type;
         int mode;
     
         std::string op_desc(ggml_tensor * t) override {
    @@ -2685,63 +2688,90 @@ struct test_rms_norm_mul_rope : public test_case {
         bool run_whole_graph() override { return true; }
     
         std::string vars() override {
    -        return VARS_TO_STR6(ne, eps, multi_add, set_rows, broadcast, mode);
    +        return VARS_TO_STR9(ne, eps, multi_add, mul, rope, set_rows, broadcast, mode, set_rows_type);
         }
     
         test_rms_norm_mul_rope(std::array ne, float eps = 1e-6f, bool multi_add = false,
    -                           bool set_rows = false, bool broadcast = false, int mode = GGML_ROPE_TYPE_NORMAL)
    -        : ne(ne), eps(eps), multi_add(multi_add), set_rows(set_rows), broadcast(broadcast), mode(mode) {}
    +                           bool set_rows = false, bool broadcast = false, int mode = GGML_ROPE_TYPE_NORMAL,
    +                           bool mul = true, bool rope = true, ggml_type set_rows_type = GGML_TYPE_F16)
    +        : ne(ne), eps(eps), multi_add(multi_add), mul(mul), rope(rope), set_rows(set_rows), broadcast(broadcast),
    +          set_rows_type(set_rows_type), mode(mode) {}
     
         ggml_tensor * build_graph(ggml_context * ctx) override {
    -        ggml_tensor * a = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, ne[0], ne[1], ne[2], 1);
    -        ggml_tensor * b = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, ne[0], ne[1], ne[2], 1);
    -        ggml_tensor * c = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, ne[0], ne[1], ne[2], 1);
    +        ggml_tensor * a = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, ne[0], ne[1], ne[2], ne[3]);
    +
    +        ggml_tensor * b = nullptr;
    +        ggml_tensor * c = nullptr;
    +        ggml_tensor * w = nullptr;
    +
    +        if (multi_add || (mul && !broadcast)) {
    +            b = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, ne[0], ne[1], ne[2], 1);
    +        }
    +        if (multi_add) {
    +            c = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, ne[0], ne[1], ne[2], 1);
    +        }
    +        if (mul) {
    +            w = broadcast ? ggml_new_tensor_1d(ctx, GGML_TYPE_F32, ne[0]) : b;
    +        }
     
             if (multi_add) {
                 a = ggml_add(ctx, ggml_add(ctx, a, b), c);
             }
     
    -        ggml_tensor * w = broadcast ? ggml_new_tensor_1d(ctx, GGML_TYPE_F32, ne[0]) : b;
    +        a = ggml_rms_norm(ctx, a, eps);
     
    -        a = ggml_mul(ctx, ggml_rms_norm(ctx, a, eps), w);
    -
    -        ggml_tensor * pos = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, ne[2]);
    -
    -        ggml_tensor * rope = ggml_rope(ctx, a, pos, ne[0], mode);
    -
    -        ggml_tensor * out;
    -
    -        if (set_rows) {
    -            ggml_tensor * view = ggml_view_2d(ctx, rope, ne[0] * ne[1], ne[2], rope->nb[2], 0);
    -
    -            ggml_tensor * dst = ggml_new_tensor_4d(ctx, GGML_TYPE_F16, ne[0] * ne[1], ne[2] * ne[3], 1, 1);
    -            ggml_set_name(dst, "dst");
    -
    -            ggml_tensor * row_idxs = ggml_new_tensor_3d(ctx, GGML_TYPE_I64, ne[2], 1, 1);
    -            ggml_set_name(row_idxs, "row_idxs");
    -
    -            out = ggml_set_rows(ctx, dst, view, row_idxs);
    -            ggml_set_name(out, "out");
    -        } else {
    -            out = rope;
    +        if (mul) {
    +            a = ggml_mul(ctx, a, w);
             }
     
    -        return out;
    +        if (rope) {
    +            const bool is_mrope = mode & GGML_ROPE_TYPE_MROPE;
    +            ggml_tensor * pos = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, ne[2] * (is_mrope ? 4 : 1));
    +
    +            if (is_mrope) {
    +                const int n_dims = ne[0];
    +                int sections[4] = { n_dims/3, n_dims/3, n_dims/3, 0 };
    +                a = ggml_rope_multi(ctx, a, pos, nullptr, n_dims, sections, mode, 0, 10000.0f, 1.0f, 0.0f, 1.0f, 32.0f, 1.0f);
    +            } else {
    +                a = ggml_rope(ctx, a, pos, ne[0], mode);
    +            }
    +        }
    +
    +        if (set_rows) {
    +            ggml_tensor * view = ggml_view_2d(ctx, a, ne[0] * ne[1], ne[2], a->nb[2], 0);
    +
    +            ggml_tensor * dst = ggml_new_tensor_2d(ctx, set_rows_type, ne[0] * ne[1], ne[2] * 2);
    +            ggml_set_name(dst, "dst");
    +
    +            ggml_tensor * row_idxs = ggml_new_tensor_1d(ctx, GGML_TYPE_I64, ne[2]);
    +            ggml_set_name(row_idxs, "row_idxs");
    +
    +            a = ggml_set_rows(ctx, dst, view, row_idxs);
    +        }
    +
    +        ggml_set_name(a, "out");
    +        return a;
         }
     
         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->type == GGML_TYPE_I64 || t->type == GGML_TYPE_I32) {
    -                if (ggml_is_view_op(t->op)) {
    -                    continue;
    +            if (t->type == GGML_TYPE_I64) {
    +                init_set_rows_row_ids(t, ne[2] * 2);
    +            } else if (t->type == GGML_TYPE_I32) {
    +                std::vector data(ggml_nelements(t));
    +                for (int32_t & value : data) {
    +                    value = rand() % 512;
                     }
    -
    -                init_set_rows_row_ids(t, ne[2]);
    +                ggml_backend_tensor_set(t, data.data(), 0, ggml_nbytes(t));
                 } else {
                     init_tensor_uniform(t);
                 }
             }
         }
    +
    +    double max_nmse_err() override {
    +        return ne[0] == 8192 ? 5e-6 : test_case::max_nmse_err();
    +    }
     };
     
     // GGML_OP_ARGMAX
    @@ -3636,13 +3666,16 @@ 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 (+ GGML_OP_MUL)
     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 post_mul;
    +    const bool alias_rms_input;
    +    const bool weight_broadcast;
     
         std::string op_desc(ggml_tensor * t) override {
             GGML_UNUSED(t);
    @@ -3652,20 +3685,23 @@ struct test_rms_norm_mul_add : public test_case {
         bool run_whole_graph() override { return true; }
     
         std::string vars() override {
    -        return VARS_TO_STR5(type, ne, eps, broadcast, multi_add);
    +        return VARS_TO_STR8(type, ne, eps, broadcast, multi_add, post_mul, alias_rms_input, weight_broadcast);
         }
     
         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 post_mul = false,
    +            bool alias_rms_input = false, bool weight_broadcast = false)
    +        : type(type), ne(ne), eps(eps), broadcast(broadcast), multi_add(multi_add), post_mul(post_mul),
    +          alias_rms_input(alias_rms_input), weight_broadcast(weight_broadcast) {}
     
         ggml_tensor * build_graph(ggml_context * ctx) override {
             std::array broadcast_dims = {ne[0]*2, ne[1]*3, ne[2]*3, ne[3]*4};
     
             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 = weight_broadcast ? ggml_new_tensor_1d(ctx, type, ne[0]) : ggml_new_tensor(ctx, type, 4, ne.data());
             ggml_tensor * c = ggml_new_tensor(ctx, type, 4, ne.data());
    +        ggml_tensor * d = nullptr;
     
             ggml_set_param(a);
             ggml_set_name(a, "a");
    @@ -3676,10 +3712,20 @@ struct test_rms_norm_mul_add : public test_case {
     
             // Use a, b and c early, so we don't end up with an OP_NONE between rms_norm and mul
             a = ggml_add(ctx, ggml_add(ctx, a, b), c);
    +        if (post_mul) {
    +            d = ggml_new_tensor_1d(ctx, type, 1);
    +            ggml_set_param(d);
    +            ggml_set_name(d, "d");
    +            a = ggml_add(ctx, a, d);
    +        }
             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 * mul = ggml_mul(ctx, ggml_rms_norm(ctx, a, eps), b);
    +        ggml_tensor * out = alias_rms_input ? ggml_add_inplace(ctx, a, mul) : ggml_add(ctx, mul, c);
    +        if (post_mul) {
    +            out = ggml_mul(ctx, out, d);
    +        }
             ggml_set_name(out, "out");
     
             return out;
    @@ -8848,7 +8894,7 @@ static std::vector> make_test_cases_eval() {
         test_cases.emplace_back(new test_set_rows(GGML_TYPE_F16, GGML_TYPE_F16, GGML_TYPE_I64, { 1, 8, 1, 3 }, { 1, 1 }, 2, true));
         test_cases.emplace_back(new test_set_rows(GGML_TYPE_F16, GGML_TYPE_F16, GGML_TYPE_I32, { 1, 8, 1, 3 }, { 1, 1 }, 2, true));
     
    -    for (int mode : { GGML_ROPE_TYPE_NORMAL, GGML_ROPE_TYPE_NEOX, GGML_ROPE_TYPE_MROPE, GGML_ROPE_TYPE_VISION }) {
    +    for (int mode : { GGML_ROPE_TYPE_NORMAL, GGML_ROPE_TYPE_NEOX, GGML_ROPE_TYPE_MROPE, GGML_ROPE_TYPE_VISION, GGML_ROPE_TYPE_IMROPE }) {
             for (ggml_type type : {GGML_TYPE_F16, GGML_TYPE_F32}) {
                 for (int ne2 : {1, 8, 512}) {
                     test_cases.emplace_back(new test_rope_set_rows(type, GGML_TYPE_I64, { 128, 32, ne2, 1 }, mode));
    @@ -8856,6 +8902,7 @@ static std::vector> make_test_cases_eval() {
                 }
             }
         }
    +    test_cases.emplace_back(new test_rope_set_rows(GGML_TYPE_F32, GGML_TYPE_I32, { 128, 32, 8, 1 }, GGML_ROPE_TYPE_IMROPE));
     
         for (ggml_type type_input : {GGML_TYPE_F32}) {
             for (ggml_op_pool pool_type : {GGML_OP_POOL_AVG, GGML_OP_POOL_MAX}) {
    @@ -9437,6 +9484,11 @@ static std::vector> make_test_cases_eval() {
         // in-place tests
         test_cases.emplace_back(new test_rms_norm(GGML_TYPE_F32, {64, 5, 4, 3}, false, 1e-6f, true));
     
    +    for (ggml_type set_rows_type : { GGML_TYPE_F32, GGML_TYPE_F16 }) {
    +        test_cases.emplace_back(new test_rms_norm_mul_rope({ 256, 1, 1, 1 }, 1e-6f, false, true, false, GGML_ROPE_TYPE_NORMAL, false, false, set_rows_type));
    +        test_cases.emplace_back(new test_rms_norm_mul_rope({ 128, 4, 3, 1 }, 1e-6f, false, true, false, GGML_ROPE_TYPE_NORMAL, false, false, set_rows_type));
    +    }
    +
         for (float eps : { 0.0f, 1e-6f, 1e-4f, 1e-1f, 1.0f }) {
             for (uint32_t n : { 64, 1025 }) {
                 test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, { n, 5, 4, 3 }, eps, false));
    @@ -9462,10 +9514,20 @@ static std::vector> make_test_cases_eval() {
             test_cases.emplace_back(new test_add_add(GGML_TYPE_F16, GGML_TYPE_F32, { n, 5, 4, 3 }, true, false));
         }
     
    +    test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, { 1536, 1, 1, 1 }, 1e-6f, false, false, true));
    +    test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, { 256, 4, 1, 1 }, 1e-6f, false, false, true));
    +    test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, { 256, 4, 3, 2 }, 1e-6f, false, false, true));
    +    test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, { 256, 4, 3, 2 }, 1e-6f, false, false, true, false, true));
    +    test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, { 1536, 1, 1, 1 }, 1e-6f, false, false, false, true));
    +    test_cases.emplace_back(new test_rms_norm_mul_add(GGML_TYPE_F32, { 256, 4, 1, 1 }, 1e-6f, false, false, false, true));
    +
    +    test_cases.emplace_back(new test_rms_norm_mul_rope({128, 4, 7, 2}));
    +    test_cases.emplace_back(new test_rms_norm_mul_rope({128, 4, 7, 2}, 1e-6f, false, true));
    +
         for (auto multi_add : {false, true}) {
             for (auto set_rows : {false, true}) {
                 for (auto broadcast : {false, true}) {
    -                for (auto rope : {GGML_ROPE_TYPE_NORMAL, GGML_ROPE_TYPE_NEOX}) {
    +                for (auto rope : {GGML_ROPE_TYPE_NORMAL, GGML_ROPE_TYPE_NEOX, GGML_ROPE_TYPE_IMROPE}) {
                         test_cases.emplace_back(new test_rms_norm_mul_rope({768, 1, 1, 1}, 1e-6f, multi_add, set_rows, broadcast, rope));
                         test_cases.emplace_back(new test_rms_norm_mul_rope({768, 3, 1, 1}, 1e-6f, multi_add, set_rows, broadcast, rope));
                         test_cases.emplace_back(new test_rms_norm_mul_rope({768, 3, 5, 1}, 1e-6f, multi_add, set_rows, broadcast, rope));
    
    From 992cb503cdacf691ef06c332d05243bc7807257b Mon Sep 17 00:00:00 2001
    From: Aman Gupta 
    Date: Mon, 7 Sep 2026 14:10:40 +0800
    Subject: [PATCH 16/31] ggml: allow backend inputs to not create another split
     (#28387)
    
    ---
     ggml/src/ggml-backend.cpp | 15 ++-------------
     1 file changed, 2 insertions(+), 13 deletions(-)
    
    diff --git a/ggml/src/ggml-backend.cpp b/ggml/src/ggml-backend.cpp
    index 6862128e6..40e50c5c9 100644
    --- a/ggml/src/ggml-backend.cpp
    +++ b/ggml/src/ggml-backend.cpp
    @@ -849,7 +849,7 @@ static void ggml_backend_sched_split_inputs_grow(struct ggml_backend_sched_split
         int new_cap = GGML_SCHED_MAX_SPLIT_INPUTS;
         if (split->inputs_capacity > 0) {
             new_cap = 2*split->inputs_capacity;
    -        GGML_LOG_WARN("%s: increasing split inputs capacity from %d to %d\n", __func__, split->inputs_capacity, new_cap);
    +        GGML_LOG_DEBUG("%s: increasing split inputs capacity from %d to %d\n", __func__, split->inputs_capacity, new_cap);
         }
         auto * pnew = (struct ggml_tensor **) realloc((void *) split->inputs, new_cap * sizeof(struct ggml_tensor *));
         if (pnew == NULL) {
    @@ -864,7 +864,7 @@ static void ggml_backend_sched_graph_inputs_grow(ggml_backend_sched_t sched) {
         int new_cap = GGML_SCHED_MAX_SPLIT_INPUTS;
         if (sched->graph_inputs_capacity > 0) {
             new_cap = 2*sched->graph_inputs_capacity;
    -        GGML_LOG_WARN("%s: increasing graph inputs capacity from %d to %d\n", __func__, sched->graph_inputs_capacity, new_cap);
    +        GGML_LOG_DEBUG("%s: increasing graph inputs capacity from %d to %d\n", __func__, sched->graph_inputs_capacity, new_cap);
         }
         auto * pnew = (struct ggml_tensor **) realloc((void *) sched->graph_inputs, new_cap * sizeof(struct ggml_tensor *));
         if (pnew == NULL) {
    @@ -1338,17 +1338,6 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra
                                 break;
                             }
                         }
    -                    // 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) {
    -                        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);
    -                        if (src_backend_id != cur_backend_id && tensor_id_copy(id, cur_backend_id, 0) == NULL && !supported) {
    -                            need_new_split = true;
    -                            break;
    -                        }
    -                    }
                     }
                 }
     
    
    From b74f590eafec2fafc6e0e98ee93b2e5d3efa9042 Mon Sep 17 00:00:00 2001
    From: Siavash Norouzi <35790025+siavashnorouzi@users.noreply.github.com>
    Date: Sun, 6 Sep 2026 23:23:21 -0700
    Subject: [PATCH 17/31] ggml-cuda: fix divergent barrier in f16 flash attention
     (#27870)
    
    * ggml-cuda: fix divergent barrier in f16 flash attention
    
    * ggml-cuda: avoid duplicate metadata pointer setup
    ---
     ggml/src/ggml-cuda/fattn-mma-f16.cuh | 102 +++++++++++++--------------
     1 file changed, 51 insertions(+), 51 deletions(-)
    
    diff --git a/ggml/src/ggml-cuda/fattn-mma-f16.cuh b/ggml/src/ggml-cuda/fattn-mma-f16.cuh
    index 126a4c452..bc5060e81 100644
    --- a/ggml/src/ggml-cuda/fattn-mma-f16.cuh
    +++ b/ggml/src/ggml-cuda/fattn-mma-f16.cuh
    @@ -1545,77 +1545,77 @@ static __device__ __forceinline__ void flash_attn_ext_f16_process_tile(
             }
         }
     
    -    if (np > 1 && threadIdx.y % np == 0) {
    -        // Combine the meta data for parallel warps via shared memory.
    -        // Warps with threadIdx.y % np != 0 must NOT return early.
    -        // All threads must return simultaneously to avoid race conditions with work on the next tile.
    -
    +    if (np > 1) {
             constexpr int nmeta = np*cols_per_warp >= warp_size ? np*cols_per_warp/warp_size : 1;
     
    +        float KQ_cmn;
    +        float KQ_cms[nmeta];
    +        float KQ_crs;
    +
             const int jc_meta = threadIdx.y*cols_per_warp + (np*cols_per_warp < warp_size ? threadIdx.x % (np*cols_per_warp) : threadIdx.x);
             float2 * const meta_ptr = ((float2 *) tile_Q) + jc_meta*(tile_stride/2) + nbatch_combine/2;
    -        float2 meta[nmeta];
    -#pragma unroll
    -        for (int imeta = 0; imeta < nmeta; ++imeta) {
    -            meta[imeta] = meta_ptr[imeta * warp_size * tile_stride/2];
    -        }
     
    -        float KQ_cmn = meta[0].x; // KQ combine max new, max between all parallel warps.
    +        if (threadIdx.y % np == 0) {
    +            // Combine the meta data for parallel warps via shared memory.
    +            float2 meta[nmeta];
     #pragma unroll
    -        for (int imeta = 1; imeta < nmeta; ++imeta) {
    -            KQ_cmn = fmaxf(KQ_cmn, meta[imeta].x);
    -        }
    -#pragma unroll
    -        for (int offset = np*cols_per_warp/2; offset >= cols_per_warp; offset >>= 1) {
    -            if (offset < warp_size) {
    -                KQ_cmn = fmaxf(KQ_cmn, __shfl_xor_sync(0xFFFFFFFF, KQ_cmn, offset, warp_size));
    +            for (int imeta = 0; imeta < nmeta; ++imeta) {
    +                meta[imeta] = meta_ptr[imeta * warp_size * tile_stride/2];
                 }
    -        }
     
    -        float KQ_cms[nmeta]; // KQ combine max scale per warp.
    +            KQ_cmn = meta[0].x; // KQ combine max new, max between all parallel warps.
     #pragma unroll
    -        for (int imeta = 0; imeta < nmeta; ++imeta) {
    -            KQ_cms[imeta] = expf(meta[imeta].x - KQ_cmn);
    -        }
    +            for (int imeta = 1; imeta < nmeta; ++imeta) {
    +                KQ_cmn = fmaxf(KQ_cmn, meta[imeta].x);
    +            }
    +#pragma unroll
    +            for (int offset = np*cols_per_warp/2; offset >= cols_per_warp; offset >>= 1) {
    +                if (offset < warp_size) {
    +                    KQ_cmn = fmaxf(KQ_cmn, __shfl_xor_sync(0xFFFFFFFF, KQ_cmn, offset, warp_size));
    +                }
    +            }
     
    -        float KQ_crs = KQ_cms[0]*meta[0].y; // KQ combine rowsum, scaled sum of all parallel warps.
     #pragma unroll
    -        for (int imeta = 1; imeta < nmeta; ++imeta) {
    -            KQ_crs += KQ_cms[imeta]*meta[imeta].y;
    -        }
    +            for (int imeta = 0; imeta < nmeta; ++imeta) {
    +                KQ_cms[imeta] = expf(meta[imeta].x - KQ_cmn);
    +            }
    +
    +            KQ_crs = KQ_cms[0]*meta[0].y; // KQ combine rowsum, scaled sum of all parallel warps.
     #pragma unroll
    -        for (int offset = np*cols_per_warp/2; offset >= cols_per_warp; offset >>= 1) {
    -            if (offset < warp_size) {
    -                KQ_crs += __shfl_xor_sync(0xFFFFFFFF, KQ_crs, offset, warp_size);
    +            for (int imeta = 1; imeta < nmeta; ++imeta) {
    +                KQ_crs += KQ_cms[imeta]*meta[imeta].y;
    +            }
    +#pragma unroll
    +            for (int offset = np*cols_per_warp/2; offset >= cols_per_warp; offset >>= 1) {
    +                if (offset < warp_size) {
    +                    KQ_crs += __shfl_xor_sync(0xFFFFFFFF, KQ_crs, offset, warp_size);
    +                }
                 }
             }
     
             __syncthreads();
     
    -        // Write back combined meta data:
    +        if (threadIdx.y % np == 0) {
    +            // Write back combined meta data:
     #pragma unroll
    -        for (int imeta = 0; imeta < nmeta; ++imeta) {
    -            if (np*cols_per_warp >= warp_size || threadIdx.x < np*cols_per_warp) {
    -                // Combined KQ max scale + rowsum.
    -                meta_ptr[imeta * warp_size * tile_stride/2] = make_float2(KQ_cms[imeta], KQ_crs);
    +            for (int imeta = 0; imeta < nmeta; ++imeta) {
    +                if (np*cols_per_warp >= warp_size || threadIdx.x < np*cols_per_warp) {
    +                    // Combined KQ max scale + rowsum.
    +                    meta_ptr[imeta * warp_size * tile_stride/2] = make_float2(KQ_cms[imeta], KQ_crs);
    +                }
    +            }
    +
    +            // Combined KQ max + rowsum.
    +            static_assert(cols_per_warp <= warp_size);
    +            if (needs_fixup && (cols_per_warp == warp_size || threadIdx.x < cols_per_warp)) {
    +                float2 * dstk_fixup_meta = dstk_fixup + blockIdx.x*ncols;
    +                dstk_fixup_meta[(threadIdx.y/np)*cols_per_warp + threadIdx.x] = make_float2(KQ_cmn, KQ_crs);
    +            }
    +            if (is_fixup && (cols_per_warp == warp_size || threadIdx.x < cols_per_warp)) {
    +                float2 * dstk_fixup_meta = dstk_fixup + (gridDim.x + blockIdx.x)*ncols;
    +                dstk_fixup_meta[(threadIdx.y/np)*cols_per_warp + threadIdx.x] = make_float2(KQ_cmn, KQ_crs);
                 }
             }
    -
    -        // Combined KQ max + rowsum.
    -        static_assert(cols_per_warp <= warp_size);
    -        if (needs_fixup && (cols_per_warp == warp_size || threadIdx.x < cols_per_warp)) {
    -            float2 * dstk_fixup_meta = dstk_fixup + blockIdx.x*ncols;
    -            dstk_fixup_meta[(threadIdx.y/np)*cols_per_warp + threadIdx.x] = make_float2(KQ_cmn, KQ_crs);
    -        }
    -        if (is_fixup && (cols_per_warp == warp_size || threadIdx.x < cols_per_warp)) {
    -            float2 * dstk_fixup_meta = dstk_fixup + (gridDim.x + blockIdx.x)*ncols;
    -            dstk_fixup_meta[(threadIdx.y/np)*cols_per_warp + threadIdx.x] = make_float2(KQ_cmn, KQ_crs);
    -        }
    -    } else if (np > 1) {
    -        // Warps with threadIdx.y % np == 0 execute a __syncthreads() in the if branch.
    -        // Therefore, all other warps also need to execute a __syncthreads().
    -        // Otherwise the points at which warps synchronize with each other would become misaligned.
    -        __syncthreads();
         }
     
     #pragma unroll
    
    From 9a7570587ce908b0073a0458877205b80627f393 Mon Sep 17 00:00:00 2001
    From: DevVexus <63028748+devvexus@users.noreply.github.com>
    Date: Mon, 7 Sep 2026 02:12:50 -0500
    Subject: [PATCH 18/31] convert : write explicit recurrent_layers for
     Qwen3-Next / Qwen3.5 (#28208)
    
    Problem
    - Loader prefers `.attention.recurrent_layers`, falls back to `full_attention_interval` if missing
    - Converter only ever writes the interval. gguf-py has no constant/writer for the array
    - Interval can only describe evenly spaced full-attention layers. Any non-uniform `layer_types` gets reconstructed wrong
    - No error, no warning. Model loads, runs, wrong layers get wrong ops. Full-attn layers marked recurrent lose their KV cache
    - Every published Qwen3.5 checkpoint is uniform so nobody's hit it yet
    
    Repro
    12 layers, periods 4/3/5:
    
        layer:  0 1 2 3 4 5 6 7 8 9 10 11
        actual: L L L F L L F L L L  L  F
        loader: L L L F L L L F L L  L  F
                            ^ ^
    
    Layer 6 is full attn, loaded as recurrent. Layer 7 the reverse.
    52-layer non-uniform stack: 15/52 mis-typed.
    
    Fix
    - `constants.py`: add `Keys.Attention.RECURRENT_LAYERS` (name already registered in llama-arch.cpp)
    - `gguf_writer.py`: add `add_recurrent_layers()`, same shape as `add_rope_pattern()`
    - `conversion/qwen.py`: emit array from `layer_types` in `Qwen3NextModel.set_gguf_parameters` (covers 3-Next, 3.5, 3.5-MoE)
    
    Notes
    - Array is padded with `false` for MTP blocks. `get_key_or_arr` checks length against `n_layer_all`, which includes MTP. Matches the fallback's `i < n_layer()` guard
    - Interval is still written. Old builds only understand the interval
    - `layer_types` length != `num_hidden_layers` now raises in converter instead of producing a GGUF that fails at load
    
    Tested
    - End-to-end on a 62-layer non-uniform Qwen3.8-27B (2 linear layers removed). Loader reads the array, 62 blocks, 0 mismatches. Without fix: interval fallback, mis-typed
    - MTP padding NOT tested on a real MTP model. Reasoned from qwen35.cpp + get_key_or_arr. Would appreciate a check
    
    Co-authored-by: Claude Opus 5 
    ---
     conversion/qwen.py          | 7 +++++++
     gguf-py/gguf/constants.py   | 1 +
     gguf-py/gguf/gguf_writer.py | 3 +++
     3 files changed, 11 insertions(+)
    
    diff --git a/conversion/qwen.py b/conversion/qwen.py
    index 419611896..c7e0809f3 100644
    --- a/conversion/qwen.py
    +++ b/conversion/qwen.py
    @@ -379,6 +379,13 @@ class Qwen3NextModel(_QwenMtpMixin, Qwen2MoeModel):
             self.gguf_writer.add_ssm_group_count(self.hparams["linear_num_key_heads"])
             self.gguf_writer.add_ssm_time_step_rank(self.hparams["linear_num_value_heads"])
             self.gguf_writer.add_ssm_inner_size(self.hparams["linear_value_head_dim"] * self.hparams["linear_num_value_heads"])
    +        if (layer_types := self.hparams.get("layer_types")) is not None:
    +            n_layer = self.hparams["num_hidden_layers"]
    +            if len(layer_types) != n_layer:
    +                raise ValueError(f"layer_types has {len(layer_types)} entries, expected num_hidden_layers ({n_layer})")
    +            recurrent = [t == "linear_attention" for t in layer_types]
    +            recurrent += [False] * (self.block_count - n_layer)
    +            self.gguf_writer.add_recurrent_layers(recurrent)
             self.gguf_writer.add_full_attention_interval(self.hparams.get("full_attention_interval", 4))
             if (rope_dim := self.hparams.get("head_dim")) is None:
                 rope_dim = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
    diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py
    index d51e459dd..d3a639f37 100644
    --- a/gguf-py/gguf/constants.py
    +++ b/gguf-py/gguf/constants.py
    @@ -215,6 +215,7 @@ class Keys:
             KV_LORA_RANK_SWA             = "{arch}.attention.kv_lora_rank_swa"
             SHARED_KV_LAYERS             = "{arch}.attention.shared_kv_layers"
             SLIDING_WINDOW_PATTERN       = "{arch}.attention.sliding_window_pattern"
    +        RECURRENT_LAYERS             = "{arch}.attention.recurrent_layers"
             TEMPERATURE_SCALE            = "{arch}.attention.temperature_scale"
             ROPE_PATTERN                 = "{arch}.attention.rope_pattern"
     
    diff --git a/gguf-py/gguf/gguf_writer.py b/gguf-py/gguf/gguf_writer.py
    index 50e4d7c53..ed5a185b3 100644
    --- a/gguf-py/gguf/gguf_writer.py
    +++ b/gguf-py/gguf/gguf_writer.py
    @@ -841,6 +841,9 @@ class GGUFWriter:
             else:
                 self.add_array(key, value)
     
    +    def add_recurrent_layers(self, value: Sequence[bool]) -> None:
    +        self.add_array(Keys.Attention.RECURRENT_LAYERS.format(arch=self.arch), value)
    +
         def add_rope_pattern(self, value: Sequence[bool]) -> None:
             self.add_array(Keys.Attention.ROPE_PATTERN.format(arch=self.arch), value)
     
    
    From 5202104b59ada9005db079eea43882a2b7bf5802 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?=
     
    Date: Mon, 7 Sep 2026 09:14:32 +0200
    Subject: [PATCH 19/31] caps : recheck typed content if template checks for
     string (#28511)
    
    ---
     common/jinja/caps.cpp    | 32 ++++++++++++++++++++++++++++++++
     common/jinja/runtime.cpp | 10 ++++++++--
     2 files changed, 40 insertions(+), 2 deletions(-)
    
    diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp
    index 9971c021e..c5962ab77 100644
    --- a/common/jinja/caps.cpp
    +++ b/common/jinja/caps.cpp
    @@ -117,6 +117,7 @@ caps caps_get(jinja::program & prog) {
     
         JJ_DEBUG("%s\n", ">>> Running capability check: typed content");
     
    +    bool checks_for_string = false;
         static const std::string content_marker = "STRING_MARKER";
     
         // case: typed content support
    @@ -136,6 +137,10 @@ caps caps_get(jinja::program & prog) {
             [&](context &, bool success, value & messages, value &, const std::string & rendered) {
                 auto & content = messages->at(0)->at("content");
                 caps_print_stats(content, "messages[0].content");
    +            if (has_op(content, "test_is_string")) {
    +                // checked if content is string
    +                checks_for_string = true;
    +            }
                 bool used_as_array = has_op(content, "selectattr") || has_op(content, "array_access");
                 if (used_as_array) {
                     // accessed as an array
    @@ -151,6 +156,33 @@ caps caps_get(jinja::program & prog) {
             }
         );
     
    +    if (checks_for_string) {
    +        caps_try_execute(
    +            prog,
    +            [&]() {
    +                // messages
    +                return json::array({
    +                    {
    +                        {"role", "user"},
    +                        {"content", json::array({
    +                        })}
    +                    }
    +                });
    +            },
    +            nullptr, // ctx_fn
    +            nullptr, // tools_fn
    +            [&](context &, bool success, value & messages, value &, const std::string &) {
    +                auto & content = messages->at(0)->at("content");
    +                caps_print_stats(content, "messages[0].content");
    +                bool used_as_array = has_op(content, "selectattr") || has_op(content, "array_access");
    +                if (used_as_array && success) {
    +                    // accessed as an array
    +                    result.supports_typed_content = true;
    +                }
    +            }
    +        );
    +    }
    +
         JJ_DEBUG("%s\n", ">>> Running capability check: system prompt");
     
         // case: system prompt support
    diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp
    index 4ce79e32a..b02992529 100644
    --- a/common/jinja/runtime.cpp
    +++ b/common/jinja/runtime.cpp
    @@ -412,12 +412,18 @@ value test_expression::execute_impl(context & ctx) {
             throw std::runtime_error("Invalid test expression");
         }
     
    -    auto it = builtins.find("test_is_" + test_id);
    -    JJ_DEBUG("Test expression %s '%s' %s (using function 'test_is_%s')", operand->type().c_str(), test_id.c_str(), negate ? "(negate)" : "", test_id.c_str());
    +    const std::string test_name = "test_is_" + test_id;
    +    auto it = builtins.find(test_name);
    +    JJ_DEBUG("Test expression %s '%s' %s (using function '%s')", operand->type().c_str(), test_id.c_str(), negate ? "(negate)" : "", test_name.c_str());
         if (it == builtins.end()) {
             throw std::runtime_error("Unknown test '" + test_id + "'");
         }
     
    +    if (ctx.is_get_stats) {
    +        value_t::stats_t::mark_used(input);
    +        input->stats.ops.insert(test_name);
    +    }
    +
         auto res = it->second(args);
     
         if (negate) {
    
    From 1173700b9c12fa7d7ccfd74752a35ec2d7b4552b Mon Sep 17 00:00:00 2001
    From: Daniel Bevenius 
    Date: Mon, 7 Sep 2026 12:11:40 +0200
    Subject: [PATCH 20/31] examples : print ggml_version and ggml_commit in
     test-cmake [no ci] (#28538)
    
    This commit adds the printing of the ggml version and commit to the
    test-cmake example.
    
    The motivation is just to be able to quickly verify that the correct
    version of ggml is being used.
    
    Example output:
    ```console
    test-cmake] llama.cpp version: 0.4.0-dev, build: 10837 (5202104b5)
    [test-cmake] ggml version: 0.23.0, commit: 5202104b5
    [test-cmake] Initializing backend...
    ...
    ```
    ---
     examples/test-cmake/test-cmake.cpp | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/examples/test-cmake/test-cmake.cpp b/examples/test-cmake/test-cmake.cpp
    index c5c4765b4..dc1a9ae60 100644
    --- a/examples/test-cmake/test-cmake.cpp
    +++ b/examples/test-cmake/test-cmake.cpp
    @@ -2,8 +2,9 @@
     #include 
     
     int main(void) {
    -    printf("[test-cmake] version: %s, build: %d (%s)\n",
    +    printf("[test-cmake] llama.cpp version: %s, build: %d (%s)\n",
                llama_version(), LLAMA_BUILD_NUMBER, LLAMA_BUILD_COMMIT);
    +    printf("[test-cmake] ggml version: %s, commit: %s\n", ggml_version(), ggml_commit());
         printf("[test-cmake] Initializing backend...\n");
         llama_backend_init();
         printf("[test-cmake] Backend initialized.\n");
    
    From 0cae43063cf15170e91a2ff4d034da0ecef4a1b2 Mon Sep 17 00:00:00 2001
    From: Jeff Bolz 
    Date: Mon, 7 Sep 2026 05:22:10 -0500
    Subject: [PATCH 21/31] vulkan: support type-aligned GET_ROWS (#28253)
    
    * vulkan: fall back to CPU for GET_ROWS with misaligned offsets
    
    The Vulkan GET_ROWS shader asserts when a tensor's backing-buffer offset
    plus view_offs is misaligned w.r.t. minStorageBufferOffsetAlignment
    (see init_pushconst_tensor_offsets). Previously this caused a hard crash
    on models using ggml_view + ggml_get_rows (e.g. Qwen3-TTS, Qwen3-VL).
    
    Return false from supports_op() in the misaligned case so the scheduler
    falls back to CPU, matching the existing pattern for PAD_REFLECT_1D and
    other unsupported op/shape combinations.
    
    Repro: llama-tts -m Qwen3-TTS-*.gguf -mm mmproj-*.gguf -ngl 99
    Crash: GGML_ASSERT(dst->op != GGML_OP_GET_ROWS || (a_offset == 0 && ...)) failed
    
    * vulkan: trim comment for GET_ROWS misalign fallback
    
    * vulkan: fix file corruption in gated_linear_attn struct
    
    * vulkan: properly handle misaligned offsets in GET_ROWS quantized path
    
    - get_rows_quant.comp was missing get_aoffset()/get_boffset()/get_doffset()
      calls that are already present in get_rows.comp, causing GGML_ASSERT crashes
      when GET_ROWS operates on views with non-zero view_offs, as produced by
      KV cache slices in Qwen3-TTS and Qwen3-VL.
    - Remove the defensive misalignment GGML_ASSERT in init_pushconst_tensor_offsets
      for the binary push-constants specialization, since both get_rows.comp and
      get_rows_quant.comp now correctly apply per-tensor base offsets.
    - Remove the workaround CPU fallback in supports_op() for GET_ROWS, since the
      Vulkan backend now handles misaligned offsets natively (no more bailout).
    - Add backend test coverage with view_src0=true (ggml_view_4d into a padded
      tensor) for F32, F16, Q4_0, Q4_K, Q8_0, and I32 types, exercising both the
      non-quantized (get_rows.comp) and quantized (get_rows_quant.comp) paths
      with non-zero view_offs that reproduce the original Qwen3-TTS crash.
    
    * tests: trim redundant comments in test_get_rows vs0 region
    
    * tests: trim redundant comments in test_get_rows vs0 region (follow-up)
    
    * vulkan: bind tensor base for binary ops, pass full view_offs via push constants
    
    For ops using vk_op_binary_push_constants (GET_ROWS, ADD, SUB, MUL, etc.),
    bind the view_src base and pass the full view_offs divided by type_size via
    push constant misalign_offsets. This avoids truncation when misalign_bytes is
    not a multiple of quantized block size.
    
    ggml_vk_tensor_subbuffer gains a use_view_offs parameter. When false, the
    binding points to vk_tensor_offset (base) and size includes view_offs.
    init_pushconst_tensor_offsets computes a/b/d_offset directly from
    tensor->view_offs, which is always row-aligned and therefore exact.
    
    Added non-zero view offset (offset_rows=3) backend tests for GET_ROWS across
    all_types with be1={1,7}, v={false,true}, skipping gradient setup for view
    tensors (GGML_OP_VIEW fails ggml_set_param).
    
    All 223 GET_ROWS tests pass on Vulkan (NVIDIA RTX 5060 Ti).
    
    * vulkan: bind aligned offset for binary ops, pass adjusted misalign via push constants
    
    For ops using vk_op_binary_push_constants (GET_ROWS, ADD, SUB, etc.), bind
    the buffer to an aligned position near the view offset (not the tensor base)
    and pass the adjusted misalignment via push constants.
    
    ggml_vk_get_adjusted_misalign finds the smallest misalign that is both a
    multiple of minStorageBufferOffsetAlignment and type_size, ensuring
    misalign/type_size is exact (no truncation for quantized block types).
    
    ggml_vk_tensor_subbuffer gains use_view_offs parameter. When false, binds
    to (target - adjusted_misalign) instead of the view_src base, keeping the
    offset small enough for 16-bit/8-bit push constant fields.
    
    Added non-zero view offset (offset_rows=3) backend tests for GET_ROWS across
    all_types with be1={1,7}, v={false,true}, skipping gradient setup for view
    tensors (GGML_OP_VIEW fails ggml_set_param).
    
    All 223 GET_ROWS tests pass on Vulkan (NVIDIA RTX 5060 Ti).
    
    * vulkan: bind aligned offset for binary ops, fix UMA offset mismatch
    
    For ops using vk_op_binary_push_constants (GET_ROWS, ADD, SUB, etc.), bind
    the buffer to an aligned position near the view offset (not the tensor base)
    and pass the adjusted misalignment via push constants.
    
    Added ggml_vk_tensor_physical_offset to unify physical offset lookup across
    UMA and non-UMA devices. On UMA, resolves via ggml_vk_host_get(tensor->data);
    otherwise uses vk_tensor_offset(t) + t->view_offs. Both get_misalign_bytes and
    the new ggml_vk_get_adjusted_misalign helper build on top of this function,
    so buffer bindings and push constant offsets are always consistent regardless
    of device memory model.
    
    ggml_vk_get_adjusted_misalign finds the smallest misalign that is both a
    multiple of minStorageBufferOffsetAlignment and type_size, ensuring
    misalign/type_size is exact (no truncation for quantized block types) while
    remaining small enough for 16-bit/8-bit push constant fields
    (adjusted_misalign < lcm(align, type_size)).
    
    ggml_vk_tensor_subbuffer gains use_view_offs parameter. When false, binds
    to (physical_offset - adjusted_misalign) on both UMA and discrete GPUs,
    fixing a bug where the UMA host_get path previously skipped the adjusted
    misalign binding and returned the target offset directly.
    
    Added non-zero view offset (offset_rows=3) backend tests for GET_ROWS across
    all_types with be1={1,7}, v={false,true}, skipping gradient setup for view
    tensors (GGML_OP_VIEW fails ggml_set_param).
    
    All 223 GET_ROWS tests pass on Vulkan (NVIDIA GeForce RTX 5060 Ti).
    
    * finish misalignment fix
    
    * supports_op changes for openvino/webgpu
    
    ---------
    
    Co-authored-by: AiChiTuDouPian <15327701848@qq.com>
    ---
     ggml/src/ggml-openvino/ggml-openvino.cpp      |  4 ++
     ggml/src/ggml-vulkan/ggml-vulkan.cpp          | 45 ++++++++++++++---
     .../vulkan-shaders/get_rows_quant.comp        |  6 +--
     ggml/src/ggml-webgpu/ggml-webgpu.cpp          | 10 +++-
     tests/test-backend-ops.cpp                    | 49 +++++++++++++------
     5 files changed, 89 insertions(+), 25 deletions(-)
    
    diff --git a/ggml/src/ggml-openvino/ggml-openvino.cpp b/ggml/src/ggml-openvino/ggml-openvino.cpp
    index 4b1789713..a79562278 100644
    --- a/ggml/src/ggml-openvino/ggml-openvino.cpp
    +++ b/ggml/src/ggml-openvino/ggml-openvino.cpp
    @@ -1091,6 +1091,10 @@ static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) {
             if (op->ne[3] != 1) {
                 return {false, "GET_ROWS/SET_ROWS with ne[3] != 1 (ne[3]=" + std::to_string(op->ne[3]) + ") is not supported"};
             }
    +        if (op->op == GGML_OP_GET_ROWS && ggml_is_quantized(op->src[0]->type) &&
    +            op->src[0]->view_src != nullptr && op->src[0]->view_offs != 0) {
    +            return {false, "GET_ROWS with a nonzero quantized src0 view offset is not supported"};
    +        }
             if (op->op == GGML_OP_GET_ROWS && ggml_openvino_get_device_name() == "GPU" &&
                 op->src[0]->type == GGML_TYPE_BF16) {
                 return {false, "GET_ROWS with BF16 src0 is not supported on GPU"};
    diff --git a/ggml/src/ggml-vulkan/ggml-vulkan.cpp b/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    index 9b47c6c95..62f90847b 100644
    --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    @@ -2515,9 +2515,38 @@ static uint64_t vk_tensor_offset(const ggml_tensor * tensor) {
         return (uint8_t *) tensor->data - (uint8_t *) vk_ptr_base;
     }
     
    -static uint32_t get_misalign_bytes(const ggml_backend_vk_context * ctx, const ggml_tensor * t)
    -{
    -    return ((vk_tensor_offset(t) + t->view_offs) & (ctx->device->properties.limits.minStorageBufferOffsetAlignment - 1));;
    +static void ggml_vk_host_get(const vk_device& device, const void * ptr, vk_buffer& buf, size_t& buf_offset);
    +
    +static size_t ggml_vk_tensor_buffer_offset(const ggml_backend_vk_context * ctx, const ggml_tensor * t) {
    +    // vk_tensor_offset() is relative to vk_ptr_base, but mapped host tensors need an offset relative to their Vulkan buffer.
    +    if (ctx->device->uma) {
    +        vk_buffer buf = nullptr;
    +        size_t off = 0;
    +        ggml_vk_host_get(ctx->device, t->data, buf, off);
    +        if (buf) {
    +            return off;
    +        }
    +    }
    +    return (size_t)(vk_tensor_offset(t) + t->view_offs);
    +}
    +
    +static size_t ggml_vk_descriptor_offset(size_t tensor_offset, size_t alignment, size_t type_size) {
    +    // Move the descriptor back until its distance to the tensor is divisible by the tensor type size.
    +    size_t descriptor_offset = tensor_offset & ~(alignment - 1);
    +    while ((tensor_offset - descriptor_offset) % type_size != 0) {
    +        GGML_ASSERT(descriptor_offset >= alignment);
    +        descriptor_offset -= alignment;
    +    }
    +
    +    return descriptor_offset;
    +}
    +
    +static uint32_t get_misalign_bytes(const ggml_backend_vk_context * ctx, const ggml_tensor * t) {
    +    const size_t tensor_offset = ggml_vk_tensor_buffer_offset(ctx, t);
    +    const size_t descriptor_offset = ggml_vk_descriptor_offset(
    +        tensor_offset, ctx->device->properties.limits.minStorageBufferOffsetAlignment, ggml_type_size(t->type));
    +    GGML_ASSERT(tensor_offset - descriptor_offset <= UINT32_MAX);
    +    return tensor_offset - descriptor_offset;
     }
     
     static uint32_t ggml_vk_concat_unit_size(ggml_type type) {
    @@ -8265,10 +8294,12 @@ static vk_subbuffer ggml_vk_tensor_subbuffer(
     
         size_t size = ggml_nbytes(tensor);
     
    -    size_t misalign_bytes = offset & (ctx->device->properties.limits.minStorageBufferOffsetAlignment - 1);
    +    const size_t descriptor_offset = ggml_vk_descriptor_offset(
    +        offset, ctx->device->properties.limits.minStorageBufferOffsetAlignment, ggml_type_size(tensor->type));
    +    const size_t misalign_bytes = offset - descriptor_offset;
         // The shader must support misaligned offsets when indexing into the buffer
         GGML_ASSERT(allow_misalign || misalign_bytes == 0);
    -    offset &= ~misalign_bytes;
    +    offset = descriptor_offset;
         size += misalign_bytes;
     
         return vk_subbuffer{buffer, offset, size};
    @@ -12155,7 +12186,9 @@ template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk
         const uint32_t b_offset = get_misalign_bytes(ctx, src1) / ggml_type_size(src1->type);
         const uint32_t d_offset = get_misalign_bytes(ctx, dst) / ggml_type_size(dst->type);
     
    -    GGML_ASSERT(dst->op != GGML_OP_GET_ROWS || (a_offset == 0 && b_offset == 0 && d_offset == 0));
    +    GGML_ASSERT(a_offset <= 0xFFFF);
    +    GGML_ASSERT(b_offset <= 0xFF);
    +    GGML_ASSERT(d_offset <= 0xFF);
     
         p.misalign_offsets = (a_offset << 16) | (b_offset << 8) | d_offset;
     
    diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/get_rows_quant.comp b/ggml/src/ggml-vulkan/vulkan-shaders/get_rows_quant.comp
    index 9dba437ed..19af30ac9 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/get_rows_quant.comp
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/get_rows_quant.comp
    @@ -27,10 +27,10 @@ void main() {
                 const uint i11 = gid_z / p.ne12;
                 const uint i12 = gid_z % p.ne12;
     
    -            const uint i01 = data_b[i10*p.nb10 + i11*p.nb11 + i12*p.nb12];
    +            const uint i01 = data_b[get_boffset() + i10*p.nb10 + i11*p.nb11 + i12*p.nb12];
     
    -            const uint a_offset = i01*p.nb01 + i11*p.nb02 + i12*p.nb03;
    -            const uint d_offset = i10*p.nb21 + i11*p.nb22 + i12*p.nb23;
    +            const uint a_offset = get_aoffset() + i01*p.nb01 + i11*p.nb02 + i12*p.nb03;
    +            const uint d_offset = get_doffset() + i10*p.nb21 + i11*p.nb22 + i12*p.nb23;
     
                 const uint ib = a_offset + i00/QUANT_K; // block index
                 const uint iqs = (i00%QUANT_K)/QUANT_R; // quant index
    diff --git a/ggml/src/ggml-webgpu/ggml-webgpu.cpp b/ggml/src/ggml-webgpu/ggml-webgpu.cpp
    index 1a43c7273..2e6c5a8c5 100644
    --- a/ggml/src/ggml-webgpu/ggml-webgpu.cpp
    +++ b/ggml/src/ggml-webgpu/ggml-webgpu.cpp
    @@ -4323,13 +4323,21 @@ static bool ggml_backend_webgpu_device_supports_op(ggml_backend_dev_t dev, const
                                 op->type == GGML_TYPE_Q4_0) &&
                                src0->type == GGML_TYPE_F32 && (src1->type == GGML_TYPE_I64 || src1->type == GGML_TYPE_I32));
                 break;
    -        case GGML_OP_GET_ROWS:
    +        case GGML_OP_GET_ROWS: {
    +            const size_t storage_alignment =
    +                ctx->webgpu_global_ctx->capabilities.limits.minStorageBufferOffsetAlignment;
    +            const size_t src_address_unit =
    +                src0->type == GGML_TYPE_F32 && op->ne[0] % 4 == 0 ? 4 * sizeof(float) : ggml_type_size(src0->type);
    +            if (ggml_webgpu_tensor_misalignment(src0, storage_alignment) % src_address_unit != 0) {
    +                break;
    +            }
                 if (src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16 || ggml_webgpu_supported_qtype(src0->type)) {
                     supports_op = (op->type == GGML_TYPE_F32);
                 } else if (src0->type == GGML_TYPE_I32) {
                     supports_op = op->type == GGML_TYPE_I32;
                 }
                 break;
    +        }
             case GGML_OP_MUL_MAT:
                 {
                     switch (src1->type) {
    diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp
    index 6a8cfd46d..eeaca940f 100644
    --- a/tests/test-backend-ops.cpp
    +++ b/tests/test-backend-ops.cpp
    @@ -2336,27 +2336,40 @@ struct test_get_rows : public test_case {
         const int r; // rows to get
         const int be1; // batch size
         const int be2; // batch size
    -    const bool v; // view (non-contiguous src1)
    +    const bool v; // view src1
    +    const bool vs0; // view src0
     
         std::string vars() override {
    -        return VARS_TO_STR7(type, n, m, r, be1, be2, v);
    +        return VARS_TO_STR8(type, n, m, r, be1, be2, v, vs0);
         }
     
    -    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)
    -        : type(type), n(n), m(m), r(r), be1(be1), be2(be2), v(v) {}
    +    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) {}
     
         ggml_tensor * build_graph(ggml_context * ctx) override {
    -        ggml_tensor * in = ggml_new_tensor_4d(ctx, type, n, m, be1, be2);
    -        ggml_set_name(in, "in");
    +        ggml_tensor * in;
    +        if (vs0) {
    +            const int offset_rows = 3;
    +            const int padded_m = m + offset_rows;
    +            ggml_tensor * in_padded = ggml_new_tensor_4d(ctx, type, n, padded_m, be1, be2);
    +            ggml_set_name(in_padded, "in_padded");
    +            in = ggml_view_4d(ctx, in_padded, n, m, be1, be2,
    +                              in_padded->nb[1], in_padded->nb[2], in_padded->nb[3],
    +                              offset_rows * in_padded->nb[1]);
    +            ggml_set_name(in, "in_view");
    +        } else {
    +            in = ggml_new_tensor_4d(ctx, type, n, m, be1, be2);
    +            ggml_set_name(in, "in");
    +        }
     
    -        ggml_tensor * rows = ggml_new_tensor_3d(ctx, GGML_TYPE_I32, r, be1, be2);
    +        ggml_tensor * rows = ggml_new_tensor_3d(ctx, GGML_TYPE_I32, v ? r + 1 : r, be1, be2);
             ggml_set_name(rows, "rows");
             if (v) {
    -            rows = ggml_view_3d(ctx, rows, r/2, be1, be2, rows->nb[1], rows->nb[2], 0);
    +            rows = ggml_view_3d(ctx, rows, r/2, be1, be2, rows->nb[1], rows->nb[2], rows->nb[0]);
                 ggml_set_name(rows, "view_of_rows");
             }
     
    -        const bool grad_supported = ggml_is_matrix(in) && ggml_is_vector(rows);
    +        const bool grad_supported = !vs0 && ggml_is_matrix(in) && ggml_is_vector(rows);
             if (grad_supported) {
                 ggml_set_param(in);
                 // rows is a constant input -> no gradients
    @@ -2370,14 +2383,16 @@ struct test_get_rows : public test_case {
     
         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) {
    -                if (ggml_is_view_op(t->op)) { continue; }
                     // rows
    -                std::vector data(r*be1*be2);
    -                for (int i = 0; i < r*be1*be2; i++) {
    +                std::vector data(ggml_nelements(t));
    +                for (size_t i = 0; i < data.size(); i++) {
                         data[i] = rand() % m;
                     }
    -                ggml_backend_tensor_set(t, data.data(), 0, r * be1 * be2 * sizeof(int));
    +                ggml_backend_tensor_set(t, data.data(), 0, data.size() * sizeof(int));
                 } else {
                     init_tensor_uniform(t);
                 }
    @@ -8848,13 +8863,17 @@ static std::vector> make_test_cases_eval() {
         for (ggml_type type : all_types) {
             for (int b : {1, 7}) {
                 for (bool v : {false, true}) {
    -                test_cases.emplace_back(new test_get_rows(type, 256, 5, 4, b, 1, v));
    +                for (bool vs0 : {false, true}) {
    +                    test_cases.emplace_back(new test_get_rows(type, 256, 5, 4, b, 1, v, vs0));
    +                }
                 }
             }
         }
         for (int b : {1, 7}) {
             for (bool v : {false, true}) {
    -            test_cases.emplace_back(new test_get_rows(GGML_TYPE_I32, 256, 5, 4, b, 1, v));
    +            for (bool vs0 : {false, true}) {
    +                test_cases.emplace_back(new test_get_rows(GGML_TYPE_I32, 256, 5, 4, b, 1, v, vs0));
    +            }
             }
         }
     
    
    From 73ab7599b553c03f6f5d2db24a18ad76f2eb36a3 Mon Sep 17 00:00:00 2001
    From: Pranesh Gonegandla 
    Date: Mon, 7 Sep 2026 11:36:58 +0000
    Subject: [PATCH 22/31] CUDA: branchless Q4_K/Q5_K unpack to speed up mmvq, L2
     prefetch on DGX Spark (#26705)
    
    * Update Q4_K and Q5_K to use branchless computation, which stops the scale unpack being re-executed for every column in mmvq, improving perf at batch sizes > 1
    
    * Gating the change off from DGX Spark due to no gain
    
    * Adding prefetch gated to Spark, making branchless change in Q4_K and Q5_K general and modifying switch points based on latest perf data
    
    * Guard the mmvq L2 prefetch against MUSA as well as HIP
    
    * Define the mmvq L2 prefetch only under the Spark guard
    
    * Update switch point for Q4_K to accommodate more models
    
    * Remove stale comments
    
    * Add block_size to ggml_cuda_type_traits and create a separate mmvq_should_prefetch function
    
    * Rename block_size to bs for cleaner indentation
    
    * Fix build error on non-Spark CUDA arch with appropriate conditional around new function added
    
    ---------
    
    Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com>
    ---
     ggml/src/ggml-cuda/common.cuh  | 24 +++++++++++++++
     ggml/src/ggml-cuda/mmvq.cu     | 55 +++++++++++++++++++++++++++++++---
     ggml/src/ggml-cuda/vecdotq.cuh | 41 +++++++++++++++----------
     3 files changed, 100 insertions(+), 20 deletions(-)
    
    diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh
    index ed0ea60bd..7fb04c95f 100644
    --- a/ggml/src/ggml-cuda/common.cuh
    +++ b/ggml/src/ggml-cuda/common.cuh
    @@ -976,6 +976,7 @@ template<>
     struct ggml_cuda_type_traits {
         static constexpr int qk = 1;
         static constexpr int qr = 1;
    +    static constexpr int bs = sizeof(ggml_half);
     };
     
     template<>
    @@ -983,6 +984,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK1_0;
         static constexpr int qr = QR1_0;
         static constexpr int qi = QI1_0;
    +    static constexpr int bs = sizeof(block_q1_0);
     };
     
     template<>
    @@ -990,6 +992,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK2_0;
         static constexpr int qr = QR2_0;
         static constexpr int qi = QI2_0;
    +    static constexpr int bs = sizeof(block_q2_0);
     };
     
     template<>
    @@ -997,6 +1000,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK4_0;
         static constexpr int qr = QR4_0;
         static constexpr int qi = QI4_0;
    +    static constexpr int bs = sizeof(block_q4_0);
     };
     
     template<>
    @@ -1004,6 +1008,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK4_1;
         static constexpr int qr = QR4_1;
         static constexpr int qi = QI4_1;
    +    static constexpr int bs = sizeof(block_q4_1);
     };
     
     template<>
    @@ -1011,6 +1016,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK5_0;
         static constexpr int qr = QR5_0;
         static constexpr int qi = QI5_0;
    +    static constexpr int bs = sizeof(block_q5_0);
     };
     
     template<>
    @@ -1018,6 +1024,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK5_1;
         static constexpr int qr = QR5_1;
         static constexpr int qi = QI5_1;
    +    static constexpr int bs = sizeof(block_q5_1);
     };
     
     template<>
    @@ -1025,6 +1032,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK8_0;
         static constexpr int qr = QR8_0;
         static constexpr int qi = QI8_0;
    +    static constexpr int bs = sizeof(block_q8_0);
     };
     
     template<>
    @@ -1032,6 +1040,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_MXFP4;
         static constexpr int qr = QR_MXFP4;
         static constexpr int qi = QI_MXFP4;
    +    static constexpr int bs = sizeof(block_mxfp4);
     };
     
     template<>
    @@ -1039,6 +1048,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_NVFP4;
         static constexpr int qr = QR_NVFP4;
         static constexpr int qi = QI_NVFP4;
    +    static constexpr int bs = sizeof(block_nvfp4);
     };
     
     template<>
    @@ -1046,6 +1056,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR2_K;
         static constexpr int qi = QI2_K;
    +    static constexpr int bs = sizeof(block_q2_K);
     };
     
     template<>
    @@ -1053,6 +1064,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR3_K;
         static constexpr int qi = QI3_K;
    +    static constexpr int bs = sizeof(block_q3_K);
     };
     
     template<>
    @@ -1060,6 +1072,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR4_K;
         static constexpr int qi = QI4_K;
    +    static constexpr int bs = sizeof(block_q4_K);
     };
     
     template<>
    @@ -1067,6 +1080,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR5_K;
         static constexpr int qi = QI5_K;
    +    static constexpr int bs = sizeof(block_q5_K);
     };
     
     template<>
    @@ -1074,6 +1088,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR6_K;
         static constexpr int qi = QI6_K;
    +    static constexpr int bs = sizeof(block_q6_K);
     };
     
     template<>
    @@ -1081,6 +1096,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR2_XXS;
         static constexpr int qi = QI2_XXS;
    +    static constexpr int bs = sizeof(block_iq2_xxs);
     };
     
     template<>
    @@ -1088,6 +1104,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR2_XS;
         static constexpr int qi = QI2_XS;
    +    static constexpr int bs = sizeof(block_iq2_xs);
     };
     
     template<>
    @@ -1095,6 +1112,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR2_S;
         static constexpr int qi = QI2_S;
    +    static constexpr int bs = sizeof(block_iq2_s);
     };
     
     template<>
    @@ -1102,6 +1120,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR3_XXS;
         static constexpr int qi = QI3_XXS;
    +    static constexpr int bs = sizeof(block_iq3_xxs);
     };
     
     template<>
    @@ -1109,6 +1128,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR1_S;
         static constexpr int qi = QI1_S;
    +    static constexpr int bs = sizeof(block_iq1_s);
     };
     
     template<>
    @@ -1116,6 +1136,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR1_M;
         static constexpr int qi = QI1_M;
    +    static constexpr int bs = sizeof(block_iq1_m);
     };
     
     template<>
    @@ -1123,6 +1144,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK4_NL;
         static constexpr int qr = QR4_NL;
         static constexpr int qi = QI4_NL;
    +    static constexpr int bs = sizeof(block_iq4_nl);
     };
     
     template<>
    @@ -1130,6 +1152,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR4_XS;
         static constexpr int qi = QI4_XS;
    +    static constexpr int bs = sizeof(block_iq4_xs);
     };
     
     template<>
    @@ -1137,6 +1160,7 @@ struct ggml_cuda_type_traits {
         static constexpr int qk = QK_K;
         static constexpr int qr = QR3_S;
         static constexpr int qi = QI3_S;
    +    static constexpr int bs = sizeof(block_iq3_s);
     };
     
     //////////////////////
    diff --git a/ggml/src/ggml-cuda/mmvq.cu b/ggml/src/ggml-cuda/mmvq.cu
    index f65e0fbcd..6305230b1 100644
    --- a/ggml/src/ggml-cuda/mmvq.cu
    +++ b/ggml/src/ggml-cuda/mmvq.cu
    @@ -6,6 +6,35 @@
     #include 
     #include 
     
    +// only enabled on DGX Spark, where it is a gain on every type below. On the higher-bandwidth parts the kernel
    +// has little exposed latency left to hide and the extra requests cost more than they save.
    +// For perf data, see https://github.com/ggml-org/llama.cpp/pull/26705#issuecomment-5569335031
    +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == GGML_CUDA_CC_DGX_SPARK
    +// returns true only for those quants that benefit from prefetch and false otherwise
    +static constexpr __host__ __device__ bool mmvq_should_prefetch(ggml_type type) {
    +    switch (type) {
    +        case GGML_TYPE_Q4_0:
    +        case GGML_TYPE_Q5_0:
    +        case GGML_TYPE_Q8_0:
    +        case GGML_TYPE_MXFP4:
    +        case GGML_TYPE_Q3_K:
    +        case GGML_TYPE_Q4_K:
    +        case GGML_TYPE_Q5_K:
    +        case GGML_TYPE_Q6_K:
    +        case GGML_TYPE_IQ1_M:
    +        case GGML_TYPE_IQ4_NL:
    +        case GGML_TYPE_IQ4_XS:
    +            return true;
    +        default:
    +            return false;
    +    }
    +}
    +
    +static __device__ __forceinline__ void mmvq_prefetch_l2(const void * p) {
    +    asm volatile("prefetch.global.L2 [%0];" :: "l"(p));
    +}
    +#endif
    +
     typedef float (*vec_dot_q_cuda_t)(const void * __restrict__ vbq, const block_q8_1 * __restrict__ bq8_1, const int & kbx, const int & iqs);
     
     static constexpr __device__ vec_dot_q_cuda_t get_vec_dot_q_cuda(ggml_type type) {
    @@ -298,9 +327,6 @@ bool ggml_cuda_should_use_mmvq(enum ggml_type type, int cc, int64_t ne11) {
                     return ne11 <= 4;
                 case GGML_TYPE_Q3_K:
                     return ne11 <= 6;
    -            case GGML_TYPE_Q4_K:
    -            case GGML_TYPE_Q5_K:
    -                return ne11 <= 7;
                 default:
                     return ne11 <= MMVQ_MAX_BATCH_SIZE;
             }
    @@ -310,8 +336,9 @@ bool ggml_cuda_should_use_mmvq(enum ggml_type type, int cc, int64_t ne11) {
                 case GGML_TYPE_Q2_K:
                 case GGML_TYPE_Q3_K:
                 case GGML_TYPE_Q4_K:
    -            case GGML_TYPE_Q5_K:
                     return ne11 <= 5;
    +            case GGML_TYPE_Q5_K:
    +                return ne11 <= 6;
                 case GGML_TYPE_Q6_K:
                     return ne11 <= 7;
                 default:
    @@ -675,6 +702,26 @@ static __global__ void mul_mat_vec_q(
             // x block quant index when casting the quants to int
             const int kqs = vdr * (tid % (qi/vdr));
     
    +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == GGML_CUDA_CC_DGX_SPARK
    +        // start the next iterations' weight loads early
    +        if constexpr (mmvq_should_prefetch(type)) {
    +            constexpr int pf_dist = 2; // loop iterations, not blocks
    +            const int kbx_pf = kbx + pf_dist*blocks_per_iter;
    +            if (kbx_pf < blocks_per_row_x) {
    +#pragma unroll
    +                for (int i = 0; i < rows_per_cuda_block; ++i) {
    +                    const size_t off = (size_t)(kbx_offset + i*stride_row_x + kbx_pf) * ggml_cuda_type_traits::bs;
    +                    mmvq_prefetch_l2((const char *) vx + off);
    +                    if constexpr (has_fusion) {
    +                        if (use_gate) {
    +                            mmvq_prefetch_l2((const char *) vgate + off);
    +                        }
    +                    }
    +                }
    +            }
    +        }
    +#endif
    +
     #pragma unroll
             for (int j = 0; j < ncols_dst; ++j) {
     #pragma unroll
    diff --git a/ggml/src/ggml-cuda/vecdotq.cuh b/ggml/src/ggml-cuda/vecdotq.cuh
    index ec117c57d..f2a6f2009 100644
    --- a/ggml/src/ggml-cuda/vecdotq.cuh
    +++ b/ggml/src/ggml-cuda/vecdotq.cuh
    @@ -936,16 +936,20 @@ static __device__ __forceinline__ float vec_dot_q4_K_q8_1(
         v[0] = q4[0];
         v[1] = q4[4];
     
    +    // branchless so nvcc can hoist this out of the ncols_dst loop
         const uint16_t * scales = (const uint16_t *)bq4_K->scales;
    +    const int j  = bq8_offset/2;
    +    const int jm = j & 1;
    +
    +    const uint32_t s0 = scales[jm + 0];
    +    const uint32_t s2 = scales[jm + 2];
    +    const uint32_t s4 = scales[jm + 4];
    +
    +    const uint32_t hi = (uint32_t) -(int32_t) (j >= 2);
    +
         uint16_t aux[2];
    -    const int j = bq8_offset/2;
    -    if (j < 2) {
    -        aux[0] = scales[j+0] & 0x3f3f;
    -        aux[1] = scales[j+2] & 0x3f3f;
    -    } else {
    -        aux[0] = ((scales[j+2] >> 0) & 0x0f0f) | ((scales[j-2] & 0xc0c0) >> 2);
    -        aux[1] = ((scales[j+2] >> 4) & 0x0f0f) | ((scales[j-0] & 0xc0c0) >> 2);
    -    }
    +    aux[0] = (uint16_t) (((s0 & 0x3f3f) & ~hi) | ((((s4 >> 0) & 0x0f0f) | ((s0 & 0xc0c0) >> 2)) & hi));
    +    aux[1] = (uint16_t) (((s2 & 0x3f3f) & ~hi) | ((((s4 >> 4) & 0x0f0f) | ((s2 & 0xc0c0) >> 2)) & hi));
         const uint8_t * sc = (const uint8_t *)aux;
         const uint8_t * m  = sc + 2;
     
    @@ -981,16 +985,21 @@ static __device__ __forceinline__ float vec_dot_q5_K_q8_1(
         vh[0] = qh[0] >> bq8_offset;
         vh[1] = qh[4] >> bq8_offset;
     
    +    // same as q4_K
         const uint16_t * scales = (const uint16_t *)bq5_K->scales;
    +    const int j  = bq8_offset/2;
    +    const int jm = j & 1;
    +
    +    const uint32_t s0 = scales[jm + 0];
    +    const uint32_t s2 = scales[jm + 2];
    +    const uint32_t s4 = scales[jm + 4];
    +
    +    const uint32_t hi = (uint32_t) -(int32_t) (j >= 2);
    +
         uint16_t aux[2];
    -    const int j = bq8_offset/2;
    -    if (j < 2) {
    -        aux[0] = scales[j+0] & 0x3f3f;
    -        aux[1] = scales[j+2] & 0x3f3f;
    -    } else {
    -        aux[0] = ((scales[j+2] >> 0) & 0x0f0f) | ((scales[j-2] & 0xc0c0) >> 2);
    -        aux[1] = ((scales[j+2] >> 4) & 0x0f0f) | ((scales[j-0] & 0xc0c0) >> 2);
    -    }
    +    aux[0] = (uint16_t) (((s0 & 0x3f3f) & ~hi) | ((((s4 >> 0) & 0x0f0f) | ((s0 & 0xc0c0) >> 2)) & hi));
    +    aux[1] = (uint16_t) (((s2 & 0x3f3f) & ~hi) | ((((s4 >> 4) & 0x0f0f) | ((s2 & 0xc0c0) >> 2)) & hi));
    +
         const uint8_t * sc = (const uint8_t *)aux;
         const uint8_t * m  = sc + 2;
     
    
    From d23c47f2a9175c514556fc2fc69b2e670f37ae2c Mon Sep 17 00:00:00 2001
    From: fairydreaming <166155368+fairydreaming@users.noreply.github.com>
    Date: Mon, 7 Sep 2026 15:20:58 +0200
    Subject: [PATCH 23/31] convert : refactor Hy4-preview conversion - move HC
     tensor mapping to the global map (#28451)
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Co-authored-by: Stanisław Szymczyk 
    ---
     conversion/hy_v4.py            | 111 +++++++--------------------------
     gguf-py/gguf/tensor_mapping.py |  38 +++++++++++
     2 files changed, 60 insertions(+), 89 deletions(-)
    
    diff --git a/conversion/hy_v4.py b/conversion/hy_v4.py
    index f564b9ec2..358e21fe5 100644
    --- a/conversion/hy_v4.py
    +++ b/conversion/hy_v4.py
    @@ -9,20 +9,6 @@ from .base import ModelBase, gguf, logger
     from .deepseek import DeepseekV2Model
     
     
    -def split_kv_b_proj(weight: torch.Tensor, n_head: int, qk_nope: int, v_head_dim: int):
    -    """Split kv_b_proj into k_b (transposed) and v_b, matching DeepSeek MLA absorption.
    -
    -    weight: [n_head*(qk_nope+v_head_dim), kv_lora_rank].
    -    Returns (k_b, v_b): k_b [n_head, kv_lora_rank, qk_nope], v_b [n_head, v_head_dim, kv_lora_rank].
    -    """
    -    kv_lora = weight.shape[-1]
    -    assert weight.shape[0] == n_head * (qk_nope + v_head_dim)
    -    kv_b = weight.view(n_head, qk_nope + v_head_dim, kv_lora)
    -    k_b, v_b = torch.split(kv_b, [qk_nope, v_head_dim], dim=1)
    -    k_b = k_b.transpose(1, 2).contiguous()  # [n_head, kv_lora, qk_nope]
    -    return k_b, v_b.contiguous()
    -
    -
     def split_gate_up(weight: torch.Tensor, moe_intermediate_size: int):
         """Split a fused stacked gate_up expert tensor into (gate, up).
     
    @@ -36,6 +22,7 @@ def split_gate_up(weight: torch.Tensor, moe_intermediate_size: int):
     
     
     @ModelBase.register("HYV4ForCausalLM")
    +@ModelBase.example("tencent/Hy4-preview")
     class HYV4Model(DeepseekV2Model):
         """HY_V4: DeepSeek-V3 style MLA + MoE with iHC, a gated MLA output and a learnable sink.
     
    @@ -54,6 +41,8 @@ class HYV4Model(DeepseekV2Model):
     
         model_arch = gguf.MODEL_ARCH.HY_V4
     
    +    merge_expert = False
    +
         # tensors a "full" indexer layer must carry
         INDEXER_SUFFIXES = frozenset({
             "self_attn.indexer.wq_b.weight",
    @@ -186,6 +175,10 @@ class HYV4Model(DeepseekV2Model):
                 )
     
         def prepare_tensors(self):
    +        # Hy4-preview for some reason has num_key_value_heads equal to 8, so override it here
    +        # without this conversion/deepseek.py fails on assert
    +        self.hparams["num_key_value_heads"] = self.hparams["num_attention_heads"]
    +
             # validate before the base materializes tensors, so a mismatch fails early
             is_full = self.indexer_is_full()
             if is_full is not None:
    @@ -227,85 +220,25 @@ class HYV4Model(DeepseekV2Model):
     
         def modify_tensors(self, data_torch: torch.Tensor, name: str, bid: int | None) -> Iterable[tuple[str, torch.Tensor]]:
             hparams = self.hparams
    -        n_head = hparams["num_attention_heads"]
    -        qk_nope = hparams["qk_nope_head_dim"]
    -        v_head_dim = hparams["v_head_dim"]
             moe_inter = hparams["moe_intermediate_size"]
     
             tn = self.format_tensor_name
     
    -        # ---- global (non per-layer) ----
    -        if name == "model.embed_tokens.weight":
    -            return [(tn(gguf.MODEL_TENSOR.TOKEN_EMBD), data_torch)]
    -        if name == "model.norm.weight":
    -            return [(tn(gguf.MODEL_TENSOR.OUTPUT_NORM), data_torch)]
    -        if name == "lm_head.weight":
    -            return [(tn(gguf.MODEL_TENSOR.OUTPUT), data_torch)]
    -        if name == "model.hc_head.hc_head_fn":
    -            return [(tn(gguf.MODEL_TENSOR.HC_HEAD_FN), data_torch)]
    -        if name == "model.hc_head.hc_head_base":
    -            return [(tn(gguf.MODEL_TENSOR.HC_HEAD_BASE), data_torch)]
    -        if name == "model.hc_head.hc_head_scale":
    -            return [(tn(gguf.MODEL_TENSOR.HC_HEAD_SCALE), data_torch)]
    -
    -        assert bid is not None, f"expected a per-layer tensor, got {name!r}"
    -
    -        # ---- per-layer, keyed by suffix after 'model.layers.{bid}.' ----
    -        suffix = name.split(f"model.layers.{bid}.", 1)[-1]
    -
    -        # note: q_b_proj and kv_a_proj_with_mqa are mapped straight through (no RoPE permute),
    -        # the graph rotates consecutive pairs so the rows need no reordering
    -        simple = {
    -            "input_layernorm.weight":          (gguf.MODEL_TENSOR.ATTN_NORM, ".weight"),
    -            "post_attention_layernorm.weight": (gguf.MODEL_TENSOR.FFN_NORM,  ".weight"),
    -            "self_attn.q_a_proj.weight":       (gguf.MODEL_TENSOR.ATTN_Q_A, ".weight"),
    -            "self_attn.q_a_layernorm.weight":  (gguf.MODEL_TENSOR.ATTN_Q_A_NORM, ".weight"),
    -            "self_attn.q_b_proj.weight":       (gguf.MODEL_TENSOR.ATTN_Q_B, ".weight"),
    -            "self_attn.kv_a_proj_with_mqa.weight": (gguf.MODEL_TENSOR.ATTN_KV_A_MQA, ".weight"),
    -            "self_attn.kv_a_layernorm.weight": (gguf.MODEL_TENSOR.ATTN_KV_A_NORM, ".weight"),
    -            "self_attn.o_proj.weight":         (gguf.MODEL_TENSOR.ATTN_OUT, ".weight"),
    -            "self_attn.linear_gate.weight":    (gguf.MODEL_TENSOR.ATTN_GATE, ".weight"),
    -            "self_attn.learnable_sink_param":  (gguf.MODEL_TENSOR.ATTN_SINKS, ".weight"),
    -            "self_attn.indexer.wq_b.weight":   (gguf.MODEL_TENSOR.INDEXER_ATTN_Q_B, ".weight"),
    -            "self_attn.indexer.wk.weight":     (gguf.MODEL_TENSOR.INDEXER_ATTN_K, ".weight"),
    -            "self_attn.indexer.k_norm.weight": (gguf.MODEL_TENSOR.INDEXER_K_NORM, ".weight"),
    -            "self_attn.indexer.k_norm.bias":   (gguf.MODEL_TENSOR.INDEXER_K_NORM, ".bias"),
    -            "self_attn.indexer.weights_proj.weight": (gguf.MODEL_TENSOR.INDEXER_PROJ, ".weight"),
    -            "hc_attn_layer.hc_pre.hc_fn":      (gguf.MODEL_TENSOR.HC_ATTN_FN, ".weight"),
    -            "hc_attn_layer.hc_pre.hc_base":    (gguf.MODEL_TENSOR.HC_ATTN_BASE, ".weight"),
    -            "hc_attn_layer.hc_pre.hc_scale":   (gguf.MODEL_TENSOR.HC_ATTN_SCALE, ".weight"),
    -            "hc_mlp_layer.hc_pre.hc_fn":       (gguf.MODEL_TENSOR.HC_FFN_FN, ".weight"),
    -            "hc_mlp_layer.hc_pre.hc_base":     (gguf.MODEL_TENSOR.HC_FFN_BASE, ".weight"),
    -            "hc_mlp_layer.hc_pre.hc_scale":    (gguf.MODEL_TENSOR.HC_FFN_SCALE, ".weight"),
    -            "mlp.gate.weight":                 (gguf.MODEL_TENSOR.FFN_GATE_INP, ".weight"),
    -            "mlp.gate.e_score_correction.bias":(gguf.MODEL_TENSOR.FFN_EXP_PROBS_B, ".bias"),
    -            "mlp.gate_proj.weight":            (gguf.MODEL_TENSOR.FFN_GATE, ".weight"),
    -            "mlp.up_proj.weight":              (gguf.MODEL_TENSOR.FFN_UP, ".weight"),
    -            "mlp.down_proj.weight":            (gguf.MODEL_TENSOR.FFN_DOWN, ".weight"),
    -            "mlp.shared_experts.gate_proj.weight": (gguf.MODEL_TENSOR.FFN_GATE_SHEXP, ".weight"),
    -            "mlp.shared_experts.up_proj.weight":   (gguf.MODEL_TENSOR.FFN_UP_SHEXP, ".weight"),
    -            "mlp.shared_experts.down_proj.weight": (gguf.MODEL_TENSOR.FFN_DOWN_SHEXP, ".weight"),
    -        }
    -        if suffix in simple:
    -            key, sfx = simple[suffix]
    -            return [(tn(key, bid, sfx), data_torch)]
    -
    -        # kv_b_proj: split into k_b (transposed) and v_b
    -        if suffix == "self_attn.kv_b_proj.weight":
    -            k_b, v_b = split_kv_b_proj(data_torch, n_head, qk_nope, v_head_dim)
    -            return [
    -                (tn(gguf.MODEL_TENSOR.ATTN_K_B, bid), k_b),
    -                (tn(gguf.MODEL_TENSOR.ATTN_V_B, bid), v_b),
    -            ]
    -
             # fused stacked experts: split gate_up into gate/up
    -        if suffix == "mlp.experts.gate_up_proj":
    +        if name.endswith("mlp.experts.gate_up_proj"):
                 gate, up = split_gate_up(data_torch, moe_inter)
    -            return [
    -                (tn(gguf.MODEL_TENSOR.FFN_GATE_EXP, bid), gate),
    -                (tn(gguf.MODEL_TENSOR.FFN_UP_EXP, bid), up),
    -            ]
    -        if suffix == "mlp.experts.down_proj":
    -            return [(tn(gguf.MODEL_TENSOR.FFN_DOWN_EXP, bid), data_torch)]
    +            yield from super().modify_tensors(gate, tn(gguf.MODEL_TENSOR.FFN_GATE_EXP, bid), bid)
    +            yield from super().modify_tensors(up,   tn(gguf.MODEL_TENSOR.FFN_UP_EXP,   bid), bid)
    +            return
     
    -        raise ValueError(f"Unsupported HY_V4 tensor {name!r} (suffix {suffix!r})")
    +        # add .weight suffixes
    +        if name.endswith("mlp.experts.down_proj") or name.endswith(".self_attn.learnable_sink_param"):
    +            name += ".weight"
    +
    +        if re.search(r"\.hc_head\.hc_head_(?:fn|base|scale)$", name):
    +            name += ".weight"
    +
    +        if re.search(r"\.hc_(?:attn|mlp)_layer\.hc_pre\.hc_(?:fn|base|scale)$", name):
    +            name += ".weight"
    +
    +        yield from super().modify_tensors(data_torch, name, bid)
    diff --git a/gguf-py/gguf/tensor_mapping.py b/gguf-py/gguf/tensor_mapping.py
    index d644d502e..d2dfeece5 100644
    --- a/gguf-py/gguf/tensor_mapping.py
    +++ b/gguf-py/gguf/tensor_mapping.py
    @@ -385,6 +385,7 @@ class TensorNameMap:
             MODEL_TENSOR.ATTN_SINKS: (
                 "model.layers.{bid}.self_attn.sinks", # openai-moe
                 "model.layers.{bid}.self_attn.attention_sink_bias", # mimov2
    +            "model.layers.{bid}.self_attn.learnable_sink_param", # hy-v4
             ),
     
             MODEL_TENSOR.ATTN_GATE: (
    @@ -392,6 +393,7 @@ class TensorNameMap:
                 "model.layers.{bid}.linear_attn.in_proj_z",  # qwen3.5
                 "model.layers.{bid}.self_attn.g_proj",    # step3.5 head-wise attention gate
                 "model.layers.{bid}.self_attn.output_gate",  # minimax-01
    +            "model.layers.{bid}.self_attn.linear_gate",  # hy-v4
             ),
     
             # Feed-forward norm
    @@ -1329,6 +1331,42 @@ class TensorNameMap:
                 "model.layers.{bid}.self_attn.index_q_norm", # MSA
             ),
     
    +        MODEL_TENSOR.HC_ATTN_FN: (
    +            "model.layers.{bid}.hc_attn_layer.hc_pre.hc_fn", # hy-v4
    +        ),
    +
    +        MODEL_TENSOR.HC_ATTN_BASE: (
    +            "model.layers.{bid}.hc_attn_layer.hc_pre.hc_base", # hy-v4
    +        ),
    +
    +        MODEL_TENSOR.HC_ATTN_SCALE: (
    +            "model.layers.{bid}.hc_attn_layer.hc_pre.hc_scale", # hy-v4
    +        ),
    +
    +        MODEL_TENSOR.HC_FFN_FN: (
    +            "model.layers.{bid}.hc_mlp_layer.hc_pre.hc_fn", # hy-v4
    +        ),
    +
    +        MODEL_TENSOR.HC_FFN_BASE: (
    +            "model.layers.{bid}.hc_mlp_layer.hc_pre.hc_base", # hy-v4
    +        ),
    +
    +        MODEL_TENSOR.HC_FFN_SCALE: (
    +            "model.layers.{bid}.hc_mlp_layer.hc_pre.hc_scale", # hy-v4
    +        ),
    +
    +        MODEL_TENSOR.HC_HEAD_FN: (
    +            "model.hc_head.hc_head_fn",  # hy-v4
    +        ),
    +
    +        MODEL_TENSOR.HC_HEAD_BASE: (
    +            "model.hc_head.hc_head_base", # hy-v4
    +        ),
    +
    +        MODEL_TENSOR.HC_HEAD_SCALE: (
    +            "model.hc_head.hc_head_scale", # hy-v4
    +        ),
    +
             ############################################################################
             # TODO: these do not belong to block_mappings_cfg - move them to mappings_cfg
             MODEL_TENSOR.ENC_OUTPUT_NORM: (
    
    From 4735997382b5bcf8d4c197b0fef16256ce2992f7 Mon Sep 17 00:00:00 2001
    From: AuroraRAS 
    Date: Mon, 7 Sep 2026 22:21:42 +0900
    Subject: [PATCH 24/31] ggml: add gfx90c HIP support (#26454)
    
    * ggml: add gfx90c HIP support
    
    * ggml: make gfx90c HIP support compliant with specifications
    ---
     ggml/src/ggml-cuda/common.cuh    | 15 +++++++++------
     ggml/src/ggml-cuda/ggml-cuda.cu  |  1 +
     ggml/src/ggml-cuda/mmq.cu        |  4 ++--
     ggml/src/ggml-cuda/vendors/hip.h |  4 ++--
     4 files changed, 14 insertions(+), 10 deletions(-)
    
    diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh
    index 7fb04c95f..7d14ce906 100644
    --- a/ggml/src/ggml-cuda/common.cuh
    +++ b/ggml/src/ggml-cuda/common.cuh
    @@ -69,6 +69,8 @@
     #define GGML_CUDA_CC_GCN4       (GGML_CUDA_CC_OFFSET_AMD + 0x803)  // Tonga, Fiji, Polaris, minimum for fast fp16
     #define GGML_CUDA_CC_VEGA       (GGML_CUDA_CC_OFFSET_AMD + 0x900)  // Vega56/64, minimum for fp16 dual issue
     #define GGML_CUDA_CC_VEGA20     (GGML_CUDA_CC_OFFSET_AMD + 0x906)  // MI50/Radeon VII, minimum for dp4a
    +#define GGML_CUDA_CC_GFX909     (GGML_CUDA_CC_OFFSET_AMD + 0x909)  // GCN APU
    +#define GGML_CUDA_CC_GFX90C     (GGML_CUDA_CC_OFFSET_AMD + 0x90c)  // GCN APU
     #define GGML_CUDA_CC_CDNA1      (GGML_CUDA_CC_OFFSET_AMD + 0x908)  // MI100, minimum for MFMA, acc registers
     #define GGML_CUDA_CC_CDNA2      (GGML_CUDA_CC_OFFSET_AMD + 0x90a)  // MI210 (gfx90a), minimum acc register renaming
     #define GGML_CUDA_CC_CDNA3      (GGML_CUDA_CC_OFFSET_AMD + 0x942)  // MI300
    @@ -89,12 +91,13 @@
     #define GGML_CUDA_CC_IS_RDNA3_5(cc) (cc >= GGML_CUDA_CC_RDNA3_5 && cc < GGML_CUDA_CC_RDNA4)
     #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(cc)     (cc > GGML_CUDA_CC_OFFSET_AMD && cc < GGML_CUDA_CC_CDNA1)
    -#define GGML_CUDA_CC_IS_CDNA(cc)    (cc >= GGML_CUDA_CC_CDNA1 && cc < GGML_CUDA_CC_RDNA1)
    -#define GGML_CUDA_CC_IS_CDNA1(cc)   (cc >= GGML_CUDA_CC_CDNA1 && cc < GGML_CUDA_CC_CDNA2)
    -#define GGML_CUDA_CC_IS_CDNA2(cc)   (cc >= GGML_CUDA_CC_CDNA2 && cc < GGML_CUDA_CC_CDNA3)
    -#define GGML_CUDA_CC_IS_CDNA3(cc)   (cc >= GGML_CUDA_CC_CDNA3 && cc < GGML_CUDA_CC_CDNA4)
    -#define GGML_CUDA_CC_IS_CDNA4(cc)   (cc >= GGML_CUDA_CC_CDNA4 && cc < GGML_CUDA_CC_RDNA1)
    +#define GGML_CUDA_CC_IS_GCN_APU(cc) ((cc) == GGML_CUDA_CC_GFX909 || (cc) == GGML_CUDA_CC_GFX90C)
    +#define GGML_CUDA_CC_IS_GCN(cc)     ((cc > GGML_CUDA_CC_OFFSET_AMD && cc < GGML_CUDA_CC_CDNA1) || GGML_CUDA_CC_IS_GCN_APU(cc))
    +#define GGML_CUDA_CC_IS_CDNA(cc)    (!GGML_CUDA_CC_IS_GCN_APU(cc) && cc >= GGML_CUDA_CC_CDNA1 && cc < GGML_CUDA_CC_RDNA1)
    +#define GGML_CUDA_CC_IS_CDNA1(cc)   (GGML_CUDA_CC_IS_CDNA(cc) && cc >= GGML_CUDA_CC_CDNA1 && cc < GGML_CUDA_CC_CDNA2)
    +#define GGML_CUDA_CC_IS_CDNA2(cc)   (GGML_CUDA_CC_IS_CDNA(cc) && cc >= GGML_CUDA_CC_CDNA2 && cc < GGML_CUDA_CC_CDNA3)
    +#define GGML_CUDA_CC_IS_CDNA3(cc)   (GGML_CUDA_CC_IS_CDNA(cc) && cc >= GGML_CUDA_CC_CDNA3 && cc < GGML_CUDA_CC_CDNA4)
    +#define GGML_CUDA_CC_IS_CDNA4(cc)   (GGML_CUDA_CC_IS_CDNA(cc) && cc >= GGML_CUDA_CC_CDNA4 && cc < GGML_CUDA_CC_RDNA1)
     
     // Moore Threads
     #define MUSART_HMASK 40300 // MUSA rc4.3, min. ver. for half2 -> uint mask comparisons
    diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu
    index 45e9537f0..0e6601f03 100644
    --- a/ggml/src/ggml-cuda/ggml-cuda.cu
    +++ b/ggml/src/ggml-cuda/ggml-cuda.cu
    @@ -212,6 +212,7 @@ static int ggml_cuda_parse_id(char devName[]) {
         }
         archNum += archMajor * 0x100;
         archNum += archMinor;
    +
         return archNum;
     }
     #endif // defined(GGML_USE_HIP)
    diff --git a/ggml/src/ggml-cuda/mmq.cu b/ggml/src/ggml-cuda/mmq.cu
    index 7fb440148..9beff0d9b 100644
    --- a/ggml/src/ggml-cuda/mmq.cu
    +++ b/ggml/src/ggml-cuda/mmq.cu
    @@ -375,10 +375,10 @@ bool ggml_cuda_should_use_mmq(enum ggml_type type, int cc, int64_t ne11, int64_t
             return true;
         }
     
    -    // gfx900 (Vega 10) lacks native dp4a, loses to dequant + hipBLAS
    +    // gfx900 (Vega 10), gfx909, and gfx90c lack native dp4a, losing to dequant + hipBLAS
         // for dense matrices; keep MMQ only for MoE, where the
         // hipBLAS path is much slower.
    -    if (cc == GGML_CUDA_CC_VEGA) {
    +    if (cc == GGML_CUDA_CC_VEGA || GGML_CUDA_CC_IS_GCN_APU(cc)) {
             return n_experts > 0;
         }
     
    diff --git a/ggml/src/ggml-cuda/vendors/hip.h b/ggml/src/ggml-cuda/vendors/hip.h
    index 9aa558f3f..2fc0fe9fd 100644
    --- a/ggml/src/ggml-cuda/vendors/hip.h
    +++ b/ggml/src/ggml-cuda/vendors/hip.h
    @@ -176,9 +176,9 @@
     
     #define __CUDA_ARCH__ 1300
     
    -#if defined(__gfx900__) || defined(__gfx906__)
    +#if defined(__gfx900__) || defined(__gfx906__) || defined(__gfx909__) || defined(__gfx90c__)
     #define GCN5
    -#endif // defined(__gfx900__) || defined(__gfx906__)
    +#endif // defined(__gfx900__) || defined(__gfx906__) || defined(__gfx909__) || defined(__gfx90c__)
     
     #if defined(__gfx803__)
     #define GCN4
    
    From 0c963452ea7d19f872e455257509a4ff00e7dfc7 Mon Sep 17 00:00:00 2001
    From: ravel7524 <58877666+ravel7524@users.noreply.github.com>
    Date: Mon, 7 Sep 2026 09:22:42 -0400
    Subject: [PATCH 25/31] CUDA: size routed MoE MMQ N-tiles from typical expert
     width on RDNA3 (#24546)
    
    * adjust ncols_picker for routed MoE in mul_mat_q_case function
    
    * Adding CDNA, RDNA2 and RDNA4
    
    * fix: update mmq_use_routed_moe_ncols_picker to include NVIDIA + Volta support
    
    * feat: enhance mmq configuration for various architectures with moe_ncols_min_cc support
    
    * refactor: replace moe_ncols_min_cc with use_typical_moe_ncols in mmq configuration files
    
    * HIP: mmq: enable typical moe ncols on RDNA4
    
    ---------
    
    Co-authored-by: Carl Philipp Klemm 
    ---
     ggml/src/ggml-cuda/mmq-config-ampere.cuh      |  3 ++-
     ggml/src/ggml-cuda/mmq-config-blackwell.cuh   |  1 +
     ggml/src/ggml-cuda/mmq-config-cdna.cuh        |  3 ++-
     ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh |  3 ++-
     .../src/ggml-cuda/mmq-config-pascal-older.cuh |  3 ++-
     ggml/src/ggml-cuda/mmq-config-rdna2.cuh       |  3 ++-
     ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh     |  3 ++-
     ggml/src/ggml-cuda/mmq-config-rdna3.cuh       |  3 ++-
     ggml/src/ggml-cuda/mmq-config-rdna4.cuh       |  3 ++-
     ggml/src/ggml-cuda/mmq.cuh                    | 23 +++++++++++++++----
     10 files changed, 36 insertions(+), 12 deletions(-)
    
    diff --git a/ggml/src/ggml-cuda/mmq-config-ampere.cuh b/ggml/src/ggml-cuda/mmq-config-ampere.cuh
    index 9f9fd1973..2c00aef2c 100644
    --- a/ggml/src/ggml-cuda/mmq-config-ampere.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-ampere.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_ampere(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = false;
         CASE(GGML_TYPE_Q1_0, 256, 1, 128,   8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
         CASE(GGML_TYPE_Q1_0, 256, 1, 128,  16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
         CASE(GGML_TYPE_Q1_0, 256, 1, 128,  32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
    @@ -379,5 +380,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         CASE(GGML_TYPE_NVFP4, 256, 1, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
         CASE(GGML_TYPE_NVFP4, 256, 1, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
     
    -    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
     }
    diff --git a/ggml/src/ggml-cuda/mmq-config-blackwell.cuh b/ggml/src/ggml-cuda/mmq-config-blackwell.cuh
    index 9fbe32b69..8f928e217 100644
    --- a/ggml/src/ggml-cuda/mmq-config-blackwell.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-blackwell.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_blackwell(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = false;
         CASE(GGML_TYPE_MXFP4, 256, 1, 128,   8, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
         CASE(GGML_TYPE_MXFP4, 256, 1, 128,  16, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
         CASE(GGML_TYPE_MXFP4, 256, 1, 128,  32, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
    diff --git a/ggml/src/ggml-cuda/mmq-config-cdna.cuh b/ggml/src/ggml-cuda/mmq-config-cdna.cuh
    index 4a8d89f72..1d51a773b 100644
    --- a/ggml/src/ggml-cuda/mmq-config-cdna.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-cdna.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_cdna(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = false;
         CASE(GGML_TYPE_Q1_0, 512, 1, 128,  16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
         CASE(GGML_TYPE_Q1_0, 512, 1, 128,  32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
         CASE(GGML_TYPE_Q1_0, 512, 1, 128,  64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
    @@ -181,5 +182,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         CASE(GGML_TYPE_NVFP4, 512, 1, 128,  48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
         CASE(GGML_TYPE_NVFP4, 512, 1, 128,  64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
     
    -    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
     }
    diff --git a/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh b/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh
    index 83eb7c146..557a04e18 100644
    --- a/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_pascal_dp4a(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = false;
         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, 64,  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);
    @@ -269,5 +270,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, 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 2a8dc9e1a..751ce026d 100644
    --- a/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_pascal_older(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = false;
         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, 64,  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);
    @@ -269,5 +270,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
     }
    diff --git a/ggml/src/ggml-cuda/mmq-config-rdna2.cuh b/ggml/src/ggml-cuda/mmq-config-rdna2.cuh
    index 8324d9e1a..c1efef56f 100644
    --- a/ggml/src/ggml-cuda/mmq-config-rdna2.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-rdna2.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna2(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = false;
         CASE(GGML_TYPE_Q1_0, 256, 2, 128,   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, 128,  32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
    @@ -269,5 +270,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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);
     
    -    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, 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 180b2d937..10fdad663 100644
    --- a/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna3_5(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = false;
         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);
    @@ -286,5 +287,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
     }
    diff --git a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh
    index 3a3ef7bd9..ba569337b 100644
    --- a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna3(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = true;
         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, 128, 2,  64,  64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
    @@ -270,5 +271,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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, 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);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
     }
    diff --git a/ggml/src/ggml-cuda/mmq-config-rdna4.cuh b/ggml/src/ggml-cuda/mmq-config-rdna4.cuh
    index 9293d9d55..6cce1d7e8 100644
    --- a/ggml/src/ggml-cuda/mmq-config-rdna4.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-rdna4.cuh
    @@ -1,4 +1,5 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna4(ggml_type type, int J, bool fallback) {
    +    constexpr bool use_typical_moe_ncols = true;
         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, 128, 2,  64,  64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
    @@ -286,5 +287,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
     }
    diff --git a/ggml/src/ggml-cuda/mmq.cuh b/ggml/src/ggml-cuda/mmq.cuh
    index b4a747720..b28b960cd 100644
    --- a/ggml/src/ggml-cuda/mmq.cuh
    +++ b/ggml/src/ggml-cuda/mmq.cuh
    @@ -170,12 +170,13 @@ struct ggml_cuda_mmq_config {
         int                       J;           // SRAM tile width in src1->ne[1]/dst->ne[1] direction.
         ggml_cuda_mmq_sram_layout sram_layout; // SRAM tile length in src0->ne[0]/src1->ne[0] direction (physical 32 bit elements).
         int                       K_vram;      // VRAM tile length in src0->ne[0]/src1->ne[0] direction (logical elements).
    +    bool                      use_typical_moe_ncols;
         bool                      stream_k;    // Whether or not to use stream-k decomposition.
         bool                      fallback;    // Whether a fallback for out-of-bounds check in src0->ne[1] direction is needed.
     
         constexpr __host__ __device__ ggml_cuda_mmq_config(
    -            ggml_type type, int nthreads, int occupancy, int I, int J, ggml_cuda_mmq_sram_layout sram_layout, int K_vram, bool stream_k, bool fallback) :
    -        type(type), nthreads(nthreads), occupancy(occupancy), I(I), J(J), sram_layout(sram_layout), K_vram(K_vram), stream_k(stream_k), fallback(fallback) {}
    +            ggml_type type, int nthreads, int occupancy, int I, int J, ggml_cuda_mmq_sram_layout sram_layout, int K_vram, bool use_typical_moe_ncols, bool stream_k, bool fallback) :
    +        type(type), nthreads(nthreads), occupancy(occupancy), I(I), J(J), sram_layout(sram_layout), K_vram(K_vram), use_typical_moe_ncols(use_typical_moe_ncols), stream_k(stream_k), fallback(fallback) {}
     
         constexpr __device__ int rows_per_warp() const {
     #if defined(AMD_MFMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE)
    @@ -210,7 +211,7 @@ struct ggml_cuda_mmq_config {
             static_assert((I_)        %  32 == 0,                             "bad I");                                                       \
             static_assert((J_)        %   8 == 0,                             "bad J");                                                       \
             static_assert((K_vram_)   % 256 == 0,                             "bad K_vram");                                                  \
    -        return ggml_cuda_mmq_config((type_), (nthreads_), (occupancy_), (I_), (J_), (sram_layout_), (K_vram_), (stream_k_), (fallback_)); \
    +        return ggml_cuda_mmq_config((type_), (nthreads_), (occupancy_), (I_), (J_), (sram_layout_), (K_vram_), use_typical_moe_ncols, (stream_k_), (fallback_)); \
         }                                                                                                                                     \
     
     #include "mmq-config-pascal-older.cuh"
    @@ -1473,6 +1474,20 @@ void mul_mat_q_switch_J(ggml_backend_cuda_context & ctx, const mmq_args & args,
         const int    cc    = ggml_cuda_info().devices[id].cc;
         const size_t smpbo = ggml_cuda_info().devices[id].smpbo;
     
    +    int64_t ncols_picker = args.ncols_max;
    +    if (args.expert_bounds != nullptr && args.nchannels_x > 0) {
    +        const int J_max = ggml_cuda_mmq_get_J_max(type, fallback, cc, 128);
    +        const ggml_cuda_mmq_config config_max = ggml_cuda_mmq_get_config(type, J_max, fallback, cc);
    +        if (config_max.use_typical_moe_ncols) {
    +            // Use the typical expert width only for tile selection.
    +            // The launch grid still uses args.ncols_max.
    +            const int64_t ncols_typical = (args.ncols_dst + args.nchannels_x - 1) / args.nchannels_x;
    +            if (ncols_typical >= 1 && ncols_typical < J_max && ncols_typical < ncols_picker) {
    +                ncols_picker = ncols_typical;
    +            }
    +        }
    +    }
    +
         int J_best        = 0;
         int ntiles_J_best = INT_MAX;
     
    @@ -1486,7 +1501,7 @@ void mul_mat_q_switch_J(ggml_backend_cuda_context & ctx, const mmq_args & args,
                 continue;
             }
     
    -        const int ntiles_x = (args.ncols_max + config.J - 1) / config.J;
    +        const int ntiles_x = (ncols_picker + config.J - 1) / config.J;
     
             if (ntiles_x < ntiles_J_best) {
                 J_best = J;
    
    From 7a333e724089d026181f51af57d504980e5761e4 Mon Sep 17 00:00:00 2001
    From: Kevin Hopper <93635715+kh0pper@users.noreply.github.com>
    Date: Mon, 7 Sep 2026 08:24:03 -0500
    Subject: [PATCH 26/31] vulkan: add DeepSeek-V4 hyper-connection fused ops
     (DSV4_HC_COMB/PRE/POST) (#26578)
    
    * vulkan: add DeepSeek-V4 hyper-connection fused ops (DSV4_HC_COMB/PRE/POST)
    
    CUDA has these ops from the DeepSeek-V4 merge and Metal gained them in
    PR 26459. Vulkan was the last major backend running the unfused primitive
    chain. On DeepSeek-V4-Flash the unfused Sinkhorn comb chain alone takes
    about 32% of decode op time on gfx1151 (Strix Halo), spread over roughly
    16k dispatches per token.
    
    dsv4_hc_comb runs the full 20-iteration Sinkhorn in registers. A token's
    4x4 comb matrix lives in 16 consecutive subgroup lanes, with idst in bits
    0-1 and isrc in bits 2-3 to match the CPU reference layout, so
    subgroupShuffleXor by 1|2 reduces rows and by 4|8 reduces columns. One
    dispatch replaces about 137 strictly ordered node executions per site.
    The shuffle masks never cross a 16-lane boundary, so a subgroup of size
    64 packs 4 independent tokens.
    
    dsv4_hc_pre and dsv4_hc_post handle the elementwise stream collapse and
    fan-out, with per-token coefficients staged in shared memory.
    
    GGML_VK_DISABLE_DSV4_HC disables all three ops. The _COMB, _PRE and
    _POST variants gate each op independently so a single kernel can be
    bisected against the unfused graph.
    
    Adds eval cases at the production n_iter=20 across batch sizes that
    cross subgroup and workgroup boundaries.
    
    * vulkan: dsv4 hc review fixes
    
    Drop the per-op env-var disables and device flags, the stride divisibility
    check (ggml guarantees it) and the workgroup-count fallback in supports_op.
    Trim the comb shader comments to the lane layout.
    
    ---------
    
    Co-authored-by: Kevin Hopper 
    ---
     ggml/src/ggml-vulkan/ggml-vulkan.cpp          | 222 ++++++++++++++++++
     .../vulkan-shaders/dsv4_hc_comb.comp          |  90 +++++++
     .../vulkan-shaders/dsv4_hc_post.comp          |  83 +++++++
     .../vulkan-shaders/dsv4_hc_pre.comp           |  59 +++++
     .../vulkan-shaders/vulkan-shaders-gen.cpp     |   3 +
     tests/test-backend-ops.cpp                    |   5 +
     6 files changed, 462 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 62f90847b..75132c0b5 100644
    --- a/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    +++ b/ggml/src/ggml-vulkan/ggml-vulkan.cpp
    @@ -1110,6 +1110,9 @@ struct vk_device_struct {
         vk_pipeline pipeline_cumsum_multipass2_f32;
         vk_pipeline pipeline_argmax_f32;
         vk_pipeline pipeline_count_equal_i32;
    +    vk_pipeline pipeline_dsv4_hc_comb_f32;
    +    vk_pipeline pipeline_dsv4_hc_pre_f32;
    +    vk_pipeline pipeline_dsv4_hc_post_f32;
         std::map pipeline_solve_tri_f32;
         vk_pipeline pipeline_im2col_f32, pipeline_im2col_f32_f16;
         vk_pipeline pipeline_im2col_3d_f32, pipeline_im2col_3d_f32_f16;
    @@ -1467,6 +1470,53 @@ struct vk_op_fwht_push_constants {
         float scale;
     };
     
    +struct vk_op_dsv4_hc_comb_push_constants {
    +    uint32_t n_tokens;
    +
    +    uint32_t nbm0; uint32_t nbm1;
    +    uint32_t nbs0;
    +    uint32_t nbb0;
    +    uint32_t nbd0; uint32_t nbd1; uint32_t nbd2;
    +
    +    uint32_t m_offset;
    +    uint32_t s_offset;
    +    uint32_t b_offset;
    +    uint32_t d_offset;
    +
    +    float eps;
    +    uint32_t n_iter;
    +};
    +
    +struct vk_op_dsv4_hc_pre_push_constants {
    +    uint32_t n_embd;
    +    uint32_t n_tokens;
    +
    +    uint32_t nbx0; uint32_t nbx1; uint32_t nbx2;
    +    uint32_t nbw0; uint32_t nbw1;
    +    uint32_t nbd0; uint32_t nbd1;
    +
    +    uint32_t x_offset;
    +    uint32_t w_offset;
    +    uint32_t d_offset;
    +};
    +
    +struct vk_op_dsv4_hc_post_push_constants {
    +    uint32_t n_embd;
    +    uint32_t n_tokens;
    +
    +    uint32_t nbx0; uint32_t nbx1;
    +    uint32_t nbr0; uint32_t nbr1; uint32_t nbr2;
    +    uint32_t nbp0; uint32_t nbp1;
    +    uint32_t nbc0; uint32_t nbc1; uint32_t nbc2;
    +    uint32_t nbd0; uint32_t nbd1; uint32_t nbd2;
    +
    +    uint32_t x_offset;
    +    uint32_t r_offset;
    +    uint32_t p_offset;
    +    uint32_t c_offset;
    +    uint32_t d_offset;
    +};
    +
     struct vk_op_count_experts_push_constants {
         uint32_t ne00;
         uint32_t ne01;
    @@ -2631,6 +2681,32 @@ template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk
         GGML_UNUSED(src3);
     }
     
    +template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk_op_dsv4_hc_comb_push_constants &p, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * src3, ggml_tensor * dst) {
    +    p.m_offset = get_misalign_bytes(ctx, src0) / ggml_type_size(src0->type);
    +    p.s_offset = get_misalign_bytes(ctx, src1) / ggml_type_size(src1->type);
    +    p.b_offset = get_misalign_bytes(ctx, src2) / ggml_type_size(src2->type);
    +    p.d_offset = get_misalign_bytes(ctx, dst)  / ggml_type_size(dst->type);
    +
    +    GGML_UNUSED(src3);
    +}
    +
    +template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk_op_dsv4_hc_pre_push_constants &p, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * src3, ggml_tensor * dst) {
    +    p.x_offset = get_misalign_bytes(ctx, src0) / ggml_type_size(src0->type);
    +    p.w_offset = get_misalign_bytes(ctx, src1) / ggml_type_size(src1->type);
    +    p.d_offset = get_misalign_bytes(ctx, dst)  / ggml_type_size(dst->type);
    +
    +    GGML_UNUSED(src2);
    +    GGML_UNUSED(src3);
    +}
    +
    +template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk_op_dsv4_hc_post_push_constants &p, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * src3, ggml_tensor * dst) {
    +    p.x_offset = get_misalign_bytes(ctx, src0) / ggml_type_size(src0->type);
    +    p.r_offset = get_misalign_bytes(ctx, src1) / ggml_type_size(src1->type);
    +    p.p_offset = get_misalign_bytes(ctx, src2) / ggml_type_size(src2->type);
    +    p.c_offset = get_misalign_bytes(ctx, src3) / ggml_type_size(src3->type);
    +    p.d_offset = get_misalign_bytes(ctx, dst)  / ggml_type_size(dst->type);
    +}
    +
     struct ggml_backend_vk_buffer_context {
         vk_device_ref device;
         vk_buffer dev_buffer;
    @@ -5977,6 +6053,16 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
             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);
         }
     
    +    // comb holds a token's 4x4 matrix in one 16-lane slice of a subgroup, so it
    +    // needs at least 16 lanes, pinned to a known size.
    +    if (device->subgroup_basic && device->subgroup_shuffle && device->subgroup_require_full_support && device->subgroup_size >= 16) {
    +        const uint32_t tokens_per_workgroup = 4 * (device->subgroup_size / 16);
    +        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), {tokens_per_workgroup, 1, 1}, { device->subgroup_size }, 1, true, true, device->subgroup_size);
    +    }
    +
    +    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}, { 256 }, 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}, { 256 }, 1);
    +
         for (auto &s : device->pipeline_solve_tri_f32) {
             const vk_solve_tri_pipeline_state &state = s.first;
     
    @@ -10204,6 +10290,98 @@ static void ggml_vk_fwht(ggml_backend_vk_context * ctx, vk_context& subctx, cons
         ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { src_buf, dst_buf }, pc, { workgroups_x, 1, 1 });
     }
     
    +static uint32_t ggml_vk_nb_elem(const ggml_tensor * t, int i) {
    +    return (uint32_t)(t->nb[i] / ggml_type_size(t->type));
    +}
    +
    +static void ggml_vk_dsv4_hc_comb(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * mixes, const ggml_tensor * scale, const ggml_tensor * base, ggml_tensor * dst) {
    +    VK_LOG_DEBUG("ggml_vk_dsv4_hc_comb(" << mixes << ", " << scale << ", " << base << ", " << dst << ")");
    +
    +    vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_comb_f32;
    +    GGML_ASSERT(pipeline != nullptr);
    +
    +    const uint32_t n_tokens = (uint32_t)mixes->ne[1];
    +
    +    ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
    +
    +    const vk_subbuffer mixes_buf = ggml_vk_tensor_subbuffer(ctx, mixes, true);
    +    const vk_subbuffer scale_buf = ggml_vk_tensor_subbuffer(ctx, scale, true);
    +    const vk_subbuffer base_buf  = ggml_vk_tensor_subbuffer(ctx, base,  true);
    +    const vk_subbuffer dst_buf   = ggml_vk_tensor_subbuffer(ctx, dst,   true);
    +
    +    vk_op_dsv4_hc_comb_push_constants pc = {
    +        n_tokens,
    +        ggml_vk_nb_elem(mixes, 0), ggml_vk_nb_elem(mixes, 1),
    +        ggml_vk_nb_elem(scale, 0),
    +        ggml_vk_nb_elem(base,  0),
    +        ggml_vk_nb_elem(dst,   0), ggml_vk_nb_elem(dst, 1), ggml_vk_nb_elem(dst, 2),
    +        0, 0, 0, 0,
    +        ggml_get_op_params_f32(dst, 0),
    +        (uint32_t)ggml_get_op_params_i32(dst, 1),
    +    };
    +    init_pushconst_tensor_offsets(ctx, pc, mixes, scale, base, nullptr, dst);
    +
    +    ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { mixes_buf, scale_buf, base_buf, dst_buf }, pc, { n_tokens, 1, 1 });
    +}
    +
    +static void ggml_vk_dsv4_hc_pre(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * x, const ggml_tensor * weights, ggml_tensor * dst) {
    +    VK_LOG_DEBUG("ggml_vk_dsv4_hc_pre(" << x << ", " << weights << ", " << dst << ")");
    +
    +    vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_pre_f32;
    +    GGML_ASSERT(pipeline != nullptr);
    +
    +    const uint32_t n_embd   = (uint32_t)x->ne[0];
    +    const uint32_t n_tokens = (uint32_t)x->ne[2];
    +
    +    ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
    +
    +    const vk_subbuffer x_buf = ggml_vk_tensor_subbuffer(ctx, x,       true);
    +    const vk_subbuffer w_buf = ggml_vk_tensor_subbuffer(ctx, weights, true);
    +    const vk_subbuffer d_buf = ggml_vk_tensor_subbuffer(ctx, dst,     true);
    +
    +    vk_op_dsv4_hc_pre_push_constants pc = {
    +        n_embd, n_tokens,
    +        ggml_vk_nb_elem(x, 0), ggml_vk_nb_elem(x, 1), ggml_vk_nb_elem(x, 2),
    +        ggml_vk_nb_elem(weights, 0), ggml_vk_nb_elem(weights, 1),
    +        ggml_vk_nb_elem(dst, 0), ggml_vk_nb_elem(dst, 1),
    +        0, 0, 0,
    +    };
    +    init_pushconst_tensor_offsets(ctx, pc, x, weights, nullptr, nullptr, dst);
    +
    +    ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { x_buf, w_buf, d_buf }, pc, { n_embd, n_tokens, 1 });
    +}
    +
    +static void ggml_vk_dsv4_hc_post(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * x, const ggml_tensor * residual, const ggml_tensor * post, const ggml_tensor * comb, ggml_tensor * dst) {
    +    VK_LOG_DEBUG("ggml_vk_dsv4_hc_post(" << x << ", " << residual << ", " << post << ", " << comb << ", " << dst << ")");
    +
    +    vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_post_f32;
    +    GGML_ASSERT(pipeline != nullptr);
    +
    +    const uint32_t n_embd   = (uint32_t)x->ne[0];
    +    const uint32_t n_tokens = (uint32_t)x->ne[1];
    +
    +    ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
    +
    +    const vk_subbuffer x_buf = ggml_vk_tensor_subbuffer(ctx, x,        true);
    +    const vk_subbuffer r_buf = ggml_vk_tensor_subbuffer(ctx, residual, true);
    +    const vk_subbuffer p_buf = ggml_vk_tensor_subbuffer(ctx, post,     true);
    +    const vk_subbuffer c_buf = ggml_vk_tensor_subbuffer(ctx, comb,     true);
    +    const vk_subbuffer d_buf = ggml_vk_tensor_subbuffer(ctx, dst,      true);
    +
    +    vk_op_dsv4_hc_post_push_constants pc = {
    +        n_embd, n_tokens,
    +        ggml_vk_nb_elem(x, 0), ggml_vk_nb_elem(x, 1),
    +        ggml_vk_nb_elem(residual, 0), ggml_vk_nb_elem(residual, 1), ggml_vk_nb_elem(residual, 2),
    +        ggml_vk_nb_elem(post, 0), ggml_vk_nb_elem(post, 1),
    +        ggml_vk_nb_elem(comb, 0), ggml_vk_nb_elem(comb, 1), ggml_vk_nb_elem(comb, 2),
    +        ggml_vk_nb_elem(dst,  0), ggml_vk_nb_elem(dst,  1), ggml_vk_nb_elem(dst,  2),
    +        0, 0, 0, 0, 0,
    +    };
    +    init_pushconst_tensor_offsets(ctx, pc, x, residual, post, comb, dst);
    +
    +    ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { x_buf, r_buf, p_buf, c_buf, d_buf }, pc, { n_embd, n_tokens, 1 });
    +}
    +
     static void ggml_vk_mul_mat(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx) {
         ggml_tensor * dst = cgraph->nodes[node_idx];
         ggml_tensor * src0 = dst->src[0];
    @@ -16222,6 +16400,18 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr
         case GGML_OP_CUMSUM:
             ggml_vk_cumsum(ctx, compute_ctx, src0, node);
     
    +        break;
    +    case GGML_OP_DSV4_HC_COMB:
    +        ggml_vk_dsv4_hc_comb(ctx, compute_ctx, src0, src1, src2, node);
    +
    +        break;
    +    case GGML_OP_DSV4_HC_PRE:
    +        ggml_vk_dsv4_hc_pre(ctx, compute_ctx, src0, src1, node);
    +
    +        break;
    +    case GGML_OP_DSV4_HC_POST:
    +        ggml_vk_dsv4_hc_post(ctx, compute_ctx, src0, src1, src2, src3, node);
    +
             break;
         case GGML_OP_MEAN:
             ggml_vk_mean(ctx, compute_ctx, src0, node);
    @@ -19289,6 +19479,31 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm
                     }
                     return false;
                 }
    +        case GGML_OP_DSV4_HC_COMB:
    +        case GGML_OP_DSV4_HC_PRE:
    +        case GGML_OP_DSV4_HC_POST:
    +            {
    +                if (op->type != GGML_TYPE_F32) {
    +                    return false;
    +                }
    +                for (uint32_t i = 0; i < GGML_MAX_SRC; ++i) {
    +                    if (op->src[i] && op->src[i]->type != GGML_TYPE_F32) {
    +                        return false;
    +                    }
    +                }
    +                // hc is hardcoded to 4 in the shaders. ggml only constrains it
    +                // to 4 for COMB, so PRE/POST have to be checked here.
    +                if (op->op == GGML_OP_DSV4_HC_PRE && op->src[0]->ne[1] != 4) {
    +                    return false;
    +                }
    +                if (op->op == GGML_OP_DSV4_HC_POST && op->src[1]->ne[1] != 4) {
    +                    return false;
    +                }
    +                if (op->op == GGML_OP_DSV4_HC_COMB) {
    +                    return device->pipeline_dsv4_hc_comb_f32 != nullptr;
    +                }
    +                return true;
    +            }
             case GGML_OP_SOLVE_TRI:
                 {
                     if (op->type != GGML_TYPE_F32 || op->src[0]->type != GGML_TYPE_F32) {
    @@ -20277,6 +20492,13 @@ static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph *
                 tensor_clone = ggml_sum_rows(ggml_ctx, src_clone[0]);
             } else if (tensor->op == GGML_OP_CUMSUM) {
                 tensor_clone = ggml_cumsum(ggml_ctx, src_clone[0]);
    +        } else if (tensor->op == GGML_OP_DSV4_HC_COMB) {
    +            tensor_clone = ggml_dsv4_hc_comb(ggml_ctx, src_clone[0], src_clone[1], src_clone[2],
    +                ggml_get_op_params_f32(tensor, 0), ggml_get_op_params_i32(tensor, 1));
    +        } else if (tensor->op == GGML_OP_DSV4_HC_PRE) {
    +            tensor_clone = ggml_dsv4_hc_pre(ggml_ctx, src_clone[0], src_clone[1]);
    +        } else if (tensor->op == GGML_OP_DSV4_HC_POST) {
    +            tensor_clone = ggml_dsv4_hc_post(ggml_ctx, src_clone[0], src_clone[1], src_clone[2], src_clone[3]);
             } else if (tensor->op == GGML_OP_MEAN) {
                 tensor_clone = ggml_mean(ggml_ctx, src_clone[0]);
             } else if (tensor->op == GGML_OP_ARGMAX) {
    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 000000000..f4ac0378a
    --- /dev/null
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_comb.comp
    @@ -0,0 +1,90 @@
    +#version 450
    +
    +#extension GL_EXT_control_flow_attributes : require
    +#extension GL_KHR_shader_subgroup_basic : require
    +#extension GL_KHR_shader_subgroup_shuffle : require
    +
    +// 16 lanes per token, indexed idst + hc*isrc: idst in bits 0..1, isrc in bits 2..3,
    +// so subgroupShuffleXor by 1|2 reduces a row and by 4|8 a column.
    +
    +layout(constant_id = 0) const uint SUBGROUP_SIZE = 32;
    +
    +layout(local_size_x_id = 0, local_size_y = 4, local_size_z = 1) in;
    +
    +layout(push_constant) uniform parameter
    +{
    +    uint n_tokens;
    +
    +    uint nbm0; uint nbm1;   // mixes
    +    uint nbs0;              // scale
    +    uint nbb0;              // base
    +    uint nbd0; uint nbd1; uint nbd2;   // dst
    +
    +    uint m_offset;
    +    uint s_offset;
    +    uint b_offset;
    +    uint d_offset;
    +
    +    float eps;
    +    uint n_iter;
    +};
    +
    +layout(binding = 0, std430) readonly buffer M { float data_m[]; };
    +layout(binding = 1, std430) readonly buffer S { float data_s[]; };
    +layout(binding = 2, std430) readonly buffer B { float data_b[]; };
    +layout(binding = 3, std430) writeonly buffer D { float data_d[]; };
    +
    +const uint hc          = 4;
    +const uint comb_offset = 2 * hc;
    +
    +const uint TOKENS_PER_SUBGROUP = SUBGROUP_SIZE / 16;
    +
    +void main() {
    +    const uint lane = gl_SubgroupInvocationID;
    +    const uint blk  = lane >> 4;    // which 16-lane block, i.e. which token
    +    const uint idx  = lane & 15;    // idst + hc*isrc
    +
    +    const uint sg = gl_WorkGroupID.x * gl_WorkGroupSize.y + gl_SubgroupID;
    +    const uint it = sg * TOKENS_PER_SUBGROUP + blk;
    +
    +    // no early return, the shuffles need every lane; out-of-range blocks compute a discarded value
    +    const bool in_range = it < n_tokens;
    +
    +    const float scale_comb = data_s[s_offset + 2 * nbs0];
    +
    +    float v = 0.0f;
    +    if (in_range) {
    +        v = data_m[m_offset + (comb_offset + idx) * nbm0 + it * nbm1] * scale_comb
    +          + data_b[b_offset + (comb_offset + idx) * nbb0];
    +    }
    +
    +    // Softmax across destinations: the four lanes sharing an isrc.
    +    float vmax = max(v, subgroupShuffleXor(v, 1));
    +    vmax = max(vmax, subgroupShuffleXor(vmax, 2));
    +    v = exp(v - vmax);
    +
    +    float sum = v + subgroupShuffleXor(v, 1);
    +    sum += subgroupShuffleXor(sum, 2);
    +    v = v / sum + eps;
    +
    +    // Normalize columns: equal destination indices are four lanes apart.
    +    sum = v + subgroupShuffleXor(v, 4);
    +    sum += subgroupShuffleXor(sum, 8);
    +    v /= sum + eps;
    +
    +    for (uint i = 1; i < n_iter; ++i) {
    +        sum = v + subgroupShuffleXor(v, 1);
    +        sum += subgroupShuffleXor(sum, 2);
    +        v /= sum + eps;
    +
    +        sum = v + subgroupShuffleXor(v, 4);
    +        sum += subgroupShuffleXor(sum, 8);
    +        v /= sum + eps;
    +    }
    +
    +    if (in_range) {
    +        const uint idst = idx & 3;
    +        const uint isrc = idx >> 2;
    +        data_d[d_offset + idst * nbd0 + isrc * nbd1 + it * nbd2] = v;
    +    }
    +}
    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 000000000..bab6f8767
    --- /dev/null
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_post.comp
    @@ -0,0 +1,83 @@
    +#version 450
    +
    +#extension GL_EXT_control_flow_attributes : require
    +
    +// Fan one stream back out to hc streams and add the combination-weighted
    +// residuals:
    +//
    +//   dst[i0, idst, it] = x[i0, it]*post[idst, it]
    +//                     + sum_isrc residual[i0, isrc, it]*comb[idst, isrc, it]
    +
    +layout(constant_id = 0) const uint BLOCK_SIZE = 256;
    +
    +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in;
    +
    +layout(push_constant) uniform parameter
    +{
    +    uint n_embd;
    +    uint n_tokens;
    +
    +    uint nbx0; uint nbx1;              // x
    +    uint nbr0; uint nbr1; uint nbr2;   // residual
    +    uint nbp0; uint nbp1;              // post
    +    uint nbc0; uint nbc1; uint nbc2;   // comb
    +    uint nbd0; uint nbd1; uint nbd2;   // dst
    +
    +    uint x_offset;
    +    uint r_offset;
    +    uint p_offset;
    +    uint c_offset;
    +    uint d_offset;
    +};
    +
    +layout(binding = 0, std430) readonly buffer X { float data_x[]; };
    +layout(binding = 1, std430) readonly buffer R { float data_r[]; };
    +layout(binding = 2, std430) readonly buffer P { float data_p[]; };
    +layout(binding = 3, std430) readonly buffer C { float data_c[]; };
    +layout(binding = 4, std430) writeonly buffer D { float data_d[]; };
    +
    +const uint hc = 4;
    +
    +shared float post_s[hc];
    +shared float comb_s[hc * hc];
    +
    +void main() {
    +    const uint tid = gl_LocalInvocationID.x;
    +    const uint it  = gl_WorkGroupID.y;
    +
    +    if (tid < hc) {
    +        post_s[tid] = data_p[p_offset + tid * nbp0 + it * nbp1];
    +    }
    +    if (tid < hc * hc) {
    +        const uint idst = tid & 3;
    +        const uint isrc = tid >> 2;
    +        comb_s[tid] = data_c[c_offset + idst * nbc0 + isrc * nbc1 + it * nbc2];
    +    }
    +    barrier();
    +
    +    // After the barrier, so every invocation reaches it.
    +    const uint i0 = gl_WorkGroupID.x * BLOCK_SIZE + tid;
    +    if (i0 >= n_embd) {
    +        return;
    +    }
    +
    +    const float xv = data_x[x_offset + i0 * nbx0 + it * nbx1];
    +
    +    const uint rb = r_offset + i0 * nbr0 + it * nbr2;
    +
    +    float r[hc];
    +    [[unroll]]
    +    for (uint isrc = 0; isrc < hc; ++isrc) {
    +        r[isrc] = data_r[rb + isrc * nbr1];
    +    }
    +
    +    [[unroll]]
    +    for (uint idst = 0; idst < hc; ++idst) {
    +        float result = xv * post_s[idst];
    +        [[unroll]]
    +        for (uint isrc = 0; isrc < hc; ++isrc) {
    +            result = fma(r[isrc], comb_s[idst + hc * isrc], result);
    +        }
    +        data_d[d_offset + i0 * nbd0 + idst * nbd1 + it * nbd2] = result;
    +    }
    +}
    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 000000000..51deabbac
    --- /dev/null
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/dsv4_hc_pre.comp
    @@ -0,0 +1,59 @@
    +#version 450
    +
    +#extension GL_EXT_control_flow_attributes : require
    +
    +// Collapse the hc residual streams of a token into one, weighted per stream:
    +//
    +//   dst[i0, it] = sum_ih x[i0, ih, it] * weights[ih, it]
    +
    +layout(constant_id = 0) const uint BLOCK_SIZE = 256;
    +
    +layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in;
    +
    +layout(push_constant) uniform parameter
    +{
    +    uint n_embd;
    +    uint n_tokens;
    +
    +    uint nbx0; uint nbx1; uint nbx2;   // x
    +    uint nbw0; uint nbw1;              // weights
    +    uint nbd0; uint nbd1;              // dst
    +
    +    uint x_offset;
    +    uint w_offset;
    +    uint d_offset;
    +};
    +
    +layout(binding = 0, std430) readonly buffer X { float data_x[]; };
    +layout(binding = 1, std430) readonly buffer W { float data_w[]; };
    +layout(binding = 2, std430) writeonly buffer D { float data_d[]; };
    +
    +const uint hc = 4;
    +
    +shared float w[hc];
    +
    +void main() {
    +    const uint tid = gl_LocalInvocationID.x;
    +    const uint it  = gl_WorkGroupID.y;
    +
    +    if (tid < hc) {
    +        w[tid] = data_w[w_offset + tid * nbw0 + it * nbw1];
    +    }
    +    barrier();
    +
    +    // After the barrier, so every invocation reaches it.
    +    const uint i0 = gl_WorkGroupID.x * BLOCK_SIZE + tid;
    +    if (i0 >= n_embd) {
    +        return;
    +    }
    +
    +    const uint xb = x_offset + i0 * nbx0 + it * nbx2;
    +
    +    float result = 0.0f;
    +    [[unroll]]
    +    for (uint ih = 0; ih < hc; ++ih) {
    +        result = fma(data_x[xb + ih * nbx1], w[ih], result);
    +    }
    +
    +    data_d[d_offset + i0 * nbd0 + it * nbd1] = result;
    +}
    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 da0d54ab4..2daafdf43 100644
    --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp
    +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp
    @@ -1042,6 +1042,9 @@ void process_shaders() {
         string_to_spv("fwht_f32", "fwht.comp", {});
         string_to_spv("fwht_shmem_f32", "fwht.comp", {{"FWHT_SHMEM", "1"}});
         string_to_spv("count_equal_i32", "count_equal.comp", merge_maps(base_dict, {{"A_TYPE", "int"}, {"B_TYPE", "int"}, {"D_TYPE", "int"}}));
    +    string_to_spv("dsv4_hc_comb_f32", "dsv4_hc_comb.comp", {});
    +    string_to_spv("dsv4_hc_pre_f32",  "dsv4_hc_pre.comp",  {});
    +    string_to_spv("dsv4_hc_post_f32", "dsv4_hc_post.comp", {});
         string_to_spv("cumsum_f32", "cumsum.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}}));
         string_to_spv("cumsum_multipass1_f32", "cumsum_multipass1.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}}));
         string_to_spv("cumsum_multipass2_f32", "cumsum_multipass2.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}}));
    diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp
    index eeaca940f..19eaacbec 100644
    --- a/tests/test-backend-ops.cpp
    +++ b/tests/test-backend-ops.cpp
    @@ -8807,6 +8807,11 @@ static std::vector> make_test_cases_eval() {
         test_cases.emplace_back(new test_dsv4_hc_comb(17, 4));
         test_cases.emplace_back(new test_dsv4_hc_comb(257, 8));
         test_cases.emplace_back(new test_dsv4_hc_comb(17, 20));
    +    // production n_iter (DeepSeek-V4 uses 20) across batch sizes that cross
    +    // subgroup and workgroup boundaries; 1 = single-token decode
    +    for (int64_t n_tokens : {1, 256, 336, 512, 513, 1024, 2048}) {
    +        test_cases.emplace_back(new test_dsv4_hc_comb(n_tokens, 20));
    +    }
     
         test_cases.emplace_back(new test_dsv4_hc_pre(1, 1));
         test_cases.emplace_back(new test_dsv4_hc_pre(31, 17));
    
    From dbeb37548e25abc6e54961c4c99e63f191367809 Mon Sep 17 00:00:00 2001
    From: Titaniumtown 
    Date: Mon, 7 Sep 2026 06:24:14 -0700
    Subject: [PATCH 27/31] sycl: add a batched L2_NORM kernel (#28222)
    
    * sycl: add a batched L2_NORM kernel
    
    * sycl: batch consecutive L2_NORM siblings in the graph dispatch
    
    Measured on Intel Arc Pro B70 (Battlemage), Qwen3.6-27B Q4_K_M, f16 KV,
    npp=128 ntg=128 npl=2, GGML_SYCL profiler:
    
        L2_NORM dispatches       12480 -> 6240
        L2_NORM device time      68.77 -> 39.14 ms   (-43%)
        total device time        6782 -> 6748 ms     (-0.5%)
        wall decode t/s          flat
    
    * tests: add L2_NORM_BATCH coverage
    ---
     ggml/src/ggml-sycl/ggml-sycl.cpp | 83 ++++++++++++++++++++++++++++++++
     ggml/src/ggml-sycl/norm.cpp      | 83 ++++++++++++++++++++++++++++++++
     ggml/src/ggml-sycl/norm.hpp      |  3 ++
     tests/test-backend-ops.cpp       | 57 ++++++++++++++++++++++
     4 files changed, 226 insertions(+)
    
    diff --git a/ggml/src/ggml-sycl/ggml-sycl.cpp b/ggml/src/ggml-sycl/ggml-sycl.cpp
    index bfe6f1016..4091f73a4 100644
    --- a/ggml/src/ggml-sycl/ggml-sycl.cpp
    +++ b/ggml/src/ggml-sycl/ggml-sycl.cpp
    @@ -4858,6 +4858,78 @@ static bool ggml_sycl_mul_mat_glu_mmvq_fused(ggml_backend_sycl_context & ctx, gg
                                                    /*stride_col_dst=*/(int) glu->ne[0], stream);
     }
     
    +// Batch the run of consecutive L2_NORM siblings starting at node_idx into one launch.
    +// Returns the number of extra graph nodes consumed, or 0 if the run is shorter than two
    +// (the caller then runs the norm through the per-tensor kernel).
    +static int ggml_sycl_l2_norm_batch_fused(ggml_backend_sycl_context & ctx, ggml_cgraph * cgraph, int node_idx) {
    +    const ggml_tensor * node = cgraph->nodes[node_idx];
    +    if (ggml_sycl_info().device_count != 1 || node->type != GGML_TYPE_F32 ||
    +        node->src[0]->type != GGML_TYPE_F32 || node->src[0]->ne[0] >= 1024) {
    +        return 0;
    +    }
    +
    +    ggml_tensor * batch[GGML_SYCL_L2_BATCH_MAX];
    +    int           count = 0;
    +    int           last  = node_idx;
    +    float         eps0;
    +    memcpy(&eps0, node->op_params, sizeof(float));
    +
    +    // Conservative aliasing test: the batched norms run concurrently in one kernel,
    +    // so none may read what another writes, and none may write where another writes.
    +    auto overlaps = [](const ggml_tensor * a, const ggml_tensor * b) {
    +        const char * ab = (const char *) a->data;
    +        const char * bb = (const char *) b->data;
    +        return ab < bb + ggml_nbytes(b) && bb < ab + ggml_nbytes(a);
    +    };
    +
    +    for (int j = node_idx; j < cgraph->n_nodes && count < GGML_SYCL_L2_BATCH_MAX; ++j) {
    +        ggml_tensor * nj = cgraph->nodes[j];
    +        if (ggml_is_empty(nj) || nj->op == GGML_OP_RESHAPE || nj->op == GGML_OP_TRANSPOSE ||
    +            nj->op == GGML_OP_VIEW || nj->op == GGML_OP_PERMUTE || nj->op == GGML_OP_NONE ||
    +            (nj->flags & GGML_TENSOR_FLAG_COMPUTE) == 0) {
    +            continue;  // not a launch; cannot break a run of adjacent norms
    +        }
    +        if (nj->op != GGML_OP_L2_NORM || nj->type != GGML_TYPE_F32 ||
    +            nj->src[0]->type != GGML_TYPE_F32 || !ggml_are_same_shape(nj, node) ||
    +            !ggml_are_same_shape(nj->src[0], node->src[0])) {
    +            break;  // any other launch ends the run
    +        }
    +        bool same_nb = true;
    +        for (int d = 0; d < GGML_MAX_DIMS; ++d) {
    +            if (nj->nb[d] != node->nb[d] || nj->src[0]->nb[d] != node->src[0]->nb[d]) {
    +                same_nb = false;
    +                break;
    +            }
    +        }
    +        if (!same_nb) {
    +            break;  // one nb[] stride set is shared by the whole batch
    +        }
    +        float epsj;
    +        memcpy(&epsj, nj->op_params, sizeof(float));
    +        if (epsj != eps0) {
    +            break;  // eps mismatch ends the run
    +        }
    +        bool indep = true;
    +        for (int k = 0; k < count; ++k) {
    +            if (overlaps(nj->src[0], batch[k]) || overlaps(nj, batch[k])) {
    +                indep = false;
    +                break;
    +            }
    +        }
    +        if (!indep) {
    +            break;  // an overlapping tensor would race inside one launch
    +        }
    +        batch[count++] = nj;
    +        last           = j;
    +    }
    +    if (count < 2) {
    +        return 0;  // a lone norm falls through to the per-tensor kernel
    +    }
    +    ggml_sycl_l2_norm_batch(ctx, batch, count);
    +    return last - node_idx;
    +}
    +
    +
     __dpct_inline__ static void k_copy_src1_to_contiguous(
         const char *__restrict__ src1_original, char *__restrict__ src1_contiguous,
         const mmid_row_mapping *__restrict__ row_mapping,
    @@ -5908,6 +5980,17 @@ static void ggml_backend_sycl_graph_compute_impl(ggml_backend_sycl_context * syc
                 continue;
             }
     
    +        // Batch consecutive independent same-shape F32 L2_NORM siblings (the GDN q/k
    +        // norms) into one launch; sources are strided views of the fused qkv buffer, so
    +        // the scan skips the interleaved view nodes instead of breaking on them.
    +        if (node->op == GGML_OP_L2_NORM) {
    +            const int l2_batch_skip = ggml_sycl_l2_norm_batch_fused(*sycl_ctx, cgraph, i);
    +            if (l2_batch_skip > 0) {
    +                i += l2_batch_skip;
    +                continue;
    +            }
    +        }
    +
             if (node->op == GGML_OP_MUL_MAT && ggml_sycl_mul_mat_glu_mmvq_fused(*sycl_ctx, cgraph, i)) {
                 i += 2;
                 continue;
    diff --git a/ggml/src/ggml-sycl/norm.cpp b/ggml/src/ggml-sycl/norm.cpp
    index 2d3033729..bc36a9d4c 100644
    --- a/ggml/src/ggml-sycl/norm.cpp
    +++ b/ggml/src/ggml-sycl/norm.cpp
    @@ -543,6 +543,62 @@ static void l2_norm_f32_sycl(const float *   x,
         }
     }
     
    +// Batched L2 norm: N independent same-shape F32 tensors in one launch; the tensor
    +// index is folded into grid dim0 and each row's reduction is identical to the
    +// single-tensor kernel, so the result is bit-exact.
    +struct l2_batch_ptrs {
    +    const float * src[GGML_SYCL_L2_BATCH_MAX];
    +    float *       dst[GGML_SYCL_L2_BATCH_MAX];
    +};
    +
    +// One stride set shared by the whole batch: the caller only groups tensors whose nb[]
    +// all match, so per-tensor state stays two pointers.
    +struct l2_batch_strides {
    +    int     ne1, ne2;
    +    int64_t ss0, ss1, ss2, ss3;
    +    int64_t ds0, ds1, ds2, ds3;
    +};
    +
    +template 
    +static void l2_norm_f32_batch(l2_batch_ptrs p, l2_batch_strides st, const int ncols, const float eps,
    +                              const sycl::nd_item<3> & item_ct1) {
    +    const int t   = item_ct1.get_group(0);  // tensor index
    +    const int r   = item_ct1.get_group(2);  // flattened row over ne1*ne2*ne3
    +    const int tid = item_ct1.get_local_id(2);
    +
    +    const int i1 = r % st.ne1;
    +    const int i2 = (r / st.ne1) % st.ne2;
    +    const int i3 = r / (st.ne1 * st.ne2);
    +
    +    const float * x   = p.src[t] + i3 * st.ss3 + i2 * st.ss2 + i1 * st.ss1;
    +    float *       dst = p.dst[t] + i3 * st.ds3 + i2 * st.ds2 + i1 * st.ds1;
    +
    +    float tmp = 0.0f;
    +    for (int col = tid; col < ncols; col += warp_size) {
    +        const float xi = x[col * st.ss0];
    +        tmp += xi * xi;
    +    }
    +    tmp = block_reduce(tmp, (float *) nullptr, warp_size);
    +    const float scale = sycl::rsqrt(sycl::fmax(tmp, eps * eps));
    +    for (int col = tid; col < ncols; col += warp_size) {
    +        dst[col * st.ds0] = scale * x[col * st.ss0];
    +    }
    +}
    +
    +template 
    +static void l2_norm_f32_batch_sycl(l2_batch_ptrs p, l2_batch_strides st, const int n_tensors,
    +                                   const int ncols, const int nrows_total, const float eps,
    +                                   queue_ptr stream) {
    +    const dpct::dim3 blocks_num(nrows_total, 1, n_tensors);
    +    const dpct::dim3 block_dims(warp_size, 1, 1);
    +    stream->submit([&](sycl::handler & cgh) {
    +        cgh.parallel_for(sycl::nd_range<3>(blocks_num * block_dims, block_dims),
    +            [=](sycl::nd_item<3> item_ct1) [[sycl::reqd_sub_group_size(warp_size)]] {
    +                l2_norm_f32_batch(p, st, ncols, eps, item_ct1);
    +            });
    +    });
    +}
    +
     void ggml_sycl_op_norm(ggml_backend_sycl_context& ctx, ggml_tensor* dst) {
         const ggml_tensor * src0 = dst->src[0];
     
    @@ -961,3 +1017,30 @@ void ggml_sycl_op_l2_norm(ggml_backend_sycl_context& ctx, ggml_tensor* dst) {
         l2_norm_f32_sycl(src0_d, dst_d, ne00, ne01, ne02, ne03,
                 ss0, ss1, ss2, ss3, ds0, ds1, ds2, ds3, eps, stream, ctx.device);
     }
    +
    +// nodes[0..count) are independent, same-shape, same-eps, same-nb L2_NORM ops validated
    +// by the caller; requires ncols < 1024 (the warp reduction path).
    +void ggml_sycl_l2_norm_batch(ggml_backend_sycl_context & ctx, ggml_tensor ** nodes, int count) {
    +    const ggml_tensor * s0 = nodes[0]->src[0];
    +    const int ncols       = (int) s0->ne[0];
    +    const int nrows_total = (int) ggml_nrows(s0);
    +    float eps;
    +    memcpy(&eps, nodes[0]->op_params, sizeof(float));
    +    GGML_ASSERT(eps >= 0.0f);
    +
    +    l2_batch_ptrs p{};
    +    for (int t = 0; t < count; ++t) {
    +        p.src[t] = (const float *) nodes[t]->src[0]->data;
    +        p.dst[t] = (float *) nodes[t]->data;
    +    }
    +
    +    const ggml_tensor * d0 = nodes[0];
    +    const size_t        ts = ggml_type_size(GGML_TYPE_F32);
    +    l2_batch_strides    st{};
    +    st.ne1 = (int) s0->ne[1];
    +    st.ne2 = (int) s0->ne[2];
    +    st.ss0 = s0->nb[0] / ts; st.ss1 = s0->nb[1] / ts; st.ss2 = s0->nb[2] / ts; st.ss3 = s0->nb[3] / ts;
    +    st.ds0 = d0->nb[0] / ts; st.ds1 = d0->nb[1] / ts; st.ds2 = d0->nb[2] / ts; st.ds3 = d0->nb[3] / ts;
    +
    +    l2_norm_f32_batch_sycl(p, st, count, ncols, nrows_total, eps, ctx.stream());
    +}
    diff --git a/ggml/src/ggml-sycl/norm.hpp b/ggml/src/ggml-sycl/norm.hpp
    index ef7b2d386..46c6de2a1 100644
    --- a/ggml/src/ggml-sycl/norm.hpp
    +++ b/ggml/src/ggml-sycl/norm.hpp
    @@ -29,4 +29,7 @@ void ggml_sycl_op_group_norm(ggml_backend_sycl_context& ctx, ggml_tensor* dst);
     
     void ggml_sycl_op_l2_norm(ggml_backend_sycl_context& ctx, ggml_tensor* dst);
     
    +#define GGML_SYCL_L2_BATCH_MAX 8
    +void ggml_sycl_l2_norm_batch(ggml_backend_sycl_context & ctx, ggml_tensor ** nodes, int count);
    +
     #endif // GGML_SYCL_NORM_HPP
    diff --git a/tests/test-backend-ops.cpp b/tests/test-backend-ops.cpp
    index 19eaacbec..cd16e5851 100644
    --- a/tests/test-backend-ops.cpp
    +++ b/tests/test-backend-ops.cpp
    @@ -7206,6 +7206,49 @@ struct test_group_norm_mul_add : public test_case {
         }
     };
     
    +// GGML_OP_L2_NORM x N: independent same-shape norms in one graph (strided qkv views or
    +// contiguous), consuming adds nested so the norms stay adjacent in the graph.
    +struct test_l2_norm_batch : public test_case {
    +    const ggml_type              type;
    +    const std::array ne;
    +    const int                    n_norms;
    +    const float                  eps;
    +    const bool                   strided;
    +
    +    std::string vars() override { return VARS_TO_STR5(type, ne, n_norms, eps, strided); }
    +    std::string op_desc(ggml_tensor * t) override { GGML_UNUSED(t); return "L2_NORM_BATCH"; }
    +    bool run_whole_graph() override { return true; }
    +
    +    test_l2_norm_batch(ggml_type type = GGML_TYPE_F32, std::array ne = { 128, 16, 16, 1 },
    +                       int n_norms = 4, float eps = 1e-12f, bool strided = true)
    +        : type(type), ne(ne), n_norms(n_norms), eps(eps), strided(strided) {}
    +
    +    ggml_tensor * build_graph(ggml_context * ctx) override {
    +        GGML_ASSERT(n_norms >= 2 && n_norms <= 8);
    +        ggml_tensor * parent = nullptr;
    +        if (strided) {
    +            parent = ggml_new_tensor_4d(ctx, type, ne[0], ne[1] * n_norms, ne[2], ne[3]);  // qkv buffer
    +        }
    +        ggml_tensor * norms[8];
    +        for (int t = 0; t < n_norms; ++t) {
    +            ggml_tensor * src;
    +            if (strided) {
    +                src = ggml_view_4d(ctx, parent, ne[0], ne[1], ne[2], ne[3], parent->nb[1], parent->nb[2],
    +                                   parent->nb[3], t * ne[1] * parent->nb[1]);
    +            } else {
    +                src = ggml_new_tensor(ctx, type, 4, ne.data());
    +            }
    +            norms[t] = ggml_l2_norm(ctx, src, eps);
    +        }
    +        ggml_tensor * out = norms[n_norms - 1];
    +        for (int t = n_norms - 2; t >= 0; --t) {
    +            out = ggml_add(ctx, norms[t], out);
    +        }
    +        ggml_set_name(out, "out");
    +        return out;
    +    }
    +};
    +
     // GGML_OP_L2_NORM
     struct test_l2_norm : public test_case {
         const ggml_type type;
    @@ -9495,6 +9538,10 @@ static std::vector> make_test_cases_eval() {
                 test_cases.emplace_back(new test_l2_norm(GGML_TYPE_F32, { n, 5, 4, 3 }, eps, false));
                 test_cases.emplace_back(new test_l2_norm(GGML_TYPE_F32, { n, 5, 4, 3 }, eps, true));
                 test_cases.emplace_back(new test_l2_norm(GGML_TYPE_F32, { n, 5, 4, 3 }, eps, false, true));
    +            // sibling batching: strided (production shape) and contiguous, 2 and 4 wide
    +            test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 5, 4, 3 }, 2, eps, true));
    +            test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 5, 4, 3 }, 4, eps, true));
    +            test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 5, 4, 3 }, 4, eps, false));
             }
             // row lengths that are not a multiple of 32, for the scalar (33) and float4 (132, 260) paths
             for (uint32_t n : { 33, 132, 260 }) {
    @@ -11181,6 +11228,16 @@ static std::vector> make_test_cases_perf() {
             }
         }
     
    +    // launch-overhead isolation: single L2_NORM launch vs batched siblings at the GDN
    +    // production shape (strided qkv views) -- perf-mode only, the eval list has its own
    +    // 2/4-wide coverage
    +    for (int n : { 128, 256 }) {
    +        test_cases.emplace_back(new test_l2_norm(GGML_TYPE_F32, { n, 16, 16, 1 }, 1e-12f, false, false));
    +        test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 16, 16, 1 }, 2, 1e-12f, true));
    +        test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 16, 16, 1 }, 4, 1e-12f, true));
    +    }
    +
    +
         return test_cases;
     }
     
    
    From 160bd031b25fb93eda4b1ab1a86d860751b9e444 Mon Sep 17 00:00:00 2001
    From: Xuan-Son Nguyen 
    Date: Mon, 7 Sep 2026 15:50:46 +0200
    Subject: [PATCH 28/31] server: fix LRU hang on multiple requests same model
     (#28539)
    
    * server: fix LRU hang on multiple requests same model
    
    * server: keep a queued model out of the victim pool until its waiters leave
    
    A waiter that gave up while its model was still loading left the
    model idle with no request behind it, and nothing recounted the free
    slots, so a second request queued behind it stayed queued forever.
    tick() was only driven by requests: join, claim and the end of a
    proxied request.
    
    Keep the queue entry alive after a successful claim so the model
    coming up is never picked as a victim before its waiters use it, and
    recount the slots on every status change and whenever a waiter
    abandons the queue. The model is then evicted as soon as it comes up
    with nobody left to serve.
    
    ---------
    
    Co-authored-by: Pascal 
    ---
     tools/server/server-models.cpp         | 165 ++++++++++---------------
     tools/server/server-models.h           |   4 +
     tools/server/tests/unit/test_router.py |  20 +++
     3 files changed, 92 insertions(+), 97 deletions(-)
    
    diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp
    index db0fac995..4d2592b25 100644
    --- a/tools/server/server-models.cpp
    +++ b/tools/server/server-models.cpp
    @@ -80,18 +80,19 @@ struct server_lru_sched {
         }
     
         // returns "" if no model can be given up
    -    std::string pick_victim(std::unique_lock & lk, const std::string & exclude) {
    +    std::string pick_victim(std::unique_lock & lk) {
             check_lock(lk);
             std::string victim;
             int64_t victim_last_used = 0;
             for (const auto & m : models.mapping) {
    -            if (m.first == exclude) {
    -                continue;
    -            }
                 // a busy model is mid-request, one still coming up has no request to finish
                 if (m.second.req_count != 0 || !m.second.meta.is_ready_or_sleep()) {
                     continue;
                 }
    +            // already on its way out, or a queued request wants it
    +            if (models.stopping_models.count(m.first) || find(m.first)) {
    +                continue;
    +            }
                 if (victim.empty() || m.second.meta.last_used < victim_last_used) {
                     victim           = m.first;
                     victim_last_used = m.second.meta.last_used;
    @@ -109,7 +110,7 @@ struct server_lru_sched {
                 SRV_INF("request for name=%s joined the queue, %d waiting\n", model_id.c_str(), e->n_waiters);
                 return;
             }
    -        queue.push_back({ model_id, 1, false, false });
    +        queue.push_back({ model_id, 1, false });
             SRV_INF("models_max reached, request for name=%s queued at position %zu\n",
                     model_id.c_str(), queue.size());
         }
    @@ -144,85 +145,67 @@ struct server_lru_sched {
             return true;
         }
     
    -    // ok means the model is up: drop the entry, the other waiters just watch its status now
    +    // on failure the entry is back in line; on success it stays until its waiters leave,
    +    // so the model coming up is never picked as a victim before they use it
         void claim_done(std::unique_lock & lk, const std::string & model_id, bool ok) {
             check_lock(lk);
    +        if (ok) {
    +            return;
    +        }
             for (auto it = queue.begin(); it != queue.end(); ++it) {
                 if (it->model_id == model_id) {
    -                if (ok) {
    -                    queue.erase(it);
    -                } else {
    -                    it->loading = false;
    -                }
    +                it->loading = false;
                     return;
                 }
             }
         }
     
    -    // a model is on its way out for this entry, so other requests do not also give up one
    -    void mark_slot_pending(std::unique_lock & lk, const std::string & model_id) {
    +    // evict idle models while queued requests outnumber the slots that are free or being freed
    +    // caller must hold models.mutex; never blocks, so it is safe from any thread
    +    void tick(std::unique_lock & lk) {
             check_lock(lk);
    -        if (entry_t * e = find(model_id)) {
    -            e->slot_pending = true;
    +        if (models.base_params.models_max <= 0 || queue.empty()) {
    +            return;
             }
    -    }
    -
    -    // model_id went idle: give up its slot if a queued request needs one
    -    // thread-safe, caller must NOT hold models.mutex
    -    void on_model_idle(const std::string & model_id) {
    -        if (models.base_params.models_max <= 0) {
    -            return; // no limit, nothing is ever queued
    -        }
    -        {
    -            std::unique_lock lk(models.mutex);
    -            if (queue.empty()) {
    -                return;
    -            }
    -            size_t promised     = 0;
    -            bool   has_unserved = false;
    -            for (const auto & e : queue) {
    -                if (e.needs_slot()) {
    -                    has_unserved = true;
    -                } else {
    -                    promised++;
    -                }
    -            }
    -            if (!has_unserved) {
    -                return;
    -            }
    -            if ((int) count_running() - (int) promised < models.base_params.models_max) {
    -                return; // a slot is already on its way
    -            }
    -            // never give up a model that a queued request wants
    -            for (const auto & e : queue) {
    -                if (e.model_id == model_id) {
    -                    return;
    -                }
    -            }
    -            auto it = models.mapping.find(model_id);
    -            if (it == models.mapping.end() || it->second.req_count != 0 || !it->second.meta.is_ready_or_sleep()) {
    -                return;
    -            }
    -            for (auto & e : queue) {
    -                if (!e.slot_pending) {
    -                    e.slot_pending = true;
    -                    break;
    +        int n_running  = 0;
    +        int n_stopping = 0;
    +        for (const auto & m : models.mapping) {
    +            if (m.second.meta.is_running()) {
    +                n_running++;
    +                if (models.stopping_models.count(m.first)) {
    +                    n_stopping++;
                     }
                 }
             }
    -        SRV_INF("model name=%s went idle, giving up its slot to a queued request\n", model_id.c_str());
    -        models.unload(model_id);
    +        int n_needed  = 0;
    +        int n_claimed = 0; // claimed the slot, but load() has not spawned yet
    +        for (const auto & e : queue) {
    +            if (!e.loading) {
    +                n_needed++;
    +                continue;
    +            }
    +            auto it = models.mapping.find(e.model_id);
    +            if (it != models.mapping.end() && !it->second.meta.is_running()) {
    +                n_claimed++;
    +            }
    +        }
    +        int n_free = models.base_params.models_max - n_running + n_stopping - n_claimed;
    +        while (n_free < n_needed) {
    +            std::string victim = pick_victim(lk);
    +            if (victim.empty()) {
    +                return; // all remaining models are busy, wait for a request to end
    +            }
    +            SRV_INF("evicting idle LRU name=%s for a queued request\n", victim.c_str());
    +            models.request_stop(victim);
    +            n_free++;
    +        }
         }
     
       private:
         struct entry_t {
             std::string model_id;
    -        int  n_waiters;    // requests waiting for this model
    -        bool slot_pending; // a model is already being evicted for this entry
    -        bool loading;      // one of the waiters is doing the load right now
    -
    -        // a slot is already coming, or already taken by the load in flight
    -        bool needs_slot() const { return !slot_pending && !loading; }
    +        int  n_waiters; // requests waiting for this model
    +        bool loading;   // one of the waiters is doing the load right now
         };
     
         entry_t * find(const std::string & model_id) {
    @@ -946,7 +929,7 @@ void server_models::unload_lru() {
             if (sched->has_capacity(lk)) {
                 return;
             }
    -        lru_model_name = sched->pick_victim(lk, "");
    +        lru_model_name = sched->pick_victim(lk);
         }
         if (!lru_model_name.empty()) {
             SRV_INF("models_max limit reached, removing LRU name=%s\n", lru_model_name.c_str());
    @@ -1169,6 +1152,11 @@ void server_models::load(const std::string & name, const load_options & opts) {
         cv.notify_all();
     }
     
    +void server_models::request_stop(const std::string & name) {
    +    stopping_models.insert(name);
    +    cv_stop.notify_all();
    +}
    +
     void server_models::unload(const std::string & name) {
         std::unique_lock lk(mutex);
         auto it = mapping.find(name);
    @@ -1182,13 +1170,12 @@ void server_models::unload(const std::string & name) {
                 });
             } else if (it->second.meta.is_running()) {
                 SRV_INF("stopping model instance name=%s\n", name.c_str());
    -            stopping_models.insert(name);
                 if (it->second.meta.status == SERVER_MODEL_STATUS_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();
                 }
    -            cv_stop.notify_all();
    +            request_stop(name);
                 // status change will be handled by the managing thread
             } else {
                 SRV_WRN("model instance name=%s is not running\n", name.c_str());
    @@ -1206,8 +1193,7 @@ void server_models::unload_all() {
                     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());
    -                stopping_models.insert(name);
    -                cv_stop.notify_all();
    +                request_stop(name);
                     // status change will be handled by the managing thread
                 }
                 // moving the thread to join list to avoid deadlock
    @@ -1234,6 +1220,8 @@ void server_models::update_status(const std::string & name, const update_status_
             if (!args.progress.is_null()) {
                 meta.progress = args.progress;
             }
    +        // a model that comes up idle or goes down changes the slot count for queued requests
    +        sched->tick(lk);
         }
         // broadcast status change to SSE
         {
    @@ -1380,13 +1368,11 @@ bool server_models::ensure_model_ready(const std::string & name, const std::func
     
         bool queued   = false;
         bool did_load = false;
    -    std::string victim;
         {
             std::unique_lock lk(mutex);
             auto it = mapping.find(name);
             if (it != mapping.end() && it->second.meta.status == SERVER_MODEL_STATUS_UNLOADED) {
    -            bool has_capacity = sched->has_capacity(lk);
    -            if (has_capacity && sched->queue_empty(lk)) {
    +            if (sched->has_capacity(lk) && sched->queue_empty(lk)) {
                     lk.unlock();
                     SRV_INF("model name=%s is not loaded, loading...\n", name.c_str());
                     load(name);
    @@ -1394,21 +1380,11 @@ bool server_models::ensure_model_ready(const std::string & name, const std::func
                 } else {
                     // also queue when a slot looks free but others wait already, else they starve
                     sched->join(lk, name);
    +                sched->tick(lk);
                     queued = true;
    -                if (!has_capacity) {
    -                    // an idle model may sit here right now, do not wait for a request to end
    -                    victim = sched->pick_victim(lk, name);
    -                    if (!victim.empty()) {
    -                        sched->mark_slot_pending(lk, name);
    -                    }
    -                }
                 }
             }
         }
    -    if (!victim.empty()) {
    -        SRV_INF("evicting idle LRU name=%s to make room for name=%s\n", victim.c_str(), name.c_str());
    -        unload(victim);
    -    }
     
         // while queued, this is also where the load happens: the head of the queue does it
         SRV_INF("waiting until model name=%s is fully loaded...\n", name.c_str());
    @@ -1470,9 +1446,7 @@ bool server_models::ensure_model_ready(const std::string & name, const std::func
                     }
                     lk.lock();
                     sched->claim_done(lk, name, ok);
    -                if (ok) {
    -                    queued = false; // entry is gone, the other waiters watch the status now
    -                }
    +                sched->tick(lk);
                     continue;
                 }
     
    @@ -1480,6 +1454,7 @@ bool server_models::ensure_model_ready(const std::string & name, const std::func
             }
         } catch (...) {
             leave_queue();
    +        sched->tick(lk); // a slot freed for this waiter goes to the next one
             throw;
         }
         leave_queue();
    @@ -1529,18 +1504,14 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
                 );
     
         proxy->cleanup = [this, name]() {
    -        bool went_idle = false;
    -        {
    -            std::unique_lock lk(mutex);
    -            auto it = mapping.find(name);
    -            if (it != mapping.end() && it->second.req_count > 0) {
    -                it->second.req_count--;
    -                went_idle = it->second.req_count == 0;
    +        std::unique_lock lk(mutex);
    +        auto it = mapping.find(name);
    +        if (it != mapping.end() && it->second.req_count > 0) {
    +            it->second.req_count--;
    +            if (it->second.req_count == 0) {
    +                sched->tick(lk);
                 }
             }
    -        if (went_idle) {
    -            sched->on_model_idle(name);
    -        }
         };
     
         return proxy;
    diff --git a/tools/server/server-models.h b/tools/server/server-models.h
    index 5cbb6a801..7f6c26b35 100644
    --- a/tools/server/server-models.h
    +++ b/tools/server/server-models.h
    @@ -216,6 +216,10 @@ private:
         // not thread-safe, caller must hold mutex
         void add_model(server_model_meta && meta);
     
    +    // ask the monitoring thread to stop a running instance
    +    // not thread-safe, caller must hold mutex
    +    void request_stop(const std::string & name);
    +
         // notify SSE clients
         void notify_sse(const std::string & event, const std::string & model_id, const json & data = nullptr);
     
    diff --git a/tools/server/tests/unit/test_router.py b/tools/server/tests/unit/test_router.py
    index 96eb87978..e4b7f9fe4 100644
    --- a/tools/server/tests/unit/test_router.py
    +++ b/tools/server/tests/unit/test_router.py
    @@ -297,6 +297,26 @@ def test_router_queue_is_fifo():
         assert first.done_at < second.done_at, "queue was not served in arrival order"
     
     
    +def test_router_queue_two_waiters_share_one_eviction():
    +    """two requests that both find the same idle model must both be served in the end"""
    +    global server
    +    server.models_max = 1
    +    server.start()
    +
    +    _load_model_and_wait(MODEL_A, timeout=120)
    +
    +    # both arrive while MODEL_A is idle, so both want its slot; only one eviction can happen
    +    first = _Bg(lambda: _tokenize(MODEL_B)).start()
    +    second = _Bg(lambda: _tokenize(MODEL_C)).start()
    +
    +    first.join(90)
    +    second.join(90)
    +
    +    first.assert_ok("first queued request")
    +    second.assert_ok("second queued request")
    +    assert _get_model_status(MODEL_A) == "unloaded"
    +
    +
     def test_router_no_models_autoload():
         global server
         server.no_models_autoload = True
    
    From c0b1871bc7e4d6da285a348b93c0dbe2dbc54852 Mon Sep 17 00:00:00 2001
    From: Pascal 
    Date: Mon, 7 Sep 2026 15:55:14 +0200
    Subject: [PATCH 29/31] webgpu: format the GET_ROWS case block (#28542)
    
    Brace on its own line and body indented one level, matching the
    surrounding cases, so the webgpu clang-format check passes.
    ---
     ggml/src/ggml-webgpu/ggml-webgpu.cpp | 28 +++++++++++++++-------------
     1 file changed, 15 insertions(+), 13 deletions(-)
    
    diff --git a/ggml/src/ggml-webgpu/ggml-webgpu.cpp b/ggml/src/ggml-webgpu/ggml-webgpu.cpp
    index 2e6c5a8c5..f06a9c872 100644
    --- a/ggml/src/ggml-webgpu/ggml-webgpu.cpp
    +++ b/ggml/src/ggml-webgpu/ggml-webgpu.cpp
    @@ -4323,21 +4323,23 @@ static bool ggml_backend_webgpu_device_supports_op(ggml_backend_dev_t dev, const
                                 op->type == GGML_TYPE_Q4_0) &&
                                src0->type == GGML_TYPE_F32 && (src1->type == GGML_TYPE_I64 || src1->type == GGML_TYPE_I32));
                 break;
    -        case GGML_OP_GET_ROWS: {
    -            const size_t storage_alignment =
    -                ctx->webgpu_global_ctx->capabilities.limits.minStorageBufferOffsetAlignment;
    -            const size_t src_address_unit =
    -                src0->type == GGML_TYPE_F32 && op->ne[0] % 4 == 0 ? 4 * sizeof(float) : ggml_type_size(src0->type);
    -            if (ggml_webgpu_tensor_misalignment(src0, storage_alignment) % src_address_unit != 0) {
    +        case GGML_OP_GET_ROWS:
    +            {
    +                const size_t storage_alignment =
    +                    ctx->webgpu_global_ctx->capabilities.limits.minStorageBufferOffsetAlignment;
    +                const size_t src_address_unit =
    +                    src0->type == GGML_TYPE_F32 && op->ne[0] % 4 == 0 ? 4 * sizeof(float) : ggml_type_size(src0->type);
    +                if (ggml_webgpu_tensor_misalignment(src0, storage_alignment) % src_address_unit != 0) {
    +                    break;
    +                }
    +                if (src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16 ||
    +                    ggml_webgpu_supported_qtype(src0->type)) {
    +                    supports_op = (op->type == GGML_TYPE_F32);
    +                } else if (src0->type == GGML_TYPE_I32) {
    +                    supports_op = op->type == GGML_TYPE_I32;
    +                }
                     break;
                 }
    -            if (src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16 || ggml_webgpu_supported_qtype(src0->type)) {
    -                supports_op = (op->type == GGML_TYPE_F32);
    -            } else if (src0->type == GGML_TYPE_I32) {
    -                supports_op = op->type == GGML_TYPE_I32;
    -            }
    -            break;
    -        }
             case GGML_OP_MUL_MAT:
                 {
                     switch (src1->type) {
    
    From ccc3646c63127e32ab8e2773290a1aded6dfb69c Mon Sep 17 00:00:00 2001
    From: Zhaolun Yin <129580161+ZhaolunYin@users.noreply.github.com>
    Date: Mon, 7 Sep 2026 14:59:45 +0100
    Subject: [PATCH 30/31] nix : update deprecated expressions (#28145)
    
    * fixed warnings
    
    * fixed nixfmt warning
    ---
     .devops/nix/package.nix | 12 ++++++------
     flake.nix               |  4 ++--
     2 files changed, 8 insertions(+), 8 deletions(-)
    
    diff --git a/.devops/nix/package.nix b/.devops/nix/package.nix
    index 86d9d589d..e807b4d71 100644
    --- a/.devops/nix/package.nix
    +++ b/.devops/nix/package.nix
    @@ -31,7 +31,7 @@
         ]
         && blas.meta.available,
       useCuda ? config.cudaSupport,
    -  useMetalKit ? stdenv.isAarch64 && stdenv.isDarwin,
    +  useMetalKit ? stdenv.hostPlatform.isAarch64 && stdenv.hostPlatform.isDarwin,
       # Increases the runtime closure size by ~700M
       useMpi ? false,
       useRocm ? config.rocmSupport,
    @@ -92,7 +92,7 @@ let
     
       cudaBuildInputs = with cudaPackages; [
         cuda_cudart
    -    cuda_cccl # 
    +    cccl # 
         libcublas
       ];
     
    @@ -166,7 +166,7 @@ effectiveStdenv.mkDerivation (finalAttrs: {
       # `xcrun` is used find the path of the Metal compiler, which is varible
       # and not on $PATH
       # see https://github.com/ggml-org/llama.cpp/pull/6118 for discussion
    -  __noChroot = effectiveStdenv.isDarwin && useMetalKit && precompileMetalShaders;
    +  __noChroot = effectiveStdenv.hostPlatform.isDarwin && useMetalKit && precompileMetalShaders;
     
       nativeBuildInputs =
         [
    @@ -181,10 +181,10 @@ effectiveStdenv.mkDerivation (finalAttrs: {
           autoAddDriverRunpath
         ]
         ++ optionals (effectiveStdenv.hostPlatform.isGnu && enableStatic) [ glibc.static ]
    -    ++ optionals (effectiveStdenv.isDarwin && useMetalKit && precompileMetalShaders) [ xcrunHost ];
    +    ++ optionals (effectiveStdenv.hostPlatform.isDarwin && useMetalKit && precompileMetalShaders) [ xcrunHost ];
     
       buildInputs =
    -    optionals effectiveStdenv.isDarwin darwinBuildInputs
    +    optionals effectiveStdenv.hostPlatform.isDarwin darwinBuildInputs
         ++ optionals useCuda cudaBuildInputs
         ++ optionals useMpi [ mpi ]
         ++ optionals useRocm rocmBuildInputs
    @@ -245,7 +245,7 @@ effectiveStdenv.mkDerivation (finalAttrs: {
     
         # Configurations that are known to result in build failures. Can be
         # overridden by importing Nixpkgs with `allowBroken = true`.
    -    broken = (useMetalKit && !effectiveStdenv.isDarwin);
    +    broken = (useMetalKit && !effectiveStdenv.hostPlatform.isDarwin);
     
         description = "Inference of LLaMA model in pure C/C++${descriptionSuffix}";
         homepage = "https://github.com/ggml-org/llama.cpp/";
    diff --git a/flake.nix b/flake.nix
    index bb02c8e52..6373d3b0b 100644
    --- a/flake.nix
    +++ b/flake.nix
    @@ -128,7 +128,7 @@
               }:
               {
                 # For standardised reproducible formatting with `nix fmt`
    -            formatter = pkgs.nixfmt-rfc-style;
    +            formatter = pkgs.nixfmt;
     
                 # Unlike `.#packages`, legacyPackages may contain values of
                 # arbitrary types (including nested attrsets) and may even throw
    @@ -156,7 +156,7 @@
                     windows = config.legacyPackages.llamaPackagesWindows.llama-cpp;
                     python-scripts = config.legacyPackages.llamaPackages.python-scripts;
                   }
    -              // lib.optionalAttrs pkgs.stdenv.isLinux {
    +              // lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
                     cuda = config.legacyPackages.llamaPackagesCuda.llama-cpp;
     
                     mpi-cpu = config.packages.default.override { useMpi = true; };
    
    From e71b80510c848c00175924ecf3c40333ccae8eb5 Mon Sep 17 00:00:00 2001
    From: "Piotr Wilkin (ilintar)" 
    Date: Mon, 7 Sep 2026 16:28:19 +0200
    Subject: [PATCH 31/31] Revert "CUDA: size routed MoE MMQ N-tiles from typical
     expert width on RDNA3 (#24546)" (#28551)
    
    This reverts commit 0c963452ea7d19f872e455257509a4ff00e7dfc7.
    
    Assisted-by: Claude Fable 5.1
    Claude-Session: https://claude.ai/code/session_01Q7rfnkjzgnfhvJsdeXhdoH
    ---
     ggml/src/ggml-cuda/mmq-config-ampere.cuh      |  3 +--
     ggml/src/ggml-cuda/mmq-config-blackwell.cuh   |  1 -
     ggml/src/ggml-cuda/mmq-config-cdna.cuh        |  3 +--
     ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh |  3 +--
     .../src/ggml-cuda/mmq-config-pascal-older.cuh |  3 +--
     ggml/src/ggml-cuda/mmq-config-rdna2.cuh       |  3 +--
     ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh     |  3 +--
     ggml/src/ggml-cuda/mmq-config-rdna3.cuh       |  3 +--
     ggml/src/ggml-cuda/mmq-config-rdna4.cuh       |  3 +--
     ggml/src/ggml-cuda/mmq.cuh                    | 23 ++++---------------
     10 files changed, 12 insertions(+), 36 deletions(-)
    
    diff --git a/ggml/src/ggml-cuda/mmq-config-ampere.cuh b/ggml/src/ggml-cuda/mmq-config-ampere.cuh
    index 2c00aef2c..9f9fd1973 100644
    --- a/ggml/src/ggml-cuda/mmq-config-ampere.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-ampere.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_ampere(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = false;
         CASE(GGML_TYPE_Q1_0, 256, 1, 128,   8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
         CASE(GGML_TYPE_Q1_0, 256, 1, 128,  16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
         CASE(GGML_TYPE_Q1_0, 256, 1, 128,  32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
    @@ -380,5 +379,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         CASE(GGML_TYPE_NVFP4, 256, 1, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
         CASE(GGML_TYPE_NVFP4, 256, 1, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
     
    -    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
     }
    diff --git a/ggml/src/ggml-cuda/mmq-config-blackwell.cuh b/ggml/src/ggml-cuda/mmq-config-blackwell.cuh
    index 8f928e217..9fbe32b69 100644
    --- a/ggml/src/ggml-cuda/mmq-config-blackwell.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-blackwell.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_blackwell(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = false;
         CASE(GGML_TYPE_MXFP4, 256, 1, 128,   8, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
         CASE(GGML_TYPE_MXFP4, 256, 1, 128,  16, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
         CASE(GGML_TYPE_MXFP4, 256, 1, 128,  32, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
    diff --git a/ggml/src/ggml-cuda/mmq-config-cdna.cuh b/ggml/src/ggml-cuda/mmq-config-cdna.cuh
    index 1d51a773b..4a8d89f72 100644
    --- a/ggml/src/ggml-cuda/mmq-config-cdna.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-cdna.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_cdna(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = false;
         CASE(GGML_TYPE_Q1_0, 512, 1, 128,  16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
         CASE(GGML_TYPE_Q1_0, 512, 1, 128,  32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
         CASE(GGML_TYPE_Q1_0, 512, 1, 128,  64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
    @@ -182,5 +181,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         CASE(GGML_TYPE_NVFP4, 512, 1, 128,  48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
         CASE(GGML_TYPE_NVFP4, 512, 1, 128,  64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
     
    -    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
    +    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
     }
    diff --git a/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh b/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh
    index 557a04e18..83eb7c146 100644
    --- a/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-pascal-dp4a.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_pascal_dp4a(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = false;
         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, 64,  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);
    @@ -270,5 +269,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
    +    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-config-pascal-older.cuh b/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh
    index 751ce026d..2a8dc9e1a 100644
    --- a/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-pascal-older.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_pascal_older(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = false;
         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, 64,  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);
    @@ -270,5 +269,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
    +    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-config-rdna2.cuh b/ggml/src/ggml-cuda/mmq-config-rdna2.cuh
    index c1efef56f..8324d9e1a 100644
    --- a/ggml/src/ggml-cuda/mmq-config-rdna2.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-rdna2.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna2(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = false;
         CASE(GGML_TYPE_Q1_0, 256, 2, 128,   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, 128,  32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
    @@ -270,5 +269,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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);
     
    -    return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
    +    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-config-rdna3-5.cuh b/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh
    index 10fdad663..180b2d937 100644
    --- a/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-rdna3-5.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna3_5(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = false;
         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);
    @@ -287,5 +286,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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, use_typical_moe_ncols, false, true);
    +    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-config-rdna3.cuh b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh
    index ba569337b..3a3ef7bd9 100644
    --- a/ggml/src/ggml-cuda/mmq-config-rdna3.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-rdna3.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna3(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = true;
         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, 128, 2,  64,  64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
    @@ -271,5 +270,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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, 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, use_typical_moe_ncols, false, true);
    +    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-config-rdna4.cuh b/ggml/src/ggml-cuda/mmq-config-rdna4.cuh
    index 6cce1d7e8..9293d9d55 100644
    --- a/ggml/src/ggml-cuda/mmq-config-rdna4.cuh
    +++ b/ggml/src/ggml-cuda/mmq-config-rdna4.cuh
    @@ -1,5 +1,4 @@
     static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna4(ggml_type type, int J, bool fallback) {
    -    constexpr bool use_typical_moe_ncols = true;
         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, 128, 2,  64,  64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
    @@ -287,5 +286,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
         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, use_typical_moe_ncols, false, true);
    +    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.cuh b/ggml/src/ggml-cuda/mmq.cuh
    index b28b960cd..b4a747720 100644
    --- a/ggml/src/ggml-cuda/mmq.cuh
    +++ b/ggml/src/ggml-cuda/mmq.cuh
    @@ -170,13 +170,12 @@ struct ggml_cuda_mmq_config {
         int                       J;           // SRAM tile width in src1->ne[1]/dst->ne[1] direction.
         ggml_cuda_mmq_sram_layout sram_layout; // SRAM tile length in src0->ne[0]/src1->ne[0] direction (physical 32 bit elements).
         int                       K_vram;      // VRAM tile length in src0->ne[0]/src1->ne[0] direction (logical elements).
    -    bool                      use_typical_moe_ncols;
         bool                      stream_k;    // Whether or not to use stream-k decomposition.
         bool                      fallback;    // Whether a fallback for out-of-bounds check in src0->ne[1] direction is needed.
     
         constexpr __host__ __device__ ggml_cuda_mmq_config(
    -            ggml_type type, int nthreads, int occupancy, int I, int J, ggml_cuda_mmq_sram_layout sram_layout, int K_vram, bool use_typical_moe_ncols, bool stream_k, bool fallback) :
    -        type(type), nthreads(nthreads), occupancy(occupancy), I(I), J(J), sram_layout(sram_layout), K_vram(K_vram), use_typical_moe_ncols(use_typical_moe_ncols), stream_k(stream_k), fallback(fallback) {}
    +            ggml_type type, int nthreads, int occupancy, int I, int J, ggml_cuda_mmq_sram_layout sram_layout, int K_vram, bool stream_k, bool fallback) :
    +        type(type), nthreads(nthreads), occupancy(occupancy), I(I), J(J), sram_layout(sram_layout), K_vram(K_vram), stream_k(stream_k), fallback(fallback) {}
     
         constexpr __device__ int rows_per_warp() const {
     #if defined(AMD_MFMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE)
    @@ -211,7 +210,7 @@ struct ggml_cuda_mmq_config {
             static_assert((I_)        %  32 == 0,                             "bad I");                                                       \
             static_assert((J_)        %   8 == 0,                             "bad J");                                                       \
             static_assert((K_vram_)   % 256 == 0,                             "bad K_vram");                                                  \
    -        return ggml_cuda_mmq_config((type_), (nthreads_), (occupancy_), (I_), (J_), (sram_layout_), (K_vram_), use_typical_moe_ncols, (stream_k_), (fallback_)); \
    +        return ggml_cuda_mmq_config((type_), (nthreads_), (occupancy_), (I_), (J_), (sram_layout_), (K_vram_), (stream_k_), (fallback_)); \
         }                                                                                                                                     \
     
     #include "mmq-config-pascal-older.cuh"
    @@ -1474,20 +1473,6 @@ void mul_mat_q_switch_J(ggml_backend_cuda_context & ctx, const mmq_args & args,
         const int    cc    = ggml_cuda_info().devices[id].cc;
         const size_t smpbo = ggml_cuda_info().devices[id].smpbo;
     
    -    int64_t ncols_picker = args.ncols_max;
    -    if (args.expert_bounds != nullptr && args.nchannels_x > 0) {
    -        const int J_max = ggml_cuda_mmq_get_J_max(type, fallback, cc, 128);
    -        const ggml_cuda_mmq_config config_max = ggml_cuda_mmq_get_config(type, J_max, fallback, cc);
    -        if (config_max.use_typical_moe_ncols) {
    -            // Use the typical expert width only for tile selection.
    -            // The launch grid still uses args.ncols_max.
    -            const int64_t ncols_typical = (args.ncols_dst + args.nchannels_x - 1) / args.nchannels_x;
    -            if (ncols_typical >= 1 && ncols_typical < J_max && ncols_typical < ncols_picker) {
    -                ncols_picker = ncols_typical;
    -            }
    -        }
    -    }
    -
         int J_best        = 0;
         int ntiles_J_best = INT_MAX;
     
    @@ -1501,7 +1486,7 @@ void mul_mat_q_switch_J(ggml_backend_cuda_context & ctx, const mmq_args & args,
                 continue;
             }
     
    -        const int ntiles_x = (ncols_picker + config.J - 1) / config.J;
    +        const int ntiles_x = (args.ncols_max + config.J - 1) / config.J;
     
             if (ntiles_x < ntiles_J_best) {
                 J_best = J;