File size: 8,220 Bytes
e0265b9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | from __future__ import annotations
import math
import os
from pathlib import Path
from typing import Any
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
def dataset_image_count(raw_path: object) -> int:
path = Path(str(raw_path or "")).expanduser()
if not path.is_dir():
return 0
try:
return sum(
1 for item in path.rglob("*")
if item.is_file() and item.suffix.casefold() in IMAGE_EXTENSIONS
)
except OSError:
return 0
def _available_vram_gb() -> float | None:
try:
import pynvml
pynvml.nvmlInit()
try:
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
return pynvml.nvmlDeviceGetMemoryInfo(handle).total / (1024 ** 3)
finally:
pynvml.nvmlShutdown()
except Exception:
return None
def recommend_training_settings(
trainer: str,
image_count: int,
resolution: int,
*,
vram_gb: float | None = None,
) -> dict[str, Any]:
"""Return an explainable, conservative starting recipe for manual review."""
trainer = str(trainer).casefold()
images = max(10, int(image_count))
resolution = max(64, min(512, int(resolution)))
vram = _available_vram_gb() if vram_gb is None else vram_gb
cpu_workers = max(2, min(8, (os.cpu_count() or 4) // 2))
if trainer == "lora":
epochs = max(20, min(200, round(8_000 / images)))
return {
"epochs": epochs,
"settings": {},
"summary": (
f"ORION chose {epochs} epochs from about {images:,} images. "
"The connected LoRA trainer continues to own its detailed recipe."
),
}
exposure_target = 180_000 if trainer == "ddpm" else 120_000
minimum_epochs = 25
maximum_epochs = 600 if trainer == "ddpm" else 300
epochs = max(minimum_epochs, min(maximum_epochs, round(exposure_target / images)))
batch_by_resolution = {
64: 16 if trainer == "ddpm" else 12,
128: 12 if trainer == "ddpm" else 8,
256: 4 if trainer == "ddpm" else 4,
384: 2,
512: 1,
}
nearest_resolution = min(batch_by_resolution, key=lambda size: abs(size - resolution))
batch = batch_by_resolution[nearest_resolution]
if vram is not None and vram < 8:
batch = max(1, batch // 2)
gradient_checkpointing = resolution >= 384 or (vram is not None and vram < 8)
settings: dict[str, Any] = {
"batch_size": batch,
"learning_rate": 0.0001 if trainer == "ddpm" else 0.0002,
"gradient_accumulation_steps": 1,
"dataloader_num_workers": cpu_workers,
"mixed_precision": "fp16",
"save_every": max(5, min(25, max(1, epochs // 10))),
"preview_steps": 50 if trainer == "ddpm" else 10,
"preview_every": max(5, min(50, max(1, epochs // 10))),
"training_intensity": 100,
"gradient_checkpointing": gradient_checkpointing,
}
if trainer == "flow":
settings["gradient_accumulation"] = settings["gradient_accumulation_steps"]
settings["workers"] = settings["dataloader_num_workers"]
memory_note = (
f" using the detected {vram:.0f} GB GPU" if vram is not None
else " without assuming a specific GPU"
)
return {
"epochs": epochs,
"settings": settings,
"summary": (
f"ORION chose about {epochs:,} epochs ({images * epochs:,} image exposures), "
f"batch {batch} at {resolution}px{memory_note}. Review this starting recipe before training."
),
}
def review_training_plan(plan: Any) -> dict[str, Any]:
"""Give a conservative, explainable review without changing user settings."""
findings: list[dict[str, str]] = []
training_steps = [step for step in plan.steps if step.tool_id.endswith("_trainer")]
projected_counts = {
str(Path(str(step.arguments.get("output_dir", ""))).expanduser()): int(
step.arguments.get("image_count", 0) or 0
)
for step in plan.steps
if step.tool_id in {"dataset_collector", "youtube_video_collector"}
and step.arguments.get("output_dir")
}
total_steps = 0
estimated_high_minutes = 0
for step in training_steps:
args = step.arguments
dataset_key = str(Path(str(args.get("dataset_dir", ""))).expanduser())
images = dataset_image_count(args.get("dataset_dir")) or projected_counts.get(dataset_key, 0)
epochs = max(1, int(args.get("epochs", 1) or 1))
batch = max(1, int(args.get("batch_size", 1) or 1))
accumulation = max(
1,
int(args.get("gradient_accumulation_steps", args.get("gradient_accumulation", 1)) or 1),
)
resolution = max(64, int(args.get("resolution", 256) or 256))
exposures = images * epochs if images else 0
optimizer_steps = math.ceil(images / batch / accumulation) * epochs if images else 0
total_steps += optimizer_steps
# This remains deliberately broad: it is a planning guardrail, not a promise.
if exposures:
resolution_factor = (resolution / 256) ** 2
estimated_high_minutes += max(1, math.ceil(exposures * resolution_factor / batch * 0.003))
label = step.title or step.tool_id.replace("_", " ").title()
if images >= 1_000 and epochs >= 300:
suggested_epochs = max(25, min(150, round(180_000 / images)))
findings.append({
"level": "warning",
"message": (
f"{label}: {images:,} images × {epochs:,} epochs requests "
f"{exposures:,} image exposures. This resembles a small-dataset preset. "
f"Review the intent; about {suggested_epochs} epochs is a safer initial test."
),
})
elif exposures >= 1_000_000:
findings.append({
"level": "warning",
"message": f"{label}: the plan exceeds 1,000,000 image exposures; confirm this is intentional.",
})
if images >= 750 and "batch_size" in args and batch == 1 and resolution <= 256:
findings.append({
"level": "warning",
"message": (
f"{label}: batch size 1 at {resolution}px may leave substantial GPU capacity unused. "
"Try a short test with a larger batch if VRAM allows."
),
})
if epochs >= 1_000:
findings.append({
"level": "warning",
"message": f"{label}: {epochs:,} epochs is unusually long and deserves explicit review.",
})
if not training_steps:
return {}
if not findings:
findings.append({
"level": "ready",
"message": "No obviously accidental training settings were found. Estimates are still approximate.",
})
level = "warning" if any(item["level"] == "warning" for item in findings) else "ready"
return {
"agent": "ORION",
"level": level,
"headline": "Review recommended" if level == "warning" else "Plan looks reasonable",
"findings": findings,
"estimated_optimizer_steps": total_steps,
"estimated_high_minutes": estimated_high_minutes,
"settings_changed": False,
}
def apply_orion_review(plan: Any) -> dict[str, Any]:
review = review_training_plan(plan)
plan.orion_review = review
if not review:
return review
lines = [f"ORION — {review['headline']}"]
lines.extend(f"• {item['message']}" for item in review["findings"])
if review["estimated_optimizer_steps"]:
lines.append(f"• Estimated optimizer steps: about {review['estimated_optimizer_steps']:,}")
plan.summary += "\n\n" + "\n".join(lines)
if review["level"] == "warning":
plan.requires_confirmation = True
reason = "ORION found unusual training settings. Review his findings before starting."
if reason not in plan.confirmation_reason:
plan.confirmation_reason = "; ".join(filter(None, [plan.confirmation_reason, reason]))
return review
|