apolinario commited on
Commit
22a27ba
·
1 Parent(s): 647b4ff
ui/src/app/jobs/new/SimplifiedJob.tsx CHANGED
@@ -22,6 +22,10 @@ const ACCEPTED_FILE_TYPES = {
22
  'text/*': ['.txt'],
23
  };
24
 
 
 
 
 
25
  const slugify = (value: string) =>
26
  value
27
  .toLowerCase()
@@ -29,6 +33,14 @@ const slugify = (value: string) =>
29
  .replace(/^-+|-+$/g, '')
30
  .slice(0, 64);
31
 
 
 
 
 
 
 
 
 
32
  type SimplifiedJobProps = {
33
  jobConfig: JobConfig;
34
  setJobConfig: (value: any, key: string) => void;
@@ -100,7 +112,7 @@ export default function SimplifiedJob({
100
  const [trainDatasetMode, setTrainDatasetMode] = useState<DatasetMode>('upload');
101
  const [trainModeTouched, setTrainModeTouched] = useState(hasCustomTrainPath);
102
  const [trainDatasetName, setTrainDatasetName] = useState(() =>
103
- slugify(jobConfig.config.name || 'training-data'),
104
  );
105
  const [trainDatasetNameTouched, setTrainDatasetNameTouched] = useState(false);
106
  const [trainDatasetResolvedName, setTrainDatasetResolvedName] = useState<string | null>(null);
@@ -115,7 +127,7 @@ export default function SimplifiedJob({
115
  const [controlDatasetMode, setControlDatasetMode] = useState<DatasetMode>('upload');
116
  const [controlModeTouched, setControlModeTouched] = useState(hasCustomControlPath);
117
  const [controlDatasetName, setControlDatasetName] = useState(() =>
118
- slugify(`${jobConfig.config.name || 'training'}-control`),
119
  );
120
  const [controlDatasetNameTouched, setControlDatasetNameTouched] = useState(false);
121
  const [controlDatasetResolvedName, setControlDatasetResolvedName] = useState<string | null>(null);
@@ -148,6 +160,13 @@ export default function SimplifiedJob({
148
  }
149
  }, [process]);
150
 
 
 
 
 
 
 
 
151
  const handleModelChange = (newModel: string) => {
152
  const currentArch = modelArchs.find(a => a.name === process.model.arch);
153
  if (!currentArch || currentArch.name === newModel) {
@@ -177,6 +196,7 @@ export default function SimplifiedJob({
177
  setJobConfig(false, 'config.process[0].model.low_vram');
178
 
179
  setJobConfig(newModel, 'config.process[0].model.arch');
 
180
 
181
  // Sync dataset controls
182
  const hasControl = nextArch?.additionalSections?.includes('datasets.control_path') ?? false;
@@ -231,10 +251,10 @@ export default function SimplifiedJob({
231
 
232
  useEffect(() => {
233
  if (!trainDatasetNameTouched) {
234
- setTrainDatasetName(slugify(jobConfig.config.name || 'training-data'));
235
  }
236
  if (!controlDatasetNameTouched) {
237
- setControlDatasetName(slugify(`${jobConfig.config.name || 'training'}-control`));
238
  }
239
  }, [jobConfig.config.name, trainDatasetNameTouched, controlDatasetNameTouched]);
240
 
@@ -490,21 +510,25 @@ export default function SimplifiedJob({
490
  <TextInput
491
  label="Trigger Word"
492
  value={process.trigger_word || ''}
 
493
  onChange={value => setJobConfig(value?.trim() === '' ? null : value, 'config.process[0].trigger_word')}
494
  placeholder="Optional keyword"
495
  />
496
  <NumberInput
497
  label="Training Steps"
498
  value={process.train.steps}
 
499
  onChange={value => setJobConfig(value, 'config.process[0].train.steps')}
500
  min={1}
501
  required
502
  />
503
  <NumberInput
504
- label="Batch Size"
505
- value={process.train.batch_size}
506
- onChange={value => setJobConfig(value, 'config.process[0].train.batch_size')}
507
- min={1}
 
 
508
  required
509
  />
510
  </div>
@@ -545,40 +569,32 @@ export default function SimplifiedJob({
545
  value={trainingBackend}
546
  onChange={value => setTrainingBackend?.(value as 'local' | 'hf-jobs')}
547
  options={effectiveBackendOptions}
548
- disabled={forceHFBackend || !setTrainingBackend}
549
- />
550
- {trainingBackend === 'local' && (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  <SelectInput
552
- label="GPU"
553
- value={gpuIDs ?? ''}
554
- onChange={value => setGpuIDs(value)}
555
- options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
 
 
 
556
  />
557
- )}
558
- <NumberInput
559
- label="Learning Rate"
560
- value={process.train.lr}
561
- onChange={value => setJobConfig(value, 'config.process[0].train.lr')}
562
- min={0}
563
- step={0.000001}
564
- required
565
- />
566
- <NumberInput
567
- label="LoRA Rank"
568
- value={process.network?.linear ?? 32}
569
- onChange={handleLoraRankChange}
570
- min={1}
571
- required
572
- />
573
- <SelectInput
574
- label="Optimizer"
575
- value={process.train.optimizer}
576
- onChange={value => setJobConfig(value, 'config.process[0].train.optimizer')}
577
- options={[
578
- { value: 'adamw8bit', label: 'AdamW8Bit' },
579
- { value: 'adafactor', label: 'Adafactor' },
580
- ]}
581
- />
582
  <div className="flex items-center gap-2 pt-2">
583
  <Checkbox
584
  label="Skip First Sample"
@@ -672,6 +688,7 @@ export default function SimplifiedJob({
672
  <TextInput
673
  label="Default Caption"
674
  value={dataset.default_caption || ''}
 
675
  onChange={value => setJobConfig(value || '', 'config.process[0].datasets[0].default_caption')}
676
  placeholder="Optional fallback caption"
677
  />
 
22
  'text/*': ['.txt'],
23
  };
24
 
25
+ const FOUR_BATCH_ARCHES = new Set(['flux', 'flux_kontext', 'flex1', 'flex2', 'wan21:1b']);
26
+
27
+ const resolveBatchSize = (arch: string) => (FOUR_BATCH_ARCHES.has(arch) ? 4 : 1);
28
+
29
  const slugify = (value: string) =>
30
  value
31
  .toLowerCase()
 
33
  .replace(/^-+|-+$/g, '')
34
  .slice(0, 64);
35
 
36
+ const buildDatasetName = (base: string, suffix: string) => {
37
+ const slug = slugify(base || 'dataset');
38
+ if (!slug) {
39
+ return `dataset${suffix}`;
40
+ }
41
+ return `${slug}${suffix}`;
42
+ };
43
+
44
  type SimplifiedJobProps = {
45
  jobConfig: JobConfig;
46
  setJobConfig: (value: any, key: string) => void;
 
112
  const [trainDatasetMode, setTrainDatasetMode] = useState<DatasetMode>('upload');
113
  const [trainModeTouched, setTrainModeTouched] = useState(hasCustomTrainPath);
114
  const [trainDatasetName, setTrainDatasetName] = useState(() =>
115
+ buildDatasetName(jobConfig.config.name || 'training-data', '-dataset'),
116
  );
117
  const [trainDatasetNameTouched, setTrainDatasetNameTouched] = useState(false);
118
  const [trainDatasetResolvedName, setTrainDatasetResolvedName] = useState<string | null>(null);
 
127
  const [controlDatasetMode, setControlDatasetMode] = useState<DatasetMode>('upload');
128
  const [controlModeTouched, setControlModeTouched] = useState(hasCustomControlPath);
129
  const [controlDatasetName, setControlDatasetName] = useState(() =>
130
+ buildDatasetName(jobConfig.config.name || 'training', '-control-dataset'),
131
  );
132
  const [controlDatasetNameTouched, setControlDatasetNameTouched] = useState(false);
133
  const [controlDatasetResolvedName, setControlDatasetResolvedName] = useState<string | null>(null);
 
160
  }
161
  }, [process]);
162
 
163
+ useEffect(() => {
164
+ const desiredBatchSize = resolveBatchSize(process.model.arch);
165
+ if (process.train.batch_size !== desiredBatchSize) {
166
+ setJobConfig(desiredBatchSize, 'config.process[0].train.batch_size');
167
+ }
168
+ }, [process.model.arch, process.train.batch_size, setJobConfig]);
169
+
170
  const handleModelChange = (newModel: string) => {
171
  const currentArch = modelArchs.find(a => a.name === process.model.arch);
172
  if (!currentArch || currentArch.name === newModel) {
 
196
  setJobConfig(false, 'config.process[0].model.low_vram');
197
 
198
  setJobConfig(newModel, 'config.process[0].model.arch');
199
+ setJobConfig(resolveBatchSize(newModel), 'config.process[0].train.batch_size');
200
 
201
  // Sync dataset controls
202
  const hasControl = nextArch?.additionalSections?.includes('datasets.control_path') ?? false;
 
251
 
252
  useEffect(() => {
253
  if (!trainDatasetNameTouched) {
254
+ setTrainDatasetName(buildDatasetName(jobConfig.config.name || 'training-data', '-dataset'));
255
  }
256
  if (!controlDatasetNameTouched) {
257
+ setControlDatasetName(buildDatasetName(jobConfig.config.name || 'training', '-control-dataset'));
258
  }
259
  }, [jobConfig.config.name, trainDatasetNameTouched, controlDatasetNameTouched]);
260
 
 
510
  <TextInput
511
  label="Trigger Word"
512
  value={process.trigger_word || ''}
513
+ docKey="config.process[0].trigger_word"
514
  onChange={value => setJobConfig(value?.trim() === '' ? null : value, 'config.process[0].trigger_word')}
515
  placeholder="Optional keyword"
516
  />
517
  <NumberInput
518
  label="Training Steps"
519
  value={process.train.steps}
520
+ docKey="config.process[0].train.steps"
521
  onChange={value => setJobConfig(value, 'config.process[0].train.steps')}
522
  min={1}
523
  required
524
  />
525
  <NumberInput
526
+ label="Learning Rate"
527
+ value={process.train.lr}
528
+ docKey="config.process[0].train.lr"
529
+ onChange={value => setJobConfig(value, 'config.process[0].train.lr')}
530
+ min={0}
531
+ step={0.000001}
532
  required
533
  />
534
  </div>
 
569
  value={trainingBackend}
570
  onChange={value => setTrainingBackend?.(value as 'local' | 'hf-jobs')}
571
  options={effectiveBackendOptions}
572
+ disabled={forceHFBackend || !setTrainingBackend}
573
+ />
574
+ {trainingBackend === 'local' && (
575
+ <SelectInput
576
+ label="GPU"
577
+ value={gpuIDs ?? ''}
578
+ onChange={value => setGpuIDs(value)}
579
+ options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
580
+ />
581
+ )}
582
+ <NumberInput
583
+ label="LoRA Rank"
584
+ value={process.network?.linear ?? 32}
585
+ onChange={handleLoraRankChange}
586
+ min={1}
587
+ required
588
+ />
589
  <SelectInput
590
+ label="Optimizer"
591
+ value={process.train.optimizer}
592
+ onChange={value => setJobConfig(value, 'config.process[0].train.optimizer')}
593
+ options={[
594
+ { value: 'adamw8bit', label: 'AdamW8Bit' },
595
+ { value: 'adafactor', label: 'Adafactor' },
596
+ ]}
597
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  <div className="flex items-center gap-2 pt-2">
599
  <Checkbox
600
  label="Skip First Sample"
 
688
  <TextInput
689
  label="Default Caption"
690
  value={dataset.default_caption || ''}
691
+ docKey="datasets.default_caption"
692
  onChange={value => setJobConfig(value || '', 'config.process[0].datasets[0].default_caption')}
693
  placeholder="Optional fallback caption"
694
  />
ui/src/app/jobs/new/jobConfig.ts CHANGED
@@ -53,7 +53,7 @@ export const defaultJobConfig: JobConfig = {
53
  },
54
  datasets: [defaultDatasetConfig],
55
  train: {
56
- batch_size: 4,
57
  bypass_guidance_embedding: true,
58
  steps: 1200,
59
  gradient_accumulation: 1,
@@ -69,7 +69,7 @@ export const defaultJobConfig: JobConfig = {
69
  },
70
  unload_text_encoder: false,
71
  cache_text_embeddings: false,
72
- lr: 0.0001,
73
  ema_config: {
74
  use_ema: false,
75
  ema_decay: 0.99,
 
53
  },
54
  datasets: [defaultDatasetConfig],
55
  train: {
56
+ batch_size: 1,
57
  bypass_guidance_embedding: true,
58
  steps: 1200,
59
  gradient_accumulation: 1,
 
69
  },
70
  unload_text_encoder: false,
71
  cache_text_embeddings: false,
72
+ lr: 0.0005,
73
  ema_config: {
74
  use_ema: false,
75
  ema_decay: 0.99,
ui/src/app/jobs/new/options.ts CHANGED
@@ -48,6 +48,7 @@ export const modelArchs: ModelArch[] = [
48
  'config.process[0].model.quantize_te': [true, false],
49
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
50
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
 
51
  },
52
  disableSections: ['network.conv'],
53
  },
@@ -63,6 +64,7 @@ export const modelArchs: ModelArch[] = [
63
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
64
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
65
  'config.process[0].train.timestep_type': ['weighted', 'sigmoid'],
 
66
  },
67
  disableSections: ['network.conv'],
68
  additionalSections: ['datasets.control_path', 'sample.ctrl_img'],
@@ -79,6 +81,7 @@ export const modelArchs: ModelArch[] = [
79
  'config.process[0].train.bypass_guidance_embedding': [true, false],
80
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
81
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
 
82
  },
83
  disableSections: ['network.conv'],
84
  },
@@ -107,6 +110,7 @@ export const modelArchs: ModelArch[] = [
107
  'config.process[0].train.bypass_guidance_embedding': [true, false],
108
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
109
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
 
110
  },
111
  disableSections: ['network.conv'],
112
  },
@@ -138,6 +142,7 @@ export const modelArchs: ModelArch[] = [
138
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
139
  'config.process[0].sample.num_frames': [41, 1],
140
  'config.process[0].sample.fps': [16, 1],
 
141
  },
142
  disableSections: ['network.conv'],
143
  additionalSections: ['datasets.num_frames', 'model.low_vram'],
@@ -371,6 +376,7 @@ export const modelArchs: ModelArch[] = [
371
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
372
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
373
  'config.process[0].train.lr': [0.0002, 0.0001],
 
374
  'config.process[0].train.timestep_type': ['shift', 'sigmoid'],
375
  'config.process[0].network.network_kwargs.ignore_if_contains': [['ff_i.experts', 'ff_i.gate'], []],
376
  },
@@ -392,6 +398,7 @@ export const modelArchs: ModelArch[] = [
392
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
393
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
394
  'config.process[0].train.lr': [0.0001, 0.0001],
 
395
  'config.process[0].train.timestep_type': ['weighted', 'sigmoid'],
396
  'config.process[0].network.network_kwargs.ignore_if_contains': [['ff_i.experts', 'ff_i.gate'], []],
397
  },
 
48
  'config.process[0].model.quantize_te': [true, false],
49
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
50
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
51
+ 'config.process[0].train.batch_size': [4, 1],
52
  },
53
  disableSections: ['network.conv'],
54
  },
 
64
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
65
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
66
  'config.process[0].train.timestep_type': ['weighted', 'sigmoid'],
67
+ 'config.process[0].train.batch_size': [4, 1],
68
  },
69
  disableSections: ['network.conv'],
70
  additionalSections: ['datasets.control_path', 'sample.ctrl_img'],
 
81
  'config.process[0].train.bypass_guidance_embedding': [true, false],
82
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
83
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
84
+ 'config.process[0].train.batch_size': [4, 1],
85
  },
86
  disableSections: ['network.conv'],
87
  },
 
110
  'config.process[0].train.bypass_guidance_embedding': [true, false],
111
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
112
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
113
+ 'config.process[0].train.batch_size': [4, 1],
114
  },
115
  disableSections: ['network.conv'],
116
  },
 
142
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
143
  'config.process[0].sample.num_frames': [41, 1],
144
  'config.process[0].sample.fps': [16, 1],
145
+ 'config.process[0].train.batch_size': [4, 1],
146
  },
147
  disableSections: ['network.conv'],
148
  additionalSections: ['datasets.num_frames', 'model.low_vram'],
 
376
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
377
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
378
  'config.process[0].train.lr': [0.0002, 0.0001],
379
+ 'config.process[0].train.batch_size': [1, 1],
380
  'config.process[0].train.timestep_type': ['shift', 'sigmoid'],
381
  'config.process[0].network.network_kwargs.ignore_if_contains': [['ff_i.experts', 'ff_i.gate'], []],
382
  },
 
398
  'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
399
  'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
400
  'config.process[0].train.lr': [0.0001, 0.0001],
401
+ 'config.process[0].train.batch_size': [1, 1],
402
  'config.process[0].train.timestep_type': ['weighted', 'sigmoid'],
403
  'config.process[0].network.network_kwargs.ignore_if_contains': [['ff_i.experts', 'ff_i.gate'], []],
404
  },
ui/src/docs.tsx CHANGED
@@ -51,6 +51,24 @@ const docs: { [key: string]: ConfigDoc } = {
51
  </>
52
  ),
53
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  'datasets.control_path': {
55
  title: 'Control Dataset',
56
  description: (
@@ -60,6 +78,15 @@ const docs: { [key: string]: ConfigDoc } = {
60
  </>
61
  ),
62
  },
 
 
 
 
 
 
 
 
 
63
  'datasets.num_frames': {
64
  title: 'Number of Frames',
65
  description: (
 
51
  </>
52
  ),
53
  },
54
+ 'config.process[0].train.steps': {
55
+ title: 'Training Steps',
56
+ description: (
57
+ <>
58
+ Total number of optimization steps to run. For LoRA Frenzi submissions keep this at or below 5,000 steps. If you
59
+ adjust batch size or gradient accumulation, the effective number of image updates scales accordingly.
60
+ </>
61
+ ),
62
+ },
63
+ 'config.process[0].train.lr': {
64
+ title: 'Learning Rate',
65
+ description: (
66
+ <>
67
+ Base learning rate for the optimizer. Typical LoRA runs stay in the 1e-4 to 5e-4 range. Higher values train
68
+ faster but can destabilize or overfit; lower values are more conservative.
69
+ </>
70
+ ),
71
+ },
72
  'datasets.control_path': {
73
  title: 'Control Dataset',
74
  description: (
 
78
  </>
79
  ),
80
  },
81
+ 'datasets.default_caption': {
82
+ title: 'Default Caption',
83
+ description: (
84
+ <>
85
+ Optional text that will be used as the caption when an image does not have its own caption file. If every image
86
+ already has a matching <code>.txt</code> caption, you can leave this blank.
87
+ </>
88
+ ),
89
+ },
90
  'datasets.num_frames': {
91
  title: 'Number of Frames',
92
  description: (