File size: 13,619 Bytes
e0265b9 c61c435 e0265b9 c61c435 e0265b9 c61c435 e0265b9 f8c73f9 e0265b9 c61c435 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 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 | 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:
if not str(raw_path or "").strip():
return 0
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 _resolution_extent(value: object, fallback: int = 256) -> int:
raw = str(value or fallback).strip().lower()
if "x" in raw:
try:
return max(int(part.strip()) for part in raw.split("x", 1))
except ValueError:
return fallback
try:
return int(raw)
except ValueError:
return fallback
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, _resolution_extent(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
if step.tool_id == "wan_video_trainer":
from adam.video_lora import clips_in
count = len(clips_in(Path(str(args.get("dataset_dir", "")))))
epochs = int(args.get("epochs", 1))
findings.append({"level": "warning" if epochs > 100 or count < 20 else "ready", "message": (
f"Wan video: {count} clips, {epochs} epochs, frame buckets {args.get('target_frames', '25,49')}. "
"Each clip can contribute multiple frame buckets. Inspect motion and captions; video runtime requires a measured run."
)})
continue
if step.tool_id == "oasis_trainer":
from adam.oasis_dataset import inspect_oasis_dataset, oasis_pace
gap = max(1, int(args.get("frame_gap", 1) or 1))
pace = oasis_pace(args.get("dataset_dir", ""), frame_gap=gap)
recommended_gap = pace["recommended_frame_gap"]
capture_fps = pace["capture_fps"]
native_fps = pace["native_ai_fps"]
if isinstance(capture_fps, (int, float)) and isinstance(native_fps, (int, float)):
message = (
f"{step.title or 'Oasis'}: {float(capture_fps):g} FPS capture with "
f"prediction gap {gap} trains at a native pace of {float(native_fps):g} AI FPS."
)
if isinstance(recommended_gap, int) and recommended_gap != gap:
message += f" Dataset metadata recommends gap {recommended_gap} for responsive control."
findings.append({"level": "warning", "message": message})
else:
findings.append({"level": "ready", "message": message})
report = inspect_oasis_dataset(args.get("dataset_dir", ""), frame_gap=gap)
if report.ok and report.valid_transitions:
batch = max(1, int(args.get("batch_size", 1) or 1))
accumulation = max(1, int(args.get("gradient_accumulation", 1) or 1))
requested_chunk = max(0, int(args.get("chunk_size", 0) or 0))
transitions_per_epoch = min(report.valid_transitions, requested_chunk) if requested_chunk else report.valid_transitions
steps_per_epoch = math.ceil(transitions_per_epoch / batch / accumulation)
epochs = max(1, int(args.get("epochs", 1) or 1))
optimizer_steps = steps_per_epoch * epochs
total_steps += optimizer_steps
label = step.title or "Oasis"
findings.append({
"level": "ready",
"message": (
f"{label}: {report.valid_transitions:,} valid transitions; "
f"{transitions_per_epoch:,} used per epoch; about "
f"{steps_per_epoch:,} optimizer steps per epoch."
),
})
if report.valid_transitions >= 7_500 and not requested_chunk:
findings.append({
"level": "warning",
"message": (
f"{label}: every epoch uses all {report.valid_transitions:,} transitions. "
"Use a balanced 5,000-transition chunk or explicitly confirm the longer run."
),
})
if optimizer_steps >= 100_000:
findings.append({
"level": "warning",
"message": (
f"{label}: this plan schedules about {optimizer_steps:,} optimizer steps. "
"Run the short benchmark and inspect rollout previews before committing."
),
})
idle_ratio = report.idle_rows / max(1, report.valid_rows)
if idle_ratio < 0.05:
findings.append({
"level": "warning",
"message": (
f"{label}: only {idle_ratio:.1%} of labelled frames are idle. "
"Record more no-input gameplay to improve stable pauses."
),
})
rare_threshold = max(10, math.ceil(report.valid_rows * 0.01))
rare_controls = [
name for name, count in report.action_counts.items()
if 0 < count < rare_threshold
]
if rare_controls and not bool(args.get("balance_actions", False)):
findings.append({
"level": "warning",
"message": (
"Rare controls are present (" + ", ".join(rare_controls[:5])
+ "); turn on Balance rare actions or record more examples."
),
})
continue
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, _resolution_extent(args.get("resolution", 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
|