mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-09 22:37:28 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e6f04a75e | ||
|
|
10a60cf303 | ||
|
|
79e2e74eb1 | ||
|
|
8e2d31e0eb | ||
|
|
baef3ed9a1 | ||
|
|
f39148a953 | ||
|
|
50e3e3e480 | ||
|
|
64df9183f5 |
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -537,6 +537,18 @@ static void dequantize_row_mxfp4_sycl(const void * vx, dst_t * y, const int64_t
|
||||
});
|
||||
}
|
||||
|
||||
template <typename dst_t>
|
||||
static void dequantize_row_mxfp4_sycl_reorder(const void * vx, dst_t * y, const int64_t k, dpct::queue_ptr stream) {
|
||||
GGML_ASSERT(k % QK_MXFP4 == 0);
|
||||
const int n_warp = (k / QK_MXFP4 + WARP_SIZE - 1) / WARP_SIZE;
|
||||
stream->parallel_for(
|
||||
sycl::nd_range<3>(sycl::range<3>(1, 1, n_warp) * sycl::range<3>(1, 1, WARP_SIZE),
|
||||
sycl::range<3>(1, 1, WARP_SIZE)),
|
||||
[=](sycl::nd_item<3> item_ct1) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
|
||||
dequantize_block_mxfp4_reorder(vx, y, k, item_ct1);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename dst_t>
|
||||
static void dequantize_row_nvfp4_sycl(const void * vx, dst_t * y, const int64_t k, dpct::queue_ptr stream) {
|
||||
GGML_ASSERT(k % QK_NVFP4 == 0);
|
||||
@@ -728,6 +740,9 @@ to_fp16_sycl_t ggml_get_to_fp16_sycl(ggml_type type, ggml_tensor * dst) {
|
||||
case GGML_TYPE_IQ4_NL:
|
||||
return dequantize_row_iq4_nl_sycl;
|
||||
case GGML_TYPE_MXFP4:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_mxfp4_sycl_reorder;
|
||||
}
|
||||
return dequantize_row_mxfp4_sycl;
|
||||
case GGML_TYPE_NVFP4:
|
||||
return dequantize_row_nvfp4_sycl;
|
||||
@@ -819,6 +834,9 @@ to_fp32_sycl_t ggml_get_to_fp32_sycl(ggml_type type, ggml_tensor *dst) {
|
||||
case GGML_TYPE_IQ4_NL:
|
||||
return dequantize_row_iq4_nl_sycl;
|
||||
case GGML_TYPE_MXFP4:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_mxfp4_sycl_reorder;
|
||||
}
|
||||
return dequantize_row_mxfp4_sycl;
|
||||
case GGML_TYPE_NVFP4:
|
||||
return dequantize_row_nvfp4_sycl;
|
||||
|
||||
@@ -1646,6 +1646,26 @@ static void dequantize_block_mxfp4(const void * __restrict__ vx, dst_t * __restr
|
||||
}
|
||||
}
|
||||
|
||||
// Reordered MXFP4 ([qs...][e...], see ggml_sycl_reordered::block_q_t<MXFP4>): one work-item per block.
|
||||
template <typename dst_t>
|
||||
static void dequantize_block_mxfp4_reorder(const void * __restrict__ vx, dst_t * __restrict__ yy, int64_t k,
|
||||
const sycl::nd_item<3> & item_ct1) {
|
||||
const int64_t ib = (int64_t) item_ct1.get_group(2) * WARP_SIZE + item_ct1.get_local_id(2);
|
||||
if (ib >= k / QK_MXFP4) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t * qs = (const uint8_t *) vx + ib * (QK_MXFP4 / 2);
|
||||
const float d = ggml_sycl_e8m0_to_fp32(((const uint8_t *) vx)[k / 2 + ib]) * 0.5f;
|
||||
dst_t * y = yy + ib * QK_MXFP4;
|
||||
|
||||
#pragma unroll
|
||||
for (int j = 0; j < QK_MXFP4 / 2; ++j) {
|
||||
y[j] = d * kvalues_mxfp4[qs[j] & 0xf];
|
||||
y[j + QK_MXFP4 / 2] = d * kvalues_mxfp4[qs[j] >> 4];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
template <typename dst_t>
|
||||
static void dequantize_block_nvfp4(
|
||||
|
||||
@@ -773,7 +773,8 @@ ggml_backend_sycl_buffer_init_tensor(ggml_backend_buffer_t buffer,
|
||||
case GGML_TYPE_Q3_K:
|
||||
case GGML_TYPE_Q4_K:
|
||||
case GGML_TYPE_Q5_K:
|
||||
case GGML_TYPE_Q6_K:{
|
||||
case GGML_TYPE_Q6_K:
|
||||
case GGML_TYPE_MXFP4:{
|
||||
ggml_tensor_extra_gpu * extra = new ggml_tensor_extra_gpu{};
|
||||
tensor->extra = extra;
|
||||
ctx->tensor_extras.push_back(extra);
|
||||
@@ -4623,6 +4624,58 @@ static bool reorder_qw_q6_k_moe(uint8_t * data_device, size_t expert_bytes, int6
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reorder each MXFP4 expert slice into [qs][e]: 16-byte nibble blocks, then one E8M0 byte per block.
|
||||
// Experts are self-contained, so the tensor is reordered a few experts at a time through a small
|
||||
// temporary: a whole-tensor temporary (hundreds of MB) can exceed the VRAM left on a nearly full card,
|
||||
// and on Windows the driver then pages device memory out to host RAM instead of failing.
|
||||
static bool reorder_qw_mxfp4_moe(uint8_t * data_device, size_t expert_bytes, int64_t n_expert, dpct::queue_ptr stream) {
|
||||
GGML_ASSERT(expert_bytes % sizeof(block_mxfp4) == 0);
|
||||
const int blocks_per_expert = (int) (expert_bytes / sizeof(block_mxfp4));
|
||||
const size_t max_chunk_bytes = 32u << 20;
|
||||
const int64_t chunk_experts = std::max<int64_t>(1, std::min<int64_t>(n_expert, (int64_t) (max_chunk_bytes / expert_bytes)));
|
||||
|
||||
sycl_reorder_temp_buffer tmp(stream, (size_t) chunk_experts * expert_bytes);
|
||||
if (!tmp) {
|
||||
GGML_LOG_WARN("%s: failed to allocate %zu bytes for reorder temp buffer, skipping reorder\n", __func__,
|
||||
(size_t) chunk_experts * expert_bytes);
|
||||
return false;
|
||||
}
|
||||
uint8_t * tmp_buf = static_cast<uint8_t *>(tmp.ptr);
|
||||
|
||||
// the queue is in-order: each chunk's copy into tmp_buf waits for the previous chunk's kernel
|
||||
for (int64_t e0 = 0; e0 < n_expert; e0 += chunk_experts) {
|
||||
const int64_t n_chunk = std::min(chunk_experts, n_expert - e0);
|
||||
uint8_t * chunk = data_device + (size_t) e0 * expert_bytes;
|
||||
|
||||
sycl::event copy_event;
|
||||
SYCL_CHECK(CHECK_TRY_ERROR(copy_event = stream->memcpy(tmp_buf, chunk, (size_t) n_chunk * expert_bytes)));
|
||||
if (!g_ggml_sycl_use_async_mem_op) {
|
||||
copy_event.wait();
|
||||
}
|
||||
|
||||
const int total_blocks = blocks_per_expert * (int) n_chunk;
|
||||
auto reorder_event = stream->parallel_for(total_blocks, [=](auto gb_) {
|
||||
const int gb = gb_;
|
||||
const int e = gb / blocks_per_expert;
|
||||
const int ib = gb % blocks_per_expert;
|
||||
const block_mxfp4 * x = (const block_mxfp4 *) (tmp_buf + (size_t) e * expert_bytes);
|
||||
uint8_t * base = chunk + (size_t) e * expert_bytes;
|
||||
|
||||
uint8_t * qs_ptr = base;
|
||||
uint8_t * e_ptr = qs_ptr + (QK_MXFP4 / 2) * (size_t) blocks_per_expert;
|
||||
|
||||
for (int j = 0; j < QK_MXFP4 / 2; ++j) {
|
||||
qs_ptr[(size_t) ib * (QK_MXFP4 / 2) + j] = x[ib].qs[j];
|
||||
}
|
||||
e_ptr[ib] = x[ib].e;
|
||||
});
|
||||
if (!g_ggml_sycl_use_async_mem_op) {
|
||||
reorder_event.wait_and_throw();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool reorder_qw_q2_k(uint8_t * data_device, size_t size, size_t offset, dpct::queue_ptr stream) {
|
||||
GGML_ASSERT(size % sizeof(block_q2_K) == 0);
|
||||
GGML_ASSERT(offset % sizeof(block_q2_K) == 0);
|
||||
@@ -4832,6 +4885,8 @@ static bool reorder_qw(const ggml_tensor * src0, dpct::queue_ptr stream) {
|
||||
return reorder_qw_q5_k_moe(data_device, src0->nb[2], src0->ne[2], stream);
|
||||
case GGML_TYPE_Q6_K:
|
||||
return reorder_qw_q6_k_moe(data_device, src0->nb[2], src0->ne[2], stream);
|
||||
case GGML_TYPE_MXFP4:
|
||||
return reorder_qw_mxfp4_moe(data_device, src0->nb[2], src0->ne[2], stream);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -4905,7 +4960,12 @@ static void opt_for_reorder_id(ggml_backend_sycl_context * ctx, const ggml_tenso
|
||||
if (!g_ggml_sycl_enable_optimize || !ctx->opt_feature.reorder) {
|
||||
return;
|
||||
}
|
||||
if (src0->type != GGML_TYPE_Q4_K && src0->type != GGML_TYPE_Q5_K && src0->type != GGML_TYPE_Q6_K) {
|
||||
if (src0->type != GGML_TYPE_Q4_K && src0->type != GGML_TYPE_Q5_K && src0->type != GGML_TYPE_Q6_K &&
|
||||
src0->type != GGML_TYPE_MXFP4) {
|
||||
return;
|
||||
}
|
||||
// The MXFP4 reorder kernels use 8-byte vector loads, so every expert slice must stay aligned.
|
||||
if (src0->type == GGML_TYPE_MXFP4 && (src0->nb[2] % 16 != 0 || (uintptr_t) src0->data % 16 != 0)) {
|
||||
return;
|
||||
}
|
||||
ggml_tensor_extra_gpu * extra = static_cast<ggml_tensor_extra_gpu *>(src0->extra);
|
||||
@@ -5388,6 +5448,11 @@ static void ggml_sycl_mul_mat_id(ggml_backend_sycl_context & ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// The per-expert loop below reads the experts in whatever layout they have: reorder MXFP4 here as well, so prompt processing does not depend on a single-token decode having run first.
|
||||
if (src0->type == GGML_TYPE_MXFP4) {
|
||||
opt_for_reorder_id(&ctx, src0);
|
||||
}
|
||||
|
||||
std::vector<char> ids_host(ggml_nbytes(ids));
|
||||
const char * ids_dev = (const char *) ids->data;
|
||||
|
||||
|
||||
@@ -1285,6 +1285,65 @@ static void reorder_mul_mat_vec_q8_0_q8_1_sycl_switch_ncols(
|
||||
}
|
||||
}
|
||||
|
||||
// MXFP4 reorder GEMV. Only MoE expert slices are reordered (opt_for_reorder_id); these dense entry
|
||||
// points serve per-expert ggml_sycl_mul_mat calls from multi-token MUL_MAT_ID after that reorder.
|
||||
static void reorder_mul_mat_vec_mxfp4_q8_1_sycl(const void * vx, const void * vy, float * dst, const int ncols,
|
||||
const int nrows, dpct::queue_ptr stream) {
|
||||
GGML_ASSERT(ncols % QK_MXFP4 == 0);
|
||||
constexpr size_t num_subgroups = WARP_SIZE;
|
||||
const int block_num_y = ceil_div(nrows, GGML_SYCL_MMV_Y * (int) num_subgroups);
|
||||
const sycl::range<3> block_nums(1, 1, block_num_y);
|
||||
const sycl::range<3> block_dims(1, GGML_SYCL_MMV_Y, num_subgroups * WARP_SIZE);
|
||||
|
||||
stream->submit([&](sycl::handler & cgh) {
|
||||
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
|
||||
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
|
||||
mul_mat_vec_q_reorder<reorder_vec_dot_q_sycl<GGML_TYPE_MXFP4>>(vx, vy, dst, ncols, nrows,
|
||||
nd_item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
template <int ncols_dst>
|
||||
static void reorder_mul_mat_vec_mxfp4_q8_1_sycl_ncols(
|
||||
const void * vx, const void * vy, float * dst,
|
||||
const int ncols, const int nrows,
|
||||
const int stride_col_y_bytes, const int stride_col_dst,
|
||||
dpct::queue_ptr stream) {
|
||||
GGML_ASSERT(ncols % QK_MXFP4 == 0);
|
||||
constexpr size_t num_subgroups = WARP_SIZE;
|
||||
const int block_num_y = ceil_div(nrows, GGML_SYCL_MMV_Y * (int) num_subgroups);
|
||||
const sycl::range<3> block_nums(1, 1, block_num_y);
|
||||
const sycl::range<3> block_dims(1, GGML_SYCL_MMV_Y, num_subgroups * WARP_SIZE);
|
||||
|
||||
stream->submit([&](sycl::handler & cgh) {
|
||||
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
|
||||
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
|
||||
mul_mat_vec_q_reorder_ncols<reorder_vec_dot_q_sycl<GGML_TYPE_MXFP4>, ncols_dst>(
|
||||
vx, /*vgate=*/ nullptr, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst,
|
||||
/*glu_op=*/ GGML_GLU_OP_SWIGLU, nd_item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static void reorder_mul_mat_vec_mxfp4_q8_1_sycl_switch_ncols(
|
||||
const void * vx, const void * vy, float * dst,
|
||||
const int ncols, const int nrows, const int ncols_dst,
|
||||
const int stride_col_y_bytes, const int stride_col_dst,
|
||||
dpct::queue_ptr stream) {
|
||||
switch (ncols_dst) {
|
||||
case 1: reorder_mul_mat_vec_mxfp4_q8_1_sycl(vx, vy, dst, ncols, nrows, stream); break;
|
||||
case 2: reorder_mul_mat_vec_mxfp4_q8_1_sycl_ncols<2>(vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, stream); break;
|
||||
case 3: reorder_mul_mat_vec_mxfp4_q8_1_sycl_ncols<3>(vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, stream); break;
|
||||
case 4: reorder_mul_mat_vec_mxfp4_q8_1_sycl_ncols<4>(vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, stream); break;
|
||||
case 5: reorder_mul_mat_vec_mxfp4_q8_1_sycl_ncols<5>(vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, stream); break;
|
||||
case 6: reorder_mul_mat_vec_mxfp4_q8_1_sycl_ncols<6>(vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, stream); break;
|
||||
case 7: reorder_mul_mat_vec_mxfp4_q8_1_sycl_ncols<7>(vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, stream); break;
|
||||
case 8: reorder_mul_mat_vec_mxfp4_q8_1_sycl_ncols<8>(vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, stream); break;
|
||||
default: GGML_ABORT("unsupported ncols_dst=%d for MXFP4 reorder multi-col MMVQ", ncols_dst);
|
||||
}
|
||||
}
|
||||
|
||||
static void mul_mat_vec_q8_0_q8_1_sycl(const void *vx, const void *vy,
|
||||
float *dst, const int ncols,
|
||||
const int nrows,
|
||||
@@ -2765,7 +2824,21 @@ void ggml_sycl_op_mul_mat_vec_q(ggml_backend_sycl_context & ctx, const ggml_tens
|
||||
}
|
||||
break;
|
||||
case GGML_TYPE_MXFP4:
|
||||
if (i == 0 && src1_ncols > 1 && src1_ncols <= 8) {
|
||||
if ((ggml_tensor_extra_gpu *) dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (i == 0 && src1_ncols > 1 && src1_ncols <= 8) {
|
||||
const int stride_col_y_bytes = src1_padded_col_size * q8_1_ts / q8_1_bs;
|
||||
const int stride_col_dst = dst->ne[0];
|
||||
GGML_SYCL_DEBUG("Calling reorder_mul_mat_vec_mxfp4_q8_1_sycl_switch_ncols ncols=%d\n", (int)src1_ncols);
|
||||
reorder_mul_mat_vec_mxfp4_q8_1_sycl_switch_ncols(
|
||||
src0_dd_i, src1_ddq_i, dst_dd_i, ne00, row_diff,
|
||||
src1_ncols, stride_col_y_bytes, stride_col_dst, stream);
|
||||
return;
|
||||
} else {
|
||||
GGML_SYCL_DEBUG("Calling reorder_mul_mat_vec_mxfp4_q8_1_sycl\n");
|
||||
reorder_mul_mat_vec_mxfp4_q8_1_sycl(src0_dd_i, src1_ddq_i_bs, dst_dd_i_bs, ne00, row_diff, stream);
|
||||
}
|
||||
} else if (i == 0 && src1_ncols > 1 && src1_ncols <= 8) {
|
||||
const int stride_col_y = src1_padded_col_size / QK8_1;
|
||||
const int stride_col_dst = dst->ne[0];
|
||||
GGML_SYCL_DEBUG("Calling mul_mat_vec_mxfp4_q8_1_sycl_switch_ncols ncols=%d\n", (int)src1_ncols);
|
||||
@@ -3111,6 +3184,11 @@ bool ggml_sycl_mul_mat_vec_q_id_reorder(
|
||||
vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used,
|
||||
expert_weight_stride, dst_row_stride, src1_row_stride, stream);
|
||||
return true;
|
||||
case GGML_TYPE_MXFP4:
|
||||
launch_mul_mat_vec_q_moe_reorder<reorder_vec_dot_q_sycl<GGML_TYPE_MXFP4>>(
|
||||
vx_base, vy, ids_dev, dst_base, ncols, nrows, n_experts_used,
|
||||
expert_weight_stride, dst_row_stride, src1_row_stride, stream);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -199,6 +199,27 @@ template <> struct block_q_t<GGML_TYPE_Q8_0> {
|
||||
static constexpr int block_to_q8_1_ratio() { return traits::qk / QK8_1; } // 1
|
||||
};
|
||||
|
||||
template <> struct block_q_t<GGML_TYPE_MXFP4> {
|
||||
struct traits {
|
||||
static constexpr uint32_t qk = QK_MXFP4; // 32
|
||||
static constexpr uint32_t qi = QI_MXFP4; // 4
|
||||
static constexpr uint32_t qr = QR_MXFP4; // 2
|
||||
static constexpr uint32_t vdr_mmvq = 2;
|
||||
};
|
||||
|
||||
// MXFP4 reorder layout: [qs0|qs1|...|qsN][e0|e1|...|eN]
|
||||
// The 17-byte AoS block leaves qs unaligned; split out, every 16-byte nibble block is aligned.
|
||||
static constexpr std::pair<int, int> get_block_offset(const int block_index, const int /* nblocks */) {
|
||||
return { block_index * (QK_MXFP4 / 2), 0 };
|
||||
}
|
||||
|
||||
static constexpr std::pair<int, int> get_d_offset(int nrows, int ncols, const int block_index) {
|
||||
return { (ncols / 2 * nrows) + block_index, 0 };
|
||||
}
|
||||
|
||||
static constexpr int block_to_q8_1_ratio() { return traits::qk / QK8_1; } // 1
|
||||
};
|
||||
|
||||
} // namespace ggml_sycl_reordered
|
||||
|
||||
#endif // GGML_SYCL_QUANTS_HPP
|
||||
|
||||
@@ -148,6 +148,28 @@ static __dpct_inline__ sycl::int2 get_int_from_table_16(
|
||||
dpct::byte_level_permute(tmp[0], tmp[1], 0x7531));
|
||||
}
|
||||
|
||||
// Four E2M1 codes (one per byte, bits 0..3) to their kvalues_mxfp4 int8 values. SWAR arithmetic
|
||||
// replaces get_int_from_table_16 for MXFP4: dpct::byte_level_permute is emulated with 64-bit shifts,
|
||||
// eight per int, which made the MXFP4 GEMV compute-bound on Intel GPUs.
|
||||
// Magnitudes 0,1,2,3,4,6,8,12 = m + [m>=5] + [m>=6] + 3*[m>=7]; each byte stays below 256, so the
|
||||
// byte-wise adds never carry. -0 (code 8) is left as 0 so the two's-complement +1 cannot carry either.
|
||||
static __dpct_inline__ int mxfp4_codes_to_int8(const uint32_t x) {
|
||||
const uint32_t m = x & 0x07070707u;
|
||||
const uint32_t ge5 = ((m + 0x03030303u) >> 3) & 0x01010101u;
|
||||
const uint32_t ge6 = ((m + 0x02020202u) >> 3) & 0x01010101u;
|
||||
const uint32_t ge7 = ((m + 0x01010101u) >> 3) & 0x01010101u;
|
||||
const uint32_t mag = m + ge5 + ge6 + 3u * ge7;
|
||||
const uint32_t nz = ((mag + 0x7f7f7f7fu) >> 7) & 0x01010101u;
|
||||
const uint32_t neg = (x >> 3) & nz & 0x01010101u;
|
||||
return (int) ((mag ^ (neg * 0xffu)) + neg);
|
||||
}
|
||||
|
||||
// Same result as get_int_from_table_16(q4, kvalues_mxfp4): x = low nibbles, y = high nibbles.
|
||||
static __dpct_inline__ sycl::int2 get_int_from_mxfp4(const int q4) {
|
||||
return sycl::int2(mxfp4_codes_to_int8((uint32_t) q4 & 0x0f0f0f0fu),
|
||||
mxfp4_codes_to_int8(((uint32_t) q4 >> 4) & 0x0f0f0f0fu));
|
||||
}
|
||||
|
||||
#define VDR_Q2_K_Q8_1_MMVQ 1
|
||||
|
||||
// contiguous v/x values
|
||||
@@ -795,6 +817,41 @@ template <> struct reorder_vec_dot_q_sycl<GGML_TYPE_Q6_K> {
|
||||
vl, vh, u0, u1, scs[0], scs[4], *d, d80, d81);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct reorder_vec_dot_q_sycl<GGML_TYPE_MXFP4> {
|
||||
static constexpr ggml_type gtype = GGML_TYPE_MXFP4;
|
||||
|
||||
using mxfp4_block = ggml_sycl_reordered::block_q_t<GGML_TYPE_MXFP4>;
|
||||
using mxfp4_traits = typename mxfp4_block::traits;
|
||||
|
||||
__dpct_inline__ float operator()(const void * __restrict__ vbq, const std::pair<int, int> ibx_offset,
|
||||
const std::pair<int, int> d_offset, const int8_t * q8_1_quant_ptr,
|
||||
const sycl::half2 * q8_1_ds, const int & iqs) {
|
||||
static_assert(mxfp4_traits::vdr_mmvq == 2, "vector load assumes vdr_mmvq == 2");
|
||||
const uint8_t * base = static_cast<const uint8_t *>(vbq);
|
||||
|
||||
// Reordered nibble blocks are 16 contiguous bytes and iqs is 0 or 2, so each lane's two
|
||||
// weight ints are one aligned 8-byte load (the AoS layout needed eight byte loads).
|
||||
const sycl::int2 q4 = *reinterpret_cast<const sycl::int2 *>(base + ibx_offset.first + sizeof(int) * iqs);
|
||||
const uint8_t e = base[d_offset.first];
|
||||
|
||||
// Low nibbles pair with q8_1 ints iqs..iqs+1, high nibbles with iqs+4..iqs+5.
|
||||
const sycl::int2 u_lo = *reinterpret_cast<const sycl::int2 *>(q8_1_quant_ptr + sizeof(int) * iqs);
|
||||
const sycl::int2 u_hi = *reinterpret_cast<const sycl::int2 *>(q8_1_quant_ptr + sizeof(int) * (iqs + 4));
|
||||
|
||||
const sycl::int2 v0 = get_int_from_mxfp4(q4.x());
|
||||
const sycl::int2 v1 = get_int_from_mxfp4(q4.y());
|
||||
|
||||
int sumi = 0;
|
||||
sumi = ggml_sycl_dp4a(v0.x(), u_lo.x(), sumi);
|
||||
sumi = ggml_sycl_dp4a(v0.y(), u_hi.x(), sumi);
|
||||
sumi = ggml_sycl_dp4a(v1.x(), u_lo.y(), sumi);
|
||||
sumi = ggml_sycl_dp4a(v1.y(), u_hi.y(), sumi);
|
||||
|
||||
const float d = ggml_sycl_e8m0_to_fp32(e) * 0.5f * static_cast<float>((*q8_1_ds)[0]);
|
||||
return d * sumi;
|
||||
}
|
||||
};
|
||||
#define VDR_Q4_0_Q8_1_MMVQ 2
|
||||
#define VDR_Q4_0_Q8_1_MMQ 4
|
||||
|
||||
@@ -1124,7 +1181,7 @@ static __dpct_inline__ float vec_dot_mxfp4_q8_1(const void * __restrict__ vbq,
|
||||
#pragma unroll
|
||||
for (int l = 0; l < VDR_MXFP4_Q8_1_MMVQ; ++l) {
|
||||
const int aux_q4 = get_int_b1(bq4->qs, iqs + l);
|
||||
const sycl::int2 v = get_int_from_table_16(aux_q4, kvalues_mxfp4);
|
||||
const sycl::int2 v = get_int_from_mxfp4(aux_q4);
|
||||
sumi = ggml_sycl_dp4a(v.x(), q8[l + 0], sumi);
|
||||
sumi = ggml_sycl_dp4a(v.y(), q8[l + 4], sumi);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+2
-9
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Vendored
+2
@@ -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'));
|
||||
});
|
||||
+1184
File diff suppressed because it is too large
Load Diff
Vendored
+913
-72
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user