| """Training driver for WiSER CIR set prediction. |
| |
| `CsiSetTrainer.step(batch, gt)` wires together the cached scene-encoder, |
| the TX-modulator, the DETR head, the Hungarian matcher, and the loss bundle. |
| |
| AC-5 invariant: the cache wrapper calls `model.scene_encode` at most `U` times |
| and `model.modulate_scene_tx` at most `P` times. Both invariants are tested |
| with U < P in `tests/test_cache_invariants.py`. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import time as _time |
| from dataclasses import dataclass |
|
|
| import torch |
|
|
| from ..config import LossConfig as SharedLossConfig, ModelConfig as SharedModelConfig |
| from ..models.model import ModelConfig, SparseCsiDetrModel |
| from .cached_forward import CachedSceneTxForward |
| from .losses import LossWeights, csi_set_loss |
| from .matching import MatcherConfig, hungarian_match_batch |
|
|
|
|
| def _sync_now(is_cuda: bool) -> float: |
| """Return `time.time()` after a CUDA sync when `is_cuda` is True. |
| |
| Consolidates the repeated `if _is_cuda: torch.cuda.synchronize(); t = time.time()` |
| phase-timer boilerplate without changing timing semantics. |
| """ |
| if is_cuda: |
| torch.cuda.synchronize() |
| return _time.time() |
|
|
|
|
| @dataclass |
| class TrainerConfig: |
| model: ModelConfig = None |
| loss_weights: LossWeights = None |
| matcher: MatcherConfig = None |
|
|
| @classmethod |
| def from_shared(cls, shared_model: SharedModelConfig, shared_loss: SharedLossConfig) -> "TrainerConfig": |
| return cls( |
| model=ModelConfig.from_shared(shared_model), |
| loss_weights=LossWeights.from_shared(shared_loss), |
| matcher=MatcherConfig(), |
| ) |
|
|
|
|
| class CsiSetTrainer: |
| """Minimal trainer exposing `step(batch, gt)` with split cache invariants.""" |
|
|
| def __init__(self, model: SparseCsiDetrModel, config: TrainerConfig | None = None) -> None: |
| self.model = model |
| self.cfg = config or TrainerConfig() |
| if self.cfg.model is None: |
| self.cfg.model = model.config |
| if self.cfg.loss_weights is None: |
| self.cfg.loss_weights = LossWeights() |
| if self.cfg.matcher is None: |
| self.cfg.matcher = MatcherConfig() |
| self.cached_forward = CachedSceneTxForward() |
|
|
| def _scene_encoder(self, voxel_level): |
| """Called once per unique scene: runs the REAL backbone.""" |
| return self.model.scene_encode(voxel_level) |
|
|
| def _tx_modulator(self, scene_tokens, tx_coord): |
| """Called once per unique (scene, TX): cheap per-TX modulation.""" |
| tx_emb = self.model.encode_tx(tx_coord.unsqueeze(0)) |
| out = self.model.modulate_scene_tx(scene_tokens, tx_emb) |
| return out["feats"] |
|
|
| def _autocast_ctx(self): |
| """bf16 autocast when on CUDA with the trellis2 backbone (FlashAttention |
| requires fp16/bf16). Everything else runs fp32.""" |
| use_trellis = getattr(self.model.config.backbone, "kind", "") == "trellis2" |
| device_type = "cuda" if torch.cuda.is_available() else "cpu" |
| if use_trellis and device_type == "cuda": |
| return torch.autocast(device_type="cuda", dtype=torch.bfloat16) |
| |
| from contextlib import nullcontext |
| return nullcontext() |
|
|
| def step(self, batch: dict, gt: dict) -> dict: |
| _is_cuda = torch.cuda.is_available() |
| with self._autocast_ctx(): |
| tx_mem = self.cached_forward.forward_batch( |
| batch, |
| scene_encoder=self._scene_encoder, |
| tx_modulator=self._tx_modulator, |
| ) |
| if tx_mem.dim() == 4: |
| tx_mem = tx_mem.squeeze(1) |
| rx_xyz_norm = batch["rx_xyz_norm"] |
| |
| |
| _t_dec = _sync_now(_is_cuda) |
| decoded = self.model.head.decode(tx_mem, rx_xyz_norm) |
| _t_proj = _sync_now(_is_cuda) |
| decoder_s = _t_proj - _t_dec |
| predictions = self.model.head.project(decoded) |
| head_s = _sync_now(_is_cuda) - _t_proj |
|
|
| |
| |
| |
| _t_match = _sync_now(_is_cuda) |
| matchings = hungarian_match_batch(predictions, gt, self.cfg.matcher) |
| _t_loss = _sync_now(_is_cuda) |
| match_s = _t_loss - _t_match |
|
|
| loss_bundle = csi_set_loss(predictions, gt, matchings, weights=self.cfg.loss_weights) |
| loss_s = _sync_now(_is_cuda) - _t_loss |
|
|
| return { |
| "predictions": predictions, |
| "matchings": matchings, |
| "loss_bundle": loss_bundle, |
| "cache_counters": { |
| "c_enc": self.cached_forward.counters.c_enc, |
| "c_txmod": self.cached_forward.counters.c_txmod, |
| "u": self.cached_forward.counters.u, |
| "p": self.cached_forward.counters.p, |
| "q": self.cached_forward.counters.q, |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| "phase_times_s": { |
| "scene_encode": float(self.cached_forward.counters.scene_encode_s), |
| "tx_modulate": float(self.cached_forward.counters.tx_modulate_s), |
| "decoder": float(decoder_s), |
| "head": float(head_s), |
| "matching": float(match_s), |
| "loss_construction": float(loss_s), |
| }, |
| } |
|
|
|
|
| __all__ = ["TrainerConfig", "CsiSetTrainer"] |
|
|