CI / test (push) Successful in 9m1s
For each layout modelplan.Options offers here — one card, tensor parallel across a node's cards, llama.cpp's layer split, a pipeline across nodes on a fabric — modelplan.GPUTokensPerSecond gives one reply's speed: the bytes a token reads over each card's bandwidth, by its model name (a table, SXM and PCIe apart), plus two all-reduces a layer for tensor parallel (8 us on NVLink, 25 us and up on PCIe), turns between cards for a layer split, and a hop per node over InfiniBand or RoCE for a pipeline. The explorer and a repository's files show the fastest, with the layout; mixed classes are judged class by class, FP8 on 8.9 cards only. Calibrated on cuda01's Quadro RTX 6000 (672 GB/s): qwen3-vl-8b Q8, 8.7 GB, 62.1 tok/s measured, 59 estimated; Qwen3-Coder-30B-A3B Q4_K_M, 18.6 GB, 144 measured, which says a model of experts reads 1.6 times its active share (ActiveShare now does, on the CPU too). Tensor parallel and fabrics are not measured here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
627 lines
34 KiB
HTML
627 lines
34 KiB
HTML
{{define "admin_mirror"}}<!doctype html>
|
|
<html lang="{{lang}}"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
{{/* Only while something is downloading, and not while a form is being
|
|
filled in: pages.js holds the reload off then. */}}
|
|
{{if .RefreshSec}}<meta name="llmgw-refresh" content="{{.RefreshSec}}">{{end}}
|
|
<script defer src="{{asset "pages.js"}}"></script>
|
|
<title>{{t "mirror.title"}} · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
|
|
</head><body><div class="wrap">
|
|
<header>
|
|
<h1>{{t "mirror.heading"}}</h1>
|
|
<p class="sub">{{.Addr}}{{if .Unreachable}} {{t "mirror.did_not_answer"}}{{end}}</p>
|
|
{{template "nav" .Nav}}
|
|
</header>
|
|
|
|
{{/* Links rather than a script's panels: each tab has an address, the back
|
|
button works, and only the one shown asks anything of the registry. */}}
|
|
<nav class="page-tabs" aria-label="{{t "mirror.tabs_aria"}}">
|
|
{{range .Tabs}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="page"{{end}}>{{.Label}}{{with .Count}} <span class="count">{{.}}</span>{{end}}</a>
|
|
{{end}}</nav>
|
|
|
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
|
{{if .Message}}<p class="ok">{{.Message}}</p>{{end}}
|
|
|
|
{{if eq .Tab "held"}}
|
|
{{if .Unreachable}}
|
|
{{/* Said outright rather than shown as an empty table. A mirror holding
|
|
nothing and a mirror that is not there look the same in a list of zero
|
|
rows, and they call for opposite responses. */}}
|
|
<p class="note">{{th "mirror.unreachable" .Addr}}</p>
|
|
{{else}}
|
|
|
|
<div id="held">
|
|
{{if .Rows}}
|
|
<table class="rows compact held">
|
|
<tr><th>{{t "mirror.col.model"}}</th><th>{{t "mirror.col.files"}}</th><th class="num">{{t "mirror.col.size"}}</th><th>{{t "mirror.col.runs"}}</th><th></th></tr>
|
|
{{range .Groups}}{{$grouped := gt .Models 1}}
|
|
{{if $grouped}}
|
|
<tr class="model-group"><td colspan="5"><strong>{{.Family}}</strong>
|
|
<span class="opt">{{tn "mirror.group.models" .Models (bytes .Bytes)}}</span>
|
|
{{if .Fetching}}<span class="tag new">{{t "mirror.state.fetching"}}</span>{{end}}
|
|
{{if .Failed}}<span class="tag bad">{{t "mirror.state.failed"}}</span>{{end}}
|
|
{{if .InUse}}<span class="tag">{{t "mirror.group.in_use"}}</span>{{end}}</td></tr>
|
|
{{end}}
|
|
{{range .Rows}}{{$row0 := .}}<tr{{if $grouped}} class="model-version"{{end}}{{if .Detail}} class="open"{{end}}>
|
|
<td>{{if eq (printf "%s" .State) "present"}}<a class="open-held" aria-expanded="{{if .Detail}}true{{else}}false{{end}}"
|
|
href="/admin/mirror?tab=held{{if not .Detail}}&open={{.Open}}{{end}}"><strong>{{.Model}}</strong></a>
|
|
{{else}}<strong>{{.Model}}</strong>{{end}} <span class="opt">{{.Version}}</span>
|
|
{{if eq .Kind "adapter"}}<span class="tag new" title="{{t "mirror.adapter_title" .Arch}}">{{t "mirror.adapter"}}</span>{{end}}</td>
|
|
<td class="file" title="{{if .SHA256}}{{t "mirror.file_sha" .File .SHA256}}{{else}}{{.File}}{{end}}">
|
|
{{if gt .Files 1}}{{tn "mirror.files" .Files}}{{else}}<code>{{.File}}</code>{{end}}</td>
|
|
<td class="num">
|
|
{{if eq (printf "%s" .State) "fetching"}}
|
|
{{if .HasPercent}}{{t "mirror.percent_of" .Percent (bytes .BytesTotal)}}
|
|
{{else}}{{/* No total came from upstream, so no percentage is invented —
|
|
a bar that guesses its own end and then stops moving is worse
|
|
than a number that only goes up. */}}
|
|
{{t "mirror.so_far" (bytes .BytesDone)}}{{end}}
|
|
{{else if eq (printf "%s" .State) "failed"}}—
|
|
{{else}}{{bytes .BytesDone}}{{end}}
|
|
</td>
|
|
<td>
|
|
{{if eq (printf "%s" .State) "fetching"}}<span class="state state-starting">{{t "mirror.state.fetching"}}</span>
|
|
{{else if eq (printf "%s" .State) "failed"}}<span class="state state-failed">{{t "mirror.state.failed"}}</span>
|
|
{{else if .UsedBy}}{{$row := .}}{{range .UsedBy}}<div class="use">
|
|
{{/* Named only when the catalog calls it something other than the
|
|
mirror does; otherwise it is the model column again. */}}
|
|
{{if ne .Name $row.Model}}<span class="as" title="{{.Name}}">{{t "mirror.use.as" .Name}}</span>{{end}}
|
|
{{if .Declared}}<span class="mode-fixed" title="{{t "mirror.use.yaml_title"}}">
|
|
{{- t "mirror.use.yaml" (t (print "mirror.mode." .Mode))}}</span>
|
|
{{else}}
|
|
{{/* Submitted on change: the choice is the action, and a second click
|
|
to confirm it would only be a way to forget to. */}}
|
|
<form method="post" action="/admin/mirror/mode" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<input type="hidden" name="model" value="{{.Name}}">
|
|
<input type="hidden" name="replicas" value="{{.Replicas}}">
|
|
<select name="mode" class="mode" data-autosubmit aria-label="{{t "mirror.use.mode_aria" .Name}}">
|
|
<option value="always"{{if eq .Mode "always"}} selected{{end}}>{{t "mirror.mode.always"}}</option>
|
|
<option value="ondemand"{{if eq .Mode "ondemand"}} selected{{end}}>{{t "mirror.mode.ondemand"}}</option>
|
|
<option value="stopped"{{if eq .Mode "stopped"}} selected{{end}}>{{t "mirror.mode.stopped"}}</option>
|
|
</select>
|
|
{{/* The button is in the catalogue: what a noscript holds is read as text, not markup. */}}<noscript>{{th "mirror.use.set"}}</noscript>
|
|
</form>
|
|
{{end}}
|
|
{{if .Fleet}}<span class="runs-now{{if or .Parked .Unplaceable}} bad{{else if lt .Ready .Wanted}} short{{end}}" title="{{if .Starting}}{{t "mirror.use.runs_title_starting"}}{{else}}{{t "mirror.use.runs_title"}}{{end}}">
|
|
{{- if .Parked}}{{t "mirror.use.parked"}}
|
|
{{- else if and (eq .Wanted 0) (eq .Ready 0)}}{{t "mirror.use.none_running"}}
|
|
{{- else}}{{.Ready}}/{{.Wanted}}{{if .Starting}} +{{.Starting}}{{end}}{{end}}
|
|
{{- if .Unplaceable}} · {{t "mirror.use.no_room"}}{{end}}
|
|
{{- with .Nodes}} · {{range $i, $n := .}}{{if $i}}, {{end}}<span class="node" title="{{$n.Full}}">{{$n.Short}}</span>{{end}}{{end}}</span>{{end}}
|
|
</div>{{end}}
|
|
{{else}}<span class="opt">{{t "mirror.not_served"}}</span>{{end}}
|
|
</td>
|
|
<td class="actions">
|
|
{{if eq (printf "%s" .State) "failed"}}
|
|
{{/* Retry, not remove. A fetch that failed left nothing on disk, so the
|
|
remove would come back "the mirror holds nothing for this" — a button
|
|
whose only outcome is a confusing error. Starting the same download
|
|
again is what somebody looking at a failed row wants, and the mirror
|
|
allows it: only a download still running is refused. */}}
|
|
<form method="post" action="/admin/mirror/fetch" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<input type="hidden" name="source" value="{{.Source}}">
|
|
<input type="hidden" name="model" value="{{.Model}}">
|
|
<input type="hidden" name="version" value="{{.Version}}">
|
|
<input type="hidden" name="file" value="{{.File}}">
|
|
<button type="submit" class="link">{{t "mirror.retry"}}</button>
|
|
</form>
|
|
{{else if eq (printf "%s" .State) "present"}}
|
|
<button type="button" class="link copy-url" data-url="{{.URL}}" title="{{.URL}}">{{t "mirror.copy_address"}}</button>
|
|
{{if and $.HasCatalog (not .UsedBy)}}{{if .Serve}}<a href="/admin/mirror">{{t "mirror.close"}}</a>
|
|
{{else}}<a href="/admin/mirror?serve={{.Model}}:{{.Version}}">{{t "mirror.serve"}}</a>{{end}}{{end}}
|
|
<form method="post" action="/admin/mirror/{{.Model}}/{{.Version}}/remove"
|
|
class="inline" data-confirm="{{if .UsedBy}}{{t "mirror.remove_confirm_in_use" .Model .Version}}{{else}}{{t "mirror.remove_confirm" .Model .Version}}{{end}}">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<button type="submit" class="link danger">{{t "mirror.remove"}}</button>
|
|
</form>
|
|
{{end}}
|
|
</td>
|
|
</tr>
|
|
{{if .Err}}<tr><td colspan="5" class="error-row">{{.Err}}</td></tr>{{end}}
|
|
{{with .Detail}}<tr class="held-detail"><td colspan="5">{{template "mirror_held_detail" dict "D" . "Row" $row0 "CSRF" $.CSRF}}</td></tr>{{end}}
|
|
|
|
{{with .Serve}}
|
|
<tr class="editing"><td colspan="5">
|
|
{{if .Refusal}}
|
|
{{/* Offered and then failing at placement is worse than not offered at all. */}}
|
|
<p class="error">{{t "serve.refusal" .Refusal}}</p>
|
|
{{else}}
|
|
{{if .Warning}}<p class="note error-row">{{.Warning}}.</p>{{end}}
|
|
<form method="post" action="/admin/mirror/serve" class="serve-form" data-engine="{{.Engine}}" data-free-gpus="{{.FreeGPUs}}">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<input type="hidden" name="source" value="{{.Source}}">
|
|
<input type="hidden" name="engine" value="{{.Engine}}">
|
|
<p class="sub">{{th "serve.serving" .Source .Engine}}
|
|
{{if .Files}}{{t "serve.whole_version" .Files}}{{end}}</p>
|
|
<div class="field-row">
|
|
<label>{{t "serve.name"}} <input type="text" name="name" value="{{.Model}}" required></label>
|
|
<label>{{t "serve.aliases"}} <span class="opt">{{t "serve.aliases_hint"}}</span>
|
|
<input type="text" name="aliases" placeholder="{{t "serve.aliases_example"}}"></label>
|
|
<label>{{t "serve.replicas"}} <input type="text" name="replicas" value="1" required></label>
|
|
<label>{{t "serve.runs"}}
|
|
<select name="mode">
|
|
<option value="always">{{t "mirror.mode.always"}}</option>
|
|
<option value="ondemand">{{t "serve.mode.ondemand"}}</option>
|
|
</select></label>
|
|
</div>
|
|
{{/* Reaches clients through /v1/models, which for most of them is the only
|
|
view of what this gateway serves. Nine names and nothing to choose
|
|
between them is a choice made by guesswork. */}}
|
|
<label>{{t "serve.description"}} <span class="opt">{{t "serve.description_hint"}}</span>
|
|
<input type="text" name="description"
|
|
placeholder="{{t "serve.description_example"}}"></label>
|
|
<div class="field-row">
|
|
<label>{{t "serve.context_length"}}
|
|
<input type="text" name="context_length" value="4096" required></label>
|
|
<label>{{t "serve.max_output"}} <span class="opt">{{t "serve.optional"}}</span>
|
|
<input type="text" name="max_output" value="1024"></label>
|
|
<label>{{t "serve.ctx_pool"}} <span class="opt">{{t "serve.ctx_pool_hint"}}</span>
|
|
<input type="text" name="ctx_size" value="{{.CtxPool}}"></label>
|
|
</div>
|
|
{{/* Where it runs is a choice with consequences, so it is a choice: the
|
|
GPU classes the fleet has, and the CPU for llama.cpp. A profile name
|
|
field stood here once, and reading as though naming a profile "gpu"
|
|
would put it on one, it put GGUF models on the CPU of GPU clusters. */}}
|
|
<div class="field-row">
|
|
<label>{{t "serve.placement"}}
|
|
<select name="placement">
|
|
{{range .Placements}}<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>{{end}}
|
|
</select></label>
|
|
<label>{{t "serve.gpus"}} <span class="opt">{{t "serve.gpus_hint"}}</span>
|
|
<input type="text" name="gpus" value="{{.GPUs}}" size="3"></label>
|
|
{{if eq .Engine "llamacpp"}}<label>{{t "serve.parallel"}} <span class="opt">{{t "serve.parallel_hint"}}</span>
|
|
<input type="text" name="parallel" value="{{.Parallel}}" size="3"></label>{{end}}
|
|
</div>
|
|
{{if .FreeGPUs}}<label class="cpu-ok"><input type="checkbox" name="cpu_ok" value="yes">
|
|
{{t "serve.cpu_ok" .FreeGPUs}}</label>{{end}}
|
|
<div class="field-row">
|
|
<label>{{t "serve.memory"}} <span class="opt">{{t "serve.memory_hint"}}</span>
|
|
<input type="text" name="min_memory_gb" value="{{.MinMemoryGB}}" required></label>
|
|
<label>{{t "serve.cpu_cores"}} <span class="opt">{{t "serve.cpu_cores_hint"}}</span>
|
|
<input type="text" name="cpu_cores" value="{{.CPUCores}}"></label>
|
|
<label>{{t "serve.selector"}} <span class="opt">{{t "serve.selector_hint"}}</span>
|
|
<input type="text" name="selector_key" placeholder="{{t "serve.selector_key_example"}}" size="8">
|
|
<input type="text" name="selector_value" placeholder="{{t "serve.selector_value_example"}}" size="10"></label>
|
|
</div>
|
|
<p class="sub serve-summary" hidden></p>
|
|
<button type="submit">{{t "serve.add"}}</button>
|
|
</form>
|
|
<script>
|
|
// What it will run as, in one line, kept up to date as the form changes:
|
|
// engine, where, how many cards, memory, context and how it is shared. The
|
|
// sentences are the ones the server describes a model with, in the page's
|
|
// language; fmt fills their %s and %d, numbered ones included.
|
|
(function () {
|
|
const f = document.currentScript.previousElementSibling;
|
|
if (!f || !f.classList.contains("serve-form")) return;
|
|
const T = {
|
|
cpu: {{t "serve.on_cpu"}}, gpu1: {{t "serve.on_gpu_one"}}, gpus: {{t "serve.on_gpus"}},
|
|
mem: {{t "serve.host_memory"}}, cores: {{t "serve.cores.other"}},
|
|
poolPar: {{t "serve.pool_parallel"}}, pool: {{t "serve.pool"}},
|
|
runAs: {{t "serve.will_run_as"}}, free: {{t "serve.free_gpus_warning"}},
|
|
};
|
|
const fmt = (s, ...a) => {
|
|
let i = 0;
|
|
return s.replace(/%(?:\[(\d+)\])?[sd]/g, (_, n) => { const k = n ? +n - 1 : i; i = k + 1; return String(a[k]); });
|
|
};
|
|
const out = f.querySelector(".serve-summary"), ok = f.querySelector(".cpu-ok");
|
|
const v = (n) => (f.elements[n] ? f.elements[n].value.trim() : "");
|
|
const engine = { llamacpp: "llama.cpp", vllm: "vLLM", sglang: "SGLang" }[f.dataset.engine] || f.dataset.engine;
|
|
function draw() {
|
|
const place = v("placement"), gpus = parseInt(v("gpus"), 10) || 1;
|
|
const onCPU = place === "cpu" || place === "";
|
|
let where = fmt(T.cpu, engine);
|
|
if (!onCPU) {
|
|
const cls = place.replace(/^gpu:/, "");
|
|
where = gpus > 1 ? fmt(T.gpus, engine, gpus, cls) : fmt(T.gpu1, engine, cls);
|
|
}
|
|
const parts = [where];
|
|
if (v("min_memory_gb")) parts.push(fmt(T.mem, v("min_memory_gb")));
|
|
if (onCPU && v("cpu_cores")) parts.push(fmt(T.cores, v("cpu_cores")));
|
|
const pool = parseInt(v("ctx_size"), 10), par = parseInt(v("parallel"), 10);
|
|
if (pool && par) parts.push(fmt(T.poolPar, pool, par, Math.floor(pool / par)));
|
|
else if (pool) parts.push(fmt(T.pool, pool));
|
|
out.textContent = fmt(T.runAs, parts.join(", "));
|
|
const warn = onCPU && +f.dataset.freeGpus > 0;
|
|
out.classList.toggle("error", warn);
|
|
if (warn) out.textContent += " " + fmt(T.free, f.dataset.freeGpus);
|
|
if (ok) ok.hidden = !onCPU;
|
|
out.hidden = false;
|
|
}
|
|
f.addEventListener("input", draw);
|
|
f.addEventListener("change", draw);
|
|
draw();
|
|
})();
|
|
</script>
|
|
<p class="note">{{t "serve.pool_note"}}</p>
|
|
<p class="note">{{t "serve.memory_note" .MemoryBasis}}</p>
|
|
{{end}}
|
|
</td></tr>
|
|
{{end}}
|
|
{{end}}
|
|
{{end}}
|
|
</table>
|
|
<p class="note">{{th "mirror.copy_note"}}</p>
|
|
<script>
|
|
(function () {
|
|
var copied = {{t "common.copied"}}, address = {{t "mirror.address_prompt"}};
|
|
document.addEventListener('click', function (e) {
|
|
var b = e.target.closest('.copy-url');
|
|
if (!b) return;
|
|
var done = function (t) { var o = b.textContent; b.textContent = t; setTimeout(function () { b.textContent = o; }, 1500); };
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(b.dataset.url).then(function () { done(copied); },
|
|
function () { window.prompt(address, b.dataset.url); });
|
|
} else { window.prompt(address, b.dataset.url); }
|
|
});
|
|
})();
|
|
</script>
|
|
{{else}}<p class="empty">{{if .HasRegistry}}{{th "mirror.empty_explore"}}{{else}}{{th "mirror.empty"}}{{end}}</p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{if and (eq .Tab "explore") .HasRegistry}}
|
|
{{if not .Repo}}{{with .Explore}}
|
|
<nav class="pills" aria-label="{{t "explore.views_aria"}}">{{range .Views}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="page"{{end}}>{{.Label}}</a>{{end}}</nav>
|
|
{{if eq .View "adapters"}}
|
|
<p class="note">{{if .Of}}{{th "explore.adapters_for" .Of}}
|
|
<a href="/admin/mirror?tab=explore&view=adapters">{{t "explore.all_adapters"}}</a>{{else}}{{th "explore.adapters_any"}}{{end}}
|
|
{{t "explore.adapters_note"}}</p>
|
|
{{else}}
|
|
<div class="explore-filters">
|
|
<nav class="pills small" aria-label="{{t "explore.task_aria"}}">{{range .Tasks}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="true"{{end}}>{{.Label}}</a>{{end}}</nav>
|
|
<nav class="pills small" aria-label="{{t "explore.size_aria"}}">{{range .Sizes}}<a href="{{.Href}}"{{if .Active}} class="on" aria-current="true"{{end}}>{{.Label}}</a>{{end}}</nav>
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|
|
<form method="get" action="/admin/mirror" class="explore-search">
|
|
<input type="hidden" name="tab" value="explore">
|
|
{{with .Explore}}<input type="hidden" name="view" value="{{.View}}">
|
|
<input type="hidden" name="task" value="{{.Task}}">
|
|
<input type="hidden" name="size" value="{{.Size}}">
|
|
{{if .Of}}<input type="hidden" name="of" value="{{.Of}}">{{end}}
|
|
<input type="hidden" name="x" value="1">{{end}}
|
|
<input type="search" name="q" value="{{.Query}}" placeholder="{{t "explore.search_placeholder"}}" aria-label="{{t "explore.search_aria"}}">
|
|
<label class="check"><input type="checkbox" name="here" value="1" data-autosubmit{{if .Explore}}{{if .Explore.Here}} checked{{end}}{{end}}> {{t "explore.only_here"}}</label>
|
|
<button type="submit">{{t "explore.search"}}</button>
|
|
</form>
|
|
{{end}}
|
|
|
|
{{if .RegistryErr}}
|
|
{{/* Its own line, not the page's error: the registry is somebody else's server
|
|
and it being down says nothing about the mirror, which is what this page is
|
|
for and has to keep working when Hugging Face does not. */}}
|
|
<p class="note error-row">{{t "explore.registry_err" .RegistryErr}}</p>
|
|
{{end}}
|
|
|
|
{{if not .Repo}}{{with .Explore}}
|
|
{{with .Fit}}<p class="note">{{.}}</p>{{end}}
|
|
{{if .Rows}}
|
|
<table class="rows explore">
|
|
<tr><th>{{t "explore.col.repo"}}</th><th class="num">{{t "explore.col.size"}}</th><th>{{t "explore.col.arch"}}</th><th class="num">{{t "explore.col.context"}}</th>
|
|
<th class="num" title="{{t "explore.col.gpu_title"}}">{{t "explore.col.gpu"}}</th>
|
|
<th class="num" title="{{t "explore.col.cpu_title"}}">{{t "explore.col.cpu"}}</th>
|
|
<th class="num">{{t "explore.col.downloads"}}</th><th class="num">{{t "explore.col.likes"}}</th><th class="num">{{t "explore.col.updated"}}</th></tr>
|
|
{{range .Rows}}<tr class="explore-row">
|
|
<td><a href="{{.Href}}" class="open-repo" data-repo="{{.ID}}" aria-expanded="false"><code>{{.ID}}</code></a>
|
|
{{if .New}}<span class="tag new">{{t "explore.new"}}</span>{{end}}{{if .Gated.Is}}<span class="tag">{{t "explore.gated"}}</span>{{end}}
|
|
{{if or .BaseModel .License}}<span class="from" title="{{with .BaseModel}}{{t "explore.from" .}}{{end}}{{if and .BaseModel .License}} · {{end}}{{.License}}">{{with .BaseModel}}{{t "explore.from" .}}{{end}}{{if and .BaseModel .License}} · {{end}}{{.License}}</span>{{end}}</td>
|
|
<td class="num">{{.ParamsLabel}}</td>
|
|
<td>{{if .Architecture}}<span{{if not .Verdict.Servable}} class="cannot"{{end}} title="{{.Verdict.Reason}}">{{.Architecture}}{{if .Verdict.Checked}}{{if .Verdict.Servable}} ✓{{else}} ✗{{end}}{{end}}</span>{{else}}—{{end}}</td>
|
|
<td class="num">{{.ContextLabel}}</td>
|
|
<td class="num">{{if .GPUSpeed}}<span title="{{.GPULayout}}">{{.GPUSpeed}}</span>{{else}}—{{end}}</td>
|
|
<td class="num">{{if .CPUSpeed}}<span title="{{t "explore.cpu_on" .CPUNode}}">{{.CPUSpeed}}</span>{{else}}—{{end}}</td>
|
|
<td class="num">{{compact .Downloads}}</td>
|
|
<td class="num">{{compact .Likes}}</td>
|
|
<td class="num">{{.Updated}}</td>
|
|
</tr>{{end}}
|
|
</table>
|
|
<script>
|
|
// A repository opens beneath its row rather than replacing the list: the
|
|
// list is what somebody is choosing from, and losing it to read one card
|
|
// means finding their place again for the next. The link still goes to the
|
|
// repository's own page when this does not run, or with a modifier key.
|
|
const exploreText = {
|
|
loading: {{t "explore.js.loading"}}, loadFailed: {{t "explore.js.load_failed"}},
|
|
starting: {{t "explore.js.starting"}}, refused: {{t "explore.js.refused"}},
|
|
follow: {{t "explore.js.follow"}}, started: {{t "explore.js.started"}},
|
|
unreachable: {{t "explore.js.unreachable"}},
|
|
};
|
|
document.querySelector("table.explore").addEventListener("click", async function (e) {
|
|
const a = e.target.closest("a.open-repo");
|
|
if (!a || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
e.preventDefault();
|
|
const row = a.closest("tr");
|
|
let detail = row.nextElementSibling;
|
|
if (detail && detail.classList.contains("repo-detail")) {
|
|
detail.hidden = !detail.hidden;
|
|
a.setAttribute("aria-expanded", String(!detail.hidden));
|
|
row.classList.toggle("open", !detail.hidden);
|
|
return;
|
|
}
|
|
detail = document.createElement("tr");
|
|
detail.className = "repo-detail";
|
|
const td = document.createElement("td");
|
|
td.colSpan = row.children.length;
|
|
td.textContent = exploreText.loading;
|
|
detail.appendChild(td);
|
|
row.after(detail);
|
|
row.classList.add("open");
|
|
a.setAttribute("aria-expanded", "true");
|
|
try {
|
|
const resp = await fetch("/admin/mirror/repo?repo=" + encodeURIComponent(a.dataset.repo));
|
|
const text = await resp.text();
|
|
if (!resp.ok) { td.textContent = text.slice(0, 300); td.classList.add("error-row"); return; }
|
|
// Our own server's rendering of the repository, the card already
|
|
// stripped of anything that runs: the same HTML the full page shows.
|
|
td.innerHTML = text;
|
|
} catch (err) {
|
|
td.textContent = exploreText.loadFailed + " " + err;
|
|
}
|
|
});
|
|
|
|
// A fetch started from an opened repository answers where it was asked:
|
|
// the outcome under its button, and the mirror's table above brought up to
|
|
// date and followed until the download is done. A form post would reload
|
|
// the page, closing the repository and losing the list's place.
|
|
document.querySelector("table.explore").addEventListener("submit", async function (e) {
|
|
const form = e.target.closest('form[action="/admin/mirror/fetch"]');
|
|
if (!form) return;
|
|
e.preventDefault();
|
|
const btn = form.querySelector("button[type=submit]");
|
|
const was = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = exploreText.starting;
|
|
let note = form.parentElement.querySelector(".fetch-note");
|
|
if (!note) {
|
|
note = document.createElement("div");
|
|
note.className = "fetch-note note";
|
|
form.after(note);
|
|
}
|
|
try {
|
|
const resp = await fetch(form.action, {
|
|
method: "POST", body: new URLSearchParams(new FormData(form)),
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
const out = await resp.json().catch(() => ({ error: "(" + resp.status + ")" }));
|
|
if (!resp.ok) {
|
|
note.textContent = out.error || exploreText.refused;
|
|
note.classList.add("bad");
|
|
btn.disabled = false;
|
|
btn.textContent = was;
|
|
return;
|
|
}
|
|
note.classList.remove("bad");
|
|
note.textContent = "✓ " + out.message + " — ";
|
|
const link = document.createElement("a");
|
|
link.href = "/admin/mirror?tab=held";
|
|
link.textContent = exploreText.follow;
|
|
note.appendChild(link);
|
|
btn.textContent = exploreText.started;
|
|
} catch (err) {
|
|
note.textContent = exploreText.unreachable + " " + err;
|
|
note.classList.add("bad");
|
|
btn.disabled = false;
|
|
btn.textContent = was;
|
|
}
|
|
});
|
|
</script>
|
|
{{if .Hidden}}<p class="note">{{tn "explore.hidden" .Hidden}}
|
|
{{t "explore.untick"}}</p>{{end}}
|
|
<p class="note">{{t "explore.gated_note"}}</p>
|
|
{{else if not $.RegistryErr}}
|
|
<p class="empty">{{if $.Query}}{{if .Hidden}}{{tn "explore.empty.query_hidden" .Hidden $.Query}}{{else}}{{t "explore.empty.query" $.Query}}{{end}}
|
|
{{- else}}{{if .Hidden}}{{tn "explore.empty.hidden" .Hidden}}{{else}}{{t "explore.empty.nothing"}}{{end}}{{end}}</p>
|
|
{{end}}
|
|
{{end}}{{end}}
|
|
|
|
{{if .Repo}}{{template "mirror_repo" .}}{{end}}
|
|
{{end}}
|
|
|
|
{{if eq .Tab "fetch"}}
|
|
<p class="note">{{t "mirror.fetch.intro"}}</p>
|
|
<form method="post" action="/admin/mirror/fetch">
|
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
|
<input type="hidden" name="tab" value="fetch">
|
|
<label>{{t "mirror.fetch.source"}}
|
|
<input type="text" name="source" required
|
|
placeholder="{{t "mirror.fetch.source_example"}}"></label>
|
|
<div class="field-row">
|
|
<label>{{t "mirror.fetch.model"}} <input type="text" name="model" required placeholder="{{t "mirror.fetch.model_example"}}"></label>
|
|
<label>{{t "mirror.fetch.version"}} <input type="text" name="version" required placeholder="2026-09"></label>
|
|
<label>{{t "mirror.fetch.file"}} <span class="opt">{{t "serve.optional"}}</span>
|
|
<input type="text" name="file" placeholder="{{t "mirror.fetch.file_hint"}}"></label>
|
|
</div>
|
|
<label class="inline-check"><input type="checkbox" name="force" value="1">
|
|
{{t "mirror.fetch.force"}} <span class="opt">{{t "mirror.fetch.force_hint"}}</span></label>
|
|
<button type="submit">{{t "mirror.fetch.start"}}</button>
|
|
</form>
|
|
<p class="note">{{t "mirror.fetch.note"}}</p>
|
|
{{end}}
|
|
|
|
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
|
</div></body></html>{{end}}
|
|
|
|
{{/* One repository: its card and its files. The whole page when opened by
|
|
address, and the row beneath a listing entry when opened from the list. */}}
|
|
{{define "mirror_repo"}}
|
|
{{if not .Inline}}<p><a href="{{.Back}}">{{t "card.back"}}</a></p>
|
|
<h3>{{.Repo.ID}} <span class="opt">{{t "card.at" .Repo.ShortRevision}}</span></h3>{{end}}
|
|
<p class="repo-meta">
|
|
{{if .Inline}}<span>{{th "card.at_code" .Repo.ShortRevision}}</span>{{end}}
|
|
{{with .Repo.BaseModel}}<span>{{t "card.from"}} {{range $i, $b := .}}{{if $i}}, {{end}}<code>{{$b}}</code>{{end}}</span>{{end}}
|
|
{{with .Repo.License}}<span>{{t "card.license" .}}</span>{{end}}
|
|
{{with .Repo.Languages}}<span>{{join .}}</span>{{end}}
|
|
<span><a href="{{.RepoURL}}" rel="noopener noreferrer">{{t "card.on_registry"}}</a></span>
|
|
</p>
|
|
{{if .Card}}
|
|
<details class="model-card" open>
|
|
<summary>{{t "card.model_card"}}</summary>
|
|
<div class="card-body">{{.Card}}</div>
|
|
</details>
|
|
{{end}}
|
|
{{if .RepoSets}}
|
|
<table class="rows repo-files">
|
|
<tr><th>{{t "card.col.what"}}</th><th class="num">{{t "card.col.size"}}</th><th>{{t "card.col.checksum"}}</th><th></th></tr>
|
|
{{range .RepoSets}}<tr>
|
|
<td><code>{{.Name}}</code>
|
|
{{if .Shards}}<span class="opt">{{t "card.shards" .Shards}}</span>{{end}}
|
|
{{with .Companions}}<span class="opt" title="{{join .}}">{{t "card.companions" (len .)}}</span>{{end}}
|
|
{{if eq .Kind "adapter"}}<span class="tag new" title="{{t "card.adapter_title" .Arch}}">{{t "card.adapter" .Arch}}</span>
|
|
{{else if eq .Kind "model"}}<span class="opt">{{if .Arch}}{{t "card.model_arch" .Arch}}{{else}}{{t "card.model"}}{{end}}</span>{{end}}
|
|
{{if .GPUSpeed}}<span class="opt" title="{{t "explore.col.gpu_title"}}">{{t "card.gpu_speed" .GPUSpeed .GPULayout}}</span>{{end}}
|
|
{{if .CPUSpeed}}<span class="opt" title="{{t "explore.col.cpu_title"}}">{{t "card.cpu_speed" .CPUSpeed .CPUNode}}</span>{{end}}</td>
|
|
<td class="num">{{bytes .Bytes}}</td>
|
|
<td>{{with .SHA256}}<code title="{{.}}">{{slice . 0 12}}</code>{{else}}—{{end}}</td>
|
|
<td>
|
|
{{if not .Verdict.Servable}}
|
|
<span class="state state-failed" title="{{.Verdict.Reason}}">{{t "card.cannot_serve"}}</span>
|
|
<span class="clip-inline" title="{{.Verdict.Reason}}">{{.Verdict.Reason}}</span>
|
|
{{else}}
|
|
{{if .Verdict.Architecture}}<span class="opt" title="{{.Verdict.Reason}}">{{.Verdict.Architecture}}{{if .Verdict.Checked}} ✓{{end}}</span>{{end}}
|
|
<form method="post" action="/admin/mirror/fetch" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<input type="hidden" name="tab" value="explore">
|
|
{{range .Sources}}<input type="hidden" name="source" value="{{.URL}}">
|
|
<input type="hidden" name="file" value="{{.Path}}">
|
|
{{end}}
|
|
<input type="text" name="model" value="{{.Model}}" size="24">
|
|
<input type="text" name="version" value="{{.Version}}" size="10">
|
|
<button type="submit">{{t "card.fetch"}}</button>
|
|
</form>
|
|
{{end}}
|
|
</td>
|
|
</tr>{{end}}
|
|
</table>
|
|
{{if .PickleHidden}}<p class="note">{{tn "card.pickle_hidden" .PickleHidden}}</p>{{end}}
|
|
<p class="note">{{t "card.weights_note"}}</p>
|
|
<p class="note">{{th "card.pinned_note" .Repo.ShortRevision}}</p>
|
|
{{else}}
|
|
<p class="empty">{{if .PickleHidden}}{{tn "card.only_pickle" .PickleHidden .Repo.ID}}{{else}}{{t "card.no_weights" .Repo.ID}}{{end}}</p>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{/* The card beneath a held version: what runs it, where, and how many. */}}
|
|
{{define "mirror_held_detail"}}{{$row := .Row}}{{$csrf := .CSRF}}{{$scopes := .D.Scopes}}
|
|
<div class="held-card">
|
|
<p class="repo-meta">
|
|
<span>{{if gt $row.Files 1}}{{tn "mirror.files" $row.Files}}{{else}}<code>{{$row.File}}</code>{{end}}</span>
|
|
<span>{{bytes $row.BytesDone}}</span>
|
|
{{with $row.SHA256}}<span translate="no">sha256 <code title="{{.}}">{{slice . 0 12}}</code></span>{{end}}
|
|
<span><code>{{$row.URL}}</code></span>
|
|
</p>
|
|
{{if not .D.Models}}
|
|
<p class="note">{{t "mirror.detail.not_in_catalog"}}
|
|
{{if $row.Serve}}{{else}}{{th "mirror.detail.use_serve"}}{{end}}</p>
|
|
{{end}}
|
|
{{range .D.Models}}
|
|
<section class="held-model">
|
|
<h3>{{.Name}}
|
|
{{with .View}}<span class="opt">{{t "mirror.detail.ready" .Ready .Desired}}{{if ne .Total .Ready}} · {{t "mirror.detail.in_all" .Total}}{{end}}</span>
|
|
{{if .Parked}}<span class="tag bad">{{t "mirror.use.parked"}}</span>{{else if .Unplaceable}}<span class="tag bad">{{t "mirror.detail.does_not_fit"}}</span>{{end}}{{end}}
|
|
</h3>
|
|
{{with .View.Reason}}<p class="note error-row">{{.}}</p>{{end}}
|
|
|
|
{{if .Replicas}}
|
|
<table class="rows compact replicas">
|
|
<tr><th>{{t "mirror.replica.col.replica"}}</th><th>{{t "mirror.replica.col.node"}}</th><th>{{t "mirror.replica.col.state"}}</th><th class="num">{{t "mirror.replica.col.age"}}</th><th class="num">CPU</th><th class="num">{{t "mirror.replica.col.memory"}}</th></tr>
|
|
{{range .Replicas}}{{$rep := .}}{{range $i, $s := .Nodes}}<tr>
|
|
<td>{{if eq $i 0}}<code>{{$rep.ID}}</code>{{end}}</td>
|
|
<td><a href="/admin/nodes/{{$s.Node}}">{{$s.Node}}</a>{{if $s.MemoryGB}} <span class="opt">{{$s.MemoryGB}} GB</span>{{end}}</td>
|
|
<td>{{if eq $i 0}}<span class="state state-{{if eq $rep.State "ready"}}ready{{else if eq $rep.State "failed"}}failed{{else}}starting{{end}}">{{$rep.StateText}}</span>
|
|
{{with $rep.Restart}}<span class="opt">{{t "mirror.replica.restart" .}}</span>{{end}}{{end}}</td>
|
|
<td class="num">{{if eq $i 0}}{{age $rep.Age}}{{end}}</td>
|
|
<td class="num">{{if $s.CPU}}{{t "mirror.replica.cores" (num $s.CPU 1)}}{{else}}—{{end}}</td>
|
|
<td class="num">{{if $s.MemBytes}}{{bytes $s.MemBytes}}{{else}}—{{end}}</td>
|
|
</tr>{{end}}{{end}}
|
|
</table>
|
|
{{else}}<p class="note">{{if eq .Mode "ondemand"}}{{t "mirror.detail.none_running_ondemand"}}{{else}}{{t "mirror.detail.none_running"}}{{end}}</p>{{end}}
|
|
|
|
{{if .Headroom}}
|
|
<p class="headroom">
|
|
{{.RoomText}}
|
|
{{with .PreemptText}}<br><span class="note">{{.}}</span>{{end}}
|
|
</p>
|
|
{{if and (not .Headroom.Limited) .Headroom.Blocked}}<details class="blocked"><summary>{{if .Headroom.More}}{{t "mirror.room.why_not_more"}}{{else}}{{t "mirror.room.why_not_another"}}{{end}}</summary>
|
|
<ul>{{range .Headroom.Blocked}}<li>{{.}}</li>{{end}}</ul></details>{{end}}
|
|
{{end}}
|
|
|
|
{{if not .Declared}}{{$mname := .Name}}
|
|
<div class="adapters">
|
|
<h4>{{t "mirror.adapters.heading"}} <a class="note" href="{{.FindAdapters}}">{{t "mirror.adapters.find"}}</a></h4>
|
|
{{if .Adapters}}<table class="compact rows">
|
|
<tr><th>{{t "mirror.adapters.asked_as"}}</th><th>{{t "mirror.adapters.file"}}</th><th class="num">{{t "mirror.adapters.scale"}}</th><th></th></tr>
|
|
{{range .Adapters}}{{$a := .}}<tr>
|
|
<td><code>{{.Name}}</code></td>
|
|
<td class="clip" title="{{.Source}}">{{.Source}}</td>
|
|
<td class="num">{{.EffectiveScale}}</td>
|
|
<td class="actions"><form method="post" action="/admin/mirror/adapters" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$csrf}}">
|
|
<input type="hidden" name="model" value="{{$mname}}">
|
|
<input type="hidden" name="open" value="{{$row.Open}}">
|
|
<input type="hidden" name="op" value="remove">
|
|
<input type="hidden" name="name" value="{{$a.Name}}">
|
|
<button type="submit" class="link danger">{{t "mirror.remove"}}</button></form></td>
|
|
</tr>{{end}}</table>
|
|
{{else}}<p class="note">{{t "mirror.adapters.none"}}</p>{{end}}
|
|
{{if .AdapterChoices}}{{$m := .}}
|
|
<form method="post" action="/admin/mirror/adapters" class="adapter-form">
|
|
<input type="hidden" name="csrf" value="{{$csrf}}">
|
|
<input type="hidden" name="model" value="{{.Name}}">
|
|
<input type="hidden" name="open" value="{{$row.Open}}">
|
|
<input type="hidden" name="op" value="add">
|
|
<div class="field-row">
|
|
<label>{{t "mirror.adapters.adapter_file"}} <select name="source" required data-suggest-into="name">
|
|
{{range .AdapterChoices}}<option value="{{.URL}}" data-suggest="{{.Suggest}}">{{.Label}}</option>{{end}}
|
|
</select></label>
|
|
<label>{{t "mirror.adapters.asked_as"}} <input type="text" name="name" value="{{(index .AdapterChoices 0).Suggest}}" required></label>
|
|
<label>{{t "mirror.adapters.scale"}} <input type="text" name="scale" value="1" size="4"></label>
|
|
<button type="submit">{{t "mirror.adapters.attach"}}</button>
|
|
</div>
|
|
</form>{{else}}<p class="note">{{t "mirror.adapters.no_choice"}}</p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Declared}}
|
|
<p class="note">{{t "mirror.detail.declared"}}</p>
|
|
{{else}}
|
|
<form method="post" action="/admin/mirror/replicas" class="replicas-form">
|
|
<input type="hidden" name="csrf" value="{{$csrf}}">
|
|
<input type="hidden" name="model" value="{{.Name}}">
|
|
<input type="hidden" name="open" value="{{$row.Open}}">
|
|
<div class="field-row">
|
|
<label>{{t "mirror.form.min"}}
|
|
<input type="number" name="min" value="{{.Min}}" min="0"{{if .MostNow}} max="{{.MostNow}}"{{end}} required></label>
|
|
<label>{{t "mirror.form.max"}}
|
|
<input type="number" name="max" value="{{.Max}}" min="0" max="64" required></label>
|
|
<label>{{t "mirror.form.class"}}
|
|
<select name="class">
|
|
<option value=""{{if eq .Class ""}} selected{{end}}>{{if eq .Mode "ondemand"}}{{t "mirror.class.default" (t "mirror.class.opportunistic")}}{{else}}{{t "mirror.class.default" (t "mirror.class.standard")}}{{end}}</option>
|
|
<option value="critical"{{if eq .Class "critical"}} selected{{end}}>{{t "mirror.class.critical"}}</option>
|
|
<option value="standard"{{if eq .Class "standard"}} selected{{end}}>{{t "mirror.class.standard"}}</option>
|
|
<option value="opportunistic"{{if eq .Class "opportunistic"}} selected{{end}}>{{t "mirror.class.opportunistic"}}</option>
|
|
</select></label>
|
|
{{if $scopes}}{{$owner := .Owner}}<label>{{t "mirror.form.owner"}}
|
|
<select name="owner">
|
|
<option value=""{{if eq $owner ""}} selected{{end}}>{{t "mirror.form.shared"}}</option>
|
|
{{range $scopes}}<option value="{{.Key}}"{{if eq .Key $owner}} selected{{end}}>{{.Name}}</option>{{end}}
|
|
</select></label>{{end}}
|
|
</div>
|
|
<div class="field-row">
|
|
<label>{{t "mirror.form.memory"}} <span class="opt">{{t "mirror.form.per_replica"}}</span>
|
|
<input type="number" name="memory" value="{{.MemoryGB}}" min="0"></label>
|
|
<label>{{t "mirror.form.cores"}} <span class="opt">{{t "mirror.form.cores_hint"}}</span>
|
|
<input type="number" name="cores" value="{{.CPUCores}}" min="0"></label>
|
|
<button type="submit">{{t "common.save"}}</button>
|
|
</div>
|
|
<p class="note">{{if .MostNow}}{{if and .Headroom .Headroom.ByPreempting}}{{t "mirror.form.most_now_preempting" .MostNow}}{{else}}{{t "mirror.form.most_now" .MostNow}}{{end}} {{end}}{{t "mirror.form.note"}}</p>
|
|
</form>
|
|
{{end}}
|
|
</section>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|