mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-07 16:37:57 +02:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a5c985904 | ||
|
|
81ef2a3dd3 | ||
|
|
2498e9ae76 | ||
|
|
b774d2c807 | ||
|
|
affdf585c1 | ||
|
|
b9bf09af84 | ||
|
|
a693dd45c2 | ||
|
|
8635b58aae | ||
|
|
296b0f8881 | ||
|
|
b14462c0c9 | ||
|
|
ccc3646c63 | ||
|
|
c0b1871bc7 | ||
|
|
160bd031b2 | ||
|
|
dbeb37548e | ||
|
|
7a333e7240 | ||
|
|
0c963452ea | ||
|
|
4735997382 | ||
|
|
d23c47f2a9 | ||
|
|
73ab7599b5 | ||
|
|
0cae43063c | ||
|
|
1173700b9c | ||
|
|
5202104b59 | ||
|
|
9a7570587c | ||
|
|
b74f590eaf | ||
|
|
992cb503cd |
@@ -31,7 +31,7 @@
|
||||
]
|
||||
&& blas.meta.available,
|
||||
useCuda ? config.cudaSupport,
|
||||
useMetalKit ? stdenv.isAarch64 && stdenv.isDarwin,
|
||||
useMetalKit ? stdenv.hostPlatform.isAarch64 && stdenv.hostPlatform.isDarwin,
|
||||
# Increases the runtime closure size by ~700M
|
||||
useMpi ? false,
|
||||
useRocm ? config.rocmSupport,
|
||||
@@ -92,7 +92,7 @@ let
|
||||
|
||||
cudaBuildInputs = with cudaPackages; [
|
||||
cuda_cudart
|
||||
cuda_cccl # <nv/target>
|
||||
cccl # <nv/target>
|
||||
libcublas
|
||||
];
|
||||
|
||||
@@ -166,7 +166,7 @@ effectiveStdenv.mkDerivation (finalAttrs: {
|
||||
# `xcrun` is used find the path of the Metal compiler, which is varible
|
||||
# and not on $PATH
|
||||
# see https://github.com/ggml-org/llama.cpp/pull/6118 for discussion
|
||||
__noChroot = effectiveStdenv.isDarwin && useMetalKit && precompileMetalShaders;
|
||||
__noChroot = effectiveStdenv.hostPlatform.isDarwin && useMetalKit && precompileMetalShaders;
|
||||
|
||||
nativeBuildInputs =
|
||||
[
|
||||
@@ -181,10 +181,10 @@ effectiveStdenv.mkDerivation (finalAttrs: {
|
||||
autoAddDriverRunpath
|
||||
]
|
||||
++ optionals (effectiveStdenv.hostPlatform.isGnu && enableStatic) [ glibc.static ]
|
||||
++ optionals (effectiveStdenv.isDarwin && useMetalKit && precompileMetalShaders) [ xcrunHost ];
|
||||
++ optionals (effectiveStdenv.hostPlatform.isDarwin && useMetalKit && precompileMetalShaders) [ xcrunHost ];
|
||||
|
||||
buildInputs =
|
||||
optionals effectiveStdenv.isDarwin darwinBuildInputs
|
||||
optionals effectiveStdenv.hostPlatform.isDarwin darwinBuildInputs
|
||||
++ optionals useCuda cudaBuildInputs
|
||||
++ optionals useMpi [ mpi ]
|
||||
++ optionals useRocm rocmBuildInputs
|
||||
@@ -245,7 +245,7 @@ effectiveStdenv.mkDerivation (finalAttrs: {
|
||||
|
||||
# Configurations that are known to result in build failures. Can be
|
||||
# overridden by importing Nixpkgs with `allowBroken = true`.
|
||||
broken = (useMetalKit && !effectiveStdenv.isDarwin);
|
||||
broken = (useMetalKit && !effectiveStdenv.hostPlatform.isDarwin);
|
||||
|
||||
description = "Inference of LLaMA model in pure C/C++${descriptionSuffix}";
|
||||
homepage = "https://github.com/ggml-org/llama.cpp/";
|
||||
|
||||
+221
-58
@@ -8,6 +8,7 @@
|
||||
#include "json.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
@@ -534,15 +535,18 @@ static gguf_split_info get_gguf_split_info(const std::string & path) {
|
||||
}
|
||||
|
||||
// Q4_0 -> 4, F16 -> 16, NVFP4 -> 4, Q8_K_M -> 8, etc
|
||||
static int extract_quant_bits(const std::string & filename) {
|
||||
auto split = get_gguf_split_info(filename);
|
||||
static int quant_bits_from_tag(const std::string & tag) {
|
||||
auto pos = tag.find_first_of("0123456789");
|
||||
|
||||
auto pos = split.tag.find_first_of("0123456789");
|
||||
if (pos == std::string::npos) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return std::stoi(split.tag.substr(pos));
|
||||
return std::stoi(tag.substr(pos));
|
||||
}
|
||||
|
||||
static int extract_quant_bits(const std::string & filename) {
|
||||
return quant_bits_from_tag(get_gguf_split_info(filename).tag);
|
||||
}
|
||||
|
||||
static hf_cache::hf_files get_split_files(const hf_cache::hf_files & files,
|
||||
@@ -563,12 +567,127 @@ static hf_cache::hf_files get_split_files(const hf_cache::hf_files & files,
|
||||
return result;
|
||||
}
|
||||
|
||||
// pick the best sibling GGUF whose filename contains `keyword` (e.g. "mmproj" / "mtp"),
|
||||
// sidecar filename tokens, as used in `<quant>-<sidecar>` download tags,
|
||||
// e.g. `Q4_0-mtp` for `mtp-Model-Q4_0.gguf`, `BF16-mmproj` for `mmproj-BF16.gguf`
|
||||
static const std::vector<std::string> sidecar_tokens = {
|
||||
"mtp", "eagle3", "dflash", "dspark", "mmproj", "imatrix",
|
||||
};
|
||||
|
||||
static bool iequals(const std::string & a, const std::string & b) {
|
||||
return a.size() == b.size() &&
|
||||
std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) {
|
||||
return std::tolower((unsigned char) x) == std::tolower((unsigned char) y);
|
||||
});
|
||||
}
|
||||
|
||||
// split a `<quant>-<sidecar>` tag into its parts, e.g. `Q4_0-mtp` -> {`Q4_0`, `mtp`};
|
||||
// a bare sidecar tag (`mtp`) yields an empty quant; no sidecar yields an empty token
|
||||
static std::pair<std::string, std::string> split_sidecar_tag(const std::string & tag) {
|
||||
for (const auto & t : sidecar_tokens) {
|
||||
if (tag.size() > t.size() + 1 && iequals(tag.substr(tag.size() - t.size() - 1), "-" + t)) {
|
||||
return { tag.substr(0, tag.size() - t.size() - 1), t };
|
||||
}
|
||||
if (iequals(tag, t)) {
|
||||
return { "", t };
|
||||
}
|
||||
}
|
||||
return { tag, "" };
|
||||
}
|
||||
|
||||
// filename with directory and extension removed, e.g. `sub/mtp-Model-Q4_0.gguf` -> `mtp-Model-Q4_0`
|
||||
static std::string stem_of(const std::string & path) {
|
||||
std::string base = path;
|
||||
if (auto pos = base.rfind('/'); pos != std::string::npos) {
|
||||
base = base.substr(pos + 1);
|
||||
}
|
||||
string_remove_suffix(base, ".gguf");
|
||||
return base;
|
||||
}
|
||||
|
||||
// a sidecar file carries its token as a name segment in any position and case,
|
||||
// e.g. `mmproj-Model-F16.gguf`, `Model-mtp-Q4_0.gguf`, `Model-Q4_0-mtp.gguf`
|
||||
// or the short `mmproj-F16.gguf`; the token must be a whole segment, so an
|
||||
// unrelated name that merely contains it (`smtp-Model.gguf`) is a plain model
|
||||
static std::string sidecar_token_of(const std::string & path) {
|
||||
std::string base = stem_of(path);
|
||||
|
||||
for (char & c : base) {
|
||||
c = (char) std::tolower((unsigned char) c);
|
||||
}
|
||||
if (base.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const auto & t : sidecar_tokens) {
|
||||
if (base == t) {
|
||||
return t; // the sidecar file itself, e.g. `imatrix.gguf`
|
||||
}
|
||||
if (base.rfind(t + "-", 0) == 0) {
|
||||
return t; // `mtp-Model-Q4_0.gguf`
|
||||
}
|
||||
if (base.find("-" + t + "-") != std::string::npos) {
|
||||
return t; // `Model-mtp-Q4_0.gguf`
|
||||
}
|
||||
// `Model-Q4_0-mtp.gguf`, optionally with a `-draft` tail
|
||||
if (string_ends_with(base, "-" + t) || string_ends_with(base, "-" + t + "-draft")) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// name with the sidecar token segment removed, lowercased for tag parsing,
|
||||
// e.g. `Model-MTP-Q4_0` -> `model-q4_0`
|
||||
static std::string strip_sidecar_token(const std::string & base, const std::string & token) {
|
||||
std::string lower = base;
|
||||
|
||||
for (char & c : lower) {
|
||||
c = (char) std::tolower((unsigned char) c);
|
||||
}
|
||||
|
||||
if (lower == token) {
|
||||
return {};
|
||||
}
|
||||
if (lower.rfind(token + "-", 0) == 0) {
|
||||
return lower.substr(token.size() + 1);
|
||||
}
|
||||
const std::string seg = "-" + token + "-";
|
||||
if (auto pos = lower.find(seg); pos != std::string::npos) {
|
||||
return lower.substr(0, pos) + "-" + lower.substr(pos + seg.size());
|
||||
}
|
||||
if (string_ends_with(lower, "-" + token + "-draft")) {
|
||||
return lower.substr(0, lower.size() - token.size() - 7);
|
||||
}
|
||||
if (string_ends_with(lower, "-" + token)) {
|
||||
return lower.substr(0, lower.size() - token.size() - 1);
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
|
||||
// the quant a sidecar file belongs to, from its name with the token stripped;
|
||||
// a short-form name (`mmproj-F16.gguf`) leaves the bare quant, which has no
|
||||
// `-` separator for the tag regex, so it becomes the tag directly
|
||||
static std::string sidecar_quant(const std::string & path, const std::string & token) {
|
||||
std::string name = strip_sidecar_token(stem_of(path), token);
|
||||
std::string tag = get_gguf_split_info(name + ".gguf").tag;
|
||||
|
||||
if (tag.empty() && name.find('-') == std::string::npos) {
|
||||
for (char & c : name) {
|
||||
c = (char) std::toupper((unsigned char) c);
|
||||
}
|
||||
tag = name;
|
||||
}
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
// pick the best sibling GGUF carrying the sidecar `token` (e.g. "mmproj" / "mtp"),
|
||||
// preferring deeper shared directory prefix with the model, then exact `tag` match,
|
||||
// then closest quantization to the tag when given, or to the model otherwise
|
||||
// an empty `model` skips the directory constraint: the sidecar is matched by tag alone
|
||||
static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & keyword,
|
||||
const std::string & token,
|
||||
const std::string & tag = "") {
|
||||
hf_cache::hf_file best;
|
||||
size_t best_depth = 0;
|
||||
@@ -589,32 +708,32 @@ static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files,
|
||||
model_bits = extract_quant_bits(model);
|
||||
}
|
||||
auto model_parts = string_split<std::string>(model, '/');
|
||||
auto model_dir = model_parts.end() - 1;
|
||||
|
||||
for (const auto & f : files) {
|
||||
if (!string_ends_with(f.path, ".gguf") ||
|
||||
f.path.find(keyword) == std::string::npos) {
|
||||
if (sidecar_token_of(f.path) != token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto sib_parts = string_split<std::string>(f.path, '/');
|
||||
auto sib_dir = sib_parts.end() - 1;
|
||||
|
||||
auto [_, dir] = std::mismatch(model_parts.begin(), model_dir,
|
||||
sib_parts.begin(), sib_dir);
|
||||
if (dir != sib_dir) {
|
||||
continue;
|
||||
size_t depth = 0;
|
||||
if (!model.empty()) {
|
||||
auto model_dir = model_parts.end() - 1;
|
||||
auto [_, dir] = std::mismatch(model_parts.begin(), model_dir,
|
||||
sib_parts.begin(), sib_dir);
|
||||
if (dir != sib_dir) {
|
||||
continue;
|
||||
}
|
||||
depth = dir - sib_parts.begin();
|
||||
}
|
||||
|
||||
size_t depth = dir - sib_parts.begin();
|
||||
auto bits = extract_quant_bits(f.path);
|
||||
auto diff = std::abs(bits - model_bits);
|
||||
|
||||
std::string path_upper = f.path;
|
||||
for (char & c : path_upper) {
|
||||
c = (char) std::toupper((unsigned char) c);
|
||||
}
|
||||
bool exact = !tag_upper.empty() && path_upper.find("-" + tag_upper + ".") != std::string::npos;
|
||||
// rank by the quant the sidecar belongs to, with the token segment
|
||||
// stripped from its name
|
||||
auto tag = sidecar_quant(f.path, token);
|
||||
auto bits = quant_bits_from_tag(tag);
|
||||
auto diff = std::abs(bits - model_bits);
|
||||
bool exact = !tag_upper.empty() && tag == tag_upper;
|
||||
|
||||
if (!found || depth > best_depth ||
|
||||
(depth == best_depth && exact && !best_exact) ||
|
||||
@@ -637,43 +756,31 @@ static hf_cache::hf_file find_best_mmproj(const hf_cache::hf_files & files,
|
||||
static hf_cache::hf_file find_best_mtp(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "mtp-", tag);
|
||||
return find_best_sibling(files, model, "mtp", tag);
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_eagle3(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "eagle3-", tag);
|
||||
return find_best_sibling(files, model, "eagle3", tag);
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_dflash(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "dflash-", tag);
|
||||
return find_best_sibling(files, model, "dflash", tag);
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_dspark(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "dspark-", tag);
|
||||
return find_best_sibling(files, model, "dspark", tag);
|
||||
}
|
||||
|
||||
// a plain model file: a GGUF whose name carries no sidecar token segment,
|
||||
// so `smtp-Model.gguf` counts and every sidecar form does not
|
||||
static bool gguf_filename_is_model(const std::string & filepath) {
|
||||
if (!string_ends_with(filepath, ".gguf")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string filename = filepath;
|
||||
if (auto pos = filename.rfind('/'); pos != std::string::npos) {
|
||||
filename = filename.substr(pos + 1);
|
||||
}
|
||||
|
||||
return filename.find("mmproj") == std::string::npos &&
|
||||
filename.find("imatrix") == std::string::npos &&
|
||||
filename.find("mtp-") == std::string::npos &&
|
||||
filename.find("eagle3-") == std::string::npos &&
|
||||
filename.find("dflash-") == std::string::npos &&
|
||||
filename.find("dspark-") == std::string::npos;
|
||||
return string_ends_with(filepath, ".gguf") && sidecar_token_of(filepath).empty();
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_model(const hf_cache::hf_files & files,
|
||||
@@ -765,8 +872,27 @@ common_download_hf_plan common_download_get_hf_plan(const common_params_model &
|
||||
}
|
||||
} else {
|
||||
primary = find_best_model(all, tag);
|
||||
|
||||
// a `<quant>-<sidecar>` tag (e.g. `Q4_0-mtp`) requests that sidecar alone;
|
||||
// every token-bearing file is a sidecar (find_best_model skips them),
|
||||
// so the sidecar resolves here whenever the tag carries one
|
||||
auto [base_tag, sidecar] = split_sidecar_tag(tag);
|
||||
|
||||
if (primary.path.empty() && !sidecar.empty()) {
|
||||
auto found = find_best_sibling(all, "", sidecar, base_tag);
|
||||
|
||||
if (!found.path.empty()) {
|
||||
if (sidecar == "mtp") plan.mtp = found;
|
||||
else if (sidecar == "eagle3") plan.eagle3 = found;
|
||||
else if (sidecar == "dflash") plan.dflash = found;
|
||||
else if (sidecar == "dspark") plan.dspark = found;
|
||||
else plan.mmproj = found;
|
||||
}
|
||||
}
|
||||
|
||||
// a requested sidecar can resolve on its own, without a full model of the same tag
|
||||
if (primary.path.empty() && !opts.download_mtp && !opts.download_dflash && !opts.download_eagle3 && !opts.download_dspark) {
|
||||
if (primary.path.empty() && sidecar.empty() &&
|
||||
!opts.download_mtp && !opts.download_dflash && !opts.download_eagle3 && !opts.download_dspark) {
|
||||
LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str());
|
||||
list_available_gguf_files(all);
|
||||
return plan;
|
||||
@@ -794,7 +920,7 @@ common_download_hf_plan common_download_get_hf_plan(const common_params_model &
|
||||
plan.dspark = find_best_dspark(all, primary.path, tag);
|
||||
}
|
||||
|
||||
if (primary.path.empty() &&
|
||||
if (primary.path.empty() && plan.mmproj.local_path.empty() &&
|
||||
plan.mtp.local_path.empty() && plan.dflash.local_path.empty() && plan.eagle3.local_path.empty() && plan.dspark.local_path.empty()) {
|
||||
LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str());
|
||||
list_available_gguf_files(all);
|
||||
@@ -968,17 +1094,34 @@ std::vector<common_cached_model_info> common_list_cached_models() {
|
||||
auto files = hf_cache::get_cached_files();
|
||||
|
||||
for (const auto & f : files) {
|
||||
auto split = get_gguf_split_info(f.path);
|
||||
if (split.index != 1 || split.tag.empty() ||
|
||||
split.prefix.find("mmproj") != std::string::npos ||
|
||||
split.prefix.find("mtp-") != std::string::npos ||
|
||||
split.prefix.find("eagle3-") != std::string::npos ||
|
||||
split.prefix.find("dflash-") != std::string::npos ||
|
||||
split.prefix.find("dspark-") != std::string::npos) {
|
||||
continue;
|
||||
// a sidecar file is listed under its own `<quant>-<sidecar>` tag, so a
|
||||
// cached `mtp-Model-Q4_0.gguf`, `Model-mtp-Q4_0.gguf`, `Model-Q4_0-mtp.gguf`
|
||||
// or short `mmproj-F16.gguf` shows up as `<repo>:Q4_0-mtp` / `<repo>:F16-mmproj`;
|
||||
// files whose name carries no token stay loadable models
|
||||
auto token = sidecar_token_of(f.path);
|
||||
|
||||
std::string tag;
|
||||
if (token.empty()) {
|
||||
auto split = get_gguf_split_info(f.path);
|
||||
|
||||
if (split.index != 1 || split.tag.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tag = split.tag;
|
||||
} else {
|
||||
tag = sidecar_quant(f.path, token);
|
||||
|
||||
// a bare sidecar file has no tag to request it by, stay hidden
|
||||
if (tag.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tag += "-" + token;
|
||||
}
|
||||
if (seen.insert(f.repo_id + ":" + split.tag).second) {
|
||||
result.push_back({f.repo_id, split.tag});
|
||||
|
||||
if (seen.insert(f.repo_id + ":" + tag).second) {
|
||||
result.push_back({f.repo_id, tag});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1014,7 +1157,15 @@ bool common_download_remove(const std::string & hf_repo_with_tag) {
|
||||
return hf_cache::remove_cached_repo(repo_id);
|
||||
}
|
||||
|
||||
std::string tag_upper = tag;
|
||||
// a `<quant>-<sidecar>` tag (`Q4_0-mtp`) targets that sidecar alone; a bare
|
||||
// sidecar tag (`mtp`) is ambiguous across quants and is rejected
|
||||
auto [base_tag, sidecar] = split_sidecar_tag(tag);
|
||||
if (!sidecar.empty() && base_tag.empty()) {
|
||||
LOG_ERR("%s: bare sidecar tag '%s': use `<quant>-<sidecar>`\n", __func__, tag.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string tag_upper = sidecar.empty() ? tag : base_tag;
|
||||
for (char & c : tag_upper) {
|
||||
c = (char) std::toupper((unsigned char) c);
|
||||
}
|
||||
@@ -1024,13 +1175,25 @@ bool common_download_remove(const std::string & hf_repo_with_tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// collect snapshot entries whose tag matches
|
||||
// collect the snapshot entries the tag selects; sidecar files keep their
|
||||
// own tags, so a plain quant tag never removes them
|
||||
std::vector<fs::path> to_remove;
|
||||
for (const auto & f : files) {
|
||||
auto split = get_gguf_split_info(f.path);
|
||||
if (split.tag == tag_upper) {
|
||||
to_remove.emplace_back(f.local_path);
|
||||
auto token = sidecar_token_of(f.path);
|
||||
|
||||
if (sidecar.empty()) {
|
||||
if (!token.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (get_gguf_split_info(f.path).tag != tag_upper) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (token != sidecar || sidecar_quant(f.path, sidecar) != tag_upper) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
to_remove.emplace_back(f.local_path);
|
||||
}
|
||||
|
||||
if (to_remove.empty()) {
|
||||
|
||||
@@ -117,6 +117,7 @@ caps caps_get(jinja::program & prog) {
|
||||
|
||||
JJ_DEBUG("%s\n", ">>> Running capability check: typed content");
|
||||
|
||||
bool checks_for_string = false;
|
||||
static const std::string content_marker = "STRING_MARKER";
|
||||
|
||||
// case: typed content support
|
||||
@@ -136,6 +137,10 @@ caps caps_get(jinja::program & prog) {
|
||||
[&](context &, bool success, value & messages, value &, const std::string & rendered) {
|
||||
auto & content = messages->at(0)->at("content");
|
||||
caps_print_stats(content, "messages[0].content");
|
||||
if (has_op(content, "test_is_string")) {
|
||||
// checked if content is string
|
||||
checks_for_string = true;
|
||||
}
|
||||
bool used_as_array = has_op(content, "selectattr") || has_op(content, "array_access");
|
||||
if (used_as_array) {
|
||||
// accessed as an array
|
||||
@@ -151,6 +156,33 @@ caps caps_get(jinja::program & prog) {
|
||||
}
|
||||
);
|
||||
|
||||
if (checks_for_string) {
|
||||
caps_try_execute(
|
||||
prog,
|
||||
[&]() {
|
||||
// messages
|
||||
return json::array({
|
||||
{
|
||||
{"role", "user"},
|
||||
{"content", json::array({
|
||||
})}
|
||||
}
|
||||
});
|
||||
},
|
||||
nullptr, // ctx_fn
|
||||
nullptr, // tools_fn
|
||||
[&](context &, bool success, value & messages, value &, const std::string &) {
|
||||
auto & content = messages->at(0)->at("content");
|
||||
caps_print_stats(content, "messages[0].content");
|
||||
bool used_as_array = has_op(content, "selectattr") || has_op(content, "array_access");
|
||||
if (used_as_array && success) {
|
||||
// accessed as an array
|
||||
result.supports_typed_content = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
JJ_DEBUG("%s\n", ">>> Running capability check: system prompt");
|
||||
|
||||
// case: system prompt support
|
||||
|
||||
@@ -412,12 +412,18 @@ value test_expression::execute_impl(context & ctx) {
|
||||
throw std::runtime_error("Invalid test expression");
|
||||
}
|
||||
|
||||
auto it = builtins.find("test_is_" + test_id);
|
||||
JJ_DEBUG("Test expression %s '%s' %s (using function 'test_is_%s')", operand->type().c_str(), test_id.c_str(), negate ? "(negate)" : "", test_id.c_str());
|
||||
const std::string test_name = "test_is_" + test_id;
|
||||
auto it = builtins.find(test_name);
|
||||
JJ_DEBUG("Test expression %s '%s' %s (using function '%s')", operand->type().c_str(), test_id.c_str(), negate ? "(negate)" : "", test_name.c_str());
|
||||
if (it == builtins.end()) {
|
||||
throw std::runtime_error("Unknown test '" + test_id + "'");
|
||||
}
|
||||
|
||||
if (ctx.is_get_stats) {
|
||||
value_t::stats_t::mark_used(input);
|
||||
input->stats.ops.insert(test_name);
|
||||
}
|
||||
|
||||
auto res = it->second(args);
|
||||
|
||||
if (negate) {
|
||||
|
||||
+22
-89
@@ -9,20 +9,6 @@ from .base import ModelBase, gguf, logger
|
||||
from .deepseek import DeepseekV2Model
|
||||
|
||||
|
||||
def split_kv_b_proj(weight: torch.Tensor, n_head: int, qk_nope: int, v_head_dim: int):
|
||||
"""Split kv_b_proj into k_b (transposed) and v_b, matching DeepSeek MLA absorption.
|
||||
|
||||
weight: [n_head*(qk_nope+v_head_dim), kv_lora_rank].
|
||||
Returns (k_b, v_b): k_b [n_head, kv_lora_rank, qk_nope], v_b [n_head, v_head_dim, kv_lora_rank].
|
||||
"""
|
||||
kv_lora = weight.shape[-1]
|
||||
assert weight.shape[0] == n_head * (qk_nope + v_head_dim)
|
||||
kv_b = weight.view(n_head, qk_nope + v_head_dim, kv_lora)
|
||||
k_b, v_b = torch.split(kv_b, [qk_nope, v_head_dim], dim=1)
|
||||
k_b = k_b.transpose(1, 2).contiguous() # [n_head, kv_lora, qk_nope]
|
||||
return k_b, v_b.contiguous()
|
||||
|
||||
|
||||
def split_gate_up(weight: torch.Tensor, moe_intermediate_size: int):
|
||||
"""Split a fused stacked gate_up expert tensor into (gate, up).
|
||||
|
||||
@@ -36,6 +22,7 @@ def split_gate_up(weight: torch.Tensor, moe_intermediate_size: int):
|
||||
|
||||
|
||||
@ModelBase.register("HYV4ForCausalLM")
|
||||
@ModelBase.example("tencent/Hy4-preview")
|
||||
class HYV4Model(DeepseekV2Model):
|
||||
"""HY_V4: DeepSeek-V3 style MLA + MoE with iHC, a gated MLA output and a learnable sink.
|
||||
|
||||
@@ -54,6 +41,8 @@ class HYV4Model(DeepseekV2Model):
|
||||
|
||||
model_arch = gguf.MODEL_ARCH.HY_V4
|
||||
|
||||
merge_expert = False
|
||||
|
||||
# tensors a "full" indexer layer must carry
|
||||
INDEXER_SUFFIXES = frozenset({
|
||||
"self_attn.indexer.wq_b.weight",
|
||||
@@ -186,6 +175,10 @@ class HYV4Model(DeepseekV2Model):
|
||||
)
|
||||
|
||||
def prepare_tensors(self):
|
||||
# Hy4-preview for some reason has num_key_value_heads equal to 8, so override it here
|
||||
# without this conversion/deepseek.py fails on assert
|
||||
self.hparams["num_key_value_heads"] = self.hparams["num_attention_heads"]
|
||||
|
||||
# validate before the base materializes tensors, so a mismatch fails early
|
||||
is_full = self.indexer_is_full()
|
||||
if is_full is not None:
|
||||
@@ -227,85 +220,25 @@ class HYV4Model(DeepseekV2Model):
|
||||
|
||||
def modify_tensors(self, data_torch: torch.Tensor, name: str, bid: int | None) -> Iterable[tuple[str, torch.Tensor]]:
|
||||
hparams = self.hparams
|
||||
n_head = hparams["num_attention_heads"]
|
||||
qk_nope = hparams["qk_nope_head_dim"]
|
||||
v_head_dim = hparams["v_head_dim"]
|
||||
moe_inter = hparams["moe_intermediate_size"]
|
||||
|
||||
tn = self.format_tensor_name
|
||||
|
||||
# ---- global (non per-layer) ----
|
||||
if name == "model.embed_tokens.weight":
|
||||
return [(tn(gguf.MODEL_TENSOR.TOKEN_EMBD), data_torch)]
|
||||
if name == "model.norm.weight":
|
||||
return [(tn(gguf.MODEL_TENSOR.OUTPUT_NORM), data_torch)]
|
||||
if name == "lm_head.weight":
|
||||
return [(tn(gguf.MODEL_TENSOR.OUTPUT), data_torch)]
|
||||
if name == "model.hc_head.hc_head_fn":
|
||||
return [(tn(gguf.MODEL_TENSOR.HC_HEAD_FN), data_torch)]
|
||||
if name == "model.hc_head.hc_head_base":
|
||||
return [(tn(gguf.MODEL_TENSOR.HC_HEAD_BASE), data_torch)]
|
||||
if name == "model.hc_head.hc_head_scale":
|
||||
return [(tn(gguf.MODEL_TENSOR.HC_HEAD_SCALE), data_torch)]
|
||||
|
||||
assert bid is not None, f"expected a per-layer tensor, got {name!r}"
|
||||
|
||||
# ---- per-layer, keyed by suffix after 'model.layers.{bid}.' ----
|
||||
suffix = name.split(f"model.layers.{bid}.", 1)[-1]
|
||||
|
||||
# note: q_b_proj and kv_a_proj_with_mqa are mapped straight through (no RoPE permute),
|
||||
# the graph rotates consecutive pairs so the rows need no reordering
|
||||
simple = {
|
||||
"input_layernorm.weight": (gguf.MODEL_TENSOR.ATTN_NORM, ".weight"),
|
||||
"post_attention_layernorm.weight": (gguf.MODEL_TENSOR.FFN_NORM, ".weight"),
|
||||
"self_attn.q_a_proj.weight": (gguf.MODEL_TENSOR.ATTN_Q_A, ".weight"),
|
||||
"self_attn.q_a_layernorm.weight": (gguf.MODEL_TENSOR.ATTN_Q_A_NORM, ".weight"),
|
||||
"self_attn.q_b_proj.weight": (gguf.MODEL_TENSOR.ATTN_Q_B, ".weight"),
|
||||
"self_attn.kv_a_proj_with_mqa.weight": (gguf.MODEL_TENSOR.ATTN_KV_A_MQA, ".weight"),
|
||||
"self_attn.kv_a_layernorm.weight": (gguf.MODEL_TENSOR.ATTN_KV_A_NORM, ".weight"),
|
||||
"self_attn.o_proj.weight": (gguf.MODEL_TENSOR.ATTN_OUT, ".weight"),
|
||||
"self_attn.linear_gate.weight": (gguf.MODEL_TENSOR.ATTN_GATE, ".weight"),
|
||||
"self_attn.learnable_sink_param": (gguf.MODEL_TENSOR.ATTN_SINKS, ".weight"),
|
||||
"self_attn.indexer.wq_b.weight": (gguf.MODEL_TENSOR.INDEXER_ATTN_Q_B, ".weight"),
|
||||
"self_attn.indexer.wk.weight": (gguf.MODEL_TENSOR.INDEXER_ATTN_K, ".weight"),
|
||||
"self_attn.indexer.k_norm.weight": (gguf.MODEL_TENSOR.INDEXER_K_NORM, ".weight"),
|
||||
"self_attn.indexer.k_norm.bias": (gguf.MODEL_TENSOR.INDEXER_K_NORM, ".bias"),
|
||||
"self_attn.indexer.weights_proj.weight": (gguf.MODEL_TENSOR.INDEXER_PROJ, ".weight"),
|
||||
"hc_attn_layer.hc_pre.hc_fn": (gguf.MODEL_TENSOR.HC_ATTN_FN, ".weight"),
|
||||
"hc_attn_layer.hc_pre.hc_base": (gguf.MODEL_TENSOR.HC_ATTN_BASE, ".weight"),
|
||||
"hc_attn_layer.hc_pre.hc_scale": (gguf.MODEL_TENSOR.HC_ATTN_SCALE, ".weight"),
|
||||
"hc_mlp_layer.hc_pre.hc_fn": (gguf.MODEL_TENSOR.HC_FFN_FN, ".weight"),
|
||||
"hc_mlp_layer.hc_pre.hc_base": (gguf.MODEL_TENSOR.HC_FFN_BASE, ".weight"),
|
||||
"hc_mlp_layer.hc_pre.hc_scale": (gguf.MODEL_TENSOR.HC_FFN_SCALE, ".weight"),
|
||||
"mlp.gate.weight": (gguf.MODEL_TENSOR.FFN_GATE_INP, ".weight"),
|
||||
"mlp.gate.e_score_correction.bias":(gguf.MODEL_TENSOR.FFN_EXP_PROBS_B, ".bias"),
|
||||
"mlp.gate_proj.weight": (gguf.MODEL_TENSOR.FFN_GATE, ".weight"),
|
||||
"mlp.up_proj.weight": (gguf.MODEL_TENSOR.FFN_UP, ".weight"),
|
||||
"mlp.down_proj.weight": (gguf.MODEL_TENSOR.FFN_DOWN, ".weight"),
|
||||
"mlp.shared_experts.gate_proj.weight": (gguf.MODEL_TENSOR.FFN_GATE_SHEXP, ".weight"),
|
||||
"mlp.shared_experts.up_proj.weight": (gguf.MODEL_TENSOR.FFN_UP_SHEXP, ".weight"),
|
||||
"mlp.shared_experts.down_proj.weight": (gguf.MODEL_TENSOR.FFN_DOWN_SHEXP, ".weight"),
|
||||
}
|
||||
if suffix in simple:
|
||||
key, sfx = simple[suffix]
|
||||
return [(tn(key, bid, sfx), data_torch)]
|
||||
|
||||
# kv_b_proj: split into k_b (transposed) and v_b
|
||||
if suffix == "self_attn.kv_b_proj.weight":
|
||||
k_b, v_b = split_kv_b_proj(data_torch, n_head, qk_nope, v_head_dim)
|
||||
return [
|
||||
(tn(gguf.MODEL_TENSOR.ATTN_K_B, bid), k_b),
|
||||
(tn(gguf.MODEL_TENSOR.ATTN_V_B, bid), v_b),
|
||||
]
|
||||
|
||||
# fused stacked experts: split gate_up into gate/up
|
||||
if suffix == "mlp.experts.gate_up_proj":
|
||||
if name.endswith("mlp.experts.gate_up_proj"):
|
||||
gate, up = split_gate_up(data_torch, moe_inter)
|
||||
return [
|
||||
(tn(gguf.MODEL_TENSOR.FFN_GATE_EXP, bid), gate),
|
||||
(tn(gguf.MODEL_TENSOR.FFN_UP_EXP, bid), up),
|
||||
]
|
||||
if suffix == "mlp.experts.down_proj":
|
||||
return [(tn(gguf.MODEL_TENSOR.FFN_DOWN_EXP, bid), data_torch)]
|
||||
yield from super().modify_tensors(gate, tn(gguf.MODEL_TENSOR.FFN_GATE_EXP, bid), bid)
|
||||
yield from super().modify_tensors(up, tn(gguf.MODEL_TENSOR.FFN_UP_EXP, bid), bid)
|
||||
return
|
||||
|
||||
raise ValueError(f"Unsupported HY_V4 tensor {name!r} (suffix {suffix!r})")
|
||||
# add .weight suffixes
|
||||
if name.endswith("mlp.experts.down_proj") or name.endswith(".self_attn.learnable_sink_param"):
|
||||
name += ".weight"
|
||||
|
||||
if re.search(r"\.hc_head\.hc_head_(?:fn|base|scale)$", name):
|
||||
name += ".weight"
|
||||
|
||||
if re.search(r"\.hc_(?:attn|mlp)_layer\.hc_pre\.hc_(?:fn|base|scale)$", name):
|
||||
name += ".weight"
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
@@ -379,6 +379,13 @@ class Qwen3NextModel(_QwenMtpMixin, Qwen2MoeModel):
|
||||
self.gguf_writer.add_ssm_group_count(self.hparams["linear_num_key_heads"])
|
||||
self.gguf_writer.add_ssm_time_step_rank(self.hparams["linear_num_value_heads"])
|
||||
self.gguf_writer.add_ssm_inner_size(self.hparams["linear_value_head_dim"] * self.hparams["linear_num_value_heads"])
|
||||
if (layer_types := self.hparams.get("layer_types")) is not None:
|
||||
n_layer = self.hparams["num_hidden_layers"]
|
||||
if len(layer_types) != n_layer:
|
||||
raise ValueError(f"layer_types has {len(layer_types)} entries, expected num_hidden_layers ({n_layer})")
|
||||
recurrent = [t == "linear_attention" for t in layer_types]
|
||||
recurrent += [False] * (self.block_count - n_layer)
|
||||
self.gguf_writer.add_recurrent_layers(recurrent)
|
||||
self.gguf_writer.add_full_attention_interval(self.hparams.get("full_attention_interval", 4))
|
||||
if (rope_dim := self.hparams.get("head_dim")) is None:
|
||||
rope_dim = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
#include <cstdio>
|
||||
|
||||
int main(void) {
|
||||
printf("[test-cmake] version: %s, build: %d (%s)\n",
|
||||
printf("[test-cmake] llama.cpp version: %s, build: %d (%s)\n",
|
||||
llama_version(), LLAMA_BUILD_NUMBER, LLAMA_BUILD_COMMIT);
|
||||
printf("[test-cmake] ggml version: %s, commit: %s\n", ggml_version(), ggml_commit());
|
||||
printf("[test-cmake] Initializing backend...\n");
|
||||
llama_backend_init();
|
||||
printf("[test-cmake] Backend initialized.\n");
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
}:
|
||||
{
|
||||
# For standardised reproducible formatting with `nix fmt`
|
||||
formatter = pkgs.nixfmt-rfc-style;
|
||||
formatter = pkgs.nixfmt;
|
||||
|
||||
# Unlike `.#packages`, legacyPackages may contain values of
|
||||
# arbitrary types (including nested attrsets) and may even throw
|
||||
@@ -156,7 +156,7 @@
|
||||
windows = config.legacyPackages.llamaPackagesWindows.llama-cpp;
|
||||
python-scripts = config.legacyPackages.llamaPackages.python-scripts;
|
||||
}
|
||||
// lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||
// lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
|
||||
cuda = config.legacyPackages.llamaPackagesCuda.llama-cpp;
|
||||
|
||||
mpi-cpu = config.packages.default.override { useMpi = true; };
|
||||
|
||||
@@ -849,7 +849,7 @@ static void ggml_backend_sched_split_inputs_grow(struct ggml_backend_sched_split
|
||||
int new_cap = GGML_SCHED_MAX_SPLIT_INPUTS;
|
||||
if (split->inputs_capacity > 0) {
|
||||
new_cap = 2*split->inputs_capacity;
|
||||
GGML_LOG_WARN("%s: increasing split inputs capacity from %d to %d\n", __func__, split->inputs_capacity, new_cap);
|
||||
GGML_LOG_DEBUG("%s: increasing split inputs capacity from %d to %d\n", __func__, split->inputs_capacity, new_cap);
|
||||
}
|
||||
auto * pnew = (struct ggml_tensor **) realloc((void *) split->inputs, new_cap * sizeof(struct ggml_tensor *));
|
||||
if (pnew == NULL) {
|
||||
@@ -864,7 +864,7 @@ static void ggml_backend_sched_graph_inputs_grow(ggml_backend_sched_t sched) {
|
||||
int new_cap = GGML_SCHED_MAX_SPLIT_INPUTS;
|
||||
if (sched->graph_inputs_capacity > 0) {
|
||||
new_cap = 2*sched->graph_inputs_capacity;
|
||||
GGML_LOG_WARN("%s: increasing graph inputs capacity from %d to %d\n", __func__, sched->graph_inputs_capacity, new_cap);
|
||||
GGML_LOG_DEBUG("%s: increasing graph inputs capacity from %d to %d\n", __func__, sched->graph_inputs_capacity, new_cap);
|
||||
}
|
||||
auto * pnew = (struct ggml_tensor **) realloc((void *) sched->graph_inputs, new_cap * sizeof(struct ggml_tensor *));
|
||||
if (pnew == NULL) {
|
||||
@@ -1338,17 +1338,6 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra
|
||||
break;
|
||||
}
|
||||
}
|
||||
// check if the split has too many inputs
|
||||
// FIXME: count the number of inputs instead of only checking when full
|
||||
if (split->n_inputs >= split->inputs_capacity) {
|
||||
const size_t id = hash_id(src);
|
||||
int src_backend_id = sched->hv_tensor_backend_ids[id];
|
||||
bool supported = ggml_backend_sched_buffer_supported(sched, src, cur_backend_id);
|
||||
if (src_backend_id != cur_backend_id && tensor_id_copy(id, cur_backend_id, 0) == NULL && !supported) {
|
||||
need_new_split = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
#define GGML_CUDA_CC_GCN4 (GGML_CUDA_CC_OFFSET_AMD + 0x803) // Tonga, Fiji, Polaris, minimum for fast fp16
|
||||
#define GGML_CUDA_CC_VEGA (GGML_CUDA_CC_OFFSET_AMD + 0x900) // Vega56/64, minimum for fp16 dual issue
|
||||
#define GGML_CUDA_CC_VEGA20 (GGML_CUDA_CC_OFFSET_AMD + 0x906) // MI50/Radeon VII, minimum for dp4a
|
||||
#define GGML_CUDA_CC_GFX909 (GGML_CUDA_CC_OFFSET_AMD + 0x909) // GCN APU
|
||||
#define GGML_CUDA_CC_GFX90C (GGML_CUDA_CC_OFFSET_AMD + 0x90c) // GCN APU
|
||||
#define GGML_CUDA_CC_CDNA1 (GGML_CUDA_CC_OFFSET_AMD + 0x908) // MI100, minimum for MFMA, acc registers
|
||||
#define GGML_CUDA_CC_CDNA2 (GGML_CUDA_CC_OFFSET_AMD + 0x90a) // MI210 (gfx90a), minimum acc register renaming
|
||||
#define GGML_CUDA_CC_CDNA3 (GGML_CUDA_CC_OFFSET_AMD + 0x942) // MI300
|
||||
@@ -89,12 +91,13 @@
|
||||
#define GGML_CUDA_CC_IS_RDNA3_5(cc) (cc >= GGML_CUDA_CC_RDNA3_5 && cc < GGML_CUDA_CC_RDNA4)
|
||||
#define GGML_CUDA_CC_IS_RDNA3(cc) (GGML_CUDA_CC_IS_RDNA3_0(cc) || GGML_CUDA_CC_IS_RDNA3_5(cc))
|
||||
#define GGML_CUDA_CC_IS_RDNA4(cc) (cc >= GGML_CUDA_CC_RDNA4)
|
||||
#define GGML_CUDA_CC_IS_GCN(cc) (cc > GGML_CUDA_CC_OFFSET_AMD && cc < GGML_CUDA_CC_CDNA1)
|
||||
#define GGML_CUDA_CC_IS_CDNA(cc) (cc >= GGML_CUDA_CC_CDNA1 && cc < GGML_CUDA_CC_RDNA1)
|
||||
#define GGML_CUDA_CC_IS_CDNA1(cc) (cc >= GGML_CUDA_CC_CDNA1 && cc < GGML_CUDA_CC_CDNA2)
|
||||
#define GGML_CUDA_CC_IS_CDNA2(cc) (cc >= GGML_CUDA_CC_CDNA2 && cc < GGML_CUDA_CC_CDNA3)
|
||||
#define GGML_CUDA_CC_IS_CDNA3(cc) (cc >= GGML_CUDA_CC_CDNA3 && cc < GGML_CUDA_CC_CDNA4)
|
||||
#define GGML_CUDA_CC_IS_CDNA4(cc) (cc >= GGML_CUDA_CC_CDNA4 && cc < GGML_CUDA_CC_RDNA1)
|
||||
#define GGML_CUDA_CC_IS_GCN_APU(cc) ((cc) == GGML_CUDA_CC_GFX909 || (cc) == GGML_CUDA_CC_GFX90C)
|
||||
#define GGML_CUDA_CC_IS_GCN(cc) ((cc > GGML_CUDA_CC_OFFSET_AMD && cc < GGML_CUDA_CC_CDNA1) || GGML_CUDA_CC_IS_GCN_APU(cc))
|
||||
#define GGML_CUDA_CC_IS_CDNA(cc) (!GGML_CUDA_CC_IS_GCN_APU(cc) && cc >= GGML_CUDA_CC_CDNA1 && cc < GGML_CUDA_CC_RDNA1)
|
||||
#define GGML_CUDA_CC_IS_CDNA1(cc) (GGML_CUDA_CC_IS_CDNA(cc) && cc >= GGML_CUDA_CC_CDNA1 && cc < GGML_CUDA_CC_CDNA2)
|
||||
#define GGML_CUDA_CC_IS_CDNA2(cc) (GGML_CUDA_CC_IS_CDNA(cc) && cc >= GGML_CUDA_CC_CDNA2 && cc < GGML_CUDA_CC_CDNA3)
|
||||
#define GGML_CUDA_CC_IS_CDNA3(cc) (GGML_CUDA_CC_IS_CDNA(cc) && cc >= GGML_CUDA_CC_CDNA3 && cc < GGML_CUDA_CC_CDNA4)
|
||||
#define GGML_CUDA_CC_IS_CDNA4(cc) (GGML_CUDA_CC_IS_CDNA(cc) && cc >= GGML_CUDA_CC_CDNA4 && cc < GGML_CUDA_CC_RDNA1)
|
||||
|
||||
// Moore Threads
|
||||
#define MUSART_HMASK 40300 // MUSA rc4.3, min. ver. for half2 -> uint mask comparisons
|
||||
@@ -976,6 +979,7 @@ template<>
|
||||
struct ggml_cuda_type_traits<GGML_TYPE_F16> {
|
||||
static constexpr int qk = 1;
|
||||
static constexpr int qr = 1;
|
||||
static constexpr int bs = sizeof(ggml_half);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -983,6 +987,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q1_0> {
|
||||
static constexpr int qk = QK1_0;
|
||||
static constexpr int qr = QR1_0;
|
||||
static constexpr int qi = QI1_0;
|
||||
static constexpr int bs = sizeof(block_q1_0);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -990,6 +995,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q2_0> {
|
||||
static constexpr int qk = QK2_0;
|
||||
static constexpr int qr = QR2_0;
|
||||
static constexpr int qi = QI2_0;
|
||||
static constexpr int bs = sizeof(block_q2_0);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -997,6 +1003,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q4_0> {
|
||||
static constexpr int qk = QK4_0;
|
||||
static constexpr int qr = QR4_0;
|
||||
static constexpr int qi = QI4_0;
|
||||
static constexpr int bs = sizeof(block_q4_0);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1004,6 +1011,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q4_1> {
|
||||
static constexpr int qk = QK4_1;
|
||||
static constexpr int qr = QR4_1;
|
||||
static constexpr int qi = QI4_1;
|
||||
static constexpr int bs = sizeof(block_q4_1);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1011,6 +1019,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q5_0> {
|
||||
static constexpr int qk = QK5_0;
|
||||
static constexpr int qr = QR5_0;
|
||||
static constexpr int qi = QI5_0;
|
||||
static constexpr int bs = sizeof(block_q5_0);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1018,6 +1027,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q5_1> {
|
||||
static constexpr int qk = QK5_1;
|
||||
static constexpr int qr = QR5_1;
|
||||
static constexpr int qi = QI5_1;
|
||||
static constexpr int bs = sizeof(block_q5_1);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1025,6 +1035,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q8_0> {
|
||||
static constexpr int qk = QK8_0;
|
||||
static constexpr int qr = QR8_0;
|
||||
static constexpr int qi = QI8_0;
|
||||
static constexpr int bs = sizeof(block_q8_0);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1032,6 +1043,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_MXFP4> {
|
||||
static constexpr int qk = QK_MXFP4;
|
||||
static constexpr int qr = QR_MXFP4;
|
||||
static constexpr int qi = QI_MXFP4;
|
||||
static constexpr int bs = sizeof(block_mxfp4);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1039,6 +1051,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_NVFP4> {
|
||||
static constexpr int qk = QK_NVFP4;
|
||||
static constexpr int qr = QR_NVFP4;
|
||||
static constexpr int qi = QI_NVFP4;
|
||||
static constexpr int bs = sizeof(block_nvfp4);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1046,6 +1059,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q2_K> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR2_K;
|
||||
static constexpr int qi = QI2_K;
|
||||
static constexpr int bs = sizeof(block_q2_K);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1053,6 +1067,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q3_K> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR3_K;
|
||||
static constexpr int qi = QI3_K;
|
||||
static constexpr int bs = sizeof(block_q3_K);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1060,6 +1075,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q4_K> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR4_K;
|
||||
static constexpr int qi = QI4_K;
|
||||
static constexpr int bs = sizeof(block_q4_K);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1067,6 +1083,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q5_K> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR5_K;
|
||||
static constexpr int qi = QI5_K;
|
||||
static constexpr int bs = sizeof(block_q5_K);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1074,6 +1091,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_Q6_K> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR6_K;
|
||||
static constexpr int qi = QI6_K;
|
||||
static constexpr int bs = sizeof(block_q6_K);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1081,6 +1099,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ2_XXS> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR2_XXS;
|
||||
static constexpr int qi = QI2_XXS;
|
||||
static constexpr int bs = sizeof(block_iq2_xxs);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1088,6 +1107,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ2_XS> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR2_XS;
|
||||
static constexpr int qi = QI2_XS;
|
||||
static constexpr int bs = sizeof(block_iq2_xs);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1095,6 +1115,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ2_S> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR2_S;
|
||||
static constexpr int qi = QI2_S;
|
||||
static constexpr int bs = sizeof(block_iq2_s);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1102,6 +1123,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ3_XXS> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR3_XXS;
|
||||
static constexpr int qi = QI3_XXS;
|
||||
static constexpr int bs = sizeof(block_iq3_xxs);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1109,6 +1131,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ1_S> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR1_S;
|
||||
static constexpr int qi = QI1_S;
|
||||
static constexpr int bs = sizeof(block_iq1_s);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1116,6 +1139,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ1_M> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR1_M;
|
||||
static constexpr int qi = QI1_M;
|
||||
static constexpr int bs = sizeof(block_iq1_m);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1123,6 +1147,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ4_NL> {
|
||||
static constexpr int qk = QK4_NL;
|
||||
static constexpr int qr = QR4_NL;
|
||||
static constexpr int qi = QI4_NL;
|
||||
static constexpr int bs = sizeof(block_iq4_nl);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1130,6 +1155,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ4_XS> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR4_XS;
|
||||
static constexpr int qi = QI4_XS;
|
||||
static constexpr int bs = sizeof(block_iq4_xs);
|
||||
};
|
||||
|
||||
template<>
|
||||
@@ -1137,6 +1163,7 @@ struct ggml_cuda_type_traits<GGML_TYPE_IQ3_S> {
|
||||
static constexpr int qk = QK_K;
|
||||
static constexpr int qr = QR3_S;
|
||||
static constexpr int qi = QI3_S;
|
||||
static constexpr int bs = sizeof(block_iq3_s);
|
||||
};
|
||||
|
||||
//////////////////////
|
||||
|
||||
@@ -1545,77 +1545,77 @@ static __device__ __forceinline__ void flash_attn_ext_f16_process_tile(
|
||||
}
|
||||
}
|
||||
|
||||
if (np > 1 && threadIdx.y % np == 0) {
|
||||
// Combine the meta data for parallel warps via shared memory.
|
||||
// Warps with threadIdx.y % np != 0 must NOT return early.
|
||||
// All threads must return simultaneously to avoid race conditions with work on the next tile.
|
||||
|
||||
if (np > 1) {
|
||||
constexpr int nmeta = np*cols_per_warp >= warp_size ? np*cols_per_warp/warp_size : 1;
|
||||
|
||||
float KQ_cmn;
|
||||
float KQ_cms[nmeta];
|
||||
float KQ_crs;
|
||||
|
||||
const int jc_meta = threadIdx.y*cols_per_warp + (np*cols_per_warp < warp_size ? threadIdx.x % (np*cols_per_warp) : threadIdx.x);
|
||||
float2 * const meta_ptr = ((float2 *) tile_Q) + jc_meta*(tile_stride/2) + nbatch_combine/2;
|
||||
float2 meta[nmeta];
|
||||
#pragma unroll
|
||||
for (int imeta = 0; imeta < nmeta; ++imeta) {
|
||||
meta[imeta] = meta_ptr[imeta * warp_size * tile_stride/2];
|
||||
}
|
||||
|
||||
float KQ_cmn = meta[0].x; // KQ combine max new, max between all parallel warps.
|
||||
if (threadIdx.y % np == 0) {
|
||||
// Combine the meta data for parallel warps via shared memory.
|
||||
float2 meta[nmeta];
|
||||
#pragma unroll
|
||||
for (int imeta = 1; imeta < nmeta; ++imeta) {
|
||||
KQ_cmn = fmaxf(KQ_cmn, meta[imeta].x);
|
||||
}
|
||||
#pragma unroll
|
||||
for (int offset = np*cols_per_warp/2; offset >= cols_per_warp; offset >>= 1) {
|
||||
if (offset < warp_size) {
|
||||
KQ_cmn = fmaxf(KQ_cmn, __shfl_xor_sync(0xFFFFFFFF, KQ_cmn, offset, warp_size));
|
||||
for (int imeta = 0; imeta < nmeta; ++imeta) {
|
||||
meta[imeta] = meta_ptr[imeta * warp_size * tile_stride/2];
|
||||
}
|
||||
}
|
||||
|
||||
float KQ_cms[nmeta]; // KQ combine max scale per warp.
|
||||
KQ_cmn = meta[0].x; // KQ combine max new, max between all parallel warps.
|
||||
#pragma unroll
|
||||
for (int imeta = 0; imeta < nmeta; ++imeta) {
|
||||
KQ_cms[imeta] = expf(meta[imeta].x - KQ_cmn);
|
||||
}
|
||||
for (int imeta = 1; imeta < nmeta; ++imeta) {
|
||||
KQ_cmn = fmaxf(KQ_cmn, meta[imeta].x);
|
||||
}
|
||||
#pragma unroll
|
||||
for (int offset = np*cols_per_warp/2; offset >= cols_per_warp; offset >>= 1) {
|
||||
if (offset < warp_size) {
|
||||
KQ_cmn = fmaxf(KQ_cmn, __shfl_xor_sync(0xFFFFFFFF, KQ_cmn, offset, warp_size));
|
||||
}
|
||||
}
|
||||
|
||||
float KQ_crs = KQ_cms[0]*meta[0].y; // KQ combine rowsum, scaled sum of all parallel warps.
|
||||
#pragma unroll
|
||||
for (int imeta = 1; imeta < nmeta; ++imeta) {
|
||||
KQ_crs += KQ_cms[imeta]*meta[imeta].y;
|
||||
}
|
||||
for (int imeta = 0; imeta < nmeta; ++imeta) {
|
||||
KQ_cms[imeta] = expf(meta[imeta].x - KQ_cmn);
|
||||
}
|
||||
|
||||
KQ_crs = KQ_cms[0]*meta[0].y; // KQ combine rowsum, scaled sum of all parallel warps.
|
||||
#pragma unroll
|
||||
for (int offset = np*cols_per_warp/2; offset >= cols_per_warp; offset >>= 1) {
|
||||
if (offset < warp_size) {
|
||||
KQ_crs += __shfl_xor_sync(0xFFFFFFFF, KQ_crs, offset, warp_size);
|
||||
for (int imeta = 1; imeta < nmeta; ++imeta) {
|
||||
KQ_crs += KQ_cms[imeta]*meta[imeta].y;
|
||||
}
|
||||
#pragma unroll
|
||||
for (int offset = np*cols_per_warp/2; offset >= cols_per_warp; offset >>= 1) {
|
||||
if (offset < warp_size) {
|
||||
KQ_crs += __shfl_xor_sync(0xFFFFFFFF, KQ_crs, offset, warp_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
|
||||
// Write back combined meta data:
|
||||
if (threadIdx.y % np == 0) {
|
||||
// Write back combined meta data:
|
||||
#pragma unroll
|
||||
for (int imeta = 0; imeta < nmeta; ++imeta) {
|
||||
if (np*cols_per_warp >= warp_size || threadIdx.x < np*cols_per_warp) {
|
||||
// Combined KQ max scale + rowsum.
|
||||
meta_ptr[imeta * warp_size * tile_stride/2] = make_float2(KQ_cms[imeta], KQ_crs);
|
||||
for (int imeta = 0; imeta < nmeta; ++imeta) {
|
||||
if (np*cols_per_warp >= warp_size || threadIdx.x < np*cols_per_warp) {
|
||||
// Combined KQ max scale + rowsum.
|
||||
meta_ptr[imeta * warp_size * tile_stride/2] = make_float2(KQ_cms[imeta], KQ_crs);
|
||||
}
|
||||
}
|
||||
|
||||
// Combined KQ max + rowsum.
|
||||
static_assert(cols_per_warp <= warp_size);
|
||||
if (needs_fixup && (cols_per_warp == warp_size || threadIdx.x < cols_per_warp)) {
|
||||
float2 * dstk_fixup_meta = dstk_fixup + blockIdx.x*ncols;
|
||||
dstk_fixup_meta[(threadIdx.y/np)*cols_per_warp + threadIdx.x] = make_float2(KQ_cmn, KQ_crs);
|
||||
}
|
||||
if (is_fixup && (cols_per_warp == warp_size || threadIdx.x < cols_per_warp)) {
|
||||
float2 * dstk_fixup_meta = dstk_fixup + (gridDim.x + blockIdx.x)*ncols;
|
||||
dstk_fixup_meta[(threadIdx.y/np)*cols_per_warp + threadIdx.x] = make_float2(KQ_cmn, KQ_crs);
|
||||
}
|
||||
}
|
||||
|
||||
// Combined KQ max + rowsum.
|
||||
static_assert(cols_per_warp <= warp_size);
|
||||
if (needs_fixup && (cols_per_warp == warp_size || threadIdx.x < cols_per_warp)) {
|
||||
float2 * dstk_fixup_meta = dstk_fixup + blockIdx.x*ncols;
|
||||
dstk_fixup_meta[(threadIdx.y/np)*cols_per_warp + threadIdx.x] = make_float2(KQ_cmn, KQ_crs);
|
||||
}
|
||||
if (is_fixup && (cols_per_warp == warp_size || threadIdx.x < cols_per_warp)) {
|
||||
float2 * dstk_fixup_meta = dstk_fixup + (gridDim.x + blockIdx.x)*ncols;
|
||||
dstk_fixup_meta[(threadIdx.y/np)*cols_per_warp + threadIdx.x] = make_float2(KQ_cmn, KQ_crs);
|
||||
}
|
||||
} else if (np > 1) {
|
||||
// Warps with threadIdx.y % np == 0 execute a __syncthreads() in the if branch.
|
||||
// Therefore, all other warps also need to execute a __syncthreads().
|
||||
// Otherwise the points at which warps synchronize with each other would become misaligned.
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
#pragma unroll
|
||||
|
||||
@@ -212,6 +212,7 @@ static int ggml_cuda_parse_id(char devName[]) {
|
||||
}
|
||||
archNum += archMajor * 0x100;
|
||||
archNum += archMinor;
|
||||
|
||||
return archNum;
|
||||
}
|
||||
#endif // defined(GGML_USE_HIP)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_ampere(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = false;
|
||||
CASE(GGML_TYPE_Q1_0, 256, 1, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
|
||||
@@ -379,5 +380,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
|
||||
CASE(GGML_TYPE_NVFP4, 256, 1, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 1, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_blackwell(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = false;
|
||||
CASE(GGML_TYPE_MXFP4, 256, 1, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
|
||||
CASE(GGML_TYPE_MXFP4, 256, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
|
||||
CASE(GGML_TYPE_MXFP4, 256, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_FP4, MMQ_ITER_K_FP4, true, true);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_cdna(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = false;
|
||||
CASE(GGML_TYPE_Q1_0, 512, 1, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
|
||||
CASE(GGML_TYPE_Q1_0, 512, 1, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
|
||||
CASE(GGML_TYPE_Q1_0, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, true, true);
|
||||
@@ -181,5 +182,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
|
||||
CASE(GGML_TYPE_NVFP4, 512, 1, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
|
||||
CASE(GGML_TYPE_NVFP4, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, true, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 512, 1, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_pascal_dp4a(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = false;
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
@@ -269,5 +270,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_pascal_older(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = false;
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
@@ -269,5 +270,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna2(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = false;
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
@@ -269,5 +270,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna3_5(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = false;
|
||||
CASE(GGML_TYPE_Q1_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
@@ -286,5 +287,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna3(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = true;
|
||||
CASE(GGML_TYPE_Q1_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
@@ -270,5 +271,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_rdna4(ggml_type type, int J, bool fallback) {
|
||||
constexpr bool use_typical_moe_ncols = true;
|
||||
CASE(GGML_TYPE_Q1_0, 128, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 128, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 128, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
@@ -286,5 +287,5 @@ static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_conf
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, use_typical_moe_ncols, false, true);
|
||||
}
|
||||
|
||||
@@ -375,10 +375,10 @@ bool ggml_cuda_should_use_mmq(enum ggml_type type, int cc, int64_t ne11, int64_t
|
||||
return true;
|
||||
}
|
||||
|
||||
// gfx900 (Vega 10) lacks native dp4a, loses to dequant + hipBLAS
|
||||
// gfx900 (Vega 10), gfx909, and gfx90c lack native dp4a, losing to dequant + hipBLAS
|
||||
// for dense matrices; keep MMQ only for MoE, where the
|
||||
// hipBLAS path is much slower.
|
||||
if (cc == GGML_CUDA_CC_VEGA) {
|
||||
if (cc == GGML_CUDA_CC_VEGA || GGML_CUDA_CC_IS_GCN_APU(cc)) {
|
||||
return n_experts > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,12 +170,13 @@ struct ggml_cuda_mmq_config {
|
||||
int J; // SRAM tile width in src1->ne[1]/dst->ne[1] direction.
|
||||
ggml_cuda_mmq_sram_layout sram_layout; // SRAM tile length in src0->ne[0]/src1->ne[0] direction (physical 32 bit elements).
|
||||
int K_vram; // VRAM tile length in src0->ne[0]/src1->ne[0] direction (logical elements).
|
||||
bool use_typical_moe_ncols;
|
||||
bool stream_k; // Whether or not to use stream-k decomposition.
|
||||
bool fallback; // Whether a fallback for out-of-bounds check in src0->ne[1] direction is needed.
|
||||
|
||||
constexpr __host__ __device__ ggml_cuda_mmq_config(
|
||||
ggml_type type, int nthreads, int occupancy, int I, int J, ggml_cuda_mmq_sram_layout sram_layout, int K_vram, bool stream_k, bool fallback) :
|
||||
type(type), nthreads(nthreads), occupancy(occupancy), I(I), J(J), sram_layout(sram_layout), K_vram(K_vram), stream_k(stream_k), fallback(fallback) {}
|
||||
ggml_type type, int nthreads, int occupancy, int I, int J, ggml_cuda_mmq_sram_layout sram_layout, int K_vram, bool use_typical_moe_ncols, bool stream_k, bool fallback) :
|
||||
type(type), nthreads(nthreads), occupancy(occupancy), I(I), J(J), sram_layout(sram_layout), K_vram(K_vram), use_typical_moe_ncols(use_typical_moe_ncols), stream_k(stream_k), fallback(fallback) {}
|
||||
|
||||
constexpr __device__ int rows_per_warp() const {
|
||||
#if defined(AMD_MFMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE)
|
||||
@@ -210,7 +211,7 @@ struct ggml_cuda_mmq_config {
|
||||
static_assert((I_) % 32 == 0, "bad I"); \
|
||||
static_assert((J_) % 8 == 0, "bad J"); \
|
||||
static_assert((K_vram_) % 256 == 0, "bad K_vram"); \
|
||||
return ggml_cuda_mmq_config((type_), (nthreads_), (occupancy_), (I_), (J_), (sram_layout_), (K_vram_), (stream_k_), (fallback_)); \
|
||||
return ggml_cuda_mmq_config((type_), (nthreads_), (occupancy_), (I_), (J_), (sram_layout_), (K_vram_), use_typical_moe_ncols, (stream_k_), (fallback_)); \
|
||||
} \
|
||||
|
||||
#include "mmq-config-pascal-older.cuh"
|
||||
@@ -1473,6 +1474,20 @@ void mul_mat_q_switch_J(ggml_backend_cuda_context & ctx, const mmq_args & args,
|
||||
const int cc = ggml_cuda_info().devices[id].cc;
|
||||
const size_t smpbo = ggml_cuda_info().devices[id].smpbo;
|
||||
|
||||
int64_t ncols_picker = args.ncols_max;
|
||||
if (args.expert_bounds != nullptr && args.nchannels_x > 0) {
|
||||
const int J_max = ggml_cuda_mmq_get_J_max(type, fallback, cc, 128);
|
||||
const ggml_cuda_mmq_config config_max = ggml_cuda_mmq_get_config(type, J_max, fallback, cc);
|
||||
if (config_max.use_typical_moe_ncols) {
|
||||
// Use the typical expert width only for tile selection.
|
||||
// The launch grid still uses args.ncols_max.
|
||||
const int64_t ncols_typical = (args.ncols_dst + args.nchannels_x - 1) / args.nchannels_x;
|
||||
if (ncols_typical >= 1 && ncols_typical < J_max && ncols_typical < ncols_picker) {
|
||||
ncols_picker = ncols_typical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int J_best = 0;
|
||||
int ntiles_J_best = INT_MAX;
|
||||
|
||||
@@ -1486,7 +1501,7 @@ void mul_mat_q_switch_J(ggml_backend_cuda_context & ctx, const mmq_args & args,
|
||||
continue;
|
||||
}
|
||||
|
||||
const int ntiles_x = (args.ncols_max + config.J - 1) / config.J;
|
||||
const int ntiles_x = (ncols_picker + config.J - 1) / config.J;
|
||||
|
||||
if (ntiles_x < ntiles_J_best) {
|
||||
J_best = J;
|
||||
|
||||
@@ -6,6 +6,35 @@
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
// only enabled on DGX Spark, where it is a gain on every type below. On the higher-bandwidth parts the kernel
|
||||
// has little exposed latency left to hide and the extra requests cost more than they save.
|
||||
// For perf data, see https://github.com/ggml-org/llama.cpp/pull/26705#issuecomment-5569335031
|
||||
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == GGML_CUDA_CC_DGX_SPARK
|
||||
// returns true only for those quants that benefit from prefetch and false otherwise
|
||||
static constexpr __host__ __device__ bool mmvq_should_prefetch(ggml_type type) {
|
||||
switch (type) {
|
||||
case GGML_TYPE_Q4_0:
|
||||
case GGML_TYPE_Q5_0:
|
||||
case GGML_TYPE_Q8_0:
|
||||
case GGML_TYPE_MXFP4:
|
||||
case GGML_TYPE_Q3_K:
|
||||
case GGML_TYPE_Q4_K:
|
||||
case GGML_TYPE_Q5_K:
|
||||
case GGML_TYPE_Q6_K:
|
||||
case GGML_TYPE_IQ1_M:
|
||||
case GGML_TYPE_IQ4_NL:
|
||||
case GGML_TYPE_IQ4_XS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static __device__ __forceinline__ void mmvq_prefetch_l2(const void * p) {
|
||||
asm volatile("prefetch.global.L2 [%0];" :: "l"(p));
|
||||
}
|
||||
#endif
|
||||
|
||||
typedef float (*vec_dot_q_cuda_t)(const void * __restrict__ vbq, const block_q8_1 * __restrict__ bq8_1, const int & kbx, const int & iqs);
|
||||
|
||||
static constexpr __device__ vec_dot_q_cuda_t get_vec_dot_q_cuda(ggml_type type) {
|
||||
@@ -298,9 +327,6 @@ bool ggml_cuda_should_use_mmvq(enum ggml_type type, int cc, int64_t ne11) {
|
||||
return ne11 <= 4;
|
||||
case GGML_TYPE_Q3_K:
|
||||
return ne11 <= 6;
|
||||
case GGML_TYPE_Q4_K:
|
||||
case GGML_TYPE_Q5_K:
|
||||
return ne11 <= 7;
|
||||
default:
|
||||
return ne11 <= MMVQ_MAX_BATCH_SIZE;
|
||||
}
|
||||
@@ -310,8 +336,9 @@ bool ggml_cuda_should_use_mmvq(enum ggml_type type, int cc, int64_t ne11) {
|
||||
case GGML_TYPE_Q2_K:
|
||||
case GGML_TYPE_Q3_K:
|
||||
case GGML_TYPE_Q4_K:
|
||||
case GGML_TYPE_Q5_K:
|
||||
return ne11 <= 5;
|
||||
case GGML_TYPE_Q5_K:
|
||||
return ne11 <= 6;
|
||||
case GGML_TYPE_Q6_K:
|
||||
return ne11 <= 7;
|
||||
default:
|
||||
@@ -675,6 +702,26 @@ static __global__ void mul_mat_vec_q(
|
||||
// x block quant index when casting the quants to int
|
||||
const int kqs = vdr * (tid % (qi/vdr));
|
||||
|
||||
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == GGML_CUDA_CC_DGX_SPARK
|
||||
// start the next iterations' weight loads early
|
||||
if constexpr (mmvq_should_prefetch(type)) {
|
||||
constexpr int pf_dist = 2; // loop iterations, not blocks
|
||||
const int kbx_pf = kbx + pf_dist*blocks_per_iter;
|
||||
if (kbx_pf < blocks_per_row_x) {
|
||||
#pragma unroll
|
||||
for (int i = 0; i < rows_per_cuda_block; ++i) {
|
||||
const size_t off = (size_t)(kbx_offset + i*stride_row_x + kbx_pf) * ggml_cuda_type_traits<type>::bs;
|
||||
mmvq_prefetch_l2((const char *) vx + off);
|
||||
if constexpr (has_fusion) {
|
||||
if (use_gate) {
|
||||
mmvq_prefetch_l2((const char *) vgate + off);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#pragma unroll
|
||||
for (int j = 0; j < ncols_dst; ++j) {
|
||||
#pragma unroll
|
||||
|
||||
@@ -936,16 +936,20 @@ static __device__ __forceinline__ float vec_dot_q4_K_q8_1(
|
||||
v[0] = q4[0];
|
||||
v[1] = q4[4];
|
||||
|
||||
// branchless so nvcc can hoist this out of the ncols_dst loop
|
||||
const uint16_t * scales = (const uint16_t *)bq4_K->scales;
|
||||
const int j = bq8_offset/2;
|
||||
const int jm = j & 1;
|
||||
|
||||
const uint32_t s0 = scales[jm + 0];
|
||||
const uint32_t s2 = scales[jm + 2];
|
||||
const uint32_t s4 = scales[jm + 4];
|
||||
|
||||
const uint32_t hi = (uint32_t) -(int32_t) (j >= 2);
|
||||
|
||||
uint16_t aux[2];
|
||||
const int j = bq8_offset/2;
|
||||
if (j < 2) {
|
||||
aux[0] = scales[j+0] & 0x3f3f;
|
||||
aux[1] = scales[j+2] & 0x3f3f;
|
||||
} else {
|
||||
aux[0] = ((scales[j+2] >> 0) & 0x0f0f) | ((scales[j-2] & 0xc0c0) >> 2);
|
||||
aux[1] = ((scales[j+2] >> 4) & 0x0f0f) | ((scales[j-0] & 0xc0c0) >> 2);
|
||||
}
|
||||
aux[0] = (uint16_t) (((s0 & 0x3f3f) & ~hi) | ((((s4 >> 0) & 0x0f0f) | ((s0 & 0xc0c0) >> 2)) & hi));
|
||||
aux[1] = (uint16_t) (((s2 & 0x3f3f) & ~hi) | ((((s4 >> 4) & 0x0f0f) | ((s2 & 0xc0c0) >> 2)) & hi));
|
||||
const uint8_t * sc = (const uint8_t *)aux;
|
||||
const uint8_t * m = sc + 2;
|
||||
|
||||
@@ -981,16 +985,21 @@ static __device__ __forceinline__ float vec_dot_q5_K_q8_1(
|
||||
vh[0] = qh[0] >> bq8_offset;
|
||||
vh[1] = qh[4] >> bq8_offset;
|
||||
|
||||
// same as q4_K
|
||||
const uint16_t * scales = (const uint16_t *)bq5_K->scales;
|
||||
const int j = bq8_offset/2;
|
||||
const int jm = j & 1;
|
||||
|
||||
const uint32_t s0 = scales[jm + 0];
|
||||
const uint32_t s2 = scales[jm + 2];
|
||||
const uint32_t s4 = scales[jm + 4];
|
||||
|
||||
const uint32_t hi = (uint32_t) -(int32_t) (j >= 2);
|
||||
|
||||
uint16_t aux[2];
|
||||
const int j = bq8_offset/2;
|
||||
if (j < 2) {
|
||||
aux[0] = scales[j+0] & 0x3f3f;
|
||||
aux[1] = scales[j+2] & 0x3f3f;
|
||||
} else {
|
||||
aux[0] = ((scales[j+2] >> 0) & 0x0f0f) | ((scales[j-2] & 0xc0c0) >> 2);
|
||||
aux[1] = ((scales[j+2] >> 4) & 0x0f0f) | ((scales[j-0] & 0xc0c0) >> 2);
|
||||
}
|
||||
aux[0] = (uint16_t) (((s0 & 0x3f3f) & ~hi) | ((((s4 >> 0) & 0x0f0f) | ((s0 & 0xc0c0) >> 2)) & hi));
|
||||
aux[1] = (uint16_t) (((s2 & 0x3f3f) & ~hi) | ((((s4 >> 4) & 0x0f0f) | ((s2 & 0xc0c0) >> 2)) & hi));
|
||||
|
||||
const uint8_t * sc = (const uint8_t *)aux;
|
||||
const uint8_t * m = sc + 2;
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -176,9 +176,9 @@
|
||||
|
||||
#define __CUDA_ARCH__ 1300
|
||||
|
||||
#if defined(__gfx900__) || defined(__gfx906__)
|
||||
#if defined(__gfx900__) || defined(__gfx906__) || defined(__gfx909__) || defined(__gfx90c__)
|
||||
#define GCN5
|
||||
#endif // defined(__gfx900__) || defined(__gfx906__)
|
||||
#endif // defined(__gfx900__) || defined(__gfx906__) || defined(__gfx909__) || defined(__gfx90c__)
|
||||
|
||||
#if defined(__gfx803__)
|
||||
#define GCN4
|
||||
|
||||
@@ -1091,6 +1091,10 @@ static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) {
|
||||
if (op->ne[3] != 1) {
|
||||
return {false, "GET_ROWS/SET_ROWS with ne[3] != 1 (ne[3]=" + std::to_string(op->ne[3]) + ") is not supported"};
|
||||
}
|
||||
if (op->op == GGML_OP_GET_ROWS && ggml_is_quantized(op->src[0]->type) &&
|
||||
op->src[0]->view_src != nullptr && op->src[0]->view_offs != 0) {
|
||||
return {false, "GET_ROWS with a nonzero quantized src0 view offset is not supported"};
|
||||
}
|
||||
if (op->op == GGML_OP_GET_ROWS && ggml_openvino_get_device_name() == "GPU" &&
|
||||
op->src[0]->type == GGML_TYPE_BF16) {
|
||||
return {false, "GET_ROWS with BF16 src0 is not supported on GPU"};
|
||||
|
||||
@@ -4858,6 +4858,78 @@ static bool ggml_sycl_mul_mat_glu_mmvq_fused(ggml_backend_sycl_context & ctx, gg
|
||||
/*stride_col_dst=*/(int) glu->ne[0], stream);
|
||||
}
|
||||
|
||||
// Batch the run of consecutive L2_NORM siblings starting at node_idx into one launch.
|
||||
// Returns the number of extra graph nodes consumed, or 0 if the run is shorter than two
|
||||
// (the caller then runs the norm through the per-tensor kernel).
|
||||
static int ggml_sycl_l2_norm_batch_fused(ggml_backend_sycl_context & ctx, ggml_cgraph * cgraph, int node_idx) {
|
||||
const ggml_tensor * node = cgraph->nodes[node_idx];
|
||||
if (ggml_sycl_info().device_count != 1 || node->type != GGML_TYPE_F32 ||
|
||||
node->src[0]->type != GGML_TYPE_F32 || node->src[0]->ne[0] >= 1024) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ggml_tensor * batch[GGML_SYCL_L2_BATCH_MAX];
|
||||
int count = 0;
|
||||
int last = node_idx;
|
||||
float eps0;
|
||||
memcpy(&eps0, node->op_params, sizeof(float));
|
||||
|
||||
// Conservative aliasing test: the batched norms run concurrently in one kernel,
|
||||
// so none may read what another writes, and none may write where another writes.
|
||||
auto overlaps = [](const ggml_tensor * a, const ggml_tensor * b) {
|
||||
const char * ab = (const char *) a->data;
|
||||
const char * bb = (const char *) b->data;
|
||||
return ab < bb + ggml_nbytes(b) && bb < ab + ggml_nbytes(a);
|
||||
};
|
||||
|
||||
for (int j = node_idx; j < cgraph->n_nodes && count < GGML_SYCL_L2_BATCH_MAX; ++j) {
|
||||
ggml_tensor * nj = cgraph->nodes[j];
|
||||
if (ggml_is_empty(nj) || nj->op == GGML_OP_RESHAPE || nj->op == GGML_OP_TRANSPOSE ||
|
||||
nj->op == GGML_OP_VIEW || nj->op == GGML_OP_PERMUTE || nj->op == GGML_OP_NONE ||
|
||||
(nj->flags & GGML_TENSOR_FLAG_COMPUTE) == 0) {
|
||||
continue; // not a launch; cannot break a run of adjacent norms
|
||||
}
|
||||
if (nj->op != GGML_OP_L2_NORM || nj->type != GGML_TYPE_F32 ||
|
||||
nj->src[0]->type != GGML_TYPE_F32 || !ggml_are_same_shape(nj, node) ||
|
||||
!ggml_are_same_shape(nj->src[0], node->src[0])) {
|
||||
break; // any other launch ends the run
|
||||
}
|
||||
bool same_nb = true;
|
||||
for (int d = 0; d < GGML_MAX_DIMS; ++d) {
|
||||
if (nj->nb[d] != node->nb[d] || nj->src[0]->nb[d] != node->src[0]->nb[d]) {
|
||||
same_nb = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!same_nb) {
|
||||
break; // one nb[] stride set is shared by the whole batch
|
||||
}
|
||||
float epsj;
|
||||
memcpy(&epsj, nj->op_params, sizeof(float));
|
||||
if (epsj != eps0) {
|
||||
break; // eps mismatch ends the run
|
||||
}
|
||||
bool indep = true;
|
||||
for (int k = 0; k < count; ++k) {
|
||||
if (overlaps(nj->src[0], batch[k]) || overlaps(nj, batch[k])) {
|
||||
indep = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!indep) {
|
||||
break; // an overlapping tensor would race inside one launch
|
||||
}
|
||||
batch[count++] = nj;
|
||||
last = j;
|
||||
}
|
||||
if (count < 2) {
|
||||
return 0; // a lone norm falls through to the per-tensor kernel
|
||||
}
|
||||
ggml_sycl_l2_norm_batch(ctx, batch, count);
|
||||
return last - node_idx;
|
||||
}
|
||||
|
||||
|
||||
__dpct_inline__ static void k_copy_src1_to_contiguous(
|
||||
const char *__restrict__ src1_original, char *__restrict__ src1_contiguous,
|
||||
const mmid_row_mapping *__restrict__ row_mapping,
|
||||
@@ -5908,6 +5980,17 @@ static void ggml_backend_sycl_graph_compute_impl(ggml_backend_sycl_context * syc
|
||||
continue;
|
||||
}
|
||||
|
||||
// Batch consecutive independent same-shape F32 L2_NORM siblings (the GDN q/k
|
||||
// norms) into one launch; sources are strided views of the fused qkv buffer, so
|
||||
// the scan skips the interleaved view nodes instead of breaking on them.
|
||||
if (node->op == GGML_OP_L2_NORM) {
|
||||
const int l2_batch_skip = ggml_sycl_l2_norm_batch_fused(*sycl_ctx, cgraph, i);
|
||||
if (l2_batch_skip > 0) {
|
||||
i += l2_batch_skip;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (node->op == GGML_OP_MUL_MAT && ggml_sycl_mul_mat_glu_mmvq_fused(*sycl_ctx, cgraph, i)) {
|
||||
i += 2;
|
||||
continue;
|
||||
|
||||
@@ -543,6 +543,62 @@ static void l2_norm_f32_sycl(const float * x,
|
||||
}
|
||||
}
|
||||
|
||||
// Batched L2 norm: N independent same-shape F32 tensors in one launch; the tensor
|
||||
// index is folded into grid dim0 and each row's reduction is identical to the
|
||||
// single-tensor kernel, so the result is bit-exact.
|
||||
struct l2_batch_ptrs {
|
||||
const float * src[GGML_SYCL_L2_BATCH_MAX];
|
||||
float * dst[GGML_SYCL_L2_BATCH_MAX];
|
||||
};
|
||||
|
||||
// One stride set shared by the whole batch: the caller only groups tensors whose nb[]
|
||||
// all match, so per-tensor state stays two pointers.
|
||||
struct l2_batch_strides {
|
||||
int ne1, ne2;
|
||||
int64_t ss0, ss1, ss2, ss3;
|
||||
int64_t ds0, ds1, ds2, ds3;
|
||||
};
|
||||
|
||||
template <int warp_size>
|
||||
static void l2_norm_f32_batch(l2_batch_ptrs p, l2_batch_strides st, const int ncols, const float eps,
|
||||
const sycl::nd_item<3> & item_ct1) {
|
||||
const int t = item_ct1.get_group(0); // tensor index
|
||||
const int r = item_ct1.get_group(2); // flattened row over ne1*ne2*ne3
|
||||
const int tid = item_ct1.get_local_id(2);
|
||||
|
||||
const int i1 = r % st.ne1;
|
||||
const int i2 = (r / st.ne1) % st.ne2;
|
||||
const int i3 = r / (st.ne1 * st.ne2);
|
||||
|
||||
const float * x = p.src[t] + i3 * st.ss3 + i2 * st.ss2 + i1 * st.ss1;
|
||||
float * dst = p.dst[t] + i3 * st.ds3 + i2 * st.ds2 + i1 * st.ds1;
|
||||
|
||||
float tmp = 0.0f;
|
||||
for (int col = tid; col < ncols; col += warp_size) {
|
||||
const float xi = x[col * st.ss0];
|
||||
tmp += xi * xi;
|
||||
}
|
||||
tmp = block_reduce<block_reduce_method::SUM, warp_size>(tmp, (float *) nullptr, warp_size);
|
||||
const float scale = sycl::rsqrt(sycl::fmax(tmp, eps * eps));
|
||||
for (int col = tid; col < ncols; col += warp_size) {
|
||||
dst[col * st.ds0] = scale * x[col * st.ss0];
|
||||
}
|
||||
}
|
||||
|
||||
template <int warp_size>
|
||||
static void l2_norm_f32_batch_sycl(l2_batch_ptrs p, l2_batch_strides st, const int n_tensors,
|
||||
const int ncols, const int nrows_total, const float eps,
|
||||
queue_ptr stream) {
|
||||
const dpct::dim3 blocks_num(nrows_total, 1, n_tensors);
|
||||
const dpct::dim3 block_dims(warp_size, 1, 1);
|
||||
stream->submit([&](sycl::handler & cgh) {
|
||||
cgh.parallel_for(sycl::nd_range<3>(blocks_num * block_dims, block_dims),
|
||||
[=](sycl::nd_item<3> item_ct1) [[sycl::reqd_sub_group_size(warp_size)]] {
|
||||
l2_norm_f32_batch<warp_size>(p, st, ncols, eps, item_ct1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ggml_sycl_op_norm(ggml_backend_sycl_context& ctx, ggml_tensor* dst) {
|
||||
const ggml_tensor * src0 = dst->src[0];
|
||||
|
||||
@@ -961,3 +1017,30 @@ void ggml_sycl_op_l2_norm(ggml_backend_sycl_context& ctx, ggml_tensor* dst) {
|
||||
l2_norm_f32_sycl<WARP_SIZE>(src0_d, dst_d, ne00, ne01, ne02, ne03,
|
||||
ss0, ss1, ss2, ss3, ds0, ds1, ds2, ds3, eps, stream, ctx.device);
|
||||
}
|
||||
|
||||
// nodes[0..count) are independent, same-shape, same-eps, same-nb L2_NORM ops validated
|
||||
// by the caller; requires ncols < 1024 (the warp reduction path).
|
||||
void ggml_sycl_l2_norm_batch(ggml_backend_sycl_context & ctx, ggml_tensor ** nodes, int count) {
|
||||
const ggml_tensor * s0 = nodes[0]->src[0];
|
||||
const int ncols = (int) s0->ne[0];
|
||||
const int nrows_total = (int) ggml_nrows(s0);
|
||||
float eps;
|
||||
memcpy(&eps, nodes[0]->op_params, sizeof(float));
|
||||
GGML_ASSERT(eps >= 0.0f);
|
||||
|
||||
l2_batch_ptrs p{};
|
||||
for (int t = 0; t < count; ++t) {
|
||||
p.src[t] = (const float *) nodes[t]->src[0]->data;
|
||||
p.dst[t] = (float *) nodes[t]->data;
|
||||
}
|
||||
|
||||
const ggml_tensor * d0 = nodes[0];
|
||||
const size_t ts = ggml_type_size(GGML_TYPE_F32);
|
||||
l2_batch_strides st{};
|
||||
st.ne1 = (int) s0->ne[1];
|
||||
st.ne2 = (int) s0->ne[2];
|
||||
st.ss0 = s0->nb[0] / ts; st.ss1 = s0->nb[1] / ts; st.ss2 = s0->nb[2] / ts; st.ss3 = s0->nb[3] / ts;
|
||||
st.ds0 = d0->nb[0] / ts; st.ds1 = d0->nb[1] / ts; st.ds2 = d0->nb[2] / ts; st.ds3 = d0->nb[3] / ts;
|
||||
|
||||
l2_norm_f32_batch_sycl<WARP_SIZE>(p, st, count, ncols, nrows_total, eps, ctx.stream());
|
||||
}
|
||||
|
||||
@@ -29,4 +29,7 @@ void ggml_sycl_op_group_norm(ggml_backend_sycl_context& ctx, ggml_tensor* dst);
|
||||
|
||||
void ggml_sycl_op_l2_norm(ggml_backend_sycl_context& ctx, ggml_tensor* dst);
|
||||
|
||||
#define GGML_SYCL_L2_BATCH_MAX 8
|
||||
void ggml_sycl_l2_norm_batch(ggml_backend_sycl_context & ctx, ggml_tensor ** nodes, int count);
|
||||
|
||||
#endif // GGML_SYCL_NORM_HPP
|
||||
|
||||
@@ -1110,6 +1110,9 @@ struct vk_device_struct {
|
||||
vk_pipeline pipeline_cumsum_multipass2_f32;
|
||||
vk_pipeline pipeline_argmax_f32;
|
||||
vk_pipeline pipeline_count_equal_i32;
|
||||
vk_pipeline pipeline_dsv4_hc_comb_f32;
|
||||
vk_pipeline pipeline_dsv4_hc_pre_f32;
|
||||
vk_pipeline pipeline_dsv4_hc_post_f32;
|
||||
std::map<vk_solve_tri_pipeline_state, vk_pipeline> pipeline_solve_tri_f32;
|
||||
vk_pipeline pipeline_im2col_f32, pipeline_im2col_f32_f16;
|
||||
vk_pipeline pipeline_im2col_3d_f32, pipeline_im2col_3d_f32_f16;
|
||||
@@ -1467,6 +1470,53 @@ struct vk_op_fwht_push_constants {
|
||||
float scale;
|
||||
};
|
||||
|
||||
struct vk_op_dsv4_hc_comb_push_constants {
|
||||
uint32_t n_tokens;
|
||||
|
||||
uint32_t nbm0; uint32_t nbm1;
|
||||
uint32_t nbs0;
|
||||
uint32_t nbb0;
|
||||
uint32_t nbd0; uint32_t nbd1; uint32_t nbd2;
|
||||
|
||||
uint32_t m_offset;
|
||||
uint32_t s_offset;
|
||||
uint32_t b_offset;
|
||||
uint32_t d_offset;
|
||||
|
||||
float eps;
|
||||
uint32_t n_iter;
|
||||
};
|
||||
|
||||
struct vk_op_dsv4_hc_pre_push_constants {
|
||||
uint32_t n_embd;
|
||||
uint32_t n_tokens;
|
||||
|
||||
uint32_t nbx0; uint32_t nbx1; uint32_t nbx2;
|
||||
uint32_t nbw0; uint32_t nbw1;
|
||||
uint32_t nbd0; uint32_t nbd1;
|
||||
|
||||
uint32_t x_offset;
|
||||
uint32_t w_offset;
|
||||
uint32_t d_offset;
|
||||
};
|
||||
|
||||
struct vk_op_dsv4_hc_post_push_constants {
|
||||
uint32_t n_embd;
|
||||
uint32_t n_tokens;
|
||||
|
||||
uint32_t nbx0; uint32_t nbx1;
|
||||
uint32_t nbr0; uint32_t nbr1; uint32_t nbr2;
|
||||
uint32_t nbp0; uint32_t nbp1;
|
||||
uint32_t nbc0; uint32_t nbc1; uint32_t nbc2;
|
||||
uint32_t nbd0; uint32_t nbd1; uint32_t nbd2;
|
||||
|
||||
uint32_t x_offset;
|
||||
uint32_t r_offset;
|
||||
uint32_t p_offset;
|
||||
uint32_t c_offset;
|
||||
uint32_t d_offset;
|
||||
};
|
||||
|
||||
struct vk_op_count_experts_push_constants {
|
||||
uint32_t ne00;
|
||||
uint32_t ne01;
|
||||
@@ -2515,9 +2565,38 @@ static uint64_t vk_tensor_offset(const ggml_tensor * tensor) {
|
||||
return (uint8_t *) tensor->data - (uint8_t *) vk_ptr_base;
|
||||
}
|
||||
|
||||
static uint32_t get_misalign_bytes(const ggml_backend_vk_context * ctx, const ggml_tensor * t)
|
||||
{
|
||||
return ((vk_tensor_offset(t) + t->view_offs) & (ctx->device->properties.limits.minStorageBufferOffsetAlignment - 1));;
|
||||
static void ggml_vk_host_get(const vk_device& device, const void * ptr, vk_buffer& buf, size_t& buf_offset);
|
||||
|
||||
static size_t ggml_vk_tensor_buffer_offset(const ggml_backend_vk_context * ctx, const ggml_tensor * t) {
|
||||
// vk_tensor_offset() is relative to vk_ptr_base, but mapped host tensors need an offset relative to their Vulkan buffer.
|
||||
if (ctx->device->uma) {
|
||||
vk_buffer buf = nullptr;
|
||||
size_t off = 0;
|
||||
ggml_vk_host_get(ctx->device, t->data, buf, off);
|
||||
if (buf) {
|
||||
return off;
|
||||
}
|
||||
}
|
||||
return (size_t)(vk_tensor_offset(t) + t->view_offs);
|
||||
}
|
||||
|
||||
static size_t ggml_vk_descriptor_offset(size_t tensor_offset, size_t alignment, size_t type_size) {
|
||||
// Move the descriptor back until its distance to the tensor is divisible by the tensor type size.
|
||||
size_t descriptor_offset = tensor_offset & ~(alignment - 1);
|
||||
while ((tensor_offset - descriptor_offset) % type_size != 0) {
|
||||
GGML_ASSERT(descriptor_offset >= alignment);
|
||||
descriptor_offset -= alignment;
|
||||
}
|
||||
|
||||
return descriptor_offset;
|
||||
}
|
||||
|
||||
static uint32_t get_misalign_bytes(const ggml_backend_vk_context * ctx, const ggml_tensor * t) {
|
||||
const size_t tensor_offset = ggml_vk_tensor_buffer_offset(ctx, t);
|
||||
const size_t descriptor_offset = ggml_vk_descriptor_offset(
|
||||
tensor_offset, ctx->device->properties.limits.minStorageBufferOffsetAlignment, ggml_type_size(t->type));
|
||||
GGML_ASSERT(tensor_offset - descriptor_offset <= UINT32_MAX);
|
||||
return tensor_offset - descriptor_offset;
|
||||
}
|
||||
|
||||
static uint32_t ggml_vk_concat_unit_size(ggml_type type) {
|
||||
@@ -2602,6 +2681,32 @@ template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk
|
||||
GGML_UNUSED(src3);
|
||||
}
|
||||
|
||||
template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk_op_dsv4_hc_comb_push_constants &p, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * src3, ggml_tensor * dst) {
|
||||
p.m_offset = get_misalign_bytes(ctx, src0) / ggml_type_size(src0->type);
|
||||
p.s_offset = get_misalign_bytes(ctx, src1) / ggml_type_size(src1->type);
|
||||
p.b_offset = get_misalign_bytes(ctx, src2) / ggml_type_size(src2->type);
|
||||
p.d_offset = get_misalign_bytes(ctx, dst) / ggml_type_size(dst->type);
|
||||
|
||||
GGML_UNUSED(src3);
|
||||
}
|
||||
|
||||
template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk_op_dsv4_hc_pre_push_constants &p, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * src3, ggml_tensor * dst) {
|
||||
p.x_offset = get_misalign_bytes(ctx, src0) / ggml_type_size(src0->type);
|
||||
p.w_offset = get_misalign_bytes(ctx, src1) / ggml_type_size(src1->type);
|
||||
p.d_offset = get_misalign_bytes(ctx, dst) / ggml_type_size(dst->type);
|
||||
|
||||
GGML_UNUSED(src2);
|
||||
GGML_UNUSED(src3);
|
||||
}
|
||||
|
||||
template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk_op_dsv4_hc_post_push_constants &p, const ggml_tensor * src0, const ggml_tensor * src1, const ggml_tensor * src2, const ggml_tensor * src3, ggml_tensor * dst) {
|
||||
p.x_offset = get_misalign_bytes(ctx, src0) / ggml_type_size(src0->type);
|
||||
p.r_offset = get_misalign_bytes(ctx, src1) / ggml_type_size(src1->type);
|
||||
p.p_offset = get_misalign_bytes(ctx, src2) / ggml_type_size(src2->type);
|
||||
p.c_offset = get_misalign_bytes(ctx, src3) / ggml_type_size(src3->type);
|
||||
p.d_offset = get_misalign_bytes(ctx, dst) / ggml_type_size(dst->type);
|
||||
}
|
||||
|
||||
struct ggml_backend_vk_buffer_context {
|
||||
vk_device_ref device;
|
||||
vk_buffer dev_buffer;
|
||||
@@ -5948,6 +6053,16 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
|
||||
ggml_vk_create_pipeline(device, device->pipeline_count_experts, "count_experts", count_experts_len, count_experts_data, "main", 2, sizeof(vk_op_count_experts_push_constants), {1, 1, 1}, {}, 1, true);
|
||||
}
|
||||
|
||||
// comb holds a token's 4x4 matrix in one 16-lane slice of a subgroup, so it
|
||||
// needs at least 16 lanes, pinned to a known size.
|
||||
if (device->subgroup_basic && device->subgroup_shuffle && device->subgroup_require_full_support && device->subgroup_size >= 16) {
|
||||
const uint32_t tokens_per_workgroup = 4 * (device->subgroup_size / 16);
|
||||
ggml_vk_create_pipeline(device, device->pipeline_dsv4_hc_comb_f32, "dsv4_hc_comb_f32", dsv4_hc_comb_f32_len, dsv4_hc_comb_f32_data, "main", 4, sizeof(vk_op_dsv4_hc_comb_push_constants), {tokens_per_workgroup, 1, 1}, { device->subgroup_size }, 1, true, true, device->subgroup_size);
|
||||
}
|
||||
|
||||
ggml_vk_create_pipeline(device, device->pipeline_dsv4_hc_pre_f32, "dsv4_hc_pre_f32", dsv4_hc_pre_f32_len, dsv4_hc_pre_f32_data, "main", 3, sizeof(vk_op_dsv4_hc_pre_push_constants), {256, 1, 1}, { 256 }, 1);
|
||||
ggml_vk_create_pipeline(device, device->pipeline_dsv4_hc_post_f32, "dsv4_hc_post_f32", dsv4_hc_post_f32_len, dsv4_hc_post_f32_data, "main", 5, sizeof(vk_op_dsv4_hc_post_push_constants), {256, 1, 1}, { 256 }, 1);
|
||||
|
||||
for (auto &s : device->pipeline_solve_tri_f32) {
|
||||
const vk_solve_tri_pipeline_state &state = s.first;
|
||||
|
||||
@@ -8265,10 +8380,12 @@ static vk_subbuffer ggml_vk_tensor_subbuffer(
|
||||
|
||||
size_t size = ggml_nbytes(tensor);
|
||||
|
||||
size_t misalign_bytes = offset & (ctx->device->properties.limits.minStorageBufferOffsetAlignment - 1);
|
||||
const size_t descriptor_offset = ggml_vk_descriptor_offset(
|
||||
offset, ctx->device->properties.limits.minStorageBufferOffsetAlignment, ggml_type_size(tensor->type));
|
||||
const size_t misalign_bytes = offset - descriptor_offset;
|
||||
// The shader must support misaligned offsets when indexing into the buffer
|
||||
GGML_ASSERT(allow_misalign || misalign_bytes == 0);
|
||||
offset &= ~misalign_bytes;
|
||||
offset = descriptor_offset;
|
||||
size += misalign_bytes;
|
||||
|
||||
return vk_subbuffer{buffer, offset, size};
|
||||
@@ -10173,6 +10290,98 @@ static void ggml_vk_fwht(ggml_backend_vk_context * ctx, vk_context& subctx, cons
|
||||
ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { src_buf, dst_buf }, pc, { workgroups_x, 1, 1 });
|
||||
}
|
||||
|
||||
static uint32_t ggml_vk_nb_elem(const ggml_tensor * t, int i) {
|
||||
return (uint32_t)(t->nb[i] / ggml_type_size(t->type));
|
||||
}
|
||||
|
||||
static void ggml_vk_dsv4_hc_comb(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * mixes, const ggml_tensor * scale, const ggml_tensor * base, ggml_tensor * dst) {
|
||||
VK_LOG_DEBUG("ggml_vk_dsv4_hc_comb(" << mixes << ", " << scale << ", " << base << ", " << dst << ")");
|
||||
|
||||
vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_comb_f32;
|
||||
GGML_ASSERT(pipeline != nullptr);
|
||||
|
||||
const uint32_t n_tokens = (uint32_t)mixes->ne[1];
|
||||
|
||||
ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
|
||||
|
||||
const vk_subbuffer mixes_buf = ggml_vk_tensor_subbuffer(ctx, mixes, true);
|
||||
const vk_subbuffer scale_buf = ggml_vk_tensor_subbuffer(ctx, scale, true);
|
||||
const vk_subbuffer base_buf = ggml_vk_tensor_subbuffer(ctx, base, true);
|
||||
const vk_subbuffer dst_buf = ggml_vk_tensor_subbuffer(ctx, dst, true);
|
||||
|
||||
vk_op_dsv4_hc_comb_push_constants pc = {
|
||||
n_tokens,
|
||||
ggml_vk_nb_elem(mixes, 0), ggml_vk_nb_elem(mixes, 1),
|
||||
ggml_vk_nb_elem(scale, 0),
|
||||
ggml_vk_nb_elem(base, 0),
|
||||
ggml_vk_nb_elem(dst, 0), ggml_vk_nb_elem(dst, 1), ggml_vk_nb_elem(dst, 2),
|
||||
0, 0, 0, 0,
|
||||
ggml_get_op_params_f32(dst, 0),
|
||||
(uint32_t)ggml_get_op_params_i32(dst, 1),
|
||||
};
|
||||
init_pushconst_tensor_offsets(ctx, pc, mixes, scale, base, nullptr, dst);
|
||||
|
||||
ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { mixes_buf, scale_buf, base_buf, dst_buf }, pc, { n_tokens, 1, 1 });
|
||||
}
|
||||
|
||||
static void ggml_vk_dsv4_hc_pre(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * x, const ggml_tensor * weights, ggml_tensor * dst) {
|
||||
VK_LOG_DEBUG("ggml_vk_dsv4_hc_pre(" << x << ", " << weights << ", " << dst << ")");
|
||||
|
||||
vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_pre_f32;
|
||||
GGML_ASSERT(pipeline != nullptr);
|
||||
|
||||
const uint32_t n_embd = (uint32_t)x->ne[0];
|
||||
const uint32_t n_tokens = (uint32_t)x->ne[2];
|
||||
|
||||
ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
|
||||
|
||||
const vk_subbuffer x_buf = ggml_vk_tensor_subbuffer(ctx, x, true);
|
||||
const vk_subbuffer w_buf = ggml_vk_tensor_subbuffer(ctx, weights, true);
|
||||
const vk_subbuffer d_buf = ggml_vk_tensor_subbuffer(ctx, dst, true);
|
||||
|
||||
vk_op_dsv4_hc_pre_push_constants pc = {
|
||||
n_embd, n_tokens,
|
||||
ggml_vk_nb_elem(x, 0), ggml_vk_nb_elem(x, 1), ggml_vk_nb_elem(x, 2),
|
||||
ggml_vk_nb_elem(weights, 0), ggml_vk_nb_elem(weights, 1),
|
||||
ggml_vk_nb_elem(dst, 0), ggml_vk_nb_elem(dst, 1),
|
||||
0, 0, 0,
|
||||
};
|
||||
init_pushconst_tensor_offsets(ctx, pc, x, weights, nullptr, nullptr, dst);
|
||||
|
||||
ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { x_buf, w_buf, d_buf }, pc, { n_embd, n_tokens, 1 });
|
||||
}
|
||||
|
||||
static void ggml_vk_dsv4_hc_post(ggml_backend_vk_context * ctx, vk_context& subctx, const ggml_tensor * x, const ggml_tensor * residual, const ggml_tensor * post, const ggml_tensor * comb, ggml_tensor * dst) {
|
||||
VK_LOG_DEBUG("ggml_vk_dsv4_hc_post(" << x << ", " << residual << ", " << post << ", " << comb << ", " << dst << ")");
|
||||
|
||||
vk_pipeline pipeline = ctx->device->pipeline_dsv4_hc_post_f32;
|
||||
GGML_ASSERT(pipeline != nullptr);
|
||||
|
||||
const uint32_t n_embd = (uint32_t)x->ne[0];
|
||||
const uint32_t n_tokens = (uint32_t)x->ne[1];
|
||||
|
||||
ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
|
||||
|
||||
const vk_subbuffer x_buf = ggml_vk_tensor_subbuffer(ctx, x, true);
|
||||
const vk_subbuffer r_buf = ggml_vk_tensor_subbuffer(ctx, residual, true);
|
||||
const vk_subbuffer p_buf = ggml_vk_tensor_subbuffer(ctx, post, true);
|
||||
const vk_subbuffer c_buf = ggml_vk_tensor_subbuffer(ctx, comb, true);
|
||||
const vk_subbuffer d_buf = ggml_vk_tensor_subbuffer(ctx, dst, true);
|
||||
|
||||
vk_op_dsv4_hc_post_push_constants pc = {
|
||||
n_embd, n_tokens,
|
||||
ggml_vk_nb_elem(x, 0), ggml_vk_nb_elem(x, 1),
|
||||
ggml_vk_nb_elem(residual, 0), ggml_vk_nb_elem(residual, 1), ggml_vk_nb_elem(residual, 2),
|
||||
ggml_vk_nb_elem(post, 0), ggml_vk_nb_elem(post, 1),
|
||||
ggml_vk_nb_elem(comb, 0), ggml_vk_nb_elem(comb, 1), ggml_vk_nb_elem(comb, 2),
|
||||
ggml_vk_nb_elem(dst, 0), ggml_vk_nb_elem(dst, 1), ggml_vk_nb_elem(dst, 2),
|
||||
0, 0, 0, 0, 0,
|
||||
};
|
||||
init_pushconst_tensor_offsets(ctx, pc, x, residual, post, comb, dst);
|
||||
|
||||
ggml_vk_dispatch_pipeline(ctx, subctx, pipeline, { x_buf, r_buf, p_buf, c_buf, d_buf }, pc, { n_embd, n_tokens, 1 });
|
||||
}
|
||||
|
||||
static void ggml_vk_mul_mat(ggml_backend_vk_context * ctx, vk_context& subctx, const struct ggml_cgraph * cgraph, int node_idx) {
|
||||
ggml_tensor * dst = cgraph->nodes[node_idx];
|
||||
ggml_tensor * src0 = dst->src[0];
|
||||
@@ -12155,7 +12364,9 @@ template <> void init_pushconst_tensor_offsets(ggml_backend_vk_context * ctx, vk
|
||||
const uint32_t b_offset = get_misalign_bytes(ctx, src1) / ggml_type_size(src1->type);
|
||||
const uint32_t d_offset = get_misalign_bytes(ctx, dst) / ggml_type_size(dst->type);
|
||||
|
||||
GGML_ASSERT(dst->op != GGML_OP_GET_ROWS || (a_offset == 0 && b_offset == 0 && d_offset == 0));
|
||||
GGML_ASSERT(a_offset <= 0xFFFF);
|
||||
GGML_ASSERT(b_offset <= 0xFF);
|
||||
GGML_ASSERT(d_offset <= 0xFF);
|
||||
|
||||
p.misalign_offsets = (a_offset << 16) | (b_offset << 8) | d_offset;
|
||||
|
||||
@@ -16189,6 +16400,18 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr
|
||||
case GGML_OP_CUMSUM:
|
||||
ggml_vk_cumsum(ctx, compute_ctx, src0, node);
|
||||
|
||||
break;
|
||||
case GGML_OP_DSV4_HC_COMB:
|
||||
ggml_vk_dsv4_hc_comb(ctx, compute_ctx, src0, src1, src2, node);
|
||||
|
||||
break;
|
||||
case GGML_OP_DSV4_HC_PRE:
|
||||
ggml_vk_dsv4_hc_pre(ctx, compute_ctx, src0, src1, node);
|
||||
|
||||
break;
|
||||
case GGML_OP_DSV4_HC_POST:
|
||||
ggml_vk_dsv4_hc_post(ctx, compute_ctx, src0, src1, src2, src3, node);
|
||||
|
||||
break;
|
||||
case GGML_OP_MEAN:
|
||||
ggml_vk_mean(ctx, compute_ctx, src0, node);
|
||||
@@ -19256,6 +19479,31 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case GGML_OP_DSV4_HC_COMB:
|
||||
case GGML_OP_DSV4_HC_PRE:
|
||||
case GGML_OP_DSV4_HC_POST:
|
||||
{
|
||||
if (op->type != GGML_TYPE_F32) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < GGML_MAX_SRC; ++i) {
|
||||
if (op->src[i] && op->src[i]->type != GGML_TYPE_F32) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// hc is hardcoded to 4 in the shaders. ggml only constrains it
|
||||
// to 4 for COMB, so PRE/POST have to be checked here.
|
||||
if (op->op == GGML_OP_DSV4_HC_PRE && op->src[0]->ne[1] != 4) {
|
||||
return false;
|
||||
}
|
||||
if (op->op == GGML_OP_DSV4_HC_POST && op->src[1]->ne[1] != 4) {
|
||||
return false;
|
||||
}
|
||||
if (op->op == GGML_OP_DSV4_HC_COMB) {
|
||||
return device->pipeline_dsv4_hc_comb_f32 != nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case GGML_OP_SOLVE_TRI:
|
||||
{
|
||||
if (op->type != GGML_TYPE_F32 || op->src[0]->type != GGML_TYPE_F32) {
|
||||
@@ -20244,6 +20492,13 @@ static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph *
|
||||
tensor_clone = ggml_sum_rows(ggml_ctx, src_clone[0]);
|
||||
} else if (tensor->op == GGML_OP_CUMSUM) {
|
||||
tensor_clone = ggml_cumsum(ggml_ctx, src_clone[0]);
|
||||
} else if (tensor->op == GGML_OP_DSV4_HC_COMB) {
|
||||
tensor_clone = ggml_dsv4_hc_comb(ggml_ctx, src_clone[0], src_clone[1], src_clone[2],
|
||||
ggml_get_op_params_f32(tensor, 0), ggml_get_op_params_i32(tensor, 1));
|
||||
} else if (tensor->op == GGML_OP_DSV4_HC_PRE) {
|
||||
tensor_clone = ggml_dsv4_hc_pre(ggml_ctx, src_clone[0], src_clone[1]);
|
||||
} else if (tensor->op == GGML_OP_DSV4_HC_POST) {
|
||||
tensor_clone = ggml_dsv4_hc_post(ggml_ctx, src_clone[0], src_clone[1], src_clone[2], src_clone[3]);
|
||||
} else if (tensor->op == GGML_OP_MEAN) {
|
||||
tensor_clone = ggml_mean(ggml_ctx, src_clone[0]);
|
||||
} else if (tensor->op == GGML_OP_ARGMAX) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#version 450
|
||||
|
||||
#extension GL_EXT_control_flow_attributes : require
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
#extension GL_KHR_shader_subgroup_shuffle : require
|
||||
|
||||
// 16 lanes per token, indexed idst + hc*isrc: idst in bits 0..1, isrc in bits 2..3,
|
||||
// so subgroupShuffleXor by 1|2 reduces a row and by 4|8 a column.
|
||||
|
||||
layout(constant_id = 0) const uint SUBGROUP_SIZE = 32;
|
||||
|
||||
layout(local_size_x_id = 0, local_size_y = 4, local_size_z = 1) in;
|
||||
|
||||
layout(push_constant) uniform parameter
|
||||
{
|
||||
uint n_tokens;
|
||||
|
||||
uint nbm0; uint nbm1; // mixes
|
||||
uint nbs0; // scale
|
||||
uint nbb0; // base
|
||||
uint nbd0; uint nbd1; uint nbd2; // dst
|
||||
|
||||
uint m_offset;
|
||||
uint s_offset;
|
||||
uint b_offset;
|
||||
uint d_offset;
|
||||
|
||||
float eps;
|
||||
uint n_iter;
|
||||
};
|
||||
|
||||
layout(binding = 0, std430) readonly buffer M { float data_m[]; };
|
||||
layout(binding = 1, std430) readonly buffer S { float data_s[]; };
|
||||
layout(binding = 2, std430) readonly buffer B { float data_b[]; };
|
||||
layout(binding = 3, std430) writeonly buffer D { float data_d[]; };
|
||||
|
||||
const uint hc = 4;
|
||||
const uint comb_offset = 2 * hc;
|
||||
|
||||
const uint TOKENS_PER_SUBGROUP = SUBGROUP_SIZE / 16;
|
||||
|
||||
void main() {
|
||||
const uint lane = gl_SubgroupInvocationID;
|
||||
const uint blk = lane >> 4; // which 16-lane block, i.e. which token
|
||||
const uint idx = lane & 15; // idst + hc*isrc
|
||||
|
||||
const uint sg = gl_WorkGroupID.x * gl_WorkGroupSize.y + gl_SubgroupID;
|
||||
const uint it = sg * TOKENS_PER_SUBGROUP + blk;
|
||||
|
||||
// no early return, the shuffles need every lane; out-of-range blocks compute a discarded value
|
||||
const bool in_range = it < n_tokens;
|
||||
|
||||
const float scale_comb = data_s[s_offset + 2 * nbs0];
|
||||
|
||||
float v = 0.0f;
|
||||
if (in_range) {
|
||||
v = data_m[m_offset + (comb_offset + idx) * nbm0 + it * nbm1] * scale_comb
|
||||
+ data_b[b_offset + (comb_offset + idx) * nbb0];
|
||||
}
|
||||
|
||||
// Softmax across destinations: the four lanes sharing an isrc.
|
||||
float vmax = max(v, subgroupShuffleXor(v, 1));
|
||||
vmax = max(vmax, subgroupShuffleXor(vmax, 2));
|
||||
v = exp(v - vmax);
|
||||
|
||||
float sum = v + subgroupShuffleXor(v, 1);
|
||||
sum += subgroupShuffleXor(sum, 2);
|
||||
v = v / sum + eps;
|
||||
|
||||
// Normalize columns: equal destination indices are four lanes apart.
|
||||
sum = v + subgroupShuffleXor(v, 4);
|
||||
sum += subgroupShuffleXor(sum, 8);
|
||||
v /= sum + eps;
|
||||
|
||||
for (uint i = 1; i < n_iter; ++i) {
|
||||
sum = v + subgroupShuffleXor(v, 1);
|
||||
sum += subgroupShuffleXor(sum, 2);
|
||||
v /= sum + eps;
|
||||
|
||||
sum = v + subgroupShuffleXor(v, 4);
|
||||
sum += subgroupShuffleXor(sum, 8);
|
||||
v /= sum + eps;
|
||||
}
|
||||
|
||||
if (in_range) {
|
||||
const uint idst = idx & 3;
|
||||
const uint isrc = idx >> 2;
|
||||
data_d[d_offset + idst * nbd0 + isrc * nbd1 + it * nbd2] = v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#version 450
|
||||
|
||||
#extension GL_EXT_control_flow_attributes : require
|
||||
|
||||
// Fan one stream back out to hc streams and add the combination-weighted
|
||||
// residuals:
|
||||
//
|
||||
// dst[i0, idst, it] = x[i0, it]*post[idst, it]
|
||||
// + sum_isrc residual[i0, isrc, it]*comb[idst, isrc, it]
|
||||
|
||||
layout(constant_id = 0) const uint BLOCK_SIZE = 256;
|
||||
|
||||
layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout(push_constant) uniform parameter
|
||||
{
|
||||
uint n_embd;
|
||||
uint n_tokens;
|
||||
|
||||
uint nbx0; uint nbx1; // x
|
||||
uint nbr0; uint nbr1; uint nbr2; // residual
|
||||
uint nbp0; uint nbp1; // post
|
||||
uint nbc0; uint nbc1; uint nbc2; // comb
|
||||
uint nbd0; uint nbd1; uint nbd2; // dst
|
||||
|
||||
uint x_offset;
|
||||
uint r_offset;
|
||||
uint p_offset;
|
||||
uint c_offset;
|
||||
uint d_offset;
|
||||
};
|
||||
|
||||
layout(binding = 0, std430) readonly buffer X { float data_x[]; };
|
||||
layout(binding = 1, std430) readonly buffer R { float data_r[]; };
|
||||
layout(binding = 2, std430) readonly buffer P { float data_p[]; };
|
||||
layout(binding = 3, std430) readonly buffer C { float data_c[]; };
|
||||
layout(binding = 4, std430) writeonly buffer D { float data_d[]; };
|
||||
|
||||
const uint hc = 4;
|
||||
|
||||
shared float post_s[hc];
|
||||
shared float comb_s[hc * hc];
|
||||
|
||||
void main() {
|
||||
const uint tid = gl_LocalInvocationID.x;
|
||||
const uint it = gl_WorkGroupID.y;
|
||||
|
||||
if (tid < hc) {
|
||||
post_s[tid] = data_p[p_offset + tid * nbp0 + it * nbp1];
|
||||
}
|
||||
if (tid < hc * hc) {
|
||||
const uint idst = tid & 3;
|
||||
const uint isrc = tid >> 2;
|
||||
comb_s[tid] = data_c[c_offset + idst * nbc0 + isrc * nbc1 + it * nbc2];
|
||||
}
|
||||
barrier();
|
||||
|
||||
// After the barrier, so every invocation reaches it.
|
||||
const uint i0 = gl_WorkGroupID.x * BLOCK_SIZE + tid;
|
||||
if (i0 >= n_embd) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float xv = data_x[x_offset + i0 * nbx0 + it * nbx1];
|
||||
|
||||
const uint rb = r_offset + i0 * nbr0 + it * nbr2;
|
||||
|
||||
float r[hc];
|
||||
[[unroll]]
|
||||
for (uint isrc = 0; isrc < hc; ++isrc) {
|
||||
r[isrc] = data_r[rb + isrc * nbr1];
|
||||
}
|
||||
|
||||
[[unroll]]
|
||||
for (uint idst = 0; idst < hc; ++idst) {
|
||||
float result = xv * post_s[idst];
|
||||
[[unroll]]
|
||||
for (uint isrc = 0; isrc < hc; ++isrc) {
|
||||
result = fma(r[isrc], comb_s[idst + hc * isrc], result);
|
||||
}
|
||||
data_d[d_offset + i0 * nbd0 + idst * nbd1 + it * nbd2] = result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#version 450
|
||||
|
||||
#extension GL_EXT_control_flow_attributes : require
|
||||
|
||||
// Collapse the hc residual streams of a token into one, weighted per stream:
|
||||
//
|
||||
// dst[i0, it] = sum_ih x[i0, ih, it] * weights[ih, it]
|
||||
|
||||
layout(constant_id = 0) const uint BLOCK_SIZE = 256;
|
||||
|
||||
layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout(push_constant) uniform parameter
|
||||
{
|
||||
uint n_embd;
|
||||
uint n_tokens;
|
||||
|
||||
uint nbx0; uint nbx1; uint nbx2; // x
|
||||
uint nbw0; uint nbw1; // weights
|
||||
uint nbd0; uint nbd1; // dst
|
||||
|
||||
uint x_offset;
|
||||
uint w_offset;
|
||||
uint d_offset;
|
||||
};
|
||||
|
||||
layout(binding = 0, std430) readonly buffer X { float data_x[]; };
|
||||
layout(binding = 1, std430) readonly buffer W { float data_w[]; };
|
||||
layout(binding = 2, std430) writeonly buffer D { float data_d[]; };
|
||||
|
||||
const uint hc = 4;
|
||||
|
||||
shared float w[hc];
|
||||
|
||||
void main() {
|
||||
const uint tid = gl_LocalInvocationID.x;
|
||||
const uint it = gl_WorkGroupID.y;
|
||||
|
||||
if (tid < hc) {
|
||||
w[tid] = data_w[w_offset + tid * nbw0 + it * nbw1];
|
||||
}
|
||||
barrier();
|
||||
|
||||
// After the barrier, so every invocation reaches it.
|
||||
const uint i0 = gl_WorkGroupID.x * BLOCK_SIZE + tid;
|
||||
if (i0 >= n_embd) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint xb = x_offset + i0 * nbx0 + it * nbx2;
|
||||
|
||||
float result = 0.0f;
|
||||
[[unroll]]
|
||||
for (uint ih = 0; ih < hc; ++ih) {
|
||||
result = fma(data_x[xb + ih * nbx1], w[ih], result);
|
||||
}
|
||||
|
||||
data_d[d_offset + i0 * nbd0 + it * nbd1] = result;
|
||||
}
|
||||
@@ -27,10 +27,10 @@ void main() {
|
||||
const uint i11 = gid_z / p.ne12;
|
||||
const uint i12 = gid_z % p.ne12;
|
||||
|
||||
const uint i01 = data_b[i10*p.nb10 + i11*p.nb11 + i12*p.nb12];
|
||||
const uint i01 = data_b[get_boffset() + i10*p.nb10 + i11*p.nb11 + i12*p.nb12];
|
||||
|
||||
const uint a_offset = i01*p.nb01 + i11*p.nb02 + i12*p.nb03;
|
||||
const uint d_offset = i10*p.nb21 + i11*p.nb22 + i12*p.nb23;
|
||||
const uint a_offset = get_aoffset() + i01*p.nb01 + i11*p.nb02 + i12*p.nb03;
|
||||
const uint d_offset = get_doffset() + i10*p.nb21 + i11*p.nb22 + i12*p.nb23;
|
||||
|
||||
const uint ib = a_offset + i00/QUANT_K; // block index
|
||||
const uint iqs = (i00%QUANT_K)/QUANT_R; // quant index
|
||||
|
||||
@@ -1042,6 +1042,9 @@ void process_shaders() {
|
||||
string_to_spv("fwht_f32", "fwht.comp", {});
|
||||
string_to_spv("fwht_shmem_f32", "fwht.comp", {{"FWHT_SHMEM", "1"}});
|
||||
string_to_spv("count_equal_i32", "count_equal.comp", merge_maps(base_dict, {{"A_TYPE", "int"}, {"B_TYPE", "int"}, {"D_TYPE", "int"}}));
|
||||
string_to_spv("dsv4_hc_comb_f32", "dsv4_hc_comb.comp", {});
|
||||
string_to_spv("dsv4_hc_pre_f32", "dsv4_hc_pre.comp", {});
|
||||
string_to_spv("dsv4_hc_post_f32", "dsv4_hc_post.comp", {});
|
||||
string_to_spv("cumsum_f32", "cumsum.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}}));
|
||||
string_to_spv("cumsum_multipass1_f32", "cumsum_multipass1.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}}));
|
||||
string_to_spv("cumsum_multipass2_f32", "cumsum_multipass2.comp", merge_maps(base_dict, {{"A_TYPE", "float"}, {"D_TYPE", "float"}}));
|
||||
|
||||
@@ -4324,12 +4324,22 @@ static bool ggml_backend_webgpu_device_supports_op(ggml_backend_dev_t dev, const
|
||||
src0->type == GGML_TYPE_F32 && (src1->type == GGML_TYPE_I64 || src1->type == GGML_TYPE_I32));
|
||||
break;
|
||||
case GGML_OP_GET_ROWS:
|
||||
if (src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16 || ggml_webgpu_supported_qtype(src0->type)) {
|
||||
supports_op = (op->type == GGML_TYPE_F32);
|
||||
} else if (src0->type == GGML_TYPE_I32) {
|
||||
supports_op = op->type == GGML_TYPE_I32;
|
||||
{
|
||||
const size_t storage_alignment =
|
||||
ctx->webgpu_global_ctx->capabilities.limits.minStorageBufferOffsetAlignment;
|
||||
const size_t src_address_unit =
|
||||
src0->type == GGML_TYPE_F32 && op->ne[0] % 4 == 0 ? 4 * sizeof(float) : ggml_type_size(src0->type);
|
||||
if (ggml_webgpu_tensor_misalignment(src0, storage_alignment) % src_address_unit != 0) {
|
||||
break;
|
||||
}
|
||||
if (src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16 ||
|
||||
ggml_webgpu_supported_qtype(src0->type)) {
|
||||
supports_op = (op->type == GGML_TYPE_F32);
|
||||
} else if (src0->type == GGML_TYPE_I32) {
|
||||
supports_op = op->type == GGML_TYPE_I32;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case GGML_OP_MUL_MAT:
|
||||
{
|
||||
switch (src1->type) {
|
||||
|
||||
@@ -215,6 +215,7 @@ class Keys:
|
||||
KV_LORA_RANK_SWA = "{arch}.attention.kv_lora_rank_swa"
|
||||
SHARED_KV_LAYERS = "{arch}.attention.shared_kv_layers"
|
||||
SLIDING_WINDOW_PATTERN = "{arch}.attention.sliding_window_pattern"
|
||||
RECURRENT_LAYERS = "{arch}.attention.recurrent_layers"
|
||||
TEMPERATURE_SCALE = "{arch}.attention.temperature_scale"
|
||||
ROPE_PATTERN = "{arch}.attention.rope_pattern"
|
||||
|
||||
|
||||
@@ -841,6 +841,9 @@ class GGUFWriter:
|
||||
else:
|
||||
self.add_array(key, value)
|
||||
|
||||
def add_recurrent_layers(self, value: Sequence[bool]) -> None:
|
||||
self.add_array(Keys.Attention.RECURRENT_LAYERS.format(arch=self.arch), value)
|
||||
|
||||
def add_rope_pattern(self, value: Sequence[bool]) -> None:
|
||||
self.add_array(Keys.Attention.ROPE_PATTERN.format(arch=self.arch), value)
|
||||
|
||||
|
||||
@@ -385,6 +385,7 @@ class TensorNameMap:
|
||||
MODEL_TENSOR.ATTN_SINKS: (
|
||||
"model.layers.{bid}.self_attn.sinks", # openai-moe
|
||||
"model.layers.{bid}.self_attn.attention_sink_bias", # mimov2
|
||||
"model.layers.{bid}.self_attn.learnable_sink_param", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.ATTN_GATE: (
|
||||
@@ -392,6 +393,7 @@ class TensorNameMap:
|
||||
"model.layers.{bid}.linear_attn.in_proj_z", # qwen3.5
|
||||
"model.layers.{bid}.self_attn.g_proj", # step3.5 head-wise attention gate
|
||||
"model.layers.{bid}.self_attn.output_gate", # minimax-01
|
||||
"model.layers.{bid}.self_attn.linear_gate", # hy-v4
|
||||
),
|
||||
|
||||
# Feed-forward norm
|
||||
@@ -1329,6 +1331,42 @@ class TensorNameMap:
|
||||
"model.layers.{bid}.self_attn.index_q_norm", # MSA
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_ATTN_FN: (
|
||||
"model.layers.{bid}.hc_attn_layer.hc_pre.hc_fn", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_ATTN_BASE: (
|
||||
"model.layers.{bid}.hc_attn_layer.hc_pre.hc_base", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_ATTN_SCALE: (
|
||||
"model.layers.{bid}.hc_attn_layer.hc_pre.hc_scale", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_FFN_FN: (
|
||||
"model.layers.{bid}.hc_mlp_layer.hc_pre.hc_fn", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_FFN_BASE: (
|
||||
"model.layers.{bid}.hc_mlp_layer.hc_pre.hc_base", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_FFN_SCALE: (
|
||||
"model.layers.{bid}.hc_mlp_layer.hc_pre.hc_scale", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_HEAD_FN: (
|
||||
"model.hc_head.hc_head_fn", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_HEAD_BASE: (
|
||||
"model.hc_head.hc_head_base", # hy-v4
|
||||
),
|
||||
|
||||
MODEL_TENSOR.HC_HEAD_SCALE: (
|
||||
"model.hc_head.hc_head_scale", # hy-v4
|
||||
),
|
||||
|
||||
############################################################################
|
||||
# TODO: these do not belong to block_mappings_cfg - move them to mappings_cfg
|
||||
MODEL_TENSOR.ENC_OUTPUT_NORM: (
|
||||
|
||||
+96
-15
@@ -2336,27 +2336,40 @@ struct test_get_rows : public test_case {
|
||||
const int r; // rows to get
|
||||
const int be1; // batch size
|
||||
const int be2; // batch size
|
||||
const bool v; // view (non-contiguous src1)
|
||||
const bool v; // view src1
|
||||
const bool vs0; // view src0
|
||||
|
||||
std::string vars() override {
|
||||
return VARS_TO_STR7(type, n, m, r, be1, be2, v);
|
||||
return VARS_TO_STR8(type, n, m, r, be1, be2, v, vs0);
|
||||
}
|
||||
|
||||
test_get_rows(ggml_type type = GGML_TYPE_F32, int n = 10, int m = 5, int r = 3, int be1 = 1, int be2 = 1, bool v = false)
|
||||
: type(type), n(n), m(m), r(r), be1(be1), be2(be2), v(v) {}
|
||||
test_get_rows(ggml_type type = GGML_TYPE_F32, int n = 10, int m = 5, int r = 3, int be1 = 1, int be2 = 1, bool v = false, bool vs0 = false)
|
||||
: type(type), n(n), m(m), r(r), be1(be1), be2(be2), v(v), vs0(vs0) {}
|
||||
|
||||
ggml_tensor * build_graph(ggml_context * ctx) override {
|
||||
ggml_tensor * in = ggml_new_tensor_4d(ctx, type, n, m, be1, be2);
|
||||
ggml_set_name(in, "in");
|
||||
ggml_tensor * in;
|
||||
if (vs0) {
|
||||
const int offset_rows = 3;
|
||||
const int padded_m = m + offset_rows;
|
||||
ggml_tensor * in_padded = ggml_new_tensor_4d(ctx, type, n, padded_m, be1, be2);
|
||||
ggml_set_name(in_padded, "in_padded");
|
||||
in = ggml_view_4d(ctx, in_padded, n, m, be1, be2,
|
||||
in_padded->nb[1], in_padded->nb[2], in_padded->nb[3],
|
||||
offset_rows * in_padded->nb[1]);
|
||||
ggml_set_name(in, "in_view");
|
||||
} else {
|
||||
in = ggml_new_tensor_4d(ctx, type, n, m, be1, be2);
|
||||
ggml_set_name(in, "in");
|
||||
}
|
||||
|
||||
ggml_tensor * rows = ggml_new_tensor_3d(ctx, GGML_TYPE_I32, r, be1, be2);
|
||||
ggml_tensor * rows = ggml_new_tensor_3d(ctx, GGML_TYPE_I32, v ? r + 1 : r, be1, be2);
|
||||
ggml_set_name(rows, "rows");
|
||||
if (v) {
|
||||
rows = ggml_view_3d(ctx, rows, r/2, be1, be2, rows->nb[1], rows->nb[2], 0);
|
||||
rows = ggml_view_3d(ctx, rows, r/2, be1, be2, rows->nb[1], rows->nb[2], rows->nb[0]);
|
||||
ggml_set_name(rows, "view_of_rows");
|
||||
}
|
||||
|
||||
const bool grad_supported = ggml_is_matrix(in) && ggml_is_vector(rows);
|
||||
const bool grad_supported = !vs0 && ggml_is_matrix(in) && ggml_is_vector(rows);
|
||||
if (grad_supported) {
|
||||
ggml_set_param(in);
|
||||
// rows is a constant input -> no gradients
|
||||
@@ -2370,14 +2383,16 @@ struct test_get_rows : public test_case {
|
||||
|
||||
void initialize_tensors(ggml_context * ctx) override {
|
||||
for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != NULL; t = ggml_get_next_tensor(ctx, t)) {
|
||||
if (ggml_is_view_op(t->op)) {
|
||||
continue;
|
||||
}
|
||||
if (t->type == GGML_TYPE_I32) {
|
||||
if (ggml_is_view_op(t->op)) { continue; }
|
||||
// rows
|
||||
std::vector<int> data(r*be1*be2);
|
||||
for (int i = 0; i < r*be1*be2; i++) {
|
||||
std::vector<int> data(ggml_nelements(t));
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
data[i] = rand() % m;
|
||||
}
|
||||
ggml_backend_tensor_set(t, data.data(), 0, r * be1 * be2 * sizeof(int));
|
||||
ggml_backend_tensor_set(t, data.data(), 0, data.size() * sizeof(int));
|
||||
} else {
|
||||
init_tensor_uniform(t);
|
||||
}
|
||||
@@ -7191,6 +7206,49 @@ struct test_group_norm_mul_add : public test_case {
|
||||
}
|
||||
};
|
||||
|
||||
// GGML_OP_L2_NORM x N: independent same-shape norms in one graph (strided qkv views or
|
||||
// contiguous), consuming adds nested so the norms stay adjacent in the graph.
|
||||
struct test_l2_norm_batch : public test_case {
|
||||
const ggml_type type;
|
||||
const std::array<int64_t, 4> ne;
|
||||
const int n_norms;
|
||||
const float eps;
|
||||
const bool strided;
|
||||
|
||||
std::string vars() override { return VARS_TO_STR5(type, ne, n_norms, eps, strided); }
|
||||
std::string op_desc(ggml_tensor * t) override { GGML_UNUSED(t); return "L2_NORM_BATCH"; }
|
||||
bool run_whole_graph() override { return true; }
|
||||
|
||||
test_l2_norm_batch(ggml_type type = GGML_TYPE_F32, std::array<int64_t, 4> ne = { 128, 16, 16, 1 },
|
||||
int n_norms = 4, float eps = 1e-12f, bool strided = true)
|
||||
: type(type), ne(ne), n_norms(n_norms), eps(eps), strided(strided) {}
|
||||
|
||||
ggml_tensor * build_graph(ggml_context * ctx) override {
|
||||
GGML_ASSERT(n_norms >= 2 && n_norms <= 8);
|
||||
ggml_tensor * parent = nullptr;
|
||||
if (strided) {
|
||||
parent = ggml_new_tensor_4d(ctx, type, ne[0], ne[1] * n_norms, ne[2], ne[3]); // qkv buffer
|
||||
}
|
||||
ggml_tensor * norms[8];
|
||||
for (int t = 0; t < n_norms; ++t) {
|
||||
ggml_tensor * src;
|
||||
if (strided) {
|
||||
src = ggml_view_4d(ctx, parent, ne[0], ne[1], ne[2], ne[3], parent->nb[1], parent->nb[2],
|
||||
parent->nb[3], t * ne[1] * parent->nb[1]);
|
||||
} else {
|
||||
src = ggml_new_tensor(ctx, type, 4, ne.data());
|
||||
}
|
||||
norms[t] = ggml_l2_norm(ctx, src, eps);
|
||||
}
|
||||
ggml_tensor * out = norms[n_norms - 1];
|
||||
for (int t = n_norms - 2; t >= 0; --t) {
|
||||
out = ggml_add(ctx, norms[t], out);
|
||||
}
|
||||
ggml_set_name(out, "out");
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
// GGML_OP_L2_NORM
|
||||
struct test_l2_norm : public test_case {
|
||||
const ggml_type type;
|
||||
@@ -8792,6 +8850,11 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
test_cases.emplace_back(new test_dsv4_hc_comb(17, 4));
|
||||
test_cases.emplace_back(new test_dsv4_hc_comb(257, 8));
|
||||
test_cases.emplace_back(new test_dsv4_hc_comb(17, 20));
|
||||
// production n_iter (DeepSeek-V4 uses 20) across batch sizes that cross
|
||||
// subgroup and workgroup boundaries; 1 = single-token decode
|
||||
for (int64_t n_tokens : {1, 256, 336, 512, 513, 1024, 2048}) {
|
||||
test_cases.emplace_back(new test_dsv4_hc_comb(n_tokens, 20));
|
||||
}
|
||||
|
||||
test_cases.emplace_back(new test_dsv4_hc_pre(1, 1));
|
||||
test_cases.emplace_back(new test_dsv4_hc_pre(31, 17));
|
||||
@@ -8848,13 +8911,17 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
for (ggml_type type : all_types) {
|
||||
for (int b : {1, 7}) {
|
||||
for (bool v : {false, true}) {
|
||||
test_cases.emplace_back(new test_get_rows(type, 256, 5, 4, b, 1, v));
|
||||
for (bool vs0 : {false, true}) {
|
||||
test_cases.emplace_back(new test_get_rows(type, 256, 5, 4, b, 1, v, vs0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int b : {1, 7}) {
|
||||
for (bool v : {false, true}) {
|
||||
test_cases.emplace_back(new test_get_rows(GGML_TYPE_I32, 256, 5, 4, b, 1, v));
|
||||
for (bool vs0 : {false, true}) {
|
||||
test_cases.emplace_back(new test_get_rows(GGML_TYPE_I32, 256, 5, 4, b, 1, v, vs0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9471,6 +9538,10 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
test_cases.emplace_back(new test_l2_norm(GGML_TYPE_F32, { n, 5, 4, 3 }, eps, false));
|
||||
test_cases.emplace_back(new test_l2_norm(GGML_TYPE_F32, { n, 5, 4, 3 }, eps, true));
|
||||
test_cases.emplace_back(new test_l2_norm(GGML_TYPE_F32, { n, 5, 4, 3 }, eps, false, true));
|
||||
// sibling batching: strided (production shape) and contiguous, 2 and 4 wide
|
||||
test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 5, 4, 3 }, 2, eps, true));
|
||||
test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 5, 4, 3 }, 4, eps, true));
|
||||
test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 5, 4, 3 }, 4, eps, false));
|
||||
}
|
||||
// row lengths that are not a multiple of 32, for the scalar (33) and float4 (132, 260) paths
|
||||
for (uint32_t n : { 33, 132, 260 }) {
|
||||
@@ -11157,6 +11228,16 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() {
|
||||
}
|
||||
}
|
||||
|
||||
// launch-overhead isolation: single L2_NORM launch vs batched siblings at the GDN
|
||||
// production shape (strided qkv views) -- perf-mode only, the eval list has its own
|
||||
// 2/4-wide coverage
|
||||
for (int n : { 128, 256 }) {
|
||||
test_cases.emplace_back(new test_l2_norm(GGML_TYPE_F32, { n, 16, 16, 1 }, 1e-12f, false, false));
|
||||
test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 16, 16, 1 }, 2, 1e-12f, true));
|
||||
test_cases.emplace_back(new test_l2_norm_batch(GGML_TYPE_F32, { n, 16, 16, 1 }, 4, 1e-12f, true));
|
||||
}
|
||||
|
||||
|
||||
return test_cases;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
@@ -139,7 +140,7 @@ static const std::vector<std::string> hole = {
|
||||
"dflash-model-Q8_0.gguf",
|
||||
};
|
||||
|
||||
// unsloth-style naming with UD quants and a suffix MTP file
|
||||
// unsloth-style naming with UD quants and an uppercase infix MTP head
|
||||
static const std::vector<std::string> unsloth = {
|
||||
"model-UD-Q8_K_XL.gguf",
|
||||
"mmproj-BF16.gguf",
|
||||
@@ -180,6 +181,13 @@ static const std::vector<std::string> spark = {
|
||||
"dspark-model-MXFP4.gguf",
|
||||
};
|
||||
|
||||
// main weights plus a trailing-form mtp head, as in unsloth
|
||||
// gemma-4-E2B-it-GGUF (Model-Q4_0-mtp.gguf)
|
||||
static const std::vector<std::string> trailing = {
|
||||
"model-BF16.gguf",
|
||||
"model-BF16-mtp.gguf",
|
||||
};
|
||||
|
||||
// dspark outranks dflash in the type auto-selection
|
||||
static const std::vector<std::string> dspark_dflash = {
|
||||
"model-Q8_0.gguf",
|
||||
@@ -223,7 +231,7 @@ static const plan_case plan_cases[] = {
|
||||
// no tag and no default match falls back to the first model in the listing
|
||||
{"unsloth fallback", unsloth, "test/repo", "", true, true,
|
||||
"model-UD-Q8_K_XL.gguf", {"model-UD-Q8_K_XL.gguf"},
|
||||
"mmproj-BF16.gguf", "", "", "", ""},
|
||||
"mmproj-BF16.gguf", "model-MTP-BF16.gguf", "", "", ""},
|
||||
|
||||
// explicit hf_file picks that exact file
|
||||
{"flat hf_file", flat, "test/repo", "model-BF16.gguf", false, false,
|
||||
@@ -258,10 +266,11 @@ static const plan_case plan_cases[] = {
|
||||
"model-Q4_K_M.gguf", {"model-Q4_K_M.gguf"},
|
||||
"", "mtp-model-Q4_0.gguf", "dflash-model-Q8_0.gguf", "", ""},
|
||||
|
||||
// the mtp- keyword is case sensitive, a suffix -MTP file is not discovered
|
||||
// the uppercase infix MTP head is a sidecar too; it resolves at the
|
||||
// nearest quant to the tag when the exact one does not exist
|
||||
{"unsloth suffix mtp", unsloth, "test/repo:Q8_K_XL", "", true, false,
|
||||
"model-UD-Q8_K_XL.gguf", {"model-UD-Q8_K_XL.gguf"},
|
||||
"mmproj-BF16.gguf", "", "", "", ""},
|
||||
"mmproj-BF16.gguf", "model-MTP-BF16.gguf", "", "", ""},
|
||||
|
||||
// vendor prefixes and the dot quant convention both match the tag,
|
||||
// first match wins between two files at the same quant
|
||||
@@ -283,6 +292,45 @@ static const plan_case plan_cases[] = {
|
||||
{"spark tag sidecar", spark, "test/repo:BF16", "", true, false,
|
||||
"", {},
|
||||
"", "", "", "", "dspark-model-BF16.gguf"},
|
||||
|
||||
// a `<quant>-<sidecar>` tag resolves that sidecar alone, without a primary
|
||||
{"hole quant-sidecar tag", hole, "test/repo:Q4_0-mtp", "", false, false,
|
||||
"", {},
|
||||
"", "mtp-model-Q4_0.gguf", "", "", ""},
|
||||
|
||||
{"spark quant-sidecar tag", spark, "test/repo:BF16-dspark", "", false, false,
|
||||
"", {},
|
||||
"", "", "", "", "dspark-model-BF16.gguf"},
|
||||
|
||||
// a bare sidecar tag resolves the sidecar at any quant
|
||||
{"hole bare sidecar tag", hole, "test/repo:mtp", "", false, false,
|
||||
"", {},
|
||||
"", "mtp-model-Q4_0.gguf", "", "", ""},
|
||||
|
||||
// a trailing-form sidecar resolves as the sidecar alone, never as a primary
|
||||
{"trailing quant-sidecar tag", trailing, "test/repo:BF16-mtp", "", false, false,
|
||||
"", {},
|
||||
"", "model-BF16-mtp.gguf", "", "", ""},
|
||||
|
||||
// the plain tag resolves the plain model; the trailing-token file is skipped
|
||||
{"trailing plain tag", trailing, "test/repo:BF16", "", false, false,
|
||||
"model-BF16.gguf", {"model-BF16.gguf"},
|
||||
"", "", "", "", ""},
|
||||
|
||||
// a short-form sidecar (`mmproj-F16.gguf`) resolves by its bare quant tag
|
||||
{"unsloth quant-sidecar tag", unsloth, "test/repo:BF16-mmproj", "", false, false,
|
||||
"", {},
|
||||
"mmproj-BF16.gguf", "", "", "", ""},
|
||||
|
||||
// an uppercase infix sidecar resolves through the lowercase tag
|
||||
{"unsloth uppercase infix tag", unsloth, "test/repo:BF16-mtp", "", false, false,
|
||||
"", {},
|
||||
"", "model-MTP-BF16.gguf", "", "", ""},
|
||||
|
||||
// a sidecar token in the middle of the name resolves as the sidecar alone
|
||||
{"subdir quant-sidecar tag", subdir, "test/repo:Q8_0-mtp", "", false, false,
|
||||
"", {},
|
||||
"", "model-mtp-Q8_0.gguf", "", "", ""},
|
||||
};
|
||||
|
||||
static void check_plan(const plan_case & c) {
|
||||
@@ -468,6 +516,90 @@ static void test_task_assembly() {
|
||||
g_repos.clear();
|
||||
}
|
||||
|
||||
//
|
||||
// cache listing and removal against the isolated LLAMA_CACHE, using the
|
||||
// same filename grammar the plan tests exercise above
|
||||
//
|
||||
|
||||
static void cache_put(const std::string & repo, const std::string & path) {
|
||||
namespace fs = std::filesystem;
|
||||
auto local = fs::path(cached(repo, path));
|
||||
fs::create_directories(local.parent_path());
|
||||
{ std::ofstream(local) << "gguf"; }
|
||||
auto repo_dir = local.parent_path().parent_path().parent_path();
|
||||
auto refs = repo_dir / "refs";
|
||||
fs::create_directories(refs);
|
||||
auto ref = refs / "main";
|
||||
if (!fs::exists(ref)) {
|
||||
std::ofstream(ref) << COMMIT << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
static bool cache_lists(const std::string & repo_tag) {
|
||||
for (const auto & e : common_list_cached_models()) {
|
||||
if (e.to_string() == repo_tag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void test_cache_listing_and_remove() {
|
||||
namespace fs = std::filesystem;
|
||||
const std::string repo = "test/eh";
|
||||
|
||||
printf("test-model-resolution: cache listing and removal\n");
|
||||
|
||||
g_context = "cache fixture";
|
||||
cache_put(repo, "gemma-4-E2B-it-BF16.gguf"); // main weights
|
||||
cache_put(repo, "gemma-4-E2B-it-BF16-mtp.gguf"); // trailing-form draft head
|
||||
cache_put(repo, "gemma-4-31B-it-MTP-BF16.gguf"); // uppercase infix draft head
|
||||
cache_put(repo, "mmproj-gemma-4-E2B-it-BF16.gguf"); // mmproj sidecar
|
||||
cache_put(repo, "mmproj-F16.gguf"); // short-form mmproj
|
||||
cache_put(repo, "model-mtp-Q8_0.gguf"); // mid-name mtp sidecar
|
||||
|
||||
// a sidecar is listed under `<quant>-<sidecar>` in every form and case it
|
||||
// can be named; only the token-less main weights stay a loadable model
|
||||
REQUIRE(cache_lists("test/eh:BF16"));
|
||||
REQUIRE(cache_lists("test/eh:BF16-mtp"));
|
||||
REQUIRE(cache_lists("test/eh:BF16-mmproj"));
|
||||
REQUIRE(cache_lists("test/eh:F16-mmproj"));
|
||||
REQUIRE(cache_lists("test/eh:Q8_0-mtp"));
|
||||
REQUIRE(!cache_lists("test/eh:MTP"));
|
||||
|
||||
// a plain quant tag removes the model files and leaves every sidecar,
|
||||
// whatever form or case its name carries
|
||||
g_context = "remove plain quant";
|
||||
REQUIRE(common_download_remove("test/eh:BF16"));
|
||||
REQUIRE(!fs::exists(cached(repo, "gemma-4-E2B-it-BF16.gguf")));
|
||||
REQUIRE(fs::exists(cached(repo, "gemma-4-E2B-it-BF16-mtp.gguf")));
|
||||
REQUIRE(fs::exists(cached(repo, "gemma-4-31B-it-MTP-BF16.gguf")));
|
||||
REQUIRE(fs::exists(cached(repo, "mmproj-gemma-4-E2B-it-BF16.gguf")));
|
||||
REQUIRE(fs::exists(cached(repo, "mmproj-F16.gguf")));
|
||||
REQUIRE(fs::exists(cached(repo, "model-mtp-Q8_0.gguf")));
|
||||
|
||||
// a `<quant>-<sidecar>` tag removes every sidecar listed under it,
|
||||
// across forms and cases
|
||||
g_context = "remove quant-sidecar";
|
||||
REQUIRE(common_download_remove("test/eh:BF16-mtp"));
|
||||
REQUIRE(!fs::exists(cached(repo, "gemma-4-E2B-it-BF16-mtp.gguf")));
|
||||
REQUIRE(!fs::exists(cached(repo, "gemma-4-31B-it-MTP-BF16.gguf")));
|
||||
REQUIRE(fs::exists(cached(repo, "mmproj-gemma-4-E2B-it-BF16.gguf")));
|
||||
|
||||
// the short form removes by its bare quant tag
|
||||
REQUIRE(common_download_remove("test/eh:F16-mmproj"));
|
||||
REQUIRE(!fs::exists(cached(repo, "mmproj-F16.gguf")));
|
||||
|
||||
REQUIRE(common_download_remove("test/eh:Q8_0-mtp"));
|
||||
REQUIRE(!fs::exists(cached(repo, "model-mtp-Q8_0.gguf")));
|
||||
|
||||
// a bare sidecar tag is ambiguous across quants and is rejected
|
||||
g_context = "remove bare sidecar";
|
||||
cache_put(repo, "mtp-Model-Q4_0.gguf");
|
||||
REQUIRE(!common_download_remove("test/eh:mtp"));
|
||||
REQUIRE(fs::exists(cached(repo, "mtp-Model-Q4_0.gguf")));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// unbuffered, so a crash cannot swallow the reports already printed
|
||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
@@ -496,6 +628,7 @@ int main(void) {
|
||||
|
||||
test_plan_resolution();
|
||||
test_task_assembly();
|
||||
test_cache_listing_and_remove();
|
||||
|
||||
server.stop();
|
||||
server_thread.join();
|
||||
|
||||
+81
-102
@@ -80,18 +80,19 @@ struct server_lru_sched {
|
||||
}
|
||||
|
||||
// returns "" if no model can be given up
|
||||
std::string pick_victim(std::unique_lock<std::mutex> & lk, const std::string & exclude) {
|
||||
std::string pick_victim(std::unique_lock<std::mutex> & lk) {
|
||||
check_lock(lk);
|
||||
std::string victim;
|
||||
int64_t victim_last_used = 0;
|
||||
for (const auto & m : models.mapping) {
|
||||
if (m.first == exclude) {
|
||||
continue;
|
||||
}
|
||||
// a busy model is mid-request, one still coming up has no request to finish
|
||||
if (m.second.req_count != 0 || !m.second.meta.is_ready_or_sleep()) {
|
||||
continue;
|
||||
}
|
||||
// already on its way out, or a queued request wants it
|
||||
if (models.stopping_models.count(m.first) || find(m.first)) {
|
||||
continue;
|
||||
}
|
||||
if (victim.empty() || m.second.meta.last_used < victim_last_used) {
|
||||
victim = m.first;
|
||||
victim_last_used = m.second.meta.last_used;
|
||||
@@ -109,7 +110,7 @@ struct server_lru_sched {
|
||||
SRV_INF("request for name=%s joined the queue, %d waiting\n", model_id.c_str(), e->n_waiters);
|
||||
return;
|
||||
}
|
||||
queue.push_back({ model_id, 1, false, false });
|
||||
queue.push_back({ model_id, 1, false });
|
||||
SRV_INF("models_max reached, request for name=%s queued at position %zu\n",
|
||||
model_id.c_str(), queue.size());
|
||||
}
|
||||
@@ -144,85 +145,67 @@ struct server_lru_sched {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ok means the model is up: drop the entry, the other waiters just watch its status now
|
||||
// on failure the entry is back in line; on success it stays until its waiters leave,
|
||||
// so the model coming up is never picked as a victim before they use it
|
||||
void claim_done(std::unique_lock<std::mutex> & lk, const std::string & model_id, bool ok) {
|
||||
check_lock(lk);
|
||||
if (ok) {
|
||||
return;
|
||||
}
|
||||
for (auto it = queue.begin(); it != queue.end(); ++it) {
|
||||
if (it->model_id == model_id) {
|
||||
if (ok) {
|
||||
queue.erase(it);
|
||||
} else {
|
||||
it->loading = false;
|
||||
}
|
||||
it->loading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a model is on its way out for this entry, so other requests do not also give up one
|
||||
void mark_slot_pending(std::unique_lock<std::mutex> & lk, const std::string & model_id) {
|
||||
// evict idle models while queued requests outnumber the slots that are free or being freed
|
||||
// caller must hold models.mutex; never blocks, so it is safe from any thread
|
||||
void tick(std::unique_lock<std::mutex> & lk) {
|
||||
check_lock(lk);
|
||||
if (entry_t * e = find(model_id)) {
|
||||
e->slot_pending = true;
|
||||
if (models.base_params.models_max <= 0 || queue.empty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// model_id went idle: give up its slot if a queued request needs one
|
||||
// thread-safe, caller must NOT hold models.mutex
|
||||
void on_model_idle(const std::string & model_id) {
|
||||
if (models.base_params.models_max <= 0) {
|
||||
return; // no limit, nothing is ever queued
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(models.mutex);
|
||||
if (queue.empty()) {
|
||||
return;
|
||||
}
|
||||
size_t promised = 0;
|
||||
bool has_unserved = false;
|
||||
for (const auto & e : queue) {
|
||||
if (e.needs_slot()) {
|
||||
has_unserved = true;
|
||||
} else {
|
||||
promised++;
|
||||
}
|
||||
}
|
||||
if (!has_unserved) {
|
||||
return;
|
||||
}
|
||||
if ((int) count_running() - (int) promised < models.base_params.models_max) {
|
||||
return; // a slot is already on its way
|
||||
}
|
||||
// never give up a model that a queued request wants
|
||||
for (const auto & e : queue) {
|
||||
if (e.model_id == model_id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto it = models.mapping.find(model_id);
|
||||
if (it == models.mapping.end() || it->second.req_count != 0 || !it->second.meta.is_ready_or_sleep()) {
|
||||
return;
|
||||
}
|
||||
for (auto & e : queue) {
|
||||
if (!e.slot_pending) {
|
||||
e.slot_pending = true;
|
||||
break;
|
||||
int n_running = 0;
|
||||
int n_stopping = 0;
|
||||
for (const auto & m : models.mapping) {
|
||||
if (m.second.meta.is_running()) {
|
||||
n_running++;
|
||||
if (models.stopping_models.count(m.first)) {
|
||||
n_stopping++;
|
||||
}
|
||||
}
|
||||
}
|
||||
SRV_INF("model name=%s went idle, giving up its slot to a queued request\n", model_id.c_str());
|
||||
models.unload(model_id);
|
||||
int n_needed = 0;
|
||||
int n_claimed = 0; // claimed the slot, but load() has not spawned yet
|
||||
for (const auto & e : queue) {
|
||||
if (!e.loading) {
|
||||
n_needed++;
|
||||
continue;
|
||||
}
|
||||
auto it = models.mapping.find(e.model_id);
|
||||
if (it != models.mapping.end() && !it->second.meta.is_running()) {
|
||||
n_claimed++;
|
||||
}
|
||||
}
|
||||
int n_free = models.base_params.models_max - n_running + n_stopping - n_claimed;
|
||||
while (n_free < n_needed) {
|
||||
std::string victim = pick_victim(lk);
|
||||
if (victim.empty()) {
|
||||
return; // all remaining models are busy, wait for a request to end
|
||||
}
|
||||
SRV_INF("evicting idle LRU name=%s for a queued request\n", victim.c_str());
|
||||
models.request_stop(victim);
|
||||
n_free++;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct entry_t {
|
||||
std::string model_id;
|
||||
int n_waiters; // requests waiting for this model
|
||||
bool slot_pending; // a model is already being evicted for this entry
|
||||
bool loading; // one of the waiters is doing the load right now
|
||||
|
||||
// a slot is already coming, or already taken by the load in flight
|
||||
bool needs_slot() const { return !slot_pending && !loading; }
|
||||
int n_waiters; // requests waiting for this model
|
||||
bool loading; // one of the waiters is doing the load right now
|
||||
};
|
||||
|
||||
entry_t * find(const std::string & model_id) {
|
||||
@@ -946,7 +929,7 @@ void server_models::unload_lru() {
|
||||
if (sched->has_capacity(lk)) {
|
||||
return;
|
||||
}
|
||||
lru_model_name = sched->pick_victim(lk, "");
|
||||
lru_model_name = sched->pick_victim(lk);
|
||||
}
|
||||
if (!lru_model_name.empty()) {
|
||||
SRV_INF("models_max limit reached, removing LRU name=%s\n", lru_model_name.c_str());
|
||||
@@ -1169,6 +1152,11 @@ void server_models::load(const std::string & name, const load_options & opts) {
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
void server_models::request_stop(const std::string & name) {
|
||||
stopping_models.insert(name);
|
||||
cv_stop.notify_all();
|
||||
}
|
||||
|
||||
void server_models::unload(const std::string & name) {
|
||||
std::unique_lock<std::mutex> lk(mutex);
|
||||
auto it = mapping.find(name);
|
||||
@@ -1182,13 +1170,12 @@ void server_models::unload(const std::string & name) {
|
||||
});
|
||||
} else if (it->second.meta.is_running()) {
|
||||
SRV_INF("stopping model instance name=%s\n", name.c_str());
|
||||
stopping_models.insert(name);
|
||||
if (it->second.meta.status == SERVER_MODEL_STATUS_LOADING) {
|
||||
// special case: if model is in loading state, unloading means force-killing it
|
||||
SRV_WRN("model name=%s is still loading, force-killing\n", name.c_str());
|
||||
it->second.subproc->terminate();
|
||||
}
|
||||
cv_stop.notify_all();
|
||||
request_stop(name);
|
||||
// status change will be handled by the managing thread
|
||||
} else {
|
||||
SRV_WRN("model instance name=%s is not running\n", name.c_str());
|
||||
@@ -1206,8 +1193,7 @@ void server_models::unload_all() {
|
||||
inst.subproc->stopped.store(true, std::memory_order_relaxed);
|
||||
} else if (inst.meta.is_running()) {
|
||||
SRV_INF("stopping model instance name=%s\n", name.c_str());
|
||||
stopping_models.insert(name);
|
||||
cv_stop.notify_all();
|
||||
request_stop(name);
|
||||
// status change will be handled by the managing thread
|
||||
}
|
||||
// moving the thread to join list to avoid deadlock
|
||||
@@ -1234,6 +1220,8 @@ void server_models::update_status(const std::string & name, const update_status_
|
||||
if (!args.progress.is_null()) {
|
||||
meta.progress = args.progress;
|
||||
}
|
||||
// a model that comes up idle or goes down changes the slot count for queued requests
|
||||
sched->tick(lk);
|
||||
}
|
||||
// broadcast status change to SSE
|
||||
{
|
||||
@@ -1332,14 +1320,22 @@ bool server_models::remove(const std::string & name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// join before erasing - thread no longer acquires this mutex
|
||||
if (it->second.th.joinable()) {
|
||||
it->second.th.join();
|
||||
// on the cancelled-download path the status flips to DOWNLOADED while the
|
||||
// monitoring thread still has a mutex-guarded step left, so joining under
|
||||
// the lock would deadlock - join outside, as load_models() does
|
||||
std::thread th = std::move(it->second.th);
|
||||
mapping.erase(name);
|
||||
lk.unlock();
|
||||
|
||||
// join first so the monitoring thread's final mutex-guarded cleanup cannot
|
||||
// race the disk removal, then remove from disk without holding the lock
|
||||
// (best-effort: cancelled downloads may have no cached files)
|
||||
if (th.joinable()) {
|
||||
th.join();
|
||||
}
|
||||
|
||||
// remove from disk (best-effort: cancelled downloads may have no cached files)
|
||||
bool ok = common_download_remove(name);
|
||||
mapping.erase(name);
|
||||
|
||||
if (!ok) {
|
||||
SRV_WRN("removing model name=%s from disk returned false (no cached files?)\n", name.c_str());
|
||||
}
|
||||
@@ -1380,13 +1376,11 @@ bool server_models::ensure_model_ready(const std::string & name, const std::func
|
||||
|
||||
bool queued = false;
|
||||
bool did_load = false;
|
||||
std::string victim;
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(mutex);
|
||||
auto it = mapping.find(name);
|
||||
if (it != mapping.end() && it->second.meta.status == SERVER_MODEL_STATUS_UNLOADED) {
|
||||
bool has_capacity = sched->has_capacity(lk);
|
||||
if (has_capacity && sched->queue_empty(lk)) {
|
||||
if (sched->has_capacity(lk) && sched->queue_empty(lk)) {
|
||||
lk.unlock();
|
||||
SRV_INF("model name=%s is not loaded, loading...\n", name.c_str());
|
||||
load(name);
|
||||
@@ -1394,21 +1388,11 @@ bool server_models::ensure_model_ready(const std::string & name, const std::func
|
||||
} else {
|
||||
// also queue when a slot looks free but others wait already, else they starve
|
||||
sched->join(lk, name);
|
||||
sched->tick(lk);
|
||||
queued = true;
|
||||
if (!has_capacity) {
|
||||
// an idle model may sit here right now, do not wait for a request to end
|
||||
victim = sched->pick_victim(lk, name);
|
||||
if (!victim.empty()) {
|
||||
sched->mark_slot_pending(lk, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!victim.empty()) {
|
||||
SRV_INF("evicting idle LRU name=%s to make room for name=%s\n", victim.c_str(), name.c_str());
|
||||
unload(victim);
|
||||
}
|
||||
|
||||
// while queued, this is also where the load happens: the head of the queue does it
|
||||
SRV_INF("waiting until model name=%s is fully loaded...\n", name.c_str());
|
||||
@@ -1470,9 +1454,7 @@ bool server_models::ensure_model_ready(const std::string & name, const std::func
|
||||
}
|
||||
lk.lock();
|
||||
sched->claim_done(lk, name, ok);
|
||||
if (ok) {
|
||||
queued = false; // entry is gone, the other waiters watch the status now
|
||||
}
|
||||
sched->tick(lk);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1480,6 +1462,7 @@ bool server_models::ensure_model_ready(const std::string & name, const std::func
|
||||
}
|
||||
} catch (...) {
|
||||
leave_queue();
|
||||
sched->tick(lk); // a slot freed for this waiter goes to the next one
|
||||
throw;
|
||||
}
|
||||
leave_queue();
|
||||
@@ -1529,18 +1512,14 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
|
||||
);
|
||||
|
||||
proxy->cleanup = [this, name]() {
|
||||
bool went_idle = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(mutex);
|
||||
auto it = mapping.find(name);
|
||||
if (it != mapping.end() && it->second.req_count > 0) {
|
||||
it->second.req_count--;
|
||||
went_idle = it->second.req_count == 0;
|
||||
std::unique_lock<std::mutex> lk(mutex);
|
||||
auto it = mapping.find(name);
|
||||
if (it != mapping.end() && it->second.req_count > 0) {
|
||||
it->second.req_count--;
|
||||
if (it->second.req_count == 0) {
|
||||
sched->tick(lk);
|
||||
}
|
||||
}
|
||||
if (went_idle) {
|
||||
sched->on_model_idle(name);
|
||||
}
|
||||
};
|
||||
|
||||
return proxy;
|
||||
|
||||
@@ -216,6 +216,10 @@ private:
|
||||
// not thread-safe, caller must hold mutex
|
||||
void add_model(server_model_meta && meta);
|
||||
|
||||
// ask the monitoring thread to stop a running instance
|
||||
// not thread-safe, caller must hold mutex
|
||||
void request_stop(const std::string & name);
|
||||
|
||||
// notify SSE clients
|
||||
void notify_sse(const std::string & event, const std::string & model_id, const json & data = nullptr);
|
||||
|
||||
|
||||
@@ -297,6 +297,26 @@ def test_router_queue_is_fifo():
|
||||
assert first.done_at < second.done_at, "queue was not served in arrival order"
|
||||
|
||||
|
||||
def test_router_queue_two_waiters_share_one_eviction():
|
||||
"""two requests that both find the same idle model must both be served in the end"""
|
||||
global server
|
||||
server.models_max = 1
|
||||
server.start()
|
||||
|
||||
_load_model_and_wait(MODEL_A, timeout=120)
|
||||
|
||||
# both arrive while MODEL_A is idle, so both want its slot; only one eviction can happen
|
||||
first = _Bg(lambda: _tokenize(MODEL_B)).start()
|
||||
second = _Bg(lambda: _tokenize(MODEL_C)).start()
|
||||
|
||||
first.join(90)
|
||||
second.join(90)
|
||||
|
||||
first.assert_ok("first queued request")
|
||||
second.assert_ok("second queued request")
|
||||
assert _get_model_status(MODEL_A) == "unloaded"
|
||||
|
||||
|
||||
def test_router_no_models_autoload():
|
||||
global server
|
||||
server.no_models_autoload = True
|
||||
|
||||
Generated
+8
-5
@@ -71,7 +71,7 @@
|
||||
"svelte-check": "4.6.0",
|
||||
"svelte-sonner": "1.1.1",
|
||||
"tailwind-merge": "3.6.0",
|
||||
"tailwind-variants": "3.2.2",
|
||||
"tailwind-variants": "3.3.1",
|
||||
"tailwindcss": "4.3.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"typescript": "5.9.3",
|
||||
@@ -16048,13 +16048,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-variants": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz",
|
||||
"integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.3.1.tgz",
|
||||
"integrity": "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.x",
|
||||
"node": ">=16.9.x",
|
||||
"pnpm": ">=7.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -16064,6 +16064,9 @@
|
||||
"peerDependenciesMeta": {
|
||||
"tailwind-merge": {
|
||||
"optional": true
|
||||
},
|
||||
"tailwindcss": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"reset": "rm -rf .svelte-kit node_modules",
|
||||
"format": "eslint --fix . && prettier --write .",
|
||||
"format:files": "sh -c 'eslint --fix \"$@\" && prettier --write \"$@\"' sh",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test": "npm run test:ui -- --run && npm run test:client -- --run && npm run test:unit -- --run && npm run test:e2e",
|
||||
"test:e2e": "playwright test",
|
||||
@@ -90,7 +91,7 @@
|
||||
"svelte-check": "4.6.0",
|
||||
"svelte-sonner": "1.1.1",
|
||||
"tailwind-merge": "3.6.0",
|
||||
"tailwind-variants": "3.2.2",
|
||||
"tailwind-variants": "3.3.1",
|
||||
"tailwindcss": "4.3.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"typescript": "5.9.3",
|
||||
|
||||
Vendored
+12
-18
@@ -18,20 +18,16 @@ import type {
|
||||
ApiErrorResponse,
|
||||
ApiLlamaCppServerProps,
|
||||
ApiModelDataEntry,
|
||||
ApiModelListResponse,
|
||||
ApiModelLoadStage,
|
||||
ApiModelsDownloadRequest,
|
||||
ApiModelsDownloadResponse,
|
||||
ApiModelsListResponse,
|
||||
ApiModelsLoadResponse,
|
||||
ApiModelsSseData,
|
||||
ApiModelsSseEvent,
|
||||
ApiModelsSseProgress,
|
||||
ApiModelsUnloadResponse,
|
||||
ApiProcessingState,
|
||||
ApiRouterModelMeta,
|
||||
ApiRouterModelsListResponse,
|
||||
ApiRouterModelsLoadRequest,
|
||||
ApiRouterModelsLoadResponse,
|
||||
ApiRouterModelsStatusRequest,
|
||||
ApiRouterModelsStatusResponse,
|
||||
ApiRouterModelsUnloadRequest,
|
||||
ApiRouterModelsUnloadResponse,
|
||||
ChatAttachmentDisplayItem,
|
||||
// Chat types
|
||||
ChatMessagePromptProgress,
|
||||
@@ -86,19 +82,17 @@ declare global {
|
||||
ApiLlamaCppServerProps,
|
||||
ApiModelDataEntry,
|
||||
ApiModelLoadStage,
|
||||
ModelDownloadProgress,
|
||||
ApiModelsSseProgress,
|
||||
ApiModelsSseData,
|
||||
ApiModelsSseDownloadProgressData,
|
||||
ApiModelsSseEvent,
|
||||
ApiModelListResponse,
|
||||
ApiModelsListResponse,
|
||||
ApiModelsLoadResponse,
|
||||
ApiModelsDownloadRequest,
|
||||
ApiModelsDownloadResponse,
|
||||
ApiModelsUnloadResponse,
|
||||
ApiProcessingState,
|
||||
ApiRouterModelMeta,
|
||||
ApiRouterModelsLoadRequest,
|
||||
ApiRouterModelsLoadResponse,
|
||||
ApiRouterModelsStatusRequest,
|
||||
ApiRouterModelsStatusResponse,
|
||||
ApiRouterModelsListResponse,
|
||||
ApiRouterModelsUnloadRequest,
|
||||
ApiRouterModelsUnloadResponse,
|
||||
// Chat types
|
||||
ChatAttachmentDisplayItem,
|
||||
ChatMessagePromptProgress,
|
||||
|
||||
@@ -220,8 +220,7 @@ export { default as ChatFormActionModels } from './ChatForm/ChatFormActions/Chat
|
||||
*/
|
||||
export { default as ChatFormActionAddToolsSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte';
|
||||
|
||||
/**
|
||||
* Dropdown submenu for selecting reasoning effort level.
|
||||
/** Dropdown submenu for selecting reasoning effort level.
|
||||
*
|
||||
* Shows a "Reasoning" sub-menu item with a lightbulb icon indicating
|
||||
* thinking status, and a nested list of effort levels.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
getMdastNodeHash,
|
||||
isAppendMode
|
||||
} from './markdown-utils';
|
||||
import { SAFE_HTML_CONFIG } from './safe-html-config';
|
||||
import {
|
||||
ActionIconCopyToClipboard,
|
||||
CodeBlockActions,
|
||||
@@ -44,6 +45,7 @@
|
||||
import { detectIncompleteCodeBlock, highlightCode, type IncompleteCodeBlock } from '$lib/utils';
|
||||
import { sanitizeSvg } from '$lib/utils/sanitize-svg';
|
||||
import { mountSvgShadow } from '$lib/utils/svg-shadow';
|
||||
import DOMPurify from 'dompurify';
|
||||
import type { Root as HastRoot, RootContent as HastRootContent } from 'hast';
|
||||
import githubLightCss from 'highlight.js/styles/github.css?inline';
|
||||
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
|
||||
@@ -57,6 +59,8 @@
|
||||
content: string;
|
||||
class?: string;
|
||||
disableMath?: boolean;
|
||||
/** Render raw HTML found in the markdown (sanitized) instead of escaping it. */
|
||||
allowHtml?: boolean;
|
||||
}
|
||||
|
||||
interface MarkdownBlock {
|
||||
@@ -65,7 +69,13 @@
|
||||
contentHash?: string;
|
||||
}
|
||||
|
||||
let { attachments, class: className = '', content, disableMath = false }: Props = $props();
|
||||
let {
|
||||
allowHtml = false,
|
||||
attachments,
|
||||
class: className = '',
|
||||
content,
|
||||
disableMath = false
|
||||
}: Props = $props();
|
||||
|
||||
let containerRef = $state<HTMLDivElement>();
|
||||
let renderedBlocks = $state<MarkdownBlock[]>([]);
|
||||
@@ -119,6 +129,11 @@
|
||||
let pendingMarkdown: string | null = null;
|
||||
let isProcessing = false;
|
||||
|
||||
// Raw HTML in model cards renders after sanitization with an explicit allow
|
||||
// list: no scripts, event handlers, forms, iframes or style blocks pass.
|
||||
// Covers the tags model cards use plus the KaTeX output (spans with inline
|
||||
// styles and MathML).
|
||||
|
||||
// Per-instance transform cache, avoids re-transforming stable blocks during streaming
|
||||
// Garbage collected when component is destroyed (on conversation change)
|
||||
const transformCache = new SvelteMap<string, string>();
|
||||
@@ -183,7 +198,10 @@
|
||||
index: number
|
||||
): Promise<{ html: string; hash: string }> {
|
||||
const hash = getMdastNodeHash(node, index);
|
||||
const cached = transformCache.get(hash);
|
||||
// the rendered HTML also depends on allowHtml (sanitized raw vs escaped),
|
||||
// so the cache is keyed per mode
|
||||
const cacheKey = `${allowHtml ? 'a' : 'p'}:${hash}`;
|
||||
const cached = transformCache.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return { hash, html: cached };
|
||||
@@ -192,10 +210,13 @@
|
||||
const singleNodeRoot = { children: [node], type: 'root' };
|
||||
const transformedRoot = (await processorInstance.run(singleNodeRoot as MdastRoot)) as HastRoot;
|
||||
const html = processorInstance.stringify(transformedRoot);
|
||||
const safeHtml = allowHtml
|
||||
? (DOMPurify.sanitize(html, SAFE_HTML_CONFIG) as unknown as string)
|
||||
: html;
|
||||
|
||||
transformCache.set(hash, html);
|
||||
transformCache.set(cacheKey, safeHtml);
|
||||
|
||||
return { hash, html };
|
||||
return { hash, html: safeHtml };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,7 +321,7 @@
|
||||
|
||||
if (prefixMarkdown.trim()) {
|
||||
const normalizedPrefix = preprocessLaTeX(prefixMarkdown);
|
||||
const processorInstance = getMarkdownProcessor({ attachments, disableMath });
|
||||
const processorInstance = getMarkdownProcessor({ allowHtml, attachments, disableMath });
|
||||
const ast = processorInstance.parse(normalizedPrefix) as MdastRoot;
|
||||
const mdastChildren = (ast as { children?: unknown[] }).children ?? [];
|
||||
const nextBlocks: MarkdownBlock[] = [];
|
||||
@@ -350,7 +371,7 @@
|
||||
incompleteCodeBlock = null;
|
||||
|
||||
const normalized = preprocessLaTeX(markdown);
|
||||
const processorInstance = getMarkdownProcessor({ attachments, disableMath });
|
||||
const processorInstance = getMarkdownProcessor({ allowHtml, attachments, disableMath });
|
||||
const ast = processorInstance.parse(normalized) as MdastRoot;
|
||||
const mdastChildren = (ast as { children?: unknown[] }).children ?? [];
|
||||
const stableCount = Math.max(mdastChildren.length - 1, 0);
|
||||
@@ -394,6 +415,10 @@
|
||||
)) as HastRoot;
|
||||
|
||||
unstableHtml = processorInstance.stringify(transformedRoot);
|
||||
|
||||
if (allowHtml) {
|
||||
unstableHtml = DOMPurify.sanitize(unstableHtml, SAFE_HTML_CONFIG) as unknown as string;
|
||||
}
|
||||
}
|
||||
|
||||
renderedBlocks = nextBlocks;
|
||||
|
||||
@@ -41,12 +41,15 @@ export interface MarkdownProcessor {
|
||||
export interface MarkdownProcessorOptions {
|
||||
attachments?: DatabaseMessageExtra[];
|
||||
disableMath?: boolean;
|
||||
/** Render raw HTML found in the markdown instead of escaping it. */
|
||||
allowHtml?: boolean;
|
||||
}
|
||||
|
||||
const sharedPipelines = new Map<string, MarkdownProcessor>();
|
||||
const attachmentPipelines = new WeakMap<object, MarkdownProcessor>();
|
||||
const attachmentPipelines = new WeakMap<object, Map<string, MarkdownProcessor>>();
|
||||
|
||||
function buildPipeline({
|
||||
allowHtml = false,
|
||||
attachments,
|
||||
disableMath = false
|
||||
}: MarkdownProcessorOptions): MarkdownProcessor {
|
||||
@@ -57,11 +60,15 @@ function buildPipeline({
|
||||
proc = proc.use(remarkMath); // Parse $inline$ and $$block$$ math
|
||||
}
|
||||
|
||||
proc = proc
|
||||
.use(remarkBreaks) // Convert line breaks to <br>
|
||||
proc = proc.use(remarkBreaks); // Convert line breaks to <br>
|
||||
|
||||
if (!allowHtml) {
|
||||
// Treat raw HTML as literal text with preserved indentation
|
||||
.use(remarkLiteralHtml)
|
||||
.use(remarkRehype); // Convert Markdown AST to rehype
|
||||
proc = proc.use(remarkLiteralHtml);
|
||||
}
|
||||
|
||||
// Convert Markdown AST to rehype. Keep raw HTML as-is when allowHtml is set.
|
||||
proc = proc.use(remarkRehype, allowHtml ? { allowDangerousHtml: true } : undefined);
|
||||
|
||||
if (!disableMath) {
|
||||
proc = proc.use(rehypeKatex); // Render math using KaTeX
|
||||
@@ -89,17 +96,26 @@ function buildPipeline({
|
||||
|
||||
export function getMarkdownProcessor(options: MarkdownProcessorOptions): MarkdownProcessor {
|
||||
if (options.attachments && options.attachments.length > 0) {
|
||||
let cached = attachmentPipelines.get(options.attachments);
|
||||
let byOptions = attachmentPipelines.get(options.attachments);
|
||||
|
||||
if (!byOptions) {
|
||||
byOptions = new Map<string, MarkdownProcessor>();
|
||||
attachmentPipelines.set(options.attachments, byOptions);
|
||||
}
|
||||
|
||||
const key = `${Boolean(options.disableMath)}:${Boolean(options.allowHtml)}`;
|
||||
|
||||
let cached = byOptions.get(key);
|
||||
|
||||
if (!cached) {
|
||||
cached = buildPipeline(options);
|
||||
attachmentPipelines.set(options.attachments, cached);
|
||||
byOptions.set(key, cached);
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
const key = String(Boolean(options.disableMath));
|
||||
const key = `${Boolean(options.disableMath)}:${Boolean(options.allowHtml)}`;
|
||||
|
||||
let cached = sharedPipelines.get(key);
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Sanitization rules for raw HTML in model cards (MarkdownContent's
|
||||
// allowHtml mode). Kept as its own module so the adversarial test in
|
||||
// tests/client can import the exact shipped configuration.
|
||||
|
||||
/**
|
||||
* Explicit DOMPurify allow list: the tags and attributes model cards may use
|
||||
* plus the KaTeX output (spans with inline styles and MathML). Everything
|
||||
* else - scripts, event handlers, forms, iframes, style tags - is stripped.
|
||||
*/
|
||||
export const SAFE_HTML_CONFIG = {
|
||||
ALLOWED_ATTR: [
|
||||
'class',
|
||||
'style',
|
||||
'id',
|
||||
'title',
|
||||
'role',
|
||||
'aria-hidden',
|
||||
'aria-label',
|
||||
'aria-describedby',
|
||||
'aria-expanded',
|
||||
'href',
|
||||
'target',
|
||||
'rel',
|
||||
'src',
|
||||
'srcset',
|
||||
'alt',
|
||||
'width',
|
||||
'height',
|
||||
'align',
|
||||
'valign',
|
||||
'colspan',
|
||||
'rowspan',
|
||||
'controls',
|
||||
'type',
|
||||
// MathML
|
||||
'xmlns',
|
||||
'display',
|
||||
'encoding',
|
||||
'mathvariant'
|
||||
],
|
||||
ALLOWED_TAGS: [
|
||||
// text and structure
|
||||
'a',
|
||||
'b',
|
||||
'i',
|
||||
'u',
|
||||
's',
|
||||
'em',
|
||||
'strong',
|
||||
'code',
|
||||
'pre',
|
||||
'kbd',
|
||||
'sub',
|
||||
'sup',
|
||||
'br',
|
||||
'hr',
|
||||
'p',
|
||||
'span',
|
||||
'div',
|
||||
'blockquote',
|
||||
'center',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'dl',
|
||||
'dt',
|
||||
'dd',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
'colgroup',
|
||||
'col',
|
||||
'caption',
|
||||
// media
|
||||
'img',
|
||||
'picture',
|
||||
'source',
|
||||
'figure',
|
||||
'figcaption',
|
||||
'video',
|
||||
'audio',
|
||||
// MathML, for the KaTeX output
|
||||
'math',
|
||||
'mrow',
|
||||
'mi',
|
||||
'mo',
|
||||
'mn',
|
||||
'ms',
|
||||
'mtext',
|
||||
'mfrac',
|
||||
'mroot',
|
||||
'msqrt',
|
||||
'msub',
|
||||
'msup',
|
||||
'msubsup',
|
||||
'munder',
|
||||
'mover',
|
||||
'mmultiscripts',
|
||||
'mtable',
|
||||
'mtr',
|
||||
'mtd',
|
||||
'mspace',
|
||||
'mglyph',
|
||||
'maligngroup',
|
||||
'malignmark',
|
||||
'mpadded',
|
||||
'mphantom',
|
||||
'mstyle',
|
||||
'semantics',
|
||||
'annotation'
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import DialogConfirmation from '$lib/components/app/dialogs/DialogConfirmation.svelte';
|
||||
import { ModelDownloadConfirmAction } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
/** Action being confirmed; drives the wording. */
|
||||
action: ModelDownloadConfirmAction;
|
||||
/** `<repo>:<tag>` the action targets. */
|
||||
repoWithTag: string;
|
||||
onClose: () => void;
|
||||
/** Overrides the default store removal; defaults to removing the entry. */
|
||||
onConfirm?: (repoWithTag: string) => void;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
let { action, onClose, onConfirm, open = true, repoWithTag }: Props = $props();
|
||||
|
||||
// Both actions resolve through the same store removal (cancelDownload drops a
|
||||
// running download's partial files or a cached model's files); only the copy
|
||||
// differs. One component so the discover chips and the selector rows word the
|
||||
// destructive confirmations identically.
|
||||
const COPY = {
|
||||
[ModelDownloadConfirmAction.CANCEL]: {
|
||||
cancelText: 'Keep downloading',
|
||||
confirmText: 'Cancel download',
|
||||
description: (name: string) =>
|
||||
`This stops the download of ${name} and removes the partial files. Pause it instead to keep the progress.`,
|
||||
title: 'Cancel download'
|
||||
},
|
||||
[ModelDownloadConfirmAction.DELETE]: {
|
||||
cancelText: 'Keep model',
|
||||
confirmText: 'Delete',
|
||||
description: (name: string) =>
|
||||
`This permanently removes ${name} from disk. You can download it again later.`,
|
||||
title: 'Delete model'
|
||||
}
|
||||
} as const;
|
||||
|
||||
let copy = $derived(COPY[action]);
|
||||
let displayName = $derived(modelsStore.toDisplayName(repoWithTag));
|
||||
|
||||
function confirm() {
|
||||
if (onConfirm) onConfirm(repoWithTag);
|
||||
else void modelsStore.status.cancelDownload(repoWithTag);
|
||||
|
||||
onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
<DialogConfirmation
|
||||
cancelText={copy.cancelText}
|
||||
confirmText={copy.confirmText}
|
||||
description={copy.description(displayName)}
|
||||
onCancel={onClose}
|
||||
onConfirm={confirm}
|
||||
{open}
|
||||
title={copy.title}
|
||||
variant="destructive"
|
||||
/>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { ModelsDiscover } from '$lib/components/app/models/discover';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
let { onOpenChange, open = $bindable(false) }: Props = $props();
|
||||
|
||||
function handleOpenChange(value: boolean) {
|
||||
open = value;
|
||||
onOpenChange?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root onOpenChange={handleOpenChange} {open}>
|
||||
<Dialog.Content
|
||||
class="grid gap-0 p-0 md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-380!"
|
||||
style="grid-template-columns: auto 1fr;"
|
||||
>
|
||||
<ModelsDiscover />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -108,6 +108,17 @@ export { default as DialogExportSettings } from './DialogExportSettings.svelte';
|
||||
*/
|
||||
export { default as DialogConfirmation } from './DialogConfirmation.svelte';
|
||||
|
||||
/**
|
||||
* **DialogConfirmDownload** - Confirm a destructive download action
|
||||
*
|
||||
* Shared confirmation for stopping/cancelling an in-flight download or deleting
|
||||
* a downloaded model, used by the discover quant chips and the model selector's
|
||||
* download rows so both word the action identically. Owns the copy and the
|
||||
* default store removal; render one instance per surface keyed by the acted-on
|
||||
* repo:tag.
|
||||
*/
|
||||
export { default as DialogConfirmDownload } from './DialogConfirmDownload.svelte';
|
||||
|
||||
/**
|
||||
* **DialogConversationRename** - Rename a conversation
|
||||
*
|
||||
@@ -526,3 +537,13 @@ export { default as DialogMcpResourcePreview } from './DialogMcpResourcePreview.
|
||||
* ```
|
||||
*/
|
||||
export { default as DialogMermaidPreview } from './DialogMermaidPreview.svelte';
|
||||
|
||||
/**
|
||||
* **DialogModelsDiscover** - full-screen model discovery dialog.
|
||||
*
|
||||
* Two-pane layout: searchable model list (Hugging Face + llama.app catalog)
|
||||
* on the left, model details with download options on the right.
|
||||
*
|
||||
* @see ModelsDiscover in $lib/components/app/models/discover
|
||||
*/
|
||||
export { default as DialogModelsDiscover } from './DialogModelsDiscover.svelte';
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { Image, Lightbulb, Mic, Video, Wrench } from '@lucide/svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import type { ModelModalities } from '$lib/types/models';
|
||||
|
||||
interface Props {
|
||||
modalities?: ModelModalities;
|
||||
supportsThinking?: boolean;
|
||||
supportsToolUse?: boolean;
|
||||
hideCapabilities?: boolean;
|
||||
hideModalities?: boolean;
|
||||
iconSize?: string;
|
||||
gapClass?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
gapClass = 'gap-1.25',
|
||||
hideCapabilities = false,
|
||||
hideModalities = false,
|
||||
iconSize = 'h-3 w-3',
|
||||
modalities,
|
||||
supportsThinking = false,
|
||||
supportsToolUse = false
|
||||
}: Props = $props();
|
||||
|
||||
let hasModalityIcons = $derived(modalities?.vision || modalities?.video || modalities?.audio);
|
||||
</script>
|
||||
|
||||
<span class="inline-flex items-center {gapClass}">
|
||||
{#if supportsToolUse && !hideCapabilities}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Wrench class="{iconSize} text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Tool use</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
|
||||
{#if supportsThinking && !hideCapabilities}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Lightbulb class="{iconSize} text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Reasoning</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
|
||||
{#if hasModalityIcons && !hideModalities}
|
||||
<span class="inline-flex items-center text-muted-foreground">
|
||||
{#if modalities?.vision}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Image class={iconSize} />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Vision</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
|
||||
{#if modalities?.video}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Video class={iconSize} />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Video</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
|
||||
{#if modalities?.audio}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Mic class={iconSize} />
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Audio</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -1,45 +1,66 @@
|
||||
<script lang="ts">
|
||||
import ModelCapabilityIcons from './ModelCapabilityIcons.svelte';
|
||||
import { Database, ScrollText } from '@lucide/svelte';
|
||||
import { TruncatedText } from '$lib/components/app';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import {
|
||||
CAPABILITY_FLAG_KEYS,
|
||||
CAPABILITY_ICONS,
|
||||
CAPABILITY_LABELS,
|
||||
MODALITY_FLAG_KEYS,
|
||||
MODALITY_ICONS,
|
||||
MODALITY_LABELS
|
||||
} from '$lib/constants';
|
||||
import { ModelCapability, ModelModality } from '$lib/enums';
|
||||
import { type ModelSidecar } from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { ModelsService } from '$lib/services/models.service';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import type { ModelCapabilities, ModelModalities } from '$lib/types/models';
|
||||
import type { ModelModalities } from '$lib/types/models';
|
||||
import { isAuxSidecar } from '$lib/utils';
|
||||
import { formatParameters } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
modelId: string;
|
||||
hideOrgName?: boolean;
|
||||
hideName?: boolean;
|
||||
hideModalities?: boolean;
|
||||
hideCapabilities?: boolean;
|
||||
hideParameters?: boolean;
|
||||
showRaw?: boolean;
|
||||
showRawTooltip?: boolean;
|
||||
hideQuantization?: boolean;
|
||||
hideTags?: boolean;
|
||||
aliases?: string[];
|
||||
tags?: string[];
|
||||
/** Render the capability/modality/context icons on a second row. */
|
||||
iconsOnNewLine?: boolean;
|
||||
modalities?: ModelModalities;
|
||||
capabilities?: ModelCapabilities;
|
||||
supportsThinking?: boolean;
|
||||
supportsToolUse?: boolean;
|
||||
/** Context length in tokens; renders a context icon when set. */
|
||||
contextLength?: number;
|
||||
/** Min/max GGUF file size (main + draft) across quants; renders a range when set. */
|
||||
sizeRange?: { min: number; max: number } | null;
|
||||
draftSidecars?: ModelSidecar[];
|
||||
/** Allow badges to wrap onto new lines instead of truncating. */
|
||||
wrap?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
aliases,
|
||||
capabilities,
|
||||
class: className = '',
|
||||
contextLength,
|
||||
draftSidecars = [],
|
||||
hideCapabilities = false,
|
||||
hideModalities = false,
|
||||
hideName = false,
|
||||
hideOrgName = false,
|
||||
hideParameters = false,
|
||||
hideQuantization,
|
||||
hideTags,
|
||||
iconsOnNewLine = false,
|
||||
modalities,
|
||||
modelId,
|
||||
showRaw = undefined,
|
||||
showRawTooltip = false,
|
||||
sizeRange,
|
||||
supportsThinking = false,
|
||||
supportsToolUse = false,
|
||||
tags,
|
||||
wrap = false,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
@@ -47,6 +68,11 @@
|
||||
'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md border border-border/50 px-1 py-0 text-[10px] font-mono bg-foreground/15 dark:bg-foreground/10 text-foreground [a&]:hover:bg-foreground/25';
|
||||
const tagBadgeClass =
|
||||
'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md border border-border/50 px-1 py-0 text-[10px] font-mono text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground';
|
||||
const variantBadgeClass =
|
||||
'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md bg-primary px-1.5 py-0 text-[10px] font-mono font-semibold uppercase tracking-wide text-primary-foreground';
|
||||
|
||||
/** Alias badges beyond this many collapse into a single `+x more` badge. */
|
||||
const MAX_ALIAS_BADGES = 2;
|
||||
|
||||
let parsed = $derived(ModelsService.parseModelId(modelId));
|
||||
let resolvedShowRaw = $derived(
|
||||
@@ -59,104 +85,147 @@
|
||||
|
||||
let uniqueAliases = $derived([...new Set(aliases ?? [])]);
|
||||
let uniqueTags = $derived([...new Set([...(parsed.tags ?? []), ...(tags ?? [])])]);
|
||||
|
||||
const allModalities = [ModelModality.VISION, ModelModality.VIDEO, ModelModality.AUDIO] as const;
|
||||
const allCapabilities: ModelCapability[] = [ModelCapability.REASONING];
|
||||
|
||||
let activeModalities = $derived(
|
||||
allModalities.filter((modality) => modalities?.[MODALITY_FLAG_KEYS[modality]])
|
||||
);
|
||||
let activeCapabilities = $derived(
|
||||
allCapabilities.filter((capability) => capabilities?.[CAPABILITY_FLAG_KEYS[capability]])
|
||||
);
|
||||
let uniqueDraftSidecars = $derived([...new Set(draftSidecars)].filter((s) => !isAuxSidecar(s)));
|
||||
|
||||
let primaryAlias = $derived(uniqueAliases.length === 1 ? uniqueAliases[0] : null);
|
||||
let displayName = $derived(primaryAlias ?? parsed.modelName ?? modelId);
|
||||
|
||||
let hasBadges = $derived(
|
||||
parsed.sidecar ||
|
||||
(parsed.params && !hideParameters) ||
|
||||
(parsed.quantization && !resolvedHideQuantization) ||
|
||||
primaryAlias ||
|
||||
uniqueAliases.length > 1 ||
|
||||
(uniqueTags.length > 0 && !resolvedHideTags)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if resolvedShowRaw}
|
||||
<TruncatedText class="font-medium {className}" showTooltip={false} text={modelId} {...rest} />
|
||||
{:else}
|
||||
{#snippet nameAndBadges()}
|
||||
<span class="min-w-0 truncate font-medium">
|
||||
{#if !hideOrgName && parsed.orgName}{parsed.orgName}/{/if}{displayName}
|
||||
</span>
|
||||
|
||||
<span class="inline-flex items-center gap-1">
|
||||
{#if parsed.params}
|
||||
<span class={badgeClass}>
|
||||
{parsed.params}{parsed.activatedParams ? `-${parsed.activatedParams}` : ''}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if parsed.quantization && !resolvedHideQuantization}
|
||||
<span class={badgeClass}>
|
||||
{parsed.quantization}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if primaryAlias}
|
||||
{#if primaryAlias !== parsed.modelName}
|
||||
<span class={badgeClass}>{parsed.modelName ?? modelId}</span>
|
||||
{/if}
|
||||
{:else if uniqueAliases.length > 1}
|
||||
{#each uniqueAliases as alias (alias)}
|
||||
<span class={badgeClass}>{alias}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if uniqueTags.length > 0 && !resolvedHideTags}
|
||||
{#each uniqueTags as tag (tag)}
|
||||
<span class={tagBadgeClass}>{tag}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
<span class="flex min-w-0 items-center gap-1.5 {className}" {...rest}>
|
||||
{#if showRawTooltip}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="flex min-w-0 items-center gap-1.5">
|
||||
{@render nameAndBadges()}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{modelId}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
{@render nameAndBadges()}
|
||||
{#if !hideName}
|
||||
<span class="min-w-0 truncate font-medium">
|
||||
{#if !hideOrgName && parsed.orgName}{parsed.orgName}/{/if}{displayName}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if activeCapabilities.length > 0 || activeModalities.length > 0}
|
||||
<span class="inline-flex items-center gap-1.25 text-muted-foreground">
|
||||
{#each activeCapabilities as capability (capability)}
|
||||
{@const CapabilityIcon = CAPABILITY_ICONS[capability]}
|
||||
{#if hasBadges}
|
||||
<span class="inline-flex items-center gap-1 {wrap ? 'flex-wrap' : ''}">
|
||||
{#if parsed.sidecar}
|
||||
<span class={variantBadgeClass} title={`${parsed.sidecar.toUpperCase()} draft model`}>
|
||||
{parsed.sidecar}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<CapabilityIcon class="h-3 w-3 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
{#if parsed.params && !hideParameters}
|
||||
<span class={badgeClass}>
|
||||
{parsed.params}{parsed.activatedParams ? `-${parsed.activatedParams}` : ''}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{CAPABILITY_LABELS[capability]}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{#each uniqueDraftSidecars as sidecar (sidecar)}
|
||||
<span class={variantBadgeClass} title={`${sidecar.toUpperCase()} draft model available`}>
|
||||
{sidecar}
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
{#each activeModalities as modality (modality)}
|
||||
{@const ModalityIcon = MODALITY_ICONS[modality]}
|
||||
{#if parsed.quantization && !resolvedHideQuantization}
|
||||
<span class={badgeClass}>
|
||||
{parsed.quantization}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<ModalityIcon class="h-3 w-3 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
{#if primaryAlias}
|
||||
{#if primaryAlias !== parsed.modelName}
|
||||
<span class="{badgeClass} max-w-32 truncate" title={parsed.modelName ?? modelId}>
|
||||
{parsed.modelName ?? modelId}
|
||||
</span>
|
||||
{/if}
|
||||
{:else if uniqueAliases.length > 1}
|
||||
{#each uniqueAliases.slice(0, MAX_ALIAS_BADGES) as alias (alias)}
|
||||
<span class="{badgeClass} max-w-32 truncate" title={alias}>
|
||||
{alias}
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{MODALITY_LABELS[modality]}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/each}
|
||||
{#if uniqueAliases.length > MAX_ALIAS_BADGES}
|
||||
<span class={badgeClass} title={uniqueAliases.slice(MAX_ALIAS_BADGES).join(', ')}>
|
||||
+{uniqueAliases.length - MAX_ALIAS_BADGES} more
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if uniqueTags.length > 0 && !resolvedHideTags}
|
||||
{#each uniqueTags as tag (tag)}
|
||||
<span class={tagBadgeClass}>{tag}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<span
|
||||
class="flex min-w-0 items-center gap-1.5 {wrap ? 'flex-wrap' : ''} {iconsOnNewLine
|
||||
? 'flex-col items-start'
|
||||
: ''} {className}"
|
||||
{...rest}
|
||||
>
|
||||
<span class="flex min-w-0 items-center gap-1.5 {wrap ? 'flex-wrap' : ''}">
|
||||
{#if showRawTooltip}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="flex min-w-0 items-center gap-1.5">
|
||||
{@render nameAndBadges()}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{modelId}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
{@render nameAndBadges()}
|
||||
{/if}
|
||||
|
||||
{#if !iconsOnNewLine}
|
||||
<ModelCapabilityIcons
|
||||
{hideCapabilities}
|
||||
{hideModalities}
|
||||
{modalities}
|
||||
{supportsThinking}
|
||||
{supportsToolUse}
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if iconsOnNewLine || contextLength || sizeRange}
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
{#if iconsOnNewLine}
|
||||
<ModelCapabilityIcons
|
||||
{hideCapabilities}
|
||||
{hideModalities}
|
||||
{modalities}
|
||||
{supportsThinking}
|
||||
{supportsToolUse}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if contextLength}
|
||||
<span class="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<ScrollText class="h-3 w-3" />
|
||||
|
||||
<span class="text-xs">{formatParameters(contextLength)}</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if sizeRange}
|
||||
<span class="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<Database class="h-3 w-3" />
|
||||
|
||||
<span class="text-xs"
|
||||
>{HuggingFaceService.formatSizeRange(sizeRange.min, sizeRange.max)}</span
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
} from '@lucide/svelte';
|
||||
import { ActionIcon, ModelId } from '$lib/components/app';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
import { ModelCapability, ServerModelStatus } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import { modelLoadFraction, modelLoadProgressText } from '$lib/utils';
|
||||
@@ -60,7 +60,8 @@
|
||||
let loadTitle = $derived(modelLoadProgressText(loadProgress));
|
||||
let modalities = $derived(option.modalities);
|
||||
let capabilities = $derived.by(() => ({
|
||||
reasoning: modelsStore.props.checkModelSupportsThinking(option.model)
|
||||
reasoning: modelsStore.props.checkModelSupportsThinking(option.model),
|
||||
tools: option.capabilities.includes(ModelCapability.TOOL_USE)
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -85,12 +86,13 @@
|
||||
>
|
||||
<ModelId
|
||||
aliases={option.aliases}
|
||||
{capabilities}
|
||||
class="flex-1"
|
||||
{hideOrgName}
|
||||
{modalities}
|
||||
modelId={option.model}
|
||||
showRawTooltip
|
||||
supportsThinking={capabilities.reasoning}
|
||||
supportsToolUse={capabilities.tools}
|
||||
tags={option.tags}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
ModelsDiscoverDetails,
|
||||
ModelsDiscoverList,
|
||||
ModelsDiscoverListSearch
|
||||
} from '$lib/components/app/models/discover';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { modelsDiscoverStore } from '$lib/stores';
|
||||
import type { HfModelDetailInfo, HfModelSibling } from '$lib/types';
|
||||
|
||||
let selectedId = $state<string | null>(null);
|
||||
let searchQuery = $state('');
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Detail pane state, reloaded when the selection changes.
|
||||
let details = $state<HfModelDetailInfo | null>(null);
|
||||
let files = $state<HfModelSibling[]>([]);
|
||||
let readme = $state<string | null>(null);
|
||||
let detailLoading = $state(false);
|
||||
let detailError = $state<string | null>(null);
|
||||
|
||||
// Load the sidebar list on mount (the component is mounted when the dialog opens).
|
||||
$effect(() => {
|
||||
void modelsDiscoverStore.fetch();
|
||||
void modelsDiscoverStore.search('');
|
||||
});
|
||||
|
||||
// Auto-select the first model.
|
||||
$effect(() => {
|
||||
const first = modelsDiscoverStore.firstModel;
|
||||
|
||||
if (!selectedId && first) {
|
||||
selectedId = first.id;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSearchInput(value: string) {
|
||||
searchQuery = value;
|
||||
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
void modelsDiscoverStore.search(value);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Load the detail pane for the selected model (component is reused across
|
||||
// selections, so this re-fetches on every change).
|
||||
$effect(() => {
|
||||
const id = selectedId;
|
||||
|
||||
if (!id) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
detailLoading = true;
|
||||
detailError = null;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const [info, tree, readmeText] = await Promise.all([
|
||||
HuggingFaceService.getDetails(id),
|
||||
HuggingFaceService.getTree(id),
|
||||
HuggingFaceService.getReadme(id)
|
||||
]);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (!info) {
|
||||
detailError = 'Model not found';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
details = info;
|
||||
files = HuggingFaceService.filterByExtension(
|
||||
HuggingFaceService.collapseGgufShards(tree),
|
||||
'.gguf'
|
||||
);
|
||||
readme = readmeText;
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
|
||||
detailError = err instanceof Error ? err.message : 'Failed to load model';
|
||||
} finally {
|
||||
if (!cancelled) detailLoading = false;
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="w-md shrink-0 self-start border-r border-border/40 bg-background overflow-y-auto md:p-4 h-full space-y-1"
|
||||
>
|
||||
<ModelsDiscoverListSearch bind:value={searchQuery} onSearch={handleSearchInput} />
|
||||
|
||||
<!-- One list instance, so the rows keep their state across search round trips;
|
||||
skeleton rows replace them while the initial catalog or a query loads. -->
|
||||
<div>
|
||||
{#if modelsDiscoverStore.error}
|
||||
<div class="flex flex-col items-start gap-2 p-4">
|
||||
<p class="text-sm text-destructive">{modelsDiscoverStore.error}</p>
|
||||
|
||||
<Button onclick={() => void modelsDiscoverStore.fetch()} size="sm" variant="outline">
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
{:else if !modelsDiscoverStore.loading && !modelsDiscoverStore.searching && modelsDiscoverStore.models.length === 0}
|
||||
<p class="p-4 text-sm text-muted-foreground">No models found</p>
|
||||
{:else}
|
||||
<ModelsDiscoverList
|
||||
activeId={selectedId}
|
||||
loading={modelsDiscoverStore.loading || modelsDiscoverStore.searching}
|
||||
models={modelsDiscoverStore.models}
|
||||
onSelect={(id) => (selectedId = id)}
|
||||
showBaseModelAvatar
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="overflow-y-auto">
|
||||
{#if selectedId}
|
||||
<ModelsDiscoverDetails
|
||||
{details}
|
||||
error={detailError}
|
||||
{files}
|
||||
loading={detailLoading}
|
||||
modelId={selectedId}
|
||||
{readme}
|
||||
/>
|
||||
{/if}
|
||||
</main>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { DARK_INVERT_AVATAR_ORGS } from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
org: string;
|
||||
quantOrg?: string;
|
||||
size?: string;
|
||||
baseImageClass?: string;
|
||||
quantImageClass?: string;
|
||||
quantPositionClass?: string;
|
||||
quantSize?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
baseImageClass = '',
|
||||
class: className = '',
|
||||
org,
|
||||
quantImageClass = 'h-full w-full',
|
||||
quantOrg,
|
||||
quantPositionClass = '-bottom-0.75 -right-0.75',
|
||||
quantSize = 'h-4.25 w-4.25',
|
||||
size = 'h-9 w-9'
|
||||
}: Props = $props();
|
||||
|
||||
let avatarError = $state(false);
|
||||
let quantError = $state(false);
|
||||
|
||||
let invertAvatar = $derived(DARK_INVERT_AVATAR_ORGS.includes(org));
|
||||
let invertQuant = $derived(DARK_INVERT_AVATAR_ORGS.includes(quantOrg ?? ''));
|
||||
|
||||
// Monogram fallback: org initial on a hue derived from its name, so each org
|
||||
// gets a stable distinct color.
|
||||
let hue = $derived.by(() => {
|
||||
let h = 0;
|
||||
|
||||
for (let i = 0; i < org.length; i++) h = (h * 31 + org.charCodeAt(i)) >>> 0;
|
||||
|
||||
return h % 360;
|
||||
});
|
||||
|
||||
let quantHue = $derived.by(() => {
|
||||
const name = quantOrg ?? '';
|
||||
|
||||
let h = 0;
|
||||
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
|
||||
|
||||
return h % 360;
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="relative mt-0.5 inline-flex shrink-0 {className}">
|
||||
{#if avatarError}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="flex {size} items-center justify-center rounded-md text-sm font-semibold text-white"
|
||||
style="background-color: hsl({hue} 60% 45%)"
|
||||
>
|
||||
{org.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{:else}
|
||||
<div class="rounded-md">
|
||||
<img
|
||||
alt=""
|
||||
class="{size} rounded-md {invertAvatar ? 'dark:invert' : ''} {baseImageClass}"
|
||||
loading="lazy"
|
||||
onerror={() => (avatarError = true)}
|
||||
src={HuggingFaceService.getAvatarUrl(org)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if quantOrg && quantOrg !== org}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
class="absolute {quantPositionClass} {quantSize} overflow-hidden rounded-full border border-background bg-muted "
|
||||
>
|
||||
{#if quantError}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="flex h-full w-full items-center justify-center rounded-full text-[8px] font-semibold text-white"
|
||||
style="background-color: hsl({quantHue} 60% 45%)"
|
||||
>
|
||||
{quantOrg.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{:else}
|
||||
<img
|
||||
alt=""
|
||||
class="{quantImageClass} rounded-full {invertQuant ? 'dark:invert' : ''}"
|
||||
loading="lazy"
|
||||
onerror={() => (quantError = true)}
|
||||
src={HuggingFaceService.getAvatarUrl(quantOrg)}
|
||||
/>
|
||||
{/if}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{quantOrg}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</span>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { Check, Copy, X } from '@lucide/svelte';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
chatTemplate: string;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
let { chatTemplate, onOpenChange, open = $bindable(false) }: Props = $props();
|
||||
|
||||
function handleOpenChange(value: boolean) {
|
||||
open = value;
|
||||
onOpenChange?.(value);
|
||||
}
|
||||
|
||||
let copied = $state(false);
|
||||
|
||||
async function copy() {
|
||||
await copyToClipboard(chatTemplate);
|
||||
copied = true;
|
||||
setTimeout(() => (copied = false), 1500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root onOpenChange={handleOpenChange} {open}>
|
||||
<Dialog.Content
|
||||
class="flex max-h-[calc(100vh-4rem)] flex-col gap-0 p-0 md:w-[calc(100vw-4rem)]! md:max-w-4xl!"
|
||||
>
|
||||
<!-- The header's corner-pinned close X never lines up with a padded flex row,
|
||||
so it is replaced by one inside the row, aligned with the title -->
|
||||
<Dialog.Header
|
||||
class="flex-row items-center gap-2 border-b border-border/40 p-4"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<Dialog.Title class="text-sm font-semibold">Chat template</Dialog.Title>
|
||||
|
||||
<button
|
||||
aria-label="Copy chat template"
|
||||
class="inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium transition-colors hover:bg-muted"
|
||||
onclick={() => void copy()}
|
||||
type="button"
|
||||
>
|
||||
{#if copied}
|
||||
<Check class="h-3.5 w-3.5 text-green-500" />
|
||||
{:else}
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
|
||||
Copy
|
||||
</button>
|
||||
|
||||
<Dialog.Close
|
||||
aria-label="Close"
|
||||
class="ml-auto inline-flex cursor-pointer items-center justify-center rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted-foreground/10 hover:text-foreground"
|
||||
type="button"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</Dialog.Close>
|
||||
</Dialog.Header>
|
||||
|
||||
<pre
|
||||
class="flex-1 overflow-auto p-4 font-mono text-xs break-all whitespace-pre-wrap text-muted-foreground">{chatTemplate}</pre>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { ModelsDiscoverDetailsDownloadOptions } from './ModelsDiscoverDetailsDownloadOptions';
|
||||
import ModelsDiscoverDetailsHeader from './ModelsDiscoverDetailsHeader.svelte';
|
||||
import ModelsDiscoverDetailsReadme from './ModelsDiscoverDetailsReadme.svelte';
|
||||
import ModelsDiscoverDetailsSkeleton from './ModelsDiscoverDetailsSkeleton.svelte';
|
||||
import { OTHER_BIT_DEPTH } from '$lib/constants';
|
||||
import { ModelAuxSidecar } from '$lib/enums';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { HfModelDetailInfo, HfModelSibling, ModelBitDepthRow } from '$lib/types';
|
||||
import { detectThinkingSupport, detectToolUseSupport } from '$lib/utils';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
/** Full HuggingFace model id, e.g. `ggml-org/gemma-3-4b-it-GGUF`. */
|
||||
modelId: string;
|
||||
/** Model details from `/api/models/{id}?full=true`; null while loading. */
|
||||
details: HfModelDetailInfo | null;
|
||||
/** GGUF files of the repo, shards collapsed, sorted by size desc. */
|
||||
files: HfModelSibling[];
|
||||
/** README.md content, frontmatter stripped; null when unavailable. */
|
||||
readme: string | null;
|
||||
/** True while the model data is being fetched. */
|
||||
loading?: boolean;
|
||||
/** Error message when loading failed. */
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
let { details, error = null, files, loading = false, modelId, readme }: Props = $props();
|
||||
|
||||
let gguf = $derived(details?.gguf);
|
||||
let baseModels = $derived(HuggingFaceService.getBaseModels(details));
|
||||
let licenseTag = $derived.by(() => {
|
||||
const tags = details?.tags ?? [];
|
||||
|
||||
return tags.find((t) => t.startsWith('license:'))?.replace('license:', '') ?? null;
|
||||
});
|
||||
|
||||
// Capabilities derived from HF metadata. Vision comes from an mmproj sidecar
|
||||
// or a multimodal pipeline tag; tool use / reasoning from the chat template.
|
||||
let hasMmproj = $derived(
|
||||
files.some(
|
||||
(f) => HuggingFaceService.extractQuantMeta(f.path)?.sidecar === ModelAuxSidecar.MMPROJ
|
||||
)
|
||||
);
|
||||
let hasVision = $derived(hasMmproj || details?.pipeline_tag === 'image-text-to-text');
|
||||
let hasTools = $derived(detectToolUseSupport(gguf?.chat_template ?? ''));
|
||||
let hasReasoning = $derived(detectThinkingSupport(gguf?.chat_template ?? ''));
|
||||
|
||||
let bitDepthRows = $derived.by<ModelBitDepthRow[]>(() => {
|
||||
const rows = new SvelteMap<number, HfModelSibling[]>();
|
||||
|
||||
for (const file of files) {
|
||||
const meta = HuggingFaceService.extractQuantMeta(file.path);
|
||||
|
||||
// mmproj sidecars are already conveyed by the Vision capability badge;
|
||||
// imatrix ships as a normal chip with its own badge.
|
||||
if (meta?.sidecar === ModelAuxSidecar.MMPROJ) continue;
|
||||
|
||||
const depth = meta?.quant ? HuggingFaceService.getBitDepth(meta.quant) : null;
|
||||
const bucket = depth ?? OTHER_BIT_DEPTH;
|
||||
const list = rows.get(bucket) ?? [];
|
||||
|
||||
list.push(file);
|
||||
rows.set(bucket, list);
|
||||
}
|
||||
|
||||
return Array.from(rows.entries())
|
||||
.map(([bitDepth, rowFiles]) => ({ bitDepth, files: rowFiles }))
|
||||
.sort((a, b) => a.bitDepth - b.bitDepth);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<ModelsDiscoverDetailsSkeleton />
|
||||
{:else if error}
|
||||
<div class="flex h-full items-center justify-center py-20">
|
||||
<p class="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
{:else if details}
|
||||
<div class="space-y-6 p-6">
|
||||
<ModelsDiscoverDetailsHeader
|
||||
{baseModels}
|
||||
{details}
|
||||
{gguf}
|
||||
{hasReasoning}
|
||||
{hasTools}
|
||||
{hasVision}
|
||||
{licenseTag}
|
||||
{modelId}
|
||||
/>
|
||||
|
||||
<ModelsDiscoverDetailsDownloadOptions {bitDepthRows} {modelId} />
|
||||
|
||||
<ModelsDiscoverDetailsReadme {readme} />
|
||||
</div>
|
||||
{/if}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import { classify, labelFor } from './download-options.utils';
|
||||
import ModelsDiscoverDetailsDownloadOptionsDownloadCommand from './ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte';
|
||||
import ModelsDiscoverDetailsDownloadOptionsRow from './ModelsDiscoverDetailsDownloadOptionsRow.svelte';
|
||||
import { DialogConfirmDownload } from '$lib/components/app/dialogs';
|
||||
import { ModelDownloadConfirmAction, ModelSelectableFileKind } from '$lib/enums';
|
||||
import { HuggingFaceService, ModelsService } from '$lib/services';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type {
|
||||
ModelBitDepthRow,
|
||||
ModelDownloadEntryState,
|
||||
ModelQuantOption,
|
||||
ModelSelectableFile
|
||||
} from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
/** Full HuggingFace repo id, e.g. `ggml-org/gemma-3-4b-it-GGUF`. */
|
||||
modelId: string;
|
||||
/** GGUF files grouped by bit depth. */
|
||||
bitDepthRows: ModelBitDepthRow[];
|
||||
/** Download state lookup; defaults to the models store status feed. */
|
||||
getDownloadState?: (
|
||||
repoWithTag: string,
|
||||
filePath: string,
|
||||
isSidecar: boolean
|
||||
) => ModelDownloadEntryState;
|
||||
}
|
||||
|
||||
let { bitDepthRows, getDownloadState, modelId }: Props = $props();
|
||||
|
||||
// Destructive chip actions (delete a downloaded model, cancel a download) are
|
||||
// confirmed here rather than inside each chip: a single shared dialog owned by
|
||||
// the options panel, keyed by the repo+tag the user acted on, so one dialog is
|
||||
// mounted for the whole panel instead of one per chip.
|
||||
// The acted-on target is kept after closing so the copy stays rendered through
|
||||
// the dialog's close transition.
|
||||
let pending: { action: ModelDownloadConfirmAction; repoWithTag: string } = $state({
|
||||
action: ModelDownloadConfirmAction.CANCEL,
|
||||
repoWithTag: ''
|
||||
});
|
||||
let confirmOpen = $state(false);
|
||||
|
||||
function requestCancel(repoWithTag: string) {
|
||||
pending = { action: ModelDownloadConfirmAction.CANCEL, repoWithTag };
|
||||
confirmOpen = true;
|
||||
}
|
||||
|
||||
function requestDelete(repoWithTag: string) {
|
||||
pending = { action: ModelDownloadConfirmAction.DELETE, repoWithTag };
|
||||
confirmOpen = true;
|
||||
}
|
||||
|
||||
function stateFor(
|
||||
repoWithTag: string,
|
||||
filePath: string,
|
||||
isSidecar: boolean
|
||||
): ModelDownloadEntryState {
|
||||
if (getDownloadState) return getDownloadState(repoWithTag, filePath, isSidecar);
|
||||
|
||||
const isDownloading = modelsStore.status.isDownloadInProgress(repoWithTag);
|
||||
const isPaused = modelsStore.status.isDownloadPaused(repoWithTag);
|
||||
|
||||
return {
|
||||
// solo downloads register in /v1/models under the tag (args stay empty),
|
||||
// while drafts pulled by a loaded model only show up as its --model-draft
|
||||
isDownloaded:
|
||||
!isDownloading &&
|
||||
(modelsStore.status.isModelDownloaded(repoWithTag) ||
|
||||
(isSidecar && modelsStore.status.isSidecarDownloaded(modelId, filePath))),
|
||||
isDownloading,
|
||||
isFailed: modelsStore.status.hasFailedDownload(repoWithTag),
|
||||
isPaused,
|
||||
// live progress while downloading, else the frozen snapshot of the pause
|
||||
progress:
|
||||
modelsStore.status.getDownloadProgress(repoWithTag) ??
|
||||
modelsStore.status.getPausedDownloadProgress(repoWithTag),
|
||||
repoWithTag
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Every selectable file with its kind and download state, in row order.
|
||||
* Single source of truth for the chip rows and the command selects.
|
||||
*/
|
||||
let selectableFiles = $derived.by(() => {
|
||||
const files: (ModelSelectableFile & { state: ModelDownloadEntryState })[] = [];
|
||||
|
||||
for (const row of bitDepthRows) {
|
||||
for (const file of row.files) {
|
||||
const meta = HuggingFaceService.extractQuantMeta(file.path);
|
||||
const tag = ModelsService.buildDownloadTag(
|
||||
modelId,
|
||||
meta?.quant ?? null,
|
||||
meta?.sidecar ?? null
|
||||
);
|
||||
|
||||
files.push({
|
||||
...file,
|
||||
kind: classify(file.path),
|
||||
state: stateFor(tag, file.path, Boolean(meta?.sidecar))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
});
|
||||
|
||||
/** Rows for the row component: per-bit-depth files with state attached. */
|
||||
let rows = $derived.by(() =>
|
||||
bitDepthRows.map((row) => {
|
||||
const paths = new Set(row.files.map((f) => f.path));
|
||||
|
||||
return {
|
||||
bitDepth: row.bitDepth,
|
||||
files: selectableFiles.filter((f) => paths.has(f.path))
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
function optionFor(file: ModelSelectableFile): ModelQuantOption {
|
||||
return {
|
||||
label: labelFor(file.path),
|
||||
path: file.path
|
||||
};
|
||||
}
|
||||
|
||||
/** Non-draft quants for the command's base select, in row order. */
|
||||
let mainOptions = $derived(
|
||||
selectableFiles.filter((f) => f.kind === ModelSelectableFileKind.MAIN).map(optionFor)
|
||||
);
|
||||
|
||||
/**
|
||||
* Draft options for the command's draft select, with their sidecar type
|
||||
* (MTP, DFLASH...) since a repo can ship more than one draft flavour.
|
||||
*/
|
||||
let draftOptions = $derived(
|
||||
selectableFiles
|
||||
.filter((f) => f.kind === ModelSelectableFileKind.DRAFT)
|
||||
.map((f) => ({
|
||||
...optionFor(f),
|
||||
badge: HuggingFaceService.extractQuantMeta(f.path)?.sidecar ?? null
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if bitDepthRows.length}
|
||||
<section class="rounded-3xl border border-border/30 bg-muted/60 shadow-xs dark:border-border/20">
|
||||
<!-- One chip per file, each an independent download action with its own
|
||||
lifecycle state; nothing here selects anything. -->
|
||||
<div class="flex w-full flex-col divide-y divide-border/50 px-4 pb-1 dark:divide-border/35">
|
||||
{#each rows as row (row.bitDepth)}
|
||||
<ModelsDiscoverDetailsDownloadOptionsRow
|
||||
bitDepth={row.bitDepth}
|
||||
files={row.files}
|
||||
onRequestCancel={requestCancel}
|
||||
onRequestDelete={requestDelete}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Terminal command preview, standalone: its picks are not bound to the chips. -->
|
||||
<div class="border-t border-border/50 px-4 pt-3.5 pb-4 dark:border-border/35">
|
||||
<ModelsDiscoverDetailsDownloadOptionsDownloadCommand {draftOptions} {mainOptions} {modelId} />
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<DialogConfirmDownload
|
||||
action={pending.action}
|
||||
onClose={() => (confirmOpen = false)}
|
||||
open={confirmOpen}
|
||||
repoWithTag={pending.repoWithTag}
|
||||
/>
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
<script lang="ts">
|
||||
import { Check, Copy, Plus, X } from '@lucide/svelte';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import {
|
||||
DEFAULT_BASE_BIT_DEPTH,
|
||||
MODEL_ID,
|
||||
type ModelSidecar,
|
||||
OTHER_BIT_DEPTH,
|
||||
SERVE_COMMAND,
|
||||
SPEC_TYPE
|
||||
} from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { ModelQuantOption } from '$lib/types';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
modelId: string;
|
||||
/** Non-draft quants of the repo, in bit-depth row order. */
|
||||
mainOptions: ModelQuantOption[];
|
||||
/** Draft sidecar files with their sidecar badge; empty when the repo ships none. */
|
||||
draftOptions: (ModelQuantOption & { badge: ModelSidecar | null })[];
|
||||
}
|
||||
|
||||
let { draftOptions, mainOptions, modelId }: Props = $props();
|
||||
|
||||
// Command picks, owned here: nothing two-way binds them to the quant chips.
|
||||
let basePick = $state<string | null>(null);
|
||||
let draftPick = $state<string | null>(null);
|
||||
let draftTypePick = $state<ModelSidecar | null>(null);
|
||||
let withDraft = $state(false);
|
||||
|
||||
function bitDepthOf(path: string): number {
|
||||
const quant = HuggingFaceService.extractQuantMeta(path)?.quant;
|
||||
|
||||
return quant ? (HuggingFaceService.getBitDepth(quant) ?? OTHER_BIT_DEPTH) : OTHER_BIT_DEPTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base file the command points at: the user's pick while it still exists in
|
||||
* the options, else the 4-bit file, else the lowest bit depth available. A
|
||||
* stale pick (the details pane switched models) falls back on its own.
|
||||
*/
|
||||
let baseOption = $derived.by(() => {
|
||||
const picked = mainOptions.find((option) => option.path === basePick);
|
||||
|
||||
if (picked) return picked;
|
||||
|
||||
const preferred = mainOptions.find(
|
||||
(option) => bitDepthOf(option.path) === DEFAULT_BASE_BIT_DEPTH
|
||||
);
|
||||
|
||||
if (preferred) return preferred;
|
||||
|
||||
const ranked = [...mainOptions].sort((a, b) => bitDepthOf(a.path) - bitDepthOf(b.path));
|
||||
|
||||
return ranked[0] ?? null;
|
||||
});
|
||||
|
||||
/** Draft sidecar types the repo ships, in option order. */
|
||||
let specTypes = $derived(
|
||||
draftOptions
|
||||
.map((option) => option.badge)
|
||||
.filter((badge): badge is ModelSidecar => badge !== null)
|
||||
.filter((badge, index, all) => all.indexOf(badge) === index)
|
||||
);
|
||||
|
||||
/**
|
||||
* Draft type the --spec-type select points at: the user's pick while it
|
||||
* still exists, else the first type the repo ships.
|
||||
*/
|
||||
let draftType = $derived(
|
||||
draftTypePick && specTypes.includes(draftTypePick) ? draftTypePick : (specTypes[0] ?? null)
|
||||
);
|
||||
|
||||
/** Draft files of the picked type; the quant select only offers these. */
|
||||
let typeDraftOptions = $derived(draftOptions.filter((option) => option.badge === draftType));
|
||||
|
||||
/** Draft file the -hfd tag points at: the user's pick, else the first of the type. */
|
||||
let draftOption = $derived(
|
||||
withDraft
|
||||
? (typeDraftOptions.find((option) => option.path === draftPick) ??
|
||||
typeDraftOptions[0] ??
|
||||
null)
|
||||
: null
|
||||
);
|
||||
|
||||
/** Quant of the file the `-hf` tag points at; null when the file carries no quant. */
|
||||
let mainQuant = $derived(
|
||||
baseOption ? (HuggingFaceService.extractQuantMeta(baseOption.path)?.quant ?? null) : null
|
||||
);
|
||||
|
||||
/** Quant of the file the `-hfd` tag points at. */
|
||||
let draftQuant = $derived(
|
||||
draftOption ? (HuggingFaceService.extractQuantMeta(draftOption.path)?.quant ?? null) : null
|
||||
);
|
||||
|
||||
/** `--spec-type` value; null when no draft type resolved. */
|
||||
let specType = $derived(draftType ? SPEC_TYPE[draftType] : null);
|
||||
|
||||
/** The llama serve command, composed from the inline picks. */
|
||||
let command = $derived.by(() => {
|
||||
const parts = [
|
||||
SERVE_COMMAND.BIN,
|
||||
SERVE_COMMAND.SUBCOMMAND,
|
||||
SERVE_COMMAND.MODEL_FLAG,
|
||||
mainQuant ? `${modelId}${MODEL_ID.QUANTIZATION_SEPARATOR}${mainQuant}` : modelId
|
||||
];
|
||||
|
||||
if (draftOption && draftQuant) {
|
||||
parts.push(
|
||||
SERVE_COMMAND.DRAFT_FLAG,
|
||||
`${modelId}${MODEL_ID.QUANTIZATION_SEPARATOR}${draftQuant}`,
|
||||
SERVE_COMMAND.SPEC_TYPE_FLAG,
|
||||
specType ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
});
|
||||
|
||||
let copied = $state(false);
|
||||
|
||||
async function copy() {
|
||||
await copyToClipboard(command);
|
||||
copied = true;
|
||||
setTimeout(() => (copied = false), 1500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <div aria-hidden="true" class="flex items-center gap-3 mt-2 mb-4">
|
||||
<span class="h-px flex-1 bg-border/50"></span>
|
||||
|
||||
<span class="text-xs whitespace-nowrap text-muted-foreground"> or run in your terminal </span>
|
||||
|
||||
<span class="h-px flex-1 bg-border/50"></span>
|
||||
</div> -->
|
||||
|
||||
<div
|
||||
class="relative flex items-center gap-2 overflow-hidden rounded-lg border border-border/40 bg-background py-2.5 pl-4 pr-10 shadow-xs dark:border-border/35 dark:bg-background/50"
|
||||
>
|
||||
<!-- Single line: long commands scroll horizontally instead of wrapping. -->
|
||||
<div
|
||||
class="flex min-w-0 flex-1 items-center gap-x-2 overflow-x-auto py-0.5 font-mono text-xs whitespace-nowrap text-foreground/90"
|
||||
>
|
||||
<span class="shrink-0">{SERVE_COMMAND.BIN}</span>
|
||||
|
||||
<span class="shrink-0">{SERVE_COMMAND.SUBCOMMAND}</span>
|
||||
|
||||
<span class="shrink-0">{SERVE_COMMAND.MODEL_FLAG}</span>
|
||||
|
||||
<span class="shrink-0">
|
||||
{modelId}{mainQuant ? MODEL_ID.QUANTIZATION_SEPARATOR : ''}
|
||||
</span>
|
||||
|
||||
<!-- Base quant: always part of the command, the 4-bit file by default. -->
|
||||
{#if baseOption}
|
||||
<Select.Root onValueChange={(v) => v && (basePick = v)} type="single" value={baseOption.path}>
|
||||
<Select.Trigger
|
||||
aria-label="Base model quantization"
|
||||
class="-ml-2 border-primary/15 bg-primary/[0.07] font-mono text-foreground hover:bg-primary/15 focus-visible:border-primary/40 focus-visible:ring-0"
|
||||
size="xs"
|
||||
>
|
||||
{baseOption.label}
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content class="font-mono text-xs">
|
||||
{#each mainOptions as option (option.path)}
|
||||
<Select.Item class="text-xs" label={option.label} value={option.path}>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{/if}
|
||||
|
||||
<!-- Draft add: a tiny dashed affordance right of the base part; gone once added. -->
|
||||
{#if draftOptions.length && !withDraft}
|
||||
<button
|
||||
aria-label="Add draft model"
|
||||
class="mx-1 inline-flex h-5 shrink-0 cursor-pointer items-center gap-1 rounded-md border border-dashed border-border/60 px-1.5 text-[10px] text-muted-foreground transition-colors hover:border-border hover:text-foreground"
|
||||
onclick={() => (withDraft = true)}
|
||||
type="button"
|
||||
>
|
||||
<Plus class="h-3 w-3" />
|
||||
|
||||
add draft model
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Draft segment: quant and spec type of the picked draft flavour. The X
|
||||
at the end drops the whole segment (the add button is gone once added);
|
||||
it only appears while hovering the segment, or directly on touch -->
|
||||
{#if draftOption}
|
||||
<span class="group/draft inline-flex shrink-0 items-center gap-x-2">
|
||||
<span>{SERVE_COMMAND.DRAFT_FLAG}</span>
|
||||
|
||||
<span class="shrink-0">
|
||||
{modelId}{draftQuant ? MODEL_ID.QUANTIZATION_SEPARATOR : ''}
|
||||
</span>
|
||||
|
||||
<Select.Root
|
||||
onValueChange={(v) => v && (draftPick = v)}
|
||||
type="single"
|
||||
value={draftOption.path}
|
||||
>
|
||||
<Select.Trigger
|
||||
aria-label="Draft model quantization"
|
||||
class="-ml-2 border-primary/15 bg-primary/[0.07] font-mono text-foreground hover:bg-primary/15 focus-visible:border-primary/40 focus-visible:ring-0"
|
||||
size="xs"
|
||||
>
|
||||
{draftOption.label}
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content class="font-mono text-xs">
|
||||
{#each typeDraftOptions as option (option.path)}
|
||||
<Select.Item class="text-xs" label={option.label} value={option.path}>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
{#if draftType}
|
||||
<span>{SERVE_COMMAND.SPEC_TYPE_FLAG}</span>
|
||||
|
||||
<!-- the select only earns its chrome when there is a real choice to make -->
|
||||
{#if specTypes.length > 1}
|
||||
<Select.Root
|
||||
onValueChange={(v) => v && (draftTypePick = v as ModelSidecar)}
|
||||
type="single"
|
||||
value={draftType}
|
||||
>
|
||||
<Select.Trigger
|
||||
aria-label="Draft type"
|
||||
class="border-primary/15 bg-primary/[0.07] font-mono text-foreground hover:bg-primary/15 focus-visible:border-primary/40 focus-visible:ring-0"
|
||||
size="xs"
|
||||
>
|
||||
{SPEC_TYPE[draftType]}
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content class="font-mono text-xs">
|
||||
{#each specTypes as type (type)}
|
||||
<Select.Item class="text-xs" label={SPEC_TYPE[type]} value={type}>
|
||||
{SPEC_TYPE[type]}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{:else}
|
||||
<span>{SPEC_TYPE[draftType]}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<button
|
||||
aria-label="Remove draft model"
|
||||
class="shrink-0 cursor-pointer text-muted-foreground/60 opacity-0 transition-[opacity,color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] hover:text-destructive group-hover/draft:opacity-100 [@media(pointer:coarse)]:opacity-100"
|
||||
onclick={() => (withDraft = false)}
|
||||
type="button"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="Copy command"
|
||||
class="absolute top-1/2 right-2 -translate-y-1/2 cursor-pointer rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-primary/10 hover:text-foreground"
|
||||
onclick={copy}
|
||||
type="button"
|
||||
>
|
||||
{#if copied}
|
||||
<Check class="h-3.5 w-3.5 text-green-500" />
|
||||
{:else}
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
<script lang="ts">
|
||||
import ModelsDiscoverDownloadProgressBar from '../../ModelsDiscoverDownloadProgressBar.svelte';
|
||||
import { labelFor } from './download-options.utils';
|
||||
import { Check, Download, Loader2, Pause, Play, RotateCw, X } from '@lucide/svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { HfModelSibling, ModelDownloadEntryState } from '$lib/types';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
/** GGUF file the chip stands for. */
|
||||
file: HfModelSibling;
|
||||
/** Download state of the file, from the parent's status feed. */
|
||||
entry: ModelDownloadEntryState;
|
||||
/**
|
||||
* Ask the parent to confirm deleting a downloaded model. The chip owns no
|
||||
* dialog; the parent renders the single confirmation and acts on confirm.
|
||||
*/
|
||||
onRequestDelete?: (repoWithTag: string) => void;
|
||||
/** Ask the parent to confirm cancelling an in-flight download. */
|
||||
onRequestCancel?: (repoWithTag: string) => void;
|
||||
}
|
||||
|
||||
let { entry, file, onRequestCancel, onRequestDelete }: Props = $props();
|
||||
|
||||
// Sidecar kind (mtp, mmproj, ...) tag shown on every chip state; one source so
|
||||
// the idle, in-flight and downloaded variants stay identical.
|
||||
const SIDECAR_BADGE_CLASS =
|
||||
'rounded-md bg-primary px-1 py-0.5 text-[10px] font-semibold tracking-wide text-primary-foreground uppercase';
|
||||
|
||||
/** Queue the download; a failed attempt leaves partial files, drop them first. */
|
||||
async function startDownload() {
|
||||
try {
|
||||
if (entry.isFailed) await modelsStore.status.cancelDownload(entry.repoWithTag);
|
||||
|
||||
await modelsStore.status.downloadModel(entry.repoWithTag);
|
||||
} catch {
|
||||
// the store already toasted the failure
|
||||
}
|
||||
}
|
||||
|
||||
let meta = $derived(HuggingFaceService.extractQuantMeta(file.path));
|
||||
let label = $derived(labelFor(file.path));
|
||||
|
||||
let percent = $derived(
|
||||
entry.progress && entry.progress.totalBytes > 0
|
||||
? Math.round((entry.progress.downloadedBytes / entry.progress.totalBytes) * 100)
|
||||
: null
|
||||
);
|
||||
|
||||
let tooltipText = $derived(
|
||||
entry.isDownloading
|
||||
? 'Pause downloading'
|
||||
: entry.isPaused
|
||||
? 'Resume downloading'
|
||||
: entry.isDownloaded
|
||||
? 'Delete model'
|
||||
: entry.isFailed
|
||||
? `Retry download: ${file.path}`
|
||||
: `Download ${file.path}`
|
||||
);
|
||||
</script>
|
||||
|
||||
{#snippet chipBody(dividerClass: string)}
|
||||
<!-- badge, label and divider: identical on every chip state -->
|
||||
{#if meta?.sidecar}
|
||||
<span class={SIDECAR_BADGE_CLASS}>
|
||||
{meta.sidecar}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class="font-medium">{label}</span>
|
||||
|
||||
<span class={dividerClass}></span>
|
||||
{/snippet}
|
||||
|
||||
<!-- every chip is a tooltip trigger; the button renders as the trigger's child
|
||||
so no nested button element is created -->
|
||||
{#snippet tooltipTrigger(tip: string, button: Snippet<[Record<string, unknown>]>)}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
{@render button(props)}
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{tip}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/snippet}
|
||||
|
||||
{#snippet deleteChip(props: Record<string, unknown>)}
|
||||
<!-- downloaded chips are delete actions: green check by default, red X on hover -->
|
||||
<button
|
||||
{...props}
|
||||
aria-label={tooltipText}
|
||||
class="group relative inline-flex h-auto cursor-pointer items-center gap-1 rounded-md! border px-2 py-1 text-left font-mono text-xs shadow-xs transition-[background-color,border-color,transform] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97]
|
||||
border-green-600/25 bg-green-500/5 hover:border-destructive/50 hover:bg-destructive/10 dark:border-green-500/30 dark:bg-green-500/10 dark:hover:border-destructive/50 dark:hover:bg-destructive/15"
|
||||
onclick={() => onRequestDelete?.(entry.repoWithTag)}
|
||||
type="button"
|
||||
>
|
||||
{@render chipBody(
|
||||
'-my-1 mx-0.75 w-px self-stretch bg-green-600/25 transition-colors duration-200 group-hover:bg-destructive/30 dark:bg-green-600/30 dark:group-hover:bg-destructive/30'
|
||||
)}
|
||||
|
||||
<span>{HuggingFaceService.formatFileSize(file.size ?? 0)}</span>
|
||||
|
||||
<!-- icon slot: crossfade check -> x; touch devices show the delete affordance directly -->
|
||||
<span class="relative inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||
<Check
|
||||
class="absolute h-3.5 w-3.5 text-green-500 transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover:scale-75 group-hover:opacity-0 [@media(pointer:coarse)]:hidden"
|
||||
/>
|
||||
|
||||
<X
|
||||
class="absolute h-3.5 w-3.5 scale-75 text-destructive opacity-0 transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover:scale-100 group-hover:opacity-100 [@media(pointer:coarse)]:scale-100 [@media(pointer:coarse)]:opacity-100"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet pauseResumeChip(props: Record<string, unknown>)}
|
||||
<button
|
||||
{...props}
|
||||
aria-label={tooltipText}
|
||||
class="flex h-auto min-w-0 flex-1 cursor-pointer items-center gap-1 text-left"
|
||||
onclick={() => {
|
||||
if (entry.isDownloading) void modelsStore.status.pauseDownload(entry.repoWithTag);
|
||||
else void modelsStore.status.downloadModel(entry.repoWithTag).catch(() => {});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{@render chipBody('-my-1 mx-0.75 w-px self-stretch bg-border')}
|
||||
|
||||
{#if percent !== null}
|
||||
<span class="mr-1 tabular-nums">{percent}%</span>
|
||||
{:else if entry.isPaused}
|
||||
<span>Paused</span>
|
||||
{:else}
|
||||
<span>{HuggingFaceService.formatFileSize(file.size ?? 0)}</span>
|
||||
{/if}
|
||||
|
||||
{#if entry.isDownloading}
|
||||
<!-- spinner fades into the pause affordance on hover; opacity only, the
|
||||
spin keyframes own the transform so scale would fight them -->
|
||||
<span class="relative inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||
<Loader2
|
||||
class="absolute h-3.5 w-3.5 animate-spin text-muted-foreground transition-opacity duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover:opacity-0 [@media(pointer:coarse)]:hidden"
|
||||
/>
|
||||
|
||||
<Pause
|
||||
class="absolute h-3.5 w-3.5 opacity-0 transition-opacity duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover:opacity-100 [@media(pointer:coarse)]:opacity-100"
|
||||
/>
|
||||
</span>
|
||||
{:else}
|
||||
<!-- paused: the play affordance fades in on hover; visible directly on touch -->
|
||||
<span class="relative inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||
<Play
|
||||
class="absolute h-3.5 w-3.5 scale-75 opacity-0 transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover:scale-100 group-hover:opacity-100 [@media(pointer:coarse)]:scale-100 [@media(pointer:coarse)]:opacity-100"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet cancelChip(props: Record<string, unknown>)}
|
||||
<!-- cancel slot: same fixed slot and fade as the pause / play affordances
|
||||
so the two icons line up exactly; the X turns destructive on its hover -->
|
||||
<button
|
||||
{...props}
|
||||
aria-label="Cancel downloading"
|
||||
class="relative grid h-3.5 w-3.5 shrink-0 cursor-pointer items-center justify-center text-muted-foreground/70"
|
||||
onclick={() => onRequestCancel?.(entry.repoWithTag)}
|
||||
type="button"
|
||||
>
|
||||
<X
|
||||
class="h-3.5 w-3.5 transition-[opacity,transform,color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] text-destructive [@media(pointer:coarse)]:scale-100 [@media(pointer:coarse)]:opacity-100"
|
||||
/>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet downloadChip(props: Record<string, unknown>)}
|
||||
<!-- idle chips download on click (retry when the last attempt failed) -->
|
||||
<button
|
||||
{...props}
|
||||
aria-label={tooltipText}
|
||||
class="group relative inline-flex h-auto cursor-pointer items-center gap-1 overflow-hidden rounded-md! border px-2 py-1 text-left font-mono text-xs shadow-xs transition-[background-color,border-color,transform] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97]
|
||||
border-border/30 bg-background hover:bg-muted-foreground/10 dark:border-border/20 dark:bg-muted-foreground/15 dark:text-secondary-foreground dark:hover:bg-muted-foreground/25
|
||||
{entry.isFailed ? 'border-destructive!' : ''}"
|
||||
onclick={() => void startDownload()}
|
||||
type="button"
|
||||
>
|
||||
{#if entry.isFailed}
|
||||
<span
|
||||
class="rounded-md bg-destructive px-1 py-0.5 text-[10px] font-semibold tracking-wide text-destructive-foreground uppercase"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{@render chipBody('-my-1 mx-0.75 w-px self-stretch bg-border')}
|
||||
|
||||
<span>{HuggingFaceService.formatFileSize(file.size ?? 0)}</span>
|
||||
|
||||
<span class="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||
{#if entry.isFailed}
|
||||
<RotateCw class="h-3.5 w-3.5 text-destructive" />
|
||||
{:else}
|
||||
<Download
|
||||
class="h-3.5 w-3.5 text-muted-foreground transition-colors duration-150 group-hover:text-foreground"
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#if entry.isDownloaded}
|
||||
{@render tooltipTrigger(tooltipText, deleteChip)}
|
||||
{:else if entry.isDownloading || entry.isPaused}
|
||||
<!-- in-flight / paused chips: the chip body pauses / resumes on click, the X
|
||||
inside the chip cancels (stops and discards the partial files). The X slot
|
||||
is reserved, so the chip never reflows when the affordance fades in -->
|
||||
<div
|
||||
class="group relative inline-flex h-auto items-center gap-1 overflow-hidden rounded-md! border px-2 py-1 text-left font-mono text-xs shadow-xs transition-[background-color,border-color,transform] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97]
|
||||
{entry.isPaused
|
||||
? 'border-yellow-600/40 bg-yellow-500/10 hover:bg-yellow-500/20 dark:border-yellow-500/30 dark:bg-yellow-500/10'
|
||||
: 'border-border/30 bg-background hover:bg-muted-foreground/10 dark:border-border/20 dark:bg-muted-foreground/15'}"
|
||||
>
|
||||
{@render tooltipTrigger(tooltipText, pauseResumeChip)}
|
||||
|
||||
{@render tooltipTrigger('Cancel downloading', cancelChip)}
|
||||
|
||||
{#if percent !== null}
|
||||
<ModelsDiscoverDownloadProgressBar
|
||||
downloadedBytes={entry.progress?.downloadedBytes ?? 0}
|
||||
overlay
|
||||
totalBytes={entry.progress?.totalBytes ?? 0}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{@render tooltipTrigger(tooltipText, downloadChip)}
|
||||
{/if}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton from './ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte';
|
||||
import {
|
||||
BIT_DEPTH_LABEL_SUFFIX,
|
||||
GIGABYTE_LABEL,
|
||||
OTHER_BIT_DEPTH,
|
||||
OTHER_BIT_DEPTH_LABEL
|
||||
} from '$lib/constants';
|
||||
import { ModelSelectableFileKind } from '$lib/enums';
|
||||
import type { ModelDownloadEntryState, ModelSelectableFile } from '$lib/types';
|
||||
import { minMemoryTierGb } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
/** Bit depth of the row; `99` renders as "Other". */
|
||||
bitDepth: number;
|
||||
/** Every GGUF of this bit depth, with download state attached. */
|
||||
files: (ModelSelectableFile & { state: ModelDownloadEntryState })[];
|
||||
/** Forwarded to each chip: ask the parent to confirm a cancel. */
|
||||
onRequestCancel?: (repoWithTag: string) => void;
|
||||
/** Forwarded to each chip: ask the parent to confirm a delete. */
|
||||
onRequestDelete?: (repoWithTag: string) => void;
|
||||
}
|
||||
|
||||
let { bitDepth, files, onRequestCancel, onRequestDelete }: Props = $props();
|
||||
|
||||
let mainFile = $derived(files.find((f) => f.kind === ModelSelectableFileKind.MAIN) ?? null);
|
||||
let draftFile = $derived(files.find((f) => f.kind === ModelSelectableFileKind.DRAFT) ?? null);
|
||||
|
||||
let mainMemGb = $derived(mainFile ? minMemoryTierGb(mainFile.size ?? 0) : null);
|
||||
let draftMemGb = $derived(draftFile ? minMemoryTierGb(draftFile.size ?? 0) : null);
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-[5rem_1fr] items-center gap-3 py-3">
|
||||
<div class="pt-1 text-sm tabular-nums text-muted-foreground">
|
||||
{#if bitDepth === OTHER_BIT_DEPTH}
|
||||
{OTHER_BIT_DEPTH_LABEL}
|
||||
{:else}
|
||||
{bitDepth}{BIT_DEPTH_LABEL_SUFFIX}
|
||||
{/if}
|
||||
|
||||
{#if mainMemGb}
|
||||
<span class="block text-[10px] whitespace-nowrap text-muted-foreground/60">
|
||||
needs at least {mainMemGb}{GIGABYTE_LABEL}{draftMemGb
|
||||
? ` + ${draftMemGb}${GIGABYTE_LABEL}`
|
||||
: ''} memory
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-1.5">
|
||||
{#each files as file (file.path)}
|
||||
<ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton
|
||||
entry={file.state}
|
||||
{file}
|
||||
{onRequestCancel}
|
||||
{onRequestDelete}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { DRAFT_FILE_LABEL, MODEL_ID, PATH_SEPARATOR } from '$lib/constants';
|
||||
import { ModelSelectableFileKind } from '$lib/enums';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { isAuxSidecar, isDraftSidecar } from '$lib/utils';
|
||||
|
||||
/** Kind of a file path: the main weights, a draft sidecar, or an aux sidecar (mmproj). */
|
||||
export function classify(path: string): ModelSelectableFileKind {
|
||||
const sidecar = HuggingFaceService.extractQuantMeta(path)?.sidecar;
|
||||
|
||||
if (!sidecar) return ModelSelectableFileKind.MAIN;
|
||||
|
||||
return isAuxSidecar(sidecar) ? ModelSelectableFileKind.AUX : ModelSelectableFileKind.DRAFT;
|
||||
}
|
||||
|
||||
/** Display label of a file: its quant, else the file name without the extension. */
|
||||
export function labelFor(path: string): string {
|
||||
const meta = HuggingFaceService.extractQuantMeta(path);
|
||||
|
||||
if (meta?.quant) return meta.quant;
|
||||
|
||||
// Quantless sidecar files: the chip badge already carries the sidecar type,
|
||||
// so the label only marks draft files; aux sidecars badge alone.
|
||||
if (meta?.sidecar) return isDraftSidecar(meta.sidecar) ? DRAFT_FILE_LABEL : '';
|
||||
|
||||
const basename = path.split(PATH_SEPARATOR).pop() ?? path;
|
||||
|
||||
return basename.replace(MODEL_ID.WEIGHT_EXTENSION_REGEX, '');
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
*
|
||||
* MODELS DISCOVER - DETAILS - DOWNLOAD OPTIONS
|
||||
*
|
||||
* The download area of the detail pane: GGUF files grouped by bit depth, each an
|
||||
* independent download action chip, plus the standalone terminal command preview.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsDownloadOptions** - GGUF download options
|
||||
*
|
||||
* Groups GGUF files by bit depth and renders one independent download
|
||||
* action chip per file, plus the standalone terminal command preview.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsDownloadOptions } from './ModelsDiscoverDetailsDownloadOptions.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsDownloadOptionsRow** - One bit-depth group of quants
|
||||
*
|
||||
* A single bit-depth row inside ModelsDiscoverDetailsDownloadOptions: the
|
||||
* depth label with its memory hint and the quant chips of that depth.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsDownloadOptionsRow } from './ModelsDiscoverDetailsDownloadOptionsRow.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton** - One quant chip
|
||||
*
|
||||
* A single GGUF file as an independent action chip: download / retry when
|
||||
* idle, pause / resume / cancel while in flight, delete when downloaded.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton } from './ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsDownloadOptionsDownloadCommand** - Terminal command
|
||||
*
|
||||
* The `llama serve -hf ...` command box with inline quant selects and a copy
|
||||
* button; owns its picks, nothing two-way binds them to the quant chips.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsDownloadOptionsDownloadCommand } from './ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte';
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import ModelCapabilityIcons from '../../ModelCapabilityIcons.svelte';
|
||||
import ModelsDiscoverAvatar from '../ModelsDiscoverAvatar.svelte';
|
||||
import ModelsDiscoverDetailsHfHubStats from './ModelsDiscoverDetailsHfHubStats.svelte';
|
||||
import ModelsDiscoverDetailsMetadata from './ModelsDiscoverDetailsMetadata.svelte';
|
||||
import { ExternalLink } from '@lucide/svelte';
|
||||
import { ICON_CLASS_SM } from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { HfModelDetailInfo, HfModelGguf } from '$lib/types/huggingface';
|
||||
import { orgOf } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
modelId: string;
|
||||
details: HfModelDetailInfo;
|
||||
gguf?: HfModelGguf;
|
||||
baseModels: string[];
|
||||
licenseTag: string | null;
|
||||
hasVision: boolean;
|
||||
hasTools: boolean;
|
||||
hasReasoning: boolean;
|
||||
}
|
||||
|
||||
let { baseModels, details, gguf, hasReasoning, hasTools, hasVision, licenseTag, modelId }: Props =
|
||||
$props();
|
||||
|
||||
// Avatar shows the base model's org (e.g. the Qwen logo for a ggml-org GGUF)
|
||||
// with the quant org as a corner badge when they differ.
|
||||
let repoOrg = $derived(orgOf(details.id) || orgOf(modelId));
|
||||
let baseOrg = $derived(orgOf(baseModels[0]));
|
||||
let avatarOrg = $derived(baseOrg || repoOrg);
|
||||
let quantOrg = $derived(baseOrg && baseOrg !== repoOrg ? repoOrg : undefined);
|
||||
</script>
|
||||
|
||||
<header class="space-y-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<ModelsDiscoverAvatar
|
||||
org={avatarOrg}
|
||||
{quantOrg}
|
||||
quantPositionClass="-bottom-1.5 -right-1.5"
|
||||
quantSize="h-6 w-6"
|
||||
size="h-12 w-12"
|
||||
/>
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="truncate text-lg font-semibold">{details.id ?? modelId}</h1>
|
||||
|
||||
<ModelCapabilityIcons
|
||||
gapClass="gap-2"
|
||||
iconSize="h-4 w-4"
|
||||
modalities={{ audio: false, video: false, vision: hasVision }}
|
||||
supportsThinking={hasReasoning}
|
||||
supportsToolUse={hasTools}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if baseModels.length}
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="truncate text-xs text-muted-foreground">{baseModels.join(', ')}</span>
|
||||
|
||||
<a
|
||||
aria-label="View base model on HuggingFace"
|
||||
class="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
|
||||
href={HuggingFaceService.getModelUrl(baseModels[0])}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="inline-flex shrink-0 items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted"
|
||||
href={HuggingFaceService.getModelUrl(modelId)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img alt="" class="h-3.5 w-3.5" src="/recommended-mcp/huggingface.ico" />
|
||||
|
||||
View on Hugging Face
|
||||
|
||||
<ExternalLink class={ICON_CLASS_SM} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ModelsDiscoverDetailsHfHubStats {details} />
|
||||
|
||||
<ModelsDiscoverDetailsMetadata {details} {gguf} {licenseTag} {modelId} />
|
||||
</header>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Download, Heart } from '@lucide/svelte';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import type { HfModelDetailInfo } from '$lib/types/huggingface';
|
||||
|
||||
interface Props {
|
||||
details: HfModelDetailInfo;
|
||||
}
|
||||
|
||||
let { details }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
||||
{#if typeof details.downloads === 'number'}
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<Download class="h-3.5 w-3.5" />
|
||||
{HuggingFaceService.formatDownloads(details.downloads)}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if typeof details.likes === 'number'}
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<Heart class="h-3.5 w-3.5" />
|
||||
{HuggingFaceService.formatLikes(details.likes)}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if details.lastModified}
|
||||
<span>Updated {HuggingFaceService.formatRelativeTime(details.lastModified)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import ModelsDiscoverChatTemplateDialog from './ModelsDiscoverChatTemplateDialog.svelte';
|
||||
import ModelsDiscoverDetailsMetadataItem from './ModelsDiscoverDetailsMetadataItem.svelte';
|
||||
import { MessageSquareCode } from '@lucide/svelte';
|
||||
import { modelsDiscoverStore } from '$lib/stores';
|
||||
import type { HfModelDetailInfo, HfModelGguf } from '$lib/types/huggingface';
|
||||
import { formatParameters } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
/** Full HuggingFace model id, e.g. `ggml-org/gemma-3-4b-it-GGUF`. */
|
||||
modelId: string;
|
||||
details: HfModelDetailInfo;
|
||||
gguf?: HfModelGguf;
|
||||
licenseTag: string | null;
|
||||
}
|
||||
|
||||
let { details, gguf, licenseTag, modelId }: Props = $props();
|
||||
|
||||
// Catalog family description when curated, else the HF card description.
|
||||
let description = $derived(
|
||||
modelsDiscoverStore.descriptionFor(modelId) ?? details.cardData?.description
|
||||
);
|
||||
|
||||
let chatTemplateOpen = $state(false);
|
||||
</script>
|
||||
|
||||
{#if description}
|
||||
<p class="text-sm text-muted-foreground">{description}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Metadata chips: label | value pairs, matching the HF model page style -->
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
{#if gguf?.total}
|
||||
<ModelsDiscoverDetailsMetadataItem
|
||||
label="Model size"
|
||||
value="{formatParameters(gguf.total)} params"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if gguf?.context_length}
|
||||
<ModelsDiscoverDetailsMetadataItem
|
||||
label="Context"
|
||||
value={gguf.context_length.toLocaleString()}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if gguf?.architecture}
|
||||
<ModelsDiscoverDetailsMetadataItem label="Architecture" value={gguf.architecture} />
|
||||
{/if}
|
||||
|
||||
{#if gguf?.chat_template}
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted"
|
||||
onclick={() => (chatTemplateOpen = true)}
|
||||
type="button"
|
||||
>
|
||||
<MessageSquareCode class="h-3 w-3" />
|
||||
Chat template
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if licenseTag}
|
||||
<ModelsDiscoverDetailsMetadataItem label="License" value={licenseTag} />
|
||||
{/if}
|
||||
|
||||
{#if details.gated === true}
|
||||
<span
|
||||
class="rounded bg-yellow-500/10 px-2 py-0.5 text-xs font-medium text-yellow-600 dark:text-yellow-400"
|
||||
>
|
||||
gated
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if gguf?.chat_template}
|
||||
<ModelsDiscoverChatTemplateDialog
|
||||
bind:open={chatTemplateOpen}
|
||||
chatTemplate={gguf.chat_template}
|
||||
/>
|
||||
{/if}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
let { label, value }: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- Metadata chip: label | value pair, matching the HF model page style -->
|
||||
<span class="inline-flex items-center divide-x divide-border rounded-md border text-xs">
|
||||
<span class="px-2.5 py-1 text-muted-foreground">{label}</span>
|
||||
|
||||
<span class="px-2.5 py-1 font-medium">{value}</span>
|
||||
</span>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { MarkdownContent } from '$lib/components/app';
|
||||
|
||||
interface Props {
|
||||
readme: string | null;
|
||||
}
|
||||
|
||||
let { readme }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if readme}
|
||||
<section
|
||||
class="p-3 rounded-3xl border border-border/30 bg-muted/60 shadow-xs dark:border-border/20"
|
||||
>
|
||||
<MarkdownContent allowHtml class="prose-sm max-w-none" content={readme} />
|
||||
</section>
|
||||
{/if}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
</script>
|
||||
|
||||
<!-- Static skeleton of ModelsDiscoverDetails: header, metadata chips, download options and readme lines. -->
|
||||
<div class="space-y-6 p-6" data-slot="model-details-skeleton">
|
||||
<!-- Header: avatar, name with capability icons, base model line, HF link -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<Skeleton class="h-12 w-12 rounded-md" />
|
||||
|
||||
<div class="min-w-0 space-y-1.5">
|
||||
<Skeleton class="h-5 w-56 max-w-full" />
|
||||
|
||||
<Skeleton class="h-3 w-40 max-w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Skeleton class="h-7.5 w-44 shrink-0 rounded-md" />
|
||||
</div>
|
||||
|
||||
<!-- downloads / likes / last updated -->
|
||||
<div class="flex items-center gap-4">
|
||||
<Skeleton class="h-3.5 w-16" />
|
||||
|
||||
<Skeleton class="h-3.5 w-12" />
|
||||
|
||||
<Skeleton class="h-3.5 w-24" />
|
||||
</div>
|
||||
|
||||
<!-- metadata chips -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<Skeleton class="h-7 w-36 rounded-md" />
|
||||
|
||||
<Skeleton class="h-7 w-28 rounded-md" />
|
||||
|
||||
<Skeleton class="h-7 w-32 rounded-md" />
|
||||
|
||||
<Skeleton class="h-7 w-24 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download options: quant rows, CTA, terminal command -->
|
||||
<div
|
||||
class="rounded-3xl border border-border/30 bg-muted/40 p-4 shadow-xs dark:border-border/20 dark:bg-muted/50"
|
||||
>
|
||||
<div class="space-y-3 divide-y divide-border/50 dark:divide-border/35 pb-1">
|
||||
{#each [0, 1, 2] as _, index (index)}
|
||||
<div class="grid grid-cols-[5rem_1fr] items-start gap-3 py-3">
|
||||
<div class="space-y-1.5 pt-1">
|
||||
<Skeleton class="h-4 w-14" />
|
||||
|
||||
<Skeleton class="h-2.5 w-24" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-1.5">
|
||||
<Skeleton class="h-6 w-28 rounded-md" />
|
||||
|
||||
<Skeleton class="h-6 w-20 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5 border-t border-border/50 pt-3.5 dark:border-border/35">
|
||||
<Skeleton class="h-9 w-full rounded-md" />
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="h-px flex-1 bg-border/50"></span>
|
||||
|
||||
<Skeleton class="h-3 w-44" />
|
||||
|
||||
<span class="h-px flex-1 bg-border/50"></span>
|
||||
</div>
|
||||
|
||||
<Skeleton class="h-11 w-full rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Readme -->
|
||||
<div class="space-y-2.5">
|
||||
<Skeleton class="h-3.5 w-full" />
|
||||
|
||||
<Skeleton class="h-3.5 w-full" />
|
||||
|
||||
<Skeleton class="h-3.5 w-5/6" />
|
||||
|
||||
<Skeleton class="h-3.5 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
*
|
||||
* MODELS DISCOVER - DETAILS
|
||||
*
|
||||
* The right-hand detail pane of the discover view: header (avatar, name, stats, metadata
|
||||
* chips, capability badges), the download options area and the model-card README.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetails** - Model detail view
|
||||
*
|
||||
* Detail pane for the selected model. Loads its own data (details + GGUF file
|
||||
* list) from HuggingFaceService based on the `modelId` route param.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetails } from './ModelsDiscoverDetails.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsHeader** - Detail view header
|
||||
*
|
||||
* Shows the model avatar (base org + quant org corner badge), name, base model
|
||||
* info, stats, metadata chips and capability badges.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsHeader } from './ModelsDiscoverDetailsHeader.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsHfHubStats** - HuggingFace Hub stats row
|
||||
*
|
||||
* Downloads, likes and last-updated for the viewed model, formatted the way
|
||||
* the Hub shows them.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsHfHubStats } from './ModelsDiscoverDetailsHfHubStats.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsSkeleton** - Detail view loading skeleton
|
||||
*
|
||||
* Static placeholder matching the detail layout: header with avatar and name,
|
||||
* metadata chips, the download options box and readme text lines.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsSkeleton } from './ModelsDiscoverDetailsSkeleton.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsReadme** - Detail view README
|
||||
*
|
||||
* Renders the model card README as markdown.
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsReadme } from './ModelsDiscoverDetailsReadme.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsMetadata** - Detail view metadata row
|
||||
*
|
||||
* The detail view's metadata chips (model size, context, architecture, license,
|
||||
* chat template).
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsMetadata } from './ModelsDiscoverDetailsMetadata.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDetailsMetadataItem** - Single metadata chip
|
||||
*
|
||||
* One label | value chip of the detail view's metadata row (model size,
|
||||
* context, architecture, license).
|
||||
*/
|
||||
export { default as ModelsDiscoverDetailsMetadataItem } from './ModelsDiscoverDetailsMetadataItem.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverChatTemplateDialog** - Chat template viewer
|
||||
*
|
||||
* Shows the model's chat template in a scrollable dialog with a copy button.
|
||||
*/
|
||||
export { default as ModelsDiscoverChatTemplateDialog } from './ModelsDiscoverChatTemplateDialog.svelte';
|
||||
|
||||
export * from './ModelsDiscoverDetailsDownloadOptions';
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
downloadedBytes: number;
|
||||
totalBytes: number;
|
||||
overlay?: boolean;
|
||||
}
|
||||
|
||||
let { downloadedBytes, overlay = false, totalBytes }: Props = $props();
|
||||
|
||||
let fraction = $derived.by(() => {
|
||||
if (totalBytes <= 0) return 0;
|
||||
|
||||
return Math.min(Math.max(downloadedBytes / totalBytes, 0), 1);
|
||||
});
|
||||
let percent = $derived(Math.round(fraction * 100));
|
||||
</script>
|
||||
|
||||
{#if overlay}
|
||||
<div class="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 overflow-hidden rounded-b-sm">
|
||||
<div
|
||||
class="h-full animate-pulse bg-primary transition-[width] duration-200 ease-out"
|
||||
style="width: {percent}%"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="h-1 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
class="h-full animate-pulse bg-primary transition-[width] duration-200 ease-out"
|
||||
style="width: {percent}%"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import ModelsDiscoverListItem from './ModelsDiscoverListItem.svelte';
|
||||
import ModelsDiscoverListItemSkeleton from './ModelsDiscoverListItemSkeleton.svelte';
|
||||
import type { HfModelInfo } from '$lib/types/huggingface';
|
||||
|
||||
interface Props {
|
||||
models: HfModelInfo[];
|
||||
activeId?: string | null;
|
||||
loading?: boolean;
|
||||
loadingSkeletonRowsCount?: number;
|
||||
/** Show the original (base) model's org avatar instead of the repo's org. */
|
||||
showBaseModelAvatar?: boolean;
|
||||
onSelect?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
activeId = null,
|
||||
loading = false,
|
||||
loadingSkeletonRowsCount = 8,
|
||||
models,
|
||||
onSelect,
|
||||
showBaseModelAvatar = false
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<ul class="space-y-0.5 p-2">
|
||||
{#if loading}
|
||||
{#each Array(loadingSkeletonRowsCount) as _, index (index)}
|
||||
<ModelsDiscoverListItemSkeleton {index} />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each models as model (model.id)}
|
||||
<ModelsDiscoverListItem
|
||||
active={model.id === activeId}
|
||||
{model}
|
||||
{onSelect}
|
||||
{showBaseModelAvatar}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import ModelId from '../../ModelId.svelte';
|
||||
import ModelsDiscoverAvatar from '../ModelsDiscoverAvatar.svelte';
|
||||
import {
|
||||
HF_MMPROJ_FILENAME_TOKEN,
|
||||
HF_MODALITY_PIPELINE_TAGS,
|
||||
type ModelSidecar
|
||||
} from '$lib/constants';
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
import { modelsDiscoverStore } from '$lib/stores';
|
||||
import type { ModelsDiscoverSizeRange } from '$lib/stores/models-discover/index.svelte';
|
||||
import type { HfModelInfo } from '$lib/types/huggingface';
|
||||
import type { ModelModalities } from '$lib/types/models';
|
||||
import { detectThinkingSupport, detectToolUseSupport, isAuxSidecar, orgOf } from '$lib/utils';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
model: HfModelInfo;
|
||||
active?: boolean;
|
||||
/** Show the original (base) model's org avatar instead of the repo's org. */
|
||||
showBaseModelAvatar?: boolean;
|
||||
onSelect?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
let { active = false, model, onSelect, showBaseModelAvatar = false }: Props = $props();
|
||||
|
||||
let org = $derived(orgOf(model.id));
|
||||
|
||||
// Org whose avatar is shown: the base model's org when showBaseModelAvatar
|
||||
// (e.g. the Qwen logo for ggml-org/Qwen3.8-27B-GGUF), else the repo's org.
|
||||
let avatarOrg = $derived.by(() => {
|
||||
if (!showBaseModelAvatar) return org;
|
||||
|
||||
return orgOf(HuggingFaceService.getBaseModels(model)[0]) || org;
|
||||
});
|
||||
|
||||
let contextLength = $derived(model.gguf?.context_length);
|
||||
|
||||
// Reasoning support from the chat template, matching the details view.
|
||||
let supportsThinking = $derived(detectThinkingSupport(model.gguf?.chat_template ?? ''));
|
||||
|
||||
// Tool use support from the chat template.
|
||||
let supportsToolUse = $derived(detectToolUseSupport(model.gguf?.chat_template ?? ''));
|
||||
|
||||
// Modalities derived from HF metadata: vision from an mmproj sidecar or a
|
||||
// multimodal pipeline tag, audio/video from their pipeline tags.
|
||||
let modalities = $derived.by<ModelModalities>(() => {
|
||||
const tag = model.pipeline_tag ?? '';
|
||||
const vision =
|
||||
HF_MODALITY_PIPELINE_TAGS.vision.includes(tag) ||
|
||||
Boolean(
|
||||
model.siblings?.some((s) => s.rfilename.toLowerCase().includes(HF_MMPROJ_FILENAME_TOKEN))
|
||||
);
|
||||
const audio = HF_MODALITY_PIPELINE_TAGS.audio.includes(tag);
|
||||
const video = HF_MODALITY_PIPELINE_TAGS.video.includes(tag);
|
||||
|
||||
return { audio, video, vision };
|
||||
});
|
||||
|
||||
// Draft sidecars (mtp, dflash, dspark, eagle3) present in the repo, e.g.
|
||||
// speculative-decoding drafts. mmproj is excluded: it is vision, already
|
||||
// conveyed by the modalities.
|
||||
let draftSidecars = $derived.by<ModelSidecar[]>(() => {
|
||||
const set = new SvelteSet<ModelSidecar>();
|
||||
|
||||
for (const sibling of model.siblings ?? []) {
|
||||
const sidecar = HuggingFaceService.extractQuantMeta(sibling.rfilename)?.sidecar;
|
||||
|
||||
if (sidecar && !isAuxSidecar(sidecar)) set.add(sidecar);
|
||||
}
|
||||
|
||||
return [...set];
|
||||
});
|
||||
|
||||
// Min/max size across the repo's quants, draft sidecars included. The store
|
||||
// has catalog rows covered already; any other row (a search hit) measures
|
||||
// its repo once here and the result is cached per repo.
|
||||
let measuredSize = $state<ModelsDiscoverSizeRange | null>(null);
|
||||
let sizeRange = $derived(modelsDiscoverStore.cachedSizeRangeFor(model.id) ?? measuredSize);
|
||||
|
||||
$effect(() => {
|
||||
const id = model.id;
|
||||
|
||||
if (modelsDiscoverStore.cachedSizeRangeFor(id)) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
void modelsDiscoverStore.sizeRange(id).then((range) => {
|
||||
if (!cancelled) measuredSize = range ?? null;
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<li>
|
||||
<button
|
||||
aria-current={active ? 'page' : undefined}
|
||||
class="flex w-full cursor-pointer items-start gap-2.5 rounded-lg p-2.5 text-left transition-colors {active
|
||||
? 'bg-primary/10 hover:bg-primary/15'
|
||||
: 'hover:bg-muted/60'}"
|
||||
onclick={() => onSelect?.(model.id)}
|
||||
type="button"
|
||||
>
|
||||
<ModelsDiscoverAvatar
|
||||
class="mt-1"
|
||||
org={avatarOrg}
|
||||
quantOrg={showBaseModelAvatar ? org : undefined}
|
||||
/>
|
||||
|
||||
<span class="min-w-0 flex-1">
|
||||
<ModelId
|
||||
class="min-w-0"
|
||||
{contextLength}
|
||||
{draftSidecars}
|
||||
hideOrgName
|
||||
iconsOnNewLine
|
||||
{modalities}
|
||||
modelId={model.id}
|
||||
{sizeRange}
|
||||
{supportsThinking}
|
||||
{supportsToolUse}
|
||||
wrap
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
|
||||
interface Props {
|
||||
/** Row index; varies skeleton widths so the list does not look mechanical. */
|
||||
index?: number;
|
||||
}
|
||||
|
||||
let { index = 0 }: Props = $props();
|
||||
|
||||
// Deterministic width variations keyed by row position.
|
||||
const NAME_WIDTHS = ['w-40', 'w-52', 'w-44', 'w-56'];
|
||||
const BADGE_WIDTHS = [
|
||||
['w-12', 'w-14', 'w-10'],
|
||||
['w-16', 'w-12', 'w-12'],
|
||||
['w-10', 'w-16', 'w-10'],
|
||||
['w-14', 'w-10', 'w-14']
|
||||
];
|
||||
|
||||
let nameWidth = $derived(NAME_WIDTHS[index % NAME_WIDTHS.length]);
|
||||
let badgeWidths = $derived(BADGE_WIDTHS[index % BADGE_WIDTHS.length]);
|
||||
</script>
|
||||
|
||||
<!-- Static skeleton of ModelsDiscoverListItem: avatar, name and badge rows. -->
|
||||
<li>
|
||||
<div class="flex w-full items-start gap-2.5 rounded-lg p-2.5 text-left">
|
||||
<Skeleton class="h-9 w-9 shrink-0 rounded-md" />
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-1.5">
|
||||
<Skeleton class="{nameWidth} h-4 max-w-full" />
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
{#each badgeWidths as width, i (i)}
|
||||
<Skeleton class="{width} h-3.5 rounded" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { SearchInput } from '$lib/components/app';
|
||||
|
||||
interface Props {
|
||||
value?: string;
|
||||
/** Search callback; the parent owns debouncing. */
|
||||
onSearch?: (query: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
let { onSearch, placeholder = 'Search models...', value = $bindable('') }: Props = $props();
|
||||
|
||||
function handleInput(next: string) {
|
||||
value = next;
|
||||
|
||||
onSearch?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sticky top-0 z-99 p-2">
|
||||
<SearchInput bind:value onInput={(v) => handleInput(v)} {placeholder} />
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
*
|
||||
* MODELS DISCOVER - LIST
|
||||
*
|
||||
* The discover sidebar column: a debounced search field above a navigable list of
|
||||
* HuggingFace GGUF models, with skeleton rows while loading.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverList** - Sidebar model list
|
||||
*
|
||||
* Renders the discover model list as a navigable column. Each row links to the
|
||||
* model's detail and highlights the active one.
|
||||
*/
|
||||
export { default as ModelsDiscoverList } from './ModelsDiscoverList.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverListSearch** - Sidebar search input
|
||||
*
|
||||
* Debounced search field for the model list.
|
||||
*/
|
||||
export { default as ModelsDiscoverListSearch } from './ModelsDiscoverListSearch.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverListItem** - Single sidebar row
|
||||
*
|
||||
* One model entry in the discover sidebar list, selectable via `onSelect`.
|
||||
*/
|
||||
export { default as ModelsDiscoverListItem } from './ModelsDiscoverListItem.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverListItemSkeleton** - Skeleton sidebar row
|
||||
*
|
||||
* Pulsing placeholder matching a ModelsDiscoverListItem row, shown while the
|
||||
* list is loading.
|
||||
*/
|
||||
export { default as ModelsDiscoverListItemSkeleton } from './ModelsDiscoverListItemSkeleton.svelte';
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
*
|
||||
* MODELS DISCOVER
|
||||
*
|
||||
* Components for the Models Discover view: a sidebar search + list of
|
||||
* HuggingFace GGUF models and a detail view for the selected model, used as the
|
||||
* body of the discovery dialog. The list and detail trees live in their own
|
||||
* subfolders; this barrel re-exports them alongside the shared leaves.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* **ModelsDiscover** - Models discover explorer
|
||||
*
|
||||
* The complete discovery layout: a sidebar search + model list on the left and a
|
||||
* detail view for the selected model on the right. Used as the body of the
|
||||
* discovery dialog.
|
||||
*/
|
||||
export { default as ModelsDiscover } from './ModelsDiscover.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverAvatar** - Org avatar for a model row
|
||||
*
|
||||
* Shows the org's avatar image, falling back to a monogram on a stable hue
|
||||
* derived from the org name when the image fails to load. Shared by the list,
|
||||
* the detail header and the model selector rows.
|
||||
*/
|
||||
export { default as ModelsDiscoverAvatar } from './ModelsDiscoverAvatar.svelte';
|
||||
|
||||
/**
|
||||
* **ModelsDiscoverDownloadProgressBar** - Thin download progress bar
|
||||
*
|
||||
* Normalizes bytes to a 0..100% bar; can pin to the bottom edge as an overlay.
|
||||
* Shared by the quant chips and the model selector's download rows.
|
||||
*/
|
||||
export { default as ModelsDiscoverDownloadProgressBar } from './ModelsDiscoverDownloadProgressBar.svelte';
|
||||
|
||||
export * from './ModelsDiscoverList';
|
||||
export * from './ModelsDiscoverDetails';
|
||||
@@ -109,3 +109,12 @@ export { default as ModelBadge } from './ModelBadge.svelte';
|
||||
* Respects the user's `showRawModelNames` setting.
|
||||
*/
|
||||
export { default as ModelId } from './ModelId.svelte';
|
||||
|
||||
/**
|
||||
* **ModelCapabilityIcons** - Capability and modality icon row
|
||||
*
|
||||
* The shared tool-use / reasoning / vision / video / audio icon cluster with
|
||||
* tooltips, used by ModelId and the discover details header so the order and
|
||||
* styling stay consistent across every model-id surface.
|
||||
*/
|
||||
export { default as ModelCapabilityIcons } from './ModelCapabilityIcons.svelte';
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
DialogConversationRename,
|
||||
DialogModelsDiscover,
|
||||
DialogSettingsChat,
|
||||
Logo,
|
||||
SidebarNavigationActions,
|
||||
@@ -93,6 +94,7 @@
|
||||
|
||||
let renameDialogOpen = $state(false);
|
||||
let settingsDialogOpen = $state(false);
|
||||
let modelsDiscoverOpen = $state(false);
|
||||
let renameTargetConversationId = $state<string | null>(null);
|
||||
let renameDraft = $state('');
|
||||
let renameOriginalTitle = $state('');
|
||||
@@ -389,6 +391,7 @@
|
||||
bind:searchQuery
|
||||
class="px-2"
|
||||
isExpandedMode={innerWidth > 768 ? uiStore.isSidebarExpanded : true}
|
||||
onDiscoverModelsClick={() => (modelsDiscoverOpen = true)}
|
||||
onNewChat={() => {
|
||||
if (deviceStore.isMobile) {
|
||||
scheduleMobileCollapse();
|
||||
@@ -452,6 +455,8 @@
|
||||
|
||||
<DialogSettingsChat bind:open={settingsDialogOpen} />
|
||||
|
||||
<DialogModelsDiscover bind:open={modelsDiscoverOpen} />
|
||||
|
||||
<style>
|
||||
aside {
|
||||
@media (max-width: 768px) {
|
||||
|
||||
+38
-24
@@ -12,7 +12,7 @@
|
||||
SIDEBAR_ACTIONS_ITEMS
|
||||
} from '$lib/constants';
|
||||
import { SidebarAction, TooltipSide } from '$lib/enums';
|
||||
import { conversationsStore, deviceStore } from '$lib/stores';
|
||||
import { conversationsStore, deviceStore, serverStore } from '$lib/stores';
|
||||
import type { Component } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { circIn } from 'svelte/easing';
|
||||
@@ -26,6 +26,7 @@
|
||||
onSearchDeactivated?: () => void;
|
||||
onSearchClick?: () => void;
|
||||
onNewChat?: () => void;
|
||||
onDiscoverModelsClick?: () => void;
|
||||
onSettingsClick?: () => void;
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@
|
||||
class: className,
|
||||
isExpandedMode = false,
|
||||
isSearchModeActive = $bindable(false),
|
||||
onDiscoverModelsClick,
|
||||
onNewChat,
|
||||
onSearchClick,
|
||||
onSearchDeactivated,
|
||||
@@ -46,6 +48,14 @@
|
||||
|
||||
const isOnMobile = $derived(deviceStore.isMobile);
|
||||
|
||||
// Discover models needs the router's download endpoints; hide it in
|
||||
// single-model mode instead of opening a dialog that can only toast.
|
||||
const actionsItems = $derived(
|
||||
serverStore.isRouterMode
|
||||
? SIDEBAR_ACTIONS_ITEMS
|
||||
: SIDEBAR_ACTIONS_ITEMS.filter((item) => item.action !== SidebarAction.DISCOVER_MODELS)
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (isSearchModeActive && searchInputRef) {
|
||||
searchInputRef.focus();
|
||||
@@ -57,7 +67,7 @@
|
||||
|
||||
setTimeout(() => {
|
||||
initialized = true;
|
||||
}, ICON_STRIP_TRANSITION_DELAY_MULTIPLIER * SIDEBAR_ACTIONS_ITEMS.length);
|
||||
}, ICON_STRIP_TRANSITION_DELAY_MULTIPLIER * actionsItems.length);
|
||||
});
|
||||
|
||||
function handleSearchModeDeactivate() {
|
||||
@@ -107,7 +117,7 @@
|
||||
? 'hidden pointer-events-none'
|
||||
: ''}"
|
||||
>
|
||||
{#each SIDEBAR_ACTIONS_ITEMS as item, i (item.tooltip)}
|
||||
{#each actionsItems as item, i (item.tooltip)}
|
||||
{@const isActive = isItemActive(item)}
|
||||
{@const isSearchOnMobile = item.icon === Search && deviceStore.isMobile}
|
||||
{@const itemHref = isSearchOnMobile ? ROUTES.SEARCH : item.route}
|
||||
@@ -117,16 +127,18 @@
|
||||
onNewChat?.();
|
||||
void conversationsStore.openNewChat();
|
||||
}
|
||||
: item.action === SidebarAction.SETTINGS
|
||||
? () => onSettingsClick?.()
|
||||
: item.route
|
||||
? () => {
|
||||
onNewChat?.();
|
||||
goto(item.route!);
|
||||
}
|
||||
: isSearchOnMobile
|
||||
? undefined
|
||||
: onSearchClick}
|
||||
: item.action === SidebarAction.DISCOVER_MODELS
|
||||
? () => onDiscoverModelsClick?.()
|
||||
: item.action === SidebarAction.SETTINGS
|
||||
? () => onSettingsClick?.()
|
||||
: item.route
|
||||
? () => {
|
||||
onNewChat?.();
|
||||
goto(item.route!);
|
||||
}
|
||||
: isSearchOnMobile
|
||||
? undefined
|
||||
: onSearchClick}
|
||||
{@const itemTransition = {
|
||||
delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
|
||||
duration: ICON_STRIP_TRANSITION_DURATION,
|
||||
@@ -164,7 +176,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="{className} flex-col gap-1 hidden md:flex">
|
||||
{#each SIDEBAR_ACTIONS_ITEMS as item, i (item.tooltip)}
|
||||
{#each actionsItems as item, i (item.tooltip)}
|
||||
{@const isActive = isItemActive(item)}
|
||||
{@const isSearchOnMobile = item.icon === Search && deviceStore.isMobile}
|
||||
{@const itemOnClick =
|
||||
@@ -173,16 +185,18 @@
|
||||
onNewChat?.();
|
||||
void conversationsStore.openNewChat();
|
||||
}
|
||||
: item.action === SidebarAction.SETTINGS
|
||||
? () => onSettingsClick?.()
|
||||
: item.route
|
||||
? () => {
|
||||
onNewChat?.();
|
||||
goto(item.route!);
|
||||
}
|
||||
: isSearchOnMobile
|
||||
? undefined
|
||||
: onSearchClick}
|
||||
: item.action === SidebarAction.DISCOVER_MODELS
|
||||
? () => onDiscoverModelsClick?.()
|
||||
: item.action === SidebarAction.SETTINGS
|
||||
? () => onSettingsClick?.()
|
||||
: item.route
|
||||
? () => {
|
||||
onNewChat?.();
|
||||
goto(item.route!);
|
||||
}
|
||||
: isSearchOnMobile
|
||||
? undefined
|
||||
: onSearchClick}
|
||||
{@const itemTransition = {
|
||||
delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
|
||||
duration: ICON_STRIP_TRANSITION_DURATION,
|
||||
|
||||
@@ -11,18 +11,24 @@
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: 'sm' | 'default';
|
||||
size?: 'xs' | 'sm' | 'default';
|
||||
variant?: 'default' | 'plain';
|
||||
} = $props();
|
||||
|
||||
// Super small trigger: fits its selected value, for dense inline use.
|
||||
const xsClasses =
|
||||
"group flex h-6 w-fit items-center justify-between gap-1 rounded-md border border-input bg-transparent px-2 py-0 text-xs whitespace-nowrap outline-none select-none transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='text-'])]:text-muted-foreground";
|
||||
|
||||
const baseClasses = $derived(
|
||||
variant === 'plain'
|
||||
? "group inline-flex w-full items-center justify-end gap-2 whitespace-nowrap px-0 py-0 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3 [&_svg:not([class*='text-'])]:text-muted-foreground"
|
||||
: "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground"
|
||||
: size === 'xs'
|
||||
? xsClasses
|
||||
: "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground"
|
||||
);
|
||||
|
||||
const chevronClasses = $derived(
|
||||
variant === 'plain'
|
||||
variant === 'plain' || size === 'xs'
|
||||
? 'size-3 opacity-60 transition-transform group-data-[state=open]:-rotate-180'
|
||||
: 'size-4 opacity-50'
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const API_MODELS = {
|
||||
/** Download a model from HuggingFace (ROUTER mode, POST) or cancel/remove it (DELETE) */
|
||||
DELETE: '/models',
|
||||
DOWNLOAD: '/models',
|
||||
LIST: '/v1/models',
|
||||
LOAD: '/models/load',
|
||||
SSE: '/models/sse',
|
||||
|
||||
@@ -2,6 +2,12 @@ export const CLI_FLAGS = {
|
||||
AGENT: '--agent',
|
||||
API_KEY: '--api-key',
|
||||
MCP_PROXY: '--ui-mcp-proxy',
|
||||
/** Multimodal projector path; unlocks vision/audio for the model. */
|
||||
MMPROJ: '--mmproj',
|
||||
/** Draft model weights path (long form); the router records it per model. */
|
||||
MODEL_DRAFT: '--model-draft',
|
||||
/** Draft model weights path (short form). */
|
||||
MODEL_DRAFT_SHORT: '-md',
|
||||
SLOTS: '--slots',
|
||||
TOOLS: '--tools'
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* HuggingFace Hub constants.
|
||||
*
|
||||
* URLs, parsing regexes and formatting units for the HuggingFaceService.
|
||||
* Reference: https://huggingface.co/docs/huggingface_hub/package_reference/hf_api
|
||||
*/
|
||||
|
||||
// API endpoints
|
||||
|
||||
export const HF_BASE_URL = 'https://huggingface.co';
|
||||
export const HF_API_MODELS_URL = `${HF_BASE_URL}/api/models`;
|
||||
export const HF_AVATARS_URL = `${HF_BASE_URL}/api/avatars`;
|
||||
|
||||
// Query params
|
||||
|
||||
export const HF_FULL_DETAIL_PARAM = 'full=true';
|
||||
export const HF_RECURSIVE_TREE_PARAM = 'recursive=true';
|
||||
/** Search filter that restricts results to repos containing GGUF files. */
|
||||
export const HF_GGUF_FILTER = 'gguf';
|
||||
/** Repeatable `expand` query param selecting fields on the list endpoint. */
|
||||
export const HF_EXPAND_PARAM = 'expand';
|
||||
/**
|
||||
* Fields the model list endpoint omits by default but the discover list rows
|
||||
* render: `gguf` (chat template, context length, param count) drives the
|
||||
* reasoning / tool-use icons and the context badge, `siblings` the vision and
|
||||
* draft-sidecar badges. Without them those parts of a row stay empty.
|
||||
*/
|
||||
export const HF_MODEL_LIST_EXPAND: readonly string[] = [
|
||||
'author',
|
||||
'downloads',
|
||||
'gguf',
|
||||
'lastModified',
|
||||
'likes',
|
||||
'pipeline_tag',
|
||||
'siblings',
|
||||
// `base_model:` tags, so search rows can show the base org's avatar as the
|
||||
// main avatar with the quant org as the corner badge, like catalog rows.
|
||||
'tags'
|
||||
];
|
||||
|
||||
// Repo file conventions
|
||||
|
||||
export const HF_MAIN_BRANCH = 'main';
|
||||
export const HF_README_FILENAME = 'README.md';
|
||||
export const HF_RAW_PATH = 'raw';
|
||||
export const HF_TREE_PATH = 'tree';
|
||||
|
||||
// Pagination
|
||||
|
||||
export const HF_LINK_NEXT_REGEX = /<([^>]+)>;\s*rel="next"/;
|
||||
/** `Link` response header carrying the next page URL for cursor pagination. */
|
||||
export const HF_LINK_HEADER = 'Link';
|
||||
|
||||
// Fetch retry
|
||||
|
||||
export const HF_RETRY_ATTEMPTS = 3;
|
||||
export const HF_RETRY_DELAY_MS = 1000;
|
||||
export const HF_HTTP_NOT_FOUND = 404;
|
||||
export const HF_HTTP_SERVER_ERROR_MIN = 500;
|
||||
|
||||
// Search limits
|
||||
|
||||
export const HF_DEFAULT_LIMIT = 50;
|
||||
/** Safety cap on `/tree` pagination: more pages means a misbehaving endpoint. */
|
||||
export const HF_TREE_MAX_PAGES = 10;
|
||||
export const HF_MAX_LIMIT = 100;
|
||||
|
||||
// GGUF shard files
|
||||
|
||||
/** Matches a split-shard GGUF file name, e.g. `Model-00001-of-00015.gguf`. */
|
||||
export const HF_SHARD_REGEX = /-(\d{5})-of-(\d{5})\.gguf$/i;
|
||||
/** Index (1-based) of the first shard in a split-shard set. */
|
||||
export const HF_FIRST_SHARD = 1;
|
||||
/** Zero-padded width of the shard index in a split-shard file name. */
|
||||
export const HF_SHARD_PAD_WIDTH = 5;
|
||||
|
||||
// Quantization tokens
|
||||
|
||||
/** `UD-` (Unsloth Dynamic) custom quantization prefix, e.g. `UD-Q4_K_XL`. */
|
||||
export const HF_UD_QUANT_PREFIX = 'UD';
|
||||
export const HF_UD_QUANT_PREFIX_REGEX = /^UD-/i;
|
||||
/**
|
||||
* Segment marking an Unsloth `shared-` draft head that borrows the target
|
||||
* model's embedding/output weights, e.g. `...-shared-Q4_K_M.gguf`.
|
||||
*/
|
||||
export const HF_SHARED_DRAFT_TOKEN = 'shared';
|
||||
/**
|
||||
* Extracts the leading precision digits from a quant token, e.g.
|
||||
* `Q4_K_XL` -> 4, `IQ2_XXS` -> 2, `TQ1_0` -> 1, `BF16` -> 16.
|
||||
*/
|
||||
export const HF_QUANT_PRECISION_REGEX = /^(?:I?Q|TQ|BF|F|MXFP)?(\d+)/i;
|
||||
|
||||
// Model card tags
|
||||
|
||||
/** Matches the `base_model:` tag (plain or `quantized:`), capturing the repo id. */
|
||||
export const HF_BASE_MODEL_TAG_REGEX = /^base_model:(?:quantized:)?(.+)$/;
|
||||
export const HF_LICENSE_TAG_PREFIX = 'license:';
|
||||
export const HF_GATED_TAG = 'gated';
|
||||
export const HF_GGUF_TAG = 'gguf';
|
||||
export const HF_SAFETENSORS_TAG = 'safetensors';
|
||||
|
||||
// Pipeline tasks (logic use only - matching `pipeline_tag` values against tags)
|
||||
|
||||
/**
|
||||
* `pipeline_tag` values grouped by the input/output modality they imply, used
|
||||
* to derive a discover row's modality icons. A tag in more than one group (e.g.
|
||||
* `image-to-video`) lights up each modality it belongs to.
|
||||
*/
|
||||
export const HF_MODALITY_PIPELINE_TAGS: Readonly<
|
||||
Record<'audio' | 'video' | 'vision', readonly string[]>
|
||||
> = {
|
||||
audio: [
|
||||
'audio-classification',
|
||||
'audio-to-audio',
|
||||
'automatic-speech-recognition',
|
||||
'text-to-speech',
|
||||
'voice-activity-detection'
|
||||
],
|
||||
video: ['text-to-video', 'image-to-video', 'video-to-video'],
|
||||
vision: ['image-text-to-text', 'image-to-text', 'text-to-image', 'image-to-video']
|
||||
};
|
||||
|
||||
/** Filename token marking an mmproj sidecar sibling (unlocks vision / audio). */
|
||||
export const HF_MMPROJ_FILENAME_TOKEN = 'mmproj';
|
||||
|
||||
export const HF_TASK_TAGS: readonly string[] = [
|
||||
'audio-classification',
|
||||
'audio-to-audio',
|
||||
'automatic-speech-recognition',
|
||||
'conversational',
|
||||
'depth-estimation',
|
||||
'feature-extraction',
|
||||
'fill-mask',
|
||||
'image-classification',
|
||||
'image-feature-extraction',
|
||||
'image-segmentation',
|
||||
'image-text-to-text',
|
||||
'image-to-text',
|
||||
'image-to-video',
|
||||
'object-detection',
|
||||
'question-answering',
|
||||
'reinforcement-learning',
|
||||
'robotics',
|
||||
'sentence-similarity',
|
||||
'summarization',
|
||||
'text2text-generation',
|
||||
'text-classification',
|
||||
'text-generation',
|
||||
'text-to-image',
|
||||
'text-to-speech',
|
||||
'text-to-video',
|
||||
'token-classification',
|
||||
'translation',
|
||||
'video-to-video',
|
||||
'voice-activity-detection',
|
||||
'zero-shot-classification'
|
||||
];
|
||||
|
||||
// Formatting
|
||||
|
||||
export const BYTE = 1;
|
||||
export const KILOBYTE = 1_000;
|
||||
export const MEGABYTE = 1_000_000;
|
||||
export const GIGABYTE = 1_000_000_000;
|
||||
export const TERABYTE = 1_000_000_000_000;
|
||||
|
||||
/**
|
||||
* Matches a human size string (`177GB`, `1.2 TB`, `500MB`), capturing the
|
||||
* numeric value and its unit suffix. Used by `parseSizeBytes`.
|
||||
*/
|
||||
export const HF_SIZE_STRING_REGEX = /^\s*([\d.]+)\s*([a-z]+)\s*$/i;
|
||||
|
||||
/**
|
||||
* Byte multiplier for a size suffix (`k` kilobyte, `m` megabyte, ...) as used by
|
||||
* the llama.app catalog `size` strings, whose suffix is lowercase.
|
||||
*/
|
||||
export const HF_SIZE_SUFFIX_BYTES: Readonly<Record<string, number>> = {
|
||||
b: BYTE,
|
||||
g: GIGABYTE,
|
||||
k: KILOBYTE,
|
||||
m: MEGABYTE,
|
||||
t: TERABYTE
|
||||
};
|
||||
|
||||
export const BYTE_LABEL = 'B';
|
||||
export const KILOBYTE_LABEL = 'KB';
|
||||
export const MEGABYTE_LABEL = 'MB';
|
||||
export const GIGABYTE_LABEL = 'GB';
|
||||
|
||||
/** Count suffixes for compact number formatting, e.g. `1.5K`, `2.0M`. */
|
||||
export const KILO_LABEL = 'K';
|
||||
export const MEGA_LABEL = 'M';
|
||||
export const GIGA_LABEL = 'B';
|
||||
|
||||
// Relative time
|
||||
|
||||
export const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||
export const DAYS_PER_WEEK = 7;
|
||||
/** Rough month length in days, used to bucket relative timestamps. */
|
||||
export const DAYS_PER_MONTH = 30;
|
||||
export const DAYS_PER_YEAR = 365;
|
||||
|
||||
export const TODAY_LABEL = 'Today';
|
||||
export const YESTERDAY_LABEL = 'Yesterday';
|
||||
export const DAYS_AGO_LABEL = 'days ago';
|
||||
export const WEEKS_AGO_LABEL = 'weeks ago';
|
||||
export const MONTHS_AGO_LABEL = 'months ago';
|
||||
export const YEARS_AGO_LABEL = 'years ago';
|
||||
|
||||
// Cache paths
|
||||
|
||||
/**
|
||||
* Matches a local HF cache file path
|
||||
* (`.../models--<org>--<name>/snapshots/<sha>/<file>`), capturing the repo
|
||||
* directory name and the repo-relative file path.
|
||||
*/
|
||||
export const HF_CACHE_PATH_REGEX = /models--(.+?)\/snapshots\/[^/]+\/(.+)$/;
|
||||
/** Separator between org and name segments in an HF cache directory name. */
|
||||
export const HF_CACHE_DIR_SEPARATOR = '--';
|
||||
|
||||
// README
|
||||
|
||||
/** Matches a leading YAML frontmatter block (--- ... ---) in a markdown document. */
|
||||
export const HF_FRONTMATTER_REGEX = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
||||
|
||||
// Param counts
|
||||
|
||||
/**
|
||||
* Best-effort parameter count token in a model id/name, e.g. `27B` from
|
||||
* `Qwen3.8-27B-GGUF` or `300M` from `embeddinggemma-300M-GGUF`.
|
||||
*/
|
||||
export const HF_PARAM_COUNT_REGEX = /(?:^|[^a-z0-9])(\d+(?:[._]\d+)?)\s*([bm])(?![a-z0-9])/i;
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
Image as ImageIcon,
|
||||
Lightbulb as ReasoningIcon,
|
||||
Mic as AudioIcon,
|
||||
Video as VideoIcon
|
||||
Video as VideoIcon,
|
||||
Wrench as ToolUseIcon
|
||||
} from '@lucide/svelte';
|
||||
import { FileTypeCategory, ModelCapability, ModelModality } from '$lib/enums';
|
||||
import type { ModelCapabilities, ModelModalities } from '$lib/types/models';
|
||||
@@ -49,16 +50,19 @@ export const MODALITY_FLAG_KEYS: Record<
|
||||
};
|
||||
|
||||
export const CAPABILITY_ICONS: Record<ModelCapability, Component> = {
|
||||
[ModelCapability.REASONING]: ReasoningIcon
|
||||
[ModelCapability.REASONING]: ReasoningIcon,
|
||||
[ModelCapability.TOOL_USE]: ToolUseIcon
|
||||
} as const;
|
||||
|
||||
export const CAPABILITY_LABELS: Record<ModelCapability, string> = {
|
||||
[ModelCapability.REASONING]: 'Reasoning'
|
||||
[ModelCapability.REASONING]: 'Reasoning',
|
||||
[ModelCapability.TOOL_USE]: 'Tool use'
|
||||
} as const;
|
||||
|
||||
/** Maps a ModelCapability to the boolean flag it drives on the ModelCapabilities type */
|
||||
export const CAPABILITY_FLAG_KEYS: Record<ModelCapability, keyof ModelCapabilities> = {
|
||||
[ModelCapability.REASONING]: 'reasoning'
|
||||
[ModelCapability.REASONING]: 'reasoning',
|
||||
[ModelCapability.TOOL_USE]: 'tools'
|
||||
};
|
||||
|
||||
// Shared SVG icon strings for copy and preview buttons
|
||||
|
||||
@@ -45,6 +45,10 @@ export * from './message-export.constants';
|
||||
export * from './path-display.constants';
|
||||
export * from './model-id.constants';
|
||||
export * from './model-loading.constants';
|
||||
export * from './models-discover.constants';
|
||||
export * from './models-discover-download.constants';
|
||||
export * from './model-compatibility.constants';
|
||||
export * from './huggingface.constants';
|
||||
export * from './precision.constants';
|
||||
export * from './pwa.constants';
|
||||
export * from './routes.constants';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Model memory-fit constants.
|
||||
*
|
||||
* Mirrors the app's compatibility check (Model+Compatibility.swift):
|
||||
* budget = RAM x RAM_BUDGET_RATIO - RAM_OVERHEAD_MB
|
||||
* weightBytes = fileBytes x QUANT_WEIGHT
|
||||
* a file fits when weightBytes <= budget. Kept here so the estimation util and
|
||||
* any caller share one source.
|
||||
*/
|
||||
|
||||
/** Bytes in one mebibyte (MiB), used to convert a file size to MB. */
|
||||
export const MIB_BYTES = 1_048_576;
|
||||
|
||||
/** MB in one GB. */
|
||||
export const MB_PER_GB = 1024;
|
||||
|
||||
/** Overhead multiplier applied to the file size when estimating weight memory. */
|
||||
export const QUANT_WEIGHT = 1.05;
|
||||
|
||||
/** Share of RAM the app allows the model to occupy. */
|
||||
export const RAM_BUDGET_RATIO = 0.75;
|
||||
|
||||
/** Fixed RAM overhead (MB) reserved for the system and KV cache. */
|
||||
export const RAM_OVERHEAD_MB = 2048;
|
||||
|
||||
/**
|
||||
* Memory tiers (GB) covering the RAM sizes common machines ship with, in
|
||||
* small enough steps that the requirement reads honestly. Device-agnostic on
|
||||
* purpose: the server exposes no host RAM, so the UI presents the tier and
|
||||
* lets the user judge.
|
||||
*/
|
||||
export const MEM_TIERS = [4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024];
|
||||
@@ -2,35 +2,48 @@
|
||||
* Parsing of `org/ModelName[-tag][:quant]` style model IDs.
|
||||
*/
|
||||
|
||||
import { ModelAuxSidecar, ModelDraftSidecar } from '$lib/enums';
|
||||
|
||||
/** Any sidecar file type: a draft variant or an auxiliary sidecar like mmproj. */
|
||||
export type ModelSidecar = ModelDraftSidecar | ModelAuxSidecar;
|
||||
|
||||
/** All sidecar filename tokens: the bare lowercase enum values, e.g. `mtp`, `mmproj`. */
|
||||
export const SIDECAR_TOKENS: string[] = [
|
||||
...Object.values(ModelDraftSidecar),
|
||||
...Object.values(ModelAuxSidecar)
|
||||
];
|
||||
|
||||
/** Separator between token alternatives in the sidecar regexes. */
|
||||
const REGEX_ALTERNATION_SEPARATOR = '|';
|
||||
const SIDECAR_TOKEN_ALTERNATION = SIDECAR_TOKENS.join(REGEX_ALTERNATION_SEPARATOR);
|
||||
|
||||
export const MODEL_ID = {
|
||||
/**
|
||||
* Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`.
|
||||
* The leading `A`/`a` distinguishes it from a regular params segment.
|
||||
*/
|
||||
ACTIVATED_PARAMS_RE: /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/,
|
||||
ACTIVATED_PARAMS_REGEX: /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/,
|
||||
|
||||
/** Matches prefix for custom quantization types, e.g. `UD-Q8_K_XL`. */
|
||||
CUSTOM_QUANTIZATION_PREFIX_RE: /^UD$/i,
|
||||
CUSTOM_QUANTIZATION_PREFIX_REGEX: /^UD$/i,
|
||||
/** Container format segments to exclude from tags (every model uses these). */
|
||||
IGNORED_SEGMENTS: new Set(['GGUF', 'GGML']),
|
||||
/** Sentinel value returned by `indexOf` when a substring is not found. */
|
||||
NOT_FOUND: -1,
|
||||
|
||||
/** Separates `<org>` from `<model>` in a model ID, e.g. `org/ModelName`. */
|
||||
ORG_SEPARATOR: '/',
|
||||
|
||||
/**
|
||||
* Matches a parameter-count segment, e.g. `7B`, `1.5b`, `120M`.
|
||||
* The optional leading `E` covers effective-parameter sizes, e.g. Gemma's
|
||||
* `E2B`/`E4B` (MatFormer models sized by resident params).
|
||||
*/
|
||||
PARAMS_RE: /^[Ee]?\d+(\.\d+)?[BbMmKkTt]$/,
|
||||
PARAMS_REGEX: /^[Ee]?\d+(\.\d+)?[BbMmKkTt]$/,
|
||||
|
||||
/**
|
||||
* Matches a quantization/precision segment, e.g. `Q4_K_M`, `IQ4_XS`, `F16`, `BF16`, `MXFP4`.
|
||||
* Case-insensitive to handle both uppercase and lowercase inputs.
|
||||
*/
|
||||
QUANTIZATION_SEGMENT_RE: /^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i,
|
||||
QUANTIZATION_SEGMENT_REGEX: /^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i,
|
||||
|
||||
/** Separates the model path from the quantization tag, e.g. `model:Q4_K_M`. */
|
||||
QUANTIZATION_SEPARATOR: ':',
|
||||
@@ -38,6 +51,36 @@ export const MODEL_ID = {
|
||||
/** Separates named segments within the model path, e.g. `ModelName-7B-GGUF`. */
|
||||
SEGMENT_SEPARATOR: '-',
|
||||
|
||||
/**
|
||||
* Sidecar token between name segments, e.g. `Model-mtp-Q4_0.gguf`,
|
||||
* `model-eagle3-BF16.gguf`. Captures the name head and tail around the
|
||||
* token; same case-insensitive rule as the prefix form.
|
||||
*/
|
||||
SIDECAR_INFIX_REGEX: new RegExp(`^(.*)-(${SIDECAR_TOKEN_ALTERNATION})-(.+)$`, 'i'),
|
||||
|
||||
/**
|
||||
* Sidecar prefix that wraps a model id with a sidecar type, e.g.
|
||||
* `mtp-<name>.gguf`, `dflash-<name>.gguf`, `dspark-<name>.gguf`,
|
||||
* `eagle3-<name>.gguf`, `mmproj-<name>.gguf`. Captures the bare type
|
||||
* token for typed lookup.
|
||||
*
|
||||
* The token matches case-insensitively (real repos ship uppercase
|
||||
* heads, e.g. `Model-MTP-BF16.gguf`) and is normalized through
|
||||
* `sidecarFromFileToken`; the server's filename grammar
|
||||
* (common/download.cpp) matches the same segments.
|
||||
*/
|
||||
SIDECAR_PREFIX_REGEX: new RegExp(`^(${SIDECAR_TOKEN_ALTERNATION})-(.*)$`, 'i'),
|
||||
|
||||
/**
|
||||
* Trailing `-<type>` suffix marking a GGUF with an embedded draft in the
|
||||
* same weight file (MTP) or a sidecar download entry, e.g.
|
||||
* `Hy3-IQ1_M-mtp.gguf`, `Q4_K_M-dspark`. An optional `-draft` tail covers
|
||||
* standalone sidecar files, e.g. `Model-mtp-draft.gguf`. The captured
|
||||
* prefix is the candidate model id; the caller decides whether it looks
|
||||
* quantized. Case-insensitive, like the prefix form.
|
||||
*/
|
||||
SIDECAR_SUFFIX_REGEX: new RegExp(`^(.*)-(${SIDECAR_TOKEN_ALTERNATION})(-draft)?$`, 'i'),
|
||||
|
||||
/** Matches a trailing weight file extension, e.g. `model.gguf` -> `model`. */
|
||||
WEIGHT_EXTENSION_RE: /\.(gguf|ggml)$/i
|
||||
WEIGHT_EXTENSION_REGEX: /\.(gguf|ggml)$/i
|
||||
};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Models Discover - download options constants.
|
||||
*
|
||||
* Shared values backing the download-options area of the details pane: the
|
||||
* serve `--spec-type` mapping, the bit-depth buckets, and the standalone
|
||||
* command builder. Kept here (not component-local) so they sit with the rest
|
||||
* of the discover domain constants.
|
||||
*/
|
||||
|
||||
import type { ModelSidecar } from '$lib/constants';
|
||||
import { ModelAuxSidecar, ModelDraftSidecar } from '$lib/enums';
|
||||
|
||||
/**
|
||||
* `--spec-type` value for each draft sidecar; aux sidecars (mmproj, imatrix)
|
||||
* carry none and stay empty.
|
||||
*/
|
||||
export const SPEC_TYPE: Record<ModelSidecar, string> = {
|
||||
[ModelAuxSidecar.IMATRIX]: '',
|
||||
[ModelAuxSidecar.MMPROJ]: '',
|
||||
[ModelDraftSidecar.DFLASH]: 'draft-dflash',
|
||||
[ModelDraftSidecar.DSPARK]: 'draft-dspark',
|
||||
[ModelDraftSidecar.EAGLE3]: 'draft-eagle3',
|
||||
[ModelDraftSidecar.MTP]: 'draft-mtp'
|
||||
};
|
||||
|
||||
/** Bit-depth bucket for files that carry no quant token; rendered as "Other". */
|
||||
export const OTHER_BIT_DEPTH = 99;
|
||||
|
||||
/** Label for the OTHER_BIT_DEPTH bucket. */
|
||||
export const OTHER_BIT_DEPTH_LABEL = 'Other';
|
||||
|
||||
/** Suffix appended after a bit-depth number to label a row, e.g. `4-bit`. */
|
||||
export const BIT_DEPTH_LABEL_SUFFIX = '-bit';
|
||||
|
||||
/**
|
||||
* Fixed tokens of the standalone `llama serve` command preview, so the command
|
||||
* builder and its rendered spans share one source of the CLI spelling.
|
||||
*/
|
||||
export const SERVE_COMMAND = {
|
||||
BIN: 'llama',
|
||||
/** Draft model repo:tag argument (download sidecar / draft weights). */
|
||||
DRAFT_FLAG: '-hfd',
|
||||
/** Main model repo:tag argument. */
|
||||
MODEL_FLAG: '-hf',
|
||||
/** Speculative-decoding type argument for the draft. */
|
||||
SPEC_TYPE_FLAG: '--spec-type',
|
||||
SUBCOMMAND: 'serve'
|
||||
} as const;
|
||||
|
||||
/** Bit depth preferred for the command's default base quant. */
|
||||
export const DEFAULT_BASE_BIT_DEPTH = 4;
|
||||
|
||||
/** Label shown for a draft-sidecar chip whose quant is unknown. */
|
||||
export const DRAFT_FILE_LABEL = 'draft';
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Models discover constants.
|
||||
*
|
||||
* Endpoints and settings for the Models Discover dialog.
|
||||
*/
|
||||
|
||||
/** llama.app model catalog used as the default model list. Online-only source; the discover feature requires an internet connection anyway. */
|
||||
export const MODELS_DISCOVER_CATALOG_URL = 'https://llama.app/v1/catalog.json';
|
||||
|
||||
/**
|
||||
* Catalog repos fetched in parallel per batch; small on purpose so the HF API
|
||||
* is not hit with the whole catalog at once.
|
||||
*/
|
||||
export const MODELS_DISCOVER_CATALOG_BATCH = 4;
|
||||
@@ -15,6 +15,9 @@ export const STORAGE_APP_NAME_DEPRECATED = 'LlamaCppWebui';
|
||||
export const DB_APP_NAME_DEPRECATED = 'LlamacppWebui';
|
||||
|
||||
export const ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.alwaysAllowedTools`;
|
||||
|
||||
/** Paused model download ids (`<repo>:<tag>`), restored on the next page load. */
|
||||
export const PAUSED_MODEL_DOWNLOADS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.pausedModelDownloads`;
|
||||
export const CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.config`;
|
||||
export const DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledTools`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Package, Search, Settings, SquarePen } from '@lucide/svelte';
|
||||
import { Package, PackageSearch, Search, Settings, SquarePen } from '@lucide/svelte';
|
||||
import { SidebarAction, ToolSource } from '$lib/enums';
|
||||
import type { DesktopIconStripItem } from '$lib/types';
|
||||
|
||||
@@ -44,6 +44,9 @@ export const STATS_UNITS = {
|
||||
|
||||
export const DEFAULT_MOBILE_BREAKPOINT = 768;
|
||||
|
||||
/** Orgs whose avatar is dark and needs inverting in dark mode. */
|
||||
export const DARK_INVERT_AVATAR_ORGS = ['openai'];
|
||||
|
||||
/** Icon used for the model selector and the `/model` slash command. */
|
||||
export const MODEL_SELECTOR_ICON = Package;
|
||||
|
||||
@@ -61,6 +64,11 @@ export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
|
||||
tooltip: 'New chat'
|
||||
},
|
||||
{ icon: Search, keys: ['cmd', 'k'], tooltip: 'Search' },
|
||||
{
|
||||
action: SidebarAction.DISCOVER_MODELS,
|
||||
icon: PackageSearch,
|
||||
tooltip: 'Discover models'
|
||||
},
|
||||
{
|
||||
action: SidebarAction.SETTINGS,
|
||||
icon: Settings,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* HuggingFace Hub enums.
|
||||
*
|
||||
* Values mirror the strings used by the HF REST API
|
||||
* (https://huggingface.co/docs/huggingface_hub/package_reference/hf_api)
|
||||
* so they can be sent and compared directly.
|
||||
*/
|
||||
|
||||
/** Sort field for /api/models search queries. */
|
||||
export enum HfModelSort {
|
||||
CREATED_AT = 'createdAt',
|
||||
DOWNLOADS = 'downloads',
|
||||
LAST_MODIFIED = 'lastModified',
|
||||
LIKES = 'likes',
|
||||
TRENDING_SCORE = 'trendingScore'
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the sidecar token (`mtp` / `dflash` / `mmproj` / ...) sits in the
|
||||
* filename.
|
||||
* - `prefix` sidecar file that lives next to the main weights, e.g. `mtp-Q4_0.gguf`
|
||||
* - `suffix` embedded draft baked into the main weights, e.g. `Hy3-IQ1_M-mtp.gguf`
|
||||
* - `infix` standalone sidecar named between head and quant, e.g. `model-mtp-Q8_0.gguf`
|
||||
*/
|
||||
export enum SidecarForm {
|
||||
INFIX = 'infix',
|
||||
PREFIX = 'prefix',
|
||||
SUFFIX = 'suffix'
|
||||
}
|
||||
|
||||
/** Entry type in a model repository file tree (`/tree` responses). */
|
||||
export enum HfEntryType {
|
||||
DIRECTORY = 'directory',
|
||||
FILE = 'file'
|
||||
}
|
||||
@@ -57,6 +57,8 @@ export {
|
||||
SpecialFileType
|
||||
} from './files.enums';
|
||||
|
||||
export { HfEntryType, HfModelSort, SidecarForm } from './huggingface.enums';
|
||||
|
||||
export {
|
||||
MCPConnectionPhase,
|
||||
MCPLogLevel,
|
||||
@@ -67,7 +69,15 @@ export {
|
||||
JsonSchemaType
|
||||
} from './mcp.enums';
|
||||
|
||||
export { ModelCapability, ModelModality } from './model.enums';
|
||||
export {
|
||||
ModelAuxSidecar,
|
||||
ModelCapability,
|
||||
ModelDraftSidecar,
|
||||
ModelModality,
|
||||
ModelSelectableFileKind
|
||||
} from './model.enums';
|
||||
|
||||
export { ModelDownloadConfirmAction, ModelDownloadStopRequest } from './model.enums';
|
||||
|
||||
export { ServerRole, ServerModelStatus, ServerModelsSseEventType } from './server.enums';
|
||||
|
||||
|
||||
@@ -6,5 +6,62 @@ export enum ModelModality {
|
||||
}
|
||||
|
||||
export enum ModelCapability {
|
||||
REASONING = 'REASONING'
|
||||
REASONING = 'reasoning',
|
||||
TOOL_USE = 'tools'
|
||||
}
|
||||
|
||||
/**
|
||||
* Speculative-decoding draft sidecars (server spec-type draft-*).
|
||||
* Filenames use the lowercase token, e.g. `mtp-<name>.gguf` or `-mtp` suffix.
|
||||
*/
|
||||
export enum ModelDraftSidecar {
|
||||
/** DFlash block-diffusion draft (spec-type draft-dflash). */
|
||||
DFLASH = 'dflash',
|
||||
/** DSpark block-diffusion draft (spec-type draft-dspark). */
|
||||
DSPARK = 'dspark',
|
||||
/** EAGLE-3 speculative draft (spec-type draft-eagle3). */
|
||||
EAGLE3 = 'eagle3',
|
||||
/** Multi-token-prediction draft head (spec-type draft-mtp). */
|
||||
MTP = 'mtp'
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-draft sidecar file types. A sidecar is any auxiliary GGUF file
|
||||
* accompanying the main model weights.
|
||||
*/
|
||||
export enum ModelAuxSidecar {
|
||||
/** Importance-matrix data used to build imatrix quants; not loaded at serve time. */
|
||||
IMATRIX = 'imatrix',
|
||||
/** Multimodal projector: unlocks vision and/or audio input modalities. */
|
||||
MMPROJ = 'mmproj'
|
||||
}
|
||||
|
||||
/**
|
||||
* Role of a selectable GGUF in the download options: the main weights, a
|
||||
* speculative-decoding draft sidecar, or an auxiliary sidecar (mmproj).
|
||||
*/
|
||||
export enum ModelSelectableFileKind {
|
||||
AUX = 'aux',
|
||||
DRAFT = 'draft',
|
||||
MAIN = 'main'
|
||||
}
|
||||
|
||||
/**
|
||||
* Why an in-flight download is being stopped, so the terminal `download_failed`
|
||||
* feed event can be attributed: a user pause (resumable) or a user cancel
|
||||
* (discard). Distinguishes these from a genuine download failure.
|
||||
*/
|
||||
export enum ModelDownloadStopRequest {
|
||||
CANCEL = 'cancel',
|
||||
PAUSE = 'pause'
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructive download action the user is asked to confirm: stop and discard an
|
||||
* in-flight download, or delete an already-downloaded model from disk. Both
|
||||
* resolve through the same store removal call, differing only in the copy.
|
||||
*/
|
||||
export enum ModelDownloadConfirmAction {
|
||||
CANCEL = 'cancel',
|
||||
DELETE = 'delete'
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user