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"}} |
|---|