Download satlink/tools/make_reference.py from lulidong/rs: direct link, hf CLI and curl.
- Browser
- Download file 7.43 kB
-
https://huggingface.co/lulidong/rs/resolve/main/satlink/tools/make_reference.py
- Command line
-
hf download hf://lulidong/rs/satlink/tools/make_reference.py
-
curl -L -o make_reference.py https://huggingface.co/lulidong/rs/resolve/main/satlink/tools/make_reference.py
7.43 kB
| """Record a reference encoding + reconstruction of the bundled images, for the stability tests. | |
| What is being pinned is the PICTURE, not the codes. Codes are an argmin over 4096 near-tied | |
| codebook entries; fp16 flips about 0.5% of them against fp32. Measured, that costs 0.47/255 mean | |
| absolute pixel difference -- against a reconstruction error of 10.25/255 -- and the differences sit | |
| on high-frequency edges, where a code flip moves an edge by a fraction of a pixel and a single | |
| pixel can swing 159/255 without anything semantic changing. So the reference stores both, and the | |
| tests judge on mean pixel difference. | |
| JSON plus PNG, no binary blobs: a reference is something a human should be able to open and diff | |
| when a new device disagrees. | |
| python -m satlink.tools.make_reference | |
| """ | |
| import argparse | |
| import glob | |
| import hashlib | |
| import json | |
| import os | |
| import sys | |
| import torch | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) | |
| from satlink.frame import VQFrame # noqa: E402 | |
| from satlink.protocol import LinkProfile # noqa: E402 | |
| from satlink.sat.encoder import SatEncoder # noqa: E402 | |
| def _pixel_grid(model, prof): | |
| f = model.patch_size * model.merge | |
| return (max(f, round(prof.height / f) * f) // model.patch_size, | |
| max(f, round(prof.width / f) * f) // model.patch_size) | |
| def _teacher_forced_logprobs(model, frame, ids, pos, answer, device): | |
| """-> the logprob the model assigns to each answer token, given the prompt and all earlier | |
| answer tokens. One forward pass, deterministic, no decoding.""" | |
| import torch | |
| full = torch.cat([ids, torch.tensor(answer, device=device)]) | |
| nxt = int(pos.max()) + 1 | |
| tail = torch.arange(nxt, nxt + len(answer), device=device).expand(3, -1) | |
| fpos = torch.cat([pos, tail], dim=1) | |
| with torch.no_grad(), model.reference_attention(): | |
| h = model(full, fpos, frame=frame) | |
| logits = model.compute_logits(h[len(ids) - 1:-1]).float() | |
| lp = torch.log_softmax(logits, -1) | |
| return lp[torch.arange(len(answer)), torch.tensor(answer, device=device)].tolist() | |
| def _record_answers(ref, a): | |
| """Record, per image: the generated answer, and the logprob the model assigns to each of its | |
| tokens under teacher forcing. | |
| The test uses BOTH. Teacher forcing is the stable measurement -- one forward pass, no decoding, | |
| a continuous number that moves smoothly with numerical drift. Free generation is the one that | |
| tells you what a user would actually see, but it is brittle by nature: greedy decoding turns | |
| any near-tie into a different trajectory, so it is reported as a common-prefix length rather | |
| than an equality check. | |
| """ | |
| import torch | |
| from tokenizers import Tokenizer | |
| from satlink.frame import VQFrame | |
| from satlink.ground.infer import greedy | |
| from satlink.ground.modeling_vqvlm import mrope_positions | |
| if a.latent_ground: | |
| from satlink.ground.modeling_latentvlm import LatentGlm4vForConditionalGeneration as M | |
| model = M.from_pretrained(a.model, a.latent_ground, lora=a.lora, device=a.device) | |
| tail, path = "<|assistant|>", "latent" | |
| else: | |
| from satlink.ground.modeling_vqvlm import VQGlm4vForConditionalGeneration as M | |
| model = M.from_pretrained(a.model, a.decoder, LinkProfile(512, 512), device=a.device) | |
| tail, path = "<|assistant|>\n", "pixel" | |
| tok = Tokenizer.from_file(os.path.join(a.model, "tokenizer.json")) | |
| IMG = model.image_token_id | |
| ref["answers"] = {"path": path, "question": a.question, "items": {}} | |
| for name, meta in sorted(ref["images"].items()): | |
| prof = LinkProfile(meta["width"], meta["height"], n_tok=a.n_tok) | |
| frame = VQFrame(torch.tensor(meta["codes"], dtype=torch.long), prof).to(a.device) | |
| n_img = model.n_image_tokens(prof) if a.latent_ground else model.n_image_tokens() | |
| gh, gw = model.image_grid(prof) if a.latent_ground else _pixel_grid(model, prof) | |
| ids = torch.tensor( | |
| tok.encode("[gMASK]<sop><|user|>\n<|begin_of_image|>", add_special_tokens=False).ids | |
| + [IMG] * n_img | |
| + tok.encode(f"<|end_of_image|>{a.question}{tail}", add_special_tokens=False).ids, | |
| device=a.device) | |
| pos = mrope_positions(ids, IMG, gh, gw, model.merge).to(a.device) | |
| with model.reference_attention(): | |
| ans = greedy(model, ids, pos, frame, max_new=a.answer_tokens) | |
| lp = _teacher_forced_logprobs(model, frame, ids, pos, ans, a.device) | |
| ref["answers"]["items"][name] = { | |
| "tokens": ans, "logprobs": [round(x, 4) for x in lp], | |
| "text": tok.decode(ans, skip_special_tokens=False)} | |
| print(f" answer {name:24} {len(ans)} tok, mean logprob {sum(lp)/len(lp):.4f}") | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--assets", default="satlink/assets") | |
| ap.add_argument("--sat", default="satlink/weights/sat_encoder.pt") | |
| ap.add_argument("--decoder", default="satlink/weights/ground_decoder.pt") | |
| ap.add_argument("--n-tok", type=int, default=48) | |
| ap.add_argument("--model", default=None, | |
| help="also record a reference ANSWER and its teacher-forced logprobs") | |
| ap.add_argument("--latent-ground", default=None) | |
| ap.add_argument("--lora", default=None) | |
| ap.add_argument("--question", default="where is the largest building in the image") | |
| ap.add_argument("--answer-tokens", type=int, default=96) | |
| ap.add_argument("--device", default="cuda") | |
| a = ap.parse_args() | |
| from PIL import Image | |
| import torchvision.transforms.functional as TF | |
| from satlink.ground.decoder import GroundDecoder | |
| gnd = GroundDecoder(a.decoder).to(a.device) | |
| ref = {"n_tok": a.n_tok, "dtype": "float32", | |
| "device": torch.cuda.get_device_name(0) if a.device.startswith("cuda") else "cpu", | |
| "note": "codes recorded in fp32; tests compare RECONSTRUCTED PIXELS, not codes", | |
| "images": {}} | |
| os.makedirs(os.path.join(a.assets, "reference"), exist_ok=True) | |
| for path in sorted(glob.glob(os.path.join(a.assets, "*.png"))): | |
| img = Image.open(path).convert("RGB") | |
| prof = LinkProfile(*img.size, n_tok=a.n_tok) | |
| sat = SatEncoder(a.sat, prof, device=a.device, dtype=torch.float32) | |
| codes = sat.encode(img) | |
| with torch.no_grad(): | |
| rec = gnd(VQFrame(codes.tokens, prof).to(a.device)).cpu() | |
| name = os.path.basename(path) | |
| rec_path = os.path.join(a.assets, "reference", name) | |
| TF.to_pil_image(rec).save(rec_path, "PNG", optimize=True) | |
| ref["images"][name] = { | |
| "sha256": hashlib.sha256(open(path, "rb").read()).hexdigest()[:16], | |
| "width": img.size[0], "height": img.size[1], "grid": list(prof.grid), | |
| "tile_origins": [prof.tile_origin(i) for i in range(prof.n_tiles)], | |
| "recon": f"reference/{name}", | |
| "codes": codes.tolist(), | |
| } | |
| print(f" {name:26} {img.size[0]}x{img.size[1]:<5} {prof.n_tiles} crops -> " | |
| f"{prof.frame_bytes} B, recon {os.path.getsize(rec_path)/1e3:.0f} KB") | |
| if a.model: | |
| _record_answers(ref, a) | |
| out = os.path.join(a.assets, "reference.json") | |
| json.dump(ref, open(out, "w"), indent=1) | |
| print(f"-> {out} ({os.path.getsize(out)/1e6:.2f} MB, recorded on {ref['device']})") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |