#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import importlib.metadata import importlib.util import inspect import json import os import shutil import subprocess import sys from collections import Counter from pathlib import Path from typing import Any import numpy as np from PIL import Image, ImageDraw import torch REPO_ROOT = Path(__file__).resolve().parents[1] DEFAULT_OUTPUT_DIR = REPO_ROOT / "analysis_outputs" / "a5500_eval_golden_reference" DEFAULT_CONFIG_ENTRY = "config/grpo.py:general_radiomics_omnigen_4gpu_kl_eval" DEFAULT_SFT_LORA = Path("/home/wenting/gen_joint/results_new/scratch_15k") DEFAULT_RL_LORA = REPO_ROOT / "logs/radiomics/img-only-r32-a64-bs32-evalbs24-kl-beta0p005-scratch-15k/checkpoints/checkpoint-190/lora" DEFAULT_OMNIGEN_CODE_ROOT = Path("/home/wenting/gen_joint") KEY_FLOW_FILES = [ "scripts/single_node/eval_4gpu.sh", "scripts/single_node/eval_4gpu_scratch15k_image_only.sh", "scripts/eval_omnigen.py", "scripts/train_omnigen.py", "config/grpo.py", "flow_grpo/omnigen_patch/omnigen_pipeline_with_logprob.py", "flow_grpo/omnigen_patch/joint_model_loader.py", "flow_grpo/omnigen_patch/__init__.py", ] KEY_GEN_FILES = [ "OmniGen/__init__.py", "OmniGen/pipeline.py", "OmniGen/scheduler.py", "OmniGen/model.py", "OmniGen/processor.py", "OmniGen/transformer.py", ] FLOW_DIFF_TARGETS = [ "scripts/single_node/eval_4gpu.sh", "scripts/single_node/eval_4gpu_scratch15k_image_only.sh", "scripts/eval_omnigen.py", "scripts/train_omnigen.py", "config/grpo.py", "flow_grpo/omnigen_patch", ] GEN_DIFF_TARGETS = [ "OmniGen", ] PACKAGES = { "torch": "torch", "xformers": "xformers", "diffusers": "diffusers", "transformers": "transformers", "accelerate": "accelerate", "peft": "peft", "safetensors": "safetensors", "ml_collections": "ml-collections", "huggingface_hub": "huggingface-hub", "numpy": "numpy", "Pillow": "Pillow", } def _sha256(path: Path) -> str | None: if not path.is_file(): return None digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def _file_record(path: Path) -> dict[str, Any]: return { "path": str(path), "exists": path.is_file(), "size": path.stat().st_size if path.is_file() else None, "sha256": _sha256(path), } def _run(cmd: list[str], cwd: Path) -> dict[str, Any]: try: proc = subprocess.run(cmd, cwd=str(cwd), check=False, text=True, capture_output=True) return {"cmd": cmd, "returncode": proc.returncode, "stdout": proc.stdout.strip(), "stderr": proc.stderr.strip()} except OSError as exc: return {"cmd": cmd, "error": repr(exc)} def _package_versions() -> dict[str, Any]: versions = {} for label, package in PACKAGES.items(): try: versions[label] = importlib.metadata.version(package) except importlib.metadata.PackageNotFoundError: versions[label] = None return versions def _plain(value: Any) -> Any: if hasattr(value, "to_dict"): return _plain(value.to_dict()) if hasattr(value, "items"): return {str(key): _plain(item) for key, item in value.items()} if isinstance(value, tuple): return [_plain(item) for item in value] if isinstance(value, list): return [_plain(item) for item in value] return value def _load_config(config_entry: str): module_path, function_name = config_entry.split(":", 1) module_file = (REPO_ROOT / module_path).resolve() if not Path(module_path).is_absolute() else Path(module_path) spec = importlib.util.spec_from_file_location("a5500_eval_ref_config", module_file) if spec is None or spec.loader is None: raise RuntimeError(f"Could not load config from {module_file}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return getattr(module, function_name)() def _relocate_path(value: Any, replacements: list[Any]) -> Any: if not isinstance(value, str): return value for old_root, new_root in replacements: old_root = str(old_root).rstrip("/") new_root = str(new_root).rstrip("/") if value == old_root: return new_root if value.startswith(f"{old_root}/"): return f"{new_root}/{value[len(old_root) + 1:]}" return value def _relocate_metadata(metadata: dict[str, Any], replacements: list[Any]) -> dict[str, Any]: metadata = dict(metadata) for key in ("output_image", "gt_image", "output_mask", "gt_mask", "mask"): if key in metadata: metadata[key] = _relocate_path(metadata[key], replacements) if "input_images" in metadata: metadata["input_images"] = [_relocate_path(path, replacements) for path in metadata["input_images"]] return metadata def _dataset_file(dataset: Any, split: str) -> Path: if hasattr(dataset, "get"): file_path = dataset.get(f"{split}_jsonl") or dataset.get("jsonl") if file_path is None and dataset.get("root"): file_path = Path(dataset.get("root")) / f"{split}_metadata.jsonl" if file_path is None: raise ValueError(f"Dataset config is missing {split}_jsonl/jsonl/root") return Path(file_path).expanduser().resolve() return Path(dataset).expanduser().resolve() / f"{split}_metadata.jsonl" def _first_sample(config, sample_id: str | None = None) -> dict[str, Any]: dataset = config.dataset replacements = list(dataset.get("path_replacements") or []) if hasattr(dataset, "get") else [] file_path = _dataset_file(dataset, "test") with file_path.open("r", encoding="utf-8") as handle: for line in handle: if not line.strip(): continue metadata = _relocate_metadata(json.loads(line), replacements) if sample_id and metadata.get("sample_id") != sample_id: continue input_images = metadata.get("input_images") or [] gt_path = metadata.get("gt_image") or metadata.get("output_image") if input_images and gt_path and Path(input_images[0]).exists() and Path(gt_path).exists(): return {"metadata": metadata, "dataset_file": str(file_path)} raise RuntimeError(f"No usable sample found in {file_path} for sample_id={sample_id!r}") def _image_stats(path: Path) -> dict[str, Any]: image = Image.open(path).convert("RGB") arr = np.asarray(image) flat = arr.reshape(-1) counts = Counter(flat.tolist()) mode_value, mode_count = counts.most_common(1)[0] return { "path": str(path), "sha256": _sha256(path), "size": list(image.size), "mode": image.mode, "min": int(arr.min()), "max": int(arr.max()), "mean": float(arr.mean()), "std": float(arr.std()), "pixel_mode_value": int(mode_value), "pixel_mode_count": int(mode_count), } def _to_rgb_pil(image): from scripts.train_omnigen import _to_rgb_pil as train_to_rgb_pil return train_to_rgb_pil(image) def _save_contact_sheet(input_path: Path, output_path: Path, gt_path: Path, sheet_path: Path) -> None: panels = [ ("Input", Image.open(input_path).convert("RGB")), ("Output", Image.open(output_path).convert("RGB")), ("GT", Image.open(gt_path).convert("RGB")), ] target_w = max(image.width for _, image in panels) target_h = max(image.height for _, image in panels) gap = 12 title_h = 24 canvas = Image.new("RGB", (target_w * 3 + gap * 2, target_h + title_h), "white") draw = ImageDraw.Draw(canvas) x = 0 for label, image in panels: draw.text((x, 4), label, fill=(0, 0, 0)) canvas.paste(image.resize((target_w, target_h), Image.Resampling.BILINEAR), (x, title_h)) x += target_w + gap canvas.save(sheet_path) def _lora_fingerprint(path: Path) -> dict[str, Any]: from safetensors.torch import load_file model_path = path / "adapter_model.safetensors" config_path = path / "adapter_config.json" result = { "path": str(path), "adapter_model": _file_record(model_path), "adapter_config": _file_record(config_path), "num_keys": None, "first_10_tensors": [], } if model_path.is_file(): tensors = load_file(str(model_path), device="cpu") result["num_keys"] = len(tensors) for name in sorted(tensors)[:10]: tensor = tensors[name] result["first_10_tensors"].append( {"name": name, "shape": list(tensor.shape), "dtype": str(tensor.dtype)} ) return result def _hf_snapshot_fingerprint(model_root: Path) -> dict[str, Any]: cache_root = model_root.parent.parent if model_root.parent.name == "snapshots" else None snapshots = [] if cache_root is not None: snapshots_dir = cache_root / "snapshots" if snapshots_dir.is_dir(): snapshots = sorted(path.name for path in snapshots_dir.iterdir() if path.is_dir()) key_names = [ "config.json", "model.safetensors.index.json", "special_tokens_map.json", "tokenizer.json", "tokenizer_config.json", "vae/config.json", "vae/diffusion_pytorch_model.safetensors", ] files = [] for name in key_names: path = model_root / name if path.exists(): files.append(_file_record(path)) return { "resolved_snapshot_path": str(model_root), "snapshot_commit_id": model_root.name if model_root.parent.name == "snapshots" else None, "all_snapshots": snapshots, "multiple_snapshots": len(snapshots) > 1, "key_files": files, } def _import_paths() -> dict[str, Any]: import diffusers import transformers import OmniGen from OmniGen import OmniGenPipeline, OmniGenScheduler from flow_grpo.omnigen_patch import omnigen_pipeline_with_logprob return { "OmniGen.__file__": getattr(OmniGen, "__file__", None), "OmniGenPipeline": inspect.getfile(OmniGenPipeline), "OmniGenScheduler": inspect.getfile(OmniGenScheduler), "pipeline_with_logprob": inspect.getfile(omnigen_pipeline_with_logprob.pipeline_with_logprob), "pipeline_with_logprob_unwrapped": inspect.getfile(inspect.unwrap(omnigen_pipeline_with_logprob.pipeline_with_logprob)), "diffusers.__file__": getattr(diffusers, "__file__", None), "transformers.__file__": getattr(transformers, "__file__", None), } def _runtime_env() -> dict[str, Any]: return { "python_executable": sys.executable, "python_version": sys.version, "conda_default_env": os.environ.get("CONDA_DEFAULT_ENV"), "conda_prefix": os.environ.get("CONDA_PREFIX"), "torch_version": torch.__version__, "cuda_available": torch.cuda.is_available(), "torch_cuda_version": torch.version.cuda, "cuda_visible_devices": os.environ.get("CUDA_VISIBLE_DEVICES"), "gpu_model": torch.cuda.get_device_name(0) if torch.cuda.is_available() else None, "device_capability": list(torch.cuda.get_device_capability(0)) if torch.cuda.is_available() else None, "allow_tf32_matmul": torch.backends.cuda.matmul.allow_tf32, "allow_tf32_cudnn": torch.backends.cudnn.allow_tf32, "cudnn_benchmark": torch.backends.cudnn.benchmark, "bf16_supported": torch.cuda.is_available() and torch.cuda.is_bf16_supported(), "packages": _package_versions(), "env": { key: os.environ.get(key) for key in [ "HF_HOME", "HF_HUB_CACHE", "PYTHONPATH", "OMNIGEN_CODE_ROOT", "SFT_LORA_PATH", "EVAL_LORA_PATH", "DATASET_ROOT", "TRAIN_JSONL", "TEST_JSONL", ] }, } def _repo_state(gen_root: Path) -> dict[str, Any]: return { "flow_grpo_cxr": { "cwd": str(REPO_ROOT), "head": _run(["git", "rev-parse", "HEAD"], REPO_ROOT), "status": _run(["git", "status", "--short"], REPO_ROOT), "diff_name_only": _run(["git", "diff", "--name-only"], REPO_ROOT), "targeted_diff": _run(["git", "diff", "--", *FLOW_DIFF_TARGETS], REPO_ROOT), }, "gen_joint": { "cwd": str(gen_root), "head": _run(["git", "rev-parse", "HEAD"], gen_root), "status": _run(["git", "status", "--short"], gen_root), "diff_name_only": _run(["git", "diff", "--name-only"], gen_root), "targeted_diff": _run(["git", "diff", "--", *GEN_DIFF_TARGETS], gen_root), }, } def _key_hashes(gen_root: Path) -> dict[str, Any]: hashes = {} for rel in KEY_FLOW_FILES: hashes[f"flow_grpo_cxr/{rel}"] = _file_record(REPO_ROOT / rel) for rel in KEY_GEN_FILES: hashes[f"gen_joint/{rel}"] = _file_record(gen_root / rel) return hashes def _copy_image(src: Path, dst: Path) -> dict[str, Any]: dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst) return _image_stats(dst) def _generate_one(config, metadata: dict[str, Any], output_path: Path, *, eval_lora_path: Path | None) -> dict[str, Any]: from peft import PeftModel from scripts.train_omnigen import load_omnigen_components, merge_lora_into_base_model from flow_grpo.omnigen_patch.omnigen_pipeline_with_logprob import pipeline_with_logprob if not torch.cuda.is_available(): raise RuntimeError("CUDA is not available; refusing to generate a golden A5500 eval image on CPU.") device = torch.device("cuda") weight_dtype = torch.bfloat16 model, vae, processor = load_omnigen_components(config, device, weight_dtype) merge_lora_path = getattr(config.train, "merge_lora_path", None) if merge_lora_path: model = merge_lora_into_base_model(model, merge_lora_path, weight_dtype, trainable=False) if eval_lora_path is not None: model = PeftModel.from_pretrained(model, str(eval_lora_path), is_trainable=False) if hasattr(model, "set_adapter"): model.set_adapter("default") model.to(dtype=weight_dtype) model.eval() input_images = metadata.get("input_images") or [] instruction = metadata.get("instruction") if not instruction: instruction = f"<|image_1|> {metadata['prompt']}" with torch.no_grad(): with torch.autocast(device_type="cuda", dtype=torch.bfloat16): collected = pipeline_with_logprob( model, vae, processor, [instruction], [input_images], height=config.resolution, width=config.resolution, num_inference_steps=config.sample.eval_num_steps, guidance_scale=config.sample.eval_guidance_scale, img_guidance_scale=config.sample.eval_img_guidance_scale, max_input_image_size=config.sample.max_input_image_size, use_img_guidance=config.sample.use_img_guidance, use_input_image_size_as_output=config.sample.use_input_image_size_as_output, dtype=weight_dtype, output_type="pt", noise_level=getattr(config.sample, "noise_level", 0.0), sde_type=config.sample.sde_type, ) image = collected["images"].float().cpu().numpy()[0] pil = _to_rgb_pil(image) output_path.parent.mkdir(parents=True, exist_ok=True) pil.save(output_path, format="PNG") return _image_stats(output_path) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--output-dir", default=str(DEFAULT_OUTPUT_DIR)) parser.add_argument("--config", default=DEFAULT_CONFIG_ENTRY) parser.add_argument("--omnigen-code-root", default=str(DEFAULT_OMNIGEN_CODE_ROOT)) parser.add_argument("--sft-lora-path", default=str(DEFAULT_SFT_LORA)) parser.add_argument("--eval-lora-path", default=str(DEFAULT_RL_LORA)) parser.add_argument("--sample-id", default=None) parser.add_argument("--skip-rl", action="store_true") parser.add_argument("--skip-generation", action="store_true", help="Collect static fingerprint only; do not generate images.") args = parser.parse_args() output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) gen_root = Path(args.omnigen_code_root).expanduser().resolve() os.environ.setdefault("OMNIGEN_CODE_ROOT", str(gen_root)) os.environ.setdefault("SFT_LORA_PATH", str(Path(args.sft_lora_path).expanduser().resolve())) if str(gen_root) not in sys.path: sys.path.insert(0, str(gen_root)) config = _load_config(args.config) config.train.merge_lora_path = str(Path(args.sft_lora_path).expanduser().resolve()) sample_bundle = _first_sample(config, args.sample_id) metadata = sample_bundle["metadata"] input_path = Path((metadata.get("input_images") or [])[0]).expanduser().resolve() gt_path = Path(metadata.get("gt_image") or metadata.get("output_image")).expanduser().resolve() from scripts.train_omnigen import resolve_model_root model_root = Path(resolve_model_root(config.pretrained.model)).resolve() imports = _import_paths() sample_id = metadata.get("sample_id") or gt_path.stem sample_dir = output_dir / str(sample_id) input_copy = sample_dir / "input.png" gt_copy = sample_dir / "gt.png" sft_output = sample_dir / "generated_sft_only.png" rl_output = sample_dir / "generated_checkpoint_190.png" sft_sheet = sample_dir / "contact_sheet_sft_only.png" rl_sheet = sample_dir / "contact_sheet_checkpoint_190.png" input_stats = _copy_image(input_path, input_copy) gt_stats = _copy_image(gt_path, gt_copy) sft_stats = None if not args.skip_generation: sft_stats = _generate_one(config, metadata, sft_output, eval_lora_path=None) _save_contact_sheet(input_copy, sft_output, gt_copy, sft_sheet) rl_stats = None eval_lora_path = Path(args.eval_lora_path).expanduser().resolve() if args.eval_lora_path else None if not args.skip_generation and not args.skip_rl and eval_lora_path is not None and eval_lora_path.exists(): rl_stats = _generate_one(config, metadata, rl_output, eval_lora_path=eval_lora_path) _save_contact_sheet(input_copy, rl_output, gt_copy, rl_sheet) fingerprint = { "config_entry": args.config, "repo_state": _repo_state(gen_root), "key_file_hashes": _key_hashes(gen_root), "runtime_env": _runtime_env(), "import_paths": imports, "sft_lora": _lora_fingerprint(Path(args.sft_lora_path).expanduser().resolve()), "checkpoint_190_lora": _lora_fingerprint(eval_lora_path) if eval_lora_path else None, "hf_snapshot": _hf_snapshot_fingerprint(model_root), "dataset": { "config": _plain(config.dataset), "test_file": sample_bundle["dataset_file"], }, "single_sample": { "sample_id": sample_id, "prompt": metadata.get("prompt"), "instruction": metadata.get("instruction"), "metadata": metadata, "resolved_input_path": str(input_path), "resolved_gt_path": str(gt_path), "input_copy": input_stats, "gt_copy": gt_stats, "noise_level": getattr(config.sample, "noise_level", None), "guidance_scale": getattr(config.sample, "eval_guidance_scale", None), "img_guidance_scale": getattr(config.sample, "eval_img_guidance_scale", None), "eval_num_steps": getattr(config.sample, "eval_num_steps", None), "sde_type": getattr(config.sample, "sde_type", None), "sft_only_output": sft_stats, "sft_contact_sheet": _file_record(sft_sheet) if sft_stats else None, "checkpoint_190_output": rl_stats, "checkpoint_190_contact_sheet": _file_record(rl_sheet) if rl_stats else None, }, "generation_status": { "skip_generation": bool(args.skip_generation), "cuda_available_at_runtime": torch.cuda.is_available(), "note": "Generated image fields are null when skip_generation is true.", }, } fingerprint_path = output_dir / "fingerprint.json" fingerprint_path.write_text(json.dumps(fingerprint, indent=2, sort_keys=True), encoding="utf-8") print(json.dumps(fingerprint, indent=2, sort_keys=True)) print(f"Wrote {fingerprint_path}") return 0 if __name__ == "__main__": raise SystemExit(main())