| """Strict, auditable L2CS checkpoint loading. |
| |
| This module intentionally does not replace the historical loader in ``teacher.py``. |
| It is the loader for new, versioned KD artifacts only. |
| """ |
| from __future__ import annotations |
|
|
| from dataclasses import asdict, dataclass |
| import hashlib |
| from pathlib import Path |
| from typing import Mapping |
|
|
| import torch |
|
|
| from src.models.teacher import L2CS |
|
|
|
|
| PREFIX_MAP = { |
| "conv1.": "backbone.0.", |
| "bn1.": "backbone.1.", |
| "layer1.": "backbone.4.", |
| "layer2.": "backbone.5.", |
| "layer3.": "backbone.6.", |
| "layer4.": "backbone.7.", |
| } |
|
|
|
|
| @dataclass(frozen=True) |
| class StrictLoadAudit: |
| checkpoint_path: str |
| checkpoint_sha256: str |
| checkpoint_tensor_count: int |
| mapped_tensor_count: int |
| model_tensor_count: int |
| allowed_model_only_keys: tuple[str, ...] |
| inference_sha256: str | None = None |
|
|
|
|
| def file_sha256(path: str | Path) -> str: |
| digest = hashlib.sha256() |
| with Path(path).open("rb") as stream: |
| for block in iter(lambda: stream.read(1024 * 1024), b""): |
| digest.update(block) |
| return digest.hexdigest().upper() |
|
|
|
|
| def _extract_state_dict(checkpoint: object) -> Mapping[str, torch.Tensor]: |
| if not isinstance(checkpoint, Mapping): |
| raise TypeError(f"Checkpoint must be a mapping, received {type(checkpoint)!r}") |
| state = checkpoint.get("state_dict", checkpoint) |
| if not isinstance(state, Mapping): |
| raise TypeError("Checkpoint 'state_dict' must be a mapping") |
| tensors = {str(key): value for key, value in state.items() if torch.is_tensor(value)} |
| if not tensors: |
| raise ValueError("Checkpoint contains no tensor parameters") |
| return tensors |
|
|
|
|
| def remap_l2cs_resnet_key(key: str) -> str: |
| """Map raw torchvision/L2CS ResNet names into the wrapper namespace.""" |
| if key.startswith("module."): |
| key = key[len("module.") :] |
| for source, target in PREFIX_MAP.items(): |
| if key.startswith(source): |
| return target + key[len(source) :] |
| return key |
|
|
|
|
| def _remap_and_validate( |
| state: Mapping[str, torch.Tensor], model: L2CS |
| ) -> dict[str, torch.Tensor]: |
| model_state = model.state_dict() |
| remapped: dict[str, torch.Tensor] = {} |
| source_for_target: dict[str, str] = {} |
| problems: list[str] = [] |
|
|
| for source_key, tensor in state.items(): |
| target_key = remap_l2cs_resnet_key(source_key) |
| if target_key in remapped: |
| problems.append( |
| f"duplicate mapping: {source_key!r} and {source_for_target[target_key]!r} " |
| f"both map to {target_key!r}" |
| ) |
| continue |
| if target_key not in model_state: |
| problems.append(f"unexpected checkpoint tensor: {source_key!r} -> {target_key!r}") |
| continue |
| expected = model_state[target_key] |
| if tuple(tensor.shape) != tuple(expected.shape): |
| problems.append( |
| f"shape mismatch for {source_key!r} -> {target_key!r}: " |
| f"checkpoint={tuple(tensor.shape)}, model={tuple(expected.shape)}" |
| ) |
| continue |
| if tensor.dtype != expected.dtype: |
| problems.append( |
| f"dtype mismatch for {source_key!r} -> {target_key!r}: " |
| f"checkpoint={tensor.dtype}, model={expected.dtype}" |
| ) |
| continue |
| remapped[target_key] = tensor |
| source_for_target[target_key] = source_key |
|
|
| model_only = sorted(set(model_state) - set(remapped)) |
| if model_only != ["idx_tensor"]: |
| problems.append(f"missing model tensors other than allowed idx_tensor: {model_only}") |
| if len(remapped) != len(state): |
| problems.append(f"mapped {len(remapped)} of {len(state)} checkpoint tensors") |
| if problems: |
| raise RuntimeError("Strict L2CS load validation failed:\n- " + "\n- ".join(problems)) |
| return remapped |
|
|
|
|
| def fixed_inference_sha256(model: L2CS, device: torch.device) -> str: |
| """Hash deterministic logits at the recovered checkpoint's documented 448px input.""" |
| values = torch.linspace(-1.0, 1.0, steps=3 * 448 * 448, dtype=torch.float32) |
| fixed_input = values.reshape(1, 3, 448, 448).to(device) |
| with torch.inference_mode(): |
| pitch, yaw = model(fixed_input) |
| output = torch.cat((pitch, yaw), dim=1).detach().cpu().contiguous().numpy() |
| return hashlib.sha256(output.tobytes(order="C")).hexdigest().upper() |
|
|
|
|
| def load_teacher_model_strict( |
| checkpoint_path: str | Path, |
| *, |
| backbone: str = "resnet50", |
| num_bins: int = 90, |
| device: str | torch.device = "cpu", |
| compute_inference_hash: bool = True, |
| ) -> tuple[L2CS, StrictLoadAudit]: |
| """Load every trained checkpoint tensor or fail before inference.""" |
| checkpoint_path = Path(checkpoint_path).resolve() |
| target_device = torch.device(device) |
| checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True) |
| state = _extract_state_dict(checkpoint) |
| model = L2CS(backbone_name=backbone, num_bins=num_bins, pretrained=False) |
| remapped = _remap_and_validate(state, model) |
|
|
| |
| |
| completed = dict(remapped) |
| completed["idx_tensor"] = model.state_dict()["idx_tensor"] |
| model.load_state_dict(completed, strict=True) |
| model.to(target_device) |
| model.eval() |
|
|
| inference_hash = fixed_inference_sha256(model, target_device) if compute_inference_hash else None |
| audit = StrictLoadAudit( |
| checkpoint_path=str(checkpoint_path), |
| checkpoint_sha256=file_sha256(checkpoint_path), |
| checkpoint_tensor_count=len(state), |
| mapped_tensor_count=len(remapped), |
| model_tensor_count=len(model.state_dict()), |
| allowed_model_only_keys=("idx_tensor",), |
| inference_sha256=inference_hash, |
| ) |
| return model, audit |
|
|
|
|
| def audit_to_dict(audit: StrictLoadAudit) -> dict: |
| return asdict(audit) |
|
|