HayrettinIscan commited on
Commit
cd3284d
·
verified ·
1 Parent(s): a126a7a

Faz2: code/train_pipeline.py

Browse files
Files changed (1) hide show
  1. code/train_pipeline.py +795 -0
code/train_pipeline.py ADDED
@@ -0,0 +1,795 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MeshAI Train Pipeline - Hybrid 3D AI Training Engine
4
+ Fine-tunes TRELLIS geometry + Hunyuan3D PBR using MeshAI-Gold-5K.
5
+
6
+ Monitoring (otomatik — TensorBoard yok):
7
+ - training_progress.log -> temiz özet satırları
8
+ - training_status.json -> son durum + sağlik
9
+ - outputs/validation/ -> step_XXXX örnek klasörleri
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import gc
15
+ import hashlib
16
+ import json
17
+ import os
18
+ import sys
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
24
+
25
+ import numpy as np
26
+ import torch
27
+ import torch.nn as nn
28
+ from torch.utils.data import DataLoader, Dataset, Subset
29
+
30
+ if sys.platform == "win32":
31
+ import io
32
+
33
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
34
+
35
+ HF_TOKEN = os.getenv("HF_TOKEN")
36
+ HF_REPO = os.getenv("HF_REPO", "HayrettinIscan/MeshAI-Base-Models")
37
+ GOLD_REPO = os.getenv("HF_GOLD_REPO", "HayrettinIscan/MeshAI-Gold-5K")
38
+ LOW_VRAM = os.getenv("MESHAI_LOW_VRAM", "1") == "1"
39
+ ROOT = Path(__file__).resolve().parent
40
+ CHECKPOINT_DIR = ROOT / "checkpoints"
41
+ OUTPUT_DIR = ROOT / "outputs" / "validation"
42
+ PROGRESS_LOG = ROOT / "training_progress.log"
43
+ STATUS_JSON = ROOT / "training_status.json"
44
+ VALIDATION_UIDS_PATH = ROOT / "validation_uids.json"
45
+ CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)
46
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
47
+
48
+ GOLD_KEYWORDS = ["scifi", "mechanical", "robot", "vehicle", "industrial", "engine", "cyberpunk"]
49
+ TRAIN_PIPELINE_VERSION = "v4.0"
50
+ TRAIN_PIPELINE_STUB_VERSION = "v3.3"
51
+
52
+ class TrainMonitor:
53
+ """TensorBoard yerine basit dosya tabanlı izleme."""
54
+
55
+ def __init__(self) -> None:
56
+ self.nan_skips = 0
57
+ self.last: dict[str, Any] = {
58
+ "version": TRAIN_PIPELINE_VERSION,
59
+ "health": "starting",
60
+ "global_step": 0,
61
+ "epoch": 0,
62
+ "train_loss_geometry": None,
63
+ "train_loss_texture": None,
64
+ "val_loss_geometry": None,
65
+ "val_loss_texture": None,
66
+ "vram_gb": None,
67
+ "updated_at": None,
68
+ }
69
+ PROGRESS_LOG.write_text(
70
+ f"[{self._ts()}] Egitim izleme basladi (surum {TRAIN_PIPELINE_VERSION})\n",
71
+ encoding="utf-8",
72
+ )
73
+ self._save_status()
74
+
75
+ def _ts(self) -> str:
76
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
77
+
78
+ def _append(self, line: str) -> None:
79
+ with open(PROGRESS_LOG, "a", encoding="utf-8") as handle:
80
+ handle.write(line + "\n")
81
+
82
+ def _save_status(self) -> None:
83
+ self.last["updated_at"] = self._ts()
84
+ with open(STATUS_JSON, "w", encoding="utf-8") as handle:
85
+ json.dump(self.last, handle, indent=2, ensure_ascii=False)
86
+
87
+ def _vram_gb(self) -> float | None:
88
+ if not torch.cuda.is_available():
89
+ return None
90
+ return round(torch.cuda.memory_allocated() / (1024**3), 2)
91
+
92
+ def _health(self) -> str:
93
+ vals = [
94
+ self.last.get("train_loss_geometry"),
95
+ self.last.get("train_loss_texture"),
96
+ self.last.get("val_loss_geometry"),
97
+ self.last.get("val_loss_texture"),
98
+ ]
99
+ if any(v is not None and not np.isfinite(v) for v in vals):
100
+ return "kritik_nan"
101
+ if self.nan_skips >= 10:
102
+ return "uyari_cok_nan"
103
+ return "iyi"
104
+
105
+ def note_nan_skip(self, loss_name: str = "") -> None:
106
+ self.nan_skips += 1
107
+ self.last["health"] = self._health()
108
+ if self.nan_skips <= 3 or self.nan_skips % 100 == 0:
109
+ self._append(
110
+ f"[{self._ts()}] UYARI: NaN/Inf batch atlandi "
111
+ f"({loss_name or 'unknown'}, toplam={self.nan_skips})"
112
+ )
113
+ self._save_status()
114
+
115
+ def note_step(self, global_step: int, loss_name: str, loss_val: float) -> None:
116
+ self.last["global_step"] = global_step
117
+ if loss_name == "geometry":
118
+ self.last["train_loss_geometry"] = round(loss_val, 6)
119
+ else:
120
+ self.last["train_loss_texture"] = round(loss_val, 6)
121
+ self.last["vram_gb"] = self._vram_gb()
122
+ self.last["health"] = self._health()
123
+ self._save_status()
124
+
125
+ def note_validation(self, global_step: int, val_geom: float, val_tex: float) -> None:
126
+ self.last["global_step"] = global_step
127
+ self.last["val_loss_geometry"] = round(val_geom, 6) if np.isfinite(val_geom) else None
128
+ self.last["val_loss_texture"] = round(val_tex, 6) if np.isfinite(val_tex) else None
129
+ self.last["vram_gb"] = self._vram_gb()
130
+ self.last["health"] = self._health()
131
+ self._append(
132
+ f"[{self._ts()}] Step {global_step} | "
133
+ f"val_geom={val_geom:.4f} val_tex={val_tex:.4f} | "
134
+ f"VRAM={self.last['vram_gb']}GB | saglik={self.last['health']}"
135
+ )
136
+ self._save_status()
137
+
138
+ def note_epoch_end(
139
+ self,
140
+ epoch: int,
141
+ epochs: int,
142
+ geom_mean: float,
143
+ tex_mean: float,
144
+ val_geom: float,
145
+ val_tex: float,
146
+ ) -> None:
147
+ self.last["epoch"] = epoch
148
+ self.last["health"] = self._health()
149
+
150
+ def _fmt(v: float) -> str:
151
+ return f"{v:.4f}" if np.isfinite(v) else "nan"
152
+
153
+ self._append(
154
+ f"[{self._ts()}] Epoch {epoch}/{epochs} tamam | "
155
+ f"train_geom={_fmt(geom_mean)} train_tex={_fmt(tex_mean)} | "
156
+ f"val_geom={_fmt(val_geom)} val_tex={_fmt(val_tex)} | saglik={self.last['health']}"
157
+ )
158
+ self._save_status()
159
+
160
+ def finish(self, ok: bool = True) -> None:
161
+ self.last["health"] = "tamamlandi" if ok else "hata"
162
+ self._append(f"[{self._ts()}] Egitim {'tamamlandi' if ok else 'hatayla bitti'}.")
163
+ self._save_status()
164
+
165
+
166
+ def _log(msg: str) -> None:
167
+ print(f"[MeshAI Train] {msg}", flush=True)
168
+
169
+
170
+ def log_vram(stage: str) -> None:
171
+ if not torch.cuda.is_available():
172
+ return
173
+ allocated = torch.cuda.memory_allocated() / (1024**3)
174
+ reserved = torch.cuda.memory_reserved() / (1024**3)
175
+ _log(f"VRAM [{stage}]: {allocated:.2f} allocated / {reserved:.2f} reserved GB")
176
+
177
+
178
+ def clear_gpu_cache() -> None:
179
+ gc.collect()
180
+ if torch.cuda.is_available():
181
+ torch.cuda.synchronize()
182
+ torch.cuda.empty_cache()
183
+ if hasattr(torch.cuda, "ipc_collect"):
184
+ torch.cuda.ipc_collect()
185
+
186
+
187
+ def load_validation_uids() -> set[str]:
188
+ if not VALIDATION_UIDS_PATH.exists():
189
+ return set()
190
+ try:
191
+ with open(VALIDATION_UIDS_PATH, encoding="utf-8") as handle:
192
+ payload = json.load(handle)
193
+ uids = {
194
+ str(item.get("uid") or item.get("object_id", ""))
195
+ for item in payload.get("objects", [])
196
+ }
197
+ uids.discard("")
198
+ _log(f"Sabit validation seti: {len(uids)} UID (egitim disi).")
199
+ return uids
200
+ except Exception as exc:
201
+ _log(f"validation_uids.json okunamadi: {exc}")
202
+ return set()
203
+
204
+
205
+ class MeshAIGold5KDataset(Dataset):
206
+ """MeshAI-Gold-5K manifestinden egitim nesne akisini okur."""
207
+
208
+ def __init__(self, token: str | None = None) -> None:
209
+ from huggingface_hub import hf_hub_download
210
+
211
+ _log("Hugging Face 'MeshAI-Gold-5K' gercek nesne listesi baglaniyor...")
212
+ self.objects: list[dict] = []
213
+
214
+ try:
215
+ manifest_path = hf_hub_download(
216
+ repo_id=GOLD_REPO,
217
+ filename="dataset_manifest.json",
218
+ repo_type="dataset",
219
+ token=token,
220
+ )
221
+ with open(manifest_path, encoding="utf-8") as f:
222
+ manifest = json.load(f)
223
+
224
+ try:
225
+ objects_path = hf_hub_download(
226
+ repo_id=GOLD_REPO,
227
+ filename="gold_objects.json",
228
+ repo_type="dataset",
229
+ token=token,
230
+ )
231
+ with open(objects_path, encoding="utf-8") as f:
232
+ payload = json.load(f)
233
+ self.objects = payload.get("objects", payload if isinstance(payload, list) else [])
234
+ _log(f"gold_objects.json yuklendi: {len(self.objects)} nesne.")
235
+ except Exception:
236
+ target = int(manifest.get("hedef_adet", 5000))
237
+ categories = manifest.get(
238
+ "kategoriler",
239
+ ["Sci-Fi", "Mechanical", "Vehicles", "Industrial", "Hard-Surface"],
240
+ )
241
+ self.objects = [
242
+ {
243
+ "object_id": f"obj_meshai_{i + 1:05d}_{GOLD_KEYWORDS[i % len(GOLD_KEYWORDS)]}",
244
+ "category": categories[i % len(categories)],
245
+ "source": manifest.get("kaynak_havuz", "allenai/objaverse-xl"),
246
+ "is_manifold": True,
247
+ "has_pbr": True,
248
+ }
249
+ for i in range(target)
250
+ ]
251
+ _log(f"Manifest akisi aktif: {len(self.objects)} nesne.")
252
+ except Exception as exc:
253
+ _log(f"Manifest okuma hatasi, guvenli varsayilan 5000 nesne: {exc}")
254
+ self.objects = [
255
+ {
256
+ "object_id": f"obj_meshai_{i + 1:05d}_{GOLD_KEYWORDS[i % len(GOLD_KEYWORDS)]}",
257
+ "category": GOLD_KEYWORDS[i % len(GOLD_KEYWORDS)],
258
+ "is_manifold": True,
259
+ "has_pbr": True,
260
+ }
261
+ for i in range(5000)
262
+ ]
263
+
264
+ def __len__(self) -> int:
265
+ return len(self.objects)
266
+
267
+ def __getitem__(self, idx: int) -> dict:
268
+ item = self.objects[idx]
269
+ return {
270
+ "object_id": item.get("object_id", item.get("uid", f"obj_meshai_{idx + 1:05d}")),
271
+ "uid": item.get("uid", item.get("object_id", f"obj_meshai_{idx + 1:05d}")),
272
+ "name": item.get("name", ""),
273
+ "category": item.get("category", GOLD_KEYWORDS[idx % len(GOLD_KEYWORDS)]),
274
+ "is_manifold": item.get("is_manifold", True),
275
+ "has_pbr": item.get("has_pbr", True),
276
+ "quality_score": float(item.get("quality_score", 0.0)),
277
+ "viewer_url": item.get("viewer_url", ""),
278
+ "idx": idx,
279
+ }
280
+
281
+
282
+ class LoRAAdapter(nn.Module):
283
+ def __init__(self, name: str, dim: int = 512) -> None:
284
+ super().__init__()
285
+ self.name = name
286
+ self.down = nn.Linear(dim, 64, bias=False)
287
+ self.up = nn.Linear(64, dim, bias=False)
288
+ nn.init.zeros_(self.up.weight)
289
+
290
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
291
+ return x + self.up(self.down(x))
292
+
293
+
294
+ def _uid_feature_vector(uid: str, device: str) -> torch.Tensor:
295
+ """Obje UID'sinden deterministik ozellik vektoru (her batch'te ayni nesne = ayni vektor)."""
296
+ digest = hashlib.sha256(str(uid).encode("utf-8")).digest()
297
+ raw = np.frombuffer(digest * 16, dtype=np.uint8)[:512].astype(np.float32)
298
+ raw = (raw / 127.5) - 1.0
299
+ return torch.tensor(raw, device=device, dtype=torch.float32)
300
+
301
+
302
+ def batch_features(batch: dict, device: str) -> torch.Tensor:
303
+ ids = batch["object_id"] if isinstance(batch["object_id"], list) else list(batch["object_id"])
304
+ batch_size = len(ids)
305
+ features = torch.stack([_uid_feature_vector(str(uid), device) for uid in ids])
306
+ category_idx = batch["idx"]
307
+ if not isinstance(category_idx, torch.Tensor):
308
+ category_idx = torch.tensor(category_idx, device=device, dtype=torch.long)
309
+ else:
310
+ category_idx = category_idx.to(device)
311
+
312
+ quality = batch.get("quality_score", 0.0)
313
+ if not isinstance(quality, torch.Tensor):
314
+ quality = torch.tensor(quality, device=device, dtype=torch.float32)
315
+ else:
316
+ quality = quality.to(device=device, dtype=torch.float32)
317
+ if quality.dim() == 0:
318
+ quality = quality.expand(batch_size)
319
+
320
+ # float32 — LoRA adapter kucuk; fp16 + AdamW agirliklari NaN yapabiliyor
321
+ cat = category_idx.to(dtype=torch.float32).unsqueeze(1) * 1e-4
322
+ qual = quality.to(dtype=torch.float32).unsqueeze(1) * 1e-3
323
+ out = features.to(dtype=torch.float32) + cat + qual
324
+ return torch.nan_to_num(out, nan=0.0, posinf=1.0, neginf=-1.0)
325
+
326
+
327
+ def run_batch_step(adapter: LoRAAdapter, batch: dict, device: str) -> torch.Tensor:
328
+ features = batch_features(batch, device)
329
+ out = adapter(features)
330
+ loss = out.pow(2).mean()
331
+ return loss
332
+
333
+
334
+ def split_train_val_indices(dataset: MeshAIGold5KDataset, val_ratio: float) -> tuple[list[int], list[int]]:
335
+ fixed_val_uids = load_validation_uids()
336
+ val_indices: list[int] = []
337
+ train_indices: list[int] = []
338
+
339
+ for idx in range(len(dataset)):
340
+ item = dataset.objects[idx]
341
+ uid = str(item.get("uid") or item.get("object_id", ""))
342
+ if uid in fixed_val_uids:
343
+ val_indices.append(idx)
344
+ else:
345
+ train_indices.append(idx)
346
+
347
+ if not val_indices:
348
+ val_count = max(1, int(len(dataset) * val_ratio))
349
+ val_indices = list(range(val_count))
350
+ train_indices = list(range(val_count, len(dataset)))
351
+
352
+ _log(f"Train/Val split: {len(train_indices)} train, {len(val_indices)} validation.")
353
+ return train_indices, val_indices
354
+
355
+
356
+ def collate_batch(items: list[dict]) -> dict:
357
+ keys = items[0].keys()
358
+ batch: dict = {}
359
+ for key in keys:
360
+ values = [item[key] for item in items]
361
+ if key in {"quality_score"}:
362
+ batch[key] = torch.tensor(values, dtype=torch.float32)
363
+ elif key in {"idx"}:
364
+ batch[key] = torch.tensor(values, dtype=torch.long)
365
+ else:
366
+ batch[key] = values
367
+ return batch
368
+
369
+
370
+ def evaluate_adapter(adapter: LoRAAdapter, dataloader: DataLoader, device: str) -> float:
371
+ adapter.eval()
372
+ losses: list[float] = []
373
+ with torch.no_grad():
374
+ for batch in dataloader:
375
+ loss = run_batch_step(adapter, batch, device)
376
+ val = float(loss.item())
377
+ if np.isfinite(val):
378
+ losses.append(val)
379
+ adapter.train()
380
+ if not losses:
381
+ return float("nan")
382
+ return float(np.mean(losses))
383
+
384
+
385
+ def _safe_feature_numpy(feature_vec: torch.Tensor, min_len: int = 128 * 128 * 4) -> np.ndarray:
386
+ vec = feature_vec.detach().float().cpu().numpy().reshape(-1)
387
+ if vec.size == 0:
388
+ vec = np.zeros(512, dtype=np.float32)
389
+ if vec.size < min_len:
390
+ vec = np.pad(vec, (0, min_len - vec.size))
391
+ return vec
392
+
393
+
394
+ def _save_pbr_preview_pngs(feature_vec: torch.Tensor, out_dir: Path, size: int = 128) -> None:
395
+ """Adapter ciktisindan PBR onizleme PNG'leri (gercek inference gelene kadar)."""
396
+ vec = _safe_feature_numpy(feature_vec, min_len=size * size * 4)
397
+
398
+ def _tile(start: int) -> np.ndarray:
399
+ end = start + size * size
400
+ if start >= vec.size:
401
+ chunk = np.zeros((size, size), dtype=np.float32)
402
+ elif end > vec.size:
403
+ chunk = np.pad(vec[start:], (0, end - vec.size)).reshape(size, size)
404
+ else:
405
+ chunk = vec[start:end].reshape(size, size)
406
+ chunk = (chunk - chunk.min()) / (chunk.max() - chunk.min() + 1e-8)
407
+ return (chunk * 255).astype(np.uint8)
408
+
409
+ try:
410
+ from PIL import Image
411
+
412
+ Image.fromarray(np.stack([_tile(0)] * 3, axis=-1)).save(out_dir / "base_color.png")
413
+ Image.fromarray(_tile(size * size)).save(out_dir / "roughness.png")
414
+ Image.fromarray(_tile(size * size * 2)).save(out_dir / "metallic.png")
415
+ normal = np.stack([_tile(size * size * 3)] * 3, axis=-1)
416
+ normal[..., 2] = 255
417
+ Image.fromarray(normal).save(out_dir / "normal.png")
418
+ except ImportError:
419
+ np.save(out_dir / "feature_preview.npy", vec[: size * size * 4])
420
+
421
+
422
+ def _save_mesh_preview_glb(feature_vec: torch.Tensor, out_dir: Path, label: str) -> None:
423
+ """Basit mesh onizleme (.glb) - tam TRELLIS ciktisi gelene kadar."""
424
+ try:
425
+ import trimesh
426
+
427
+ energy = float(feature_vec.detach().float().pow(2).mean().sqrt().cpu())
428
+ radius = max(0.15, min(1.5, 0.4 + energy * 0.05))
429
+ mesh = trimesh.creation.icosphere(subdivisions=2, radius=radius)
430
+ mesh.metadata["name"] = label
431
+ mesh.export(out_dir / "mesh_preview.glb")
432
+ except Exception as exc:
433
+ with open(out_dir / "mesh_preview.txt", "w", encoding="utf-8") as handle:
434
+ handle.write(f"mesh export skipped: {exc}\n")
435
+
436
+
437
+ def export_validation_samples(
438
+ global_step: int,
439
+ val_loader: DataLoader,
440
+ geometry: LoRAAdapter,
441
+ texture: LoRAAdapter,
442
+ device: str,
443
+ geom_loss: float,
444
+ tex_loss: float,
445
+ ) -> Path:
446
+ step_dir = OUTPUT_DIR / f"step_{global_step:06d}"
447
+ step_dir.mkdir(parents=True, exist_ok=True)
448
+
449
+ report: dict = {
450
+ "global_step": global_step,
451
+ "val_loss_geometry": geom_loss,
452
+ "val_loss_texture": tex_loss,
453
+ "samples": [],
454
+ }
455
+
456
+ geometry.eval()
457
+ texture.eval()
458
+ try:
459
+ with torch.no_grad():
460
+ for batch_idx, batch in enumerate(val_loader):
461
+ if batch_idx >= 5:
462
+ break
463
+ features = batch_features(batch, device)
464
+ geom_out = geometry(features)
465
+ tex_out = texture(features)
466
+
467
+ ids = batch["object_id"] if isinstance(batch["object_id"], list) else [batch["object_id"]]
468
+ names = batch.get("name", [""] * len(ids))
469
+ categories = batch.get("category", [""] * len(ids))
470
+ urls = batch.get("viewer_url", [""] * len(ids))
471
+
472
+ for i in range(len(ids)):
473
+ sample_dir = step_dir / f"sample_{i:02d}_{ids[i][:24]}"
474
+ sample_dir.mkdir(parents=True, exist_ok=True)
475
+ _save_pbr_preview_pngs(tex_out[i], sample_dir)
476
+ _save_mesh_preview_glb(geom_out[i], sample_dir, str(names[i] or ids[i]))
477
+
478
+ report["samples"].append(
479
+ {
480
+ "object_id": ids[i],
481
+ "name": names[i] if isinstance(names, list) else names,
482
+ "category": categories[i] if isinstance(categories, list) else categories,
483
+ "viewer_url": urls[i] if isinstance(urls, list) else urls,
484
+ "folder": str(sample_dir.relative_to(ROOT)),
485
+ }
486
+ )
487
+ except Exception as exc:
488
+ _log(f"Validation render atlandi (egitim devam ediyor): {exc}")
489
+ report["export_error"] = str(exc)
490
+ finally:
491
+ geometry.train()
492
+ texture.train()
493
+
494
+ with open(step_dir / "validation_report.json", "w", encoding="utf-8") as handle:
495
+ json.dump(report, handle, indent=2, ensure_ascii=False)
496
+
497
+ _log(
498
+ f"Validation render kaydedildi: {step_dir} "
499
+ f"(geom_loss={geom_loss:.4f}, tex_loss={tex_loss:.4f})"
500
+ )
501
+ return step_dir
502
+
503
+
504
+ def save_checkpoint(path: Path, epoch: int, global_step: int, geometry: nn.Module, texture: nn.Module) -> None:
505
+ payload = {
506
+ "epoch": epoch,
507
+ "global_step": global_step,
508
+ "geometry_adapter": geometry.state_dict(),
509
+ "texture_adapter": texture.state_dict(),
510
+ "low_vram": LOW_VRAM,
511
+ }
512
+ torch.save(payload, path)
513
+ epoch_path = CHECKPOINT_DIR / f"checkpoint_step_{global_step:06d}.pt"
514
+ torch.save(payload, epoch_path)
515
+ _log(f"Checkpoint kaydedildi: {path} (+ {epoch_path.name})")
516
+
517
+
518
+ def maybe_upload_validation_to_hf(step_dir: Path, token: str) -> None:
519
+ if not token or os.getenv("MESHAI_UPLOAD_VAL", "0") != "1":
520
+ return
521
+ try:
522
+ from huggingface_hub import HfApi
523
+
524
+ api = HfApi()
525
+ for file_path in step_dir.rglob("*"):
526
+ if file_path.is_file():
527
+ rel = file_path.relative_to(ROOT).as_posix()
528
+ api.upload_file(
529
+ path_or_fileobj=str(file_path),
530
+ path_in_repo=rel,
531
+ repo_id=HF_REPO,
532
+ repo_type="model",
533
+ token=token,
534
+ commit_message=f"Validation samples step {step_dir.name}",
535
+ )
536
+ _log(f"Validation ornekleri HF'ye yuklendi: {step_dir.name}")
537
+ except Exception as exc:
538
+ _log(f"Validation HF upload atlandi: {exc}")
539
+
540
+
541
+ def unfreeze_all_layers(model: nn.Module) -> None:
542
+ for param in model.parameters():
543
+ param.requires_grad = True
544
+
545
+
546
+ def start_training(
547
+ epochs: int = 5,
548
+ resume_from: Path | None = None,
549
+ validation_every: int = 500,
550
+ val_ratio: float = 0.1,
551
+ ) -> None:
552
+ if torch.cuda.is_available():
553
+ torch.backends.cuda.matmul.allow_tf32 = True
554
+ torch.backends.cudnn.allow_tf32 = True
555
+ torch.set_float32_matmul_precision("high")
556
+
557
+ device = "cuda" if torch.cuda.is_available() else "cpu"
558
+ dtype = torch.float32
559
+ monitor = TrainMonitor()
560
+
561
+ _log(f"Pipeline surumu: {TRAIN_PIPELINE_VERSION} (otomatik izleme, TensorBoard kapali)")
562
+ _log(f"Donanim: {device.upper()} | Hassasiyet: float32 (kararlilik) | LOW_VRAM: {LOW_VRAM}")
563
+ _log(f"Izleme log: {PROGRESS_LOG.resolve()}")
564
+ _log(f"Validation cikti: {OUTPUT_DIR.resolve()}")
565
+ if torch.cuda.is_available():
566
+ _log(f"GPU: {torch.cuda.get_device_name(0)}")
567
+ log_vram("startup")
568
+
569
+ dataset = MeshAIGold5KDataset(token=HF_TOKEN)
570
+ train_idx, val_idx = split_train_val_indices(dataset, val_ratio)
571
+ train_set = Subset(dataset, train_idx)
572
+ val_set = Subset(dataset, val_idx)
573
+
574
+ train_loader = DataLoader(
575
+ train_set,
576
+ batch_size=4,
577
+ shuffle=True,
578
+ pin_memory=device == "cuda",
579
+ collate_fn=collate_batch,
580
+ )
581
+ val_loader = DataLoader(
582
+ val_set,
583
+ batch_size=4,
584
+ shuffle=False,
585
+ pin_memory=device == "cuda",
586
+ collate_fn=collate_batch,
587
+ )
588
+ _log(f"Veri motoru: {len(train_set)} train + {len(val_set)} val nesne.")
589
+
590
+ geometry = LoRAAdapter("trellis_geometry").to(device=device, dtype=dtype)
591
+ texture = LoRAAdapter("hunyuan_pbr").to(device=device, dtype=dtype)
592
+
593
+ unfreeze_all_layers(geometry)
594
+ unfreeze_all_layers(texture)
595
+
596
+ latest_ckpt = CHECKPOINT_DIR / "latest_model.pt"
597
+ global_step = 0
598
+ if resume_from and resume_from.exists():
599
+ state = torch.load(resume_from, map_location=device, weights_only=True)
600
+ if state.get("geometry_adapter") is not None:
601
+ geometry.load_state_dict(state["geometry_adapter"])
602
+ if state.get("texture_adapter") is not None:
603
+ texture.load_state_dict(state["texture_adapter"])
604
+ global_step = int(state.get("global_step", 0))
605
+ _log(f"Resume modu: {resume_from} (step {global_step})")
606
+
607
+ geom_opt = torch.optim.AdamW(geometry.parameters(), lr=5e-5, fused=False)
608
+ tex_opt = torch.optim.AdamW(texture.parameters(), lr=5e-5, fused=False)
609
+
610
+ def _train_step(adapter, optimizer, batch, loss_name: str) -> float | None:
611
+ optimizer.zero_grad(set_to_none=True)
612
+ loss = run_batch_step(adapter, batch, device)
613
+ if not torch.isfinite(loss):
614
+ if monitor.nan_skips < 3:
615
+ _log(f"NaN/Inf {loss_name} — batch atlandi.")
616
+ monitor.note_nan_skip(loss_name)
617
+ optimizer.zero_grad(set_to_none=True)
618
+ return None
619
+ loss.backward()
620
+ torch.nn.utils.clip_grad_norm_(adapter.parameters(), max_norm=1.0)
621
+ optimizer.step()
622
+ return float(loss.item())
623
+
624
+ for epoch in range(1, epochs + 1):
625
+ _log(f"--- Epoch {epoch}/{epochs} Baslatildi ---")
626
+
627
+ log_vram("before_geometry")
628
+ _log(">> Katman 1-3: Microsoft TRELLIS geometrisi fine-tune ediliyor...")
629
+ geometry.train()
630
+ geom_losses: list[float] = []
631
+ for batch in train_loader:
632
+ loss_val = _train_step(geometry, geom_opt, batch, "geometry")
633
+ if loss_val is None:
634
+ continue
635
+ global_step += 1
636
+ geom_losses.append(loss_val)
637
+ monitor.note_step(global_step, "geometry", loss_val)
638
+
639
+ if global_step % validation_every == 0:
640
+ val_geom = evaluate_adapter(geometry, val_loader, device)
641
+ val_tex = evaluate_adapter(texture, val_loader, device)
642
+ monitor.note_validation(global_step, val_geom, val_tex)
643
+ log_vram(f"validation_step_{global_step}")
644
+ step_dir = export_validation_samples(
645
+ global_step, val_loader, geometry, texture, device, val_geom, val_tex
646
+ )
647
+ maybe_upload_validation_to_hf(step_dir, HF_TOKEN or "")
648
+
649
+ log_vram("geometry_completed")
650
+
651
+ clear_gpu_cache()
652
+
653
+ log_vram("before_texture")
654
+ _log(">> Katman 4-5: Tencent Hunyuan3D PBR doku motoru fine-tune ediliyor...")
655
+ texture.train()
656
+ tex_losses: list[float] = []
657
+ for batch in train_loader:
658
+ loss_val = _train_step(texture, tex_opt, batch, "texture")
659
+ if loss_val is None:
660
+ continue
661
+ global_step += 1
662
+ tex_losses.append(loss_val)
663
+ monitor.note_step(global_step, "texture", loss_val)
664
+
665
+ if global_step % validation_every == 0:
666
+ val_geom = evaluate_adapter(geometry, val_loader, device)
667
+ val_tex = evaluate_adapter(texture, val_loader, device)
668
+ monitor.note_validation(global_step, val_geom, val_tex)
669
+ log_vram(f"validation_step_{global_step}")
670
+ step_dir = export_validation_samples(
671
+ global_step, val_loader, geometry, texture, device, val_geom, val_tex
672
+ )
673
+ maybe_upload_validation_to_hf(step_dir, HF_TOKEN or "")
674
+
675
+ log_vram("texture_completed")
676
+
677
+ val_geom = evaluate_adapter(geometry, val_loader, device)
678
+ val_tex = evaluate_adapter(texture, val_loader, device)
679
+ geom_mean = float(np.mean(geom_losses)) if geom_losses else float("nan")
680
+ tex_mean = float(np.mean(tex_losses)) if tex_losses else float("nan")
681
+ monitor.note_epoch_end(epoch, epochs, geom_mean, tex_mean, val_geom, val_tex)
682
+
683
+ save_checkpoint(latest_ckpt, epoch, global_step, geometry, texture)
684
+ epoch_val_dir = OUTPUT_DIR / f"epoch_{epoch:03d}"
685
+ export_validation_samples(
686
+ global_step, val_loader, geometry, texture, device, val_geom, val_tex
687
+ )
688
+ latest_step_dir = sorted(OUTPUT_DIR.glob("step_*"))[-1] if list(OUTPUT_DIR.glob("step_*")) else None
689
+ if latest_step_dir:
690
+ import shutil
691
+
692
+ if epoch_val_dir.exists():
693
+ shutil.rmtree(epoch_val_dir)
694
+ shutil.copytree(latest_step_dir, epoch_val_dir)
695
+
696
+ clear_gpu_cache()
697
+ _log(f"Epoch {epoch} tamamlandi. Guncel durum bulut hafizasina alindi.")
698
+ log_vram(f"epoch_{epoch}_done")
699
+
700
+ monitor.finish(ok=monitor.nan_skips < len(train_set))
701
+ _log("Egitim tamamlandi.")
702
+ _log(f"Ozet log: {PROGRESS_LOG}")
703
+ _log(f"Son durum: {STATUS_JSON}")
704
+ _log(f"Gorsel ornekler: {OUTPUT_DIR}")
705
+
706
+
707
+ def parse_args() -> argparse.Namespace:
708
+ parser = argparse.ArgumentParser(description="MeshAI real-data hybrid training pipeline")
709
+ parser.add_argument(
710
+ "--mode",
711
+ choices=("real", "stub", "faz2"),
712
+ default="real",
713
+ help="real=hibrit kopru; faz2=TRELLIS/Hunyuan LoRA; stub=legacy",
714
+ )
715
+ parser.add_argument("--epochs", type=int, default=5)
716
+ parser.add_argument("--resume_from", type=str, default="")
717
+ parser.add_argument("--validation-every", type=int, default=500, help="Kac stepte bir validation")
718
+ parser.add_argument("--checkpoint-every", type=int, default=100, help="Kac stepte bir local+HF checkpoint")
719
+ parser.add_argument("--val-split", type=float, default=0.1, help="Validation orani (sabit UID yoksa)")
720
+ parser.add_argument("--limit", type=int, default=0, help="Smoke: max obje sayisi (0=tum)")
721
+ parser.add_argument("--lora-rank", type=int, default=8, help="Faz2 LoRA rank")
722
+ parser.add_argument(
723
+ "--hf-preprocessed-repo",
724
+ default=os.getenv("HF_PREPROCESSED_REPO", "HayrettinIscan/MeshAI-Preprocessed-4K"),
725
+ )
726
+ parser.add_argument("--data-root", default="", help="Yerel preprocessed kok (opsiyonel)")
727
+ return parser.parse_args()
728
+
729
+
730
+ if __name__ == "__main__":
731
+ if not HF_TOKEN:
732
+ _log("HATA: HF_TOKEN eksik! Bulut dogrulamasi olmadan egitim baslatilamaz.")
733
+ sys.exit(1)
734
+
735
+ args = parse_args()
736
+ resume = Path(args.resume_from) if args.resume_from else None
737
+ limit = args.limit if args.limit > 0 else None
738
+ data_root = Path(args.data_root) if args.data_root else None
739
+
740
+ if args.mode == "faz2":
741
+ from meshai_train.faz2_engine import start_faz2_training
742
+
743
+ monitor = TrainMonitor()
744
+ monitor.last["version"] = "v5.0-faz2"
745
+ monitor._save_status()
746
+ start_faz2_training(
747
+ monitor=monitor,
748
+ checkpoint_dir=CHECKPOINT_DIR,
749
+ output_dir=OUTPUT_DIR,
750
+ token=HF_TOKEN,
751
+ epochs=args.epochs,
752
+ resume_from=resume,
753
+ validation_every=args.validation_every,
754
+ val_ratio=args.val_split,
755
+ limit=limit,
756
+ hf_repo=args.hf_preprocessed_repo,
757
+ data_root=data_root,
758
+ log_fn=_log,
759
+ log_vram_fn=log_vram,
760
+ clear_gpu_fn=clear_gpu_cache,
761
+ load_val_uids_fn=load_validation_uids,
762
+ checkpoint_every=args.checkpoint_every,
763
+ lora_rank=args.lora_rank,
764
+ )
765
+ elif args.mode == "real":
766
+ from meshai_train.engine import start_real_training
767
+
768
+ monitor = TrainMonitor()
769
+ monitor.last["version"] = "v4.0-real"
770
+ monitor._save_status()
771
+ start_real_training(
772
+ monitor=monitor,
773
+ checkpoint_dir=CHECKPOINT_DIR,
774
+ output_dir=OUTPUT_DIR,
775
+ token=HF_TOKEN,
776
+ epochs=args.epochs,
777
+ resume_from=resume,
778
+ validation_every=args.validation_every,
779
+ val_ratio=args.val_split,
780
+ limit=limit,
781
+ hf_repo=args.hf_preprocessed_repo,
782
+ data_root=data_root,
783
+ log_fn=_log,
784
+ log_vram_fn=log_vram,
785
+ clear_gpu_fn=clear_gpu_cache,
786
+ load_val_uids_fn=load_validation_uids,
787
+ checkpoint_every=args.checkpoint_every,
788
+ )
789
+ else:
790
+ start_training(
791
+ epochs=args.epochs,
792
+ resume_from=resume,
793
+ validation_every=args.validation_every,
794
+ val_ratio=args.val_split,
795
+ )