From 2a45068461a47d8c8c262ea84e1d304186e1f962 Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Mon, 26 Oct 2026 18:30:16 +0100 Subject: [PATCH] The explorer lists what this cluster's hardware holds Judged by the nodes as they are now, through modelplan.Options, the planner that serving a model uses: GGUF at Q4_K_M from the parameter count, safetensors at the size the registry's dtype breakdown gives (packed AWQ, GPTQ and MXFP4 at four bits), FP8 only where a card has compute capability 8.9, MLX safetensors never. A picture model is judged against one card. A cluster with cards asks the registry for safetensors too, a hundred at a time; one without asks for GGUF only. The page says what it judged by: the largest card, its capability, the most RAM on one node. On a repository's page a file set no node holds is marked with what it needs, against what the best node offers, and has no Fetch button. Tried against the PoC's seven nodes and Hugging Face's trending hundred: 63 shown, the rest too big for one 24 GB Turing card or FP8. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 + docs/quick-install.md | 8 +- internal/control/adminapi/api.go | 7 +- internal/hf/hf.go | 70 ++++++++- internal/web/admin_mirror.go | 6 + internal/web/explore_fit.go | 181 +++++++++++++++++++++++ internal/web/explore_fit_test.go | 145 ++++++++++++++++++ internal/web/locales/en/mirror.json | 16 +- internal/web/locales/tr/mirror.json | 10 +- internal/web/mirror_explore.go | 24 ++- internal/web/templates/admin_mirror.html | 1 + 11 files changed, 458 insertions(+), 18 deletions(-) create mode 100644 internal/web/explore_fit.go create mode 100644 internal/web/explore_fit_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf6cb7..d307a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,14 @@ APIs change between minors without a deprecation period. ### Added +- **The explorer lists what this cluster can run.** Judged by the nodes + as they are now — every card's memory and compute capability, every + node's RAM — through the same planner as serving a model, rather than a + size picked from a list. A model too big for any node, in FP8 without a + card of 8.9, in MLX, or in neither GGUF nor safetensors, is left out and + counted; the page says what it judged by. With cards, safetensors repositories are + listed beside GGUF. On a repository's page a file too big for the + cluster is shown with what it needs and no Fetch button. - **Weights that run code are refused.** Python pickles — `.bin`, `.pt`, `.pth`, `.ckpt` — run whatever they hold when loaded, which is how the malicious models on Hugging Face work. The node refuses such a source, diff --git a/docs/quick-install.md b/docs/quick-install.md index 49ef7e6..c3a5561 100644 --- a/docs/quick-install.md +++ b/docs/quick-install.md @@ -146,7 +146,13 @@ agent is stopped. ## 4. Add a model Open `/admin/mirror` and choose **Explore models**. It lists what is popular on -Hugging Face, showing only what your nodes can run. +Hugging Face, showing only what your nodes can run: judged by the cluster as it +is now — the largest card and its compute capability, the most memory on one +node — so a model too big for them, in a form those cards lack (FP8 needs 8.9), +or in neither GGUF nor safetensors is left out, and the page says how many. +Without GPUs it lists GGUF only. Untick **only what these nodes can run** to +see everything. On a model's page a file too big for the cluster is shown +without a **Fetch** button, with what it would need. 1. Click a model to read its card. 2. Pick one of its files, usually a `q4_k_m` for a first try, and click diff --git a/internal/control/adminapi/api.go b/internal/control/adminapi/api.go index f9cff98..5c6a7e4 100644 --- a/internal/control/adminapi/api.go +++ b/internal/control/adminapi/api.go @@ -1829,9 +1829,12 @@ func (a *API) setOrigin(w http.ResponseWriter, r *http.Request) { // PoolNodes is the fleet's ready nodes as a plan sees them: their cards, // which of them are free, the fabric they share, and their memory. -func (a *API) PoolNodes() []modelplan.PoolNode { +func (a *API) PoolNodes() []modelplan.PoolNode { return PoolNodesOf(a.Nodes()) } + +// PoolNodesOf is nodes as a plan sees them, the ready ones only. +func PoolNodesOf(nodes []NodeView) []modelplan.PoolNode { var out []modelplan.PoolNode - for _, n := range a.Nodes() { + for _, n := range nodes { if n.State != "ready" { continue } diff --git a/internal/hf/hf.go b/internal/hf/hf.go index f68adb6..7168570 100644 --- a/internal/hf/hf.go +++ b/internal/hf/hf.go @@ -18,6 +18,7 @@ import ( "net/http" "net/url" "regexp" + "slices" "strings" "sync" "time" @@ -69,15 +70,78 @@ type Model struct { // for one that has none. GGUF *GGUFInfo `json:"gguf"` // Safetensors is the registry's summary of original weights. - Safetensors *struct { - Total int64 `json:"total"` - } `json:"safetensors,omitempty"` + Safetensors *SafetensorsInfo `json:"safetensors,omitempty"` // Siblings are the repository's files, when asked for with WithFiles. Siblings []struct { RFilename string `json:"rfilename"` } `json:"siblings,omitempty"` } +// SafetensorsInfo is the registry's summary of a repository's safetensors. +type SafetensorsInfo struct { + Total int64 `json:"total"` + // Parameters counts them by dtype: {"BF16": 8e9}. A packed quantisation + // (AWQ, GPTQ, MLX) is counted as its I32 or U32 at the count of weights + // it unpacks to, not of the integers that hold them. + Parameters map[string]int64 `json:"parameters,omitempty"` +} + +// dtypeBytes is what one parameter of a safetensors dtype takes. +var dtypeBytes = map[string]float64{ + "F64": 8, "I64": 8, "U64": 8, "F32": 4, "BF16": 2, "F16": 2, "I16": 2, "U16": 2, + "F8_E4M3": 1, "F8_E5M2": 1, "F8_E8M0": 1, "I8": 1, "U8": 1, "BOOL": 1, +} + +// WeightBytes is about how much the safetensors weights take: each dtype's +// count at its size, and a packed quantisation at 4 bits a weight, 8 for +// one tagged 8-bit — I32 or U32 always, U8 for MXFP4 and NVFP4, which the +// registry counts at the weights they unpack to. 0 when the registry has +// no summary. +func (m Model) WeightBytes() int64 { + if m.Safetensors == nil { + return 0 + } + if len(m.Safetensors.Parameters) == 0 { + return m.Safetensors.Total * 2 + } + packed := 0.5 + if m.HasTag("8-bit") { + packed = 1 + } + fp4 := m.HasTag("mxfp4") || m.HasTag("nvfp4") + var b float64 + for dt, n := range m.Safetensors.Parameters { + switch per, ok := dtypeBytes[dt]; { + case dt == "U8" && fp4: + b += float64(n) * 0.5 + case ok: + b += float64(n) * per + case dt == "I32" || dt == "U32": + b += float64(n) * packed + default: + b += float64(n) * 2 + } + } + return int64(b) +} + +// FP8 reports whether the safetensors weights are in an 8-bit float, which +// vLLM runs only on cards of compute capability 8.9 or newer. +func (m Model) FP8() bool { + if m.Safetensors == nil { + return false + } + for dt := range m.Safetensors.Parameters { + if strings.HasPrefix(dt, "F8_") { + return true + } + } + return false +} + +// HasTag reports whether the registry tags the repository so. +func (m Model) HasTag(tag string) bool { return slices.Contains(m.Tags, tag) } + // GGUFInfo is the registry's summary of a repository's GGUF weights. type GGUFInfo struct { // Total is the parameter count. diff --git a/internal/web/admin_mirror.go b/internal/web/admin_mirror.go index 88c19db..e8ef983 100644 --- a/internal/web/admin_mirror.go +++ b/internal/web/admin_mirror.go @@ -806,6 +806,7 @@ func (p *AccountPages) openRepo(r *http.Request, d *adminMirrorData, repo string } weights = append(weights, f) } + fit := p.fitOf() for _, set := range hf.Group(weights) { rs := repoSet{ FileSet: set, @@ -816,6 +817,11 @@ func (p *AccountPages) openRepo(r *http.Request, d *adminMirrorData, repo string Version: got.ShortRevision(), } rs.Verdict = p.verdictFor(langOf(r), got.Architecture, set.Name) + if fit != nil && rs.Verdict.Servable { + if v, ok := fit.holds(langOf(r), set.Name, set.Bytes, got.Params); !ok { + rs.Verdict = v + } + } for _, f := range set.Files { rs.Sources = append(rs.Sources, repoSource{URL: got.DownloadURL("", f.Path), Path: f.Path}) diff --git a/internal/web/explore_fit.go b/internal/web/explore_fit.go new file mode 100644 index 0000000..d95cd44 --- /dev/null +++ b/internal/web/explore_fit.go @@ -0,0 +1,181 @@ +package web + +import ( + "slices" + "strconv" + "strings" + + "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" + "gitea.mynodes.xyz/ilker/llmgw/internal/gguf" + "gitea.mynodes.xyz/ilker/llmgw/internal/hf" + "gitea.mynodes.xyz/ilker/llmgw/internal/modelplan" +) + +// The explorer lists what this cluster can run, judged by the nodes it has +// now rather than by a size picked from a list: a site with H100s sees the +// 70B models, one with a T4 and CPUs sees what fits there. From the +// registry's listing alone — a parameter count, safetensors by dtype, tags +// — so no repository is opened to decide; the repository's page sizes each +// file exactly. + +// clusterFit is the hardware a listing is judged against. +type clusterFit struct { + pool []modelplan.PoolNode + // fp8 are the nodes with a card vLLM runs FP8 weights on, compute + // capability 8.9 or newer, as the agent requires. + fp8 []modelplan.PoolNode + // card is the largest card's memory in GB, cc its compute capability; + // host the most memory one node has. + card int + cc string + host int +} + +// fp8Capability is the oldest card vLLM runs FP8 weights on. +const fp8Capability = 8.9 + +// fitOf is the cluster's hardware now, nil when there is none to judge by: +// no fleet to ask, or no node ready. +func (p *AccountPages) fitOf() *clusterFit { + if p.fleet == nil { + return nil + } + nodes := p.fleet.Nodes() + c := &clusterFit{pool: adminapi.PoolNodesOf(nodes)} + if len(c.pool) == 0 { + return nil + } + newest := map[string]float64{} + for _, n := range nodes { + for _, s := range n.ComputeCapabilities { + if v, err := strconv.ParseFloat(s, 64); err == nil && v > newest[n.ID] { + newest[n.ID] = v + } + } + } + for _, n := range c.pool { + c.host = max(c.host, n.HostMemGB) + if n.GPUs > 0 && n.GPUMemGB >= c.card { + if n.GPUMemGB > c.card || newest[n.ID] > atof(c.cc) { + c.cc = strconv.FormatFloat(newest[n.ID], 'f', 1, 64) + } + c.card = n.GPUMemGB + } + if newest[n.ID] >= fp8Capability { + c.fp8 = append(c.fp8, n) + } + } + return c +} + +func atof(s string) float64 { v, _ := strconv.ParseFloat(s, 64); return v } + +// gpus reports whether any node has a card. +func (c *clusterFit) gpus() bool { return c.card > 0 } + +// pictureTasks are a pipeline ComfyUI runs, on a card, rather than an +// engine for text. +var pictureTasks = []string{"text-to-image", "image-to-image", "image-to-3d", "text-to-3d", + "text-to-video", "image-to-video", "unconditional-image-generation"} + +// ggufBitsPerWeight is the quantisation a GGUF repository is judged by: +// Q4_K_M, the smallest of those published that answers as well as most +// people want. A repository of Q8 only is judged kindly; its page says. +const ggufBitsPerWeight = 4.8 + +// runs reports whether some node here could hold the repository's model. +func (c *clusterFit) runs(m hf.Model) bool { + params := m.Params() + if slices.Contains(pictureTasks, m.PipelineTag) || m.HasTag("diffusers") { + b := m.WeightBytes() + if b == 0 { + b = params * 2 + } + // ComfyUI holds the model on one card, with room for its working. + return c.gpus() && float64(b+2<<30) <= float64(c.card)*0.9*(1<<30) + } + if m.GGUF != nil || m.HasTag("gguf") && m.Safetensors == nil { + if params == 0 { + return true + } + b := int64(float64(params) * ggufBitsPerWeight / 8) + if anyFits(modelplan.Options([]modelplan.Artifact{{Name: "m-Q4_K_M.gguf", Format: "gguf", Bytes: b, + Quant: "Q4_K_M"}}, modelplan.Shape{Params: params}, c.pool, 0)) { + return true + } + } + // MLX safetensors are Apple's, which no engine here reads; a repository + // tagged so may still carry a GGUF, judged above. + if (m.Safetensors != nil || m.HasTag("safetensors")) && !m.HasTag("mlx") { + // vLLM, which runs on cards only. + pool := c.pool + if m.FP8() { + pool = c.fp8 + } + if !c.gpus() || len(pool) == 0 { + return false + } + b := m.WeightBytes() + if b == 0 { + if params == 0 { + return true + } + b = params * 2 + } + return anyFits(modelplan.Options([]modelplan.Artifact{{Name: "m.safetensors", Format: "safetensors", + Bytes: b}}, modelplan.Shape{Params: params}, pool, 0)) + } + return false +} + +func anyFits(opts []modelplan.Option) bool { + return slices.ContainsFunc(opts, modelplan.Option.Fits) +} + +// note is what the listing was judged by, for the page to say. +func (c *clusterFit) note(l *Lang) string { + if !c.gpus() { + return l.T("explore.fit_cpu", c.host) + } + return l.T("explore.fit", c.card, c.cc, c.host) +} + +// holds judges one file set of an open repository, whose size is known: a +// verdict that it cannot be served here, and false, when no node holds it. +func (c *clusterFit) holds(l *Lang, name string, bytes, params int64) (gguf.Verdict, bool) { + format := "" + switch lower := strings.ToLower(name); { + case strings.HasSuffix(lower, ".gguf"): + format = "gguf" + case strings.HasSuffix(lower, ".safetensors"): + format = "safetensors" + } + if format == "" || bytes <= 0 { + return gguf.Verdict{}, true + } + opts := modelplan.Options([]modelplan.Artifact{{Name: name, Format: format, Bytes: bytes, + Quant: modelplan.QuantOf(name)}}, modelplan.Shape{Params: params}, c.pool, 0) + if anyFits(opts) { + return gguf.Verdict{}, true + } + if format == "safetensors" && !c.gpus() { + return gguf.Verdict{Checked: true, Reason: l.T("card.no_gpu")}, false + } + if len(opts) == 0 { + // No width of any node's cards holds it, so none is offered: what + // the weights take against all the cards one node has. + have := 0 + for _, n := range c.pool { + have = max(have, n.GPUs*n.GPUMemGB) + } + return gguf.Verdict{Checked: true, Reason: l.T("card.too_big", + float64(bytes)/(1<<30), float64(have)*0.9)}, false + } + best := opts[0] + for _, o := range opts[1:] { + if o.HaveGB-o.NeedGB > best.HaveGB-best.NeedGB { + best = o + } + } + return gguf.Verdict{Checked: true, Reason: l.T("card.too_big", best.NeedGB, best.HaveGB)}, false +} diff --git a/internal/web/explore_fit_test.go b/internal/web/explore_fit_test.go new file mode 100644 index 0000000..96cdf41 --- /dev/null +++ b/internal/web/explore_fit_test.go @@ -0,0 +1,145 @@ +package web + +import ( + "strings" + "testing" + "time" + + "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" + "gitea.mynodes.xyz/ilker/llmgw/internal/hf" +) + +// nodesFleet is a cluster of the given nodes, all ready. +type nodesFleet struct { + fakeFleet + nodes []adminapi.NodeView +} + +func (f nodesFleet) Nodes() []adminapi.NodeView { return f.nodes } + +func gpuNode(id, class string, cards int, cc string, ramGB int) adminapi.NodeView { + return adminapi.NodeView{ID: id, State: "ready", GPUTotal: cards, GPUFree: cards, MemoryGB: ramGB, + Classes: []string{class}, ClassGPUs: []adminapi.ClassGPUs{{Class: class, Total: cards, Free: cards}}, + ComputeCapabilities: []string{cc}} +} + +func cpuNode(id string, ramGB int) adminapi.NodeView { + return adminapi.NodeView{ID: id, State: "ready", MemoryGB: ramGB} +} + +func stModel(id string, params map[string]int64, tags ...string) hf.Model { + var total int64 + for dt, n := range params { + if dt != "I32" { + total += n + } + } + return hf.Model{ID: id, CreatedAt: time.Now(), LastModified: time.Now(), PipelineTag: "text-generation", + Tags: append([]string{"safetensors"}, tags...), Safetensors: &hf.SafetensorsInfo{Total: total, Parameters: params}} +} + +// listing is what the explorer shows of these repositories on this cluster. +func listing(t *testing.T, nodes []adminapi.NodeView, ms ...hf.Model) (string, *fakeRegistry) { + t.Helper() + reg := &fakeRegistry{results: ms} + mux := mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg, Fleet: nodesFleet{nodes: nodes}}) + return mirrorGet(t, mux, "/admin/mirror?tab=explore").Body.String(), reg +} + +func TestTheExplorerListsWhatThisClustersHardwareHolds(t *testing.T) { + // The PoC: one Turing card of 24 GB, CPU nodes of up to 31 GB. + poc := []adminapi.NodeView{gpuNode("cuda01", "6000-24g", 1, "7.5", 251), cpuNode("node05", 31)} + body, reg := listing(t, poc, + ggufModel("a/Small-GGUF", "qwen3", 8e9, time.Now()), + ggufModel("a/Huge-GGUF", "qwen3moe", 671e9, time.Now()), + stModel("a/Eight-BF16", map[string]int64{"BF16": 8e9}), + stModel("a/ThirtyTwo-BF16", map[string]int64{"BF16": 32e9}), + stModel("a/Eight-FP8", map[string]int64{"F8_E4M3": 8e9, "BF16": 1e8}), + stModel("a/ThirtyTwo-AWQ", map[string]int64{"BF16": 1e9, "I32": 31e9}, "awq", "4-bit"), + stModel("a/Eight-MLX", map[string]int64{"U32": 8e9}, "mlx", "8-bit"), + ) + if reg.query.GGUF || reg.query.Limit != 100 { + t.Errorf("with a card, safetensors are asked for too, more of them: %+v", reg.query) + } + for id, want := range map[string]bool{ + "a/Small-GGUF": true, "a/Huge-GGUF": false, + "a/Eight-BF16": true, "a/ThirtyTwo-BF16": false, + "a/Eight-FP8": false, "a/ThirtyTwo-AWQ": true, "a/Eight-MLX": false, + } { + if got := strings.Contains(body, id+"<") || strings.Contains(body, id+"\""); got != want { + t.Errorf("%s listed = %v, want %v", id, got, want) + } + } + if !strings.Contains(body, "largest card has 24 GB (compute capability 7.5)") || + !strings.Contains(body, "4 more are hidden") { + t.Errorf("the page should say what it judged by and how many it left out: %s", body) + } +} + +func TestANewerClusterSeesTheModelsItCanRun(t *testing.T) { + // A customer's: eight H100s. What the PoC cannot run, this one lists. + h100 := []adminapi.NodeView{gpuNode("gpu01", "h100-80g", 8, "9.0", 1024)} + body, _ := listing(t, h100, + stModel("a/ThirtyTwo-BF16", map[string]int64{"BF16": 32e9}), + stModel("a/Eight-FP8", map[string]int64{"F8_E4M3": 8e9, "BF16": 1e8}), + ggufModel("a/Huge-GGUF", "qwen3moe", 671e9, time.Now()), + ) + for _, id := range []string{"a/ThirtyTwo-BF16", "a/Eight-FP8", "a/Huge-GGUF"} { + if !strings.Contains(body, id) { + t.Errorf("%s should be listed on eight H100s", id) + } + } +} + +func TestACPUOnlyClusterIsShownGGUFOnly(t *testing.T) { + body, reg := listing(t, []adminapi.NodeView{cpuNode("c1", 32)}, + ggufModel("a/Small-GGUF", "qwen3", 8e9, time.Now()), + ggufModel("a/Big-GGUF", "qwen3", 70e9, time.Now()), + stModel("a/Eight-BF16", map[string]int64{"BF16": 8e9}), + ) + if !reg.query.GGUF { + t.Errorf("without cards only GGUF runs, and should be asked for: %+v", reg.query) + } + if !strings.Contains(body, "a/Small-GGUF") || strings.Contains(body, "a/Big-GGUF") || strings.Contains(body, "a/Eight-BF16") { + t.Errorf("a 32 GB CPU node holds the 8B GGUF only: %s", body) + } + if !strings.Contains(body, "no GPUs, so GGUF only") { + t.Error("the page should say it judged by a cluster without cards") + } +} + +func TestARepositorysFileTooBigForThisClusterIsNotOffered(t *testing.T) { + poc := []adminapi.NodeView{gpuNode("cuda01", "6000-24g", 1, "7.5", 251), cpuNode("node05", 31)} + reg := &fakeRegistry{repo: hf.Repo{ID: "a/M", Revision: "abc123def456", Params: 32e9, + Files: []hf.File{ + {Path: "M-Q4_K_M.gguf", Bytes: 19 << 30}, + {Path: "model-00001-of-00002.safetensors", Bytes: 32 << 30}, + {Path: "model-00002-of-00002.safetensors", Bytes: 32 << 30}, + }}} + mux := mirrorPageLlama(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg, Fleet: nodesFleet{nodes: poc}}) + body := mirrorGet(t, mux, "/admin/mirror?repo=a/M").Body.String() + if !strings.Contains(body, "/resolve/abc123def456/M-Q4_K_M.gguf") { + t.Errorf("the GGUF fits cuda01's RAM and should be offered: %s", body) + } + if strings.Contains(body, "/resolve/abc123def456/model-00001-of-00002.safetensors") || + !strings.Contains(body, "too big for this cluster") { + t.Errorf("64 GB of safetensors on one 24 GB card should be marked, not offered: %s", body) + } +} + +func TestPackedWeightsAreSizedAsTheyAreStored(t *testing.T) { + // gpt-oss-20b: MXFP4 in U8, counted by the registry at the weights it + // unpacks to. At a byte each it read as 22 GB and missed a 24 GB card. + oss := stModel("openai/gpt-oss-20b", map[string]int64{"BF16": 1804459584, "U8": 19110297600}, "8-bit", "mxfp4") + if gb := float64(oss.WeightBytes()) / (1 << 30); gb < 11 || gb > 13 { + t.Errorf("gpt-oss-20b sized %.1f GB, want about 12", gb) + } + // Tagged mlx, but with a GGUF too: the GGUF runs. + both := ggufModel("a/Both", "qwen3", 27e9, time.Now()) + both.Tags = []string{"mlx", "safetensors", "gguf"} + poc := []adminapi.NodeView{gpuNode("cuda01", "6000-24g", 1, "7.5", 251)} + body, _ := listing(t, poc, oss, both) + if !strings.Contains(body, "openai/gpt-oss-20b") || !strings.Contains(body, "a/Both") { + t.Errorf("both run on cuda01: %s", body) + } +} diff --git a/internal/web/locales/en/mirror.json b/internal/web/locales/en/mirror.json index 9ecee29..8ee7188 100644 --- a/internal/web/locales/en/mirror.json +++ b/internal/web/locales/en/mirror.json @@ -71,6 +71,8 @@ "explore.search_placeholder": "Search Hugging Face — qwen coder, gemma, bge…", "explore.search_aria": "Search Hugging Face", "explore.only_here": "only what these nodes can run", + "explore.fit": "Listed by what this cluster holds now: its largest card has %d GB (compute capability %s), and the most memory on one node is %d GB. A model too big for them, or in a form these cards lack (FP8 needs 8.9), is left out.", + "explore.fit_cpu": "Listed by what this cluster holds now: no GPUs, so GGUF only, and the most memory on one node is %d GB. A model too big for that is left out.", "explore.search": "Search", "explore.registry_err": "The registry: %s", "explore.col.repo": "Repository", @@ -95,15 +97,15 @@ "explore.js.follow": "follow it under Held", "explore.js.started": "Started", "explore.js.unreachable": "Could not reach the gateway:", - "explore.hidden.one": "%d more is hidden: nothing in it these nodes can load — an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", - "explore.hidden.other": "%d more are hidden: nothing in them these nodes can load — an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", + "explore.hidden.one": "%d more is hidden: it does not run here — too big for these nodes, an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", + "explore.hidden.other": "%d more are hidden: they do not run here — too big for these nodes, an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", "explore.untick": "Untick “only what these nodes can run” to see them.", "explore.gated_note": "A gated repository needs its terms accepted with a Hugging Face account, and this browses anonymously — the files will list, and fetching one will fail at the mirror. Download it by hand and put it in the mirror directory, or give the mirror a token.", - "explore.empty.query_hidden.one": "Nothing matched “%[2]s” that these nodes can run — %[1]d result was hidden: an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", - "explore.empty.query_hidden.other": "Nothing matched “%[2]s” that these nodes can run — %[1]d results were hidden: an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", + "explore.empty.query_hidden.one": "Nothing matched “%[2]s” that these nodes can run — %[1]d result was hidden: too big for these nodes, an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", + "explore.empty.query_hidden.other": "Nothing matched “%[2]s” that these nodes can run — %[1]d results were hidden: too big for these nodes, an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", "explore.empty.query": "Nothing matched “%s”.", - "explore.empty.hidden.one": "Nothing to list that these nodes can run — %d result was hidden: an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", - "explore.empty.hidden.other": "Nothing to list that these nodes can run — %d results were hidden: an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", + "explore.empty.hidden.one": "Nothing to list that these nodes can run — %d result was hidden: too big for these nodes, an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", + "explore.empty.hidden.other": "Nothing to list that these nodes can run — %d results were hidden: too big for these nodes, an architecture llama.cpp cannot load, or weights in neither GGUF nor safetensors.", "explore.empty.nothing": "Nothing to list.", "mirror.fetch.intro": "By hand, for anything the search does not reach.", @@ -137,6 +139,8 @@ "card.model": "model", "card.cannot_serve": "cannot be served", "card.imatrix": "it is an importance matrix for quantising, not a model", + "card.too_big": "too big for this cluster: it needs %.0f GB where the most a node here offers it is %.0f GB", + "card.no_gpu": "safetensors run on a GPU, and this cluster has none; take a GGUF", "card.pickle_hidden.one": "%d file is not shown: it is in a pickle format, which runs code when it is loaded, and nodes refuse it.", "card.pickle_hidden.other": "%d files are not shown: they are in a pickle format, which runs code when it is loaded, and nodes refuse them.", "card.only_pickle.one": "%[2]s has weights only in a pickle format (%[1]d file), which runs code when it is loaded; nodes refuse it. Look for a safetensors or GGUF version of the model.", diff --git a/internal/web/locales/tr/mirror.json b/internal/web/locales/tr/mirror.json index ce6dae3..d35fae9 100644 --- a/internal/web/locales/tr/mirror.json +++ b/internal/web/locales/tr/mirror.json @@ -69,6 +69,8 @@ "explore.search_placeholder": "Hugging Face'te ara — qwen coder, gemma, bge…", "explore.search_aria": "Hugging Face'te ara", "explore.only_here": "yalnızca bu düğümlerin çalıştırabildikleri", + "explore.fit": "Bu kümenin şu anki donanımına göre listelendi: en büyük kart %d GB (compute capability %s), tek düğümdeki en çok bellek %d GB. Bunlara sığmayan ya da bu kartların çalıştıramadığı biçimdeki (FP8 için 8.9 gerekir) modeller listede yok.", + "explore.fit_cpu": "Bu kümenin şu anki donanımına göre listelendi: GPU yok, yalnızca GGUF; tek düğümdeki en çok bellek %d GB. Buna sığmayan modeller listede yok.", "explore.search": "Ara", "explore.registry_err": "Model deposu: %s", "explore.col.repo": "Depo", @@ -93,12 +95,12 @@ "explore.js.follow": "Aynadakiler sekmesinden izleyin", "explore.js.started": "Başladı", "explore.js.unreachable": "Ağ geçidine ulaşılamadı:", - "explore.hidden.other": "%d sonuç daha gizlendi: düğümlerin yükleyebileceği bir şey yok — llama.cpp'nin yükleyemediği bir mimari, ya da ne GGUF ne safetensors olan ağırlıklar.", + "explore.hidden.other": "%d sonuç daha gizlendi: burada çalışmaz — bu düğümlere sığmıyor, llama.cpp'nin yükleyemediği bir mimari, ya da ne GGUF ne safetensors olan ağırlıklar.", "explore.untick": "Onları görmek için “yalnızca bu düğümlerin çalıştırabildikleri” kutusunun işaretini kaldırın.", "explore.gated_note": "Erişimi kısıtlı bir depo, koşullarının bir Hugging Face hesabıyla kabul edilmesini ister; buradaki tarama ise anonimdir — dosyalar listelenir, ancak birini indirmek aynada başarısız olur. Dosyayı elle indirip ayna dizinine koyun ya da aynaya bir token verin.", - "explore.empty.query_hidden.other": "“%[2]s” ile eşleşen ve bu düğümlerin çalıştırabileceği bir şey yok — %[1]d sonuç gizlendi: llama.cpp'nin yükleyemediği bir mimari, ya da ne GGUF ne safetensors olan ağırlıklar.", + "explore.empty.query_hidden.other": "“%[2]s” ile eşleşen ve bu düğümlerin çalıştırabileceği bir şey yok — %[1]d sonuç gizlendi: bu düğümlere sığmıyor, llama.cpp'nin yükleyemediği bir mimari, ya da ne GGUF ne safetensors olan ağırlıklar.", "explore.empty.query": "“%s” ile eşleşen bir şey yok.", - "explore.empty.hidden.other": "Bu düğümlerin çalıştırabileceği listelenecek bir şey yok — %d sonuç gizlendi: llama.cpp'nin yükleyemediği bir mimari, ya da ne GGUF ne safetensors olan ağırlıklar.", + "explore.empty.hidden.other": "Bu düğümlerin çalıştırabileceği listelenecek bir şey yok — %d sonuç gizlendi: bu düğümlere sığmıyor, llama.cpp'nin yükleyemediği bir mimari, ya da ne GGUF ne safetensors olan ağırlıklar.", "explore.empty.nothing": "Listelenecek bir şey yok.", "mirror.fetch.intro": "Aramanın ulaşamadığı her şey için, elle.", @@ -132,6 +134,8 @@ "card.model": "model", "card.cannot_serve": "sunulamaz", "card.imatrix": "bu bir model değil, nicemleme için bir önem matrisi", + "card.too_big": "bu küme için fazla büyük: %.0f GB gerekiyor, buradaki bir düğümün ona verebileceği en çok %.0f GB", + "card.no_gpu": "safetensors GPU'da çalışır ve bu kümede GPU yok; GGUF olanı seçin", "card.pickle_hidden.other": "%d dosya gösterilmiyor: pickle biçiminde, yüklenirken kod çalıştırır ve düğümler onu almaz.", "card.only_pickle.other": "%[2]s deposunun ağırlıkları yalnızca pickle biçiminde (%[1]d dosya); yüklenirken kod çalıştırır, düğümler almaz. Modelin safetensors ya da GGUF sürümünü arayın.", "card.fetch": "İndir", diff --git a/internal/web/mirror_explore.go b/internal/web/mirror_explore.go index 496434b..9b80cea 100644 --- a/internal/web/mirror_explore.go +++ b/internal/web/mirror_explore.go @@ -72,6 +72,10 @@ type explorer struct { Rows []exploreRow // Hidden is how many rows Here took out, so an empty page says why. Hidden int + // Fit says what hardware Here judged by; empty when there was none. + Fit string + // gpus is whether the cluster has cards, and so runs safetensors too. + gpus bool Views, Tasks, Sizes []exploreLink } @@ -195,9 +199,13 @@ func (e explorer) href(query, key, value string) template.URL { // hfQuery is what to ask the registry for. func (e explorer) hfQuery(search string) hf.Query { q := hf.Query{Search: search, Sort: hf.SortTrending, Limit: 60, - // What runs here is GGUF; the architecture check needs the registry's - // reading of one. - GGUF: e.Here} + // Without cards what runs here is GGUF only. With them safetensors + // too, and a page asks for more, since many are then left out for + // their size. + GGUF: e.Here && !e.gpus} + if e.Here && e.gpus { + q.Limit = 100 + } switch e.View { case viewDownloads: q.Sort = hf.SortDownloads @@ -255,6 +263,11 @@ func (p *AccountPages) exploreList(r *http.Request, d *adminMirrorData) { } e.Sizes = append(e.Sizes, exploreLink{label, e.href(d.Query, "size", s.key), s.key == e.Size}) } + fit := p.fitOf() + if e.Here && fit != nil { + e.gpus = fit.gpus() + e.Fit = fit.note(l) + } d.Explore = &e got, err := p.registry.Explore(r.Context(), e.hfQuery(d.Query)) @@ -293,6 +306,11 @@ func (p *AccountPages) exploreList(r *http.Request, d *adminMirrorData) { e.Hidden++ continue } + // Too big for every node here, or in a form these cards lack. + if e.Here && fit != nil && !fit.runs(m) { + e.Hidden++ + continue + } if e.Here && row.Verdict.Checked && !row.Verdict.Servable { e.Hidden++ continue diff --git a/internal/web/templates/admin_mirror.html b/internal/web/templates/admin_mirror.html index 58ac2e5..25bccb5 100644 --- a/internal/web/templates/admin_mirror.html +++ b/internal/web/templates/admin_mirror.html @@ -298,6 +298,7 @@ {{end}} {{if not .Repo}}{{with .Explore}} +{{with .Fit}}

{{.}}

{{end}} {{if .Rows}}
{{t "explore.col.repo"}}{{t "explore.col.size"}}{{t "explore.col.arch"}}{{t "explore.col.context"}}