硬件迁移:从 RTX 5090 (32GB) 迁移到 RTX 3090 (24GB) 主要改动: - 所有启动脚本和配置文件中的 /home/xsl/ 路径替换为 /home/ubuntu/ - 适配新的 24GB VRAM 环境
125 lines
5.2 KiB
Python
125 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""直接调 kohya train_network.py 真正并行训练 LoRA
|
||
|
||
绕过 photo_service 的串行阻塞,用 subprocess 直接起 train_network.py 进程,
|
||
通过 ThreadPoolExecutor 控制并发数。
|
||
|
||
用法:
|
||
python /home/ubuntu/change_hair/train_lora_parallel.py
|
||
|
||
前提:step1 训练数据已准备好(data/train_material/<hid>/images/1_hairstyle/*.png)
|
||
"""
|
||
import os
|
||
import sys
|
||
import time
|
||
import subprocess
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
|
||
TRAIN_DIR = "/home/ubuntu/change_hair/data/train_material"
|
||
KOHYA_WORKDIR = "/home/ubuntu/change_hair/project/kohya_ss_home/kohya_ss"
|
||
BASE_MODEL = "/home/ubuntu/change_hair/project/onediff/stable-diffusion-webui/models/Stable-diffusion/v1-5-pruned-emaonly.safetensors"
|
||
KOHYA_ACCEL = "/home/ubuntu/miniconda3/envs/kohya/bin/accelerate"
|
||
LOG_DIR = "/home/ubuntu/change_hair/project/logs"
|
||
|
||
# 本次要训练的发型
|
||
HAIRSTYLES = ["chang_tuoyuan", "chang_zhixian", "chang_huaban", "chang_xinxing"]
|
||
# chang_bolang 已完成,跳过
|
||
PARALLEL = 2
|
||
|
||
|
||
def log(msg):
|
||
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
||
|
||
|
||
def train_one(hair_id):
|
||
"""训练单个发型的 LoRA,返回 (hair_id, success, log_path)"""
|
||
images_dir = os.path.join(TRAIN_DIR, hair_id, "images")
|
||
model_dir = os.path.join(TRAIN_DIR, hair_id, "model")
|
||
os.makedirs(model_dir, exist_ok=True)
|
||
|
||
# 准备 sample prompt 文件
|
||
sample_dir = os.path.join(model_dir, "sample")
|
||
os.makedirs(sample_dir, exist_ok=True)
|
||
sample_txt = os.path.join(sample_dir, "prompt.txt")
|
||
with open(sample_txt, "w") as f:
|
||
f.write('titor hairstyle, easyphoto, faceless, no human, white background, simple background, '
|
||
' --n low quality, worst quality, bad anatomy, bad composition, poor, low effort --h 768 '
|
||
'--w 768 --s 30 --l 7')
|
||
|
||
log_path = os.path.join(LOG_DIR, f"train_{hair_id}.log")
|
||
|
||
cmd = [
|
||
"bash", "-c",
|
||
f'cd {KOHYA_WORKDIR} && '
|
||
f'HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 CUDA_VISIBLE_DEVICES=0 '
|
||
f'{KOHYA_ACCEL} launch --num_cpu_threads_per_process=2 "./train_network.py" --enable_bucket '
|
||
f'--min_bucket_reso=256 --max_bucket_reso=2048 --pretrained_model_name_or_path="{BASE_MODEL}" '
|
||
f'--train_data_dir={images_dir} --resolution="2000,2000" '
|
||
f'--output_dir={model_dir} '
|
||
'--network_alpha="64" --save_model_as=safetensors --network_module=networks.lora --text_encoder_lr=5e-05 '
|
||
'--unet_lr=0.0001 --network_dim=128 --output_name="hairstyle_hd_lora" --lr_scheduler_num_cycles="20" '
|
||
'--no_half_vae --learning_rate="0.0001" --lr_scheduler="cosine" --lr_warmup_steps="650" --train_batch_size="1" '
|
||
'--max_train_steps="1500" --save_every_n_epochs="100" --mixed_precision="fp16" --save_precision="fp16" '
|
||
'--caption_extension=".txt" --sample_sampler=ddim '
|
||
f'--sample_prompts={sample_txt} --sample_every_n_epochs="1000" '
|
||
'--seed="1234" --cache_latents --optimizer_type="AdamW" --max_data_loader_n_workers="0" --bucket_reso_steps=64 '
|
||
'--sdpa --bucket_no_upscale --noise_offset=0.0'
|
||
]
|
||
|
||
log(f" ▶ {hair_id} 开始训练,日志: {log_path}")
|
||
t0 = time.time()
|
||
try:
|
||
with open(log_path, "w") as lf:
|
||
proc = subprocess.run(cmd, stdout=lf, stderr=subprocess.STDOUT, timeout=1800)
|
||
ok = proc.returncode == 0
|
||
lora = os.path.join(model_dir, "hairstyle_hd_lora.safetensors")
|
||
ok = ok and os.path.exists(lora) and os.path.getsize(lora) > 100000000
|
||
elapsed = time.time() - t0
|
||
sz = os.path.getsize(lora) // 1024 // 1024 if os.path.exists(lora) else 0
|
||
log(f" {'✓' if ok else '✗'} {hair_id} {'训练完成' if ok else '训练失败'} ({sz}MB, {elapsed:.0f}s)")
|
||
return (hair_id, ok, log_path)
|
||
except subprocess.TimeoutExpired:
|
||
log(f" ✗ {hair_id} 训练超时")
|
||
return (hair_id, False, log_path)
|
||
except Exception as e:
|
||
log(f" ✗ {hair_id} 训练异常: {e}")
|
||
return (hair_id, False, log_path)
|
||
|
||
|
||
def main():
|
||
t0 = time.time()
|
||
log(f"并行 LoRA 训练: {HAIRSTYLES} (并发={PARALLEL})")
|
||
|
||
results = {}
|
||
with ThreadPoolExecutor(max_workers=PARALLEL) as ex:
|
||
futures = {ex.submit(train_one, hid): hid for hid in HAIRSTYLES}
|
||
for fut in as_completed(futures):
|
||
hid = futures[fut]
|
||
try:
|
||
hair_id, ok, log_path = fut.result()
|
||
results[hair_id] = ok
|
||
except Exception as e:
|
||
log(f" ✗ {hid} 异常: {e}")
|
||
results[hid] = False
|
||
|
||
log(f"\n{'='*60}")
|
||
log(f"全部完成,耗时 {time.time()-t0:.0f}s")
|
||
for hid, ok in results.items():
|
||
lora = os.path.join(TRAIN_DIR, hid, "model", "hairstyle_hd_lora.safetensors")
|
||
sz = os.path.getsize(lora) // 1024 // 1024 if os.path.exists(lora) else 0
|
||
log(f" {'✓' if ok else '✗'} {hid} ({sz}MB)")
|
||
success = [h for h, ok in results.items() if ok]
|
||
log(f"成功: {success}")
|
||
|
||
# 写完成清单
|
||
done_file = os.path.join(LOG_DIR, "train_lora_done.json")
|
||
import json
|
||
with open(done_file, "w") as f:
|
||
json.dump({"completed": success, "all": HAIRSTYLES}, f)
|
||
log(f"完成清单: {done_file}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|