Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ cscope.po.out


# End of https://www.toptal.com/developers/gitignore/api/tags

.DS_Store
20 changes: 15 additions & 5 deletions web/src/modules/inferences/CatalogPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "@modelsphere/ui";
import { Boxes, Library, MoreHorizontal, TriangleAlert } from "lucide-react";
import { useModulePath } from "@/shell";
import { api } from "@swiss/lib/api";
import { api, type CatalogInfo } from "@swiss/lib/api";
import { facetsOf, formatUplift, sorters, type Facets } from "@swiss/lib/catalog";
import { useCatalogChoice } from "@swiss/components/CatalogChoice";
import { useT } from "@/modules/inferences/i18n";
Expand All @@ -41,7 +41,7 @@ export function CatalogPage() {
queryFn: () => api.catalog(choice.selected),
enabled: !!choice.selected,
});
const all = useMemo(() => catalog.data?.index.models.map(facetsOf) ?? [], [catalog.data]);
const all = useMemo(() => catalog.data?.models.map(facetsOf) ?? [], [catalog.data]);
const options = useMemo(() => facetOptions(all), [all]);
const f = filtersFrom(params);
const set = (patch: Partial<CatalogFilters>) => setParams((prev) => withFilters(prev, patch), { replace: true });
Expand Down Expand Up @@ -70,11 +70,16 @@ export function CatalogPage() {
{choice.several && (
<FilterSelect
title={t("catalog.catalog")}
options={choice.catalogs.map((c) => ({ value: c.name, label: c.name }))}
options={choice.catalogs.map((c) => ({ value: c.name, label: c.name, description: c.schema === "v1" ? t("catalog.legacy") : undefined }))}
value={choice.selected}
onValueChange={(v) => v && choice.choose(v)}
/>
)}
{catalog.data?.schema === "v1" && (
<Badge variant="warning" title={t("catalog.legacyHint")}>
{t("catalog.legacy")}
</Badge>
)}
{facet("family")}
{facet("engine")}
{facet("hardware")}
Expand Down Expand Up @@ -181,7 +186,7 @@ function ModelCard({ x, catalog, to, onDeploy }: { x: Facets; catalog: string; t
<span className="truncate" title={m.source.hf}>
{m.source.hf}
</span>
<span>v{m.latest}</span>
{x.version && <span>v{x.version}</span>}
<span>{t("catalog.variants", { n: x.variants.length })}</span>
</div>

Expand All @@ -207,7 +212,7 @@ function ModelCard({ x, catalog, to, onDeploy }: { x: Facets; catalog: string; t
);
}

function NoCatalog({ catalogs, choose }: { catalogs: { name: string; source: string; default?: boolean }[]; choose: (name: string) => void }) {
function NoCatalog({ catalogs, choose }: { catalogs: CatalogInfo[]; choose: (name: string) => void }) {
const t = useT();
const p = useModulePath();
if (catalogs.length === 0) {
Expand All @@ -231,6 +236,11 @@ function NoCatalog({ catalogs, choose }: { catalogs: { name: string; source: str
<button key={c.name} type="button" onClick={() => choose(c.name)} className="flex w-full items-center gap-3 rounded-lg border bg-card p-3 text-left hover:border-primary/40">
<span className="font-medium">{c.name}</span>
{c.default && <Badge variant="secondary">{t("catalog.default")}</Badge>}
{c.schema === "v1" && (
<Badge variant="warning" title={t("catalog.legacyHint")}>
{t("catalog.legacy")}
</Badge>
)}
<span className="ms-auto truncate text-xs text-muted-foreground">{c.source}</span>
</button>
))}
Expand Down
84 changes: 45 additions & 39 deletions web/src/modules/inferences/ModelPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
AlertDescription,
Badge,
Button,
DataSelect,
DetailHeader,
Empty,
EmptyHeader,
Expand All @@ -21,8 +20,8 @@ import {
} from "@modelsphere/ui";
import { TriangleAlert } from "lucide-react";
import { useModulePath } from "@/shell";
import { api, type Node, type Variant } from "@swiss/lib/api";
import { comparison, formatUplift, httpLink, reportLink, variantKind, workloadSummary, type Kind } from "@swiss/lib/catalog";
import { api, type ModelVariant, type Node } from "@swiss/lib/api";
import { comparison, formatUplift, httpLink, reportLink, sharedVersion, variantKind, workloadSummary, type Kind } from "@swiss/lib/catalog";
import { gpuModels, vendorLabel } from "@swiss/lib/gpu";
import { useCatalogChoice } from "@swiss/components/CatalogChoice";
import { useT } from "@/modules/inferences/i18n";
Expand All @@ -35,19 +34,19 @@ export function ModelPage() {
const navigate = useNavigate();
const { name = "" } = useParams();
const [params, setParams] = useSearchParams();
const version = params.get("version") ?? undefined;
const tab = params.get("tab") === "info" ? "info" : "variants";
const choice = useCatalogChoice();
// The default variant resolved, for what only its file says: the served name.
const model = useQuery({
queryKey: ["model", name, version, choice.selected],
queryFn: () => api.model(name, version, choice.selected),
queryKey: ["model", choice.selected, name, "", ""],
queryFn: () => api.model(name, { catalog: choice.selected }),
enabled: !!choice.selected,
});
const index = useQuery({ queryKey: ["catalog", choice.selected], queryFn: () => api.catalog(choice.selected), enabled: !!choice.selected });
const nodes = useQuery({ queryKey: ["nodes"], queryFn: api.nodes, retry: false });
const cluster = useQuery({ queryKey: ["cluster"], queryFn: api.cluster });
const [deploying, setDeploying] = useState<DeployTarget | null>(null);
const onDeploy = cluster.data?.allowDeploy ? (x: { model: string; catalog: string; version?: string; variant?: string }) => setDeploying({ kind: "deploy", ...x }) : undefined;
const onDeploy = cluster.data?.allowDeploy ? (x: { model: string; catalog: string; variant?: string }) => setDeploying({ kind: "deploy", ...x }) : undefined;

const setParam = (k: string, v?: string) =>
setParams((prev) => {
Expand All @@ -74,43 +73,41 @@ export function ModelPage() {
);
}

const e = model.data.entry;
const im = index.data?.index.models.find((m) => m.name === name);
const versions = im?.versions.map((v) => v.version) ?? [e.version];
const deprecated = im?.deprecated;
const cmp = comparison(e.variants, e.version, im?.tuning);
const site = index.data?.index.site;
const variants = [...e.variants].sort((a, b) => Number(!!b.default) - Number(!!a.default));
const { model: m, variant: resolved, schema } = model.data;
const deprecated = m.deprecated;
const cmp = comparison(m.variants, m.tuning);
const site = index.data?.site;
const variants = [...m.variants].sort((a, b) => Number(!!b.default) - Number(!!a.default));
// On v1 every variant is at its entry's version; on v2 each is at its own.
const shared = sharedVersion(variants);
const digest = shared ? variants[0].digest : undefined;

return page(
<Tabs value={tab} onValueChange={(v) => setParam("tab", v === "info" ? "info" : undefined)} className="flex-col gap-4">
<DetailHeader
onBack={back}
backLabel={t("model.back")}
title={e.displayName || e.name}
subtitle={e.description}
title={m.displayName || m.name}
subtitle={m.description}
notice={deprecated ? { tone: "warning", title: t("model.deprecated", { reason: typeof deprecated === "string" ? deprecated : "" }) } : undefined}
actions={
onDeploy
? [{ key: "deploy", label: t("model.deploy"), onClick: () => onDeploy({ model: name, catalog: choice.selected, version }) }]
? [{ key: "deploy", label: t("model.deploy"), onClick: () => onDeploy({ model: name, catalog: choice.selected }) }]
: undefined
}
meta={[
{ label: t("model.fields.family"), value: e.family || "-" },
{ label: t("model.fields.source"), value: <span title={e.source.hf}>{e.source.hf}</span> },
{ label: t("model.fields.license"), value: e.license || "-" },
{ label: t("model.fields.family"), value: m.family || "-" },
{ label: t("model.fields.source"), value: <span title={m.source.hf}>{m.source.hf}</span> },
{ label: t("model.fields.version"), value: shared ?? t("model.perVariant") },
{
label: t("model.fields.version"),
label: t("model.fields.schema"),
value:
versions.length > 1 ? (
<DataSelect
className="h-7 w-32"
value={e.version}
onValueChange={(v) => setParam("version", v === im?.latest ? undefined : v)}
options={versions.map((v) => ({ value: v, label: v === im?.latest ? `${v} (${t("model.latest")})` : v }))}
/>
schema === "v1" ? (
<Badge variant="warning" title={t("catalog.legacyHint")}>
{t("catalog.legacy")}
</Badge>
) : (
e.version
schema
),
},
]}
Expand All @@ -136,11 +133,12 @@ export function ModelPage() {
<VariantCard
key={v.id}
v={v}
kind={variantKind(v, im?.tuning)}
kind={variantKind(v, m.tuning)}
version={shared ? undefined : v.version}
uplift={cmp && cmp.optimized === v.id ? cmp : null}
report={cmp && cmp.optimized === v.id ? reportLink(site, name, cmp.report) : undefined}
nodes={nodes.data?.nodes}
onDeploy={onDeploy ? () => onDeploy({ model: name, catalog: choice.selected, version, variant: v.id }) : undefined}
onDeploy={onDeploy ? () => onDeploy({ model: name, catalog: choice.selected, variant: v.id }) : undefined}
/>
))}
</div>
Expand All @@ -153,13 +151,12 @@ export function ModelPage() {
columns={1}
labelClassName="w-28 shrink-0"
items={[
{ label: t("model.fields.source"), value: [e.source.hf, e.source.revision].filter(Boolean).join(" @ ") },
{ label: t("model.fields.servedName"), value: e.servedName },
{ label: t("model.fields.license"), value: e.license },
{ label: t("model.fields.version"), value: e.version },
{ label: t("model.fields.digest"), value: e.digest && <code className="font-mono text-xs break-all">{e.digest}</code> },
{ label: t("model.fields.tags"), value: e.tags?.length ? e.tags.join(", ") : undefined },
{ label: t("model.fields.sizeLabel"), value: e.source.sizeGiB ? t("model.fields.size", { n: e.source.sizeGiB }) : undefined },
{ label: t("model.fields.source"), value: [m.source.hf, m.source.revision].filter(Boolean).join(" @ ") },
{ label: t("model.fields.servedName"), value: resolved.servedName },
{ label: t("model.fields.version"), value: shared },
{ label: t("model.fields.digest"), value: digest && <code className="font-mono text-xs break-all">{digest}</code> },
{ label: t("model.fields.tags"), value: m.tags?.length ? m.tags.join(", ") : undefined },
{ label: t("model.fields.sizeLabel"), value: m.source.sizeGiB ? t("model.fields.size", { n: m.source.sizeGiB }) : undefined },
].filter((i) => i.value)}
/>
</SectionCard>
Expand All @@ -172,13 +169,16 @@ export function ModelPage() {
function VariantCard({
v,
kind,
version,
uplift,
report,
nodes,
onDeploy,
}: {
v: Variant;
v: ModelVariant;
kind: Kind;
// The variant's own version, when the model's variants are not all at one.
version?: string;
uplift: ReturnType<typeof comparison>;
report?: string;
nodes?: Node[];
Expand All @@ -193,7 +193,13 @@ function VariantCard({
title={
<span className="inline-flex flex-wrap items-center gap-2">
<span className="font-mono">{v.id}</span>
{version && <span className="text-xs text-muted-foreground">v{version}</span>}
{v.default && <Badge variant="secondary">{t("model.default")}</Badge>}
{v.deprecated && (
<Badge variant="warning" title={typeof v.deprecated === "string" ? v.deprecated : undefined}>
{t("catalog.deprecated")}
</Badge>
)}
{kind.optimized && <Badge variant="success">{t("model.optimized")}</Badge>}
{kind.baseline && <Badge variant="outline">{t("model.baseline")}</Badge>}
<Badge variant="outline">{v.engine}</Badge>
Expand Down
23 changes: 9 additions & 14 deletions web/src/modules/inferences/SetupPage.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,39 @@
import { useNavigate } from "react-router";
import { useQuery } from "@tanstack/react-query";
import { Alert, AlertDescription, PageBanner, Skeleton } from "@modelsphere/ui";
import { Settings2, TriangleAlert } from "lucide-react";
import { PageBanner, Skeleton } from "@modelsphere/ui";
import { Settings2 } from "lucide-react";
import { useModulePath } from "@/shell";
import { api, type ProfileResponse } from "@swiss/lib/api";
import { useT } from "@/modules/inferences/i18n";
import { ProfileEditorBody, SaveButton, useProfileEditor } from "@/modules/inferences/components/ProfileEditor";

// The first run only: the gate sends a site that has a profile to the site
// profile page, so this always starts from swissd's default.
export function SetupPage() {
const t = useT();
const existing = useQuery({ queryKey: ["profile"], queryFn: api.profile, retry: false });
const template = useQuery({ queryKey: ["profile-template"], queryFn: api.profileTemplate, enabled: existing.isSuccess && !existing.data.profile });
const start = existing.data?.profile ? existing.data : template.data;
const template = useQuery({ queryKey: ["profile-template"], queryFn: api.profileTemplate });
return (
<div className="flex h-full flex-col">
<PageBanner title={t("profile.setup.title")} description={t("profile.setup.description")} icon={<Settings2 className="size-5" />} />
<div className="min-h-0 flex-1 overflow-auto bg-surface-page p-4">
<div className="mx-auto max-w-4xl space-y-4">
{existing.data?.error && (
<Alert variant="destructive">
<TriangleAlert />
<AlertDescription>{t("profile.parseError", { error: existing.data.error })}</AlertDescription>
</Alert>
)}
{start?.profile ? <Setup start={start} saveLabel={t(existing.data?.profile ? "profile.setup.saveExisting" : "profile.setup.save")} /> : <Skeleton className="h-96 w-full" />}
{template.data?.profile ? <Setup start={template.data} /> : <Skeleton className="h-96 w-full" />}
</div>
</div>
</div>
);
}

function Setup({ start, saveLabel }: { start: ProfileResponse; saveLabel: string }) {
function Setup({ start }: { start: ProfileResponse }) {
const t = useT();
const p = useModulePath();
const navigate = useNavigate();
const editor = useProfileEditor(start.profile!, start.yaml ?? "", () => navigate(p(""), { replace: true }));
return (
<>
<ProfileEditorBody editor={editor} />
<div className="flex justify-end">
<SaveButton editor={editor} label={saveLabel} />
<SaveButton editor={editor} label={t("profile.setup.save")} />
</div>
</>
);
Expand Down
Loading
Loading