diff --git a/common/arg.cpp b/common/arg.cpp index e3be4df6c..4c837dfff 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -3497,7 +3497,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex throw std::invalid_argument("unknown speculative decoding type without draft model"); } } - ).set_examples({LLAMA_EXAMPLE_SERVER})); + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_SPEC_TYPE")); add_opt(common_arg( {"--spec-ngram-size-n"}, "N", string_format("ngram size N for ngram-simple/ngram-map speculative decoding, length of lookup n-gram (default: %d)", params.speculative.ngram_size_n), diff --git a/common/chat.cpp b/common/chat.cpp index 9acbde7c0..0900d854c 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -951,7 +951,9 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp for (auto msg : inputs.messages) { if (msg.contains("reasoning_content") && msg.at("reasoning_content").is_string()) { msg["thinking"] = msg.at("reasoning_content"); - msg.erase("content"); + if (msg.contains("tool_calls") && msg.at("tool_calls").is_array() && !msg.at("tool_calls").empty()) { + msg.erase("content"); + } } adjusted_messages.push_back(msg); } diff --git a/convert_hf_to_gguf.py b/convert_hf_to_gguf.py index 46469c862..8cfd0bf2f 100755 --- a/convert_hf_to_gguf.py +++ b/convert_hf_to_gguf.py @@ -1062,6 +1062,10 @@ class TextModel(ModelBase): self.gguf_writer.add_head_count_kv(n_head_kv) logger.info(f"gguf: key-value head count = {n_head_kv}") + if self.hparams.get("is_causal") is False: + self.gguf_writer.add_causal_attention(False) + logger.info("gguf: causal attention = False") + # TODO: Handle "sliding_attention" similarly when models start implementing it rope_params = self.rope_parameters.get("full_attention", self.rope_parameters) if (rope_type := rope_params.get("rope_type")) is not None: diff --git a/ggml/src/ggml-sycl/upscale.cpp b/ggml/src/ggml-sycl/upscale.cpp deleted file mode 100644 index 18c743de4..000000000 --- a/ggml/src/ggml-sycl/upscale.cpp +++ /dev/null @@ -1,410 +0,0 @@ -#include "upscale.hpp" - -static void upscale_f32(const float * x, float * dst, - const int nb00, const int nb01, const int nb02, const int nb03, - const int ne10, const int ne11, const int ne12, const int ne13, - const float sf0, const float sf1, const float sf2, const float sf3) { - auto item_ct1 = sycl::ext::oneapi::this_work_item::get_nd_item<3>(); - int index = item_ct1.get_local_id(2) + item_ct1.get_group(2) * item_ct1.get_local_range(2); - if (index >= ne10 * ne11 * ne12 * ne13) { - return; - } - - int i10 = index % ne10; - int i11 = (index / ne10) % ne11; - int i12 = (index / (ne10 * ne11)) % ne12; - int i13 = (index / (ne10 * ne11 * ne12)) % ne13; - - int i00 = i10 / sf0; - int i01 = i11 / sf1; - int i02 = i12 / sf2; - int i03 = i13 / sf3; - - dst[index] = *((const float*)((const char*)x + i03 * nb03 + i02 * nb02 + - i01 * nb01 + i00 * nb00)); -} - -static void upscale_f32_bilinear(const float * x, float * dst, - const int nb00, const int nb01, const int nb02, const int nb03, - const int ne00_src, const int ne01_src, - const int ne10_dst, const int ne11_dst, const int ne12_dst, const int ne13_dst, - const float sf0, const float sf1, const float sf2, const float sf3, - const float pixel_offset) { - auto item_ct1 = sycl::ext::oneapi::this_work_item::get_nd_item<3>(); - const int64_t index = item_ct1.get_local_id(2) + - item_ct1.get_group(2) * item_ct1.get_local_range(2); - const int64_t dst_total_elements = ne10_dst * ne11_dst * ne12_dst * ne13_dst; - - if (index >= dst_total_elements) { - return; - } - - const int i10_dst = index % ne10_dst; - const int i11_dst = (index / ne10_dst) % ne11_dst; - const int i12_dst = (index / (ne10_dst * ne11_dst)) % ne12_dst; - const int i13_dst = index / (ne10_dst * ne11_dst * ne12_dst); - - const int i02_src = (int)(i12_dst / sf2); - const int i03_src = (int)(i13_dst / sf3); - - const float y_src_f = ((float)i11_dst + pixel_offset) / sf1 - pixel_offset; - int y0_src = (int) sycl::floor((float) y_src_f); - int y1_src = y0_src + 1; - - y0_src = sycl::max(0, sycl::min(y0_src, ne01_src - 1)); - y1_src = sycl::max(0, sycl::min(y1_src, ne01_src - 1)); - - float dy = y_src_f - (float)y0_src; - dy = sycl::max(0.0f, sycl::min(dy, 1.0f)); - - float x_src_f = ((float)i10_dst + pixel_offset) / sf0 - pixel_offset; - int x0_src = (int) sycl::floor(x_src_f); - int x1_src = x0_src + 1; - - x0_src = sycl::max(0, sycl::min(x0_src, ne00_src - 1)); - x1_src = sycl::max(0, sycl::min(x1_src, ne00_src - 1)); - - float dx = x_src_f - (float)x0_src; - dx = sycl::max(0.0f, sycl::min(dx, 1.0f)); - - const float* p_a = - (const float*)((const char*)x + (int64_t)x0_src * nb00 + - (int64_t)y0_src * nb01 + (int64_t)i02_src * nb02 + - (int64_t)i03_src * nb03); - const float* p_b = - (const float*)((const char*)x + (int64_t)x1_src * nb00 + - (int64_t)y0_src * nb01 + (int64_t)i02_src * nb02 + - (int64_t)i03_src * nb03); - const float* p_c = - (const float*)((const char*)x + (int64_t)x0_src * nb00 + - (int64_t)y1_src * nb01 + (int64_t)i02_src * nb02 + - (int64_t)i03_src * nb03); - const float* p_d = - (const float*)((const char*)x + (int64_t)x1_src * nb00 + - (int64_t)y1_src * nb01 + (int64_t)i02_src * nb02 + - (int64_t)i03_src * nb03); - - const float val_a = *p_a; - const float val_b = *p_b; - const float val_c = *p_c; - const float val_d = *p_d; - - float result = val_a * (1.0f - dx) * (1.0f - dy) + - val_b * dx * (1.0f - dy) + - val_c * (1.0f - dx) * dy + - val_d * dx * dy; - - dst[index] = result; -} - -// Similar to F.interpolate(..., mode="bilinear", align_corners=False, antialias=True) -// https://github.com/pytorch/pytorch/blob/8871ff29b743948d1225389d5b7068f37b22750b/aten/src/ATen/native/cpu/UpSampleKernel.cpp -static void upscale_f32_bilinear_antialias(const float * src0, - float * dst, - const int nb00, - const int nb01, - const int nb02, - const int nb03, - const int ne00_src, - const int ne01_src, - const int ne10_dst, - const int ne11_dst, - const int ne12_dst, - const int ne13_dst, - const float sf0, - const float sf1, - const float sf2, - const float sf3, - const float pixel_offset) { - auto item_ct1 = sycl::ext::oneapi::this_work_item::get_nd_item<3>(); - const int64_t index = item_ct1.get_local_id(2) + - item_ct1.get_group(2) * item_ct1.get_local_range(2); - const int64_t dst_total_elements = ne10_dst * ne11_dst * ne12_dst * ne13_dst; - - if (index >= dst_total_elements) { - return; - } - - const int i10_dst = index % ne10_dst; - const int i11_dst = (index / ne10_dst) % ne11_dst; - const int i12_dst = (index / (ne10_dst * ne11_dst)) % ne12_dst; - const int i13_dst = index / (ne10_dst * ne11_dst * ne12_dst); - - const int i02_src = (int)(i12_dst / sf2); - const int i03_src = (int)(i13_dst / sf3); - - const float y = ((float)i11_dst + pixel_offset) / sf1; - const float x = ((float)i10_dst + pixel_offset) / sf0; - - // support and invscale, minimum 1 pixel for bilinear - const float support1 = sycl::max(1.0f / sf1, 1.0f); - const float invscale1 = 1.0f / support1; - const float support0 = sycl::max(1.0f / sf0, 1.0f); - const float invscale0 = 1.0f / support0; - - // the range of source pixels that contribute - const int64_t x_min = sycl::max(int64_t(0), int64_t(x - support0 + pixel_offset)); - const int64_t x_max = sycl::min(int64_t(ne00_src), int64_t(x + support0 + pixel_offset)); - const int64_t y_min = sycl::max(int64_t(0), int64_t(y - support1 + pixel_offset)); - const int64_t y_max = sycl::min(int64_t(ne01_src), int64_t(y + support1 + pixel_offset)); - - // bilinear filter with antialiasing - float val = 0.0f; - float total_weight = 0.0f; - - auto triangle_filter = [](float x) -> float { - return sycl::max(1.0f - sycl::fabs(x), 0.0f); - }; - - for (int64_t sy = y_min; sy < y_max; sy++) { - const float weight_y = triangle_filter((sy - y + pixel_offset) * invscale1); - - for (int64_t sx = x_min; sx < x_max; sx++) { - const float weight_x = triangle_filter((sx - x + pixel_offset) * invscale0); - const float weight = weight_x * weight_y; - - if (weight <= 0.0f) { - continue; - } - - const float pixel = - *(const float*)((const char*)src0 + sx * nb00 + sy * nb01 + - i02_src * nb02 + i03_src * nb03); - val += pixel * weight; - total_weight += weight; - } - } - - if (total_weight > 0.0f) { - val /= total_weight; - } - - dst[index] = val; -} - -namespace bicubic_interpolation { -static float weight1(float x, const float &a) { return ((a + 2) * x - (a + 3)) * x * x + 1; }; -static float weight2(float x, const float &a) { return ((a * x - 5 * a) * x + 8 * a) * x - 4 * a; }; - -static float bicubic(float p0, float p1, float p2, float p3, float x, float a) { - const float w0 = weight2(x + 1, a); - const float w1 = weight1(x + 0, a); - const float w2 = weight1(1 - x, a); - const float w3 = weight2(2 - x, a); - return p0 * w0 + p1 * w1 + p2 * w2 + p3 * w3; -}; - -} - -static void upscale_f32_bicubic(const float * x, float * dst, - const int nb00, const int nb01, const int nb02, const int nb03, - const int ne00_src, const int ne01_src, - const int ne10_dst, const int ne11_dst, const int ne12_dst, const int ne13_dst, - const float sf0, const float sf1, const float sf2, const float sf3, - const float pixel_offset) { - auto item_ct1 = sycl::ext::oneapi::this_work_item::get_nd_item<3>(); - const float a = -0.75f; - using bicubic_interpolation::bicubic; - - const int64_t index = item_ct1.get_local_id(2) + - item_ct1.get_group(2) * item_ct1.get_local_range(2); - const int64_t dst_total_elements = - ne10_dst * ne11_dst * ne12_dst * ne13_dst; - - if (index >= dst_total_elements) { - return; - } - - const int i10_dst = index % ne10_dst; - const int i11_dst = (index / ne10_dst) % ne11_dst; - const int i12_dst = (index / (ne10_dst * ne11_dst)) % ne12_dst; - const int i13_dst = index / (ne10_dst * ne11_dst * ne12_dst); - - const int i02_src = (int)(i12_dst / sf2); - const int i03_src = (int)(i13_dst / sf3); - - const float y_src_f = ((float)i11_dst + pixel_offset) / sf1 - pixel_offset; - const int y0_src = (int) sycl::floor((float) y_src_f); - const float dy = y_src_f - (float)y0_src; - - const float x_src_f = ((float)i10_dst + pixel_offset) / sf0 - pixel_offset; - const int x0_src = (int) sycl::floor((float) x_src_f); - const float dx = x_src_f - (float)x0_src; - - const char * x_base = (const char *)x + (int64_t)i02_src * nb02 + (int64_t)i03_src * nb03; - - auto load = [=](int x_off, int y_off) -> float { - int i00_src = sycl::max(0, sycl::min(x0_src + x_off, ne00_src - 1)); - int i01_src = sycl::max(0, sycl::min(y0_src + y_off, ne01_src - 1)); - return *(const float *)(x_base + (int64_t)i00_src * nb00 + (int64_t)i01_src * nb01); - }; - - const float result = bicubic( - bicubic(load(-1, -1), load(0, -1), load(1, -1), load(2, -1), dx, a), - bicubic(load(-1, 0), load(0, 0), load(1, 0), load(2, 0), dx, a), - bicubic(load(-1, 1), load(0, 1), load(1, 1), load(2, 1), dx, a), - bicubic(load(-1, 2), load(0, 2), load(1, 2), load(2, 2), dx, a), - dy, - a); - - dst[index] = result; -} - -static void upscale_f32_sycl(const float * x, - float * dst, - const int nb00, - const int nb01, - const int nb02, - const int nb03, - const int ne10, - const int ne11, - const int ne12, - const int ne13, - const float sf0, - const float sf1, - const float sf2, - const float sf3, - dpct::queue_ptr stream) { - const int64_t dst_size = ne10 * ne11 * ne12 * ne13; - const int64_t num_blocks = (dst_size + SYCL_UPSCALE_BLOCK_SIZE - 1) / SYCL_UPSCALE_BLOCK_SIZE; - - stream->parallel_for( - sycl::nd_range<3>( - sycl::range<3>(1, 1, num_blocks) * sycl::range<3>(1, 1, SYCL_UPSCALE_BLOCK_SIZE), - sycl::range<3>(1, 1, SYCL_UPSCALE_BLOCK_SIZE)), - [=](sycl::nd_item<3> item_ct1) { - upscale_f32(x, dst, nb00, nb01, nb02, nb03, ne10, ne11, ne12, ne13, sf0, sf1, sf2, sf3); - }); -} - -static void upscale_f32_bilinear_sycl(const float * x, - float * dst, - const int nb00, - const int nb01, - const int nb02, - const int nb03, - const int ne00_src, - const int ne01_src, - const int ne10_dst, - const int ne11_dst, - const int ne12_dst, - const int ne13_dst, - const float sf0, - const float sf1, - const float sf2, - const float sf3, - const float pixel_offset, - bool antialias, - dpct::queue_ptr stream) { - const int64_t dst_size = ne10_dst * ne11_dst * ne12_dst * ne13_dst; - const int64_t num_blocks = (dst_size + SYCL_UPSCALE_BLOCK_SIZE - 1) / SYCL_UPSCALE_BLOCK_SIZE; - - if (antialias) { - stream->parallel_for( - sycl::nd_range<3>( - sycl::range<3>(1, 1, num_blocks) * sycl::range<3>(1, 1, SYCL_UPSCALE_BLOCK_SIZE), - sycl::range<3>(1, 1, SYCL_UPSCALE_BLOCK_SIZE)), - [=](sycl::nd_item<3> item_ct1) { - upscale_f32_bilinear_antialias( - x, dst, nb00, nb01, nb02, nb03, ne00_src, ne01_src, ne10_dst, ne11_dst, - ne12_dst, ne13_dst, sf0, sf1, sf2, sf3, pixel_offset); - }); - } else { - stream->parallel_for( - sycl::nd_range<3>( - sycl::range<3>(1, 1, num_blocks) * sycl::range<3>(1, 1, SYCL_UPSCALE_BLOCK_SIZE), - sycl::range<3>(1, 1, SYCL_UPSCALE_BLOCK_SIZE)), - [=](sycl::nd_item<3> item_ct1) { - upscale_f32_bilinear( - x, dst, nb00, nb01, nb02, nb03, ne00_src, ne01_src, ne10_dst, ne11_dst, ne12_dst, - ne13_dst, sf0, sf1, sf2, sf3, pixel_offset); - }); - } -} - -static void upscale_f32_bicubic_sycl(const float * x, - float * dst, - const int nb00, - const int nb01, - const int nb02, - const int nb03, - const int ne00_src, - const int ne01_src, - const int ne10_dst, - const int ne11_dst, - const int ne12_dst, - const int ne13_dst, - const float sf0, - const float sf1, - const float sf2, - const float sf3, - const float pixel_offset, - dpct::queue_ptr stream) { - const int64_t dst_size = ne10_dst * ne11_dst * ne12_dst * ne13_dst; - const int64_t num_blocks = (dst_size + SYCL_UPSCALE_BLOCK_SIZE - 1) / SYCL_UPSCALE_BLOCK_SIZE; - - { - stream->submit([&](sycl::handler & cgh) { - cgh.parallel_for( - sycl::nd_range<3>( - sycl::range<3>(1, 1, num_blocks) * sycl::range<3>(1, 1, SYCL_UPSCALE_BLOCK_SIZE), - sycl::range<3>(1, 1, SYCL_UPSCALE_BLOCK_SIZE)), - [=](sycl::nd_item<3> item_ct1) { - upscale_f32_bicubic( - x, dst, nb00, nb01, nb02, nb03, ne00_src, ne01_src, ne10_dst, ne11_dst, - ne12_dst, ne13_dst, sf0, sf1, sf2, sf3, pixel_offset); - }); - }); - } -} - -void ggml_sycl_op_upscale(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { - const ggml_tensor * src0 = dst->src[0]; - const float * src0_d = (const float *)src0->data; - float * dst_d = (float *)dst->data; - dpct::queue_ptr stream = ctx.stream(); - - GGML_ASSERT(src0->type == GGML_TYPE_F32); - GGML_ASSERT( dst->type == GGML_TYPE_F32); - - const int mode_flags = dst->op_params[0]; - const ggml_scale_mode mode = (ggml_scale_mode)(mode_flags & 0xFF); - - float sf0 = (float)dst->ne[0]/src0->ne[0]; - float sf1 = (float)dst->ne[1]/src0->ne[1]; - float sf2 = (float)dst->ne[2]/src0->ne[2]; - const float sf3 = (float)dst->ne[3]/src0->ne[3]; - - float pixel_offset = 0.5f; - if (mode_flags & GGML_SCALE_FLAG_ALIGN_CORNERS) { - sf0 = dst->ne[0] > 1 && src0->ne[0] > 1 - ? (float)(dst->ne[0] - 1) / (src0->ne[0] - 1) - : sf0; - sf1 = dst->ne[1] > 1 && src0->ne[1] > 1 - ? (float)(dst->ne[1] - 1) / (src0->ne[1] - 1) - : sf1; - pixel_offset = 0.0f; - } - - if (mode == GGML_SCALE_MODE_NEAREST) { - upscale_f32_sycl( - src0_d, dst_d, src0->nb[0], src0->nb[1], src0->nb[2], src0->nb[3], - dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], sf0, sf1, sf2, sf3, stream); - } else if (mode == GGML_SCALE_MODE_BILINEAR) { - const bool antialias = (mode_flags & GGML_SCALE_FLAG_ANTIALIAS); - upscale_f32_bilinear_sycl( - src0_d, dst_d, src0->nb[0], src0->nb[1], src0->nb[2], src0->nb[3], - src0->ne[0], src0->ne[1], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - sf0, sf1, sf2, sf3, pixel_offset, antialias, stream); - } else if (mode == GGML_SCALE_MODE_BICUBIC) { - upscale_f32_bicubic_sycl( - src0_d, dst_d, src0->nb[0], src0->nb[1], src0->nb[2], src0->nb[3], - src0->ne[0], src0->ne[1], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - sf0, sf1, sf2, sf3, pixel_offset, stream); - } -} - -void ggml_sycl_upscale(ggml_backend_sycl_context & ctx, ggml_tensor * dst) { - scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/1); - ggml_sycl_op_upscale(ctx, dst); -} diff --git a/ggml/src/ggml-sycl/upscale.hpp b/ggml/src/ggml-sycl/upscale.hpp deleted file mode 100644 index c36c1bdc9..000000000 --- a/ggml/src/ggml-sycl/upscale.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include -#include "dpct/helper.hpp" -#include "common.hpp" - -#define SYCL_UPSCALE_BLOCK_SIZE 256 - -void ggml_sycl_upscale(ggml_backend_sycl_context & ctx, ggml_tensor * dst); 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 ce7f2d699..3f494eb4d 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl +++ b/ggml/src/ggml-vulkan/vulkan-shaders/mul_mm_funcs.glsl @@ -444,19 +444,20 @@ void load_a_to_shmem(const uint pos_a, const uint row, const uint col, const uin const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; const uint buf_idx = col * SHMEM_STRIDE + row * LOAD_VEC_A / 2; - const uint ib = idx / 128; // 2 values per idx - const uint ib32 = (idx % 128) / 16; // 0..7 - const uint iq = 16 * ib32 + 2 * (idx % 8); + const uint ib = idx / 64; // 4 values per idx + const uint ib32 = (idx % 64) / 8; // 0..7 + const uint iq = 4 * ib32 + (idx % 4); const uint sl = (data_a[ib].scales_l[ib32/2] >> (4 * (ib32 & 1))) & 0xF; const uint sh = ((data_a[ib].scales_h) >> (2 * ib32)) & 3; - const uint qshift = (idx & 8) >> 1; - u8vec2 qs = unpack8((uint(data_a_packed16[ib].qs[iq/2]) >> qshift) & 0x0F0F).xy; + const uint qshift = idx & 4; + u8vec4 qs = unpack8((uint(data_a_packed32[ib].qs[iq]) >> qshift) & 0x0F0F0F0F); const float d = float(data_a[ib].d); - const vec2 v = d * float(int(sl | (sh << 4)) - 32) * vec2(kvalues_iq4nl[qs.x], kvalues_iq4nl[qs.y]); + const vec4 v = d * float(int(sl | (sh << 4)) - 32) * vec4(kvalues_iq4nl[qs.x], kvalues_iq4nl[qs.y], kvalues_iq4nl[qs.z], kvalues_iq4nl[qs.w]); buf_a[buf_idx ] = FLOAT_TYPE_VEC2(v.xy); + buf_a[buf_idx + 1] = FLOAT_TYPE_VEC2(v.zw); #elif defined(DATA_A_IQ4_NL) const uint idx = pos_a + col * p.stride_a / LOAD_VEC_A + row; const uint buf_idx = col * SHMEM_STRIDE + row * LOAD_VEC_A / 4; 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 80db516a9..9c2ea2e59 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -571,7 +571,7 @@ void matmul_shaders(bool fp16, MatMulIdType matmul_id_type, bool coopmat, bool c std::string load_vec_quant = "2"; if ((tname == "q4_0") || (tname == "q4_1") || (tname == "q5_1") || (tname == "iq1_s") || (tname == "iq1_m") || (tname == "iq2_xxs") || (tname == "iq2_xs") || (tname == "iq2_s")) load_vec_quant = "8"; - else if ((tname == "q5_0") || (tname == "q8_0") || (tname == "q2_k") || (tname == "q4_k") || (tname == "q5_k") || (tname == "iq3_xxs") || (tname == "iq3_s") || (tname == "iq4_nl") || (tname == "mxfp4")) + else if ((tname == "q5_0") || (tname == "q8_0") || (tname == "q2_k") || (tname == "q4_k") || (tname == "q5_k") || (tname == "iq3_xxs") || (tname == "iq3_s") || (tname == "iq4_xs") || (tname == "iq4_nl") || (tname == "mxfp4")) load_vec_quant = "4"; if (tname == "bf16") { diff --git a/gguf-py/gguf/gguf_writer.py b/gguf-py/gguf/gguf_writer.py index 662dda3cf..57f9fd1a5 100644 --- a/gguf-py/gguf/gguf_writer.py +++ b/gguf-py/gguf/gguf_writer.py @@ -425,8 +425,7 @@ class GGUFWriter: fout = self.fout[file_id] # pop the first tensor info - # TODO: cleaner way to get the first key - first_tensor_name = [name for name, _ in zip(self.tensors[file_id].keys(), range(1))][0] + first_tensor_name = next(iter(self.tensors[file_id])) ti = self.tensors[file_id].pop(first_tensor_name) assert ti.nbytes == tensor.nbytes diff --git a/src/llama-vocab.cpp b/src/llama-vocab.cpp index 7e0f6d761..65b801734 100644 --- a/src/llama-vocab.cpp +++ b/src/llama-vocab.cpp @@ -2365,19 +2365,28 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) { throw std::runtime_error("cannot find tokenizer vocab in model file\n"); } + const uint32_t n_tokens = gguf_get_arr_n(ctx, token_idx); + const float * scores = nullptr; const int score_idx = gguf_find_key(ctx, kv(LLM_KV_TOKENIZER_SCORES).c_str()); if (score_idx != -1) { + const uint32_t n_scores = gguf_get_arr_n(ctx, score_idx); + if (n_scores < n_tokens) { + throw std::runtime_error("Index out of array bounds for scores (" + std::to_string(n_scores) + " < " + std::to_string(n_tokens) + ")\n"); + } scores = (const float * ) gguf_get_arr_data(ctx, score_idx); } const int * toktypes = nullptr; const int toktype_idx = gguf_find_key(ctx, kv(LLM_KV_TOKENIZER_TOKEN_TYPE).c_str()); if (toktype_idx != -1) { + const uint32_t n_toktypes = gguf_get_arr_n(ctx, toktype_idx); + if (n_toktypes < n_tokens) { + throw std::runtime_error("Index out of array bounds for toktypes (" + std::to_string(n_toktypes) + " < " + std::to_string(n_tokens) + ")\n"); + } toktypes = (const int * ) gguf_get_arr_data(ctx, toktype_idx); } - uint32_t n_tokens = gguf_get_arr_n(ctx, token_idx); id_to_token.resize(n_tokens); for (uint32_t i = 0; i < n_tokens; i++) { diff --git a/src/models/bitnet.cpp b/src/models/bitnet.cpp index ccf5bc8e8..9f41b7d82 100644 --- a/src/models/bitnet.cpp +++ b/src/models/bitnet.cpp @@ -121,6 +121,9 @@ llm_build_bitnet::llm_build_bitnet(const llama_model & model, const llm_graph_pa cur = ggml_add(ctx0, cur, ffn_inp); cb(cur, "l_out", il); + cur = build_cvec(cur, il); + cb(cur, "l_out", il); + // input for next layer inpL = cur; } diff --git a/src/models/chatglm.cpp b/src/models/chatglm.cpp index 5887ed22e..cd11581a5 100644 --- a/src/models/chatglm.cpp +++ b/src/models/chatglm.cpp @@ -111,8 +111,13 @@ llm_build_chatglm::llm_build_chatglm(const llama_model & model, const llm_graph_ } - inpL = ggml_add(ctx0, cur, ffn_inp); - cb(inpL, "l_out", il); + cur = ggml_add(ctx0, cur, ffn_inp); + + cur = build_cvec(cur, il); + cb(cur, "l_out", il); + + // input for next layer + inpL = cur; } cur = build_norm(inpL, diff --git a/src/models/cogvlm.cpp b/src/models/cogvlm.cpp index 2ef2b6e38..fa7a54ba1 100644 --- a/src/models/cogvlm.cpp +++ b/src/models/cogvlm.cpp @@ -86,6 +86,10 @@ llm_build_cogvlm::llm_build_cogvlm(const llama_model & model, const llm_graph_pa cur = ggml_add(ctx0, cur, ffn_inp); cb(cur, "ffn_out", il); + cur = build_cvec(cur, il); + cb(cur, "l_out", il); + + // input for next layer inpL = cur; } diff --git a/src/models/eurobert.cpp b/src/models/eurobert.cpp index e8628d165..4ca9af873 100644 --- a/src/models/eurobert.cpp +++ b/src/models/eurobert.cpp @@ -82,6 +82,7 @@ llm_build_eurobert::llm_build_eurobert(const llama_model & model, const llm_grap cur = ggml_add(ctx0, cur, ffn_inp); + // input for next layer inpL = cur; } cur = inpL; diff --git a/src/models/jais.cpp b/src/models/jais.cpp index 135bf288b..b28243901 100644 --- a/src/models/jais.cpp +++ b/src/models/jais.cpp @@ -66,8 +66,14 @@ llm_build_jais::llm_build_jais(const llama_model & model, const llm_graph_params LLM_FFN_SILU, LLM_FFN_PAR, il); cb(cur, "ffn_out", il); } - inpL = ggml_add(ctx0, cur, ffn_inp); - cb(inpL, "l_out", il); + + cur = ggml_add(ctx0, cur, ffn_inp); + + cur = build_cvec(cur, il); + cb(cur, "l_out", il); + + // input for next layer + inpL = cur; } cur = build_norm(inpL, model.output_norm, diff --git a/src/models/kimi-linear.cpp b/src/models/kimi-linear.cpp index 4d62f4e71..f189b7107 100644 --- a/src/models/kimi-linear.cpp +++ b/src/models/kimi-linear.cpp @@ -362,6 +362,7 @@ llm_build_kimi_linear::llm_build_kimi_linear(const llama_model & model, const ll cur = build_cvec(cur, il); cb(cur, "l_out", il); + // input for next layer inpL = cur; } cur = inpL; diff --git a/src/models/lfm2.cpp b/src/models/lfm2.cpp index dfa322166..925c3dc9b 100644 --- a/src/models/lfm2.cpp +++ b/src/models/lfm2.cpp @@ -177,6 +177,9 @@ llm_build_lfm2::llm_build_lfm2(const llama_model & model, const llm_graph_ cb(ffn_norm_out, "model.layers.{}.ffn_out", il); cur = ggml_add(ctx0, cur, ffn_out); + + cur = build_cvec(cur, il); + cb(cur, "l_out", il); } cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1); diff --git a/src/models/plamo2.cpp b/src/models/plamo2.cpp index f02acbc18..0bde0b3d8 100644 --- a/src/models/plamo2.cpp +++ b/src/models/plamo2.cpp @@ -71,6 +71,7 @@ llm_build_plamo2::llm_build_plamo2(const llama_model & model, const llm_graph_pa cur = ggml_add(ctx0, cur, residual); cb(cur, "ffn_residual", il); + // input for next layer inpL = cur; } diff --git a/src/models/plamo3.cpp b/src/models/plamo3.cpp index 32af6e046..7cb9da6e7 100644 --- a/src/models/plamo3.cpp +++ b/src/models/plamo3.cpp @@ -109,6 +109,8 @@ llm_build_plamo3::llm_build_plamo3(const llama_model & model, const llm_gr cur = build_cvec(cur, il); cb(cur, "l_out", il); + + // input for next layer inpL = cur; } diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp index d07579ee8..e0e48d2a4 100644 --- a/src/models/qwen35.cpp +++ b/src/models/qwen35.cpp @@ -64,6 +64,9 @@ llm_build_qwen35::llm_build_qwen35(const llama_model & model, const llm_graph_pa cur = ggml_add(ctx0, cur, ffn_residual); cb(cur, "post_ffn", il); + cur = build_cvec(cur, il); + cb(cur, "l_out", il); + // Input for next layer inpL = cur; } diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp index b38660c0b..15baea80b 100644 --- a/src/models/qwen35moe.cpp +++ b/src/models/qwen35moe.cpp @@ -64,6 +64,9 @@ llm_build_qwen35moe::llm_build_qwen35moe(const llama_model & model, const llm_gr cur = ggml_add(ctx0, cur, ffn_residual); cb(cur, "post_moe", il); + cur = build_cvec(cur, il); + cb(cur, "l_out", il); + // Input for next layer inpL = cur; } diff --git a/src/models/qwen3next.cpp b/src/models/qwen3next.cpp index bdc64f983..26c275593 100644 --- a/src/models/qwen3next.cpp +++ b/src/models/qwen3next.cpp @@ -56,6 +56,9 @@ llm_build_qwen3next::llm_build_qwen3next(const llama_model & model, const llm_gr cur = ggml_add(ctx0, cur, ffn_residual); cb(cur, "post_moe", il); + cur = build_cvec(cur, il); + cb(cur, "l_out", il); + // Input for next layer inpL = cur; } diff --git a/src/models/smallthinker.cpp b/src/models/smallthinker.cpp index e2155aace..0f7ef462b 100644 --- a/src/models/smallthinker.cpp +++ b/src/models/smallthinker.cpp @@ -101,6 +101,7 @@ llm_build_smallthinker::llm_build_smallthinker(const llama_model & model, cur = ffn_out; cur = ggml_add(ctx0, cur, ffn_inp); + cur = build_cvec(cur, il); cb(cur, "l_out", il); diff --git a/src/models/step35-iswa.cpp b/src/models/step35-iswa.cpp index 176209cd9..c80cb26c5 100644 --- a/src/models/step35-iswa.cpp +++ b/src/models/step35-iswa.cpp @@ -145,9 +145,11 @@ llm_build_step35_iswa::llm_build_step35_iswa(const llama_model & model, const ll cb(cur, "ffn_out", il); } cur = ggml_add(ctx0, cur, ffn_inp); + cur = build_cvec(cur, il); cb(cur, "l_out", il); + // input for next layer inpL = cur; } diff --git a/tools/mtmd/clip-graph.h b/tools/mtmd/clip-graph.h index 4c7f7504c..3604bf77e 100644 --- a/tools/mtmd/clip-graph.h +++ b/tools/mtmd/clip-graph.h @@ -41,6 +41,11 @@ struct clip_graph { virtual ~clip_graph() = default; virtual ggml_cgraph * build() = 0; + // wrapper around ggml_mul_mat, allow hooking (e.g. LoRA, clamping) depending on the model + // tensor w should be the weight matrix, and tensor x should be the input + virtual ggml_tensor * build_mm(ggml_tensor * w, ggml_tensor * x) const; + // TODO: build_mm(w, b, x) to support bias + // // utility functions // diff --git a/tools/mtmd/clip.cpp b/tools/mtmd/clip.cpp index 4f5f9e0e7..48307631d 100644 --- a/tools/mtmd/clip.cpp +++ b/tools/mtmd/clip.cpp @@ -305,6 +305,10 @@ clip_graph::clip_graph(clip_ctx * ctx, const clip_image_f32 & img) : gf = ggml_new_graph_custom(ctx0, ctx->max_nodes, false); } +ggml_tensor * clip_graph::build_mm(ggml_tensor * w, ggml_tensor * x) const { + return ggml_mul_mat(ctx0, w, x); +} + void clip_graph::cb(ggml_tensor * cur, const char * name, int il) const { if (il >= 0) { ggml_format_name(cur, "%s-%d", name, il); @@ -376,7 +380,7 @@ ggml_tensor * clip_graph::build_vit( ggml_tensor * Vcur = nullptr; if (layer.qkv_w != nullptr) { // fused qkv - cur = ggml_mul_mat(ctx0, layer.qkv_w, cur); + cur = build_mm(layer.qkv_w, cur); if (layer.qkv_b != nullptr) { cur = ggml_add(ctx0, cur, layer.qkv_b); } @@ -410,17 +414,17 @@ ggml_tensor * clip_graph::build_vit( } else { // separate q, k, v - Qcur = ggml_mul_mat(ctx0, layer.q_w, cur); + Qcur = build_mm(layer.q_w, cur); if (layer.q_b) { Qcur = ggml_add(ctx0, Qcur, layer.q_b); } - Kcur = ggml_mul_mat(ctx0, layer.k_w, cur); + Kcur = build_mm(layer.k_w, cur); if (layer.k_b) { Kcur = ggml_add(ctx0, Kcur, layer.k_b); } - Vcur = ggml_mul_mat(ctx0, layer.v_w, cur); + Vcur = build_mm(layer.v_w, cur); if (layer.v_b) { Vcur = ggml_add(ctx0, Vcur, layer.v_b); } @@ -567,7 +571,7 @@ ggml_tensor * clip_graph::build_ffn( ffn_op_type type_op, int il) const { - ggml_tensor * tmp = up ? ggml_mul_mat(ctx0, up, cur) : cur; + ggml_tensor * tmp = up ? build_mm(up, cur) : cur; cb(tmp, "ffn_up", il); if (up_b) { @@ -576,7 +580,7 @@ ggml_tensor * clip_graph::build_ffn( } if (gate) { - cur = ggml_mul_mat(ctx0, gate, cur); + cur = build_mm(gate, cur); cb(cur, "ffn_gate", il); if (gate_b) { @@ -630,7 +634,7 @@ ggml_tensor * clip_graph::build_ffn( } if (down) { - cur = ggml_mul_mat(ctx0, down, cur); + cur = build_mm(down, cur); } if (down_b) { @@ -696,7 +700,7 @@ ggml_tensor * clip_graph::build_attn( cb(cur, "kqv_out", il); if (wo) { - cur = ggml_mul_mat(ctx0, wo, cur); + cur = build_mm(wo, cur); } if (wo_b) { diff --git a/tools/mtmd/models/cogvlm.cpp b/tools/mtmd/models/cogvlm.cpp index d5b739c68..44bc88442 100644 --- a/tools/mtmd/models/cogvlm.cpp +++ b/tools/mtmd/models/cogvlm.cpp @@ -19,7 +19,7 @@ ggml_cgraph * clip_graph_cogvlm::build() { auto & layer = model.layers[il]; ggml_tensor * cur = inpL; - cur = ggml_mul_mat(ctx0, layer.qkv_w, cur); + cur = build_mm(layer.qkv_w, cur); cur = ggml_add(ctx0, cur, layer.qkv_b); @@ -67,7 +67,7 @@ ggml_cgraph * clip_graph_cogvlm::build() { ggml_row_size(inpL->type, n_embd), 0); // Multiply with mm_model_proj - cur = ggml_mul_mat(ctx0, model.mm_model_proj, cur); + cur = build_mm(model.mm_model_proj, cur); // Apply layernorm, weight, bias cur = build_norm(cur, model.mm_post_fc_norm_w, model.mm_post_fc_norm_b, NORM_TYPE_NORMAL, 1e-5, -1); @@ -76,16 +76,16 @@ ggml_cgraph * clip_graph_cogvlm::build() { cur = ggml_gelu_inplace(ctx0, cur); // Branch 1: multiply with mm_h_to_4h_w - ggml_tensor * h_to_4h = ggml_mul_mat(ctx0, model.mm_h_to_4h_w, cur); + ggml_tensor * h_to_4h = build_mm(model.mm_h_to_4h_w, cur); // Branch 2: multiply with mm_gate_w - ggml_tensor * gate = ggml_mul_mat(ctx0, model.mm_gate_w, cur); + ggml_tensor * gate = build_mm(model.mm_gate_w, cur); // Apply silu gate = ggml_swiglu_split(ctx0, gate, h_to_4h); // Apply mm_4h_to_h_w - cur = ggml_mul_mat(ctx0, model.mm_4h_to_h_w, gate); + cur = build_mm(model.mm_4h_to_h_w, gate); // Concatenate with boi and eoi cur = ggml_concat(ctx0, model.mm_boi, cur, 1); diff --git a/tools/mtmd/models/conformer.cpp b/tools/mtmd/models/conformer.cpp index 9b1fab487..f58c5048f 100644 --- a/tools/mtmd/models/conformer.cpp +++ b/tools/mtmd/models/conformer.cpp @@ -56,7 +56,7 @@ ggml_cgraph * clip_graph_conformer::build() { cur = ggml_reshape_2d(ctx0, cur, cur->ne[0] * cur->ne[1], cur->ne[2]); // calculate out - cur = ggml_mul_mat(ctx0, model.pre_encode_out_w, cur); + cur = build_mm(model.pre_encode_out_w, cur); cur = ggml_add(ctx0, cur, model.pre_encode_out_b); cb(cur, "conformer.pre_encode.out", -1); } @@ -87,7 +87,7 @@ ggml_cgraph * clip_graph_conformer::build() { cur = build_norm(residual, layer.ln_1_w, layer.ln_1_b, NORM_TYPE_NORMAL, 1e-5, il); cb(cur, "conformer.layers.{}.norm_self_att", il); - ggml_tensor * Qcur = ggml_mul_mat(ctx0, layer.q_w, cur); + ggml_tensor * Qcur = build_mm(layer.q_w, cur); Qcur = ggml_add(ctx0, Qcur, layer.q_b); Qcur = ggml_reshape_3d(ctx0, Qcur, d_head, n_head, Qcur->ne[1]); ggml_tensor * Q_bias_u = ggml_add(ctx0, Qcur, layer.pos_bias_u); @@ -96,12 +96,12 @@ ggml_cgraph * clip_graph_conformer::build() { Q_bias_v = ggml_permute(ctx0, Q_bias_v, 0, 2, 1, 3); // TODO @ngxson : some cont can/should be removed when ggml_mul_mat support these cases - ggml_tensor * Kcur = ggml_mul_mat(ctx0, layer.k_w, cur); + ggml_tensor * Kcur = build_mm(layer.k_w, cur); Kcur = ggml_add(ctx0, Kcur, layer.k_b); Kcur = ggml_reshape_3d(ctx0, Kcur, d_head, n_head, Kcur->ne[1]); Kcur = ggml_cont(ctx0, ggml_permute(ctx0, Kcur, 0, 2, 1, 3)); - ggml_tensor * Vcur = ggml_mul_mat(ctx0, layer.v_w, cur); + ggml_tensor * Vcur = build_mm(layer.v_w, cur); Vcur = ggml_add(ctx0, Vcur, layer.v_b); Vcur = ggml_reshape_3d(ctx0, Vcur, d_head, n_head, Vcur->ne[1]); Vcur = ggml_cont(ctx0, ggml_permute(ctx0, Vcur, 1, 2, 0, 3)); @@ -111,7 +111,7 @@ ggml_cgraph * clip_graph_conformer::build() { matrix_ac = ggml_cont(ctx0, ggml_permute(ctx0, matrix_ac, 1, 0, 2, 3)); cb(matrix_ac, "conformer.layers.{}.self_attn.id3", il); - auto * p = ggml_mul_mat(ctx0, layer.linear_pos_w, pos_emb); + auto * p = build_mm(layer.linear_pos_w, pos_emb); cb(p, "conformer.layers.{}.self_attn.linear_pos", il); p = ggml_reshape_3d(ctx0, p, d_head, n_head, p->ne[1]); p = ggml_permute(ctx0, p, 0, 2, 1, 3); @@ -143,7 +143,7 @@ ggml_cgraph * clip_graph_conformer::build() { x = ggml_permute(ctx0, x, 2, 0, 1, 3); x = ggml_cont_2d(ctx0, x, x->ne[0] * x->ne[1], x->ne[2]); - ggml_tensor * out = ggml_mul_mat(ctx0, layer.o_w, x); + ggml_tensor * out = build_mm(layer.o_w, x); out = ggml_add(ctx0, out, layer.o_b); cb(out, "conformer.layers.{}.self_attn.linear_out", il); @@ -157,7 +157,7 @@ ggml_cgraph * clip_graph_conformer::build() { // conv { auto * x = cur; - x = ggml_mul_mat(ctx0, layer.conv_pw1_w, x); + x = build_mm(layer.conv_pw1_w, x); x = ggml_add(ctx0, x, layer.conv_pw1_b); cb(x, "conformer.layers.{}.conv.pointwise_conv1", il); @@ -181,7 +181,7 @@ ggml_cgraph * clip_graph_conformer::build() { x = ggml_silu(ctx0, x); // pointwise_conv2 - x = ggml_mul_mat(ctx0, layer.conv_pw2_w, x); + x = build_mm(layer.conv_pw2_w, x); x = ggml_add(ctx0, x, layer.conv_pw2_b); cur = x; diff --git a/tools/mtmd/models/glm4v.cpp b/tools/mtmd/models/glm4v.cpp index 6f52df41a..9dbb162c5 100644 --- a/tools/mtmd/models/glm4v.cpp +++ b/tools/mtmd/models/glm4v.cpp @@ -97,7 +97,7 @@ ggml_cgraph * clip_graph_glm4v::build() { // FC projector { - cur = ggml_mul_mat(ctx0, model.projection, cur); + cur = build_mm(model.projection, cur); // default LayerNorm (post_projection_norm) cur = build_norm(cur, model.mm_post_norm_w, model.mm_post_norm_b, NORM_TYPE_NORMAL, 1e-5, -1); cur = ggml_gelu_erf(ctx0, cur); diff --git a/tools/mtmd/models/llama4.cpp b/tools/mtmd/models/llama4.cpp index 30d1df5bc..01af54bba 100644 --- a/tools/mtmd/models/llama4.cpp +++ b/tools/mtmd/models/llama4.cpp @@ -22,7 +22,7 @@ ggml_cgraph * clip_graph_llama4::build() { ggml_tensor * kernel = ggml_reshape_4d(ctx0, model.patch_embeddings_0, patch_size, patch_size, 3, n_embd); inp = ggml_im2col(ctx0, kernel, inp, patch_size, patch_size, 0, 0, 1, 1, true, inp->type); - inp = ggml_mul_mat(ctx0, model.patch_embeddings_0, inp); + inp = build_mm(model.patch_embeddings_0, inp); inp = ggml_reshape_2d(ctx0, inp, n_embd, n_patches); cb(inp, "patch_conv", -1); } @@ -78,15 +78,15 @@ ggml_cgraph * clip_graph_llama4::build() { // based on Llama4VisionMLP2 (always uses GELU activation, no bias) { - cur = ggml_mul_mat(ctx0, model.mm_model_mlp_1_w, cur); + cur = build_mm(model.mm_model_mlp_1_w, cur); cur = ggml_gelu(ctx0, cur); - cur = ggml_mul_mat(ctx0, model.mm_model_mlp_2_w, cur); + cur = build_mm(model.mm_model_mlp_2_w, cur); cur = ggml_gelu(ctx0, cur); cb(cur, "adapter_mlp", -1); } // Llama4MultiModalProjector - cur = ggml_mul_mat(ctx0, model.mm_model_proj, cur); + cur = build_mm(model.mm_model_proj, cur); cb(cur, "projected", -1); // build the graph diff --git a/tools/mtmd/models/llava.cpp b/tools/mtmd/models/llava.cpp index 0bfb5f05f..4af17ccfe 100644 --- a/tools/mtmd/models/llava.cpp +++ b/tools/mtmd/models/llava.cpp @@ -70,17 +70,17 @@ ggml_cgraph * clip_graph_llava::build() { // self-attention { - ggml_tensor * Qcur = ggml_mul_mat(ctx0, layer.q_w, cur); + ggml_tensor * Qcur = build_mm(layer.q_w, cur); if (layer.q_b) { Qcur = ggml_add(ctx0, Qcur, layer.q_b); } - ggml_tensor * Kcur = ggml_mul_mat(ctx0, layer.k_w, cur); + ggml_tensor * Kcur = build_mm(layer.k_w, cur); if (layer.k_b) { Kcur = ggml_add(ctx0, Kcur, layer.k_b); } - ggml_tensor * Vcur = ggml_mul_mat(ctx0, layer.v_w, cur); + ggml_tensor * Vcur = build_mm(layer.v_w, cur); if (layer.v_b) { Vcur = ggml_add(ctx0, Vcur, layer.v_b); } @@ -164,17 +164,17 @@ ggml_cgraph * clip_graph_llava::build() { // llava projector if (proj_type == PROJECTOR_TYPE_MLP) { - embeddings = ggml_mul_mat(ctx0, model.mm_0_w, embeddings); + embeddings = build_mm(model.mm_0_w, embeddings); embeddings = ggml_add(ctx0, embeddings, model.mm_0_b); embeddings = ggml_gelu(ctx0, embeddings); if (model.mm_2_w) { - embeddings = ggml_mul_mat(ctx0, model.mm_2_w, embeddings); + embeddings = build_mm(model.mm_2_w, embeddings); embeddings = ggml_add(ctx0, embeddings, model.mm_2_b); } } else if (proj_type == PROJECTOR_TYPE_MLP_NORM) { - embeddings = ggml_mul_mat(ctx0, model.mm_0_w, embeddings); + embeddings = build_mm(model.mm_0_w, embeddings); embeddings = ggml_add(ctx0, embeddings, model.mm_0_b); // ggml_tensor_printf(embeddings, "mm_0_w",0,true,false); // First LayerNorm @@ -186,7 +186,7 @@ ggml_cgraph * clip_graph_llava::build() { embeddings = ggml_gelu(ctx0, embeddings); // Second linear layer - embeddings = ggml_mul_mat(ctx0, model.mm_3_w, embeddings); + embeddings = build_mm(model.mm_3_w, embeddings); embeddings = ggml_add(ctx0, embeddings, model.mm_3_b); // Second LayerNorm @@ -197,10 +197,10 @@ ggml_cgraph * clip_graph_llava::build() { else if (proj_type == PROJECTOR_TYPE_LDP) { // MobileVLM projector int n_patch = 24; - ggml_tensor * mlp_1 = ggml_mul_mat(ctx0, model.mm_model_mlp_1_w, embeddings); + ggml_tensor * mlp_1 = build_mm(model.mm_model_mlp_1_w, embeddings); mlp_1 = ggml_add(ctx0, mlp_1, model.mm_model_mlp_1_b); mlp_1 = ggml_gelu(ctx0, mlp_1); - ggml_tensor * mlp_3 = ggml_mul_mat(ctx0, model.mm_model_mlp_3_w, mlp_1); + ggml_tensor * mlp_3 = build_mm(model.mm_model_mlp_3_w, mlp_1); mlp_3 = ggml_add(ctx0, mlp_3, model.mm_model_mlp_3_b); // mlp_3 shape = [1, 576, 2048], ne = [2048, 576, 1, 1] @@ -229,10 +229,10 @@ ggml_cgraph * clip_graph_llava::build() { // block_1 shape = [1, 2048, 1, 1], ne = [1, 1, 2048, 1] // pointwise conv block_1 = ggml_reshape_2d(ctx0, block_1, block_1->ne[0]*block_1->ne[1]*block_1->ne[2], block_1->ne[3]); - block_1 = ggml_mul_mat(ctx0, model.mm_model_block_1_block_1_fc1_w, block_1); + block_1 = build_mm(model.mm_model_block_1_block_1_fc1_w, block_1); block_1 = ggml_add(ctx0, block_1, model.mm_model_block_1_block_1_fc1_b); block_1 = ggml_relu(ctx0, block_1); - block_1 = ggml_mul_mat(ctx0, model.mm_model_block_1_block_1_fc2_w, block_1); + block_1 = build_mm(model.mm_model_block_1_block_1_fc2_w, block_1); block_1 = ggml_add(ctx0, block_1, model.mm_model_block_1_block_1_fc2_b); block_1 = ggml_hardsigmoid(ctx0, block_1); // block_1_hw shape = [1, 2048, 24, 24], ne = [24, 24, 2048, 1], block_1 shape = [1, 2048], ne = [2048, 1, 1, 1] @@ -244,7 +244,7 @@ ggml_cgraph * clip_graph_llava::build() { block_1 = ggml_cont(ctx0, ggml_permute(ctx0, block_1, 1, 0, 2, 3)); // block_1 shape = [1, 24*24, 2048], ne = [24*24, 2048, 1] - block_1 = ggml_mul_mat(ctx0, model.mm_model_block_1_block_2_0_w, block_1); + block_1 = build_mm(model.mm_model_block_1_block_2_0_w, block_1); block_1 = ggml_reshape_4d(ctx0, block_1, block_1->ne[0], w, h, block_1->ne[3]); // block_1 shape = [1, 24, 24, 2048], ne = [2048, 24, 24, 1] @@ -277,10 +277,10 @@ ggml_cgraph * clip_graph_llava::build() { // block_1 shape = [1, 2048, 1, 1], ne = [1, 1, 2048, 1] // pointwise conv block_1 = ggml_reshape_2d(ctx0, block_1, block_1->ne[0]*block_1->ne[1]*block_1->ne[2], block_1->ne[3]); - block_1 = ggml_mul_mat(ctx0, model.mm_model_block_2_block_1_fc1_w, block_1); + block_1 = build_mm(model.mm_model_block_2_block_1_fc1_w, block_1); block_1 = ggml_add(ctx0, block_1, model.mm_model_block_2_block_1_fc1_b); block_1 = ggml_relu(ctx0, block_1); - block_1 = ggml_mul_mat(ctx0, model.mm_model_block_2_block_1_fc2_w, block_1); + block_1 = build_mm(model.mm_model_block_2_block_1_fc2_w, block_1); block_1 = ggml_add(ctx0, block_1, model.mm_model_block_2_block_1_fc2_b); block_1 = ggml_hardsigmoid(ctx0, block_1); @@ -292,7 +292,7 @@ ggml_cgraph * clip_graph_llava::build() { block_1 = ggml_reshape_3d(ctx0, block_1, w*h, block_1->ne[2], block_1->ne[3]); block_1 = ggml_cont(ctx0, ggml_permute(ctx0, block_1, 1, 0, 2, 3)); // block_1 shape = [1, 24*24, 2048], ne = [24*24, 2048, 1] - block_1 = ggml_mul_mat(ctx0, model.mm_model_block_2_block_2_0_w, block_1); + block_1 = build_mm(model.mm_model_block_2_block_2_0_w, block_1); block_1 = ggml_reshape_4d(ctx0, block_1, block_1->ne[0], w, h, block_1->ne[3]); @@ -307,10 +307,10 @@ ggml_cgraph * clip_graph_llava::build() { else if (proj_type == PROJECTOR_TYPE_LDPV2) { int n_patch = 24; - ggml_tensor * mlp_0 = ggml_mul_mat(ctx0, model.mm_model_mlp_0_w, embeddings); + ggml_tensor * mlp_0 = build_mm(model.mm_model_mlp_0_w, embeddings); mlp_0 = ggml_add(ctx0, mlp_0, model.mm_model_mlp_0_b); mlp_0 = ggml_gelu(ctx0, mlp_0); - ggml_tensor * mlp_2 = ggml_mul_mat(ctx0, model.mm_model_mlp_2_w, mlp_0); + ggml_tensor * mlp_2 = build_mm(model.mm_model_mlp_2_w, mlp_0); mlp_2 = ggml_add(ctx0, mlp_2, model.mm_model_mlp_2_b); // mlp_2 ne = [2048, 576, 1, 1] // // AVG Pool Layer 2*2, strides = 2 @@ -344,15 +344,15 @@ ggml_cgraph * clip_graph_llava::build() { embeddings = ggml_add(ctx0, embeddings, model.mm_model_adapter_conv_b); // GLU { - embeddings = ggml_mul_mat(ctx0, model.mm_model_mlp_0_w, embeddings); + embeddings = build_mm(model.mm_model_mlp_0_w, embeddings); embeddings = ggml_norm(ctx0, embeddings, eps); embeddings = ggml_add(ctx0, ggml_mul(ctx0, embeddings, model.mm_model_ln_q_w), model.mm_model_ln_q_b); embeddings = ggml_gelu_inplace(ctx0, embeddings); ggml_tensor * x = embeddings; - embeddings = ggml_mul_mat(ctx0, model.mm_model_mlp_2_w, embeddings); - x = ggml_mul_mat(ctx0, model.mm_model_mlp_1_w,x); + embeddings = build_mm(model.mm_model_mlp_2_w, embeddings); + x = build_mm(model.mm_model_mlp_1_w,x); embeddings = ggml_swiglu_split(ctx0, embeddings, x); - embeddings = ggml_mul_mat(ctx0, model.mm_model_mlp_3_w, embeddings); + embeddings = build_mm(model.mm_model_mlp_3_w, embeddings); } // arrangement of BOI/EOI token embeddings // note: these embeddings are not present in text model, hence we cannot process them as text tokens diff --git a/tools/mtmd/models/minicpmv.cpp b/tools/mtmd/models/minicpmv.cpp index 3594ea29f..924117ab2 100644 --- a/tools/mtmd/models/minicpmv.cpp +++ b/tools/mtmd/models/minicpmv.cpp @@ -38,7 +38,7 @@ ggml_cgraph * clip_graph_minicpmv::build() { // resampler projector (it is just another transformer) ggml_tensor * q = model.mm_model_query; - ggml_tensor * v = ggml_mul_mat(ctx0, model.mm_model_kv_proj, embeddings); + ggml_tensor * v = build_mm(model.mm_model_kv_proj, embeddings); // norm q = build_norm(q, model.mm_model_ln_q_w, model.mm_model_ln_q_b, NORM_TYPE_NORMAL, eps, -1); @@ -77,13 +77,13 @@ ggml_cgraph * clip_graph_minicpmv::build() { // Use actual config value if available, otherwise fall back to hardcoded values int num_query = hparams.minicpmv_query_num; ggml_tensor * Q = ggml_add(ctx0, - ggml_mul_mat(ctx0, model.mm_model_attn_q_w, q), + build_mm(model.mm_model_attn_q_w, q), model.mm_model_attn_q_b); ggml_tensor * K = ggml_add(ctx0, - ggml_mul_mat(ctx0, model.mm_model_attn_k_w, k), + build_mm(model.mm_model_attn_k_w, k), model.mm_model_attn_k_b); ggml_tensor * V = ggml_add(ctx0, - ggml_mul_mat(ctx0, model.mm_model_attn_v_w, v), + build_mm(model.mm_model_attn_v_w, v), model.mm_model_attn_v_b); Q = ggml_reshape_3d(ctx0, Q, d_head, n_head, num_query); @@ -105,7 +105,7 @@ ggml_cgraph * clip_graph_minicpmv::build() { embeddings = build_norm(embeddings, model.mm_model_ln_post_w, model.mm_model_ln_post_b, NORM_TYPE_NORMAL, eps, -1); // projection - embeddings = ggml_mul_mat(ctx0, model.mm_model_proj, embeddings); + embeddings = build_mm(model.mm_model_proj, embeddings); // build the graph ggml_build_forward_expand(gf, embeddings); diff --git a/tools/mtmd/models/mobilenetv5.cpp b/tools/mtmd/models/mobilenetv5.cpp index 593afa1dd..1c42218d2 100644 --- a/tools/mtmd/models/mobilenetv5.cpp +++ b/tools/mtmd/models/mobilenetv5.cpp @@ -429,7 +429,7 @@ ggml_cgraph * clip_graph_mobilenetv5::build() { // PyTorch: embedding_projection = nn.Linear(vision_hidden, text_hidden, bias=False) // Weight stored as [out_features, in_features] = [text_hidden_size, vision_hidden_size] if (model.mm_input_proj_w) { - cur = ggml_mul_mat(ctx0, model.mm_input_proj_w, cur); + cur = build_mm(model.mm_input_proj_w, cur); } // 5. POST PROJECTION NORM diff --git a/tools/mtmd/models/pixtral.cpp b/tools/mtmd/models/pixtral.cpp index a849210b5..d6d037b69 100644 --- a/tools/mtmd/models/pixtral.cpp +++ b/tools/mtmd/models/pixtral.cpp @@ -43,7 +43,7 @@ ggml_cgraph * clip_graph_pixtral::build() { // project to n_embd cur = ggml_reshape_2d(ctx0, cur, cur->ne[0], cur->ne[1] * cur->ne[2]); - cur = ggml_mul_mat(ctx0, model.mm_patch_merger_w, cur); + cur = build_mm(model.mm_patch_merger_w, cur); } // LlavaMultiModalProjector (always using GELU activation) diff --git a/tools/mtmd/models/qwen2vl.cpp b/tools/mtmd/models/qwen2vl.cpp index 85f158bb1..ebf107573 100644 --- a/tools/mtmd/models/qwen2vl.cpp +++ b/tools/mtmd/models/qwen2vl.cpp @@ -90,11 +90,11 @@ ggml_cgraph * clip_graph_qwen2vl::build() { // self-attention { ggml_tensor * Qcur = ggml_add(ctx0, - ggml_mul_mat(ctx0, layer.q_w, cur), layer.q_b); + build_mm(layer.q_w, cur), layer.q_b); ggml_tensor * Kcur = ggml_add(ctx0, - ggml_mul_mat(ctx0, layer.k_w, cur), layer.k_b); + build_mm(layer.k_w, cur), layer.k_b); ggml_tensor * Vcur = ggml_add(ctx0, - ggml_mul_mat(ctx0, layer.v_w, cur), layer.v_b); + build_mm(layer.v_w, cur), layer.v_b); Qcur = ggml_reshape_3d(ctx0, Qcur, d_head, n_head, n_patches); Kcur = ggml_reshape_3d(ctx0, Kcur, d_head, n_head, n_patches); diff --git a/tools/mtmd/models/qwen3vl.cpp b/tools/mtmd/models/qwen3vl.cpp index 5ecb10fe4..fa1100dda 100644 --- a/tools/mtmd/models/qwen3vl.cpp +++ b/tools/mtmd/models/qwen3vl.cpp @@ -85,7 +85,7 @@ ggml_cgraph * clip_graph_qwen3vl::build() { // self-attention { - cur = ggml_mul_mat(ctx0, layer.qkv_w, cur); + cur = build_mm(layer.qkv_w, cur); cur = ggml_add(ctx0, cur, layer.qkv_b); ggml_tensor * Qcur = ggml_view_3d(ctx0, cur, d_head, n_head, n_pos, diff --git a/tools/mtmd/models/siglip.cpp b/tools/mtmd/models/siglip.cpp index 75f9b4db4..9dafa35ea 100644 --- a/tools/mtmd/models/siglip.cpp +++ b/tools/mtmd/models/siglip.cpp @@ -43,7 +43,7 @@ ggml_cgraph * clip_graph_siglip::build() { // https://github.com/huggingface/transformers/blob/0a950e0bbe1ed58d5401a6b547af19f15f0c195e/src/transformers/models/idefics3/modeling_idefics3.py#L578 const int scale_factor = model.hparams.n_merge; cur = build_patch_merge_permute(cur, scale_factor); - cur = ggml_mul_mat(ctx0, model.projection, cur); + cur = build_mm(model.projection, cur); } else if (proj_type == PROJECTOR_TYPE_LFM2) { // pixel unshuffle block diff --git a/tools/mtmd/models/whisper-enc.cpp b/tools/mtmd/models/whisper-enc.cpp index 2f2b12775..ed61bb05b 100644 --- a/tools/mtmd/models/whisper-enc.cpp +++ b/tools/mtmd/models/whisper-enc.cpp @@ -59,7 +59,7 @@ ggml_cgraph * clip_graph_whisper_enc::build() { cur = ggml_mul(ctx0, cur, model.mm_norm_pre_w); // ffn in - cur = ggml_mul_mat(ctx0, model.mm_1_w, cur); + cur = build_mm(model.mm_1_w, cur); // swiglu // see SwiGLU in ultravox_model.py, the second half passed through is silu, not the first half @@ -70,11 +70,11 @@ ggml_cgraph * clip_graph_whisper_enc::build() { cur = ggml_mul(ctx0, cur, model.mm_norm_mid_w); // ffn out - cur = ggml_mul_mat(ctx0, model.mm_2_w, cur); + cur = build_mm(model.mm_2_w, cur); } else if (proj_type == PROJECTOR_TYPE_QWEN2A) { // projector - cur = ggml_mul_mat(ctx0, model.mm_fc_w, cur); + cur = build_mm(model.mm_fc_w, cur); cur = ggml_add(ctx0, cur, model.mm_fc_b); } else if (proj_type == PROJECTOR_TYPE_VOXTRAL) { diff --git a/tools/mtmd/models/youtuvl.cpp b/tools/mtmd/models/youtuvl.cpp index ffbf2be55..cd8f6d446 100644 --- a/tools/mtmd/models/youtuvl.cpp +++ b/tools/mtmd/models/youtuvl.cpp @@ -43,7 +43,7 @@ ggml_cgraph * clip_graph_youtuvl::build() { ctx0, inp, 3*patch_size* patch_size, Hm * Wm * m * m, 1); } - inp = ggml_mul_mat(ctx0, model.patch_embeddings_0, inp); + inp = build_mm(model.patch_embeddings_0, inp); if (model.patch_bias) { inp = ggml_add(ctx0, inp, model.patch_bias); @@ -97,11 +97,11 @@ ggml_cgraph * clip_graph_youtuvl::build() { // self-attention { ggml_tensor * Qcur = ggml_add(ctx0, - ggml_mul_mat(ctx0, layer.q_w, cur), layer.q_b); + build_mm(layer.q_w, cur), layer.q_b); ggml_tensor * Kcur = ggml_add(ctx0, - ggml_mul_mat(ctx0, layer.k_w, cur), layer.k_b); + build_mm(layer.k_w, cur), layer.k_b); ggml_tensor * Vcur = ggml_add(ctx0, - ggml_mul_mat(ctx0, layer.v_w, cur), layer.v_b); + build_mm(layer.v_w, cur), layer.v_b); Qcur = ggml_reshape_3d(ctx0, Qcur, d_head, n_head, n_patches); Kcur = ggml_reshape_3d(ctx0, Kcur, d_head, n_head, n_patches); diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz index 07f7b7e42..f1ccf5a75 100644 Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte index b9015c196..42191be89 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte @@ -5,9 +5,12 @@ import Label from '$lib/components/ui/label/label.svelte'; import * as Select from '$lib/components/ui/select'; import { Textarea } from '$lib/components/ui/textarea'; - import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO, SETTINGS_KEYS } from '$lib/constants'; + import { SETTING_CONFIG_INFO, SETTINGS_KEYS } from '$lib/constants'; import { SettingsFieldType } from '$lib/enums/settings'; import { settingsStore } from '$lib/stores/settings.svelte'; + import { serverStore } from '$lib/stores/server.svelte'; + import { modelsStore, selectedModelName } from '$lib/stores/models.svelte'; + import { normalizeFloatingPoint } from '$lib/utils/precision'; import { ChatSettingsParameterSourceIndicator } from '$lib/components/app'; import type { Component } from 'svelte'; @@ -20,35 +23,36 @@ let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props(); - // Helper function to get parameter source info for syncable parameters - function getParameterSourceInfo(key: string) { - if (!settingsStore.canSyncParameter(key)) { - return null; + // server sampling defaults for placeholders + let sp = $derived.by(() => { + if (serverStore.isRouterMode) { + const m = selectedModelName(); + if (m) { + const p = modelsStore.getModelProps(m); + return (p?.default_generation_settings?.params ?? {}) as Record; + } } - - return settingsStore.getParameterInfo(key); - } + return (serverStore.defaultParams ?? {}) as Record; + }); {#each fields as field (field.key)}
{#if field.type === SettingsFieldType.INPUT} - {@const paramInfo = getParameterSourceInfo(field.key)} {@const currentValue = String(localConfig[field.key] ?? '')} - {@const propsDefault = paramInfo?.serverDefault} + {@const serverDefault = sp[field.key]} {@const isCustomRealTime = (() => { - if (!paramInfo || propsDefault === undefined) return false; + if (serverDefault == null) return false; + if (currentValue === '') return false; - // Apply same rounding logic for real-time comparison - const inputValue = currentValue; - const numericInput = parseFloat(inputValue); + const numericInput = parseFloat(currentValue); const normalizedInput = !isNaN(numericInput) ? Math.round(numericInput * 1000000) / 1000000 - : inputValue; + : currentValue; const normalizedDefault = - typeof propsDefault === 'number' - ? Math.round(propsDefault * 1000000) / 1000000 - : propsDefault; + typeof serverDefault === 'number' + ? Math.round(serverDefault * 1000000) / 1000000 + : serverDefault; return normalizedInput !== normalizedDefault; })()} @@ -74,7 +78,9 @@ // Update local config immediately for real-time badge feedback onConfigChange(field.key, e.currentTarget.value); }} - placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`} + placeholder={sp[field.key] != null + ? `Default: ${normalizeFloatingPoint(sp[field.key])}` + : ''} class="w-full {isCustomRealTime ? 'pr-8' : ''}" /> {#if isCustomRealTime} @@ -82,9 +88,7 @@ type="button" onclick={() => { settingsStore.resetParameterToServerDefault(field.key); - // Trigger UI update by calling onConfigChange with the default value - const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key]; - onConfigChange(field.key, String(defaultValue)); + onConfigChange(field.key, ''); }} class="absolute top-1/2 right-2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted" aria-label="Reset to default" @@ -112,7 +116,7 @@ id={field.key} value={String(localConfig[field.key] ?? '')} onchange={(e) => onConfigChange(field.key, e.currentTarget.value)} - placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`} + placeholder="" class="min-h-[10rem] w-full md:max-w-2xl" /> @@ -140,14 +144,12 @@ (opt: { value: string; label: string; icon?: Component }) => opt.value === localConfig[field.key] )} - {@const paramInfo = getParameterSourceInfo(field.key)} {@const currentValue = localConfig[field.key]} - {@const propsDefault = paramInfo?.serverDefault} + {@const serverDefault = sp[field.key]} {@const isCustomRealTime = (() => { - if (!paramInfo || propsDefault === undefined) return false; - - // For select fields, do direct comparison (no rounding needed) - return currentValue !== propsDefault; + if (serverDefault == null) return false; + if (currentValue === '' || currentValue === undefined) return false; + return currentValue !== serverDefault; })()}
@@ -190,9 +192,7 @@ type="button" onclick={() => { settingsStore.resetParameterToServerDefault(field.key); - // Trigger UI update by calling onConfigChange with the default value - const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key]; - onConfigChange(field.key, String(defaultValue)); + onConfigChange(field.key, ''); }} class="absolute top-1/2 right-8 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted" aria-label="Reset to default" diff --git a/tools/server/webui/src/lib/components/app/models/ModelId.svelte b/tools/server/webui/src/lib/components/app/models/ModelId.svelte index 9b25d05c1..5fda49342 100644 --- a/tools/server/webui/src/lib/components/app/models/ModelId.svelte +++ b/tools/server/webui/src/lib/components/app/models/ModelId.svelte @@ -28,6 +28,11 @@ let parsed = $derived(ModelsService.parseModelId(modelId)); let resolvedShowRaw = $derived(showRaw ?? (config().showRawModelNames as boolean) ?? false); + let displayName = $derived( + aliases && aliases.length > 0 ? aliases[0] : (parsed.modelName ?? modelId) + ); + let remainingAliases = $derived(aliases && aliases.length > 1 ? aliases.slice(1) : []); + let allTags = $derived([...(parsed.tags ?? []), ...(tags ?? [])]); {#if resolvedShowRaw} @@ -35,7 +40,7 @@ {:else} - {#if showOrgName && parsed.orgName}{parsed.orgName}/{/if}{parsed.modelName ?? modelId} + {#if showOrgName && parsed.orgName && !(aliases && aliases.length > 0)}{parsed.orgName}/{/if}{displayName} {#if parsed.params} @@ -50,14 +55,14 @@ {/if} - {#if aliases && aliases.length > 0} - {#each aliases as alias (alias)} + {#if remainingAliases.length > 0} + {#each remainingAliases as alias (alias)} {alias} {/each} {/if} - {#if tags && tags.length > 0} - {#each tags as tag (tag)} + {#if allTags.length > 0} + {#each allTags as tag (tag)} {tag} {/each} {/if} diff --git a/tools/server/webui/src/lib/constants/localstorage-keys.ts b/tools/server/webui/src/lib/constants/localstorage-keys.ts index 6b9a9e0e2..dc4d69b4e 100644 --- a/tools/server/webui/src/lib/constants/localstorage-keys.ts +++ b/tools/server/webui/src/lib/constants/localstorage-keys.ts @@ -1,3 +1,4 @@ export const CONFIG_LOCALSTORAGE_KEY = 'LlamaCppWebui.config'; export const USER_OVERRIDES_LOCALSTORAGE_KEY = 'LlamaCppWebui.userOverrides'; export const FAVOURITE_MODELS_LOCALSTORAGE_KEY = 'LlamaCppWebui.favouriteModels'; +export const MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY = 'LlamaCppWebui.mcpDefaultEnabled'; diff --git a/tools/server/webui/src/lib/constants/model-id.ts b/tools/server/webui/src/lib/constants/model-id.ts index eb6662a02..ee314d167 100644 --- a/tools/server/webui/src/lib/constants/model-id.ts +++ b/tools/server/webui/src/lib/constants/model-id.ts @@ -11,10 +11,16 @@ export const MODEL_ID_SEGMENT_SEPARATOR = '-'; export const MODEL_ID_QUANTIZATION_SEPARATOR = ':'; /** - * Matches a trailing ALL-CAPS format segment, e.g. `GGUF`, `BF16`, `Q4_K_M`. - * Must be at least 2 uppercase letters, optionally followed by uppercase letters or digits. + * Matches a quantization/precision segment, e.g. `Q4_K_M`, `IQ4_XS`, `F16`, `BF16`, `MXFP4`. + * Case-insensitive to handle both uppercase and lowercase inputs. */ -export const MODEL_FORMAT_SEGMENT_RE = /^[A-Z]{2,}[A-Z0-9]*$/; +export const MODEL_QUANTIZATION_SEGMENT_RE = + /^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i; + +/** + * Matches prefix for custom quantization types, e.g. `UD-Q8_K_XL`. + */ +export const MODEL_CUSTOM_QUANTIZATION_PREFIX_RE = /^UD$/i; /** * Matches a parameter-count segment, e.g. `7B`, `1.5b`, `120M`. @@ -22,7 +28,12 @@ export const MODEL_FORMAT_SEGMENT_RE = /^[A-Z]{2,}[A-Z0-9]*$/; export const MODEL_PARAMS_RE = /^\d+(\.\d+)?[BbMmKkTt]$/; /** - * Matches an activated-parameter-count segment, e.g. `A10B`, `A2.4b`. - * The leading `A` distinguishes it from a regular params segment. + * Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`. + * The leading `A`/`a` distinguishes it from a regular params segment. */ -export const MODEL_ACTIVATED_PARAMS_RE = /^A\d+(\.\d+)?[BbMmKkTt]$/; +export const MODEL_ACTIVATED_PARAMS_RE = /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/; + +/** + * Container format segments to exclude from tags (every model uses these). + */ +export const MODEL_IGNORED_SEGMENTS = new Set(['GGUF', 'GGML']); diff --git a/tools/server/webui/src/lib/constants/settings-config.ts b/tools/server/webui/src/lib/constants/settings-config.ts index 91d0ecee3..3a4a8f3cb 100644 --- a/tools/server/webui/src/lib/constants/settings-config.ts +++ b/tools/server/webui/src/lib/constants/settings-config.ts @@ -1,8 +1,8 @@ import { ColorMode } from '$lib/enums/ui'; import { Monitor, Moon, Sun } from '@lucide/svelte'; -export const SETTING_CONFIG_DEFAULT: Record = { - // Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value. Do not use null or undefined for default value. +export const SETTING_CONFIG_DEFAULT: Record = { + // Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value. // Do not use nested objects, keep it single level. Prefix the key if you need to group them. apiKey: '', systemMessage: '', @@ -30,27 +30,30 @@ export const SETTING_CONFIG_DEFAULT: Record = agenticMaxToolPreviewLines: 25, showToolCallInProgress: false, alwaysShowAgenticTurns: false, - // make sure these default values are in sync with `common.h` - samplers: 'top_k;typ_p;top_p;min_p;temperature', + // sampling params: empty means "use server default" + // the server / preset is the source of truth + // empty values are shown as placeholders from /props in the UI + // and are NOT sent in API requests, letting the server decide + samplers: '', backend_sampling: false, - temperature: 0.8, - dynatemp_range: 0.0, - dynatemp_exponent: 1.0, - top_k: 40, - top_p: 0.95, - min_p: 0.05, - xtc_probability: 0.0, - xtc_threshold: 0.1, - typ_p: 1.0, - repeat_last_n: 64, - repeat_penalty: 1.0, - presence_penalty: 0.0, - frequency_penalty: 0.0, - dry_multiplier: 0.0, - dry_base: 1.75, - dry_allowed_length: 2, - dry_penalty_last_n: -1, - max_tokens: -1, + temperature: undefined, + dynatemp_range: undefined, + dynatemp_exponent: undefined, + top_k: undefined, + top_p: undefined, + min_p: undefined, + xtc_probability: undefined, + xtc_threshold: undefined, + typ_p: undefined, + repeat_last_n: undefined, + repeat_penalty: undefined, + presence_penalty: undefined, + frequency_penalty: undefined, + dry_multiplier: undefined, + dry_base: undefined, + dry_allowed_length: undefined, + dry_penalty_last_n: undefined, + max_tokens: undefined, custom: '', // custom json-stringified object // experimental features pyInterpreterEnabled: false, diff --git a/tools/server/webui/src/lib/services/models.service.ts b/tools/server/webui/src/lib/services/models.service.ts index de90c48cf..209bd7cab 100644 --- a/tools/server/webui/src/lib/services/models.service.ts +++ b/tools/server/webui/src/lib/services/models.service.ts @@ -2,9 +2,11 @@ import { ServerModelStatus } from '$lib/enums'; import { apiFetch, apiPost } from '$lib/utils'; import type { ParsedModelId } from '$lib/types/models'; import { - MODEL_FORMAT_SEGMENT_RE, + MODEL_QUANTIZATION_SEGMENT_RE, + MODEL_CUSTOM_QUANTIZATION_PREFIX_RE, MODEL_PARAMS_RE, MODEL_ACTIVATED_PARAMS_RE, + MODEL_IGNORED_SEGMENTS, MODEL_ID_NOT_FOUND, MODEL_ID_ORG_SEPARATOR, MODEL_ID_SEGMENT_SEPARATOR, @@ -119,8 +121,9 @@ export class ModelsService { /** * Parse a model ID string into its structured components. * - * Handles the convention: - * `/-(-)-:` + * Handles conventions like: + * `/-(-)(-)(-):` + * `.` (dot-separated quantization, e.g. `model.Q4_K_M`) * * @param modelId - Raw model identifier string * @returns Structured {@link ParsedModelId} with all detected fields @@ -132,11 +135,11 @@ export class ModelsService { modelName: null, params: null, activatedParams: null, - format: null, quantization: null, tags: [] }; + // 1. Extract colon-separated quantization (e.g. `model:Q4_K_M`) const colonIdx = modelId.indexOf(MODEL_ID_QUANTIZATION_SEPARATOR); let modelPath: string; @@ -147,6 +150,7 @@ export class ModelsService { modelPath = modelId; } + // 2. Extract org name (e.g. `org/model` -> org = "org") const slashIdx = modelPath.indexOf(MODEL_ID_ORG_SEPARATOR); let modelStr: string; @@ -157,37 +161,66 @@ export class ModelsService { modelStr = modelPath; } - const segments = modelStr.split(MODEL_ID_SEGMENT_SEPARATOR); + // 3. Handle dot-separated quantization (e.g. `model-name.Q4_K_M`) + const dotIdx = modelStr.lastIndexOf('.'); - if (segments.length > 0 && MODEL_FORMAT_SEGMENT_RE.test(segments[segments.length - 1])) { - result.format = segments.pop()!; + if (dotIdx !== MODEL_ID_NOT_FOUND && !result.quantization) { + const afterDot = modelStr.slice(dotIdx + 1); + + if (MODEL_QUANTIZATION_SEGMENT_RE.test(afterDot)) { + result.quantization = afterDot; + modelStr = modelStr.slice(0, dotIdx); + } } - const paramsRe = MODEL_PARAMS_RE; - const activatedParamsRe = MODEL_ACTIVATED_PARAMS_RE; + const segments = modelStr.split(MODEL_ID_SEGMENT_SEPARATOR); + // 4. Detect trailing quantization from dash-separated segments + // Handle UD-prefixed quantization (e.g. `UD-Q8_K_XL`) and + // standalone quantization (e.g. `Q4_K_M`, `BF16`, `F16`, `MXFP4`) + if (!result.quantization && segments.length > 1) { + const last = segments[segments.length - 1]; + const secondLast = segments.length > 2 ? segments[segments.length - 2] : null; + + if (MODEL_QUANTIZATION_SEGMENT_RE.test(last)) { + if (secondLast && MODEL_CUSTOM_QUANTIZATION_PREFIX_RE.test(secondLast)) { + result.quantization = `${secondLast}-${last}`; + segments.splice(segments.length - 2, 2); + } else { + result.quantization = last; + segments.pop(); + } + } + } + + // 5. Find params and activated params let paramsIdx = MODEL_ID_NOT_FOUND; let activatedParamsIdx = MODEL_ID_NOT_FOUND; for (let i = 0; i < segments.length; i++) { const seg = segments[i]; - if (paramsIdx === -1 && paramsRe.test(seg)) { + + if (paramsIdx === MODEL_ID_NOT_FOUND && MODEL_PARAMS_RE.test(seg)) { paramsIdx = i; result.params = seg.toUpperCase(); - } else if (activatedParamsRe.test(seg)) { + } else if (paramsIdx !== MODEL_ID_NOT_FOUND && MODEL_ACTIVATED_PARAMS_RE.test(seg)) { activatedParamsIdx = i; result.activatedParams = seg.toUpperCase(); } } + // 6. Model name = segments before params; tags = remaining segments after params const pivotIdx = paramsIdx !== MODEL_ID_NOT_FOUND ? paramsIdx : segments.length; result.modelName = segments.slice(0, pivotIdx).join(MODEL_ID_SEGMENT_SEPARATOR) || null; if (paramsIdx !== MODEL_ID_NOT_FOUND) { - result.tags = segments - .slice(paramsIdx + 1) - .filter((_, relIdx) => paramsIdx + 1 + relIdx !== activatedParamsIdx); + result.tags = segments.slice(paramsIdx + 1).filter((_, relIdx) => { + const absIdx = paramsIdx + 1 + relIdx; + if (absIdx === activatedParamsIdx) return false; + + return !MODEL_IGNORED_SEGMENTS.has(segments[absIdx].toUpperCase()); + }); } return result; diff --git a/tools/server/webui/src/lib/stores/conversations.svelte.ts b/tools/server/webui/src/lib/stores/conversations.svelte.ts index 39f206479..3cfbd3d1c 100644 --- a/tools/server/webui/src/lib/stores/conversations.svelte.ts +++ b/tools/server/webui/src/lib/stores/conversations.svelte.ts @@ -36,7 +36,8 @@ import { ISO_TIME_SEPARATOR, ISO_TIME_SEPARATOR_REPLACEMENT, NON_ALPHANUMERIC_REGEX, - MULTIPLE_UNDERSCORE_REGEX + MULTIPLE_UNDERSCORE_REGEX, + MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY } from '$lib/constants'; class ConversationsStore { @@ -61,7 +62,37 @@ class ConversationsStore { isInitialized = $state(false); /** Pending MCP server overrides for new conversations (before first message) */ - pendingMcpServerOverrides = $state([]); + pendingMcpServerOverrides = $state(ConversationsStore.loadMcpDefaults()); + + /** Load MCP default overrides from localStorage */ + private static loadMcpDefaults(): McpServerOverride[] { + if (typeof globalThis.localStorage === 'undefined') return []; + try { + const raw = localStorage.getItem(MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return []; + return parsed.filter( + (o: unknown) => typeof o === 'object' && o !== null && 'serverId' in o && 'enabled' in o + ) as McpServerOverride[]; + } catch { + return []; + } + } + + /** Persist MCP default overrides to localStorage */ + private saveMcpDefaults(): void { + if (typeof globalThis.localStorage === 'undefined') return; + const plain = this.pendingMcpServerOverrides.map((o) => ({ + serverId: o.serverId, + enabled: o.enabled + })); + if (plain.length > 0) { + localStorage.setItem(MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY, JSON.stringify(plain)); + } else { + localStorage.removeItem(MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY); + } + } /** Callback for title update confirmation dialog */ titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise; @@ -261,6 +292,8 @@ class ConversationsStore { clearActiveConversation(): void { this.activeConversation = null; this.activeMessages = []; + // reload MCP defaults so new chats inherit persisted state + this.pendingMcpServerOverrides = ConversationsStore.loadMcpDefaults(); } /** @@ -597,6 +630,7 @@ class ConversationsStore { this.pendingMcpServerOverrides = [...this.pendingMcpServerOverrides, { serverId, enabled }]; } } + this.saveMcpDefaults(); } /** @@ -621,6 +655,7 @@ class ConversationsStore { */ clearPendingMcpServerOverrides(): void { this.pendingMcpServerOverrides = []; + this.saveMcpDefaults(); } /** diff --git a/tools/server/webui/src/lib/stores/mcp.svelte.ts b/tools/server/webui/src/lib/stores/mcp.svelte.ts index dadf8fda6..efc8cf060 100644 --- a/tools/server/webui/src/lib/stores/mcp.svelte.ts +++ b/tools/server/webui/src/lib/stores/mcp.svelte.ts @@ -208,23 +208,16 @@ class MCPStore { } /** - * Checks if a server is enabled, considering per-chat overrides. + * Checks if a server is enabled for a given chat. + * Only per-chat overrides (persisted in localStorage for new chats, + * or in IndexedDB for existing conversations) control enabled state. */ #checkServerEnabled( server: MCPServerSettingsEntry, perChatOverrides?: McpServerOverride[] ): boolean { - if (!server.enabled) { - return false; - } - - if (perChatOverrides) { - const override = perChatOverrides.find((o) => o.serverId === server.id); - - return override?.enabled ?? false; - } - - return false; + const override = perChatOverrides?.find((o) => o.serverId === server.id); + return override?.enabled ?? false; } /** @@ -570,18 +563,8 @@ class MCPStore { getEnabledServersForConversation( perChatOverrides?: McpServerOverride[] ): MCPServerSettingsEntry[] { - if (!perChatOverrides?.length) { - return []; - } - return this.getServers().filter((server) => { - if (!server.enabled) { - return false; - } - - const override = perChatOverrides.find((o) => o.serverId === server.id); - - return override?.enabled ?? false; + return this.#checkServerEnabled(server, perChatOverrides); }); } diff --git a/tools/server/webui/src/lib/stores/settings.svelte.ts b/tools/server/webui/src/lib/stores/settings.svelte.ts index 8ab817c07..2fbff8312 100644 --- a/tools/server/webui/src/lib/stores/settings.svelte.ts +++ b/tools/server/webui/src/lib/stores/settings.svelte.ts @@ -289,16 +289,10 @@ class SettingsStore { const serverDefaults = this.getServerDefaults(); if (serverDefaults[key] !== undefined) { - const value = normalizeFloatingPoint(serverDefaults[key]); - - this.config[key as keyof SettingsConfigType] = - value as SettingsConfigType[keyof SettingsConfigType]; - } else { - if (key in SETTING_CONFIG_DEFAULT) { - const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key); - - setConfigValue(this.config, key, defaultValue); - } + // sampling param known by server: clear it, let server decide + setConfigValue(this.config, key, ''); + } else if (key in SETTING_CONFIG_DEFAULT) { + setConfigValue(this.config, key, getConfigValue(SETTING_CONFIG_DEFAULT, key)); } this.userOverrides.delete(key); @@ -319,12 +313,7 @@ class SettingsStore { */ syncWithServerDefaults(): void { const propsDefaults = this.getServerDefaults(); - - if (Object.keys(propsDefaults).length === 0) { - console.warn('No server defaults available for initialization'); - - return; - } + if (Object.keys(propsDefaults).length === 0) return; for (const [key, propsValue] of Object.entries(propsDefaults)) { const currentValue = getConfigValue(this.config, key); @@ -332,17 +321,14 @@ class SettingsStore { const normalizedCurrent = normalizeFloatingPoint(currentValue); const normalizedDefault = normalizeFloatingPoint(propsValue); + // if user value matches server, it's not a real override if (normalizedCurrent === normalizedDefault) { this.userOverrides.delete(key); - setConfigValue(this.config, key, propsValue); - } else if (!this.userOverrides.has(key)) { - setConfigValue(this.config, key, propsValue); } } this.saveConfig(); - console.log('Settings initialized with props defaults:', propsDefaults); - console.log('Current user overrides after sync:', Array.from(this.userOverrides)); + console.log('User overrides after sync:', Array.from(this.userOverrides)); } /** @@ -352,19 +338,11 @@ class SettingsStore { */ forceSyncWithServerDefaults(): void { const propsDefaults = this.getServerDefaults(); - const syncableKeys = ParameterSyncService.getSyncableParameterKeys(); - - for (const key of syncableKeys) { + for (const key of ParameterSyncService.getSyncableParameterKeys()) { if (propsDefaults[key] !== undefined) { - const normalizedValue = normalizeFloatingPoint(propsDefaults[key]); - - setConfigValue(this.config, key, normalizedValue); - } else { - if (key in SETTING_CONFIG_DEFAULT) { - const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key); - - setConfigValue(this.config, key, defaultValue); - } + setConfigValue(this.config, key, ''); + } else if (key in SETTING_CONFIG_DEFAULT) { + setConfigValue(this.config, key, getConfigValue(SETTING_CONFIG_DEFAULT, key)); } this.userOverrides.delete(key); diff --git a/tools/server/webui/src/lib/types/models.d.ts b/tools/server/webui/src/lib/types/models.d.ts index dc8e86485..b4d5f11f5 100644 --- a/tools/server/webui/src/lib/types/models.d.ts +++ b/tools/server/webui/src/lib/types/models.d.ts @@ -25,7 +25,6 @@ export interface ParsedModelId { modelName: string | null; params: string | null; activatedParams: string | null; - format: string | null; quantization: string | null; tags: string[]; } diff --git a/tools/server/webui/src/lib/types/settings.d.ts b/tools/server/webui/src/lib/types/settings.d.ts index 67194d12e..360740ab0 100644 --- a/tools/server/webui/src/lib/types/settings.d.ts +++ b/tools/server/webui/src/lib/types/settings.d.ts @@ -5,7 +5,7 @@ import type { DatabaseMessageExtra } from './database'; import type { ParameterSource, SyncableParameterType, SettingsFieldType } from '$lib/enums'; import type { Icon } from '@lucide/svelte'; -export type SettingsConfigValue = string | number | boolean; +export type SettingsConfigValue = string | number | boolean | undefined; export interface SettingsFieldConfig { key: string; diff --git a/tools/server/webui/tests/unit/model-id-parser.test.ts b/tools/server/webui/tests/unit/model-id-parser.test.ts new file mode 100644 index 000000000..3c2937d35 --- /dev/null +++ b/tools/server/webui/tests/unit/model-id-parser.test.ts @@ -0,0 +1,270 @@ +import { describe, expect, it } from 'vitest'; +import { ModelsService } from '$lib/services/models.service'; + +const { parseModelId } = ModelsService; + +describe('parseModelId', () => { + it('handles unknown patterns correctly', () => { + expect(parseModelId('model-name-1')).toStrictEqual({ + activatedParams: null, + modelName: 'model-name-1', + orgName: null, + params: null, + quantization: null, + raw: 'model-name-1', + tags: [] + }); + + expect(parseModelId('org/model-name-2')).toStrictEqual({ + activatedParams: null, + modelName: 'model-name-2', + orgName: 'org', + params: null, + quantization: null, + raw: 'org/model-name-2', + tags: [] + }); + }); + + it('extracts model parameters correctly', () => { + expect(parseModelId('model-100B-BF16')).toMatchObject({ params: '100B' }); + expect(parseModelId('model-100B:Q4_K_M')).toMatchObject({ params: '100B' }); + }); + + it('extracts model parameters correctly in lowercase', () => { + expect(parseModelId('model-100b-bf16')).toMatchObject({ params: '100B' }); + expect(parseModelId('model-100b:q4_k_m')).toMatchObject({ params: '100B' }); + }); + + it('extracts activated parameters correctly', () => { + expect(parseModelId('model-100B-A10B-BF16')).toMatchObject({ activatedParams: 'A10B' }); + expect(parseModelId('model-100B-A10B:Q4_K_M')).toMatchObject({ activatedParams: 'A10B' }); + }); + + it('extracts activated parameters correctly in lowercase', () => { + expect(parseModelId('model-100b-a10b-bf16')).toMatchObject({ activatedParams: 'A10B' }); + expect(parseModelId('model-100b-a10b:q4_k_m')).toMatchObject({ activatedParams: 'A10B' }); + }); + + it('extracts quantization correctly', () => { + // Dash-separated quantization + expect(parseModelId('model-100B-UD-IQ1_S')).toMatchObject({ quantization: 'UD-IQ1_S' }); + expect(parseModelId('model-100B-IQ4_XS')).toMatchObject({ quantization: 'IQ4_XS' }); + expect(parseModelId('model-100B-Q4_K_M')).toMatchObject({ quantization: 'Q4_K_M' }); + expect(parseModelId('model-100B-Q8_0')).toMatchObject({ quantization: 'Q8_0' }); + expect(parseModelId('model-100B-UD-Q8_K_XL')).toMatchObject({ quantization: 'UD-Q8_K_XL' }); + expect(parseModelId('model-100B-F16')).toMatchObject({ quantization: 'F16' }); + expect(parseModelId('model-100B-BF16')).toMatchObject({ quantization: 'BF16' }); + expect(parseModelId('model-100B-MXFP4')).toMatchObject({ quantization: 'MXFP4' }); + + // Colon-separated quantization + expect(parseModelId('model-100B:UD-IQ1_S')).toMatchObject({ quantization: 'UD-IQ1_S' }); + expect(parseModelId('model-100B:IQ4_XS')).toMatchObject({ quantization: 'IQ4_XS' }); + expect(parseModelId('model-100B:Q4_K_M')).toMatchObject({ quantization: 'Q4_K_M' }); + expect(parseModelId('model-100B:Q8_0')).toMatchObject({ quantization: 'Q8_0' }); + expect(parseModelId('model-100B:UD-Q8_K_XL')).toMatchObject({ quantization: 'UD-Q8_K_XL' }); + expect(parseModelId('model-100B:F16')).toMatchObject({ quantization: 'F16' }); + expect(parseModelId('model-100B:BF16')).toMatchObject({ quantization: 'BF16' }); + expect(parseModelId('model-100B:MXFP4')).toMatchObject({ quantization: 'MXFP4' }); + + // Dot-separated quantization + expect(parseModelId('nomic-embed-text-v2-moe.Q4_K_M')).toMatchObject({ + quantization: 'Q4_K_M' + }); + }); + + it('extracts additional tags correctly', () => { + expect(parseModelId('model-100B-foobar-Q4_K_M')).toMatchObject({ tags: ['foobar'] }); + expect(parseModelId('model-100B-A10B-foobar-1M-BF16')).toMatchObject({ + tags: ['foobar', '1M'] + }); + expect(parseModelId('model-100B-1M-foobar:UD-Q8_K_XL')).toMatchObject({ + tags: ['1M', 'foobar'] + }); + }); + + it('filters out container format segments from tags', () => { + expect(parseModelId('model-100B-GGUF-Instruct-BF16')).toMatchObject({ + tags: ['Instruct'] + }); + expect(parseModelId('model-100B-GGML-Instruct:Q4_K_M')).toMatchObject({ + tags: ['Instruct'] + }); + }); + + it('handles real-world examples correctly', () => { + expect(parseModelId('meta-llama/Llama-3.1-8B')).toStrictEqual({ + activatedParams: null, + modelName: 'Llama-3.1', + orgName: 'meta-llama', + params: '8B', + quantization: null, + raw: 'meta-llama/Llama-3.1-8B', + tags: [] + }); + + expect(parseModelId('openai/gpt-oss-120b-MXFP4')).toStrictEqual({ + activatedParams: null, + modelName: 'gpt-oss', + orgName: 'openai', + params: '120B', + quantization: 'MXFP4', + raw: 'openai/gpt-oss-120b-MXFP4', + tags: [] + }); + + expect(parseModelId('openai/gpt-oss-20b:Q4_K_M')).toStrictEqual({ + activatedParams: null, + modelName: 'gpt-oss', + orgName: 'openai', + params: '20B', + quantization: 'Q4_K_M', + raw: 'openai/gpt-oss-20b:Q4_K_M', + tags: [] + }); + + expect(parseModelId('Qwen/Qwen3-Coder-30B-A3B-Instruct-1M-BF16')).toStrictEqual({ + activatedParams: 'A3B', + modelName: 'Qwen3-Coder', + orgName: 'Qwen', + params: '30B', + quantization: 'BF16', + raw: 'Qwen/Qwen3-Coder-30B-A3B-Instruct-1M-BF16', + tags: ['Instruct', '1M'] + }); + }); + + it('handles real-world examples with quantization in segments', () => { + expect(parseModelId('meta-llama/Llama-4-Scout-17B-16E-Instruct-Q4_K_M')).toStrictEqual({ + activatedParams: null, + modelName: 'Llama-4-Scout', + orgName: 'meta-llama', + params: '17B', + quantization: 'Q4_K_M', + raw: 'meta-llama/Llama-4-Scout-17B-16E-Instruct-Q4_K_M', + tags: ['16E', 'Instruct'] + }); + + expect(parseModelId('MiniMaxAI/MiniMax-M2-IQ4_XS')).toStrictEqual({ + activatedParams: null, + modelName: 'MiniMax-M2', + orgName: 'MiniMaxAI', + params: null, + quantization: 'IQ4_XS', + raw: 'MiniMaxAI/MiniMax-M2-IQ4_XS', + tags: [] + }); + + expect(parseModelId('MiniMaxAI/MiniMax-M2-UD-Q3_K_XL')).toStrictEqual({ + activatedParams: null, + modelName: 'MiniMax-M2', + orgName: 'MiniMaxAI', + params: null, + quantization: 'UD-Q3_K_XL', + raw: 'MiniMaxAI/MiniMax-M2-UD-Q3_K_XL', + tags: [] + }); + + expect(parseModelId('mistralai/Devstral-2-123B-Instruct-2512-Q4_K_M')).toStrictEqual({ + activatedParams: null, + modelName: 'Devstral-2', + orgName: 'mistralai', + params: '123B', + quantization: 'Q4_K_M', + raw: 'mistralai/Devstral-2-123B-Instruct-2512-Q4_K_M', + tags: ['Instruct', '2512'] + }); + + expect(parseModelId('mistralai/Devstral-Small-2-24B-Instruct-2512-Q8_0')).toStrictEqual({ + activatedParams: null, + modelName: 'Devstral-Small-2', + orgName: 'mistralai', + params: '24B', + quantization: 'Q8_0', + raw: 'mistralai/Devstral-Small-2-24B-Instruct-2512-Q8_0', + tags: ['Instruct', '2512'] + }); + + expect(parseModelId('noctrex/GLM-4.7-Flash-MXFP4_MOE')).toStrictEqual({ + activatedParams: null, + modelName: 'GLM-4.7-Flash', + orgName: 'noctrex', + params: null, + quantization: 'MXFP4_MOE', + raw: 'noctrex/GLM-4.7-Flash-MXFP4_MOE', + tags: [] + }); + + expect(parseModelId('Qwen/Qwen3-Coder-Next-Q4_K_M')).toStrictEqual({ + activatedParams: null, + modelName: 'Qwen3-Coder-Next', + orgName: 'Qwen', + params: null, + quantization: 'Q4_K_M', + raw: 'Qwen/Qwen3-Coder-Next-Q4_K_M', + tags: [] + }); + + expect(parseModelId('openai/gpt-oss-120b-Q4_K_M')).toStrictEqual({ + activatedParams: null, + modelName: 'gpt-oss', + orgName: 'openai', + params: '120B', + quantization: 'Q4_K_M', + raw: 'openai/gpt-oss-120b-Q4_K_M', + tags: [] + }); + + expect(parseModelId('openai/gpt-oss-20b-F16')).toStrictEqual({ + activatedParams: null, + modelName: 'gpt-oss', + orgName: 'openai', + params: '20B', + quantization: 'F16', + raw: 'openai/gpt-oss-20b-F16', + tags: [] + }); + + expect(parseModelId('nomic-embed-text-v2-moe.Q4_K_M')).toStrictEqual({ + activatedParams: null, + modelName: 'nomic-embed-text-v2-moe', + orgName: null, + params: null, + quantization: 'Q4_K_M', + raw: 'nomic-embed-text-v2-moe.Q4_K_M', + tags: [] + }); + }); + + it('handles ambiguous model names', () => { + // Qwen3.5 Instruct vs Thinking — tags should distinguish them + expect(parseModelId('Qwen/Qwen3.5-30B-A3B-Instruct')).toMatchObject({ + modelName: 'Qwen3.5', + params: '30B', + activatedParams: 'A3B', + tags: ['Instruct'] + }); + + expect(parseModelId('Qwen/Qwen3.5-30B-A3B-Thinking')).toMatchObject({ + modelName: 'Qwen3.5', + params: '30B', + activatedParams: 'A3B', + tags: ['Thinking'] + }); + + // Dot-separated quantization with variant suffixes + expect(parseModelId('gemma-3-27b-it-heretic-v2.Q8_0')).toMatchObject({ + modelName: 'gemma-3', + params: '27B', + quantization: 'Q8_0', + tags: ['it', 'heretic', 'v2'] + }); + + expect(parseModelId('gemma-3-27b-it.Q8_0')).toMatchObject({ + modelName: 'gemma-3', + params: '27B', + quantization: 'Q8_0', + tags: ['it'] + }); + }); +});