RoofSegmentation2 / export_text_embeddings.py
Deagin's picture
Claude Opus 5 (1M context)
Fix 16GB OOM: never build the SigLIP2 text tower on the Space
e510bac
Raw History Blame Contribute Delete
3.67 kB
"""Offline export of the SigLIP2 text embeddings for RoofSight's fixed prompts.
Run this ONCE on a machine with RAM headroom (~14 GB peak). It loads the full
`siglip2-g` adaptor — which instantiates the entire google/siglip2-giant model
just to keep its text tower — encodes the fixed prompt set, and writes the
resulting (num_prompts, D) tensor to disk.
The Space then loads with the `siglip2` adaptor factory removed from RADIO's
registry, so the text tower is never built there. See radio_backbone.load_model.
Also captures a reference feature map so the stripped runtime path can be
verified as numerically identical:
python export_text_embeddings.py # export + reference
python verify_stripped_adaptor.py # compare
"""
import os
import sys
import numpy as np
import torch
from PIL import Image
from radio_backbone import ROOF_PROMPTS, NON_ROOF_PROMPTS, PATCH_SIZE
OUT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "text_embeddings.pt")
REF_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_ref_features.pt")
REF_IMAGE = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"_run_output", "1310_susan_circle", "original.png",
)
# CPU + fp32 throughout: the reference must be deterministic so the parity
# check measures the adaptor swap, not autocast/GPU nondeterminism.
DEVICE = "cpu"
def main():
all_labels = ROOF_PROMPTS + NON_ROOF_PROMPTS
print(f"Loading FULL c-radio_v4-h + siglip2-g adaptor on {DEVICE} (this is the heavy path)...")
model = torch.hub.load(
"NVlabs/RADIO",
"radio_model",
version="c-radio_v4-h",
adaptor_names=["siglip2-g"],
vitdet_window_size=8,
progress=True,
skip_validation=True,
trust_repo=True,
)
model.eval()
model.to(DEVICE)
adaptor = model.adaptors["siglip2-g"]
assert hasattr(adaptor, "encode_text"), (
"adaptor has no encode_text — the siglip2 factory was not used. "
"This script must run WITHOUT the registry patch."
)
print(f"Encoding {len(all_labels)} prompts...")
text_input = adaptor.tokenizer(all_labels)
text_input = {k: v.to(DEVICE) for k, v in text_input.items()}
with torch.no_grad():
embeddings = adaptor.encode_text(text_input, normalize=True).float().cpu().clone()
print(f" embeddings: {tuple(embeddings.shape)} dtype={embeddings.dtype}")
torch.save({"prompts": all_labels, "embeddings": embeddings}, OUT_PATH)
print(f" wrote {OUT_PATH} ({os.path.getsize(OUT_PATH) / 1024:.1f} KB)")
# --- Reference feature map, for the parity check ---
if not os.path.exists(REF_IMAGE):
print(f"\nNo reference image at {REF_IMAGE}; skipping reference capture.")
return
print(f"\nCapturing reference features from {REF_IMAGE}...")
image = np.array(Image.open(REF_IMAGE).convert("RGB"))
x = torch.from_numpy(image).permute(2, 0, 1).float().div_(255.0).unsqueeze(0)
snapped = model.get_nearest_supported_resolution(*x.shape[-2:])
if snapped != x.shape[-2:]:
x = torch.nn.functional.interpolate(x, snapped, mode="bilinear", align_corners=False)
with torch.no_grad():
out = model(x.to(DEVICE))
_, features = out["siglip2-g"]
torch.save(
{
"features": features.float().cpu().clone(),
"snapped": tuple(snapped),
"image_shape": image.shape,
},
REF_PATH,
)
print(f" features: {tuple(features.shape)}")
print(f" wrote {REF_PATH}")
print("\nDone. Now run: python verify_stripped_adaptor.py")
if __name__ == "__main__":
sys.exit(main())