mohansshf commited on
Commit
f9d416e
·
verified ·
1 Parent(s): d36ba74

Upload portable training script

Browse files
Files changed (1) hide show
  1. train_portable.py +434 -0
train_portable.py ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Portable DPSS Training Script.
4
+
5
+ Runs on ANY GPU machine: local workstation, Colab, RunPod, Lambda, Vast.ai, etc.
6
+
7
+ Workflow:
8
+ 1. Downloads the ArchCAD-40K dataset from HuggingFace Hub
9
+ 2. Downloads upstream DPSS code bundle from mohansshf/dpss-archcad
10
+ 3. Downloads HRNet-W48 pretrained backbone weights
11
+ 4. Trains DPSS model (PointTransformerV2 + HRNet48 + Mask2Former decoder)
12
+ 5. Pushes best checkpoint to mohansshf/dpss-archcad (or saves locally)
13
+
14
+ Prerequisites:
15
+ pip install torch torchvision numpy scipy Pillow munch pyyaml \\
16
+ tensorboardX tensorboard tqdm huggingface_hub
17
+ pip install 'git+https://github.com/facebookresearch/detectron2.git'
18
+ # Optional (speeds up pointops ~3x on GPU):
19
+ pip install 'git+https://github.com/facebookresearch/pytorch3d.git@stable'
20
+
21
+ Quick start:
22
+ python train_portable.py # Full training, batch_size=8
23
+ python train_portable.py --batch_size 4 # Smaller batch for 24GB GPUs
24
+ python train_portable.py --batch_size 2 --fp16 # For 16GB GPUs (T4/4090)
25
+ python train_portable.py --epochs 5 --no_push # Quick test run
26
+ """
27
+
28
+ import argparse
29
+ import json as json_mod
30
+ import os
31
+ import re
32
+ import signal
33
+ import subprocess
34
+ import sys
35
+ import time
36
+
37
+
38
+ def parse_args():
39
+ parser = argparse.ArgumentParser(description="Train DPSS model")
40
+ parser.add_argument("--batch_size", type=int, default=8,
41
+ help="Per-GPU batch size (8 for 80GB, 4 for 24GB, 2 for 16GB)")
42
+ parser.add_argument("--epochs", type=int, default=100)
43
+ parser.add_argument("--patience", type=int, default=10,
44
+ help="Early stopping patience (epochs without sPQ improvement)")
45
+ parser.add_argument("--fp16", action="store_true",
46
+ help="Mixed precision training (helps on smaller GPUs)")
47
+ parser.add_argument("--lr", type=float, default=None,
48
+ help="Learning rate (default: auto-scaled from config)")
49
+ parser.add_argument("--work_dir", type=str, default="/tmp/dpss_checkpoints",
50
+ help="Directory for checkpoints")
51
+ parser.add_argument("--data_dir", type=str, default="/tmp/archcad_400k",
52
+ help="Dataset directory (auto-downloaded if missing)")
53
+ parser.add_argument("--code_dir", type=str, default="/tmp/upstream",
54
+ help="Upstream code directory (auto-downloaded if missing)")
55
+ parser.add_argument("--hub_repo", type=str, default="mohansshf/dpss-archcad",
56
+ help="HF Hub repo to push weights to")
57
+ parser.add_argument("--no_push", action="store_true",
58
+ help="Don't push to HF Hub (save locally only)")
59
+ parser.add_argument("--resume", type=str, default=None,
60
+ help="Resume from a specific checkpoint path")
61
+ parser.add_argument("--install_deps", action="store_true",
62
+ help="Install detectron2 and pytorch3d from source before training")
63
+ return parser.parse_args()
64
+
65
+
66
+ def install_build_deps():
67
+ """Install detectron2 + pytorch3d from source."""
68
+ print("=" * 60)
69
+ print("INSTALLING BUILD DEPENDENCIES")
70
+ print("=" * 60)
71
+
72
+ print("\n>>> Installing detectron2 from source...")
73
+ subprocess.run(
74
+ [sys.executable, "-m", "pip", "install",
75
+ "git+https://github.com/facebookresearch/detectron2.git"],
76
+ check=True,
77
+ )
78
+
79
+ print("\n>>> Installing pytorch3d from source (may take 5-10 min)...")
80
+ try:
81
+ subprocess.run(
82
+ [sys.executable, "-m", "pip", "install",
83
+ "git+https://github.com/facebookresearch/pytorch3d.git@stable"],
84
+ check=True, timeout=900,
85
+ )
86
+ print(" pytorch3d installed (CUDA-accelerated pointops)")
87
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
88
+ print(" WARNING: pytorch3d install failed — using pure-PyTorch pointops (slower but correct)")
89
+
90
+ print()
91
+
92
+
93
+ def setup_data(data_dir):
94
+ """Download and extract ArchCAD dataset if not already present."""
95
+ json_dir = os.path.join(data_dir, "json")
96
+ png_dir = os.path.join(data_dir, "png")
97
+
98
+ if os.path.isdir(json_dir) and os.path.isdir(png_dir):
99
+ n_json = len([f for f in os.listdir(json_dir) if f.endswith(".json")])
100
+ n_png = len([f for f in os.listdir(png_dir) if f.endswith(".png")])
101
+ if n_json > 0 and n_png > 0:
102
+ print(f"Dataset already present: {n_json} json, {n_png} png")
103
+ return
104
+
105
+ from huggingface_hub import hf_hub_download
106
+ import zipfile
107
+
108
+ os.makedirs(data_dir, exist_ok=True)
109
+
110
+ for zipname in ["json.zip", "png.zip"]:
111
+ print(f">>> Downloading {zipname} from jackluoluo/ArchCAD...")
112
+ zip_path = hf_hub_download(
113
+ repo_id="jackluoluo/ArchCAD",
114
+ filename=f"data/{zipname}",
115
+ repo_type="dataset",
116
+ )
117
+ print(f" Extracting -> {data_dir}")
118
+ with zipfile.ZipFile(zip_path, "r") as zf:
119
+ zf.extractall(data_dir)
120
+
121
+ n_json = len([f for f in os.listdir(json_dir) if f.endswith(".json")])
122
+ n_png = len([f for f in os.listdir(png_dir) if f.endswith(".png")])
123
+ print(f" Dataset ready: {n_json} json, {n_png} png")
124
+ assert n_json > 0 and n_png > 0, "Dataset download/extraction failed!"
125
+
126
+
127
+ def setup_code(code_dir):
128
+ """Download and extract upstream DPSS code if not already present."""
129
+ train_py = os.path.join(code_dir, "tools", "train.py")
130
+ if os.path.exists(train_py):
131
+ print(f"Upstream code already present at {code_dir}")
132
+ return
133
+
134
+ from huggingface_hub import hf_hub_download
135
+ import tarfile
136
+
137
+ os.makedirs(code_dir, exist_ok=True)
138
+
139
+ print(">>> Downloading upstream DPSS code from mohansshf/dpss-archcad...")
140
+ code_tar = hf_hub_download(
141
+ repo_id="mohansshf/dpss-archcad",
142
+ filename="upstream_code.tar.gz",
143
+ repo_type="model",
144
+ )
145
+ print(f" Extracting -> {code_dir}")
146
+ with tarfile.open(code_tar, "r:gz") as tar:
147
+ tar.extractall(code_dir)
148
+
149
+ assert os.path.exists(train_py), f"Code extraction failed — {train_py} not found!"
150
+ print(" Code ready")
151
+
152
+
153
+ def setup_pretrained(code_dir):
154
+ """Download HRNet-W48 pretrained weights."""
155
+ pretrained_dir = os.path.join(code_dir, "pretrained")
156
+ os.makedirs(pretrained_dir, exist_ok=True)
157
+ hrnet_path = os.path.join(pretrained_dir, "hrnet_ocr_cocostuff_3965_torch04.pth")
158
+
159
+ if os.path.exists(hrnet_path) and os.path.getsize(hrnet_path) > 1e6:
160
+ print(f"HRNet weights already present: {os.path.getsize(hrnet_path) / 1e6:.0f} MB")
161
+ return hrnet_path
162
+
163
+ hrnet_url = "https://github.com/hsfzxjy/models.storage/releases/download/HRNet-OCR/hrnet_ocr_cocostuff_3965_torch04.pth"
164
+ print(">>> Downloading HRNet-W48 pretrained weights...")
165
+ subprocess.run(["wget", "-q", "--show-progress", "-O", hrnet_path, hrnet_url], check=True)
166
+ print(f" Downloaded: {os.path.getsize(hrnet_path) / 1e6:.0f} MB")
167
+ return hrnet_path
168
+
169
+
170
+ def generate_config(data_dir, code_dir, hrnet_path, batch_size, epochs, fp16, lr=None):
171
+ """Generate runtime YAML config from upstream template."""
172
+ import yaml
173
+
174
+ config_path = os.path.join(code_dir, "configs", "svg", "svg_pointT.yaml")
175
+ with open(config_path) as f:
176
+ cfg = yaml.safe_load(f)
177
+
178
+ # Data paths
179
+ cfg["data"]["train"]["data_root"] = data_dir
180
+ cfg["data"]["test"]["data_root"] = data_dir
181
+
182
+ # Hyperparams
183
+ cfg["dataloader"]["train"]["batch_size"] = batch_size
184
+ cfg["dataloader"]["test"]["batch_size"] = max(batch_size, 4) # eval needs >=4 for BatchNorm
185
+ cfg["epochs"] = epochs
186
+ cfg["fp16"] = fp16
187
+
188
+ # Learning rate
189
+ if lr is not None:
190
+ cfg["optimizer"]["lr"] = lr
191
+
192
+ # Pretrained weight paths (absolute)
193
+ cfg["model"]["vision"]["pretrained"] = hrnet_path
194
+ cfg["model"]["vision"]["yaml"] = os.path.join(code_dir, "configs", "svg", "hrnet48.yaml")
195
+
196
+ # Save
197
+ runtime_path = os.path.join(code_dir, "runtime_config.yaml")
198
+ with open(runtime_path, "w") as f:
199
+ yaml.dump(cfg, f, default_flow_style=False)
200
+
201
+ print(f"\nConfig: batch_size={batch_size}, epochs={epochs}, fp16={fp16}")
202
+ base_lr = lr or cfg["optimizer"]["lr"]
203
+ print(f" Base LR: {base_lr} (will be auto-scaled by train.py for effective batch size)")
204
+ return runtime_path
205
+
206
+
207
+ def train(code_dir, config_path, work_dir, epochs, patience):
208
+ """Launch training and monitor for early stopping + push checkpoints."""
209
+ import torch
210
+
211
+ print("\n" + "=" * 60)
212
+ print("DPSS TRAINING")
213
+ print("=" * 60)
214
+ if torch.cuda.is_available():
215
+ print(f"GPU: {torch.cuda.get_device_name(0)}")
216
+ print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
217
+ else:
218
+ print("WARNING: No CUDA GPU detected! Training will be extremely slow.")
219
+ print(f"Epochs: {epochs}")
220
+ print(f"Patience: {patience}")
221
+ print(f"Work dir: {work_dir}")
222
+ print()
223
+
224
+ os.makedirs(work_dir, exist_ok=True)
225
+
226
+ # Resume detection
227
+ resume_arg = []
228
+ latest_pth = os.path.join(work_dir, "latest.pth")
229
+ if os.path.exists(latest_pth):
230
+ print(f"Resuming from: latest.pth")
231
+ resume_arg = ["--resume", latest_pth]
232
+ else:
233
+ existing = sorted(
234
+ [f for f in os.listdir(work_dir) if f.startswith("epoch_") and f.endswith(".pth")],
235
+ key=lambda x: int(x.split("_")[1].split(".")[0])
236
+ ) if os.path.isdir(work_dir) else []
237
+ if existing:
238
+ latest = os.path.join(work_dir, existing[-1])
239
+ print(f"Resuming from: {latest}")
240
+ resume_arg = ["--resume", latest]
241
+ else:
242
+ print("Starting fresh training")
243
+
244
+ # Launch training subprocess
245
+ os.chdir(code_dir)
246
+ env = os.environ.copy()
247
+ env["PYTHONPATH"] = code_dir
248
+ env["PYTHONUNBUFFERED"] = "1"
249
+
250
+ cmd = [
251
+ sys.executable, "-m", "torch.distributed.run",
252
+ "--nproc_per_node=1",
253
+ "--master_port=29500",
254
+ "tools/train.py",
255
+ config_path,
256
+ "--dist",
257
+ "--work_dir", work_dir,
258
+ ] + resume_arg
259
+
260
+ print(f"CMD: {' '.join(cmd)}")
261
+ print("=" * 60 + "\n")
262
+
263
+ process = subprocess.Popen(
264
+ cmd, env=env,
265
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
266
+ text=True, bufsize=1,
267
+ start_new_session=True,
268
+ )
269
+
270
+ # Regex for log parsing
271
+ spq_pattern = re.compile(r"INFO\s+-\s+PQ / RQ / SQ\s*:\s*([\d.]+)")
272
+ loss_pattern = re.compile(r"\bloss:\s*([-+\d.eEinfaN]+)")
273
+
274
+ best_spq = 0.0
275
+ epochs_no_improve = 0
276
+ spq_history = []
277
+ nan_count = 0
278
+
279
+ # Restore early stopping state
280
+ es_file = os.path.join(work_dir, "early_stopping.json")
281
+ if os.path.exists(es_file):
282
+ with open(es_file) as f:
283
+ es = json_mod.load(f)
284
+ best_spq = es.get("best_spq", 0)
285
+ epochs_no_improve = es.get("epochs_without_improvement", 0)
286
+ spq_history = es.get("history", [])
287
+
288
+ def kill_proc():
289
+ try:
290
+ os.killpg(os.getpgid(process.pid), signal.SIGTERM)
291
+ except Exception:
292
+ try: process.terminate()
293
+ except: pass
294
+
295
+ for line in process.stdout:
296
+ print(line, end="", flush=True)
297
+
298
+ # NaN detection
299
+ m = loss_pattern.search(line)
300
+ if m:
301
+ try:
302
+ v = float(m.group(1))
303
+ if v != v or abs(v) == float('inf'):
304
+ nan_count += 1
305
+ if nan_count >= 50:
306
+ print(f"\n>>> ABORT: {nan_count} NaN/Inf losses\n")
307
+ kill_proc()
308
+ break
309
+ except: pass
310
+
311
+ # Early stopping
312
+ m = spq_pattern.search(line)
313
+ if m:
314
+ val = float(m.group(1))
315
+ spq_history.append(val)
316
+ if val > best_spq:
317
+ best_spq = val
318
+ epochs_no_improve = 0
319
+ print(f"\n>>> New best sPQ = {val:.3f}\n")
320
+ else:
321
+ epochs_no_improve += 1
322
+ print(f"\n>>> sPQ={val:.3f} <= best={best_spq:.3f} ({epochs_no_improve}/{patience})\n")
323
+
324
+ with open(es_file, "w") as f:
325
+ json_mod.dump({"best_spq": best_spq, "epochs_without_improvement": epochs_no_improve, "history": spq_history}, f)
326
+
327
+ if epochs_no_improve >= patience:
328
+ print(f"\n{'='*60}\nEARLY STOPPING after {patience} epochs without improvement\nBest sPQ: {best_spq:.3f}\n{'='*60}\n")
329
+ kill_proc()
330
+ try: process.wait(timeout=30)
331
+ except: pass
332
+ break
333
+
334
+ process.wait()
335
+ print(f"\nTraining finished. Best sPQ: {best_spq:.3f}")
336
+ print(f"History: {[f'{s:.3f}' for s in spq_history]}")
337
+ return best_spq
338
+
339
+
340
+ def export_and_push(work_dir, hub_repo, no_push=False):
341
+ """Export clean weights and optionally push to HF Hub."""
342
+ import torch
343
+
344
+ best_path = os.path.join(work_dir, "best.pth")
345
+ if not os.path.exists(best_path):
346
+ ckpts = sorted(
347
+ [f for f in os.listdir(work_dir) if f.startswith("epoch_") and f.endswith(".pth")],
348
+ key=lambda x: int(x.split("_")[1].split(".")[0])
349
+ )
350
+ if ckpts:
351
+ best_path = os.path.join(work_dir, ckpts[-1])
352
+ else:
353
+ print("ERROR: No checkpoints found!")
354
+ return None
355
+
356
+ print(f"\n>>> Exporting weights from {best_path}")
357
+ ckpt = torch.load(best_path, map_location="cpu")
358
+ weights = ckpt.get("net", ckpt.get("state_dict", ckpt.get("model", ckpt)))
359
+ epoch = ckpt.get("epoch", "unknown")
360
+
361
+ export_path = os.path.join(work_dir, "dpss_weights.pth")
362
+ torch.save({
363
+ "state_dict": weights,
364
+ "meta": {"source": "dpss-training", "dataset": "ArchCAD-40K", "epoch": epoch},
365
+ }, export_path)
366
+
367
+ n_params = sum(p.numel() for p in weights.values()) / 1e6
368
+ size_mb = os.path.getsize(export_path) / 1e6
369
+ print(f" {n_params:.1f}M params, {size_mb:.1f} MB, epoch {epoch}")
370
+ print(f" Saved to: {export_path}")
371
+
372
+ if not no_push:
373
+ try:
374
+ from huggingface_hub import HfApi
375
+ api = HfApi()
376
+ print(f"\n>>> Pushing to {hub_repo}...")
377
+ api.upload_file(path_or_fileobj=export_path, path_in_repo="dpss_weights.pth",
378
+ repo_id=hub_repo, commit_message=f"DPSS weights (epoch {epoch})")
379
+ api.upload_file(path_or_fileobj=best_path, path_in_repo="best_full.pth",
380
+ repo_id=hub_repo, commit_message=f"Full checkpoint (epoch {epoch})")
381
+ es_file = os.path.join(work_dir, "early_stopping.json")
382
+ if os.path.exists(es_file):
383
+ api.upload_file(path_or_fileobj=es_file, path_in_repo="training_metrics.json",
384
+ repo_id=hub_repo, commit_message="Training metrics")
385
+ print(f" Pushed to https://huggingface.co/{hub_repo}")
386
+ except Exception as e:
387
+ print(f" WARNING: Push failed ({e}). Weights saved locally at {export_path}")
388
+
389
+ return export_path
390
+
391
+
392
+ def main():
393
+ args = parse_args()
394
+ t0 = time.time()
395
+
396
+ print("=" * 60)
397
+ print("DPSS PORTABLE TRAINING")
398
+ print("=" * 60)
399
+
400
+ # Optional: install build deps
401
+ if args.install_deps:
402
+ install_build_deps()
403
+
404
+ # Setup
405
+ print("\n>>> Setting up data, code, and pretrained weights...\n")
406
+ setup_data(args.data_dir)
407
+ setup_code(args.code_dir)
408
+ hrnet_path = setup_pretrained(args.code_dir)
409
+
410
+ # Config
411
+ config_path = generate_config(
412
+ args.data_dir, args.code_dir, hrnet_path,
413
+ args.batch_size, args.epochs, args.fp16, args.lr,
414
+ )
415
+
416
+ # Train
417
+ best_spq = train(args.code_dir, config_path, args.work_dir, args.epochs, args.patience)
418
+
419
+ # Export
420
+ export_path = export_and_push(args.work_dir, args.hub_repo, args.no_push)
421
+
422
+ elapsed = time.time() - t0
423
+ print(f"\n{'='*60}")
424
+ print(f"COMPLETE — {elapsed/3600:.1f}h elapsed")
425
+ print(f"Best sPQ: {best_spq:.3f}")
426
+ if export_path:
427
+ print(f"Weights: {export_path}")
428
+ if not args.no_push:
429
+ print(f"Hub: https://huggingface.co/{args.hub_repo}")
430
+ print(f"{'='*60}")
431
+
432
+
433
+ if __name__ == "__main__":
434
+ main()