Config ui section is coming along
This commit is contained in:
102
ui/src/app/api/gpu/route.ts
Normal file
102
ui/src/app/api/gpu/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Check if nvidia-smi is available
|
||||
const hasNvidiaSmi = await checkNvidiaSmi();
|
||||
|
||||
if (!hasNvidiaSmi) {
|
||||
return NextResponse.json({
|
||||
hasNvidiaSmi: false,
|
||||
gpus: [],
|
||||
error: 'nvidia-smi not found or not accessible',
|
||||
});
|
||||
}
|
||||
|
||||
// Get GPU stats
|
||||
const gpuStats = await getGpuStats();
|
||||
|
||||
return NextResponse.json({
|
||||
hasNvidiaSmi: true,
|
||||
gpus: gpuStats,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching NVIDIA GPU stats:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
hasNvidiaSmi: false,
|
||||
gpus: [],
|
||||
error: `Failed to fetch GPU stats: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNvidiaSmi(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync('which nvidia-smi');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getGpuStats() {
|
||||
// Get detailed GPU information in JSON format
|
||||
const { stdout } = await execAsync(
|
||||
'nvidia-smi --query-gpu=index,name,driver_version,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.free,memory.used,power.draw,power.limit,clocks.current.graphics,clocks.current.memory --format=csv,noheader,nounits',
|
||||
);
|
||||
|
||||
// Parse CSV output
|
||||
const gpus = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(line => {
|
||||
const [
|
||||
index,
|
||||
name,
|
||||
driverVersion,
|
||||
temperature,
|
||||
gpuUtil,
|
||||
memoryUtil,
|
||||
memoryTotal,
|
||||
memoryFree,
|
||||
memoryUsed,
|
||||
powerDraw,
|
||||
powerLimit,
|
||||
clockGraphics,
|
||||
clockMemory,
|
||||
] = line.split(', ').map(item => item.trim());
|
||||
|
||||
return {
|
||||
index: parseInt(index),
|
||||
name,
|
||||
driverVersion,
|
||||
temperature: parseInt(temperature),
|
||||
utilization: {
|
||||
gpu: parseInt(gpuUtil),
|
||||
memory: parseInt(memoryUtil),
|
||||
},
|
||||
memory: {
|
||||
total: parseInt(memoryTotal),
|
||||
free: parseInt(memoryFree),
|
||||
used: parseInt(memoryUsed),
|
||||
},
|
||||
power: {
|
||||
draw: parseFloat(powerDraw),
|
||||
limit: parseFloat(powerLimit),
|
||||
},
|
||||
clocks: {
|
||||
graphics: parseInt(clockGraphics),
|
||||
memory: parseInt(clockMemory),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return gpus;
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { defaultTrainFolder } from '@/paths';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const settings = await prisma.settings.findMany();
|
||||
return NextResponse.json(settings.reduce((acc, curr) => ({ ...acc, [curr.key]: curr.value }), {}));
|
||||
const settingsObject = settings.reduce((acc: any, setting) => {
|
||||
acc[setting.key] = setting.value;
|
||||
return acc;
|
||||
}, {});
|
||||
// if TRAINING_FOLDER is not set, use default
|
||||
if (!settingsObject.TRAINING_FOLDER || settingsObject.TRAINING_FOLDER === '') {
|
||||
settingsObject.TRAINING_FOLDER = defaultTrainFolder;
|
||||
}
|
||||
return NextResponse.json(settingsObject);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function GET(request: Request) {
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { id, name, run_data } = body;
|
||||
const { id, name, job_config, gpu_id } = body;
|
||||
|
||||
if (id) {
|
||||
// Update existing training
|
||||
@@ -35,7 +35,8 @@ export async function POST(request: Request) {
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
run_data: JSON.stringify(run_data),
|
||||
gpu_id,
|
||||
job_config: JSON.stringify(job_config),
|
||||
},
|
||||
});
|
||||
return NextResponse.json(training);
|
||||
@@ -44,7 +45,8 @@ export async function POST(request: Request) {
|
||||
const training = await prisma.training.create({
|
||||
data: {
|
||||
name,
|
||||
run_data: JSON.stringify(run_data),
|
||||
gpu_id,
|
||||
job_config: JSON.stringify(job_config),
|
||||
},
|
||||
});
|
||||
return NextResponse.json(training);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import GpuMonitor from '@/components/GPUMonitor';
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="p-6 bg-gray-800 rounded-lg">
|
||||
<h2 className="text-xl font-semibold mb-2">Card {i}</h2>
|
||||
<p className="text-gray-400">Example dashboard card content</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<GpuMonitor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
95
ui/src/app/train/jobConfig.ts
Normal file
95
ui/src/app/train/jobConfig.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { JobConfig, DatasetConfig } from '@/types';
|
||||
|
||||
export const defaultDatasetConfig: DatasetConfig = {
|
||||
folder_path: '/path/to/images/folder',
|
||||
mask_path: null,
|
||||
mask_min_value: 0.1,
|
||||
default_caption: '',
|
||||
caption_ext: 'txt',
|
||||
caption_dropout_rate: 0.05,
|
||||
cache_latents_to_disk: false,
|
||||
is_reg: false,
|
||||
network_weight: 1,
|
||||
resolution: [512, 768, 1024],
|
||||
};
|
||||
|
||||
export const defaultJobConfig: JobConfig = {
|
||||
job: 'extension',
|
||||
config: {
|
||||
name: 'my_first_flex_lora_v1',
|
||||
process: [
|
||||
{
|
||||
type: 'sd_trainer',
|
||||
training_folder: 'output',
|
||||
device: 'cuda:0',
|
||||
network: {
|
||||
type: 'lora',
|
||||
linear: 16,
|
||||
linear_alpha: 16,
|
||||
},
|
||||
save: {
|
||||
dtype: 'bf16',
|
||||
save_every: 250,
|
||||
max_step_saves_to_keep: 4,
|
||||
push_to_hub: false,
|
||||
},
|
||||
datasets: [
|
||||
defaultDatasetConfig
|
||||
],
|
||||
train: {
|
||||
batch_size: 1,
|
||||
bypass_guidance_embedding: true,
|
||||
steps: 2000,
|
||||
gradient_accumulation: 1,
|
||||
train_unet: true,
|
||||
train_text_encoder: false,
|
||||
gradient_checkpointing: true,
|
||||
noise_scheduler: 'flowmatch',
|
||||
optimizer: 'adamw8bit',
|
||||
optimizer_params: {
|
||||
weight_decay: 1e-4
|
||||
},
|
||||
lr: 0.0001,
|
||||
ema_config: {
|
||||
use_ema: true,
|
||||
ema_decay: 0.99,
|
||||
},
|
||||
dtype: 'bf16',
|
||||
},
|
||||
model: {
|
||||
name_or_path: 'ostris/Flex.1-alpha',
|
||||
is_flux: true,
|
||||
quantize: true,
|
||||
quantize_te: true
|
||||
},
|
||||
sample: {
|
||||
sampler: 'flowmatch',
|
||||
sample_every: 250,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
prompts: [
|
||||
'woman with red hair, playing chess at the park, bomb going off in the background',
|
||||
'a woman holding a coffee cup, in a beanie, sitting at a cafe',
|
||||
'a horse is a DJ at a night club, fish eye lens, smoke machine, lazer lights, holding a martini',
|
||||
'a man showing off his cool new t shirt at the beach, a shark is jumping out of the water in the background',
|
||||
'a bear building a log cabin in the snow covered mountains',
|
||||
'woman playing the guitar, on stage, singing a song, laser lights, punk rocker',
|
||||
'hipster man with a beard, building a chair, in a wood shop',
|
||||
'photo of a man, white background, medium shot, modeling clothing, studio lighting, white backdrop',
|
||||
"a man holding a sign that says, 'this is a sign'",
|
||||
'a bulldog, in a post apocalyptic world, with a shotgun, in a leather jacket, in a desert, with a motorcycle',
|
||||
],
|
||||
neg: '',
|
||||
seed: 42,
|
||||
walk_seed: true,
|
||||
guidance_scale: 4,
|
||||
sample_steps: 25,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
name: '[name]',
|
||||
version: '1.0',
|
||||
},
|
||||
};
|
||||
@@ -1,37 +1,41 @@
|
||||
export interface Model {
|
||||
name_or_path: string;
|
||||
model_kwargs?: Record<string, boolean>;
|
||||
train_kwargs?: Record<string, boolean>;
|
||||
name_or_path: string;
|
||||
defaults?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
model: Model[];
|
||||
model: Model[];
|
||||
}
|
||||
|
||||
|
||||
export const options = {
|
||||
model: [
|
||||
{
|
||||
name_or_path: "ostris/Flex.1-alpha",
|
||||
model_kwargs: {
|
||||
"is_flux": true
|
||||
},
|
||||
train_kwargs: {
|
||||
"bypass_guidance_embedding": true
|
||||
}
|
||||
},
|
||||
{
|
||||
name_or_path: "black-forest-labs/FLUX.1-dev",
|
||||
model_kwargs: {
|
||||
"is_flux": true
|
||||
},
|
||||
},
|
||||
{
|
||||
name_or_path: "Alpha-VLLM/Lumina-Image-2.0",
|
||||
model_kwargs: {
|
||||
"is_lumina2": true
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
} as Option;
|
||||
model: [
|
||||
{
|
||||
name_or_path: 'ostris/Flex.1-alpha',
|
||||
defaults: {
|
||||
// default updates when [selected, unselected] in the UI
|
||||
'config.process[0].model.quantize': [true, false],
|
||||
'config.process[0].model.quantize_te': [true, false],
|
||||
'config.process[0].model.is_flux': [true, false],
|
||||
'config.process[0].train.bypass_guidance_embedding': [true, false],
|
||||
},
|
||||
},
|
||||
{
|
||||
name_or_path: 'black-forest-labs/FLUX.1-dev',
|
||||
defaults: {
|
||||
// default updates when [selected, unselected] in the UI
|
||||
'config.process[0].model.quantize': [true, false],
|
||||
'config.process[0].model.quantize_te': [true, false],
|
||||
'config.process[0].model.is_flux': [true, false],
|
||||
},
|
||||
},
|
||||
{
|
||||
name_or_path: 'Alpha-VLLM/Lumina-Image-2.0',
|
||||
defaults: {
|
||||
// default updates when [selected, unselected] in the UI
|
||||
'config.process[0].model.quantize': [false, false],
|
||||
'config.process[0].model.quantize_te': [true, false],
|
||||
'config.process[0].model.is_lumina2': [true, false],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as Option;
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
'use client';
|
||||
// todo update training folder from settings
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { options } from './options';
|
||||
|
||||
interface TrainingData {
|
||||
modelConfig: {
|
||||
name_or_path: string;
|
||||
steps: number;
|
||||
batchSize: number;
|
||||
learningRate: number;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultTrainingData: TrainingData = {
|
||||
modelConfig: {
|
||||
name_or_path: 'ostris/Flex.1-alpha',
|
||||
steps: 100,
|
||||
batchSize: 32,
|
||||
learningRate: 0.001,
|
||||
},
|
||||
};
|
||||
import { GPUApiResponse } from '@/types';
|
||||
import { defaultJobConfig, defaultDatasetConfig } from './jobConfig';
|
||||
import { JobConfig } from '@/types';
|
||||
import { objectCopy } from '@/utils/basic';
|
||||
import { useNestedState } from '@/utils/hooks';
|
||||
import { TextInput, SelectInput, Checkbox, FormGroup, NumberInput } from '@/components/formInputs';
|
||||
import Card from '@/components/Card';
|
||||
import { Trash, X } from 'lucide-react';
|
||||
|
||||
export default function TrainingForm() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const runId = searchParams.get('id');
|
||||
const [gpuID, setGpuID] = useState<number | null>(null);
|
||||
const [gpuList, setGpuList] = useState<number[]>([]);
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [trainingData, setTrainingData] = useState<TrainingData>(defaultTrainingData);
|
||||
const [jobConfig, setJobConfig] = useNestedState<JobConfig>(objectCopy(defaultJobConfig));
|
||||
const [status, setStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,13 +28,42 @@ export default function TrainingForm() {
|
||||
fetch(`/api/training?id=${runId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setName(data.name);
|
||||
setTrainingData(JSON.parse(data.run_data));
|
||||
setGpuID(data.gpu_id);
|
||||
setJobConfig(JSON.parse(data.job_config));
|
||||
})
|
||||
.catch(error => console.error('Error fetching training:', error));
|
||||
}
|
||||
}, [runId]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGpuInfo = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/gpu');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: GPUApiResponse = await response.json();
|
||||
setGpuList(data.gpus.map(gpu => gpu.index).sort());
|
||||
} catch (err) {
|
||||
console.log(`Failed to fetch GPU data: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
// setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetch('/api/settings')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setJobConfig(data.TRAINING_FOLDER, 'config.process[0].training_folder');
|
||||
})
|
||||
.catch(error => console.error('Error fetching settings:', error));
|
||||
|
||||
// Fetch immediately on component mount
|
||||
fetchGpuInfo();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('saving');
|
||||
@@ -55,8 +76,9 @@ export default function TrainingForm() {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: runId,
|
||||
name,
|
||||
run_data: trainingData,
|
||||
name: jobConfig.config.name,
|
||||
gpu_id: gpuID,
|
||||
job_config: jobConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -75,64 +97,428 @@ export default function TrainingForm() {
|
||||
}
|
||||
};
|
||||
|
||||
const updateSection = (section: keyof TrainingData, data: any) => {
|
||||
setTrainingData(prev => ({
|
||||
...prev,
|
||||
[section]: { ...prev[section], ...data },
|
||||
}));
|
||||
};
|
||||
|
||||
const modelOptions = options.model.map(model => model.name_or_path);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-8 pb-12">
|
||||
<h1 className="text-3xl font-bold mb-8">{runId ? 'Edit Training Run' : 'New Training Run'}</h1>
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-xl font-bold mb-8">{runId ? 'Edit Training Run' : 'New Training Run'}</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
Training Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-gray-600 focus:border-transparent"
|
||||
placeholder="Enter training name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card title="Job Settings">
|
||||
<TextInput
|
||||
label="Training Name"
|
||||
value={jobConfig.config.name}
|
||||
onChange={value => setJobConfig(value, 'config.name')}
|
||||
placeholder="Enter training name"
|
||||
required
|
||||
/>
|
||||
<SelectInput
|
||||
label="GPU ID"
|
||||
value={`${gpuID}`}
|
||||
className="pt-2"
|
||||
onChange={value => setGpuID(parseInt(value))}
|
||||
options={gpuList.map(gpu => ({ value: `${gpu}`, label: `GPU #${gpu}` }))}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Model Configuration Section */}
|
||||
<section className="space-y-4 p-6 bg-gray-900 rounded-lg">
|
||||
<h2 className="text-xl font-bold mb-4">Model Configuration</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Model</label>
|
||||
<select
|
||||
value={trainingData.modelConfig.name_or_path}
|
||||
onChange={e => updateSection('modelConfig', { name_or_path: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
>
|
||||
{modelOptions.map(model => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Epochs</label>
|
||||
<input
|
||||
type="number"
|
||||
value={trainingData.modelConfig.steps}
|
||||
onChange={e => updateSection('modelConfig', { steps: Number(e.target.value) })}
|
||||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
||||
{/* Model Configuration Section */}
|
||||
<Card title="Model Configuration">
|
||||
<SelectInput
|
||||
label="Name or Path"
|
||||
value={jobConfig.config.process[0].model.name_or_path}
|
||||
onChange={value => {
|
||||
// see if model changed
|
||||
const currentModel = options.model.find(
|
||||
model => model.name_or_path === jobConfig.config.process[0].model.name_or_path,
|
||||
);
|
||||
if (!currentModel || currentModel.name_or_path === value) {
|
||||
// model has not changed
|
||||
return;
|
||||
}
|
||||
// revert defaults from previous model
|
||||
for (const key in currentModel.defaults) {
|
||||
setJobConfig(currentModel.defaults[key][1], key);
|
||||
}
|
||||
// set new model
|
||||
setJobConfig(value, 'config.process[0].model.name_or_path');
|
||||
// update the defaults when a model is selected
|
||||
const model = options.model.find(model => model.name_or_path === value);
|
||||
if (model?.defaults) {
|
||||
for (const key in model.defaults) {
|
||||
setJobConfig(model.defaults[key][0], key);
|
||||
}
|
||||
}
|
||||
}}
|
||||
options={options.model.map(model => ({
|
||||
value: model.name_or_path,
|
||||
label: model.name_or_path,
|
||||
}))}
|
||||
/>
|
||||
<FormGroup label="Quantize" className="pt-2">
|
||||
<Checkbox
|
||||
label="Transformer"
|
||||
checked={jobConfig.config.process[0].model.quantize}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].model.quantize')}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Text Encoder"
|
||||
checked={jobConfig.config.process[0].model.quantize_te}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].model.quantize_te')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Card>
|
||||
{jobConfig.config.process[0].network?.linear && (
|
||||
<Card title="LoRA Configuration">
|
||||
<NumberInput
|
||||
label="Linear Rank"
|
||||
value={jobConfig.config.process[0].network.linear}
|
||||
onChange={value => {
|
||||
setJobConfig(value, 'config.process[0].network.linear');
|
||||
setJobConfig(value, 'config.process[0].network.linear_alpha');
|
||||
}}
|
||||
placeholder="eg. 16"
|
||||
min={1}
|
||||
max={1024}
|
||||
required
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
<Card title="Save Configuration">
|
||||
<SelectInput
|
||||
label="Data Type"
|
||||
value={jobConfig.config.process[0].save.dtype}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].save.dtype')}
|
||||
options={[
|
||||
{ value: 'bf16', label: 'BF16' },
|
||||
{ value: 'fp16', label: 'FP16' },
|
||||
{ value: 'fp32', label: 'FP32' },
|
||||
]}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Save Every"
|
||||
value={jobConfig.config.process[0].save.save_every}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].save.save_every')}
|
||||
placeholder="eg. 250"
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Max Step Saves to Keep"
|
||||
value={jobConfig.config.process[0].save.max_step_saves_to_keep}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].save.max_step_saves_to_keep')}
|
||||
placeholder="eg. 4"
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<div>
|
||||
<Card title="Training Configuration">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Batch Size"
|
||||
className="pt-2"
|
||||
value={jobConfig.config.process[0].train.batch_size}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].train.batch_size')}
|
||||
placeholder="eg. 4"
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Gradient Accumulation"
|
||||
className="pt-2"
|
||||
value={jobConfig.config.process[0].train.gradient_accumulation}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].train.gradient_accumulation')}
|
||||
placeholder="eg. 1"
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Steps"
|
||||
className="pt-2"
|
||||
value={jobConfig.config.process[0].train.steps}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].train.steps')}
|
||||
placeholder="eg. 2000"
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<SelectInput
|
||||
label="Optimizer"
|
||||
className="pt-2"
|
||||
value={jobConfig.config.process[0].train.optimizer}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].train.optimizer')}
|
||||
options={[
|
||||
{ value: 'adamw8bit', label: 'AdamW8Bit' },
|
||||
{ value: 'adafactor', label: 'Adafactor' },
|
||||
]}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Learning Rate"
|
||||
className="pt-2"
|
||||
value={jobConfig.config.process[0].train.lr}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].train.lr')}
|
||||
placeholder="eg. 0.0001"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Weight Decay"
|
||||
className="pt-2"
|
||||
value={jobConfig.config.process[0].train.optimizer_params.weight_decay}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].train.optimizer_params.weight_decay')}
|
||||
placeholder="eg. 0.0001"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
</div>
|
||||
<div>
|
||||
<Card title="Datasets">
|
||||
<>
|
||||
{jobConfig.config.process[0].datasets.map((dataset, i) => (
|
||||
<div key={i} className="p-4 rounded-lg bg-gray-800 relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setJobConfig(
|
||||
jobConfig.config.process[0].datasets.filter((_, index) => index !== i),
|
||||
'config.process[0].datasets',
|
||||
)
|
||||
}
|
||||
className="absolute top-2 right-2 bg-red-800 hover:bg-red-700 rounded-full p-1 text-sm transition-colors"
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
<h2 className="text-lg font-bold mb-4">Dataset {i + 1}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<TextInput
|
||||
label="Folder Path"
|
||||
value={dataset.folder_path}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].folder_path`)}
|
||||
placeholder="eg. /path/to/images/folder"
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label="Mask Folder Path"
|
||||
className="pt-2"
|
||||
value={dataset.mask_path || ''}
|
||||
onChange={value => {
|
||||
let setValue: string | null = value;
|
||||
if (!setValue || setValue.trim() === '') {
|
||||
setValue = null;
|
||||
}
|
||||
setJobConfig(setValue, `config.process[0].datasets[${i}].mask_path`);
|
||||
}}
|
||||
placeholder="eg. /path/to/masks/folder"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Mask Min Value"
|
||||
className="pt-2"
|
||||
value={dataset.mask_min_value}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].mask_min_value`)}
|
||||
placeholder="eg. 0.1"
|
||||
min={0}
|
||||
max={1}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextInput
|
||||
label="Default Caption"
|
||||
value={dataset.default_caption}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].default_caption`)}
|
||||
placeholder="eg. A photo of a cat"
|
||||
/>
|
||||
<TextInput
|
||||
label="Caption Extension"
|
||||
className="pt-2"
|
||||
value={dataset.caption_ext}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].caption_ext`)}
|
||||
placeholder="eg. txt"
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Caption Dropout Rate"
|
||||
className="pt-2"
|
||||
value={dataset.caption_dropout_rate}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].caption_dropout_rate`)}
|
||||
placeholder="eg. 0.05"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormGroup label="Settings" className="">
|
||||
<Checkbox
|
||||
label="Cache Latents to Disk"
|
||||
checked={dataset.cache_latents_to_disk || false}
|
||||
onChange={value =>
|
||||
setJobConfig(value, `config.process[0].datasets[${i}].cache_latents_to_disk`)
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Is Regularization"
|
||||
className="pt-2"
|
||||
checked={dataset.is_reg || false}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].is_reg`)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<div>
|
||||
<FormGroup label="Resolutions" className="pt-2">
|
||||
{[256, 512, 768, 1024, 1280].map(res => (
|
||||
<Checkbox
|
||||
key={res}
|
||||
label={res.toString()}
|
||||
checked={dataset.resolution.includes(res)}
|
||||
onChange={value => {
|
||||
const resolutions = dataset.resolution.includes(res)
|
||||
? dataset.resolution.filter(r => r !== res)
|
||||
: [...dataset.resolution, res];
|
||||
setJobConfig(resolutions, `config.process[0].datasets[${i}].resolution`);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setJobConfig(
|
||||
[...jobConfig.config.process[0].datasets, objectCopy(defaultDatasetConfig)],
|
||||
'config.process[0].datasets',
|
||||
)
|
||||
}
|
||||
className="w-full px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
|
||||
>
|
||||
Add Dataset
|
||||
</button>
|
||||
</>
|
||||
</Card>
|
||||
</div>
|
||||
<div>
|
||||
<Card title="Sample Configuration">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Sample Every"
|
||||
value={jobConfig.config.process[0].sample.sample_every}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.sample_every')}
|
||||
placeholder="eg. 250"
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
<SelectInput
|
||||
label="Sampler"
|
||||
className="pt-2"
|
||||
value={jobConfig.config.process[0].sample.sampler}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.sampler')}
|
||||
options={[{ value: 'flowmatch', label: 'FlowMatch' }]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Guidance Scale"
|
||||
value={jobConfig.config.process[0].sample.guidance_scale}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.guidance_scale')}
|
||||
placeholder="eg. 1.0"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Sample Steps"
|
||||
value={jobConfig.config.process[0].sample.sample_steps}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.sample_steps')}
|
||||
placeholder="eg. 1"
|
||||
className="pt-2"
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Width"
|
||||
value={jobConfig.config.process[0].sample.width}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.width')}
|
||||
placeholder="eg. 1024"
|
||||
min={256}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Height"
|
||||
value={jobConfig.config.process[0].sample.height}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.height')}
|
||||
placeholder="eg. 1024"
|
||||
className="pt-2"
|
||||
min={256}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Seed"
|
||||
value={jobConfig.config.process[0].sample.seed}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.seed')}
|
||||
placeholder="eg. 0"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
<Checkbox
|
||||
label="Walk Seed"
|
||||
className="pt-4 pl-2"
|
||||
checked={jobConfig.config.process[0].sample.walk_seed}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.walk_seed')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormGroup label={`Sample Prompts (${jobConfig.config.process[0].sample.prompts.length})`} className="pt-2">
|
||||
{jobConfig.config.process[0].sample.prompts.map((prompt, i) => (
|
||||
<div key={i} className="flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
value={prompt}
|
||||
onChange={value => setJobConfig(value, `config.process[0].sample.prompts[${i}]`)}
|
||||
placeholder="Enter prompt"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setJobConfig(
|
||||
jobConfig.config.process[0].sample.prompts.filter((_, index) => index !== i),
|
||||
'config.process[0].sample.prompts',
|
||||
)
|
||||
}
|
||||
className="rounded-full p-1 text-sm"
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setJobConfig([...jobConfig.config.process[0].sample.prompts, ''], 'config.process[0].sample.prompts')
|
||||
}
|
||||
className="w-full px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
|
||||
>
|
||||
Add Prompt
|
||||
</button>
|
||||
</FormGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
Reference in New Issue
Block a user