images/video: apply the fit-on-device toggle to catalog group rows

The Recommended list's fit-on-device toggle filtered the live Hub rows and the
flat curated rows, but the canonical catalog GROUP rows (Images / Video pages)
were gated only by the format filter. A bare click on a filtered list could then
still start an OOM load the toggle was meant to hide (LTX-2 base at 90 GB, the
Wan2.2-A14B MoE at 114 GB, both bf16-only with no GGUF fallback).

Add catalogGroupFitsDevice: a group stays visible when at least one artifact can
actually run here (already downloaded, a GGUF whose quant ladder self-fits, or a
sized artifact within 0.7*GPU + 0.7*RAM), mirroring the Recommended fit predicate
across a group's formats. Gate the search (matchedCatalogGroups) and the
Recommended-section catalog rows (render + roving keys) on it. Node-native
catalog:check assertions cover the over-budget, GGUF-fallback, downloaded, unknown
-budget, and datacenter-budget cases.
This commit is contained in:
Daniel Han 2026-07-09 09:04:16 +00:00
parent 557fc0674a
commit 5e3ab0a8c4
3 changed files with 104 additions and 9 deletions

View file

@ -13,6 +13,7 @@ import {
IMAGE_CATALOG,
VIDEO_CATALOG,
canonicalKeyFor,
catalogGroupFitsDevice,
catalogToModelOptions,
classifyGgufFit,
groupForRepoId,
@ -455,6 +456,49 @@ assert.equal(
assert.equal(loadSpecFor("Tongyi-MAI/Z-Image-Turbo", IMAGE_CATALOG)?.kind, "pipeline");
assert.equal(loadSpecFor("Qwen/Qwen-Image-2512", IMAGE_CATALOG)?.kind, "pipeline");
// ── catalogGroupFitsDevice (the fit-on-device toggle for catalog rows) ─────────
const wanA14b = groupForRepoId("Wan-AI/Wan2.2-T2V-A14B-Diffusers", VIDEO_CATALOG);
const ltxBase = groupForRepoId("Lightricks/LTX-2", VIDEO_CATALOG);
const hunyuanFit = groupForRepoId(
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v",
VIDEO_CATALOG,
);
assert.ok(wanA14b && ltxBase && hunyuanFit && ltxGroup);
const consumer = { gpuGb: 24, systemRamGb: 64 }; // budget 61.6 GB
// A bare-bf16 group over budget is hidden: this is the OOM the toggle must catch.
assert.equal(catalogGroupFitsDevice(wanA14b, consumer, notDownloaded), false); // 114 GB
assert.equal(catalogGroupFitsDevice(ltxBase, consumer, notDownloaded), false); // 90 GB
// A sized bf16 group that fits the budget stays visible (Hunyuan 40/52 GB <= 61.6).
assert.equal(catalogGroupFitsDevice(hunyuanFit, consumer, notDownloaded), true);
// But on a tiny device even those are hidden.
assert.equal(
catalogGroupFitsDevice(hunyuanFit, { gpuGb: 8, systemRamGb: 8 }, notDownloaded),
false,
);
// A GGUF in the group is always runnable (its quant ladder self-fits + offloads),
// so LTX-2.3 stays visible even on a tiny card despite its 90 GB BF16 sibling.
assert.equal(
catalogGroupFitsDevice(ltxGroup, { gpuGb: 4, systemRamGb: 4 }, notDownloaded),
true,
);
// An already-downloaded artifact keeps its group visible regardless of budget.
assert.equal(
catalogGroupFitsDevice(
wanA14b,
{ gpuGb: 8, systemRamGb: 8 },
(id) => id === "Wan-AI/Wan2.2-T2V-A14B-Diffusers",
),
true,
);
// Unknown device budget keeps everything (we cannot tell), even a 114 GB group.
assert.equal(catalogGroupFitsDevice(wanA14b, { gpuGb: 0, systemRamGb: 0 }, notDownloaded), true);
// On a B200-class budget the large bf16 groups fit and stay visible.
assert.equal(
catalogGroupFitsDevice(wanA14b, { gpuGb: 192, systemRamGb: 256 }, notDownloaded),
true,
);
// ── groupMatchesQuery ──────────────────────────────────────────────────────────
assert.ok(groupMatchesQuery(qwenGroup, "qwen"));

View file

@ -675,3 +675,31 @@ export function pickDefaultArtifact(
(a, b) => (a.approxSizeGb ?? Infinity) - (b.approxSizeGb ?? Infinity),
)[0];
}
/** Whether the "fit on device" toggle should keep a catalog group. A group stays
* visible when at least one artifact can actually run here: one already on disk
* (the user has it), a GGUF (its per-quant ladder self-fits and the backend GGUF
* path plans its own offload), or a sized artifact whose curated footprint is
* within the 0.7*GPU + 0.7*RAM budget. A group of only over-budget (or unsized)
* full-precision artifacts -- e.g. the LTX-2 base at 90 GB or Wan2.2-A14B at
* 114 GB on a consumer card -- is hidden, so a bare click on a fit-filtered list
* can no longer start an OOM load the toggle was meant to hide. An unknown device
* budget keeps everything (we cannot tell). Mirrors fitsResident/the Recommended
* fit predicate, extended across a group's formats. */
export function catalogGroupFitsDevice(
group: CatalogGroup,
budget: DeviceBudget,
isDownloaded: (repoId: string) => boolean,
): boolean {
const budgetGb =
Math.max(0, budget.gpuGb || 0) * 0.7 +
Math.max(0, budget.systemRamGb || 0) * 0.7;
if (budgetGb <= 0) return true;
return group.artifacts.some((a) => {
if (isDownloaded(a.repoId)) return true;
// A GGUF's quant ladder self-fits (llama-server offloads), so it is always a
// runnable fallback -- matching pickDefaultArtifact returning it on any budget.
if (a.format === "gguf") return true;
return a.approxSizeGb !== undefined && a.approxSizeGb <= budgetGb;
});
}

View file

@ -116,6 +116,7 @@ import {
type CatalogGroup,
type ModelArtifact,
artifactForRepoId,
catalogGroupFitsDevice,
groupForRepoId,
groupMatchesQuery,
pickDefaultArtifact,
@ -2435,6 +2436,27 @@ export function HubModelPicker({
return map;
}, [results, recommendedSearch.results]);
// The fit-on-device toggle hides a catalog group with nothing runnable here,
// exactly as it hides an over-budget Recommended row -- otherwise a bare click
// on a fit-filtered list could still start a 90-114 GB OOM load (LTX-2 base,
// Wan2.2-A14B). Off = every group passes.
const catalogGroupPassesFit = useCallback(
(g: CatalogGroup) =>
!fitOnDeviceOnly ||
catalogGroupFitsDevice(g, deviceBudget, isRepoDownloaded),
[fitOnDeviceOnly, deviceBudget, isRepoDownloaded],
);
// Curated catalog rows for the Recommended section (no query): format toggle +
// the same fit gate as the live Recommended rows. Shared by the render and the
// roving-key list so navigation matches what is on screen.
const recommendedCatalogGroups = useMemo(() => {
if (!catalog) return [];
return catalog.filter(
(g) => catalogGroupMatchesFormat(g, formatFilter) && catalogGroupPassesFit(g),
);
}, [catalog, formatFilter, catalogGroupPassesFit]);
// Recommended models that match the current search query
// Catalog groups matching a typed query (old ids, format tokens, quant names
// all match); rendered as canonical rows above the remaining search results.
@ -2443,9 +2465,10 @@ export function HubModelPicker({
return catalog.filter(
(g) =>
groupMatchesQuery(g, debouncedQuery.trim()) &&
catalogGroupMatchesFormat(g, formatFilter),
catalogGroupMatchesFormat(g, formatFilter) &&
catalogGroupPassesFit(g),
);
}, [catalog, showHfSection, debouncedQuery, formatFilter]);
}, [catalog, showHfSection, debouncedQuery, formatFilter, catalogGroupPassesFit]);
const filteredRecommendedIds = useMemo(() => {
if (!showHfSection) return [];
@ -2684,9 +2707,9 @@ export function HubModelPicker({
// flat curated safetensors rows.
if (catalog) {
keys.push(
...catalog
.filter((g) => catalogGroupMatchesFormat(g, formatFilter))
.map((g) => makeModelOptionKey("catalog-group", g.canonicalId)),
...recommendedCatalogGroups.map((g) =>
makeModelOptionKey("catalog-group", g.canonicalId),
),
);
} else {
keys.push(
@ -2713,9 +2736,9 @@ export function HubModelPicker({
fineTunedRows,
fineTunedCollapsed,
filteredRecommendedIds,
formatFilter,
hfIds,
matchedCatalogGroups,
recommendedCatalogGroups,
sortedLmStudio,
lmStudioCollapsed,
recommendedRows,
@ -4138,9 +4161,9 @@ export function HubModelPicker({
listing's task tags. Without one (legacy), the flat curated
safetensors rows. */}
{catalog
? catalog
.filter((g) => catalogGroupMatchesFormat(g, formatFilter))
.map((g) => renderCatalogGroupRow(g, "catalog-group"))
? recommendedCatalogGroups.map((g) =>
renderCatalogGroupRow(g, "catalog-group"),
)
: curatedSafetensorsRows.map((m) => {
const optionKey = makeModelOptionKey(
"curated-safetensors",