159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
"""Verify SOFIA v7.1 model scales: param count, memory footprint, forward pass.
|
||
|
||
Run:
|
||
python -m sofia_v71.verify
|
||
python -m sofia_v71.verify --preset L
|
||
python -m sofia_v71.verify --preset M --benchmark
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import time
|
||
from typing import Optional
|
||
|
||
import torch
|
||
|
||
from .blocks import is_mamba_ssm_available, is_mamba2_available
|
||
from .config import sofia_m_config, sofia_l_config, sofia_tiny_config
|
||
from .model import SOFIAv71
|
||
|
||
|
||
def count_parameters(model: torch.nn.Module) -> dict:
|
||
"""Count parameters per named child."""
|
||
counts = {}
|
||
for name, param in model.named_parameters():
|
||
top = name.split(".")[0]
|
||
counts[top] = counts.get(top, 0) + param.numel()
|
||
counts["_total"] = sum(counts.values())
|
||
return counts
|
||
|
||
|
||
def estimate_quantized_size(n_params: int, precision: str = "int8") -> float:
|
||
"""Estimate on-disk / VRAM weight storage in MB."""
|
||
bytes_per_param = {"fp32": 4, "fp16": 2, "int8": 1, "int4": 0.5}[precision]
|
||
return n_params * bytes_per_param / (1024 ** 2)
|
||
|
||
|
||
def test_forward(model: SOFIAv71, device: str = "cpu", batch_size: int = 1) -> dict:
|
||
"""Dry forward pass, return output shapes."""
|
||
model = model.to(device).eval()
|
||
cfg = model.cfg
|
||
sat = torch.randn(batch_size, 3, cfg.input_size, cfg.input_size, device=device)
|
||
uav = torch.randn(batch_size, 3, cfg.input_size, cfg.input_size, device=device)
|
||
altitude = torch.rand(batch_size, device=device) * 300.0 + 50.0
|
||
|
||
shapes = {}
|
||
with torch.no_grad():
|
||
out = model(sat=sat, uav=uav, altitude=altitude)
|
||
for k, v in out.items():
|
||
if isinstance(v, torch.Tensor):
|
||
shapes[k] = tuple(v.shape)
|
||
elif isinstance(v, list):
|
||
shapes[k] = f"list[{len(v)} × {tuple(v[0].shape)}]"
|
||
elif isinstance(v, dict):
|
||
for kk, vv in v.items():
|
||
if isinstance(vv, torch.Tensor):
|
||
shapes[f"{k}.{kk}"] = tuple(vv.shape)
|
||
return shapes
|
||
|
||
|
||
def benchmark(model: SOFIAv71, device: str = "cpu", n_warmup: int = 3, n_runs: int = 20) -> dict:
|
||
"""Measure forward pass latency."""
|
||
model = model.to(device).eval()
|
||
cfg = model.cfg
|
||
sat = torch.randn(1, 3, cfg.input_size, cfg.input_size, device=device)
|
||
uav = torch.randn(1, 3, cfg.input_size, cfg.input_size, device=device)
|
||
altitude = torch.tensor([150.0], device=device)
|
||
|
||
# Warmup
|
||
with torch.no_grad():
|
||
for _ in range(n_warmup):
|
||
_ = model(sat=sat, uav=uav, altitude=altitude)
|
||
if device.startswith("cuda"):
|
||
torch.cuda.synchronize()
|
||
|
||
# Measured runs
|
||
times = []
|
||
for _ in range(n_runs):
|
||
if device.startswith("cuda"):
|
||
torch.cuda.synchronize()
|
||
t0 = time.perf_counter()
|
||
_ = model(sat=sat, uav=uav, altitude=altitude)
|
||
if device.startswith("cuda"):
|
||
torch.cuda.synchronize()
|
||
times.append(time.perf_counter() - t0)
|
||
|
||
times_ms = [t * 1000 for t in times]
|
||
return {
|
||
"mean_ms": sum(times_ms) / len(times_ms),
|
||
"min_ms": min(times_ms),
|
||
"max_ms": max(times_ms),
|
||
"std_ms": (sum((t - sum(times_ms) / len(times_ms)) ** 2 for t in times_ms) / len(times_ms)) ** 0.5,
|
||
}
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description="Verify SOFIA v7.1 model")
|
||
parser.add_argument("--preset", choices=["Tiny", "M", "L"], default="Tiny",
|
||
help="Model preset (default: Tiny for fast smoke test)")
|
||
parser.add_argument("--device", default="cpu")
|
||
parser.add_argument("--benchmark", action="store_true")
|
||
parser.add_argument("--batch-size", type=int, default=1)
|
||
args = parser.parse_args()
|
||
|
||
cfg_fn = {"Tiny": sofia_tiny_config, "M": sofia_m_config, "L": sofia_l_config}[args.preset]
|
||
cfg = cfg_fn()
|
||
|
||
# Report mamba_ssm availability
|
||
print(f"\n=== Environment ===")
|
||
print(f" mamba_ssm v1 (Mamba-1 selective scan): "
|
||
f"{'available' if is_mamba_ssm_available() else 'NOT available'}")
|
||
print(f" mamba_ssm Mamba-2 (SSD dual form): "
|
||
f"{'available' if is_mamba2_available() else 'NOT available'}")
|
||
print(f" configured variant: {cfg.mamba_variant}, backend: {cfg.mamba_backend}")
|
||
|
||
print(f"\n=== SOFIA-{args.preset} configuration ===")
|
||
print(cfg.summary())
|
||
|
||
print("\n=== Building model ===")
|
||
model = SOFIAv71(cfg)
|
||
counts = count_parameters(model)
|
||
total = counts["_total"]
|
||
|
||
print(f"\nTotal params: {total / 1e6:.2f} M")
|
||
print("\nPer-module breakdown:")
|
||
for k, v in sorted(counts.items(), key=lambda x: -x[1]):
|
||
if k == "_total":
|
||
continue
|
||
pct = 100 * v / total
|
||
print(f" {k:30s} {v / 1e6:>8.3f} M ({pct:5.1f}%)")
|
||
|
||
print("\n=== Memory footprint estimates ===")
|
||
for prec in ["fp32", "fp16", "int8", "int4"]:
|
||
mb = estimate_quantized_size(total, prec)
|
||
print(f" {prec.upper():6s} weights: {mb:>8.1f} MB ({mb / 1024:.2f} GB)")
|
||
|
||
print("\n=== Forward pass test ===")
|
||
try:
|
||
shapes = test_forward(model, device=args.device, batch_size=args.batch_size)
|
||
for k, v in shapes.items():
|
||
print(f" out[{k}]: {v}")
|
||
except Exception as e:
|
||
print(f" ERROR during forward: {type(e).__name__}: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
if args.benchmark:
|
||
print("\n=== Latency benchmark ===")
|
||
try:
|
||
stats = benchmark(model, device=args.device)
|
||
print(f" mean: {stats['mean_ms']:.2f} ms min: {stats['min_ms']:.2f} "
|
||
f"max: {stats['max_ms']:.2f} std: {stats['std_ms']:.2f}")
|
||
except Exception as e:
|
||
print(f" ERROR during benchmark: {type(e).__name__}: {e}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|