Add WebUI functionality for training with GPU support and enhance main function for config file handling

This commit is contained in:
hndrr 2025-07-16 17:18:08 +09:00
parent f379f943ac
commit 94c99ba7a2
2 changed files with 306 additions and 6 deletions

View File

@ -70,9 +70,9 @@ def print_end_message(jobs_completed, jobs_failed):
timeout=7200, # 2 hours, increase or decrease if needed
secrets=[modal.Secret.from_name("huggingface-secret")]
)
def main(config_file_list_str: str, recover: bool = False, name: str = None):
def main(config_file_list_str: str, recover: bool = False, name: str = None):
# convert the config file list from a string to a list
config_file_list = config_file_list_str.split(",")
config_file_list = str(config_file_list_str).split(",")
jobs_completed = 0
jobs_failed = 0
@ -104,6 +104,102 @@ def main(config_file_list_str: str, recover: bool = False, name: str = None):
print_end_message(jobs_completed, jobs_failed)
# WebUIアクセス用の新しい関数を追加GPU対応でトレーニング可能
@app.function(
image=image,
gpu="A100", # ← GPUを有効化WebUIからトレーニングできるように
cpu=4,
memory=32768, # 32GB
timeout=7200, # 2時間
volumes={MOUNT_DIR: model_volume}, # ← ボリュームもマウント
secrets=[modal.Secret.from_name("huggingface-secret")]
)
def webui():
import subprocess
import time
import os
# Hugging Faceにログイン
try:
from huggingface_hub import login
hf_token = os.environ.get("HF_TOKEN")
if hf_token:
login(token=hf_token)
print("✅ Successfully logged in to Hugging Face")
else:
print("⚠️ No HF_TOKEN found")
except Exception as e:
print(f"⚠️ Failed to login to Hugging Face: {e}")
# AI-toolkit UIディレクトリに移動
os.chdir("/app/ai-toolkit/ui")
# トレーニング結果保存先を作成
os.makedirs(MOUNT_DIR, exist_ok=True)
# modal.forwardでポート8675を公開
with modal.forward(8675) as tunnel:
print(f"🌐 AI Toolkit WebUI is accessible at: {tunnel.url}")
print(f"🔧 Starting UI server...")
print(f"💾 Training outputs will be saved to: {MOUNT_DIR}")
# npm run startでWebUIを起動
env_vars = os.environ.copy()
if hf_token:
env_vars.update({
"HF_TOKEN": hf_token,
"HUGGINGFACE_HUB_TOKEN": hf_token,
"HF_API_TOKEN": hf_token
})
process = subprocess.Popen(
["npm", "run", "start"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd="/app/ai-toolkit/ui",
env=env_vars # 環境変数を明示的に渡す
)
# サーバーが起動するまで待機
print("⏳ Waiting for server to start...")
time.sleep(30)
print(f"✅ WebUI should now be accessible at: {tunnel.url}")
print(f"🚀 You can now start training jobs from the WebUI!")
# 2時間維持トレーニング時間を考慮
try:
while process.poll() is None:
# プロセスのログを定期的に出力
try:
stdout_line = process.stdout.readline()
if stdout_line:
print(f"[WebUI] {stdout_line.decode().strip()}")
stderr_line = process.stderr.readline()
if stderr_line:
print(f"[WebUI ERROR] {stderr_line.decode().strip()}")
except:
pass
time.sleep(10)
# 定期的にボリュームをコミット
model_volume.commit()
except KeyboardInterrupt:
print("🛑 Shutting down WebUI...")
process.terminate()
finally:
# プロセス終了時の最終ログ
if process.poll() is not None:
stdout, stderr = process.communicate()
if stdout:
print(f"[WebUI Final STDOUT] {stdout.decode()}")
if stderr:
print(f"[WebUI Final STDERR] {stderr.decode()}")
# 最終コミット
model_volume.commit()
print("💾 Final volume commit completed")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
@ -129,9 +225,20 @@ if __name__ == "__main__":
default=None,
help='Name to replace [name] tag in config file, useful for shared config file'
)
# WebUIオプションを追加
parser.add_argument(
'--webui',
action='store_true',
help='Launch WebUI instead of training'
)
args = parser.parse_args()
# convert list of config files to a comma-separated string for Modal compatibility
config_file_list_str = ",".join(args.config_file_list)
main.call(config_file_list_str=config_file_list_str, recover=args.recover, name=args.name)
if args.webui:
# WebUI起動
webui.remote()
else:
# 従来通りのトレーニング実行
config_file_list_str = ",".join(args.config_file_list)
main.remote(config_file_list_str=config_file_list_str, recover=args.recover, name=args.name)

193
run_modal_docker_bk.py Normal file
View File

@ -0,0 +1,193 @@
'''
ostris/ai-toolkit on https://modal.com
Run training with the following command:
modal run run_modal.py --config-file-list-str=/app/ai-toolkit/config/whatever_you_want.yml
'''
import os
os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
import sys
import modal
from dotenv import load_dotenv
# Load the .env file if it exists
load_dotenv()
sys.path.insert(0, "/app/ai-toolkit")
# must come before ANY torch or fastai imports
# import toolkit.cuda_malloc
# turn off diffusers telemetry until I can figure out how to make it opt-in
os.environ['DISABLE_TELEMETRY'] = 'YES'
# define the volume for storing model outputs, using "creating volumes lazily": https://modal.com/docs/guide/volumes
# you will find your model, samples and optimizer stored in: https://modal.com/storage/your-username/main/flux-lora-models
model_volume = modal.Volume.from_name("flux-lora-models", create_if_missing=True)
# modal_output, due to "cannot mount volume on non-empty path" requirement
MOUNT_DIR = "/app/ai-toolkit/modal_output" # modal_output, due to "cannot mount volume on non-empty path" requirement
# define modal app
image = modal.Image.from_dockerfile("docker/Dockerfile").add_local_dir("config", "/app/ai-toolkit/config").add_local_dir("input/images", '/app/ai-toolkit/input/images')
# mount for the entire ai-toolkit directory
# example: "/Users/username/ai-toolkit" is the local directory, "/root/ai-toolkit" is the remote directory
# code_mount = modal.Mount.from_local_dir("D:/ai-toolkit", remote_path="/root/ai-toolkit")
# create the Modal app with the necessary mounts and volumes
# app = modal.App(name="flux-lora-training", image=image, mounts=[code_mount], volumes={MOUNT_DIR: model_volume})
app = modal.App(name="flux-lora-training", image=image, volumes={MOUNT_DIR: model_volume})
# Check if we have DEBUG_TOOLKIT in env
if os.environ.get("DEBUG_TOOLKIT", "0") == "1":
# Set torch to trace mode
import torch
torch.autograd.set_detect_anomaly(True)
import argparse
from toolkit.job import get_job
def print_end_message(jobs_completed, jobs_failed):
failure_string = f"{jobs_failed} failure{'' if jobs_failed == 1 else 's'}" if jobs_failed > 0 else ""
completed_string = f"{jobs_completed} completed job{'' if jobs_completed == 1 else 's'}"
print("")
print("========================================")
print("Result:")
if len(completed_string) > 0:
print(f" - {completed_string}")
if len(failure_string) > 0:
print(f" - {failure_string}")
print("========================================")
@app.function(
# request a GPU with at least 24GB VRAM
# more about modal GPU's: https://modal.com/docs/guide/gpu
gpu="A100", # gpu="H100"
# more about modal timeouts: https://modal.com/docs/guide/timeouts
timeout=7200, # 2 hours, increase or decrease if needed
secrets=[modal.Secret.from_name("huggingface-secret")]
)
def main(config_file_list_str: str, recover: bool = False, name: str = None):
# convert the config file list from a string to a list
config_file_list = config_file_list_str.split(",")
jobs_completed = 0
jobs_failed = 0
print(f"Running {len(config_file_list)} job{'' if len(config_file_list) == 1 else 's'}")
for config_file in config_file_list:
try:
job = get_job(config_file, name)
job.config['process'][0]['training_folder'] = MOUNT_DIR
os.makedirs(MOUNT_DIR, exist_ok=True)
print(f"Training outputs will be saved to: {MOUNT_DIR}")
# run the job
job.run()
# commit the volume after training
model_volume.commit()
job.cleanup()
jobs_completed += 1
except Exception as e:
print(f"Error running job: {e}")
jobs_failed += 1
if not recover:
print_end_message(jobs_completed, jobs_failed)
raise e
print_end_message(jobs_completed, jobs_failed)
# WebUIアクセス用の新しい関数を追加GPU対応でトレーニング可能
@app.function(
image=image,
gpu="A100",
cpu=4,
memory=32768, # 32GB
timeout=7200, # 2時間
volumes={MOUNT_DIR: model_volume}
)
def webui():
import subprocess
import time
import os
# AI-toolkit UIディレクトリに移動
os.chdir("/app/ai-toolkit/ui")
# トレーニング結果保存先を作成
os.makedirs(MOUNT_DIR, exist_ok=True)
# modal.forwardでポート8675を公開
with modal.forward(8675) as tunnel:
print(f"🌐 AI Toolkit WebUI is accessible at: {tunnel.url}")
print(f"🔧 Starting UI server...")
print(f"💾 Training outputs will be saved to: {MOUNT_DIR}")
# npm run startでWebUIを起動
process = subprocess.Popen(
["npm", "run", "start"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd="/app/ai-toolkit/ui",
env={**os.environ, "TRAINING_FOLDER": MOUNT_DIR}
)
# サーバーが起動するまで待機
print("⏳ Waiting for server to start...")
time.sleep(30)
print(f"✅ WebUI should now be accessible at: {tunnel.url}")
print(f"🚀 You can now start training jobs from the WebUI!")
# 2時間維持トレーニング時間を考慮
try:
while process.poll() is None:
time.sleep(60)
# 定期的にボリュームをコミット
model_volume.commit()
except KeyboardInterrupt:
print("🛑 Shutting down WebUI...")
process.terminate()
finally:
# 最終コミット
model_volume.commit()
print("💾 Final volume commit completed")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# require at least one config file
parser.add_argument(
'config_file_list',
nargs='+',
type=str,
help='Name of config file (eg: person_v1 for config/person_v1.json/yaml), or full path if it is not in config folder, you can pass multiple config files and run them all sequentially'
)
# flag to continue if a job fails
parser.add_argument(
'-r', '--recover',
action='store_true',
help='Continue running additional jobs even if a job fails'
)
# optional name replacement for config file
parser.add_argument(
'-n', '--name',
type=str,
default=None,
help='Name to replace [name] tag in config file, useful for shared config file'
)
args = parser.parse_args()
# convert list of config files to a comma-separated string for Modal compatibility
config_file_list_str = ",".join(args.config_file_list)
main.call(config_file_list_str=config_file_list_str, recover=args.recover, name=args.name)