Merge branch 'nightly' into feature/windows-native-support

This commit is contained in:
Roland Tannous 2026-03-03 22:23:18 +04:00 committed by GitHub
commit 7bc235bed2
6 changed files with 125 additions and 61 deletions

View file

@ -103,9 +103,6 @@ function ModelRow({
{vramStatus === "tight" && (
<span className="text-[9px] font-medium text-amber-400">TIGHT</span>
)}
{vramStatus === "fits" && (
<span className="text-[9px] font-medium text-emerald-500/90">FIT</span>
)}
{meta ? (
<span className="text-[10px] text-muted-foreground">{meta}</span>
) : null}
@ -253,9 +250,6 @@ function GgufVariantExpander({
{fitStatus === "tight" && (
<span className="text-[9px] font-medium text-amber-400">TIGHT</span>
)}
{fitStatus === "fits" && (
<span className="text-[9px] font-medium text-emerald-500/90">FIT</span>
)}
<span className="text-[10px] text-muted-foreground">
{formatBytes(v.size_bytes)}
</span>

View file

@ -24,7 +24,7 @@ import {
import { HugeiconsIcon } from "@hugeicons/react";
import { useTrainingRuntimeStore } from "@/features/training";
import { Link, useRouterState } from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { motion } from "motion/react";
import { useState } from "react";
import { TOUR_OPEN_EVENT } from "@/features/tour";
@ -58,9 +58,9 @@ export function Navbar() {
return (
<header className="relative top-0 z-40 h-16 w-full">
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-4 sm:px-6">
<div className="mx-auto grid h-full max-w-7xl grid-cols-[1fr_auto_1fr] items-center px-4 sm:px-6">
{/* Left: logo */}
<Link to="/studio" className="flex items-center select-none">
<Link to="/studio" className="flex items-center justify-self-start select-none">
<img
src="/blacklogo.png"
alt="Unsloth"
@ -117,23 +117,21 @@ export function Navbar() {
/>
)}
<span className="relative z-10 flex items-center gap-1.5">
<AnimatePresence mode="popLayout">
{active && item.icon && (
<motion.span
key={item.href}
initial={{ width: 0, opacity: 0 }}
animate={{ width: "auto", opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: [0.165, 0.84, 0.44, 1] }}
className="overflow-hidden"
>
<HugeiconsIcon
icon={item.icon}
className="size-3.5 -mt-px"
/>
</motion.span>
)}
</AnimatePresence>
<span className="inline-flex size-3.5 items-center justify-center overflow-hidden">
<motion.span
initial={false}
animate={{
opacity: active ? 1 : 0,
scale: active ? 1 : 0.9,
}}
transition={{ duration: 0.2, ease: [0.165, 0.84, 0.44, 1] }}
>
<HugeiconsIcon
icon={item.icon}
className="size-3.5 -mt-px"
/>
</motion.span>
</span>
{item.label}
</span>
</Link>
@ -142,7 +140,7 @@ export function Navbar() {
</nav>
{/* Right: docs/tour desktop */}
<div className="hidden items-center gap-2 md:flex">
<div className="hidden items-center justify-self-end gap-2 md:flex">
<AnimatedThemeToggler
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground [&_svg]:size-4"
title="Toggle theme"
@ -182,17 +180,20 @@ export function Navbar() {
</HoverCardContent>
</HoverCard>
{tourId ? (
<button
type="button"
onClick={openTour}
className="flex h-9 items-center gap-1.5 rounded-md px-3 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Tour"
>
<HugeiconsIcon icon={CursorInfo02Icon} className="size-4" />
<span className="text-sm font-medium">Tour</span>
</button>
) : null}
<button
type="button"
onClick={tourId ? openTour : undefined}
className={cn(
"flex h-9 items-center gap-1.5 rounded-md px-3 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
!tourId && "invisible pointer-events-none",
)}
title="Tour"
aria-hidden={!tourId}
tabIndex={tourId ? 0 : -1}
>
<HugeiconsIcon icon={CursorInfo02Icon} className="size-4" />
<span className="text-sm font-medium">Tour</span>
</button>
</div>
{/* Right: mobile */}

View file

@ -33,11 +33,17 @@ import {
import { MODEL_TYPE_TO_HF_TASK } from "@/config/training";
import {
useDebouncedValue,
useGpuInfo,
useHfModelSearch,
useHfTokenValidation,
useInfiniteScroll,
} from "@/hooks";
import { formatCompact } from "@/lib/utils";
import {
type TrainingMethod as VramTrainingMethod,
type VramFitStatus,
buildModelVramMap,
} from "@/lib/vram";
import { useTrainingConfigStore } from "@/features/training";
import type { TrainingMethod } from "@/types/training";
import {
@ -50,6 +56,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useShallow } from "zustand/react/shallow";
export function ModelSelectionStep() {
const gpu = useGpuInfo();
const {
modelType,
selectedModel,
@ -93,6 +100,24 @@ export function ModelSelectionStep() {
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
// Match Studio behavior: only show exception signals (OOM/TIGHT) in training flows.
const vramMap = useMemo(() => {
const fitMap = buildModelVramMap(
hfResults,
trainingMethod as VramTrainingMethod,
gpu,
);
const map = new Map<string, { status: VramFitStatus | null; detail: string | null }>();
for (const r of hfResults) {
const fit = fitMap.get(r.id);
map.set(r.id, {
status: fit?.status ?? null,
detail: r.totalParams ? formatCompact(r.totalParams) : null,
});
}
return map;
}, [hfResults, gpu, trainingMethod]);
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
const { scrollRef, sentinelRef } = useInfiniteScroll(
fetchMore,
@ -218,19 +243,21 @@ export function ModelSelectionStep() {
>
<ComboboxList className="p-1 !max-h-none !overflow-visible">
{(id: string) => {
const r = hfResults.find((r) => r.id === id);
const sizeLabel = r?.totalParams
? formatCompact(r.totalParams)
: null;
const entry = vramMap.get(id);
const sizeLabel = entry?.detail ?? null;
const fitStatus = entry?.status ?? null;
const exceeds = fitStatus === "exceeds";
return (
<ComboboxItem
key={id}
value={id}
className="justify-between"
className={`justify-between ${exceeds ? "opacity-50" : ""}`}
>
<Tooltip>
<TooltipTrigger asChild={true}>
<span className="min-w-0 flex-1 truncate">
<span
className={`min-w-0 flex-1 truncate ${exceeds ? "line-through decoration-muted-foreground/50" : ""}`}
>
{id}
</span>
</TooltipTrigger>
@ -241,11 +268,23 @@ export function ModelSelectionStep() {
{id}
</TooltipContent>
</Tooltip>
{sizeLabel ? (
<span className="text-xs text-muted-foreground shrink-0">
{sizeLabel}
</span>
) : null}
<span className="flex items-center gap-1.5 shrink-0">
{fitStatus === "exceeds" && (
<span className="text-[9px] font-medium text-red-400">
OOM
</span>
)}
{fitStatus === "tight" && (
<span className="text-[9px] font-medium text-amber-400">
TIGHT
</span>
)}
{sizeLabel ? (
<span className="text-xs text-muted-foreground">
{sizeLabel}
</span>
) : null}
</span>
</ComboboxItem>
);
}}

View file

@ -37,8 +37,7 @@ import { formatCompact } from "@/lib/utils";
import {
type TrainingMethod as VramTrainingMethod,
type VramFitStatus,
checkVramFit,
estimateLoadingVram,
buildModelVramMap,
} from "@/lib/vram";
import {
listLocalModels,
@ -218,22 +217,23 @@ export function ModelSection() {
// Keyed by model id so the render callback is a simple O(1) lookup.
// Re-computes when the training method changes (QLoRA=4-bit vs LoRA/Full=fp16).
const vramMap = useMemo(() => {
const method = trainingMethod as VramTrainingMethod;
const fitMap = buildModelVramMap(
hfResults,
trainingMethod as VramTrainingMethod,
gpu,
);
const map = new Map<
string,
{ est: number; status: VramFitStatus | null; detail: string | null }
>();
for (const r of hfResults) {
const detail = r.totalParams ? formatCompact(r.totalParams) : null;
if (r.totalParams) {
const est = estimateLoadingVram(r.totalParams, method);
const status = gpu.available
? checkVramFit(est, gpu.memoryTotalGb)
: null;
map.set(r.id, { est, status, detail });
} else {
map.set(r.id, { est: 0, status: null, detail });
}
const fit = fitMap.get(r.id);
map.set(r.id, {
est: fit?.est ?? 0,
status: fit?.status ?? null,
detail,
});
}
return map;
}, [hfResults, gpu, trainingMethod]);

View file

@ -268,6 +268,7 @@
}
html {
@apply font-sans;
scrollbar-gutter: stable;
}
h1,
h2,

View file

@ -93,3 +93,32 @@ export function checkVramFit(
if (ratio <= 1.0) return "tight";
return "exceeds";
}
export interface ModelVramMapInput {
id: string;
totalParams?: number;
}
export interface ModelVramMapEntry {
est: number;
status: VramFitStatus | null;
}
export function buildModelVramMap(
models: ModelVramMapInput[],
method: TrainingMethod,
gpu: { available: boolean; memoryTotalGb: number },
): Map<string, ModelVramMapEntry> {
const map = new Map<string, ModelVramMapEntry>();
for (const model of models) {
if (!model.totalParams) {
map.set(model.id, { est: 0, status: null });
continue;
}
const est = estimateLoadingVram(model.totalParams, method);
const status = gpu.available ? checkVramFit(est, gpu.memoryTotalGb) : null;
map.set(model.id, { est, status });
}
return map;
}