Download upstreams/langflow/eval_ppl.py from jyliuAI/diffusion_baseline: direct link, hf CLI and curl.
- Browser
- Download file 20.4 kB
-
https://huggingface.co/jyliuAI/diffusion_baseline/resolve/main/upstreams/langflow/eval_ppl.py
- Command line
-
hf download hf://jyliuAI/diffusion_baseline/upstreams/langflow/eval_ppl.py
-
curl -L -o eval_ppl.py https://huggingface.co/jyliuAI/diffusion_baseline/resolve/main/upstreams/langflow/eval_ppl.py
20.4 kB
| """EDLM Flow-based Perplexity Evaluation Script — Multi-GPU edition. | |
| Computes NLL via the probability flow ODE: | |
| - Reconstruction loss (CE at gamma_min) | |
| - Flow integral (ODE integration of divergence) | |
| - Prior loss (Gaussian log-prob at gamma_max) | |
| Multi-GPU usage: | |
| torchrun --nproc_per_node=<NUM_GPUS> eval_ppl.py ... | |
| Single-GPU usage: | |
| python eval_ppl.py -cn langflow-owt-small eval.checkpoint_path=... | |
| Features: | |
| - Results saved to JSON file in checkpoint directory | |
| """ | |
| import json | |
| import os | |
| from datetime import datetime | |
| from datetime import timedelta | |
| import hydra | |
| import lightning as L | |
| import omegaconf | |
| import torch | |
| import torch.distributed as dist | |
| import torch.nn.functional as F | |
| import torchdiffeq | |
| from tqdm import tqdm | |
| from duo import dataloader | |
| from duo import utils | |
| from langflow.model import LangFlow | |
| torch.set_float32_matmul_precision('high') | |
| omegaconf.OmegaConf.register_new_resolver("cwd", os.getcwd) | |
| omegaconf.OmegaConf.register_new_resolver("device_count", torch.cuda.device_count) | |
| omegaconf.OmegaConf.register_new_resolver("eval", eval) | |
| omegaconf.OmegaConf.register_new_resolver("div_up", lambda x, y: (x + y - 1) // y) | |
| # --------------------------------------------------------------------------- | |
| # Distributed helpers | |
| # --------------------------------------------------------------------------- | |
| def _is_distributed() -> bool: | |
| return dist.is_available() and dist.is_initialized() | |
| def _rank() -> int: | |
| return dist.get_rank() if _is_distributed() else 0 | |
| def _world_size() -> int: | |
| return dist.get_world_size() if _is_distributed() else 1 | |
| def _is_main_process() -> bool: | |
| return _rank() == 0 | |
| def _setup_distributed(): | |
| if "RANK" in os.environ and "WORLD_SIZE" in os.environ: | |
| dist.init_process_group( | |
| backend="nccl", | |
| timeout=timedelta(minutes=10) | |
| ) | |
| local_rank = int(os.environ["LOCAL_RANK"]) | |
| torch.cuda.set_device(local_rank) | |
| if _rank() == 0: | |
| print("NCCL initialized successfully") | |
| def _teardown_distributed(): | |
| if _is_distributed(): | |
| dist.destroy_process_group() | |
| def _gather_tensor(local_tensor: torch.Tensor) -> torch.Tensor: | |
| """All-gather a 1-D tensor from every rank and concatenate on rank-0. | |
| Args: | |
| local_tensor: 1-D float tensor of arbitrary length on the current rank. | |
| Returns: | |
| Concatenated tensor on rank-0; local_tensor unchanged on other ranks. | |
| """ | |
| if not _is_distributed(): | |
| return local_tensor | |
| torch.cuda.synchronize() | |
| world = _world_size() | |
| # Share lengths so we can handle uneven splits | |
| local_len = torch.tensor([local_tensor.numel()], device=local_tensor.device) | |
| all_lens = [torch.zeros_like(local_len) for _ in range(world)] | |
| dist.all_gather(all_lens, local_len) | |
| max_len = max(t.item() for t in all_lens) | |
| # Pad to common length then gather | |
| padded = torch.zeros(max_len, dtype=local_tensor.dtype, device=local_tensor.device) | |
| padded[: local_tensor.numel()] = local_tensor | |
| gathered = [torch.zeros_like(padded) for _ in range(world)] | |
| dist.all_gather(gathered, padded) | |
| if _is_main_process(): | |
| return torch.cat([gathered[r][: all_lens[r].item()] for r in range(world)]) | |
| return local_tensor # non-main ranks don't use this | |
| # --------------------------------------------------------------------------- | |
| # Model | |
| # --------------------------------------------------------------------------- | |
| class LangFlowForFlowNLL(LangFlow): | |
| """LangFlow with flow-based NLL computation.""" | |
| def _pred_x_and_div(self, z_t, gamma, use_self_cond=False, attention_mask=None): | |
| B, L, D = z_t.shape | |
| u = torch.randn_like(z_t) | |
| if attention_mask is not None: | |
| # Zero out padding positions so they don't contribute to the | |
| # Hutchinson divergence estimate or the ODE z-trajectory. | |
| token_mask = attention_mask[:, :, None].to(u.dtype) | |
| u = u * token_mask | |
| z = z_t.clone().detach().requires_grad_(True) | |
| gamma_batch = gamma.expand(B) | |
| x_self_cond = None | |
| if use_self_cond and self.config.self_conditioning: | |
| logits_sc = self.forward(noisy_embeds=z, timesteps=gamma_batch, x_self_cond=None, return_dict=False) | |
| probs_sc = F.softmax(logits_sc, dim=-1) | |
| x_self_cond = self._embed_tokens(probs_sc).detach() | |
| logits = self.forward(noisy_embeds=z, timesteps=gamma_batch, x_self_cond=x_self_cond, return_dict=False) | |
| probs = F.softmax(logits, dim=-1) | |
| x_reconst = self._embed_tokens(probs) | |
| if attention_mask is not None: | |
| x_reconst = x_reconst * token_mask | |
| grad = torch.autograd.grad( | |
| (u * x_reconst).sum(), z, create_graph=False, retain_graph=False | |
| )[0] | |
| div = (u * grad).sum(dim=[1, 2]) | |
| return x_reconst, div | |
| def compute_flow_nll(self, x0, n_steps=128, ode_method="euler", use_self_cond=True, | |
| attention_mask=None): | |
| B, L = x0.shape | |
| device = x0.device | |
| dtype = torch.float32 | |
| x_embed = self._embed_tokens(x0).to(dtype) | |
| D = x_embed.shape[2] | |
| N = L * D | |
| gamma_0 = torch.tensor(self.proposal.gamma_min, device=device, dtype=dtype) | |
| alpha_0 = torch.sigmoid(-gamma_0).sqrt() | |
| sigma_0 = torch.sigmoid(gamma_0).sqrt() | |
| eps = torch.randn_like(x_embed) | |
| z_0 = alpha_0 * x_embed + sigma_0 * eps | |
| z_0 = z_0.detach() | |
| # === 1. Reconstruction loss at gamma_0 === | |
| gamma_batch = gamma_0.expand(B) | |
| x_self_cond = None | |
| if use_self_cond and self.config.self_conditioning: | |
| with torch.no_grad(): | |
| logits_sc = self.forward(noisy_embeds=z_0, timesteps=gamma_batch, x_self_cond=None, return_dict=False) | |
| probs_sc = F.softmax(logits_sc, dim=-1) | |
| x_self_cond = self._embed_tokens(probs_sc).detach() | |
| logits = self.forward(noisy_embeds=z_0, timesteps=gamma_batch, x_self_cond=x_self_cond, return_dict=False) | |
| reconst_nll_per_token = F.cross_entropy( | |
| logits.flatten(0, 1), x0.flatten(), reduction="none" | |
| ).reshape(B, L).detach() | |
| if attention_mask is not None: | |
| reconst_nll = (reconst_nll_per_token * attention_mask.to(dtype)).sum(dim=1) | |
| else: | |
| reconst_nll = reconst_nll_per_token.sum(dim=1) | |
| # === 2. Flow integral via ODE === | |
| def ode_func(sqrt_snr, y): | |
| gamma = -2.0 * torch.log(sqrt_snr) | |
| sigma = torch.sigmoid(gamma).sqrt() | |
| z_scaled = y[:, 1:].reshape(B, L, D) | |
| z_t = z_scaled * sigma | |
| x_reconst, x_div = self._pred_x_and_div( | |
| z_t, gamma, use_self_cond=use_self_cond, attention_mask=attention_mask) | |
| div_scaled = sigma * x_div | |
| v = x_reconst.reshape(B, -1) | |
| return torch.cat([div_scaled.view(B, 1), v], dim=1).detach() | |
| y0 = torch.cat([ | |
| torch.zeros(B, 1, device=device, dtype=dtype), | |
| (z_0 / sigma_0).reshape(B, -1) | |
| ], dim=1) | |
| t_grid = torch.linspace(0.0, 1.0, n_steps + 1, device=device) | |
| t_grid = t_grid.clamp(min=1e-5, max=1.0 - 1e-5) | |
| gamma_grid = self.proposal(t_grid).to(dtype) | |
| sqrt_snr_grid = torch.exp(-0.5 * gamma_grid) | |
| y1 = torchdiffeq.odeint(ode_func, y0, sqrt_snr_grid, method=ode_method)[-1] | |
| flow_nll = -y1[:, 0].detach() | |
| # === 3. Prior loss at gamma_1 === | |
| z_scaled_final = y1[:, 1:].reshape(B, L, D) | |
| if attention_mask is not None: | |
| token_mask = attention_mask[:, :, None].to(dtype) | |
| num_valid_per_seq = attention_mask.sum(dim=1).to(dtype) # (B,) | |
| prior_nll = (0.5 * (z_scaled_final ** 2) * token_mask).sum(dim=[1, 2]) \ | |
| - num_valid_per_seq * D / 2 | |
| else: | |
| prior_nll = 0.5 * (z_scaled_final ** 2).sum(dim=[1, 2]) - N / 2 | |
| num_valid_tokens = ( | |
| attention_mask.sum(dim=1).long() | |
| if attention_mask is not None | |
| else torch.full((B,), L, dtype=torch.long, device=device) | |
| ) | |
| return { | |
| "nll": reconst_nll + flow_nll + prior_nll, | |
| "reconst_nll": reconst_nll, | |
| "flow_nll": flow_nll, | |
| "prior_nll": prior_nll, | |
| "num_valid_tokens": num_valid_tokens, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Checkpoint / config helpers | |
| # --------------------------------------------------------------------------- | |
| def _load_from_checkpoint(config, tokenizer): | |
| local_device = f"cuda:{_rank()}" | |
| model = LangFlowForFlowNLL.from_pretrained( | |
| config.eval.checkpoint_path, | |
| torch_dtype=torch.float32, | |
| ) | |
| model.to(local_device) | |
| return model | |
| # Build eval dataloader directly, bypassing get_dataloaders() which asserts | |
| # training batch-size constraints that don't apply to evaluation. | |
| # Does not use DistributedSampler — interleaved indices give each rank a | |
| # non-overlapping shard without padding/duplication. | |
| def _build_distributed_loader(config, tokenizer, rank, world_size): | |
| if config.data.valid in ['text8', 'lm1b', 'ag_news']: | |
| validation_split = 'test' | |
| else: | |
| validation_split = 'validation' | |
| dataset_kwargs = dict( | |
| dataset_name=config.data.valid, | |
| tokenizer=tokenizer, | |
| wrap=config.data.wrap, | |
| mode=validation_split, | |
| cache_dir=config.data.cache_dir, | |
| insert_eos=config.data.insert_valid_eos, | |
| block_size=config.model.length, | |
| streaming=config.data.streaming, | |
| num_proc=config.loader.num_workers, | |
| revision=config.data.get("valid_revision", None), | |
| ) | |
| if world_size > 1: | |
| # Rank 0 prepares/caches the dataset first; others wait, then load. | |
| if rank == 0: | |
| dataloader.get_dataset(**dataset_kwargs) | |
| dist.barrier() | |
| valid_set = dataloader.get_dataset(**dataset_kwargs) | |
| eval_batch_size = config.loader.eval_batch_size | |
| if (first_n := config.eval.get("first_n", None)) is not None: | |
| valid_set = torch.utils.data.Subset(valid_set, range(min(len(valid_set), first_n))) | |
| # Multi-GPU: interleaved, non-overlapping indices — no padding needed. | |
| # Uneven tails are handled naturally: some ranks get one fewer sample, | |
| # and _gather_tensor handles variable-length tensors across ranks. | |
| total = len(valid_set) | |
| indices = list(range(rank, total, world_size)) | |
| subset = torch.utils.data.Subset(valid_set, indices) | |
| return torch.utils.data.DataLoader( | |
| subset, | |
| batch_size=eval_batch_size, | |
| shuffle=False, | |
| num_workers=config.loader.num_workers, | |
| pin_memory=True, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Main eval function | |
| # --------------------------------------------------------------------------- | |
| def _eval_langflow_flow_ppl(config, logger, tokenizer): | |
| """Evaluate LangFlow perplexity using flow-based NLL — multi-GPU aware. | |
| Each GPU processes its own shard of the validation set in parallel. | |
| Per-device average NLL values are token-weighted when aggregating across | |
| ranks, so unequal last batches and uneven shards are handled correctly. | |
| """ | |
| nfe = getattr(config.eval, 'n_steps', 128) | |
| ode_method = getattr(config.eval, 'ode_method', 'euler') | |
| rank = _rank() | |
| world_size = _world_size() | |
| local_device = torch.device(f"cuda:{rank}") | |
| if _is_main_process(): | |
| logger.info("Starting LangFlow Flow-based PPL Evaluation.") | |
| logger.info(f"World size (GPUs): {world_size}") | |
| logger.info(f"NFE: {nfe}, ODE method: {ode_method}") | |
| # --- Load model (every rank gets its own copy, already on local_device) --- | |
| model = _load_from_checkpoint(config, tokenizer) | |
| model.eval() | |
| if _is_main_process(): | |
| logger.info(f"Self-conditioning: {'ENABLED' if model.config.self_conditioning else 'DISABLED'}") | |
| disable_ema = getattr(config.eval, 'disable_ema', False) | |
| if disable_ema and hasattr(model, 'ema'): | |
| model.ema = None | |
| if getattr(model, 'ema', None) is not None: | |
| if _is_main_process(): | |
| logger.info("Using EMA weights for evaluation.") | |
| model.ema.store(model.parameters()) | |
| model.ema.copy_to(model.parameters()) | |
| use_self_cond = config.eval.get("use_self_cond", model.config.self_conditioning) | |
| loader = _build_distributed_loader(config, tokenizer, rank, world_size) | |
| # Accumulate per-sequence NLL sums and total token counts. | |
| # Summing per-sequence NLL (not averaging) ensures unequal last-batch | |
| # sizes are handled correctly when computing the token-weighted average. | |
| local_nll_sum = 0.0 | |
| local_reconst_sum = 0.0 | |
| local_flow_sum = 0.0 | |
| local_prior_sum = 0.0 | |
| local_tokens = 0 | |
| local_num_seqs = 0 | |
| seq_len = None | |
| with tqdm(loader, desc=f"[rank {rank}] Flow PPL (NFE={nfe})", | |
| disable=not _is_main_process()) as pbar: | |
| for batch in pbar: | |
| x0 = batch['input_ids'].to(local_device) # (B, L) | |
| B, seq_len = x0.shape | |
| attention_mask = batch.get('attention_mask', None) | |
| if attention_mask is not None: | |
| attention_mask = attention_mask.to(local_device) | |
| losses = model.compute_flow_nll( | |
| x0, n_steps=nfe, ode_method=ode_method, use_self_cond=use_self_cond, | |
| attention_mask=attention_mask, | |
| ) | |
| # losses['nll'] etc. are per-sequence sums (shape: [B]) | |
| local_nll_sum += losses['nll'].detach().sum().item() | |
| local_reconst_sum += losses['reconst_nll'].detach().sum().item() | |
| local_flow_sum += losses['flow_nll'].detach().sum().item() | |
| local_prior_sum += losses['prior_nll'].detach().sum().item() | |
| local_tokens += losses['num_valid_tokens'].sum().item() | |
| local_num_seqs += B | |
| if _is_main_process(): | |
| pbar.set_postfix( | |
| {'nll/tok': local_nll_sum / local_tokens} | |
| ) | |
| torch.cuda.synchronize() | |
| if _is_distributed(): | |
| dist.barrier() | |
| # ------------------------------------------------------------------ | |
| # Gather per-rank (nll_sum, token_count) to rank-0, then compute the | |
| # global token-weighted average: | |
| # | |
| # global_avg_per_token = sum_r(nll_sum_r) / sum_r(tokens_r) | |
| # | |
| # Summing NLL sums (not per-rank averages) and dividing by total tokens | |
| # is equivalent to a single global mean over every token, regardless of | |
| # how samples are split across ranks or how large each last batch is. | |
| # ------------------------------------------------------------------ | |
| def _scalar_to_cuda(v): | |
| return torch.tensor([v], dtype=torch.float32, device=local_device) | |
| gathered_nll_sum = _gather_tensor(_scalar_to_cuda(local_nll_sum)) | |
| gathered_reconst_sum = _gather_tensor(_scalar_to_cuda(local_reconst_sum)) | |
| gathered_flow_sum = _gather_tensor(_scalar_to_cuda(local_flow_sum)) | |
| gathered_prior_sum = _gather_tensor(_scalar_to_cuda(local_prior_sum)) | |
| gathered_tokens = _gather_tensor(_scalar_to_cuda(local_tokens)) | |
| gathered_num_seqs = _gather_tensor(_scalar_to_cuda(local_num_seqs)) | |
| if not _is_main_process(): | |
| return {} | |
| # --- Compute token-weighted global averages on rank-0 --- | |
| total_tokens = gathered_tokens.sum().item() | |
| global_avg_nll_per_token = gathered_nll_sum.sum().item() / total_tokens | |
| global_avg_reconst_per_token = gathered_reconst_sum.sum().item() / total_tokens | |
| global_avg_flow_per_token = gathered_flow_sum.sum().item() / total_tokens | |
| global_avg_prior_per_token = gathered_prior_sum.sum().item() / total_tokens | |
| num_samples = int(gathered_num_seqs.sum().item()) | |
| avg_valid_tokens_per_seq = total_tokens / num_samples | |
| # Convert per-token NLL back to per-sequence for reporting | |
| global_avg_nll_per_seq = global_avg_nll_per_token * avg_valid_tokens_per_seq | |
| global_avg_reconst_per_seq = global_avg_reconst_per_token * avg_valid_tokens_per_seq | |
| global_avg_flow_per_seq = global_avg_flow_per_token * avg_valid_tokens_per_seq | |
| global_avg_prior_per_seq = global_avg_prior_per_token * avg_valid_tokens_per_seq | |
| ppl = torch.exp(torch.tensor(global_avg_nll_per_token)).item() | |
| loc = model.proposal.loc | |
| scale = model.proposal.scale | |
| gumbel_loc = loc.item() if hasattr(loc, 'item') else float(loc) | |
| gumbel_scale = scale.item() if hasattr(scale, 'item') else float(scale) | |
| print("=" * 60) | |
| print("LangFlow Flow-based PPL Evaluation Results (Multi-GPU)") | |
| print("=" * 60) | |
| print(f"Checkpoint: {config.eval.checkpoint_path}") | |
| print(f"GPUs used: {world_size}") | |
| print(f"NFE: {nfe}, ODE method: {ode_method}") | |
| print(f"Self-conditioning: {'ENABLED' if model.self_conditioning else 'DISABLED'}") | |
| print(f"Gumbel loc: {gumbel_loc:.4f}, scale: {gumbel_scale:.4f}") | |
| print(f"Samples: {num_samples}, Seq len: {seq_len}") | |
| print("-" * 60) | |
| print(f"Reconstruction NLL (per seq): {global_avg_reconst_per_seq:.4f}") | |
| print(f"Flow NLL (per seq): {global_avg_flow_per_seq:.4f}") | |
| print(f"Prior NLL (per seq): {global_avg_prior_per_seq:.4f}") | |
| print(f"Total NLL (per seq): {global_avg_nll_per_seq:.4f}") | |
| print(f"NLL (per token): {global_avg_nll_per_token:.4f}") | |
| print(f"Perplexity: {ppl:.4f}") | |
| print("=" * 60) | |
| return { | |
| 'nll_per_seq': global_avg_nll_per_seq, | |
| 'nll_per_token': global_avg_nll_per_token, | |
| 'ppl': ppl, | |
| 'reconst_nll': global_avg_reconst_per_seq, | |
| 'flow_nll': global_avg_flow_per_seq, | |
| 'prior_nll': global_avg_prior_per_seq, | |
| 'nfe': nfe, | |
| 'ode_method': ode_method, | |
| 'num_samples': num_samples, | |
| 'world_size': world_size, | |
| 'seq_len': seq_len, | |
| 'gumbel_loc': gumbel_loc, | |
| 'gumbel_scale': gumbel_scale, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Logging helpers | |
| # --------------------------------------------------------------------------- | |
| def _save_results_json(results, config, logger): | |
| """Save evaluation results to a JSON file in the checkpoint directory.""" | |
| checkpoint_path = config.eval.checkpoint_path | |
| checkpoint_dir = os.path.dirname(checkpoint_path) | |
| checkpoint_name = os.path.splitext(os.path.basename(checkpoint_path))[0] | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| nfe = results.get('nfe', 128) | |
| ode_method = results.get('ode_method', 'euler') | |
| output_filename = f"flow_ppl_{checkpoint_name}_nfe{nfe}_{ode_method}_{timestamp}.json" | |
| output_path = os.path.join(checkpoint_dir, output_filename) | |
| results_with_config = { | |
| 'results': results, | |
| 'config': { | |
| 'checkpoint_path': checkpoint_path, | |
| 'seed': int(config.seed), | |
| 'data': config.data.name if hasattr(config.data, 'name') else str(config.data), | |
| 'model_length': int(config.model.length), | |
| 'batch_size': int(config.loader.eval_batch_size), | |
| }, | |
| 'timestamp': timestamp, | |
| } | |
| os.makedirs(checkpoint_dir, exist_ok=True) | |
| with open(output_path, 'w') as f: | |
| json.dump(results_with_config, f, indent=2) | |
| logger.info(f"Results saved to: {output_path}") | |
| print(f"Results saved to: {output_path}") | |
| return output_path | |
| # --------------------------------------------------------------------------- | |
| # Entry point | |
| # --------------------------------------------------------------------------- | |
| def main(config): | |
| _setup_distributed() | |
| try: | |
| L.seed_everything(config.seed + _rank()) | |
| logger = utils.get_logger(__name__) | |
| tokenizer = dataloader.get_tokenizer(config) | |
| results = _eval_langflow_flow_ppl(config, logger, tokenizer) | |
| # Only rank-0 saves results | |
| if _is_main_process() and results: | |
| _save_results_json(results, config, logger) | |
| finally: | |
| _teardown_distributed() | |
| return results | |
| if __name__ == "__main__": | |
| main() |