Fine Tuning Job Monitor
blockaiLLM fine-tuning job telemetry, loss curve convergence tracking, GPU cluster utilization, hyperparameter manifests, and LoRA checkpoint exporter.
Installation
$pnpm dlx uipkge-ng@latest add fine-tuning-job-monitor$npx uipkge-ng@latest add fine-tuning-job-monitor$yarn dlx uipkge-ng@latest add fine-tuning-job-monitor$bunx uipkge-ng@latest add fine-tuning-job-monitorManifest:
https://uipkge.dev/r/angular/fine-tuning-job-monitor.jsonInstalls to:src/app/components/blocks/fine-tuning-job-monitor/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
CheckpointRecordinterface CheckpointRecord {
id: string
epoch: number
step: number
totalSteps: number
trainLoss: number
valLoss: number
size: string
filename: string
createdAgo: string
isBest?: boolean
status: 'saved' | 'best' | 'evaluating'
}LossReadinginterface LossReading {
step: number
trainLoss: number
valLoss?: number
isProjected?: boolean
label?: string
}FineTuningJobNotificationinterface FineTuningJobNotification {
title: string
message: string
type: 'success' | 'info' | 'destructive'
}Files installed (2)
src/app/components/blocks/fine-tuning-job-monitor/fine-tuning-job-monitor.component.ts50.9 kB
import { Component, DestroyRef, Input, inject, signal, ChangeDetectionStrategy } from '@angular/core' import { Activity, AlertCircle, Bot, Check, CheckCircle2, Clock, Copy, Cpu, Database, Download, Gauge, Loader2, LucideAngularModule, Rocket, Save, ShieldCheck, Sliders, Sparkles, TrendingDown, XCircle, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface CheckpointRecord { id: string epoch: number step: number totalSteps: number trainLoss: number valLoss: number size: string filename: string createdAgo: string isBest?: boolean status: 'saved' | 'best' | 'evaluating' } export interface LossReading { step: number trainLoss: number valLoss?: number isProjected?: boolean label?: string } export interface FineTuningJobNotification { title: string message: string type: 'success' | 'info' | 'destructive' } const defaultLossData: LossReading[] = [ { step: 0, trainLoss: 1.84, valLoss: 1.92, label: 'Init' }, { step: 25, trainLoss: 1.32, valLoss: undefined }, { step: 50, trainLoss: 0.95, valLoss: 1.02, label: 'Warmup Done' }, { step: 75, trainLoss: 0.915, valLoss: undefined }, { step: 100, trainLoss: 0.892, valLoss: 0.945, label: 'Epoch 1 (Ckpt #1)' }, { step: 125, trainLoss: 0.79, valLoss: undefined }, { step: 150, trainLoss: 0.71, valLoss: 0.748, label: 'Step 150' }, { step: 175, trainLoss: 0.64, valLoss: undefined }, { step: 200, trainLoss: 0.584, valLoss: 0.612, label: 'Epoch 2 (Ckpt #2)' }, { step: 225, trainLoss: 0.535, valLoss: undefined }, { step: 250, trainLoss: 0.495, valLoss: 0.52, label: 'Step 250' }, { step: 275, trainLoss: 0.46, valLoss: undefined }, { step: 300, trainLoss: 0.435, valLoss: 0.468, label: 'Epoch 3 (Ckpt #3)' }, { step: 320, trainLoss: 0.412, valLoss: 0.458, label: 'Step 320 (Live)' }, // Projected points { step: 360, trainLoss: 0.395, valLoss: undefined, isProjected: true }, { step: 400, trainLoss: 0.38, valLoss: 0.415, isProjected: true, label: 'Epoch 4 (Proj)' }, { step: 450, trainLoss: 0.365, valLoss: undefined, isProjected: true }, { step: 500, trainLoss: 0.355, valLoss: 0.39, isProjected: true, label: 'Epoch 5 (Target)' }, ] const defaultCheckpoints: CheckpointRecord[] = [ { id: 'ckpt-3', epoch: 3, step: 300, totalSteps: 500, trainLoss: 0.435, valLoss: 0.468, size: '1.2 GB LoRA', filename: 'adapter_model_step300.safetensors', createdAgo: '24m ago', isBest: true, status: 'best', }, { id: 'ckpt-2', epoch: 2, step: 200, totalSteps: 500, trainLoss: 0.584, valLoss: 0.612, size: '1.2 GB LoRA', filename: 'adapter_model_step200.safetensors', createdAgo: '1h 44m ago', isBest: false, status: 'saved', }, { id: 'ckpt-1', epoch: 1, step: 100, totalSteps: 500, trainLoss: 0.892, valLoss: 0.945, size: '1.2 GB LoRA', filename: 'adapter_model_step100.safetensors', createdAgo: '3h 02m ago', isBest: false, status: 'saved', }, ] // SVG telemetry loss-curve geometry (viewBox 0 0 740 220). Steps map to x, loss 0..2 to y. const SVG_WIDTH = 740 const PAD_LEFT = 45 const PAD_RIGHT = 695 const USABLE_W = PAD_RIGHT - PAD_LEFT const PAD_TOP = 25 const PAD_BOTTOM = 190 const USABLE_H = PAD_BOTTOM - PAD_TOP /** * FineTuningJobMonitor -- the React block, 1:1. LLM fine-tuning job telemetry: a header * with copyable job id, live status and cancel / download actions, four telemetry cards * (training loss, validation loss, learning rate, GPU cluster), a dual-series SVG loss * convergence chart with step milestones, a checkpoint history table with deploy / * download actions, and a hyperparameters & dataset manifest. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-fine-tuning-job-monitor, [ui-fine-tuning-job-monitor]', standalone: true, // React renders the root <div> itself: keep this wrapper out of the layout. host: { class: 'contents' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiSeparatorComponent, UiTableComponent, UiTableBodyComponent, UiTableCellComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="fine-tuning-job-monitor" [class]="rootClass"> <!-- Header Section --> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <div class="flex items-center gap-2.5"> <div class="border-info/30 bg-info/10 text-info flex size-10 items-center justify-center rounded-xl border shadow-xs" > <lucide-icon [img]="Bot" class="size-5" /> </div> <div> <div class="flex flex-wrap items-center gap-2"> <h1 class="text-foreground font-mono text-xl font-bold tracking-tight break-all sm:text-2xl"> {{ fineTunedModelName }} </h1> <button type="button" class="bg-muted hover:bg-muted/80 text-muted-foreground hover:text-foreground border-border/80 inline-flex min-h-6 items-center gap-1 rounded-md border px-2 py-0.5 font-mono text-xs transition-colors" [title]="'Copy Job ID: ' + jobId" (click)="copyJobId()" > <span class="text-foreground font-medium">{{ jobId }}</span> @if (copiedJobId()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3 opacity-70" /> } </button> </div> </div> </div> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs"> <div class="flex items-center gap-1.5"> <span class="text-muted-foreground">Base Model:</span> <span class="text-foreground font-medium">{{ baseModel }}</span> </div> <span>·</span> <!-- Live Status Badge --> <div class="flex items-center gap-1.5"> @if (!isJobCancelled()) { <span class="relative flex size-2"> <span class="bg-info absolute inline-flex h-full w-full rounded-full opacity-75"></span> <span class="bg-info relative inline-flex size-2 rounded-full"></span> </span> } @else { <span class="bg-destructive size-2 rounded-full"></span> } <span [class]="statusClass()"> {{ isJobCancelled() ? 'Job Cancelled by Operator' : status }} </span> </div> <span>·</span> <div class="flex items-center gap-1"> <lucide-icon [img]="Clock" class="size-3.5" /> <span class="tabular-nums">Elapsed: {{ elapsedTime }}</span> <span class="text-muted-foreground">· ETA {{ etaRemaining }}</span> </div> </div> </div> <!-- Action Buttons --> <div class="flex flex-wrap items-center gap-2.5"> <!-- px-2.5: React's size="sm" gets it from has-[>svg], but here the svg sits inside <lucide-icon>. --> <button ui-button variant="ghost" size="sm" [disabled]="isCancelling() || isJobCancelled()" class="text-muted-foreground hover:bg-destructive/10 hover:text-destructive h-8.5 gap-1.5 px-2.5 text-xs font-medium" (click)="handleCancelJob()" > <!-- [class] (not class=) feeds only the svg, so animate-spin is not applied twice (host + svg). --> <lucide-icon [img]="XCircle" [class]="cancelIconClass()" /> <span>{{ isJobCancelled() ? 'Cancelled' : isCancelling() ? 'Cancelling...' : 'Cancel Job' }}</span> </button> <button ui-button aria-label="Download attachment" variant="default" size="sm" [disabled]="isDownloadingWeights()" class="h-8.5 gap-2 px-2.5 text-xs font-medium shadow-xs" (click)="handleDownloadWeights()" > <lucide-icon [img]="Download" [class]="downloadIconClass()" /> <span>{{ isDownloadingWeights() ? 'Preparing Archive...' : 'Download Weights / LoRA Adapter' }}</span> </button> </div> </div> <!-- Notification Toast Banner --> @if (activeNotification(); as note) { <div [class]="notificationClass(note)" role="status"> <div class="flex items-center gap-2.5"> @if (note.type === 'success') { <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" /> } @else { <lucide-icon [img]="AlertCircle" class="text-destructive size-4 shrink-0" /> } <div> <span class="text-foreground font-semibold">{{ note.title }}: </span> <span class="text-muted-foreground">{{ note.message }}</span> </div> </div> <button ui-button aria-label="Dismiss notification" variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-6" (click)="activeNotification.set(null)" > <span class="text-xs">✕</span> </button> </div> } <!-- 4 Training Telemetry Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- 1. Current Training Loss --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2"> <div class="border-info/20 bg-info/10 text-info flex size-8 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="Activity" class="size-4" /> </div> <h3 ui-card-title class="truncate text-sm font-medium">Training Loss</h3> </div> <span ui-badge wrap variant="outline" class="font-mono text-xs tabular-nums"> Step {{ currentStep }}/{{ totalSteps }} </span> </div> </div> <div ui-card-content class="space-y-2.5 pt-1"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums"> {{ fixed3(trainingLoss) }} </span> <span class="text-muted-foreground text-xs font-normal">cross-entropy</span> </div> <div class="text-success flex items-center gap-1 text-xs font-semibold"> <lucide-icon [img]="TrendingDown" class="size-3.5" /> <span class="tabular-nums">-77.6%</span> </div> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 gap-y-0.5"> <span>Initial Loss:</span> <span class="text-foreground font-mono font-medium tabular-nums">{{ fixed3(initialLoss) }}</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">Target convergence corridor: < 0.450</p> </div> </div> </div> <!-- 2. Validation Loss --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2"> <div class="border-success/20 bg-success/10 text-success flex size-8 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="ShieldCheck" class="size-4" /> </div> <h3 ui-card-title class="truncate text-sm font-medium">Validation Loss</h3> </div> <span ui-badge wrap variant="success" class="shrink-0 text-xs">Generalizing</span> </div> </div> <div ui-card-content class="space-y-2.5 pt-1"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums"> {{ fixed3(validationLoss) }} </span> <span class="text-muted-foreground text-xs font-normal">eval set</span> </div> <span class="text-success text-success text-xs font-medium tabular-nums">+0.046 gap</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Overfitting Status:</span> <span class="text-success font-medium">No Overfitting</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">Evaluated every 50 steps on 4,250 rows</p> </div> </div> </div> <!-- 3. Learning Rate --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2"> <div class="border-chart-1/20 bg-chart-1/10 text-chart-1 flex size-8 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="Gauge" class="size-4" /> </div> <h3 ui-card-title class="truncate text-sm font-medium">Learning Rate</h3> </div> <span ui-badge wrap variant="outline" class="shrink-0 font-mono text-xs">Cosine</span> </div> </div> <div ui-card-content class="space-y-2.5 pt-1"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground font-mono text-3xl font-bold tracking-tight tabular-nums"> {{ learningRate }} </span> <span class="text-muted-foreground text-xs font-normal">AdamW</span> </div> <span class="text-muted-foreground text-xs font-medium tabular-nums">Decay active</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Schedule:</span> <span class="text-foreground font-medium">Cosine Decay (10% warmup)</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">Peak: 1.5e-4 · Min: 1.0e-6</p> </div> </div> </div> <!-- 4. GPU Compute Cluster --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2"> <div class="border-warning/20 bg-warning/10 text-warning flex size-8 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="Cpu" class="size-4" /> </div> <h3 ui-card-title class="truncate text-sm font-medium">GPU Compute Cluster</h3> </div> <span ui-badge wrap variant="success" class="shrink-0 text-xs tabular-nums"> {{ gpuUtilization }}% Utilized </span> </div> </div> <div ui-card-content class="space-y-2.5 pt-1"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div> <span class="text-foreground text-lg font-bold tracking-tight sm:text-xl">{{ gpuCluster }}</span> </div> </div> <div class="space-y-1"> <div ui-progress [value]="gpuUtilization" class="[&_[data-slot=progress-indicator]]:bg-success h-1.5" ></div> <div class="text-muted-foreground flex items-center justify-between gap-x-2 text-xs"> <span> VRAM: <span class="text-foreground font-mono font-medium tabular-nums">{{ vramUsage }}</span> </span> <span class="font-mono tabular-nums">{{ locale(tokensPerSec) }} tok/s</span> </div> </div> </div> </div> </div> <!-- Training & Validation Loss SVG Trend Curve --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <lucide-icon [img]="Activity" class="text-primary size-4" /> <h3 ui-card-title class="text-base font-semibold">Training & Validation Loss Convergence</h3> </div> <p ui-card-description class="text-xs"> Dual-series loss trajectory across {{ totalSteps }} gradient steps. Solid lines represent observed loss; dashed lines denote projected cosine decay trajectory. </p> </div> <!-- Legend & Metrics --> <div class="flex flex-wrap items-center gap-3"> <div class="text-muted-foreground flex flex-wrap items-center gap-3 text-xs"> <div class="flex items-center gap-1.5"> <span class="bg-info size-2.5 rounded-full"></span> <span class="text-foreground font-medium">Training Loss ({{ fixed3(trainingLoss) }})</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-success size-2.5 rounded-full"></span> <span class="text-foreground font-medium">Validation Loss ({{ fixed3(validationLoss) }})</span> </div> <div class="flex items-center gap-1.5"> <span class="border-destructive bg-destructive h-0.5 w-3 border-b border-dashed"></span> <span class="text-muted-foreground">Target (< 0.400)</span> </div> </div> </div> </div> </div> <div ui-card-content class="space-y-4"> <!-- SVG Chart Canvas Area --> <div class="bg-muted/20 border-border/60 relative w-full overflow-x-auto rounded-lg border p-2 sm:p-4"> <svg class="h-64 w-full max-w-[600px] min-w-full" [attr.viewBox]="'0 0 ' + svgWidth + ' 220'" preserveAspectRatio="none" > <defs> <linearGradient id="grad-train-loss-angular" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="var(--color-sky-500, #0ea5e9)" stop-opacity="0.28" /> <stop offset="100%" stop-color="var(--color-sky-500, #0ea5e9)" stop-opacity="0.0" /> </linearGradient> </defs> <!-- Shaded Target Convergence Zone (Loss 0.00 to 0.40 -> Y: 190 to 157) --> <rect [attr.x]="padLeft" [attr.y]="targetLossY" [attr.width]="usableW" [attr.height]="padBottom - targetLossY" fill="var(--color-emerald-500, #10b981)" fill-opacity="0.05" rx="4" /> <!-- Horizontal Grid Lines --> <line [attr.x1]="padLeft" y1="25" [attr.x2]="padRight" y2="25" stroke="currentColor" class="text-border/40" stroke-width="1" /> <line [attr.x1]="padLeft" y1="66" [attr.x2]="padRight" y2="66" stroke="currentColor" class="text-border/40" stroke-width="1" /> <line [attr.x1]="padLeft" y1="107" [attr.x2]="padRight" y2="107" stroke="currentColor" class="text-border/40" stroke-width="1" /> <line [attr.x1]="padLeft" y1="148" [attr.x2]="padRight" y2="148" stroke="currentColor" class="text-border/40" stroke-width="1" /> <line [attr.x1]="padLeft" [attr.y1]="targetLossY" [attr.x2]="padRight" [attr.y2]="targetLossY" stroke="var(--color-rose-500, #f43f5e)" stroke-width="1.5" stroke-dasharray="4 4" /> <line [attr.x1]="padLeft" [attr.y1]="padBottom" [attr.x2]="padRight" [attr.y2]="padBottom" stroke="currentColor" class="text-border/40" stroke-width="1" /> <!-- Y-Axis Scale Labels --> <text [attr.x]="padLeft - 8" y="29" text-anchor="end" class="fill-muted-foreground font-mono text-xs"> 2.00 </text> <text [attr.x]="padLeft - 8" y="70" text-anchor="end" class="fill-muted-foreground font-mono text-xs"> 1.50 </text> <text [attr.x]="padLeft - 8" y="111" text-anchor="end" class="fill-muted-foreground font-mono text-xs"> 1.00 </text> <text [attr.x]="padLeft - 8" y="152" text-anchor="end" class="fill-muted-foreground font-mono text-xs"> 0.50 </text> <text [attr.x]="padLeft - 8" [attr.y]="targetLossY + 4" text-anchor="end" class="fill-destructive font-mono text-xs font-semibold" > 0.40 </text> <text [attr.x]="padLeft - 8" [attr.y]="padBottom + 4" text-anchor="end" class="fill-muted-foreground font-mono text-xs" > 0.00 </text> <!-- Current Step 320 Vertical Indicator Line --> <line [attr.x1]="currentStepX" y1="20" [attr.x2]="currentStepX" [attr.y2]="padBottom" stroke="var(--color-sky-500, #0ea5e9)" stroke-width="1.5" stroke-dasharray="2 3" /> <!-- Training Loss Area & Solid Line (Observed) --> <path [attr.d]="trainAreaHistoric" fill="url(#grad-train-loss-angular)" /> <path [attr.d]="trainPathHistoric" fill="none" stroke="var(--color-sky-500, #0ea5e9)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" /> <!-- Training Loss Projected Line (Dashed) --> <path [attr.d]="trainPathProjected" fill="none" stroke="var(--color-sky-500, #0ea5e9)" stroke-width="2" stroke-dasharray="4 4" stroke-opacity="0.6" stroke-linecap="round" /> <!-- Validation Loss Curve (Observed Solid Emerald) --> <path [attr.d]="valPathHistoric" fill="none" stroke="var(--color-emerald-500, #10b981)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" /> <!-- Validation Loss Projected Line (Dashed Emerald) --> <path [attr.d]="valPathProjected" fill="none" stroke="var(--color-emerald-500, #10b981)" stroke-width="2" stroke-dasharray="4 4" stroke-opacity="0.6" stroke-linecap="round" /> <!-- Validation Loss Checkpoint Nodes (Observed) --> @for (pt of valPointsHistoric; track pt.step) { <circle [attr.cx]="mapX(pt.step)" [attr.cy]="mapY(pt.valLoss ?? 0)" r="4" class="stroke-background fill-success cursor-pointer stroke-2 transition-transform hover:scale-125" (mouseenter)="selectStep(pt.step)" /> } <!-- Training Loss Interactive Point Markers --> @for (pt of historicalPoints; track pt.step; let idx = $index) { <circle [attr.cx]="mapX(pt.step)" [attr.cy]="mapY(pt.trainLoss)" [attr.r]="pt.step === currentStep ? 5 : 3" [attr.class]="trainMarkerClass(pt)" (mouseenter)="selectedStepIndex.set(idx)" /> } <!-- Current Step Marker Pin Top Flag --> <g [attr.transform]="'translate(' + (currentStepX - 44) + ', 10)'"> <rect width="88" height="18" rx="4" class="dark:fill-info fill-sky-600" /> <text x="44" y="13" text-anchor="middle" class="fill-white font-mono text-xs font-semibold"> Step 320 / 500 </text> </g> </svg> <!-- X-Axis Step Milestones --> <div class="mt-2 grid grid-cols-2 gap-2 text-xs sm:grid-cols-4 lg:grid-cols-7"> @for (pt of milestones; track $index) { <div [class]="milestoneClass(pt)"> <p class="text-foreground font-mono text-xs font-semibold tabular-nums">Step {{ pt.step }}</p> <p class="text-muted-foreground truncate text-xs">{{ pt.label }}</p> <div class="mt-0.5 flex items-center justify-center gap-1 font-mono text-xs tabular-nums"> <span class="text-info">{{ fixed3(pt.trainLoss) }}</span> @if (pt.valLoss) { <span class="text-success">/ {{ fixed3(pt.valLoss) }}</span> } </div> </div> } </div> </div> <!-- Metric Summary Strip --> <div class="grid grid-cols-2 gap-3 text-xs sm:grid-cols-3 lg:grid-cols-5"> <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground">Step Progress</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ currentStep }} / {{ totalSteps }} ({{ progressPercent }}%) </p> <div class="mt-1"> <div ui-progress [value]="progressPercent" class="[&_[data-slot=progress-indicator]]:bg-info h-1"></div> </div> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground">Tokens Processed</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">98.4M / 153.8M</p> <span class="text-muted-foreground text-xs">64.0% of corpus</span> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground">Cluster Throughput</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ locale(tokensPerSec) }} tok/s </p> <span class="text-success text-xs font-medium">Zero pipeline bubbles</span> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground">Current Epoch</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums"> Epoch {{ currentEpoch }} of {{ totalEpochs }} </p> <span class="text-muted-foreground text-xs">100 steps / epoch</span> </div> <div class="border-border bg-muted/30 col-span-2 rounded-lg border p-2.5 sm:col-span-3 lg:col-span-1"> <span class="text-muted-foreground">Convergence Projection</span> <p class="text-success text-success font-mono text-sm font-semibold tabular-nums">0.355 at Step 500</p> <span class="text-muted-foreground text-xs">Expected final loss</span> </div> </div> </div> </div> <!-- Lower Section: Checkpoint History & Hyperparameters Configuration --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-3"> <!-- Left Column (2 cols on lg): Checkpoint History Table --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs lg:col-span-2"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <lucide-icon [img]="Save" class="text-primary size-4" /> <h3 ui-card-title class="text-base font-semibold">Checkpoint History & Weight Exporter</h3> </div> <p ui-card-description class="text-xs"> Saved LoRA adapter snapshots. Deploy directly to test playground or download safe tensors. </p> </div> <span ui-badge wrap variant="outline" class="w-fit font-mono text-xs font-normal tabular-nums"> {{ checkpoints.length }} Saved Snapshots </span> </div> </div> <div ui-card-content class="p-0 sm:p-6 sm:pt-0"> <div class="overflow-x-auto"> <div ui-table> <thead ui-table-header> <tr ui-table-row> <th ui-table-head class="min-w-[140px] text-xs">Checkpoint</th> <th ui-table-head class="min-w-[80px] text-xs">Step</th> <th ui-table-head class="min-w-[90px] text-xs">Train Loss</th> <th ui-table-head class="min-w-[90px] text-xs">Val Loss</th> <th ui-table-head class="min-w-[110px] text-xs">Size & Type</th> <th ui-table-head class="min-w-[100px] text-xs">Created</th> <th ui-table-head class="min-w-[150px] text-right text-xs">Actions</th> </tr> </thead> <tbody ui-table-body> @for (ckpt of checkpoints; track ckpt.id) { <tr ui-table-row [class]="ckpt.isBest ? 'bg-success/[0.04]' : ''"> <td ui-table-cell class="text-xs whitespace-nowrap"> <div class="space-y-0.5"> <div class="flex items-center gap-1.5"> <span class="text-foreground font-mono font-medium">Epoch {{ ckpt.epoch }}.0</span> @if (ckpt.isBest) { <span ui-badge wrap variant="success" class="h-4 px-1.5 text-xs font-medium"> Best Val Loss </span> } </div> <p class="text-muted-foreground font-mono text-xs">{{ ckpt.filename }}</p> </div> </td> <td ui-table-cell class="font-mono text-xs whitespace-nowrap tabular-nums"> {{ ckpt.step }} / {{ ckpt.totalSteps }} </td> <td ui-table-cell class="text-xs whitespace-nowrap"> <span class="text-info text-info font-mono font-semibold tabular-nums"> {{ fixed3(ckpt.trainLoss) }} </span> </td> <td ui-table-cell class="text-xs whitespace-nowrap"> <span class="text-success text-success font-mono font-semibold tabular-nums"> {{ fixed3(ckpt.valLoss) }} </span> </td> <td ui-table-cell class="text-muted-foreground font-mono text-xs whitespace-nowrap tabular-nums"> {{ ckpt.size }} </td> <td ui-table-cell class="text-muted-foreground text-xs whitespace-nowrap"> {{ ckpt.createdAgo }} </td> <td ui-table-cell class="text-right whitespace-nowrap"> <div class="flex items-center justify-end gap-1.5"> <!-- px-1.5: React's size="xs" gets it from has-[>svg], but here the svg sits inside <lucide-icon>. --> <button ui-button variant="outline" size="xs" [disabled]="deployingCkptId() === ckpt.id" class="gap-1 px-1.5 text-xs" (click)="handleDeployToPlayground(ckpt)" > @if (deployingCkptId() === ckpt.id) { <lucide-icon [img]="Loader2" [class]="'size-3 animate-spin'" /> } @else { <lucide-icon [img]="Rocket" class="text-info size-3" /> } <span>{{ deployingCkptId() === ckpt.id ? 'Deploying...' : 'Deploy to Playground' }}</span> </button> <button ui-button variant="ghost" size="icon-xs" class="text-muted-foreground hover:text-foreground size-7" title="Download LoRA .safetensors" (click)="handleDownloadWeights()" > <lucide-icon [img]="Download" class="size-3.5" /> </button> </div> </td> </tr> } </tbody> </div> </div> </div> </div> <!-- Right Column (1 col on lg): Hyperparameters & Dataset Configuration Card --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs lg:col-span-1"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-2"> <lucide-icon [img]="Sliders" class="text-primary size-4" /> <h3 ui-card-title class="text-base font-semibold">Hyperparameters & Dataset</h3> </div> <span ui-badge wrap variant="outline" class="font-mono text-xs">LoRA PEFT</span> </div> <p ui-card-description class="text-xs">Fine-tuning recipe manifest and dataset partition metadata.</p> </div> <div ui-card-content class="space-y-4"> <!-- Training Dataset Box --> <div class="border-border/80 bg-muted/40 space-y-2 rounded-lg border p-3 text-xs"> <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Database" class="text-primary size-3.5" /> <span class="text-foreground font-semibold">Training Dataset</span> </div> <button type="button" class="text-muted-foreground hover:text-foreground transition-colors" title="Copy dataset filename" (click)="copyDatasetName()" > @if (copiedDataset()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } </button> </div> <p class="text-foreground font-mono text-xs font-medium break-all">{{ trainingDataset }}</p> <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-1 text-xs"> <span>{{ locale(trainingExamples) }} train · {{ locale(validationExamples) }} val</span> <span class="font-mono tabular-nums">{{ totalTokens }}</span> </div> </div> <!-- Key Parameters Grid --> <div class="space-y-2.5 text-xs"> <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5"> <span class="text-muted-foreground">Effective Batch Size:</span> <span class="text-foreground font-mono font-semibold tabular-nums"> {{ batchSize }} <span class="text-muted-foreground font-normal" >({{ microBatchSize }} × {{ gradAccumSteps }} accum)</span > </span> </div> <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5"> <span class="text-muted-foreground">Context Length:</span> <span class="text-foreground font-mono font-semibold tabular-nums"> {{ locale(contextLength) }} tokens </span> </div> <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5"> <span class="text-muted-foreground">Optimizer:</span> <span class="text-foreground font-mono font-medium">{{ optimizer }}</span> </div> <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5"> <span class="text-muted-foreground">LoRA Rank & Alpha:</span> <span class="text-foreground font-mono font-semibold tabular-nums"> r={{ loraRank }}, α={{ loraAlpha }} (dropout={{ loraDropout }}) </span> </div> <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5"> <span class="text-muted-foreground">Target Modules:</span> <span class="text-foreground font-mono text-xs">All Linear (q, k, v, o, gate, up, down)</span> </div> <div class="border-border/50 flex items-center justify-between gap-x-2 border-b pb-1.5"> <span class="text-muted-foreground">Precision & Kernel:</span> <span class="text-foreground font-mono">bfloat16 · FlashAttention-2</span> </div> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">Parallelism Strategy:</span> <span class="text-foreground font-medium">PyTorch FSDP-2 (Hybrid Sharded)</span> </div> </div> <ui-separator /> <!-- Export & Integration Note --> <div class="border-border bg-muted/20 space-y-1.5 rounded-lg border p-2.5 text-xs"> <div class="text-foreground flex items-center gap-1.5 font-medium"> <lucide-icon [img]="Sparkles" class="text-info size-3.5" /> <span>HuggingFace Hub & vLLM Ready</span> </div> <p class="text-muted-foreground leading-relaxed"> Checkpoints are stored in standard SafeTensors format with tokenizer configs ready for instant serverless vLLM / TensorRT-LLM deployment. </p> </div> </div> </div> </div> </div> `, }) export class UiFineTuningJobMonitorComponent { @Input() jobId = '#ft-job-2026-0842' @Input() baseModel = 'Meta Llama 3.3 70B Instruct' @Input() fineTunedModelName = 'llama-3.3-70b-uipkge-expert-v2' @Input() status = 'Training in Progress · Epoch 3 of 5 · 64% Complete' @Input() currentEpoch = 3 @Input() totalEpochs = 5 @Input() progressPercent = 64 @Input() currentStep = 320 @Input() totalSteps = 500 @Input() trainingLoss = 0.412 @Input() initialLoss = 1.84 @Input() validationLoss = 0.458 @Input() learningRate = '1.5e-5' @Input() gpuCluster = '8× NVIDIA H100 80GB SXM5' @Input() gpuUtilization = 100 @Input() vramUsage = '76.4 GB / 80 GB' @Input() tokensPerSec = 3480 @Input() elapsedTime = '4h 18m 22s' @Input() etaRemaining = '2h 25m' @Input() trainingDataset = 'uipkge_synthetic_sfc_pairs.jsonl' @Input() trainingExamples = 42500 @Input() validationExamples = 4250 @Input() totalTokens = '153.8M tokens' @Input() batchSize = 32 @Input() microBatchSize = 4 @Input() gradAccumSteps = 8 @Input() contextLength = 4096 @Input() optimizer = 'AdamW (beta1=0.9, beta2=0.95)' @Input() loraRank = 64 @Input() loraAlpha = 128 @Input() loraDropout = 0.05 @Input() checkpoints: CheckpointRecord[] = defaultCheckpoints @Input() lossData: LossReading[] = defaultLossData @Input('class') className?: string readonly copiedJobId = signal(false) readonly copiedDataset = signal(false) readonly isDownloadingWeights = signal(false) readonly isCancelling = signal(false) readonly isJobCancelled = signal(false) readonly deployingCkptId = signal<string | null>(null) readonly activeNotification = signal<FineTuningJobNotification | null>(null) /** Index into `lossData` of the highlighted milestone. Defaults to step 320. */ readonly selectedStepIndex = signal<number | null>(13) protected readonly Activity = Activity protected readonly AlertCircle = AlertCircle protected readonly Bot = Bot protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Copy = Copy protected readonly Cpu = Cpu protected readonly Database = Database protected readonly Download = Download protected readonly Gauge = Gauge protected readonly Loader2 = Loader2 protected readonly Rocket = Rocket protected readonly Save = Save protected readonly ShieldCheck = ShieldCheck protected readonly Sliders = Sliders protected readonly Sparkles = Sparkles protected readonly TrendingDown = TrendingDown protected readonly XCircle = XCircle protected readonly svgWidth = SVG_WIDTH protected readonly padLeft = PAD_LEFT protected readonly padRight = PAD_RIGHT protected readonly usableW = USABLE_W protected readonly padBottom = PAD_BOTTOM private readonly timers = new Set<ReturnType<typeof setTimeout>>() constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach(clearTimeout)) } get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-6', this.className) } // --- formatting ------------------------------------------------------------------------- fixed3(n: number): string { return n.toFixed(3) } locale(n: number): string { return n.toLocaleString() } // --- classes ---------------------------------------------------------------------------- statusClass(): string { return cn('font-medium', this.isJobCancelled() ? 'text-destructive' : 'text-info') } cancelIconClass(): string { return cn('size-3.5', this.isCancelling() && 'animate-spin') } downloadIconClass(): string { return cn('size-3.5', this.isDownloadingWeights() && 'animate-bounce') } notificationClass(note: FineTuningJobNotification): string { return cn( 'flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs transition-colors', note.type === 'destructive' ? 'border-destructive/30 bg-destructive/10 text-destructive' : 'border-border bg-card text-card-foreground', ) } trainMarkerClass(pt: LossReading): string { return cn( 'stroke-background cursor-pointer stroke-2 transition-transform hover:scale-150', pt.step === this.currentStep ? 'fill-info ring-info ring-2' : 'fill-info', ) } milestoneClass(pt: LossReading): string { const isSelected = this.selectedStepIndex() === this.lossData.indexOf(pt) return cn( 'rounded p-1.5 text-center transition-colors', isSelected ? 'border-border/80 bg-muted border shadow-2xs' : 'bg-muted/30 border border-transparent', ) } // --- chart geometry --------------------------------------------------------------------- mapX(step: number): number { return PAD_LEFT + (step / this.totalSteps) * USABLE_W } mapY(loss: number): number { const clamped = Math.max(0, Math.min(2.0, loss)) return PAD_BOTTOM - (clamped / 2.0) * USABLE_H } get targetLossY(): number { return this.mapY(0.4) } get currentStepX(): number { return this.mapX(this.currentStep) } get historicalPoints(): LossReading[] { return this.lossData.filter((d) => !d.isProjected) } get projectedPoints(): LossReading[] { const proj = this.lossData.filter((d) => d.isProjected) const hist = this.historicalPoints const lastHist = hist[hist.length - 1] return lastHist ? [lastHist, ...proj] : proj } get valPointsHistoric(): LossReading[] { return this.lossData.filter((d) => !d.isProjected && d.valLoss !== undefined) } get valPointsProjected(): LossReading[] { const list = this.lossData.filter((d) => d.valLoss !== undefined && d.isProjected) const hist = this.valPointsHistoric const lastValHist = hist[hist.length - 1] return lastValHist ? [lastValHist, ...list] : list } get milestones(): LossReading[] { return this.lossData.filter((d) => d.label) } get trainPathHistoric(): string { return this.curve(this.historicalPoints, (p) => p.trainLoss) } get trainAreaHistoric(): string { const pts = this.historicalPoints if (!pts.length) return '' const firstX = this.mapX(pts[0]!.step) const lastX = this.mapX(pts[pts.length - 1]!.step) return `${this.trainPathHistoric} L ${lastX},${PAD_BOTTOM} L ${firstX},${PAD_BOTTOM} Z` } get trainPathProjected(): string { return this.curve(this.projectedPoints, (p) => p.trainLoss) } get valPathHistoric(): string { return this.curve(this.valPointsHistoric, (p) => p.valLoss ?? 0) } get valPathProjected(): string { return this.curve(this.valPointsProjected, (p) => p.valLoss ?? 0) } /** Smooth cubic path through the points (control points at the horizontal midpoints). */ private curve(pts: LossReading[], loss: (p: LossReading) => number): string { if (!pts.length) return '' let path = `M ${this.mapX(pts[0]!.step)},${this.mapY(loss(pts[0]!))}` for (let i = 1; i < pts.length; i++) { const prev = pts[i - 1]! const curr = pts[i]! const x0 = this.mapX(prev.step) const y0 = this.mapY(loss(prev)) const x1 = this.mapX(curr.step) const y1 = this.mapY(loss(curr)) const cx = (x0 + x1) / 2 path += ` C ${cx},${y0} ${cx},${y1} ${x1},${y1}` } return path } selectStep(step: number): void { this.selectedStepIndex.set(this.lossData.findIndex((d) => d.step === step)) } // --- actions ---------------------------------------------------------------------------- showNotification(title: string, message: string, type: FineTuningJobNotification['type'] = 'success'): void { this.activeNotification.set({ title, message, type }) this.later(() => { this.activeNotification.update((prev) => (prev?.title === title ? null : prev)) }, 4000) } copyJobId(): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(this.jobId) this.copiedJobId.set(true) this.later(() => this.copiedJobId.set(false), 2000) } } copyDatasetName(): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(this.trainingDataset) this.copiedDataset.set(true) this.later(() => this.copiedDataset.set(false), 2000) } } handleDownloadWeights(): void { if (this.isDownloadingWeights()) return this.isDownloadingWeights.set(true) this.later(() => { this.isDownloadingWeights.set(false) this.showNotification( 'Weights Download Triggered', 'LoRA adapter weights (adapter_model.safetensors, 1.2 GB) download initialized with SHA-256 verification.', ) }, 900) } handleCancelJob(): void { if (this.isCancelling() || this.isJobCancelled()) return this.isCancelling.set(true) this.later(() => { this.isCancelling.set(false) this.isJobCancelled.set(true) this.showNotification( 'Job Cancellation Dispatched', 'SIGTERM gracefully sent to GPU cluster. Last checkpoint #3 preserved.', 'destructive', ) }, 700) } handleDeployToPlayground(ckpt: CheckpointRecord): void { this.deployingCkptId.set(ckpt.id) this.later(() => { this.deployingCkptId.set(null) this.showNotification( 'Checkpoint Deployed to Playground', `Checkpoint Epoch ${ckpt.epoch} (Step ${ckpt.step}) has been loaded into the interactive testing playground with zero cold-start latency.`, ) }, 1000) } private later(fn: () => void, ms: number): void { const t = setTimeout(() => { this.timers.delete(t) fn() }, ms) this.timers.add(t) } }src/app/components/blocks/fine-tuning-job-monitor/index.ts0.2 kB
export { UiFineTuningJobMonitorComponent, type CheckpointRecord, type FineTuningJobNotification, type LossReading, } from './fine-tuning-job-monitor.component'
Raw manifest:https://uipkge.dev/r/angular/fine-tuning-job-monitor.json