depth_fm.training.lightning_module¶
lightning_module ¶
PyTorch Lightning module for Mars DepthFM.
[REFACTOR NOTE] 1982 LOC. Candidate for future split into:
lightning_module.py (train/val/test steps), training_metrics.py
(aggregation), and visualization_worker.py (multiprocessing figure
rendering). Out of scope for the structural refactor that produced this layout.
Implements the DepthFM flow matching training loop with: - Proper train/val/test step separation - Per-epoch metric logging with all standard depth metrics - Flow evolution visualisation at configurable intervals - Normals loss warmup scheduling
DDP-safe: all sync_dist logging is unconditional, all model inference in visualization runs on all ranks (only rank 0 logs figures).
──────────────────────────────────────────────────────────────────────── Visualization throughput design (v2 — multiprocessing + round-robin) ────────────────────────────────────────────────────────────────────────
WHY THE ORIGINAL WAS SLOW
──────────────────────────
The original code used a concurrent.futures.ThreadPoolExecutor on rank 0
for background figure generation. Three problems:
-
GIL contention. matplotlib (Agg backend) is pure-Python / Cython and holds the GIL during every rasterisation call. The training DataLoader pre-fetch, metric aggregation, and gradient logging all compete for the same GIL on rank 0.
-
Rank asymmetry → DDP stalls. Only rank 0 generated figures, so it fell behind. Every subsequent DDP collective (all-reduce during backward, sync_dist logging, barrier at checkpointing) forced the other GPUs to idle until rank 0 caught up.
-
Blocking checkpoint flush.
on_save_checkpointcalled_flush_vis_tasks(timeout=120s), stalling the entire DDP group because the other ranks had already passed the save barrier and were waiting at the next collective.
HOW THIS VERSION FIXES IT
─────────────────────────
A. torch.multiprocessing worker (one per DDP rank).
A persistent daemon mp.Process (started with the spawn context
so it never inherits CUDA state — PyTorch docs mandate this) runs a
tight matplotlib→PDF/PNG loop. Because it is a process, not a
thread, it owns its own GIL and never blocks the training process.
B. Round-robin figure distribution.
Instead of funneling all 9+ figures through rank 0, we broadcast
the tiny vis snapshot (~1-2 MB of numpy arrays) from rank 0 to all
ranks via torch.distributed.broadcast, then assign figures to
ranks with task_idx % world_size. Each rank's worker renders
only its share of figures in parallel, cutting wall-clock time
by ≈ world_size×. Single-GPU training degrades gracefully (all
tasks stay on rank 0).
C. Deferred wandb upload.
All ranks write PNGs + PDFs to a shared staging directory. At the
start of the next validation epoch, rank 0 sweeps the staging
directory and does a single batched wandb.log() call. This
guarantees that uploads never block the training loop — by the time
we look, the previous epoch's figures are certainly on disk.
D. Non-blocking checkpoint.
on_save_checkpoint no longer flushes pending vis tasks. Worst
case, a crash loses some PDFs from the current epoch — acceptable
because the model weights are safe.
E. mp.SimpleQueue instead of mp.Queue.
Per PyTorch multiprocessing docs, SimpleQueue spawns no background
threads and avoids the deadlock-prone serialisation threads that
mp.Queue uses internally.
PERFORMANCE IMPACT (expected) ───────────────────────────── • GPU 0 idle time during validation: eliminated (no longer generates figures synchronously or via GIL-holding threads). • Total figure generation wall time: ÷ world_size (round-robin). • wandb upload latency: moved entirely out of the training critical path (deferred to next epoch start). • Checkpoint save time: reduced by up to 120 s (no vis flush).
DepthFMLightningModule ¶
Bases: LightningModule
Lightning module for DepthFM flow matching training.
Handles: - Conditional flow matching velocity loss - Surface normals auxiliary loss with warmup - Logit-normal timestep sampling - Noise augmentation on source distribution - Multi-step Euler inference for validation - Full metric computation and logging
Source code in src/depth_fm/training/lightning_module.py
estimate_uncertainty ¶
estimate_uncertainty(z_img: Tensor, num_samples: int = 10, num_steps: int = 4) -> tuple[torch.Tensor, torch.Tensor]
Computes the epistemic uncertainty via stochastic ODE sampling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z_img
|
Tensor
|
(B, C, H, W) Encoded input orthoimage. |
required |
num_samples
|
int
|
Number of stochastic noise initializations (N). |
10
|
num_steps
|
int
|
Euler integration steps per sample. |
4
|
Returns:
| Name | Type | Description |
|---|---|---|
mean_dtm |
Tensor
|
(B, 1, H, W) The expected topographic surface. |
var_dtm |
Tensor
|
(B, 1, H, W) The pixel-wise epistemic variance. |
Source code in src/depth_fm/training/lightning_module.py
on_train_end ¶
Final wandb upload and vis worker shutdown.