Studio: add Export to GGUF button on finished training runs (#6475)
* Studio: add Export to GGUF button on finished training runs A completed run's 'Current Run' tab greys out, so it was unclear how to export it: GGUF export lives on the separate Export page and there was no link to it from a run. Add an 'Export to GGUF' button to the run progress card (shown for completed/stopped runs) that deep-links to the Export page with that run preselected via a new ?run= search param. The Export page reads the param, selects the run, defaults to GGUF, and picks the run's main checkpoint. No retraining is required to export a finished run. * Studio: fix export deep-link checkpoint preselect and edge cases Address review feedback on the Export to GGUF deep link: - Move the main-checkpoint auto-select effect after the model-change reset effect so it runs last; previously the reset cleared the checkpoint back to null in the same commit, leaving the field empty on a deep link. - Reset the applied-run ref when the ?run= param clears (e.g. navigating to /export via the sidebar) so a later manual reselect of the same run is not treated as a deep link. - Trim trailing slashes before taking the run output-dir basename so a path like /outputs/run/ still yields a name (the button no longer disappears). * Studio: hide Export to GGUF on runs superseded by a resume A stopped run whose output_dir was later reused by a resumed run is marked resumed_later by the backend; its on-disk contents no longer match the older run's metrics. Since the export deep link selects by output-dir basename, showing the button on such a run would export the newer continuation instead of the run being viewed. Carry resumed_later into the view data and hide the button when set. --------- Co-authored-by: danielhanchen <michaelhan2050@gmail.com>
This commit is contained in:
parent
586262decd
commit
b92123a5f0
6 changed files with 88 additions and 12 deletions
|
|
@ -12,10 +12,19 @@ const ExportPage = lazy(() =>
|
|||
})),
|
||||
);
|
||||
|
||||
export type ExportSearch = {
|
||||
// Preselect a training run on the Export page (its output-dir basename, which
|
||||
// equals the checkpoint scan's model name). Set when arriving from a run view.
|
||||
run?: string;
|
||||
};
|
||||
|
||||
export const Route = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/export",
|
||||
staticData: { title: "Export" },
|
||||
beforeLoad: () => requireAuth(),
|
||||
validateSearch: (search: Record<string, unknown>): ExportSearch => ({
|
||||
run: typeof search.run === "string" ? search.run : undefined,
|
||||
}),
|
||||
component: ExportPage,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import {
|
|||
PackageIcon,
|
||||
Search01Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
|
@ -211,6 +212,29 @@ export function ExportPage() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Apply the ?run= deep link (e.g. from a finished run's "Export to GGUF"
|
||||
// button) once its run appears in the checkpoint list: select the run and
|
||||
// default to GGUF. The main checkpoint is auto-selected further below, after
|
||||
// the model-change effect that clears the checkpoint.
|
||||
const { run: preselectRun } = useSearch({ from: "/export" });
|
||||
const appliedRunRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!preselectRun) {
|
||||
// Deep link cleared (e.g. navigated to /export via the sidebar): stop
|
||||
// treating the previously preselected run specially.
|
||||
appliedRunRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (models.length === 0) return;
|
||||
if (appliedRunRef.current === preselectRun) return;
|
||||
const match = models.find((m) => m.name === preselectRun);
|
||||
if (!match) return;
|
||||
appliedRunRef.current = preselectRun;
|
||||
setSourceMode("checkpoint");
|
||||
setSelectedModelIdx(match.name);
|
||||
setExportMethod("gguf");
|
||||
}, [preselectRun, models]);
|
||||
|
||||
// ---- Fetch local models for direct export ----
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
|
@ -362,6 +386,15 @@ export function ExportPage() {
|
|||
setCheckpoint(null);
|
||||
}, [selectedModelIdx]);
|
||||
|
||||
// For a ?run= deep link, default to the run's main checkpoint. Declared after
|
||||
// the reset effect above so it runs last and isn't clobbered back to null.
|
||||
useEffect(() => {
|
||||
if (appliedRunRef.current == null) return;
|
||||
if (appliedRunRef.current !== selectedModelIdx) return;
|
||||
if (checkpoint != null || checkpointsForModel.length === 0) return;
|
||||
setCheckpoint(checkpointsForModel[0].display_name);
|
||||
}, [selectedModelIdx, checkpoint, checkpointsForModel]);
|
||||
|
||||
// Auto-reset export method if incompatible with the selected model type
|
||||
useEffect(() => {
|
||||
if (!isAdapter && (exportMethod === "merged" || exportMethod === "lora")) {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ function mapToViewData(
|
|||
currentEpoch: metrics.final_epoch,
|
||||
currentNumTokens: metrics.final_num_tokens ?? null,
|
||||
outputDir: run.output_dir ?? null,
|
||||
resumedLater: run.resumed_later ?? false,
|
||||
progressPercent:
|
||||
run.total_steps && run.final_step
|
||||
? (run.final_step / run.total_steps) * 100
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { cn } from "@/lib/utils";
|
|||
import {
|
||||
ChartAverageIcon,
|
||||
DashboardSpeed01Icon,
|
||||
FolderExportIcon,
|
||||
Notebook01Icon,
|
||||
RamMemoryIcon,
|
||||
StopIcon,
|
||||
|
|
@ -153,6 +154,21 @@ export function ProgressSection({
|
|||
await navigate({ to: "/chat" });
|
||||
};
|
||||
|
||||
// A finished run can be exported to GGUF: deep-link to the Export page with
|
||||
// this run preselected (its output-dir basename is the export model name).
|
||||
const exportRunName = data.outputDir
|
||||
? (data.outputDir.replace(/[/\\]+$/, "").split(/[/\\]/).pop() || null)
|
||||
: null;
|
||||
const canExportGguf =
|
||||
!data.isTrainingRunning &&
|
||||
!!exportRunName &&
|
||||
!data.resumedLater &&
|
||||
(data.phase === "completed" || data.phase === "stopped");
|
||||
const handleExportGguf = () => {
|
||||
if (!exportRunName) return;
|
||||
void navigate({ to: "/export", search: { run: exportRunName } });
|
||||
};
|
||||
|
||||
const stoppedLoss = getDisplayMetric(
|
||||
data.isTrainingRunning,
|
||||
data.currentLoss,
|
||||
|
|
@ -219,18 +235,31 @@ export function ProgressSection({
|
|||
accent="emerald"
|
||||
className="shadow-border border border-border/60 bg-card/90 ring-0 backdrop-blur-sm"
|
||||
headerAction={
|
||||
isHistorical ? (
|
||||
<ConfigPopoverButton configItems={configItems} />
|
||||
) : (
|
||||
<LiveTrainingHeaderActions
|
||||
configItems={configItems}
|
||||
isTrainingRunning={data.isTrainingRunning}
|
||||
onOpenStopDialog={setStopDialogOpen}
|
||||
stopDialogOpen={stopDialogOpen}
|
||||
stopRequested={stopRequested}
|
||||
onSetStopRequested={setStopRequestedLocal}
|
||||
/>
|
||||
)
|
||||
<div className="flex items-center gap-2">
|
||||
{canExportGguf && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
onClick={handleExportGguf}
|
||||
>
|
||||
<HugeiconsIcon icon={FolderExportIcon} className="size-3.5" />
|
||||
{t("studio.progress.exportGguf")}
|
||||
</Button>
|
||||
)}
|
||||
{isHistorical ? (
|
||||
<ConfigPopoverButton configItems={configItems} />
|
||||
) : (
|
||||
<LiveTrainingHeaderActions
|
||||
configItems={configItems}
|
||||
isTrainingRunning={data.isTrainingRunning}
|
||||
onOpenStopDialog={setStopDialogOpen}
|
||||
stopDialogOpen={stopDialogOpen}
|
||||
stopRequested={stopRequested}
|
||||
onSetStopRequested={setStopRequestedLocal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]">
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@ export interface TrainingViewData {
|
|||
currentEpoch: number | null;
|
||||
currentNumTokens: number | null;
|
||||
outputDir: string | null;
|
||||
// True when a newer run reused this run's output_dir (resume), so its
|
||||
// on-disk contents no longer match this (older) run's metrics.
|
||||
resumedLater?: boolean;
|
||||
progressPercent: number;
|
||||
elapsedSeconds: number | null;
|
||||
etaSeconds: number | null;
|
||||
|
|
|
|||
|
|
@ -785,6 +785,7 @@ export const en = {
|
|||
progress: {
|
||||
title: "Training Progress",
|
||||
liveMetrics: "Live training metrics",
|
||||
exportGguf: "Export to GGUF",
|
||||
openConfig: "Open training config",
|
||||
configLabel: "Training Config",
|
||||
hyperparams: "Hyperparams",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue