Added validation loss

This commit is contained in:
Jaret Burkett
2026-07-19 19:33:45 -06:00
parent cd677c70b5
commit 1eb97b7443
7 changed files with 392 additions and 16 deletions

View File

@@ -199,6 +199,8 @@ export default function SimpleJob({
const showGPUSelect = !isMac();
const validationConfig = jobConfig.config.process[0].train.validation_config;
let numDatasetCols = 4;
let numSampleTopCols = 4;
let datasetStyleClass = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6';
@@ -834,6 +836,136 @@ export default function SimpleJob({
</div>
</Card>
</div>
<div>
<Card
title="Validation"
toggled={!!validationConfig}
onToggle={value => {
if (value) {
setJobConfig(
{
validation_items: [{ image_path: '', prompt: '' }],
resolution: 512,
validate_every_n_steps: 10,
validation_sigmas: [1.0, 0.75, 0.5, 0.25],
},
'config.process[0].train.validation_config',
);
} else {
setJobConfig(undefined, 'config.process[0].train.validation_config');
}
}}
>
{validationConfig && (
<>
<p className="text-sm text-gray-400 mb-4">
Validation runs a stable loss check on a fixed set of images. Each image is encoded once at startup
and predicted at the selected sigmas with fixed seeds, so the result is always deterministic and
comparable across the run. The average loss is logged as val/loss every time validation runs. The
images need to match the concept of your dataset, but{' '}
<span className="font-bold text-gray-300">do not include the validation images in the dataset</span>.
They must be images containing the concept you want to train, but not an image trained on.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<NumberInput
label="Validate Every"
value={validationConfig.validate_every_n_steps}
onChange={value =>
setJobConfig(value, 'config.process[0].train.validation_config.validate_every_n_steps')
}
placeholder="eg. 10"
min={1}
required
/>
<NumberInput
label="Validation Resolution"
value={validationConfig.resolution}
onChange={value => setJobConfig(value, 'config.process[0].train.validation_config.resolution')}
placeholder="eg. 512"
min={64}
required
/>
<SelectInput
label="Validation Sigmas"
value={(validationConfig.validation_sigmas ?? [1.0, 0.75, 0.5, 0.25]).join(', ')}
onChange={value =>
setJobConfig(
value.split(',').map((v: string) => parseFloat(v)),
'config.process[0].train.validation_config.validation_sigmas',
)
}
options={[
{ value: '0.5', label: '0.5' },
{ value: '1, 0.5', label: '1.0, 0.5' },
{ value: '1, 0.66, 0.33', label: '1.0, 0.66, 0.33' },
{ value: '1, 0.75, 0.5, 0.25', label: '1.0, 0.75, 0.5, 0.25' },
]}
/>
</div>
<div className="mt-4">
<label className="block text-xs text-gray-300 mb-2">
Validation Images ({validationConfig.validation_items.length})
</label>
{validationConfig.validation_items.map((item, i) => (
<div key={i} className="rounded-lg pl-4 pr-1 py-3 mb-4 bg-gray-950">
<div className="flex items-center space-x-4">
<SampleControlImage
instruction="Add Image"
src={item.image_path === '' ? null : item.image_path}
onNewImageSelected={imagePath => {
setJobConfig(
imagePath ?? '',
`config.process[0].train.validation_config.validation_items[${i}].image_path`,
);
}}
/>
<div className="flex-1">
<TextInput
label="Prompt"
value={item.prompt}
onChange={value =>
setJobConfig(
value,
`config.process[0].train.validation_config.validation_items[${i}].prompt`,
)
}
placeholder="Enter prompt"
/>
</div>
<div>
<button
type="button"
onClick={() =>
setJobConfig(
validationConfig.validation_items.filter((_, index) => index !== i),
'config.process[0].train.validation_config.validation_items',
)
}
className="rounded-full p-1 text-sm"
>
<X />
</button>
</div>
</div>
</div>
))}
<button
type="button"
onClick={() =>
setJobConfig(
[...validationConfig.validation_items, { image_path: '', prompt: '' }],
'config.process[0].train.validation_config.validation_items',
)
}
className="w-full px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
>
Add Validation Image
</button>
</div>
</>
)}
</Card>
</div>
<div>
<Card title="Advanced" collapsible>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">

View File

@@ -7,9 +7,52 @@ interface CardProps {
children?: React.ReactNode;
collapsible?: boolean;
defaultOpen?: boolean;
// when provided, the card is opened/closed by a toggle switch on the right instead of a chevron
toggled?: boolean;
onToggle?: (value: boolean) => void;
}
const Card: React.FC<CardProps> = ({ title, children, collapsible, defaultOpen }) => {
const Card: React.FC<CardProps> = ({ title, children, collapsible, defaultOpen, toggled, onToggle }) => {
if (onToggle) {
return (
<section className="space-y-2 px-4 pb-2 pt-2 bg-gray-900 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex-1">
{title && (
<h2 className={classNames('text-lg font-semibold uppercase text-gray-500', toggled ? 'mb-2' : 'mb-0')}>
{title}
</h2>
)}
</div>
<button
type="button"
role="switch"
aria-checked={toggled}
onClick={() => onToggle(!toggled)}
className={classNames(
'relative ml-2 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2',
toggled ? 'bg-blue-500' : 'bg-gray-600',
'hover:bg-opacity-80',
)}
>
<span className="sr-only">Toggle {title}</span>
<span
className={classNames(
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
toggled ? 'translate-x-5' : 'translate-x-0',
)}
/>
</button>
</div>
{toggled && (
<>
{children ?? null}
<div className="pt-2"></div>
</>
)}
</section>
);
}
if (collapsible) {
return (
<Disclosure as="section" className="space-y-2 px-4 pb-2 pt-2 bg-gray-900 rounded-lg" defaultOpen={defaultOpen}>

View File

@@ -198,18 +198,26 @@ export const NumberInput = (props: NumberInputProps) => {
const numValue = Number(rawValue);
// Only apply constraints and call onChange when we have a valid number
// don't clamp to min/max while typing, it mangles partial input (typing 1024 with
// min 64 becomes 64024). Clamping happens on blur.
if (!isNaN(numValue)) {
let constrainedValue = numValue;
// Apply min/max constraints if they exist
if (min !== undefined && constrainedValue < min) {
constrainedValue = min;
}
if (max !== undefined && constrainedValue > max) {
constrainedValue = max;
}
onChange(numValue);
}
}}
onBlur={() => {
const numValue = Number(inputValue);
if (inputValue === '' || isNaN(numValue)) {
return;
}
let constrainedValue = numValue;
if (min !== undefined && constrainedValue < min) {
constrainedValue = min;
}
if (max !== undefined && constrainedValue > max) {
constrainedValue = max;
}
if (constrainedValue !== numValue) {
setInputValue(constrainedValue);
onChange(constrainedValue);
}
}}

View File

@@ -117,6 +117,18 @@ export interface EMAConfig {
ema_decay: number;
}
export interface ValidationItem {
image_path: string;
prompt: string;
}
export interface ValidationConfig {
validation_items: ValidationItem[];
resolution: number;
validate_every_n_steps: number;
validation_sigmas?: number[];
}
export interface TrainConfig {
batch_size: number;
bypass_guidance_embedding?: boolean;
@@ -151,6 +163,7 @@ export interface TrainConfig {
differential_guidance_scale?: number;
audio_loss_multiplier?: number;
max_loss?: number | null;
validation_config?: ValidationConfig;
}
export interface QuantizeKwargsConfig {