Compare commits

...
7 Commits
Author SHA1 Message Date
Xuan Son Nguyen f0440d9efc vendor: apply deep nested json patch from upstream 2026-10-09 23:35:12 +02:00
Dante 79e2e74eb1 CUDA: fix round issue, under MSVC the CPU and GPU agree (#30229) 2026-10-09 19:59:50 +02:00
Georgi Gerganov 8e2d31e0eb graph : reorder get_rows for embeddings (#30160)
* graph : reorder get_rows for embeddings

* cont : fix gemma4 and improve input embedding construction logic

* cont : add TODO for lora

* cont : fix raw embeddings path

* gemma4 : avoid ple cast in embeddings path
2026-10-09 20:43:05 +03:00
Aleksander GrygierandPascal baef3ed9a1 ui: Models Manager Follow-up Improvements (#30228)
* common : read a GGUF's trained context from its metadata

common_get_gguf_n_ctx_train opens only the file's metadata (no_alloc,
like common_get_decision_type) and reads <arch>.context_length, so a
caller can learn the trained context without loading the model. It
accepts both u32 and u64 values and returns 0 when the file is missing,
unreadable, invalid, or reports no context length.

Assisted-by: pi:zai-org/GLM-5.3-Flash

* server : report the trained context in the models listing

update_caps already resolves the model file offline to read its
modalities, so it now reads the trained context from the same GGUF
metadata, and GET /models reports it as context_length when it is
known. A router listing then carries the context without any Hub
request, which lets the UI sort and filter by it offline.

Assisted-by: pi:zai-org/GLM-5.3-Flash

* ui : take the trained context from the models listing

The router now reports context_length per model, so the option mapping
fills contextLength from it and the manager reads it before the Hub
record. The Context column, the context sort and the context filter
then work with the Hugging Face Hub API turned off. A browser suite
guards the sort and the search, the Hub-cache driven context filter and
the re-sort when details arrive after the sort was clicked.

Assisted-by: pi:zai-org/GLM-5.3-Flash

* ui : mark favorite models with a heart

A favorited model shows a rose heart in the selector even before its
row is hovered, and the crossed heart takes its place on hover, so
unfavoriting stays one hover away. The manager table marks its
favorited rows with the same heart after the badges and capabilities.

Assisted-by: pi:zai-org/GLM-5.3-Flash

* fix: UI text nit

* fix: UI nits

* fix: Favorite models grouping in models table

* feat: Remove sorting from Status column in Models Table

* server: read the GGUF metadata once per model

Read the decision type and the trained context in a single GGUF open,
accept only a UINT32 context length like the model loader, and reset
n_ctx_train with the other caps so a failed refresh drops it.

* fix: Post-review fixes

---------

Co-authored-by: Pascal <admin@serveurperso.com>
2026-10-09 19:28:54 +02:00
Martin Emrich f39148a953 llama-bench: respect -fitc if bigger than required benchmark size (#28331)
Assisted-By: opencode,llama.cpp,Qwen3.6-35B-A3B,Qwen3.8-27B
2026-10-09 19:14:30 +02:00
Anav Prasad 50e3e3e480 CUDA: Remove redundant CUDA copies after SSM_SCAN (#29807)
* CUDA: fuse copy of updated state snapshots into recurrent cache with ssm_scan

* CUDA: remove redundant cuda copies with K==1 (non spec-dec) scenario as well
2026-10-09 18:23:28 +02:00
jingzhou 64df9183f5 opencl: fix kernel compilation for a6x GPUs (#30176)
* opencl: skip kernel_cpy_f32_f32_pack on A6X to avoid shader compiler crash

* The A6x compiler backend found in iot device with a623 (E031.50.31.01)
  cannot handle kernels with a large number of arguments. Skip this
  kernel for A6x to avoid compiler crash

* opencl: A6X constant-fold workaround for get_local_size in GEMV kernels

* opencl: add Adreno 623 to A6X GPU detection list
2026-10-09 09:19:20 -07:00
35 changed files with 2847 additions and 244 deletions
+3
View File
@@ -45,6 +45,9 @@ insert_final_newline = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
[vendor/**.patch]
trim_trailing_whitespace = unset
[tools/ui/**]
indent_style = unset
indent_size = unset
+19 -16
View File
@@ -1195,7 +1195,9 @@ common_decision_type common_get_decision_type(const struct llama_model * model)
return common_decision_type_from_string(buf);
}
common_decision_type common_get_decision_type(const std::string & fname) {
common_gguf_info common_get_gguf_info(const std::string & fname) {
common_gguf_info info;
struct gguf_init_params gguf_params = {
/* .no_alloc = */ true,
/* .ctx = */ nullptr,
@@ -1203,31 +1205,32 @@ common_decision_type common_get_decision_type(const std::string & fname) {
gguf_context_ptr gguf_ctx(gguf_init_from_file(fname.c_str(), gguf_params));
if (!gguf_ctx) {
return COMMON_DECISION_TYPE_UNKNOWN; // missing or unreadable file
return info; // missing or unreadable file
}
std::string arch;
const int64_t arch_id = gguf_find_key(gguf_ctx.get(), "general.architecture");
if (arch_id < 0) {
return COMMON_DECISION_TYPE_UNKNOWN; // no architecture in the metadata
if (arch_id < 0 || gguf_get_kv_type(gguf_ctx.get(), arch_id) != GGUF_TYPE_STRING) {
return info; // no architecture in the metadata
}
if (gguf_get_kv_type(gguf_ctx.get(), arch_id) != GGUF_TYPE_STRING) {
return COMMON_DECISION_TYPE_UNKNOWN; // malformed metadata
}
arch = gguf_get_val_str(gguf_ctx.get(), arch_id);
const std::string arch = gguf_get_val_str(gguf_ctx.get(), arch_id);
if (arch.empty()) {
return COMMON_DECISION_TYPE_UNKNOWN;
return info;
}
const std::string key = arch + ".decision.type";
const int64_t type_id = gguf_find_key(gguf_ctx.get(), key.c_str());
const int64_t type_id = gguf_find_key(gguf_ctx.get(), (arch + ".decision.type").c_str());
if (type_id < 0) {
return COMMON_DECISION_TYPE_NONE;
info.decision_type = COMMON_DECISION_TYPE_NONE;
} else if (gguf_get_kv_type(gguf_ctx.get(), type_id) == GGUF_TYPE_STRING) {
info.decision_type = common_decision_type_from_string(gguf_get_val_str(gguf_ctx.get(), type_id));
}
if (gguf_get_kv_type(gguf_ctx.get(), type_id) != GGUF_TYPE_STRING) {
return COMMON_DECISION_TYPE_UNKNOWN; // malformed metadata
// same key and type as the model loader
const int64_t ctx_id = gguf_find_key(gguf_ctx.get(), (arch + ".context_length").c_str());
if (ctx_id >= 0 && gguf_get_kv_type(gguf_ctx.get(), ctx_id) == GGUF_TYPE_UINT32) {
info.n_ctx_train = gguf_get_val_u32(gguf_ctx.get(), ctx_id);
}
return common_decision_type_from_string(gguf_get_val_str(gguf_ctx.get(), type_id));
return info;
}
common_init_result::common_init_result(common_params & params, bool model_only) :
+7 -3
View File
@@ -970,9 +970,13 @@ enum common_decision_type {
common_decision_type common_get_decision_type(const struct llama_model * model);
// same as above, but reads a GGUF file; it does not load the model
// returns COMMON_DECISION_TYPE_UNKNOWN if the file is missing, unreadable, or invalid
common_decision_type common_get_decision_type(const std::string & fname);
// metadata of a GGUF file, read without loading the model
struct common_gguf_info {
common_decision_type decision_type = COMMON_DECISION_TYPE_UNKNOWN; // UNKNOWN if the file is missing, unreadable, or invalid
uint32_t n_ctx_train = 0; // 0 if unknown
};
common_gguf_info common_get_gguf_info(const std::string & fname);
// note: defines the model, context, samplers, ets. lifetimes
struct common_init_result {
+87
View File
@@ -2899,6 +2899,79 @@ static int ggml_cuda_try_gdn_cache_fusion(
return skip;
}
// match ssm_scan + the strided cpy that scatters its state snapshots into the cache, so the kernel writes them and skips the cpy
static int ggml_cuda_try_ssm_scan_cache_fusion(
const ggml_cgraph * cgraph, int node_idx, ggml_cuda_ssm_scan_fused_cache & fused_state_cpy) {
const ggml_tensor * ssm = cgraph->nodes[node_idx];
// the kernel skips the snapshot tail, so the scan output must not be a graph output
if (ssm->op != GGML_OP_SSM_SCAN || ssm->type != GGML_TYPE_F32 || (ssm->flags & GGML_TENSOR_FLAG_OUTPUT)) {
return 0;
}
const int64_t K = ggml_get_op_params_i32(ssm, 0); // snapshot slot count
const ggml_tensor * s = ssm->src[0];
const ggml_tensor * x = ssm->src[1];
const ggml_tensor * A = ssm->src[3];
const int64_t d_state = s->ne[0];
const int64_t D = d_state * s->ne[1] * x->ne[1]; // d_state * head_dim * n_head
const int64_t n_tok = x->ne[2];
const int64_t n_seqs = x->ne[3];
// only the mamba-2 kernels (group scan and SSD) write to the cache; mamba-1 still uses the cpy
if (A->nb[1] != sizeof(float) || (d_state != 96 && d_state != 128 && d_state != 256)) {
return 0;
}
// the scan reads its input rows from the cache (picked by ids), so with more than one seq a seq can read a row that another seq writes in the same launch
if (n_seqs != 1) {
return 0;
}
const int64_t n_written = std::min<int64_t>(n_tok, K);
const size_t tail_off = ggml_row_size(GGML_TYPE_F32, ggml_nelements(x));
// snapshot cpy is the first real node after the scan (skip views/no-ops)
const ggml_tensor * cpy = nullptr;
int skip = 0;
for (int j = node_idx + 1; j < cgraph->n_nodes && cpy == nullptr; ++j) {
const ggml_tensor * n = cgraph->nodes[j];
if (ggml_cuda_is_view_or_noop(n)) {
continue;
}
if (n->op != GGML_OP_CPY || (n->flags & GGML_TENSOR_FLAG_OUTPUT)) {
return 0;
}
cpy = n;
skip = j - node_idx;
}
if (cpy == nullptr) {
return 0;
}
const ggml_tensor * src = cpy->src[0]; // view of the scan snapshot tail
const ggml_tensor * dst = cpy->src[1]; // cache view the kernel writes to
// src must be this scan's snapshot tail (contiguous, at the tail offset)
if (src->op != GGML_OP_VIEW || src->view_src != ssm || src->view_offs != tail_off ||
!ggml_is_contiguous(src)) {
return 0;
}
// dst is the [D, n_seqs, n_written] cache view; require nb[1] == D, the per-seq stride the kernel takes from src0->nb[3]
const std::array<int64_t, GGML_MAX_DIMS> expected_ne = { D, n_seqs, n_written, 1 };
if (dst->op != GGML_OP_VIEW || dst->type != GGML_TYPE_F32 || dst->data == nullptr ||
!std::equal(expected_ne.begin(), expected_ne.end(), dst->ne) ||
dst->nb[0] != ggml_type_size(GGML_TYPE_F32) || dst->nb[1] != (size_t) ggml_row_size(GGML_TYPE_F32, D)) {
return 0;
}
fused_state_cpy.data = (float *) dst->data; // rollback slot 0 (newest)
fused_state_cpy.slot_stride = K > 1 ? (int64_t) (dst->nb[2] / sizeof(float)) : 0;
return skip;
}
static bool ggml_cuda_topk_moe_fusion(const struct ggml_cgraph * cgraph, int node_idx, ggml_cuda_topk_moe_args & args) {
args.sigmoid = false;
args.sqrt_softplus = false;
@@ -3585,6 +3658,20 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph
}
}
// ssm_scan -> cpy: scatter recurrent-state snapshots into the cache
if (node->op == GGML_OP_SSM_SCAN) {
ggml_cuda_ssm_scan_fused_cache fused_state_cpy;
const int nodes_to_skip = ggml_cuda_try_ssm_scan_cache_fusion(cgraph, i, fused_state_cpy);
if (nodes_to_skip > 0) {
#ifdef GGML_CUDA_DEBUG
GGML_LOG_INFO("%s: fused ssm_scan snapshot copies for %s (skipped %d nodes)\n",
__func__, node->name, nodes_to_skip);
#endif
ggml_cuda_op_ssm_scan_fused_cache(*cuda_ctx, node, fused_state_cpy);
return nodes_to_skip;
}
}
//topk-moe
if (cgraph->nodes[i]->op == GGML_OP_UNARY || cgraph->nodes[i]->op == GGML_OP_SOFT_MAX ||
cgraph->nodes[i]->op == GGML_OP_ARGSORT) {
+29 -14
View File
@@ -149,7 +149,7 @@ __global__ void __launch_bounds__(d_state, 1)
const int src0_nb2, const int src0_nb3, const int src1_nb2, const int src1_nb3,
const int src2_nb1, const int src2_nb2, const int src3_nb1,
const int src4_nb2, const int src4_nb3, const int src5_nb2, const int src5_nb3,
const int64_t s_off, const int64_t n_head, const int64_t d_head, const int64_t n_group, const int64_t n_tok, const int64_t K) {
char * s_base, const int64_t s_slot_bytes, const int64_t n_head, const int64_t d_head, const int64_t n_group, const int64_t n_tok, const int64_t K) {
const float * GGML_CUDA_RESTRICT src0 = src0_ptr;
const float * GGML_CUDA_RESTRICT src1 = src1_ptr;
const float * GGML_CUDA_RESTRICT src2 = src2_ptr;
@@ -184,7 +184,7 @@ __global__ void __launch_bounds__(d_state, 1)
const float * B_warp = (const float *) ((const char *) src4 + (seq_idx * src4_nb3) + (group_off));
const float * C_warp = (const float *) ((const char *) src5 + (seq_idx * src5_nb3) + (group_off));
float * y_warp = dst + (seq_idx * n_tok * n_head * d_head) + warp_idx;
float * s_warp = (float *) ((char *) dst + s_off + seq_idx * src0_nb3 + head_idx * src0_nb2 + head_off * d_state);
float * s_warp = (float *) (s_base + seq_idx * src0_nb3 + head_idx * src0_nb2 + head_off * d_state);
// strides across n_seq_tokens
const int stride_x = src1_nb2 / sizeof(float);
@@ -227,7 +227,7 @@ __global__ void __launch_bounds__(d_state, 1)
// Slot 0 is the final state written below; slots 1..K-1 are rollback snapshots.
const int64_t slot = n_tok - 1 - i;
if (K > 1 && slot > 0 && slot < K) {
float * s_snapshot_warp = (float *) ((char *) dst + s_off + (slot * gridDim.y + seq_idx) * src0_nb3 + head_idx * src0_nb2 + head_off * d_state);
float * s_snapshot_warp = (float *) ((char *) s_warp + slot * s_slot_bytes);
#pragma unroll
for (int j = 0; j < c_factor; j++) {
s_snapshot_warp[WARP_SIZE * j + lane] = state[j];
@@ -248,7 +248,11 @@ static void ssm_scan_f32_cuda(const float * src0, const float * src1, const floa
const int src2_nb2, const int src3_nb1, const int src4_nb2, const int src4_nb3, const int src5_nb2,
const int src5_nb3, const int64_t s_off, const int64_t d_state, const int64_t head_dim,
const int64_t n_head, const int64_t n_group, const int64_t n_tok, const int64_t n_seq,
const int64_t K, cudaStream_t stream) {
const int64_t K, const ggml_cuda_ssm_scan_fused_cache * cache, cudaStream_t stream) {
// when fused, the states go straight into the recurrent cache and the dst tail is left alone
char * const s_base = cache ? (char *) cache->data : (char *) dst + s_off;
const int64_t s_slot_bytes = cache ? cache->slot_stride * (int64_t) sizeof(float) : n_seq * (int64_t) src0_nb3;
// NOTE: if you change conditions here, be sure to update the corresponding supports_op condition!
if (src3_nb1 == sizeof(float)) {
// Mamba-2
@@ -261,7 +265,7 @@ static void ssm_scan_f32_cuda(const float * src0, const float * src1, const floa
ggml_cuda_kernel_launch(ssm_scan_f32_group<96/WARP_SIZE, 96>, launch_params,
src0, src1, src2, src3, src4, src5, src6, dst,
src0_nb2, src0_nb3, src1_nb2, src1_nb3, src2_nb1, src2_nb2, src3_nb1,
src4_nb2, src4_nb3, src5_nb2, src5_nb3, s_off, n_head, head_dim, n_group, n_tok, K);
src4_nb2, src4_nb3, src5_nb2, src5_nb3, s_base, s_slot_bytes, n_head, head_dim, n_group, n_tok, K);
} else if (d_state == 128) {
constexpr int threads = 128;
constexpr int num_warps = threads/WARP_SIZE;
@@ -271,7 +275,7 @@ static void ssm_scan_f32_cuda(const float * src0, const float * src1, const floa
ggml_cuda_kernel_launch(ssm_scan_f32_group<128/WARP_SIZE, 128>, launch_params,
src0, src1, src2, src3, src4, src5, src6, dst,
src0_nb2, src0_nb3, src1_nb2, src1_nb3, src2_nb1, src2_nb2, src3_nb1,
src4_nb2, src4_nb3, src5_nb2, src5_nb3, s_off, n_head, head_dim, n_group, n_tok, K);
src4_nb2, src4_nb3, src5_nb2, src5_nb3, s_base, s_slot_bytes, n_head, head_dim, n_group, n_tok, K);
} else if (d_state == 256) { // Falcon-H1
constexpr int threads = 256;
constexpr int num_warps = threads/WARP_SIZE;
@@ -281,7 +285,7 @@ static void ssm_scan_f32_cuda(const float * src0, const float * src1, const floa
ggml_cuda_kernel_launch(ssm_scan_f32_group<256/WARP_SIZE, 256>, launch_params,
src0, src1, src2, src3, src4, src5, src6, dst,
src0_nb2, src0_nb3, src1_nb2, src1_nb3, src2_nb1, src2_nb2, src3_nb1,
src4_nb2, src4_nb3, src5_nb2, src5_nb3, s_off, n_head, head_dim, n_group, n_tok, K);
src4_nb2, src4_nb3, src5_nb2, src5_nb3, s_base, s_slot_bytes, n_head, head_dim, n_group, n_tok, K);
} else {
GGML_ABORT("doesn't support d_state!=(96, 128 or 256).");
}
@@ -570,12 +574,13 @@ __global__ void ssm_ssd_scale_state_kernel(
}
// Copy initial state from src0[ids[s]] into s_cur for each sequence.
// src0 and s_cur can alias when the state is written straight into the cache.
// Grid: (ceil(d_state * head_dim * n_head / BLOCK), n_seqs)
template <int BLOCK_SIZE>
__global__ void ssm_ssd_init_state_kernel(
const float * __restrict__ src0, // {d_state, head_dim, n_head, n_rs}
const float * src0, // {d_state, head_dim, n_head, n_rs}
const int32_t * __restrict__ ids, // {n_seqs}
float * __restrict__ s_cur, // {d_state, head_dim, n_head, n_seqs}
float * s_cur, // {d_state, head_dim, n_head, n_seqs}
const int state_size, // d_state * head_dim * n_head
const int64_t s0_stride_seq) { // elements between state rows
const int s = blockIdx.y;
@@ -599,7 +604,8 @@ static void ssm_scan_ssd_f32_cuda(
const int A_stride, // A (src3) stride between heads
const int B_stride_tok, const int B_stride_seq, // B (src4) strides
const int C_stride_tok, const int C_stride_seq, // C (src5) strides
const int64_t s_off, const int64_t d_state, const int64_t head_dim,
float * s_cur, // state: dst state tail, or the cache when fused
const int64_t d_state, const int64_t head_dim,
const int64_t n_head, const int64_t n_group, const int64_t n_tok, const int64_t n_seq) {
cudaStream_t stream = ctx.stream();
@@ -625,7 +631,6 @@ static void ssm_scan_ssd_f32_cuda(
matmul_t * X_dt = X_dt_buf.get();
matmul_t * B_weighted = B_w_buf.get();
float * C_scaled = C_s_buf.get();
float * s_cur = (float *)((char *)dst_d + s_off); // write state directly to dst
// Step 1: softplus(dt) and parallel prefix sum over full sequence
{
@@ -780,7 +785,8 @@ static void ssm_scan_ssd_f32_cuda(
}
#endif // !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA)
void ggml_cuda_op_ssm_scan(ggml_backend_cuda_context & ctx, ggml_tensor * dst) {
static void ggml_cuda_op_ssm_scan_impl(ggml_backend_cuda_context & ctx, ggml_tensor * dst,
const ggml_cuda_ssm_scan_fused_cache * cache) {
const struct ggml_tensor * src0 = dst->src[0]; // s
const struct ggml_tensor * src1 = dst->src[1]; // x
const struct ggml_tensor * src2 = dst->src[2]; // dt
@@ -864,12 +870,21 @@ void ggml_cuda_op_ssm_scan(ggml_backend_cuda_context & ctx, ggml_tensor * dst) {
(int)(src3->nb[1] / sizeof(float)),
(int)(src4->nb[2] / sizeof(float)), (int)(src4->nb[3] / sizeof(float)),
(int)(src5->nb[2] / sizeof(float)), (int)(src5->nb[3] / sizeof(float)),
s_off, nc, nr, nh, ng, n_t, n_s);
cache ? cache->data : (float *) ((char *) dst_d + s_off), nc, nr, nh, ng, n_t, n_s);
return;
}
#endif
ssm_scan_f32_cuda(src0_d, src1_d, src2_d, src3_d, src4_d, src5_d, src6_d, dst_d,
src0->nb[2], src0->nb[3], src1->nb[2], src1->nb[3], src2->nb[1], src2->nb[2],
src3->nb[1], src4->nb[2], src4->nb[3], src5->nb[2], src5->nb[3],
s_off, nc, nr, nh, ng, n_t, n_s, K, stream);
s_off, nc, nr, nh, ng, n_t, n_s, K, cache, stream);
}
void ggml_cuda_op_ssm_scan(ggml_backend_cuda_context & ctx, ggml_tensor * dst) {
ggml_cuda_op_ssm_scan_impl(ctx, dst, nullptr);
}
void ggml_cuda_op_ssm_scan_fused_cache(ggml_backend_cuda_context & ctx, ggml_tensor * dst,
ggml_cuda_ssm_scan_fused_cache cache) {
ggml_cuda_op_ssm_scan_impl(ctx, dst, &cache);
}
+10
View File
@@ -1,3 +1,13 @@
#include "common.cuh"
// fused-kernel recurrent-state output; strides in elements (per-seq stride is always the state row size, set in-kernel)
struct ggml_cuda_ssm_scan_fused_cache {
float * data; // rollback slot 0
int64_t slot_stride; // between rollback slots
};
void ggml_cuda_op_ssm_scan(ggml_backend_cuda_context & ctx, ggml_tensor * dst);
// same op, but writes the state snapshot(s) into the cache instead of dst (see ggml_cuda_try_ssm_scan_cache_fusion)
void ggml_cuda_op_ssm_scan_fused_cache(ggml_backend_cuda_context & ctx, ggml_tensor * dst,
ggml_cuda_ssm_scan_fused_cache cache);
+1 -1
View File
@@ -107,7 +107,7 @@ static __device__ __forceinline__ float op_ceil(float x) {
}
static __device__ __forceinline__ float op_round(float x) {
return round(x);
return roundf(x);
}
static __device__ __forceinline__ float op_trunc(float x) {
+19 -6
View File
@@ -280,7 +280,8 @@ static ADRENO_GPU_GEN get_adreno_gpu_gen(const char *device_name) {
strstr(device_name, "613") || strstr(device_name, "615") ||
strstr(device_name, "616") || strstr(device_name, "618") ||
strstr(device_name, "619") || strstr(device_name, "620") ||
strstr(device_name, "630") || strstr(device_name, "640") ||
strstr(device_name, "623") || strstr(device_name, "630") ||
strstr(device_name, "640") ||
strstr(device_name, "642") || strstr(device_name, "643") ||
strstr(device_name, "644") || strstr(device_name, "650") ||
strstr(device_name, "660") || strstr(device_name, "663") ||
@@ -863,7 +864,8 @@ struct ggml_backend_opencl_context {
cl_kernel kernel_set_rows_q4_0_soa_i64, kernel_set_rows_q4_0_soa_i32;
cl_kernel kernel_rope_norm_f32, kernel_rope_norm_f16, kernel_rope_neox_f32, kernel_rope_neox_f16;
cl_kernel kernel_rope_multi_f32, kernel_rope_multi_f16, kernel_rope_vision_f32, kernel_rope_vision_f16;
cl_kernel kernel_cpy_f16_f16, kernel_cpy_f16_f32, kernel_cpy_f32_f16, kernel_cpy_f32_f32, kernel_cpy_f32_f32_pack, kernel_cpy_i32_i32;
cl_kernel kernel_cpy_f16_f16, kernel_cpy_f16_f32, kernel_cpy_f32_f16, kernel_cpy_f32_f32, kernel_cpy_i32_i32;
cl_kernel kernel_cpy_f32_f32_pack = nullptr;
cl_kernel kernel_cpy_f32_f32_flat = nullptr;
cl_kernel kernel_mul_mat_f32_f32;
cl_kernel kernel_mul_mat_f16_f16;
@@ -1604,14 +1606,18 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
#else
const std::string kernel_src = read_file("cpy.cl");
#endif
cl_program prog =
build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts);
const bool no_cpy_pack = backend_ctx->adreno_gen == ADRENO_GPU_GEN::A6X;
cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(),
no_cpy_pack ? compile_opts + " -DGGML_CL_NO_CPY_PACK" : compile_opts);
CL_CHECK((backend_ctx->kernel_cpy_f16_f16 = clCreateKernel(prog, "kernel_cpy_f16_f16", &err), err));
CL_CHECK((backend_ctx->kernel_cpy_f16_f32 = clCreateKernel(prog, "kernel_cpy_f16_f32", &err), err));
CL_CHECK((backend_ctx->kernel_cpy_f32_f16 = clCreateKernel(prog, "kernel_cpy_f32_f16", &err), err));
CL_CHECK((backend_ctx->kernel_cpy_f32_f32 = clCreateKernel(prog, "kernel_cpy_f32_f32", &err), err));
CL_CHECK((backend_ctx->kernel_cpy_f32_f32_pack = clCreateKernel(prog, "kernel_cpy_f32_f32_pack", &err), err));
if (!no_cpy_pack) {
CL_CHECK((backend_ctx->kernel_cpy_f32_f32_pack = clCreateKernel(prog, "kernel_cpy_f32_f32_pack", &err), err));
}
{ // optional: without it ggml_cl_cpy keeps the row-mapped kernel
cl_int err_flat = CL_SUCCESS;
cl_kernel k = clCreateKernel(prog, "kernel_cpy_f32_f32_flat", &err_flat);
@@ -3770,6 +3776,9 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
if (backend_ctx->has_vector_subgroup_broadcast) {
CL_gemv_compile_opts += " -DVECTOR_SUB_GROUP_BROADCAST ";
}
if (backend_ctx->adreno_gen == ADRENO_GPU_GEN::A6X) {
CL_gemv_compile_opts += " -DGGML_CL_A6X_CONSTFOLD_FIX";
}
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src_CL_gemv_general {
@@ -4306,6 +4315,9 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
if (backend_ctx->has_vector_subgroup_broadcast) {
CL_gemv_compile_opts += " -DVECTOR_SUB_GROUP_BROADCAST ";
}
if (backend_ctx->adreno_gen == ADRENO_GPU_GEN::A6X) {
CL_gemv_compile_opts += " -DGGML_CL_A6X_CONSTFOLD_FIX";
}
// Opt-in: dequant-once-per-block mc3 verify GEMV (factors q4_K dequant
// out of the 3-column loop; byte-identical, lower spill). A/B vs the
// shipped inline mc3 in the same binary.
@@ -28335,7 +28347,8 @@ static void ggml_cl_cpy(ggml_backend_t backend, const ggml_tensor * src0, const
kernel = backend_ctx->kernel_cpy_f32_f16;
break;
case GGML_TYPE_F32:
kernel = ne00 < 32 ? backend_ctx->kernel_cpy_f32_f32_pack
kernel = (ne00 < 32 && backend_ctx->kernel_cpy_f32_f32_pack)
? backend_ctx->kernel_cpy_f32_f32_pack
: backend_ctx->kernel_cpy_f32_f32;
break;
default:
+2
View File
@@ -183,6 +183,7 @@ kernel void kernel_cpy_f32_f32(
}
}
#ifndef GGML_CL_NO_CPY_PACK
kernel void kernel_cpy_f32_f32_pack(
global float * src0,
ulong offset0,
@@ -241,6 +242,7 @@ kernel void kernel_cpy_f32_f32_pack(
dst_data[i00] = src[0];
}
}
#endif // GGML_CL_NO_CPY_PACK
kernel void kernel_cpy_i32_i32(
global int * src0,
@@ -7,6 +7,14 @@
#define REQD_SUBGROUP_SIZE_64 __attribute__((qcom_reqd_sub_group_size("half")))
#endif
// A6X compiler incorrectly constant-folds get_local_size() results;
// force runtime materialization via a no-op ALU round-trip.
#ifdef GGML_CL_A6X_CONSTFOLD_FIX
#define MATERIALIZE_WG(x) do { (x) *= 2u; if ((x) > 1u) (x) /= 2u; } while(0)
#else
#define MATERIALIZE_WG(x)
#endif
// assume
#define QK4_0 32
#define N_SIMDGROUP 4
@@ -327,6 +335,7 @@ __kernel void kernel_gemv_noshuffle_q4_0_f32_mc3(
uint BLOCK_STRIDE_A = N_SIMDGROUP * M; // = 4 * M (N_SIMDGROUP is the #define 4)
uint COL_STRIDE = K / 4; // float4 pixels per activation column
uint nsg = get_local_size(1); // runtime K-split (4 default, 8 small-M)
MATERIALIZE_WG(nsg);
__private uint4 regA_hi, regA_lo;
__private half2 regS;
@@ -11,6 +11,14 @@
#define NSUBGROUPS 4
#define SUBGROUP_SIZE 64
// A6X compiler incorrectly constant-folds get_local_size() results;
// force runtime materialization via a no-op ALU round-trip.
#ifdef GGML_CL_A6X_CONSTFOLD_FIX
#define MATERIALIZE_WG(x) do { (x) *= 2u; if ((x) > 1u) (x) /= 2u; } while(0)
#else
#define MATERIALIZE_WG(x)
#endif
// scales are transposed: consecutive codes of a row are `stride` apart
inline void get_scale_min_k4(
int j,
@@ -233,6 +241,7 @@ kernel void kernel_gemv_noshuffle_q4_k_f32(
// K-split (more waves/SP -> latency hiding) while large-M keeps 4. The
// physical weight layout stride below is INDEPENDENT of this (see BLOCK_STRIDE_A).
uint nsg = get_local_size(1);
MATERIALIZE_WG(nsg);
uint K = ne00;
uint M = ne01;
@@ -400,6 +409,7 @@ kernel void kernel_gemv_noshuffle_q4_k_f32_glu(
uint gid = get_global_id(0);
ushort slid = get_sub_group_local_id();
uint nsg = get_local_size(1);
MATERIALIZE_WG(nsg);
uint K = ne00;
uint M = ne01;
@@ -514,6 +524,7 @@ kernel void kernel_gemv_noshuffle_q4_k_f32_splitk(
uint gid = get_global_id(0);
ushort slid = get_sub_group_local_id();
uint nsg = get_local_size(1);
MATERIALIZE_WG(nsg);
uint ksplit = get_num_groups(1);
uint kslice = get_group_id(1);
+15
View File
@@ -101,6 +101,13 @@ patches = {
)],
}
# local changes too large for the replacements above, kept as diffs and applied with git apply
patch_files = [
# backport of the fix for the stack overflow on deeply nested values (nlohmann/json#5387)
# TODO: remove once nlohmann/json releases a version newer than 3.12.0
"vendor/nlohmann/json-deep-nesting.patch",
]
for url, filename in vendor.items():
print(f"downloading {url} to {filename}") # noqa: NP100
urllib.request.urlretrieve(url, filename)
@@ -117,6 +124,14 @@ for filename, replacements in patches.items():
with open(filename, "w", encoding="utf-8", newline="") as f:
f.write(content)
for patch_file in patch_files:
print(f"applying {patch_file}") # noqa: NP100
try:
subprocess.check_call(["git", "apply", patch_file])
except subprocess.CalledProcessError:
print(f"Error: cannot apply {patch_file}, upstream code has changed") # noqa: NP100
sys.exit(1)
print("Splitting httplib.h...") # noqa: NP100
try:
subprocess.check_call([
+58 -25
View File
@@ -2471,19 +2471,57 @@ ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd, float to
auto inp = std::make_unique<llm_graph_input_embd>(n_embd_inp);
inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, ubatch.n_tokens);
cb(inp->tokens, "inp_tokens", -1);
ggml_set_input(inp->tokens);
res->t_inp_tokens = inp->tokens;
// mixed path (ubatch.is_mixed()): set_rows the token rows into a copy of the embd rows, with its own inputs as select branches must not share tensors
// TODO: use inp->tokens and inp->embd once ggml_build_forward_select allows it
const bool has_mixed = llm_arch_supports_mixed_batch(arch) && cparams.ctx_type == LLAMA_CONTEXT_TYPE_DEFAULT;
inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_embd_inp, ubatch.n_tokens);
cb(inp->embd, "inp_embd", -1);
ggml_set_input(inp->embd);
const int64_t n_tok_rows = has_mixed ? llm_graph_n_tok_rows(ubatch) : 0;
// token embeddings with lora and padding
auto build_tok = [&](ggml_tensor * ids) {
ggml_tensor * cur = ggml_get_rows(ctx0, tok_embd, ids);
// we have 3 standard paths to produce the input embeddings for the first layer:
// - embd0: extract from the token embeddings weight (`tok_embd`) using the input token ids
// - embd1: pass raw embeddings, skipping the `tok_embd`
// - embd2: mixed path of both tokens ids + raw embeddings (if supported)
ggml_tensor * embd0 = nullptr;
ggml_tensor * embd1 = nullptr;
ggml_tensor * embd2 = nullptr;
// construct the input tensors
{
inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, ubatch.n_tokens);
cb(inp->tokens, "inp_tokens", -1);
ggml_set_input(inp->tokens);
res->t_inp_tokens = inp->tokens;
inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_embd_inp, ubatch.n_tokens);
cb(inp->embd, "inp_embd", -1);
ggml_set_input(inp->embd);
if (has_mixed) {
inp->mixed_tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_tok_rows);
cb(inp->mixed_tokens, "inp_mixed_tokens", -1);
ggml_set_input(inp->mixed_tokens);
}
}
// the embeddings placeholders for the 3 paths
// we use ggml_build_forward_order to make the GET_ROWS ops stick at the beginning of the compute graph
// this way the embeddings remain in the host buffer, and the GET_ROWS run before any other computations
{
embd0 = ggml_get_rows(ctx0, tok_embd, inp->tokens);
ggml_build_forward_order(gf, embd0);
embd1 = inp->embd;
if (has_mixed) {
embd2 = ggml_get_rows(ctx0, tok_embd, inp->mixed_tokens);
ggml_build_forward_order(gf, embd2);
}
}
// helper for extracting token embeddings with lora and padding
// TODO: when lora is active, this is likely going to cause issues similar to https://github.com/ggml-org/llama.cpp/pull/30160
// need to add lora tests and refactor the logic to make the lora GET_ROWS go at the front of the graph
auto build_tok = [&](ggml_tensor * cur, ggml_tensor * ids) {
// apply lora for embedding tokens if needed
for (const auto & lora : *loras) {
llama_adapter_lora_weight * lw = lora.first->get_weight(tok_embd);
@@ -2514,21 +2552,15 @@ ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd, float to
std::array<ggml_tensor *, 3> inps = {};
// token embeddings path (ubatch.token != nullptr)
inps[0] = build_tok(inp->tokens);
inps[0] = build_tok(embd0, inp->tokens);
// vector embeddings path (ubatch.embd != nullptr)
inps[1] = inp->embd;
inps[1] = embd1;
assert(ggml_are_same_shape (inps[0], inps[1]));
assert(ggml_are_same_stride(inps[0], inps[1]));
// mixed path (ubatch.is_mixed()): set_rows the token rows into a copy of the embd rows, with its own inputs as select branches must not share tensors
// TODO: use inp->tokens and inp->embd once ggml_build_forward_select allows it
const bool has_mixed = llm_arch_supports_mixed_batch(arch) && cparams.ctx_type == LLAMA_CONTEXT_TYPE_DEFAULT;
if (has_mixed) {
const int64_t n_tok_rows = llm_graph_n_tok_rows(ubatch);
inp->mixed_tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_tok_rows);
cb(inp->mixed_tokens, "inp_mixed_tokens", -1);
ggml_set_input(inp->mixed_tokens);
inp->mixed_slots = ggml_new_tensor_1d(ctx0, GGML_TYPE_I64, n_tok_rows);
cb(inp->mixed_slots, "inp_mixed_slots", -1);
ggml_set_input(inp->mixed_slots);
@@ -2538,11 +2570,12 @@ ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd, float to
ggml_set_input(inp->mixed_embd);
// note: set_rows writes into its destination, so it gets a copy of the input
inps[2] = ggml_set_rows(ctx0, ggml_dup(ctx0, inp->mixed_embd), build_tok(inp->mixed_tokens), inp->mixed_slots);
}
ggml_tensor * embd_mixed = build_tok(embd2, inp->mixed_tokens);
inps[2] = ggml_set_rows(ctx0, ggml_dup(ctx0, inp->mixed_embd), embd_mixed, inp->mixed_slots);
assert(ggml_are_same_shape (inps[0], inps[1]));
assert(ggml_are_same_stride(inps[0], inps[1]));
assert(ggml_are_same_shape (inps[0], inps[2]));
assert(ggml_are_same_stride(inps[0], inps[2]));
}
const int idx = ubatch.is_mixed() ? 2 : ubatch.token ? 0 : 1;
+26 -18
View File
@@ -159,6 +159,14 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para
ggml_tensor * cur;
ggml_tensor * inpL;
// do the PLE first to guarantee it is done in the host buffer
// ref: https://github.com/ggml-org/llama.cpp/pull/30160
ggml_tensor * inp_per_layer = nullptr;
if (model.per_layer_tok_embd) {
inp_per_layer = build_inp_per_layer();
ggml_build_forward_expand(gf, inp_per_layer);
}
// important: do not normalize weights for raw embeddings input (i.e. encoded image emdeddings)
inpL = build_inp_embd(model.tok_embd, sqrtf(n_embd));
cb(inpL, "inp_scaled", -1);
@@ -171,10 +179,11 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para
ggml_tensor * inp_out_ids = build_inp_out_ids();
ggml_tensor * inp_per_layer = nullptr;
if (model.per_layer_tok_embd) {
inp_per_layer = build_inp_per_layer();
ggml_build_forward_expand(gf, inp_per_layer);
const float tok_embd_scale = sqrtf((float) n_embd_per_layer);
inp_per_layer = ggml_scale (ctx0, inp_per_layer, tok_embd_scale);
inp_per_layer = ggml_reshape_3d(ctx0, inp_per_layer, n_embd_per_layer, n_layer, inp_per_layer->ne[1]);
// inp_per_layer shape: [n_embd_per_layer, n_tokens, n_layer]
inp_per_layer = project_per_layer_inputs(inpL, inp_per_layer);
@@ -448,10 +457,13 @@ public:
llama_prefetch_rows(ple, ubatch->token, ubatch->n_tokens);
}
ggml_backend_tensor_set(tokens, ubatch->token, 0, ubatch->n_tokens * ggml_element_size(tokens));
} else if (prefetch) {
// [TAG_GEMMA4_IMG_PADDING]
} else {
const int32_t padding = 0;
llama_prefetch_rows(ple, &padding, 1);
if (prefetch) {
// [TAG_GEMMA4_IMG_PADDING]
llama_prefetch_rows(ple, &padding, 1);
}
ggml_backend_tensor_set(token0, &padding, 0, ggml_element_size(token0));
}
}
@@ -460,6 +472,7 @@ public:
}
ggml_tensor * tokens = nullptr;
ggml_tensor * token0 = nullptr;
const llama_model & model;
};
@@ -470,30 +483,25 @@ ggml_tensor * llama_model_gemma4::graph::build_inp_per_layer() {
auto inp = std::make_unique<llm_graph_input_gemma4_ple>(model);
ggml_tensor * inp_per_layer;
float tok_embd_scale = sqrtf((float) n_embd_per_layer);
// mixed ubatch: embd rows have token id 0, same padding row as below
// TODO: use ggml_build_forward_select
if (ubatch.token) {
inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, ubatch.n_tokens);
ggml_set_input(inp->tokens);
res->t_inp_tokens = inp->tokens;
inp_per_layer = ggml_get_rows (ctx0, model.per_layer_tok_embd, inp->tokens);
inp_per_layer = ggml_reshape_3d(ctx0, inp_per_layer, n_embd_per_layer, n_layer, n_tokens);
inp_per_layer = ggml_scale (ctx0, inp_per_layer, tok_embd_scale);
inp_per_layer = ggml_get_rows(ctx0, model.per_layer_tok_embd, inp->tokens);
cb(inp_per_layer, "inp_per_layer_selected", -1);
} else {
// [TAG_GEMMA4_IMG_PADDING]
// Multimodal embedding path: use padding token (ID=0) embedding
// TODO: verify if this is the correct behavior in transformers implementation
const int64_t embd_size = model.per_layer_tok_embd->ne[0]; // n_embd_per_layer * n_layer
// Extract and dequantize padding token embedding (row 0)
ggml_tensor * padding = ggml_view_1d(ctx0, model.per_layer_tok_embd, embd_size, 0);
inp_per_layer = ggml_cast (ctx0, padding, GGML_TYPE_F32);
inp_per_layer = ggml_scale(ctx0, inp_per_layer, tok_embd_scale);
// [TAG_GEMMA4_IMG_PADDING]
inp->token0 = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, 1);
ggml_set_input(inp->token0);
res->t_inp_tokens = inp->token0;
// Reshape to [n_embd_per_layer, n_layer, 1]
inp_per_layer = ggml_reshape_3d(ctx0, inp_per_layer, n_embd_per_layer, n_layer, 1);
inp_per_layer = ggml_get_rows(ctx0, model.per_layer_tok_embd, inp->token0);
cb(inp_per_layer, "inp_per_layer_multimodal", -1);
}
res->add_input(std::move(inp));
+11 -11
View File
@@ -405,10 +405,6 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa
int sections[4];
std::copy(std::begin(hparams.rope_sections), std::begin(hparams.rope_sections) + 4, sections);
ggml_tensor * inpL = build_inp_embd(model.tok_embd);
cb(inpL, "model.input_embed", -1);
ggml_build_forward_expand(gf, inpL);
auto * inp = build_inp_mem_hybrid();
// qwen4exp always builds llama_memory_hybrid_idx, so this downcast is safe
@@ -421,6 +417,17 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa
"the indexer cache must track the attention cache cell for cell");
}
ggml_tensor * ple_emb = nullptr;
if (hparams.ple_n_heads > 0) {
ple_emb = build_inp_ple(mctx_hyb);
// make sure ple_emb and build_inp_embd are in the same graph split
ggml_build_forward_expand(gf, ple_emb);
}
ggml_tensor * inpL = build_inp_embd(model.tok_embd);
cb(inpL, "model.input_embed", -1);
ggml_build_forward_expand(gf, inpL);
// the QSA layers share one set of k-pool inputs
// the CUDA lightning indexer takes 32 or 64 heads, QSA has a few, so it scores with plain ops
llm_graph_input_kpool * inp_kpool = nullptr;
@@ -431,13 +438,6 @@ llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_pa
ggml_tensor * inp_pos = build_inp_pos();
ggml_tensor * inp_out_ids = build_inp_out_ids();
ggml_tensor * ple_emb = nullptr;
if (hparams.ple_n_heads > 0) {
ple_emb = build_inp_ple(mctx_hyb);
// make sure ple_emb and build_inp_embd are in the same graph split
ggml_build_forward_expand(gf, ple_emb);
}
// the wide residual starts as hc identical copies of the embedding
ggml_tensor * res_hc = ggml_repeat_4d(ctx0,
ggml_reshape_3d(ctx0, inpL, n_embd, 1, n_tokens),
+108
View File
@@ -4768,6 +4768,104 @@ struct test_ssm_scan_rollback : public test_case {
}
};
// GGML_OP_SSM_SCAN + GGML_OP_CPY (recurrent cache fusion)
struct test_ssm_scan_cache_fusion : public test_case {
const ggml_type type;
const int64_t d_state;
const int64_t head_dim;
const int64_t n_head;
const int64_t n_group;
const int64_t n_seq_tokens;
const int64_t n_seqs;
const int64_t K; // snapshot slot count (1 = final state only)
ggml_tensor * cpy_node = nullptr;
std::string vars() override {
return VARS_TO_STR8(type, d_state, head_dim, n_head, n_group, n_seq_tokens, n_seqs, K);
}
test_ssm_scan_cache_fusion(ggml_type type = GGML_TYPE_F32,
int64_t d_state = 128, int64_t head_dim = 64, int64_t n_head = 16, int64_t n_group = 2,
int64_t n_seq_tokens = 4, int64_t n_seqs = 1, int64_t K = 4)
: type(type), d_state(d_state), head_dim(head_dim), n_head(n_head), n_group(n_group),
n_seq_tokens(n_seq_tokens), n_seqs(n_seqs), K(K) {}
ggml_tensor * build_graph(ggml_context * ctx) override {
const int64_t D = d_state * head_dim * n_head;
const int64_t n_written = std::min<int64_t>(n_seq_tokens, K);
// more cache rows per slot than seqs and a non-zero first row, so a wrong slot stride or offset shows up
const int64_t mem_size = n_seqs + 2;
const int64_t kv_head = 1;
ggml_tensor * s = ggml_new_tensor_4d(ctx, type, d_state, head_dim, n_head, n_seqs);
ggml_tensor * x = ggml_new_tensor_4d(ctx, type, head_dim, n_head, n_seq_tokens, n_seqs);
ggml_tensor * dt = ggml_new_tensor_3d(ctx, type, n_head, n_seq_tokens, n_seqs);
ggml_tensor * A = ggml_new_tensor_2d(ctx, type, 1, n_head);
ggml_tensor * B = ggml_new_tensor_4d(ctx, type, d_state, n_group, n_seq_tokens, n_seqs);
ggml_tensor * C = ggml_new_tensor_4d(ctx, type, d_state, n_group, n_seq_tokens, n_seqs);
ggml_tensor * ids = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, n_seqs);
ggml_set_name(A, "A");
ggml_set_name(ids, "ids");
ggml_tensor * out = ggml_ssm_scan(ctx, s, x, dt, A, B, C, ids, K);
ggml_set_name(out, "ssm_out");
// snapshot tail view [D, n_seqs, n_written]
ggml_tensor * src = ggml_view_3d(ctx, out,
D, n_seqs, n_written,
ggml_row_size(out->type, D),
ggml_row_size(out->type, D * n_seqs),
ggml_row_size(out->type, ggml_nelements(x)));
// recurrent cache view [D, n_seqs, n_written]
ggml_tensor * cache = ggml_new_tensor_2d(ctx, type, D, mem_size * n_written);
ggml_set_name(cache, "cache");
ggml_tensor * dst = ggml_view_3d(ctx, cache,
D, n_seqs, n_written,
cache->nb[1],
mem_size * cache->nb[1],
kv_head * cache->nb[1]);
ggml_tensor * cpy = ggml_cpy(ctx, src, dst);
ggml_set_name(cpy, "ssm_cache_cpy");
cpy_node = cpy;
// read the cpy output so that neither the scan nor the cpy is the graph output (cont, since the cache view is strided)
ggml_tensor * res = ggml_sum(ctx, ggml_cont(ctx, cpy));
return res;
}
std::string op_desc(ggml_tensor * t) override {
GGML_UNUSED(t);
return "SSM_SCAN_CACHE_FUSION";
}
bool run_whole_graph() override { return true; }
std::vector<ggml_tensor *> fusion_test_nodes() override { return { cpy_node }; }
void initialize_tensors(ggml_context * ctx) override {
for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != nullptr; t = ggml_get_next_tensor(ctx, t)) {
if (ggml_is_view_op(t->op)) { continue; }
if (strcmp(t->name, "ids") == 0) {
std::vector<int32_t> data(t->ne[0]);
for (int i = 0; i < t->ne[0]; i++) {
data[i] = i;
}
ggml_backend_tensor_set(t, data.data(), 0, t->ne[0] * sizeof(int32_t));
} else if (strcmp(t->name, "A") == 0) {
init_tensor_uniform(t, -1.0f, -0.5f);
} else if (strcmp(t->name, "cache") == 0) {
init_tensor_uniform(t, 0.0f, 0.0f);
} else {
init_tensor_uniform(t);
}
}
}
};
// GGML_OP_RWKV_WKV6
struct test_rwkv_wkv6 : public test_case {
const ggml_type type;
@@ -10445,6 +10543,16 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
test_cases.emplace_back(new test_ssm_scan(GGML_TYPE_F32, 128, 64, 16, 2, 128, 2)); // SSD multi-chunk, no tail (exercises the chunk-to-chunk state handoff)
test_cases.emplace_back(new test_ssm_scan(GGML_TYPE_F32, 128, 64, 16, 2, 128, 2, false, /*K=*/1, /*weak_decay=*/true)); // SSD multi-chunk, carried state not numerically negligible
// ssm_scan + cache cpy fusion
test_cases.emplace_back(new test_ssm_scan_cache_fusion(GGML_TYPE_F32, 128, 64, 16, 2, 4, 1, 4));
test_cases.emplace_back(new test_ssm_scan_cache_fusion(GGML_TYPE_F32, 128, 64, 16, 2, 1, 1, 4)); // n_seq_tokens < K
test_cases.emplace_back(new test_ssm_scan_cache_fusion(GGML_TYPE_F32, 128, 64, 16, 2, 8, 1, 3)); // n_seq_tokens > K
test_cases.emplace_back(new test_ssm_scan_cache_fusion(GGML_TYPE_F32, 96, 64, 16, 2, 4, 1, 4));
test_cases.emplace_back(new test_ssm_scan_cache_fusion(GGML_TYPE_F32, 256, 64, 8, 2, 4, 1, 4));
test_cases.emplace_back(new test_ssm_scan_cache_fusion(GGML_TYPE_F32, 128, 64, 16, 2, 1, 1, 1)); // K == 1, final state only
test_cases.emplace_back(new test_ssm_scan_cache_fusion(GGML_TYPE_F32, 128, 64, 16, 2, 4, 1, 1));
test_cases.emplace_back(new test_ssm_scan_cache_fusion(GGML_TYPE_F32, 128, 64, 16, 2, 300, 1, 1)); // K == 1, SSD path over two chunks
test_cases.emplace_back(new test_rwkv_wkv6(GGML_TYPE_F32, 32, 64, 1, 1));
test_cases.emplace_back(new test_rwkv_wkv6(GGML_TYPE_F32, 32, 64, 32, 1));
test_cases.emplace_back(new test_rwkv_wkv6(GGML_TYPE_F32, 32, 64, 32, 4));
+1 -1
View File
@@ -35,7 +35,7 @@ options:
--progress print test progress indicators
--no-warmup skip warmup runs before benchmarking
-fitt, --fit-target <MiB> fit model to device memory with this margin per device in MiB (default: off)
-fitc, --fit-ctx <n> minimum ctx size for --fit-target (default: 4096)
-fitc, --fit-ctx <n> minimum ctx size for --fit-target (default: 0)
-rpc, --rpc <rpc_servers> register RPC devices (comma separated)
test parameters:
+3 -2
View File
@@ -441,7 +441,7 @@ static void print_usage(int /* argc */, char ** argv) {
printf(" --progress print test progress indicators\n");
printf(" --no-warmup skip warmup runs before benchmarking\n");
printf(" -fitt, --fit-target <MiB> fit model to device memory with this margin per device in MiB (default: off)\n");
printf(" -fitc, --fit-ctx <n> minimum ctx size for --fit-target (default: 4096)\n");
printf(" -fitc, --fit-ctx <n> minimum ctx size for --fit-target (default: 0)\n");
if (llama_supports_rpc()) {
printf(" -rpc, --rpc <rpc_servers> register RPC devices (comma separated)\n");
}
@@ -2348,7 +2348,8 @@ int llama_bench(int argc, char ** argv) {
std::vector<size_t> margins(llama_max_devices(), inst.fit_target * 1024 * 1024);
uint32_t n_ctx_needed = inst.n_prompt + inst.n_gen + inst.n_depth;
// fit at least the requested minimum context size, not just the tokens the benchmark processes
uint32_t n_ctx_needed = std::max<uint32_t>(inst.n_prompt + inst.n_gen + inst.n_depth, inst.fit_min_ctx);
cparams.n_ctx = std::max(cparams.n_ctx, n_ctx_needed);
common_fit_params(inst.model.c_str(), &mparams, &cparams,
+10 -3
View File
@@ -540,6 +540,7 @@ void server_model_meta::update_args(common_preset_context & ctx_preset, std::str
void server_model_meta::update_caps(const common_params & base) {
// reset to the default so a failed refresh cannot keep old values
architecture = server_model_architecture_json(false, false, false, {"text"});
n_ctx_train = 0;
// resolve the model file offline; do not download
common_params params;
@@ -564,10 +565,12 @@ void server_model_meta::update_caps(const common_params & base) {
return;
}
// read the output modalities from the GGUF metadata
// read the output modalities and the trained context from the GGUF metadata
std::vector<std::string> output_modalities = {"text"};
if (!params.model.path.empty()) {
output_modalities = server_model_output_modalities(common_get_decision_type(params.model.path));
const common_gguf_info info = common_get_gguf_info(params.model.path);
output_modalities = server_model_output_modalities(info.decision_type);
n_ctx_train = info.n_ctx_train;
}
bool inp_image = false;
@@ -2122,9 +2125,13 @@ void server_models_routes::init_routes() {
{"source", server_model_source_to_string(meta.source)},
{"can_remove", meta.source == SERVER_MODEL_SOURCE_CACHE},
// {"need_download", meta.need_download},
// TODO: add other fields, may require reading GGUF metadata
// TODO: add other fields from the GGUF metadata
};
if (meta.n_ctx_train > 0) {
model_info["context_length"] = meta.n_ctx_train;
}
// merge with loaded_info from the child process if available
if (meta.is_running()) {
for (auto it = meta.loaded_info.begin(); it != meta.loaded_info.end(); ++it) {
+1
View File
@@ -86,6 +86,7 @@ struct server_model_meta {
int stop_timeout = 0; // seconds to wait before force-killing the model instance during shutdown
bool hidden = false; // hidden from GET /models, but still accept if requested
json architecture = server_model_architecture_json(false, false, false, {"text"});
uint32_t n_ctx_train = 0; // trained context, read from the GGUF metadata; 0 when unknown
bool is_ready() const {
return status == SERVER_MODEL_STATUS_LOADED;
@@ -12,37 +12,38 @@
}
let { isFav, option, revealOnHover = true }: Props = $props();
// the favorite heart stays visible, so the reveal applies to the other icons
const revealClass =
'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 [@media(pointer:coarse)]:pointer-events-auto [@media(pointer:coarse)]:opacity-100';
</script>
<div
class={[
'flex items-center justify-center gap-1 max-md:gap-2.5',
revealOnHover
? 'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 [@media(pointer:coarse)]:pointer-events-auto [@media(pointer:coarse)]:opacity-100'
: ''
]}
class="flex items-center justify-center gap-1 max-md:gap-2.5"
onclick={(event) => event.stopPropagation()}
onkeydown={(event) => event.stopPropagation()}
role="presentation"
>
<ActionIcon
class="h-5 w-5 hover:text-foreground"
icon={Info}
iconSize="h-4 w-4"
onclick={() =>
// a phone has no manager: its information dialog takes the icon
deviceStore.isMobile
? uiStore.openModelInformation(option)
: uiStore.openModelsManager(option.id)}
tooltip="Manage model"
tooltipAsTitle
/>
<span class={revealOnHover ? revealClass : ''}>
<ActionIcon
class="h-5 w-5 hover:text-foreground"
icon={Info}
iconSize="h-4 w-4"
onclick={() =>
// a phone has no manager: its information dialog takes the icon
deviceStore.isMobile
? uiStore.openModelInformation(option)
: uiStore.openModelsManager(option.id)}
tooltip="Manage model"
tooltipAsTitle
/>
</span>
{#if isFav}
<span class="flex h-5 w-5 items-center justify-center">
<span class="flex group-hover:hidden [@media(pointer:coarse)]:hidden">
<ActionIcon
class="h-5 w-5 text-rose-500 hover:text-foreground"
class="h-5 w-5 text-rose-500"
icon={Heart}
iconSize="h-4 w-4"
onclick={() => modelsStore.toggleFavorite(option.model)}
@@ -63,13 +64,15 @@
</span>
</span>
{:else}
<ActionIcon
class="h-5 w-5 hover:text-foreground"
icon={Heart}
iconSize="h-4 w-4"
onclick={() => modelsStore.toggleFavorite(option.model)}
tooltip="Add to favorites"
tooltipAsTitle
/>
<span class={revealOnHover ? revealClass : ''}>
<ActionIcon
class="h-5 w-5 hover:text-foreground"
icon={Heart}
iconSize="h-4 w-4"
onclick={() => modelsStore.toggleFavorite(option.model)}
tooltip="Add to favorites"
tooltipAsTitle
/>
</span>
{/if}
</div>
@@ -16,7 +16,7 @@
import type { ModelOption } from '$lib/types/models';
import { filterModelOptions } from '$lib/utils';
import { type Snippet, untrack } from 'svelte';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { SvelteMap } from 'svelte/reactivity';
interface Props {
class?: string;
@@ -170,16 +170,29 @@
if (remaining.length > 0) rest.push({ ...entry, base: remaining[0], quants: remaining });
}
const claimed = new SvelteSet<string>();
const favorites = rest.filter((entry) =>
entry.quants.some((q) => modelsStore.favoriteModelIds.has(q.model))
);
// a favorite quant stands on its own, listed flat like a loaded one, and the
// quants left behind stay with their repo in the local block
const favorites: ModelQuantGroup[] = [];
const localRest: ModelQuantGroup[] = [];
for (const entry of favorites) claimed.add(entry.key);
for (const entry of rest) {
for (const quant of entry.quants) {
if (modelsStore.favoriteModelIds.has(quant.model)) {
favorites.push({ ...entry, base: quant, key: quant.id, quants: [quant] });
}
}
const { hidden, local } = splitHiddenQuants(
rest.filter((entry) => !claimed.has(entry.key)),
(option) => modelsStore.isHidden(option.id)
const remaining = entry.quants.filter(
(quant) => !modelsStore.favoriteModelIds.has(quant.model)
);
if (remaining.length > 0) {
localRest.push({ ...entry, base: remaining[0], quants: remaining });
}
}
const { hidden, local } = splitHiddenQuants(localRest, (option) =>
modelsStore.isHidden(option.id)
);
const ordered: ModelsTableGroup[] = [];
// loaded models lead the table, then favorites, then the local block
@@ -7,7 +7,7 @@
import ModelsManagerStatusCell from './ModelsManagerStatusCell.svelte';
import { modelRowActions } from './row-actions';
import { configuredContext, downloadProgressFor } from './utils';
import { MoreHorizontal } from '@lucide/svelte';
import { Heart, MoreHorizontal } from '@lucide/svelte';
import { DropdownMenuActions } from '$lib/components/app';
import { MODEL_ROW_GRID_CLASS, MODEL_ROW_TRAILING_CELL_CLASS } from '$lib/constants';
import { ModelRowDownloadState } from '$lib/enums';
@@ -74,6 +74,13 @@
<!-- a phone has no width for the modality icons, the id needs it more -->
<ModelCapabilities hideModalities={deviceStore.isMobile} {option} />
{#if favorite}
<!-- the heart is decorative, the button's text carries the state -->
<Heart aria-hidden="true" class="h-3.5 w-3.5 shrink-0 text-rose-500" />
<span class="sr-only">favorited</span>
{/if}
</span>
</button>
@@ -7,8 +7,7 @@
hasActiveFilters,
modelContextLength,
type ModelQuantGroup,
type ModelsTableGroup,
statusRank
type ModelsTableGroup
} from './utils';
import {
ArrowDown,
@@ -155,10 +154,6 @@
return (modelContextLength(a) ?? 0) - (modelContextLength(b) ?? 0);
case ModelsTableSortKey.NAME:
return a.model.localeCompare(b.model);
case ModelsTableSortKey.STATUS:
// a running model leads, then one that is being worked on (loading,
// sleeping), then the rest; the reported status sorts the row's own cell
return statusRank(b) - statusRank(a);
default:
return 0;
}
@@ -357,9 +352,7 @@
{@render sortHeader(ModelsTableSortKey.CONTEXT, 'Context')}
</span>
<span class="justify-self-center max-md:hidden">
{@render sortHeader(ModelsTableSortKey.STATUS, 'Status')}
</span>
<span class="justify-self-center max-md:hidden">Status</span>
<span class="text-center max-md:hidden">Actions</span>
</div>
@@ -1,10 +1,5 @@
import { LOCAL_BACKEND_ID, type ModalityKey } from '$lib/constants';
import {
ModelCapability,
ModelGroupKind,
ModelsTableGroupKind,
ServerModelStatus
} from '$lib/enums';
import { ModelCapability, ModelGroupKind, ModelsTableGroupKind } from '$lib/enums';
import { HuggingFaceService, ModelsService } from '$lib/services';
import { modelsStore } from '$lib/stores';
import type { ModelDownloadEntry, ModelDownloadProgress, ModelOption } from '$lib/types/models';
@@ -27,20 +22,6 @@ export function hasActiveFilters(
return contextLimit > 0 || modalities.length > 0 || capabilities.length > 0;
}
/**
* Order a status sorts behind: loaded first, then a model being worked on
* (loading, sleeping), then the rest.
*/
export function statusRank(option: ModelOption): number {
const status = modelsStore.getModelStatus(option.model);
if (modelsStore.isModelRunning(option.model)) return 2;
if (status === ServerModelStatus.LOADING || status === ServerModelStatus.SLEEPING) return 1;
return 0;
}
/** Byte counts of a tracked download: live while it runs, frozen while paused. */
export function downloadProgressFor(repoWithTag: string): ModelDownloadProgress | null {
return (
@@ -177,7 +177,7 @@
<DropdownMenu.Content
align="end"
class="w-full md:min-w-80 md:w-112 max-w-[calc(100vw-2rem)] p-0! max-h-[min(40rem,calc(var(--bits-dropdown-menu-content-available-height)-1rem))]"
class="w-full md:min-w-80 md:w-md max-w-[calc(100vw-2rem)] p-0! max-h-[min(40rem,calc(var(--bits-dropdown-menu-content-available-height)-1rem))]"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<DropdownMenuSearchable
@@ -17,7 +17,7 @@
<DropdownMenuPrimitive.Content
bind:ref
class={cn(
'z-50 max-h-[calc(var(--bits-dropdown-menu-content-available-height)-1rem)] min-w-[8rem] origin-(--bits-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1.5 text-popover-foreground shadow-md outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:fill-mode-forwards data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-border/20',
'z-50 max-h-[calc(var(--bits-dropdown-menu-content-available-height)-1rem)] min-w-[8rem] origin-(--bits-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover p-1.5 text-popover-foreground shadow-md outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:fill-mode-forwards data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-border/20',
className
)}
data-slot="dropdown-menu-content"
@@ -17,7 +17,7 @@
<DropdownMenuPrimitive.Item
bind:ref
class={cn(
"relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 data-[variant=destructive]:data-highlighted:text-destructive dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:!text-destructive",
"relative flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 data-[variant=destructive]:data-highlighted:text-destructive dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:!text-destructive",
className
)}
data-inset={inset}
@@ -129,7 +129,7 @@ export const SETTINGS_REGISTRY: SettingsSectionEntry[] = [
// Deliberately off for now: the natural place to turn it on is the first
// run experience, once onboarding exists to ask the user about it.
defaultValue: false,
help: 'Fetch model metadata (avatars, context length, chat template, file sizes) from the Hugging Face Hub. When off, the UI only shows what the server reports for /v1/models and hides the org avatars.',
help: 'Fetch model metadata (avatars, context length, chat template) from the Hugging Face Hub. When off, the UI only shows what the server reports for /v1/models and hides the org avatars.',
key: SETTINGS_KEYS.USE_HUGGING_FACE_HUB,
label: 'Use Hugging Face Hub API for models metadata',
type: SettingsFieldType.CHECKBOX
+1 -2
View File
@@ -80,6 +80,5 @@ export enum ModelRowDownloadState {
/** Column the models manager table can be ordered by. */
export enum ModelsTableSortKey {
CONTEXT = 'context',
NAME = 'name',
STATUS = 'status'
NAME = 'name'
}
@@ -622,6 +622,13 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
capabilities: rawCapabilities.filter((value: unknown): value is string =>
Boolean(value)
),
// 0 is the server's way of leaving the trained context unknown; a
// model-mode listing reports it as meta.n_ctx_train instead
contextLength:
item.context_length ||
(typeof item.meta?.n_ctx_train === 'number' && item.meta.n_ctx_train > 0
? item.meta.n_ctx_train
: undefined),
description: details?.description,
details: details?.details,
draftSidecars: mergedDraftSidecars(
+2
View File
@@ -100,6 +100,8 @@ export interface ApiModelDataEntry {
tags?: string[];
/** Modality capabilities, reported by the router for every model regardless of load state */
architecture?: ApiModelArchitecture;
/** Trained context of the model, read from its GGUF metadata at registration */
context_length?: number;
/** Legacy meta field (may be present in older responses) */
meta?: Record<string, unknown> | null;
}
@@ -0,0 +1,243 @@
// Guards the manager table ordering and filtering: the context column sorts and
// filters once a model's context is known, from the option or the Hub record.
import ModelsManagerWrapper from './components/ModelsManagerWrapper.svelte';
import { SETTINGS_KEYS } from '$lib/constants';
import { ServerModelStatus } from '$lib/enums';
import { HuggingFaceService } from '$lib/services';
import { modelsStore, settingsStore } from '$lib/stores';
import type { ApiModelDataEntry } from '$lib/types';
import type { ModelOption } from '$lib/types/models';
import { SvelteMap } from 'svelte/reactivity';
import { beforeEach, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
function option(model: string, contextLength?: number): ModelOption {
return {
capabilities: [],
contextLength,
id: model,
model,
name: model
};
}
const models = [
option('org/alpha-8b:Q4_K_M', 8192),
option('org/beta-8b:Q4_K_M', 131072),
option('org/gamma-8b:Q4_K_M', 32768)
];
// the router listing carries no context, so a row starts without one
const modelsWithoutContext = models.map((model) => ({ ...model, contextLength: undefined }));
beforeEach(() => {
modelsStore.routerModels = [];
modelsStore.models = modelsWithoutContext;
modelsStore.favoriteModelIds = new Set();
settingsStore.config[SETTINGS_KEYS.GROUP_MODELS_BY_FAMILY] = false;
});
/** Renders the manager and waits for the test models to show. */
async function renderWithModels(rows: ModelOption[] = modelsWithoutContext) {
const screen = render(ModelsManagerWrapper);
modelsStore.models = rows;
await expect.element(screen.getByText(/gamma\s+8B/)).toBeVisible();
return screen;
}
function rowNames(container: HTMLElement): string[] {
return [...container.querySelectorAll('button[aria-pressed]')].map(
(row) => row.textContent ?? ''
);
}
/** The Hub details cache the rows read their context from. */
function detailsCache() {
return (
HuggingFaceService as unknown as {
detailsCache: SvelteMap<string, { gguf?: { context_length?: number } } | null>;
}
).detailsCache;
}
function warmCache() {
const cache = detailsCache();
cache.set('org/alpha-8b', { gguf: { context_length: 8192 } });
cache.set('org/beta-8b', { gguf: { context_length: 131072 } });
cache.set('org/gamma-8b', { gguf: { context_length: 32768 } });
}
it('sorts by context', async () => {
const screen = await renderWithModels(models);
// lowest first
await screen.getByTitle('Sort by context, lowest first').click();
const ascending = rowNames(screen.container);
await screen.getByTitle('Sort by context, highest first').click();
const descending = rowNames(screen.container);
expect(ascending).not.toEqual(descending);
});
it('sorts by context with family grouping on', async () => {
settingsStore.config[SETTINGS_KEYS.GROUP_MODELS_BY_FAMILY] = true;
const screen = await renderWithModels(models);
// lowest first
await screen.getByTitle('Sort by context, lowest first').click();
const ascending = rowNames(screen.container);
await screen.getByTitle('Sort by context, highest first').click();
const descending = rowNames(screen.container);
expect(ascending).not.toEqual(descending);
});
it('lists the favorited quants of a repo as flat rows', async () => {
// one quant of the beta repo is a favorite, the other stays with the repo
modelsStore.favoriteModelIds = new Set(['org/alpha-8b:Q4_K_M', 'org/beta-8b:Q4_K_M']);
const screen = await renderWithModels([
modelsWithoutContext[0],
modelsWithoutContext[1],
option('org/beta-8b:Q8_0'),
modelsWithoutContext[2]
]);
// the favorite quant is a model row of its own, not a repo with subitems
expect(screen.container.textContent).not.toContain('2 quants available');
// the repo appears once in favorites for its favorited quant and once in the
// local block for the quant left behind
expect(screen.getByText(/beta\s+8B/).elements().length).toBe(2);
});
/** A router listing entry carrying only the meta context. */
function entry(model: string, nCtxTrain: number): ApiModelDataEntry {
return {
created: 0,
id: model,
in_cache: false,
meta: { n_ctx_train: nCtxTrain },
object: 'model',
owned_by: 'llamacpp',
path: `/models/${model}`,
status: { value: ServerModelStatus.UNLOADED }
};
}
it('sorts by the meta context of a listing without the router field', async () => {
// a listing that skips the router's GGUF read reports the trained context
// only as meta.n_ctx_train, so the option mapping falls back to it
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input: RequestInfo | URL) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
if (url.includes('/props')) {
return new Response(
JSON.stringify({
default_generation_settings: { n_ctx: 0, params: {} },
model_alias: 'llama-server',
model_path: 'none',
role: 'router'
}),
{ headers: { 'Content-Type': 'application/json' }, status: 200 }
);
}
if (url.includes('/server')) {
return new Response(
JSON.stringify({ git_branch: 'test', git_commit: 'test', mode: 'router', version: 'test' }),
{ headers: { 'Content-Type': 'application/json' }, status: 200 }
);
}
if (/\/v1\/models|\/models\b/.test(url)) {
return new Response(
JSON.stringify({
data: [
entry('org/alpha-8b:Q4_K_M', 8192),
entry('org/beta-8b:Q4_K_M', 131072),
entry('org/gamma-8b:Q4_K_M', 32768)
],
object: 'list'
}),
{ headers: { 'Content-Type': 'application/json' }, status: 200 }
);
}
throw new Error(`unexpected fetch in the test: ${url}`);
});
const screen = render(ModelsManagerWrapper);
// the fetch maps the listing into options, the meta context fills in
await modelsStore.fetch(true);
await expect.element(screen.getByText(/gamma\s+8B/)).toBeVisible();
await screen.getByTitle('Sort by context, lowest first').click();
const names = rowNames(screen.container).join(' | ');
expect(names.indexOf('alpha')).toBeLessThan(names.indexOf('gamma'));
expect(names.indexOf('gamma')).toBeLessThan(names.indexOf('beta'));
});
it('re-sorts when the Hub details arrive after the sort was clicked', async () => {
const screen = await renderWithModels();
// the user sorts while the contexts are still unknown
await screen.getByTitle('Sort by context, lowest first').click();
// then the rows fetch their Hub records
warmCache();
// the table re-sorts once the cache answers
await vi.waitFor(() => {
const names = rowNames(screen.container).join(' | ');
expect(names.indexOf('alpha')).toBeLessThan(names.indexOf('gamma'));
expect(names.indexOf('gamma')).toBeLessThan(names.indexOf('beta'));
return names;
});
});
it('filters by search', async () => {
const screen = await renderWithModels();
await screen.getByPlaceholder('Search your models').fill('beta');
await expect.element(screen.getByText(/alpha\s+8B/)).not.toBeVisible();
await expect.element(screen.getByText(/beta\s+8B/)).toBeVisible();
});
it('filters by context and sorts from the Hub details cache', async () => {
warmCache();
const screen = await renderWithModels();
// open the context filter and ask for 32K or more
await screen.getByText('Context:').click();
await screen.getByText('32K or more').click();
await expect.element(screen.getByText(/alpha\s+8B/)).not.toBeVisible();
await expect.element(screen.getByText(/gamma\s+8B/)).toBeVisible();
// sorting re-runs once the cache answers
await screen.getByTitle('Sort by context, lowest first').click();
const names = rowNames(screen.container).join(' | ');
expect(names.indexOf('gamma')).toBeLessThan(names.indexOf('beta'));
});
File diff suppressed because it is too large Load Diff
+913 -72
View File
File diff suppressed because it is too large Load Diff