multimodalart's picture
multimodalart HF Staff
Initial UniSpace demo
2c8150b verified
Raw History Blame Contribute Delete
23.1 kB
from .ref_iqa import calculate_psnr, calculate_lpips, calculate_ssim
from .fid import calculate_rfid, calculate_gfid
import numpy as np
import torch
import torch.nn.functional as F
from PIL import Image
from torch.cuda.amp import autocast
from typing import Union, Dict, Optional
try:
from accelerate import Accelerator
except ImportError:
Accelerator = None
from torch.utils.data import DataLoader, Subset
from tqdm import tqdm
import os
import sys
from functools import lru_cache
@lru_cache(maxsize=4)
def _load_encoder_cached(model_path: str, device_str: str):
"""
懒加载并缓存 encoder(按 model_path + device 缓存,进程内只加载一次)。
使用 transformers.AutoModel + AutoProcessor。
"""
from transformers import AutoModel, AutoProcessor
print(f"[CosSim] Loading encoder from {model_path} onto {device_str} (cached)...")
processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
model = AutoModel.from_pretrained(model_path, trust_remote_code=True)
model = model.to(device_str).eval()
return model, processor
@torch.no_grad()
def calculate_encoder_cos_sim(
ref_arr: np.ndarray,
rec_arr: np.ndarray,
encoder_model: str,
device: torch.device,
batch_size: int = 64,
) -> float:
"""
用指定 encoder 对 GT 图像和重建图像编码,计算 image embedding 的 cos 相似度均值。
Args:
ref_arr: GT 图像 [N, H, W, C] uint8
rec_arr: 重建图像 [N, H, W, C] uint8
encoder_model: HuggingFace 模型路径或名称
device: 计算设备
batch_size: 每批编码的图像数
Returns:
所有样本的 cos 相似度均值 (float)
"""
device_str = "cuda" if device.type == "cuda" else "cpu"
model, processor = _load_encoder_cached(encoder_model, device_str)
N = len(ref_arr)
cos_sims = []
for start in range(0, N, batch_size):
end = min(start + batch_size, N)
ref_batch = [Image.fromarray(img) for img in ref_arr[start:end]]
rec_batch = [Image.fromarray(img) for img in rec_arr[start:end]]
# 预处理:使用 processor 处理两组图像
ref_inputs = processor(images=ref_batch, return_tensors="pt").to(device_str)
rec_inputs = processor(images=rec_batch, return_tensors="pt").to(device_str)
# 提取 image embedding
ref_feats = model.get_image_features(**ref_inputs) # [B, D]
rec_feats = model.get_image_features(**rec_inputs) # [B, D]
# L2 归一化后计算 cos 相似度
ref_feats = F.normalize(ref_feats.float(), dim=-1)
rec_feats = F.normalize(rec_feats.float(), dim=-1)
batch_cos = (ref_feats * rec_feats).sum(dim=-1) # [B]
cos_sims.append(batch_cos.cpu())
cos_sim_mean = torch.cat(cos_sims).mean().item()
return cos_sim_mean
def compute_reconstruction_metrics(
ref_arr: np.ndarray,
rec_arr: np.ndarray,
device: torch.device,
batch_size: int = 128,
metrics_to_compute = ("psnr", "ssim", "rfid"),
disable_bar: bool = True,
encoder_model: Optional[str] = None,
psnr_resize: Optional[int] = None,
l1_resize: Optional[int] = None,
) -> Dict[str, float]:
"""
Compute reconstruction metrics between reference and reconstructed images.
Args:
ref_arr: Reference images [N, H, W, C] uint8
rec_arr: Reconstructed images [N, H, W, C] uint8
device: Device for computation
batch_size: Batch size for metric computation
Returns:
Dictionary with metrics: eval/psnr, eval/ssim, eval/rfid
Note: LPIPS is not computed here since it's already tracked during training
"""
device_str = "cuda" if device.type == "cuda" else "cpu"
# For paired metrics (psnr, ssim, l1, cos_sim), align ref/rec count
n_paired = min(len(ref_arr), len(rec_arr))
ref_paired = ref_arr[:n_paired]
rec_paired = rec_arr[:n_paired]
results_dict = {}
if 'psnr' in metrics_to_compute:
psnr = calculate_psnr(ref_paired, rec_paired, batch_size, device_str, disable_bar=disable_bar, psnr_resize=psnr_resize)
results_dict["psnr"] = psnr
if 'ssim' in metrics_to_compute:
ssim = calculate_ssim(ref_paired, rec_paired, batch_size, device_str, disable_bar=disable_bar)
results_dict["ssim"] = ssim
if 'rfid' in metrics_to_compute:
rfid = calculate_rfid(ref_arr, rec_arr, batch_size, device_str)
results_dict["rfid"] = rfid
if 'l1' in metrics_to_compute:
# L1 = mean(|rec - ref|), same as train: (recon - images).abs().mean()
ref_f = ref_paired.astype(np.float32) / 255.0
rec_f = rec_paired.astype(np.float32) / 255.0
if l1_resize is not None:
# resize via torch interpolate to keep consistent with psnr_resize style
# Process in small batches to avoid CUDA OOM on large datasets
device_str = "cuda" if device.type == "cuda" else "cpu"
resize_batch = 64
n = ref_f.shape[0]
l1_sum = 0.0
l1_count = 0
for i in range(0, n, resize_batch):
ref_b = torch.from_numpy(ref_f[i:i+resize_batch]).permute(0, 3, 1, 2).to(device_str)
rec_b = torch.from_numpy(rec_f[i:i+resize_batch]).permute(0, 3, 1, 2).to(device_str)
ref_b = F.interpolate(ref_b, size=(l1_resize, l1_resize), mode="bilinear", align_corners=False)
rec_b = F.interpolate(rec_b, size=(l1_resize, l1_resize), mode="bilinear", align_corners=False)
l1_sum += (ref_b - rec_b).abs().sum().item()
l1_count += ref_b.numel()
l1 = l1_sum / l1_count
else:
l1 = np.abs(rec_f - ref_f).mean()
results_dict["l1"] = float(l1)
if 'cos_sim' in metrics_to_compute:
assert encoder_model is not None, \
"'cos_sim' metric requires encoder_model (HuggingFace model path) to be specified."
print(f"[CosSim] Computing encoder cos_sim with model: {encoder_model}")
cos_sim = calculate_encoder_cos_sim(
ref_paired, rec_paired,
encoder_model=encoder_model,
device=device,
batch_size=batch_size,
)
results_dict["cos_sim"] = cos_sim
assert len(results_dict) > 0, "No metrics were computed."
return results_dict
def compute_generation_metrics(
ref_arr: np.ndarray,
rec_arr: np.ndarray,
device: torch.device,
batch_size: int = 128,
):
device_str = "cuda" if device.type == "cuda" else "cpu"
# only eval FID
fid = calculate_gfid(rec_arr, ref_arr, batch_size, device_str)
return {
'fid': fid
}
@torch.no_grad()
def evaluate_generation_distributed(
model_fn,
sample_fn,
latent_size, # for noise
additional_model_kwargs,
use_guidance: bool,
pr,
val_dataset,
num_samples: int,
batch_size: int,
rank: Union[int, None] = None,
world_size: Union[int, None] = None,
device: torch.device = None,
experiment_dir: str = None,
global_step: int = None,
autocast_kwargs: dict = None,
metric_batch_size: int = 128,
reference_npz_path: Optional[str] = None,
accelerator: Union[Accelerator, None] = None,
) -> Optional[Dict[str, float]]:
"""
Evaluate generation metrics using all GPUs in a distributed manner.
Args:
val_dataset: Validation dataset
batch_size: Batch size per GPU for generation
rank: Current GPU rank (deprecated, use accelerator instead)
world_size: Total number of GPUs (deprecated, use accelerator instead)
device: Device to use (deprecated, use accelerator instead)
experiment_dir: Experiment directory
global_step: Current training step
autocast_kwargs: Autocast configuration
metric_batch_size: Batch size for metric computation (on rank 0)
reference_npz_path: Optional path to existing reference NPZ file
accelerator: Accelerator instance (preferred over rank/world_size/device)
Returns:
Dictionary of metrics (only on rank 0, None on other ranks)
"""
# Support both Accelerator and legacy DDP interface
if accelerator is not None:
rank = accelerator.process_index
world_size = accelerator.num_processes
device = accelerator.device
is_main_process = accelerator.is_main_process
wait_for_everyone = accelerator.wait_for_everyone
else:
# Legacy DDP mode
import torch.distributed as dist
if rank is None or world_size is None or device is None:
raise ValueError("Either accelerator must be provided, or rank/world_size/device must be provided")
is_main_process = (rank == 0)
wait_for_everyone = dist.barrier
# model.eval()
# Save shard NPZ
temp_dir = os.path.join(experiment_dir, "eval_npzs")
if is_main_process:
print(f"\n[Eval] Starting distributed sampling evaluation at step {global_step}")
os.makedirs(temp_dir, exist_ok=True)
# Wait for rank 0 to create the directory before other ranks try to save
wait_for_everyone()
# print(f"[Rank {rank}] Starting sampling...")
# Each rank processes its shard
N = min(len(val_dataset), num_samples)
chunk = N // world_size
if rank < world_size - 1:
start = rank * chunk
end = (rank + 1) * chunk
else:
# Last rank takes the remainder (and handles N < world_size gracefully)
start = rank * chunk
end = N
rank_indices = list(range(start, end))
subset = Subset(val_dataset, rank_indices)
loader = DataLoader(
subset,
batch_size=batch_size,
shuffle=False,
num_workers=4,
pin_memory=True,
drop_last=False,
)
# Reconstruct images on this rank
generations = []
iterator = tqdm(loader, desc=f"[Rank {rank}] Sampling", file=sys.stdout) if is_main_process else loader
with torch.inference_mode():
for _, label in iterator: # don't actually need images at sampling time
n = label.size(0)
z = torch.randn(n, *latent_size, device = device)
y = label.to(device)
if use_guidance:
z = torch.cat([z, z], dim=0)
y_null = torch.full((n,), null_label, device=device)
y = torch.cat([y, y_null], dim=0)
model_kwargs = dict(y=y, **additional_model_kwargs)
with autocast(**autocast_kwargs):
samples = sample_fn(z, model_fn, **model_kwargs)[-1]
if use_guidance:
samples = samples.chunk(2, dim = 0)
if callable(pr) and not hasattr(pr, 'decode'):
samples = pr(samples)
else:
samples = pr.decode(samples)
if hasattr(samples, 'x_rec'):
samples = samples.x_rec
samples = samples.clamp(0, 1)
gen_np = samples.mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
for img in gen_np:
generations.append(img)
generations = np.stack(generations)
shard_path = os.path.join(temp_dir, f"gen_{global_step:07d}_{rank:02d}.npz")
np.savez(shard_path, arr_0=generations)
if is_main_process:
print(f"[Rank {rank}] Saved {len(generations)} generation to {shard_path}")
# Wait for all ranks to finish generation
wait_for_everyone()
# Rank 0 computes metrics
metrics = None
if is_main_process:
# Combine all generation shards
all_gens = []
for r in range(world_size):
shard_file = os.path.join(temp_dir, f"gen_{global_step:07d}_{r:02d}.npz")
shard_data = np.load(shard_file)["arr_0"]
all_gens.append(shard_data)
combined_recons = np.concatenate(all_gens, axis=0)[:num_samples]
print(f"[Eval] Combined generation NPZ shape: {combined_recons.shape}")
# Load reference NPZ
ref_npz_path = reference_npz_path
if not os.path.exists(ref_npz_path):
raise FileNotFoundError(f"Reference NPZ not found at {ref_npz_path}")
ref_stats = np.load(ref_npz_path)
print(f"[Eval] Loaded reference NPZ from {ref_npz_path}")
# Compute metrics
print("[Eval] Computing metrics...")
metrics = compute_generation_metrics(
ref_stats,
combined_recons,
device,
metric_batch_size,
)
# Print results
print(f"[Eval] Step {global_step} Metrics:")
for key, value in metrics.items():
print(f" {key}: {value:.6f}")
# Cleanup reconstruction shards
for r in range(world_size):
shard_file = os.path.join(temp_dir, f"gen_{global_step:07d}_{r:02d}.npz")
if os.path.exists(shard_file):
os.remove(shard_file)
wait_for_everyone()
return metrics
@torch.no_grad()
def evaluate_reconstruction_distributed(
model,
val_dataset,
num_samples: int,
batch_size: int,
rank: Union[int, None] = None,
world_size: Union[int, None] = None,
device: torch.device = None,
experiment_dir: str = None,
global_step: int = None,
autocast_kwargs: dict = None,
metric_batch_size: int = 128,
reference_npz_path: Optional[str] = None,
metrics_to_compute: Optional[list] = ("psnr", "ssim", "rfid"),
accelerator: Union[Accelerator, None] = None,
save_recon_dir: Optional[str] = None,
image_features_mode: Optional[str] = None,
encoder_cos_sim_model: Optional[str] = None,
psnr_resize: Optional[int] = None,
l1_resize: Optional[int] = None,
) -> Optional[Dict[str, float]]:
"""
Evaluate reconstruction metrics using all GPUs in a distributed manner.
Args:
model: Model to evaluate (should be in eval mode)
val_dataset: Validation dataset
batch_size: Batch size per GPU for reconstruction
rank: Current GPU rank (deprecated, use accelerator instead)
world_size: Total number of GPUs (deprecated, use accelerator instead)
device: Device to use (deprecated, use accelerator instead)
experiment_dir: Experiment directory
global_step: Current training step
autocast_kwargs: Autocast configuration
metric_batch_size: Batch size for metric computation (on rank 0)
reference_npz_path: Optional path to existing reference NPZ file
accelerator: Accelerator instance (preferred over rank/world_size/device)
Returns:
Dictionary of metrics (only on rank 0, None on other ranks)
"""
# Support both Accelerator and legacy DDP interface
if accelerator is not None:
rank = accelerator.process_index
world_size = accelerator.num_processes
device = accelerator.device
is_main_process = accelerator.is_main_process
wait_for_everyone = accelerator.wait_for_everyone
else:
# Legacy DDP mode
import torch.distributed as dist
if rank is None or world_size is None or device is None:
raise ValueError("Either accelerator must be provided, or rank/world_size/device must be provided")
is_main_process = (rank == 0)
wait_for_everyone = dist.barrier
model.eval()
# Save shard NPZ
temp_dir = os.path.join(experiment_dir, "eval_npzs")
if is_main_process:
print(f"\n[Eval] Starting distributed reconstruction evaluation at step {global_step}")
os.makedirs(temp_dir, exist_ok=True)
# Wait for rank 0 to create the directory before other ranks try to save
wait_for_everyone()
# print(f"[Rank {rank}] Starting reconstruction...")
# Each rank processes its shard
N = min(len(val_dataset), num_samples)
chunk = N // world_size
if rank < world_size - 1:
start = rank * chunk
end = (rank + 1) * chunk
else:
# Last rank takes the remainder (and handles N < world_size gracefully)
start = rank * chunk
end = N
rank_indices = list(range(start, end))
subset = Subset(val_dataset, rank_indices)
loader = DataLoader(
subset,
batch_size=batch_size,
shuffle=False,
num_workers=4,
pin_memory=True,
drop_last=False,
)
# Reconstruct images on this rank, keep originals for distributed metrics
reconstructions = []
originals = []
iterator = tqdm(loader, desc=f"[Rank {rank}] Reconstructing", file=sys.stdout) if is_main_process else loader
with torch.inference_mode():
for images, _ in iterator:
images = images.to(device, non_blocking=True)
# Save originals before model forward
orig_np = images.clamp(0, 1).mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
with autocast(**autocast_kwargs):
if image_features_mode is not None:
recon = model(images, image_features_mode=image_features_mode)
else:
recon = model(images)
# Handle DecodeOutput from split_adaln decoder
if hasattr(recon, 'x_rec'):
recon = recon.x_rec
# Convert to numpy uint8 [H, W, C]
recon = recon.clamp(0, 1)
recon_np = recon.mul(255).permute(0, 2, 3, 1).to("cpu", dtype=torch.uint8).numpy()
for img in recon_np:
reconstructions.append(img)
for img in orig_np:
originals.append(img)
reconstructions = np.stack(reconstructions)
originals = np.stack(originals)
# Save shard for backward compat
shard_path = os.path.join(temp_dir, f"recon_{global_step:07d}_{rank:02d}.npz")
np.savez(shard_path, arr_0=reconstructions)
if is_main_process:
print(f"[Rank {rank}] Saved {len(reconstructions)} reconstructions to {shard_path}")
# Wait for all ranks to finish reconstruction
wait_for_everyone()
# ── Distributed metrics: each rank computes on its own subset ──────────
import torch.distributed as dist
from .fid import compute_inception_features, calculate_rfid_from_features
local_n = len(reconstructions)
# PSNR / SSIM: compute locally then all_reduce weighted sum
scalar_metrics_to_compute = [m for m in (metrics_to_compute or []) if m in ('psnr', 'ssim', 'lpips')]
local_scalar = {}
if scalar_metrics_to_compute:
if is_main_process:
print(f"[Eval] Rank {rank}: computing {scalar_metrics_to_compute} on {local_n} images...")
local_scalar = compute_reconstruction_metrics(
originals, reconstructions, device, metric_batch_size,
metrics_to_compute=scalar_metrics_to_compute,
disable_bar=is_main_process,
encoder_model=encoder_cos_sim_model,
psnr_resize=psnr_resize,
l1_resize=l1_resize,
)
# Weighted all_reduce: sum(metric * count) / total_count
global_scalar = {}
count_t = torch.tensor(float(local_n), device=device)
distributed = world_size > 1 and dist.is_available() and dist.is_initialized()
if distributed:
dist.all_reduce(count_t, op=dist.ReduceOp.SUM)
for k, v in local_scalar.items():
val_t = torch.tensor(v * local_n, dtype=torch.float64, device=device)
if distributed:
dist.all_reduce(val_t, op=dist.ReduceOp.SUM)
global_scalar[k] = (val_t / count_t).item()
# rFID: each rank computes inception features, all_gather, rank 0 computes FID
global_rfid = {}
if 'rfid' in (metrics_to_compute or []):
if is_main_process:
print(f"[Eval] Rank {rank}: computing inception features on {local_n} images...")
local_recon_feats = compute_inception_features(reconstructions, metric_batch_size, device)
local_ref_feats = compute_inception_features(originals, metric_batch_size, device)
recon_t = torch.from_numpy(local_recon_feats).to(device)
ref_t = torch.from_numpy(local_ref_feats).to(device)
if distributed:
all_recon = [torch.zeros_like(recon_t) for _ in range(world_size)]
all_ref = [torch.zeros_like(ref_t) for _ in range(world_size)]
dist.all_gather(all_recon, recon_t)
dist.all_gather(all_ref, ref_t)
else:
all_recon = [recon_t]
all_ref = [ref_t]
if is_main_process:
combined_recon_feats = torch.cat(all_recon, dim=0).cpu().double().numpy()
combined_ref_feats = torch.cat(all_ref, dim=0).cpu().double().numpy()
rfid = calculate_rfid_from_features(combined_recon_feats, combined_ref_feats)
global_rfid = {'rfid': rfid}
metrics = None
if is_main_process:
metrics = {**global_scalar, **global_rfid}
# Persist the all-reduced count so release validation proves how many
# examples were actually processed rather than trusting CLI intent.
metrics["_evaluated_samples"] = int(count_t.item())
print(f"[Eval] Step {global_step} Metrics:")
for key, value in metrics.items():
print(f" {key}: {value:.6f}")
# Optionally save reconstructed images
if save_recon_dir:
import threading
recons_copy = np.copy(reconstructions)
save_dir = save_recon_dir
def _save_recon_images():
os.makedirs(save_dir, exist_ok=True)
for i, img in enumerate(recons_copy):
Image.fromarray(img).save(os.path.join(save_dir, f"recon_{i:06d}.png"))
print(f"[Eval] Saved {len(recons_copy)} reconstruction images to {save_dir}")
threading.Thread(target=_save_recon_images, daemon=False).start()
# Cleanup shards
for r in range(world_size):
shard_file = os.path.join(temp_dir, f"recon_{global_step:07d}_{r:02d}.npz")
if os.path.exists(shard_file):
os.remove(shard_file)
wait_for_everyone()
model.train()
return metrics
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--ref-img", type=str, default="samples/imagenet-256-val.npz")
parser.add_argument("--rec-img", type=str, default="samples/sdvae-ft-mse-f8d4.npz")
parser.add_argument("--bs", type=int, default=128)
args = parser.parse_args()
# Load images
device = "cuda"
ref_img = np.load(args.ref_img)["arr_0"]
rec_img = np.load(args.rec_img)["arr_0"]
print(f"Loaded images: ref: {ref_img.shape}, rec: {rec_img.shape}")
psnr = calculate_psnr(ref_img, rec_img, args.bs, device)
print(f"PSNR: {psnr:.6f}")
lpips = calculate_lpips(ref_img, rec_img, args.bs, device)
print(f"LPIPS: {lpips:.6f}")
ssim_val = calculate_ssim(ref_img, rec_img, args.bs, device)
print(f"SSIM: {ssim_val:.6f}")
rfid = calculate_rfid(ref_img, rec_img, args.bs, device)
print(f"rFID: {rfid:.6f}")