| |
| """ |
| Sanity tests for make_belief_cache_v2.py outputs. |
| |
| Runs four checks per cache_mode (on already-built debug caches): |
| T1 shape + dtype invariants |
| T2 NaN/Inf-free |
| T3 index alignment with manifest (n_samples == len(manifest['samples'][:N])) |
| T4 cache_mode-specific semantic checks |
| mean_pool : must approximately match legacy v1 cache |
| (fp16 round-trip → atol=2e-2 cosine) |
| dual_pool : beliefs_img and beliefs_text differ; both unit-non-zero |
| per_frame : valid_frames & beliefs_frame consistent (zero rows ⟺ False) |
| spatial4x4 : beliefs_grid[b,f] has 16 distinct rows w/ non-trivial variance |
| when valid_frames[b,f]=True |
| (also: tta_means match v1 cache to ~1e-4 — TTA head is deterministic) |
| |
| Usage |
| ───── |
| # Build debug caches first |
| python -m training.Policy.make_belief_cache_v2 \\ |
| --sft_checkpoint checkpoints/SFT/sft_v2/best \\ |
| --cache_mode mean_pool --debug --overwrite \\ |
| --out_dir data/belief_cache_v2_debug |
| python -m training.Policy.make_belief_cache_v2 \\ |
| --sft_checkpoint checkpoints/SFT/sft_v2/best \\ |
| --cache_mode dual_pool --debug --overwrite \\ |
| --out_dir data/belief_cache_v2_debug |
| python -m training.Policy.make_belief_cache_v2 \\ |
| --sft_checkpoint checkpoints/SFT/sft_v2/best \\ |
| --cache_mode per_frame --debug --overwrite \\ |
| --out_dir data/belief_cache_v2_debug |
| python -m training.Policy.make_belief_cache_v2 \\ |
| --sft_checkpoint checkpoints/SFT/sft_v2/best \\ |
| --cache_mode spatial4x4 --debug --overwrite \\ |
| --out_dir data/belief_cache_v2_debug |
| |
| # Then run tests |
| python -m training.Policy.test_belief_cache_v2 \\ |
| --cache_root data/belief_cache_v2_debug \\ |
| --legacy_cache data/belief_cache/val.pt \\ |
| --label_dir data/policy_labels \\ |
| --n 16 |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| from pathlib import Path |
| from typing import Dict |
|
|
| import torch |
| import torch.nn.functional as F |
|
|
|
|
| def _load(p: Path) -> Dict: |
| return torch.load(p, map_location="cpu", weights_only=False) |
|
|
|
|
| def _check_no_nan_inf(name: str, t: torch.Tensor): |
| if not t.dtype.is_floating_point: |
| return |
| nans = int(torch.isnan(t).sum().item()) |
| infs = int(torch.isinf(t).sum().item()) |
| assert nans == 0, f"{name} has {nans} NaN" |
| assert infs == 0, f"{name} has {infs} Inf" |
|
|
|
|
| def _check_dtype(name: str, t: torch.Tensor, expect: torch.dtype): |
| assert t.dtype == expect, f"{name} dtype={t.dtype}, expected {expect}" |
|
|
|
|
| def _ok(msg: str): |
| print(f" PASS {msg}") |
|
|
|
|
| def _hdr(s: str): |
| print(f"\n── {s} " + "─" * (78 - len(s))) |
|
|
|
|
| def test_mean_pool(cache_root: Path, legacy_path: Path, n_expected: int): |
| _hdr("mean_pool") |
| p = cache_root / "mean_pool" / "val.pt" |
| if not p.exists(): |
| print(f" SKIP {p} not built") |
| return |
| d = _load(p) |
| b = d["beliefs"] |
| tm = d["tta_means"] |
| tv = d["tta_vars"] |
|
|
| |
| assert b.dim() == 2, f"beliefs dim={b.dim()}" |
| assert b.shape[0] == n_expected, f"beliefs N={b.shape[0]} vs {n_expected}" |
| _check_dtype("beliefs", b, torch.float16) |
| _check_dtype("tta_means", tm, torch.float32) |
| _check_dtype("tta_vars", tv, torch.float32) |
| _ok(f"shapes/dtypes beliefs={tuple(b.shape)} fp16, tta fp32") |
|
|
| |
| for k in ("beliefs", "tta_means", "tta_vars"): |
| _check_no_nan_inf(k, d[k]) |
| _ok("no NaN/Inf") |
|
|
| |
| if legacy_path.exists(): |
| v1 = _load(legacy_path) |
| v1_b = v1["beliefs"][:n_expected].float() |
| v1_tm = v1["tta_means"][:n_expected].float() |
| cos = F.cosine_similarity(b.float(), v1_b, dim=-1).mean().item() |
| tta_diff = (tm.float() - v1_tm).abs().mean().item() |
| assert cos > 0.95, f"v1↔v2 belief cosine={cos:.4f} (<0.95) — pooling logic differs" |
| assert tta_diff < 1e-2, f"tta_mean diff vs v1: {tta_diff:.6f} (head should be deterministic)" |
| _ok(f"matches legacy v1: belief_cos={cos:.4f}, tta_mae={tta_diff:.2e}") |
| else: |
| print(f" SKIP legacy comparison (no {legacy_path})") |
|
|
|
|
| def test_dual_pool(cache_root: Path, n_expected: int): |
| _hdr("dual_pool") |
| p = cache_root / "dual_pool" / "val.pt" |
| if not p.exists(): |
| print(f" SKIP {p} not built") |
| return |
| d = _load(p) |
| bi = d["beliefs_img"] |
| bt = d["beliefs_text"] |
| assert bi.shape == bt.shape and bi.shape[0] == n_expected |
| _check_dtype("beliefs_img", bi, torch.float16) |
| _check_dtype("beliefs_text", bt, torch.float16) |
| _ok(f"shapes/dtypes img={tuple(bi.shape)} text={tuple(bt.shape)} fp16") |
|
|
| _check_no_nan_inf("beliefs_img", bi) |
| _check_no_nan_inf("beliefs_text", bt) |
| _ok("no NaN/Inf") |
|
|
| |
| cos_it = F.cosine_similarity(bi.float(), bt.float(), dim=-1).mean().item() |
| assert cos_it < 0.999, f"img/text means too similar (cos={cos_it:.4f}) — split broken" |
| norm_i = bi.float().norm(dim=-1).mean().item() |
| norm_t = bt.float().norm(dim=-1).mean().item() |
| assert norm_i > 1.0 and norm_t > 1.0, \ |
| f"degenerate norms img={norm_i:.3f} text={norm_t:.3f}" |
| _ok(f"img/text differ: cos={cos_it:.4f}, norms img={norm_i:.2f} text={norm_t:.2f}") |
|
|
|
|
| def test_per_frame(cache_root: Path, n_expected: int): |
| _hdr("per_frame") |
| p = cache_root / "per_frame" / "val.pt" |
| if not p.exists(): |
| print(f" SKIP {p} not built") |
| return |
| d = _load(p) |
| bf = d["beliefs_frame"] |
| vf = d["valid_frames"] |
| bt = d["beliefs_text"] |
| assert bf.dim() == 3 and bf.shape[0] == n_expected |
| assert vf.shape == bf.shape[:2] |
| assert vf.dtype == torch.bool |
| _check_dtype("beliefs_frame", bf, torch.float16) |
| _ok(f"shapes/dtypes frame={tuple(bf.shape)} valid={tuple(vf.shape)}") |
|
|
| _check_no_nan_inf("beliefs_frame", bf) |
| _check_no_nan_inf("beliefs_text", bt) |
| _ok("no NaN/Inf") |
|
|
| |
| invalid_norms = bf[~vf].float().norm(dim=-1) |
| valid_norms = bf[ vf].float().norm(dim=-1) |
| if invalid_norms.numel() > 0: |
| assert invalid_norms.max().item() < 1e-3, \ |
| f"invalid-frame slot has nonzero belief (max norm={invalid_norms.max():.3e})" |
| if valid_norms.numel() > 0: |
| assert valid_norms.min().item() > 0.5, \ |
| f"valid frame degenerate (min norm={valid_norms.min():.3e})" |
| _ok(f"validity mask consistent: {int(vf.sum())} valid / {vf.numel()} slots") |
|
|
|
|
| def test_spatial4x4(cache_root: Path, n_expected: int): |
| _hdr("spatial4x4") |
| p = cache_root / "spatial4x4" / "val.pt" |
| if not p.exists(): |
| print(f" SKIP {p} not built") |
| return |
| d = _load(p) |
| bg = d["beliefs_grid"] |
| vf = d["valid_frames"] |
| assert bg.dim() == 4 and bg.shape[0] == n_expected and bg.shape[2] == 16 |
| assert vf.shape == bg.shape[:2] |
| _check_dtype("beliefs_grid", bg, torch.float16) |
| _ok(f"shapes/dtypes grid={tuple(bg.shape)} (B,F,16,D)") |
|
|
| _check_no_nan_inf("beliefs_grid", bg) |
| _ok("no NaN/Inf") |
|
|
| |
| |
| if vf.any(): |
| v_idx = vf.nonzero(as_tuple=False) |
| |
| sel = v_idx[:min(8, v_idx.shape[0])] |
| spatial_stds = [] |
| for (b, f) in sel.tolist(): |
| cells = bg[b, f].float() |
| |
| spatial_stds.append(cells.std(dim=0).mean().item()) |
| avg_std = float(sum(spatial_stds) / max(len(spatial_stds), 1)) |
| assert avg_std > 1e-3, \ |
| f"spatial cells nearly constant (mean std across 16 cells={avg_std:.2e})" |
| _ok(f"spatial variance OK (avg cross-cell std={avg_std:.3f})") |
|
|
| |
| inv_max = bg[~vf].float().abs().max().item() if (~vf).any() else 0.0 |
| assert inv_max < 1e-3, f"invalid frame slot nonzero (|max|={inv_max:.3e})" |
| _ok("invalid frame slots are zero") |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--cache_root", required=True, type=Path, |
| help="Root containing {mean_pool,dual_pool,per_frame,spatial4x4}/{train,val}.pt") |
| ap.add_argument("--legacy_cache", default="data/belief_cache/val.pt", type=Path, |
| help="v1 cache for cross-check (used by mean_pool test)") |
| ap.add_argument("--label_dir", default="data/policy_labels", type=Path) |
| ap.add_argument("--n", type=int, default=16, |
| help="Expected n_samples (must match --debug_samples used at build time)") |
| args = ap.parse_args() |
|
|
| |
| val_labels = json.loads((args.label_dir / "val.json").read_text()) |
| n_total = len(val_labels.get("samples", [])) |
| n_expect = min(args.n, n_total) |
| print(f"Expecting {n_expect} samples (debug_samples={args.n}, manifest has {n_total}).") |
|
|
| test_mean_pool(args.cache_root, args.legacy_cache, n_expect) |
| test_dual_pool(args.cache_root, n_expect) |
| test_per_frame(args.cache_root, n_expect) |
| test_spatial4x4(args.cache_root, n_expect) |
|
|
| print("\nAll requested tests passed.") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|