| """Cache the pooled 768-D vector for every image in a COCO split. |
| |
| python cache.py --split train2017 |
| python cache.py --split val2017 |
| |
| Selection and evaluation both read these, so the backbone is forwarded once per |
| split rather than once per experiment. Values are written straight through to a |
| memmap alongside a per-row status byte, and meta.json is written last, so an |
| interrupted run resumes with --resume instead of restarting. |
| |
| bfloat16 kernels select reduction orders by batch size, so cached values depend |
| on --batch. A cache must be built at one batch size throughout. |
| """ |
| import argparse |
| import json |
| import time |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| from PIL import Image |
|
|
| from common import COCO_ROOT, D, RES, coco_split, device, image_paths, normalize, pool |
| from common.artifacts import write_artifact |
| from common.models import load_backbone |
| from common.paths import BACKBONE |
|
|
| TODO, DONE, UNREADABLE = 0, 1, 2 |
|
|
|
|
| class Images(torch.utils.data.Dataset): |
| """Normalized images for a list of (row, path); unreadable files yield None.""" |
|
|
| def __init__(self, work): |
| self.work = work |
|
|
| def __len__(self): |
| return len(self.work) |
|
|
| def __getitem__(self, i): |
| row, path = self.work[i] |
| try: |
| return row, normalize(Image.open(path), RES, 'cpu')[0] |
| except Exception: |
| |
| return row, None |
|
|
|
|
| def collate(batch): |
| good = [(r, x) for r, x in batch if x is not None] |
| bad = [r for r, x in batch if x is None] |
| if not good: |
| return None, bad |
| rows, xs = zip(*good) |
| return (torch.tensor(rows), torch.stack(xs)), bad |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser(description=__doc__) |
| ap.add_argument('--split', default='val2017', choices=('train2017', 'val2017')) |
| ap.add_argument('--backbone', default=BACKBONE) |
| ap.add_argument('--batch', type=int, default=8) |
| ap.add_argument('--workers', type=int, default=6) |
| ap.add_argument('--resume', action='store_true') |
| ap.add_argument('--out', type=Path, default=None) |
| args = ap.parse_args() |
|
|
| out = args.out or COCO_ROOT / f'pooled_{args.split}' |
| out.mkdir(parents=True, exist_ok=True) |
| dev = device() |
|
|
| coco, id_to_file = coco_split(args.split) |
| img_ids = sorted(coco.getImgIds()) |
| paths = image_paths(id_to_file, img_ids, args.split) |
| n = len(img_ids) |
|
|
| present = all((out / f).exists() for f in ('pooled.dat', 'status.dat')) |
| if args.resume and not present: |
| raise SystemExit(f'--resume asked for but {out} has no partial run') |
| mode = 'r+' if (args.resume and present) else 'w+' |
| pooled = np.memmap(out / 'pooled.dat', np.float16, mode, shape=(n, D)) |
| status = np.memmap(out / 'status.dat', np.uint8, mode, shape=(n,)) |
|
|
| work = [(r, p) for r, p in enumerate(paths) if status[r] == TODO] |
| print(f'[init] {args.split}: {n} images, {len(work)} to compute', flush=True) |
| backbone = load_backbone(args.backbone).to(dev).eval() |
|
|
| loader = torch.utils.data.DataLoader( |
| Images(work), batch_size=args.batch, shuffle=False, |
| num_workers=args.workers, collate_fn=collate) |
|
|
| t0, seen = time.time(), 0 |
| with torch.inference_mode(): |
| for good, bad in loader: |
| for r in bad: |
| status[r] = UNREADABLE |
| if good is None: |
| continue |
| idx, x = good |
| with torch.autocast(dev, dtype=torch.bfloat16): |
| tok = backbone.forward_features(x.to(dev))['x_norm_patchtokens'] |
| rows = idx.numpy() |
| pooled[rows] = pool(tok.float()).half().cpu().numpy() |
| status[rows] = DONE |
| seen += len(rows) |
| if seen % (args.batch * 50) < args.batch: |
| pooled.flush() |
| status.flush() |
| rate = seen / max(time.time() - t0, 1e-6) |
| print(f' {seen}/{len(work)} {rate:.1f} img/s ' |
| f'ETA {(len(work) - seen) / max(rate, 1e-6) / 60:.1f} min', |
| flush=True) |
|
|
| pooled.flush() |
| status.flush() |
| done = int((np.asarray(status) == DONE).sum()) |
| np.save(out / 'pooled.npy', np.asarray(pooled)) |
| (out / 'img_ids.json').write_text(json.dumps([int(i) for i in img_ids])) |
| write_artifact(out / 'meta.json', { |
| 'split': args.split, |
| 'n_images': done, |
| 'n_unreadable': int((np.asarray(status) == UNREADABLE).sum()), |
| 'resolution': RES, |
| 'files': {'pooled.npy': {'shape': [n, D], 'dtype': 'float16'}}, |
| }, generator='cache.py', backbone=args.backbone, batch=args.batch, |
| protocol='live backbone forward at 768 px, layernorm over 768 channels ' |
| 'per patch, max over patches') |
| print(f'[done] {done}/{n} -> {out} ({time.time() - t0:.0f}s)', flush=True) |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|