| |
| """ |
| Caption ALL scene=1 images for a relation using LLaVA with DDP. |
| |
| Each GPU loads its own model copy and processes a shard. |
| Scene=0 images get an empty caption (no LLaVA needed). |
| Incrementally checkpoints after each batch so runs can be resumed. |
| |
| Usage (single GPU): |
| CUDA_VISIBLE_DEVICES=0 python EFUF/scripts/caption_ddp.py \\ |
| --relation bathroom_toilet \\ |
| --output EFUF/data/bathroom_toilet/all_captions.json |
| |
| Usage (multi-GPU via torchrun): |
| CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --nproc_per_node=4 \\ |
| EFUF/scripts/caption_ddp.py \\ |
| --relation bathroom_toilet \\ |
| --output EFUF/data/bathroom_toilet/all_captions.json |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import gc |
| import json |
| import os |
| import sys |
|
|
| import torch |
| import torch.distributed as dist |
| from tqdm import tqdm |
|
|
| sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../..")) |
| from experiment.config.relation_config import get_relation_config |
|
|
| PROMPT = "Describe this image." |
| LLAVA_MODEL = "llava-hf/llava-1.5-7b-hf" |
| DEFAULT_BATCH_SIZE = 8 |
|
|
|
|
| def _clear_gpu(device: torch.device) -> None: |
| gc.collect() |
| torch.cuda.empty_cache() |
| torch.cuda.synchronize() |
|
|
|
|
| def load_hf_dataset(dataset_id: str): |
| from datasets import load_dataset |
| return load_dataset(dataset_id) |
|
|
|
|
| def build_existing_caption_map(checkpoint_path: str, output_path: str | None = None) -> dict[str, str]: |
| cmap: dict[str, str] = {} |
|
|
| if os.path.exists(checkpoint_path): |
| with open(checkpoint_path) as f: |
| ckpt = json.load(f) |
| for split in ("train", "val"): |
| ids = ckpt.get(f"{split}_ids", []) |
| caps = ckpt.get(f"llava_{split}", []) |
| for iid, cap in zip(ids, caps): |
| cmap[str(iid)] = cap |
| pos_ids = ckpt.get(f"{split}_pos_ids", []) |
| pos_caps = ckpt.get(f"llava_{split}_pos", []) |
| if pos_caps: |
| for iid, cap in zip(pos_ids, pos_caps): |
| cmap[str(iid)] = cap |
|
|
| if output_path and os.path.exists(output_path): |
| with open(output_path) as f: |
| for entry in json.load(f): |
| iid = str(entry["image_id"]) |
| cap = entry.get("llava_caption", "") |
| if cap: |
| cmap[iid] = cap |
|
|
| return cmap |
|
|
|
|
| def load_ckpt(ckpt_path: str) -> dict[str, str]: |
| if not os.path.exists(ckpt_path): |
| return {} |
| cmap: dict[str, str] = {} |
| with open(ckpt_path) as f: |
| for line in f: |
| line = line.strip() |
| if not line: |
| continue |
| entry = json.loads(line) |
| iid = entry["image_id"] |
| cap = entry.get("llava_caption", "") |
| if cap: |
| cmap[iid] = cap |
| return cmap |
|
|
|
|
| def append_ckpt(ckpt_path: str, results: dict[str, str], scene_col: str, object_col: str, meta: dict[str, dict]): |
| with open(ckpt_path, "a") as f: |
| for iid, cap in results.items(): |
| m = meta.get(iid, {"scene": 1, "obj": 1}) |
| f.write(json.dumps({ |
| "image_id": iid, |
| "llava_caption": cap, |
| scene_col: m["scene"], |
| object_col: m["obj"], |
| "edited_caption": None, |
| }, ensure_ascii=False) + "\n") |
|
|
|
|
| def infer_shard( |
| image_ids: list[str], |
| processor, |
| model: torch.nn.Module, |
| device: torch.device, |
| batch_size: int, |
| desc: str, |
| rank: int, |
| hf_images: dict[str, "Image.Image"], |
| ckpt_path: str | None, |
| scene_col: str, |
| object_col: str, |
| meta: dict[str, dict], |
| ) -> dict[str, str]: |
| from PIL import Image |
|
|
| results: dict[str, str] = {} |
| all_imgs: dict[str, Image.Image] = {} |
| for iid in image_ids: |
| if iid in hf_images: |
| all_imgs[iid] = hf_images[iid] |
|
|
| available_ids = [iid for iid in image_ids if iid in all_imgs] |
| if not available_ids: |
| return results |
|
|
| pbar_len = (len(available_ids) + batch_size - 1) // batch_size |
| pbar = tqdm(total=pbar_len, desc=f"[GPU{rank}] {desc}", position=rank) |
| for i in range(0, len(available_ids), batch_size): |
| batch_ids = available_ids[i : i + batch_size] |
| imgs = [all_imgs[iid] for iid in batch_ids] |
| texts = [f"USER: <image>\n{PROMPT} ASSISTANT:" for _ in batch_ids] |
|
|
| inputs = processor(text=texts, images=imgs, return_tensors="pt", padding=True) |
| inputs = {k: v.to(device) if hasattr(v, "to") else v for k, v in inputs.items()} |
|
|
| with torch.inference_mode(): |
| out = model.generate(**inputs, max_new_tokens=150, do_sample=False, use_cache=True) |
|
|
| inp_len = inputs["input_ids"].shape[1] |
| batch_results: dict[str, str] = {} |
| for iid, seq in zip(batch_ids, out): |
| caption = processor.decode(seq[inp_len:], skip_special_tokens=True).strip() |
| results[iid] = caption |
| batch_results[iid] = caption |
| pbar.update(1) |
|
|
| if ckpt_path: |
| append_ckpt(ckpt_path, batch_results, scene_col, object_col, meta) |
|
|
| pbar.close() |
| return results |
|
|
|
|
| def worker(rank: int, world_size: int, args): |
| local_rank = int(os.environ.get("LOCAL_RANK", rank)) |
| device = torch.device(f"cuda:{local_rank}") |
| torch.cuda.set_device(device) |
|
|
| if rank == 0: |
| print(f"[caption_ddp] relation={args.relation}") |
| print(f"[caption_ddp] world_size={world_size}, batch_size={args.batch_size} per GPU") |
| print(f"[caption_ddp] output={args.output}") |
|
|
| rc = get_relation_config(args.relation) |
| scene_col = rc.scene_key |
| object_col = rc.object_key |
| dataset_id = rc.dataset_id |
|
|
| data_dir = os.path.join( |
| os.path.dirname(os.path.abspath(__file__)), "..", "..", |
| "VisEdit", "data", "hallucination", args.relation, |
| ) |
| ckpt_path = args.checkpoint or os.path.join(data_dir, "checkpoint.json") |
| incremental_ckpt = args.output + ".ckpt.jsonl" |
|
|
| if rank == 0: |
| print(f"[caption_ddp] Loading existing captions from {ckpt_path} ...") |
| existing_captions = build_existing_caption_map(ckpt_path, args.output) |
|
|
| if os.path.exists(incremental_ckpt): |
| incremental_captions = load_ckpt(incremental_ckpt) |
| existing_captions.update(incremental_captions) |
| if rank == 0: |
| print(f"[caption_ddp] Resumed {len(incremental_captions)} captions from incremental checkpoint") |
|
|
| if rank == 0: |
| print(f"[caption_ddp] Existing captions: {len(existing_captions)}") |
|
|
| if rank == 0: |
| print(f"[caption_ddp] Loading HF dataset {dataset_id} ...") |
| ds = load_hf_dataset(dataset_id) |
|
|
| missing_train: list[str] = [] |
| missing_val: list[str] = [] |
| missing_meta: dict[str, dict] = {} |
| all_entries: list[dict] = [] |
| hf_images: dict[str, "Image.Image"] = {} |
|
|
| for split_name in ("train", "val"): |
| hf_split = "validation" if split_name == "val" else split_name |
| split = ds[hf_split] if hf_split in ds else ds.get(split_name, ds.get("test", [])) |
|
|
| for item in split: |
| scene = int(item[scene_col]) |
| obj = int(item[object_col]) |
| iid = str(item["image_id"]) |
|
|
| if scene == 1 and "image" in item: |
| try: |
| hf_images[iid] = item["image"].convert("RGB") |
| except Exception: |
| pass |
|
|
| cap = existing_captions.get(iid, "") |
| if cap: |
| all_entries.append({ |
| "image_id": iid, |
| "llava_caption": cap, |
| scene_col: scene, |
| object_col: obj, |
| "edited_caption": None, |
| }) |
| elif scene == 1: |
| missing_meta[iid] = {"scene": scene, "obj": obj, "split": split_name} |
| if split_name == "train": |
| missing_train.append(iid) |
| else: |
| missing_val.append(iid) |
| else: |
| all_entries.append({ |
| "image_id": iid, |
| "llava_caption": "", |
| scene_col: scene, |
| object_col: obj, |
| "edited_caption": None, |
| }) |
|
|
| shard_train = missing_train[rank::world_size] |
| shard_val = missing_val[rank::world_size] |
| total_missing = len(missing_train) + len(missing_val) |
|
|
| if rank == 0: |
| print(f"[caption_ddp] Missing captions: {total_missing} ({len(missing_train)} train + {len(missing_val)} val)") |
| print(f"[caption_ddp] Each GPU gets ~{len(shard_train)} train + ~{len(shard_val)} val") |
|
|
| if total_missing == 0: |
| if rank == 0: |
| if os.path.exists(incremental_ckpt): |
| os.remove(incremental_ckpt) |
| os.makedirs(os.path.dirname(args.output), exist_ok=True) |
| with open(args.output, "w") as f: |
| json.dump(all_entries, f, indent=2, ensure_ascii=False) |
| print(f"[caption_ddp] Done! Saved {len(all_entries)} entries to {args.output}") |
| if world_size > 1: |
| dist.destroy_process_group() |
| return |
|
|
| if rank == 0: |
| print(f"[caption_ddp] Loading LLaVA {args.llava_model} on each GPU ...") |
|
|
| from transformers import LlavaForConditionalGeneration, AutoProcessor |
|
|
| processor = AutoProcessor.from_pretrained(args.llava_model) |
| processor.tokenizer.padding_side = "left" |
|
|
| torch.backends.cuda.matmul.allow_tf32 = True |
| torch.backends.cudnn.allow_tf32 = True |
|
|
| model = LlavaForConditionalGeneration.from_pretrained( |
| args.llava_model, |
| torch_dtype=torch.bfloat16, |
| device_map=None, |
| attn_implementation="sdpa", |
| ).to(device) |
| model.eval() |
|
|
| os.makedirs(os.path.dirname(args.output), exist_ok=True) |
|
|
| new_caps: dict[str, str] = {} |
| if shard_train: |
| new_caps.update(infer_shard( |
| shard_train, processor, model, device, |
| args.batch_size, "train", rank, hf_images, |
| incremental_ckpt, scene_col, object_col, missing_meta, |
| )) |
| if shard_val: |
| new_caps.update(infer_shard( |
| shard_val, processor, model, device, |
| args.batch_size, "val", rank, hf_images, |
| incremental_ckpt, scene_col, object_col, missing_meta, |
| )) |
|
|
| tmp_path = f"{args.output}.rank{rank}.tmp" |
| with open(tmp_path, "w") as f: |
| json.dump(new_caps, f, indent=2, ensure_ascii=False) |
|
|
| del model, processor |
| _clear_gpu(device) |
|
|
| if world_size > 1: |
| dist.barrier() |
|
|
| if rank == 0: |
| for r in range(world_size): |
| tmp = f"{args.output}.rank{r}.tmp" |
| if os.path.exists(tmp): |
| with open(tmp) as f: |
| shard_caps = json.load(f) |
| for iid, cap in shard_caps.items(): |
| meta = missing_meta.get(iid, {"scene": 1, "obj": 1}) |
| all_entries.append({ |
| "image_id": iid, |
| "llava_caption": cap, |
| scene_col: meta["scene"], |
| object_col: meta["obj"], |
| "edited_caption": None, |
| }) |
| os.remove(tmp) |
|
|
| if os.path.exists(incremental_ckpt): |
| os.remove(incremental_ckpt) |
| with open(args.output, "w") as f: |
| json.dump(all_entries, f, indent=2, ensure_ascii=False) |
| print(f"[caption_ddp] Done! Saved {len(all_entries)} entries to {args.output}") |
|
|
|
|
| if __name__ == "__main__": |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--relation", required=True) |
| ap.add_argument("--output", required=True) |
| ap.add_argument("--batch_size", type=int, default=DEFAULT_BATCH_SIZE) |
| ap.add_argument("--llava_model", default=LLAVA_MODEL) |
| ap.add_argument("--checkpoint", default=None) |
| ap.add_argument("--local-rank", type=int, default=0) |
| args = ap.parse_args() |
|
|
| if "RANK" in os.environ: |
| rank = int(os.environ["RANK"]) |
| world_size = int(os.environ["WORLD_SIZE"]) |
| dist.init_process_group("nccl") |
| worker(rank, world_size, args) |
| dist.destroy_process_group() |
| else: |
| worker(0, 1, args) |