Download upstreams/duo/discrete_diffusion_harness.py from jyliuAI/diffusion_baseline: direct link, hf CLI and curl.
- Browser
- Download file 15.1 kB
-
https://huggingface.co/jyliuAI/diffusion_baseline/resolve/main/upstreams/duo/discrete_diffusion_harness.py
- Command line
-
hf download hf://jyliuAI/diffusion_baseline/upstreams/duo/discrete_diffusion_harness.py
-
curl -L -o discrete_diffusion_harness.py https://huggingface.co/jyliuAI/diffusion_baseline/resolve/main/upstreams/duo/discrete_diffusion_harness.py
15.1 kB
| import torch | |
| from omegaconf import OmegaConf | |
| from lm_eval.api.model import LM | |
| from lm_eval.api.registry import register_model | |
| from lm_eval.__main__ import cli_evaluate | |
| from lm_eval.api.instance import Instance | |
| from datasets import Dataset | |
| from tqdm import tqdm | |
| import numpy as np | |
| import algo | |
| import dataloader | |
| """ | |
| # Instructions on running the eval | |
| ## What is the script doing? | |
| The script evaluates (or approximates) the log-likelihood of | |
| prefix + suffix. The script will load a checkpoint, and | |
| depending on the config, use the corresponding class | |
| (e.g. mdlm, duo, ar, etc). | |
| - For MCQ, the log-likelihood of all continuations given the | |
| same prefix is evaluated. The most likely continuation is | |
| selected as the "correct" answer according to the model. | |
| - For lambada_openai, the model is correct if the true | |
| continuation is generated as the argmax. For diffusion, we | |
| inject noise in the continuation, and check whether the true | |
| answer computed in a single forward pass is the most likely. | |
| This is naturally favoring AR models, as they run one forward | |
| pass per token, while diffusion use a single pass for all | |
| tokens for simplicity. Therefore, I usually only compare on | |
| MCQ since it is more fair. | |
| To run the script, you need to install the lm-eval-harness package: | |
| ``` | |
| pip install git+https://github.com/EleutherAI/lm-evaluation-harness | |
| ``` | |
| ## Tasks that we tested with | |
| **MCQ**: arc_easy, arc_challenge, hellaswag, winogrande, | |
| boolq, openbookqa, race, social_iqa, mathqa, piqa | |
| **Likelihood based**: lambada_openai | |
| ## Batch size that fits at the small scale | |
| - boolq -> 64 | |
| - openbookqa -> 256 | |
| - race -> 32 | |
| - social_iqa -> 512 | |
| - winogrande -> 64 | |
| - mathqa -> 64 | |
| - lambada_openai -> 64 | |
| - arc_easy -> 256 | |
| - arc_challenge -> 256 | |
| - hellaswag -> 64 | |
| - piqa -> 64 | |
| ## Important flags | |
| --trust_remote_code -> some datasets execute code when loading | |
| from huggingface. Without this flag, | |
| the script might crash. | |
| --batch_size -> max. num elements to use in parallel | |
| to eval the likelihood (for diffusion). | |
| For simplicity, inputs are NOT padded | |
| and batched for AR, though it should | |
| be fairly easy to add. | |
| --tasks -> one or multiple comma-separated tasks | |
| to evaluate on. | |
| --model_args -> string (without spaces) that contains | |
| the arguments to pass to the evaluator | |
| (stuff like checkpoints path, number | |
| of MC samples to evaluate the | |
| likelihood, path to the sentencepiece | |
| tokenizer, etc). | |
| --output_path -> path to a json file where the evaluation | |
| results will be saved, instead of | |
| only being printed in the terminal | |
| (they will always be printed). | |
| --limit -> debugging flag; limit the number of | |
| examples to a fixed amount, instead | |
| of using the whole dataset | |
| ## Example commands | |
| ### Run a single task with an AR model | |
| python discrete_diffusion_harness.py \ | |
| --tasks arc_easy \ | |
| --batch_size 256 \ | |
| --model dlm \ | |
| --model_args checkpoint_path=/home/username/baselines/ar/1M.ckpt \ | |
| --output_path ./harness_results/ar/1M/arc_easy.ckpt | |
| ### Run a single task with an MDLM model, and 2048 MC samples to approximate the likelihood | |
| python discrete_diffusion_harness.py \ | |
| --tasks arc_easy \ | |
| --model dlm \ | |
| --batch_size 256 \ | |
| --model_args checkpoint_path=/home/username/baselines/mdlm/1M.ckpt,num_mc_samples=2048 \ | |
| --output_path ./harness_results/mdlm/1M/arc_easy.ckpt | |
| ### Debug with 20 examples only | |
| python discrete_diffusion_harness.py \ | |
| --tasks arc_easy \ | |
| --model dlm \ | |
| --batch_size 256 \ | |
| --model_args checkpoint_path=/home/username/baselines/mdlm/1M.ckpt,num_mc_samples=2048 \ | |
| --limit 20 \ | |
| --output_path ./harness_results/mdlm/1M/arc_easy.ckpt | |
| ### Run the benchmarks from "The Diffusion Duality (Chapter 2)": | |
| (Arc-e, Arc-c, HSwag, WinoG, PIQA, MathQA, OQA) | |
| -> just change the checkpoint to evaluate mdlm, ar, or duo | |
| for task_config in "arc_easy 256" "arc_challenge 256" "hellaswag 64" "winogrande 64" "piqa 64" "mathqa 64" "openbookqa 256"; do | |
| task=$(echo $task_config | cut -d' ' -f1) | |
| batch_size=$(echo $task_config | cut -d' ' -f2) | |
| python discrete_diffusion_harness.py \ | |
| --batch_size $batch_size \ | |
| --tasks $task \ | |
| --model dlm \ | |
| --model_args checkpoint_path=/path/to/checkpoint.ckpt \ | |
| --output_path ./harness_results/duo/$task.json | |
| done | |
| """ | |
| def requests_to_dataset(config, requests, tokenizer, num_proc): | |
| def _tokenize(e): | |
| eos_idx = tokenizer.eos_token_id | |
| bos_idx = tokenizer.bos_token_id | |
| prefix_tokens = tokenizer(e['prefix'], | |
| return_attention_mask=False, | |
| add_special_tokens=False | |
| )['input_ids'] | |
| target_tokens = tokenizer(e['target'], | |
| return_attention_mask=False, | |
| add_special_tokens=False | |
| )['input_ids'] | |
| prefix_tokens = [bos_idx] + prefix_tokens | |
| target_tokens = target_tokens + [eos_idx] | |
| return { | |
| 'prefix_text': e['prefix'], | |
| 'target_text': e['target'], | |
| 'prefix': prefix_tokens, | |
| 'target': target_tokens, | |
| } | |
| ds = [] | |
| ds = [{'prefix': req.args[0], 'target': req.args[1]} | |
| for req in requests] | |
| ds = Dataset.from_list(ds) | |
| ds = ds.map(_tokenize, num_proc=num_proc) | |
| ds = ds.with_format('torch') | |
| seq_lenths = [len(x['prefix']) + len(x['target']) | |
| for x in ds] | |
| num_larger = len([x for x in seq_lenths | |
| if x > config.model.length]) | |
| if num_larger > 0: | |
| print(f'\033[91mThere are some examples that are longer ' | |
| f'than the context length, they will be ignored ' | |
| f'during evaluation. Number of such sequences: ' | |
| f'{num_larger}\033[0m') | |
| return ds | |
| def _eval_suffix_nll_generators(config, module, prefix, | |
| suffix, batch_size, num_samples, loss_avg_mode): | |
| device = module.device | |
| assert num_samples % batch_size == 0 | |
| full_sentence = torch.cat([prefix, suffix], dim=-1 | |
| ).repeat(batch_size, 1).to(module.device) | |
| all_ts = module._sample_t(num_samples, accum_step=None) | |
| for idx in range(0, num_samples, batch_size): | |
| t = all_ts[idx:idx+batch_size].unsqueeze(-1) | |
| dalpha_t, alpha_t = module.noise(t) | |
| alpha_t = alpha_t.to(device) | |
| sigma = module._sigma_from_alphat(alpha_t) | |
| x0 = full_sentence.to(device) | |
| # Inject noise | |
| xt = module.q_xt(full_sentence, alpha_t).to(device) | |
| if loss_avg_mode == 'full': | |
| pass # nothing to do | |
| elif loss_avg_mode == 'suffix': | |
| xt[:, :len(prefix)] = prefix | |
| # recompute alpha_t based on number of masked tokens, | |
| # for conditioning of the backbone | |
| alpha_t = (xt == x0).float().mean(dim=1)[:, None] | |
| t = module.noise.get_t_for_alpha(alpha_t) | |
| # We need to get dalpha_t for the loss: | |
| alpha_t = alpha_t.to(device) | |
| sigma = module._sigma_from_alphat(alpha_t) | |
| else: | |
| raise ValueError(loss_avg_mode) | |
| yield xt, x0, t, sigma, alpha_t, dalpha_t | |
| def eval_suffix_nll(config, module, prefix, suffix, batch_size, | |
| num_samples, loss_avg_mode): | |
| if config.algo.name in ('mdlm', 'duo', 'duo_base', | |
| 'distillation', 'ot-finetune'): | |
| return eval_suffix_nll_diffusion(config, module, prefix, | |
| suffix, batch_size, num_samples, loss_avg_mode) | |
| elif config.algo.name == 'ar': | |
| return eval_suffix_nll_ar(config, module, prefix, | |
| suffix, batch_size, num_samples, loss_avg_mode) | |
| else: | |
| raise ValueError(config.algo.name) | |
| def eval_suffix_nll_ar(config, module, prefix, suffix, | |
| batch_size, num_samples, loss_avg_mode): | |
| x_cat = torch.cat([prefix, suffix[:-1]], dim=-1) | |
| x_cat = x_cat.reshape(1, -1).to(module.device) | |
| with torch.amp.autocast('cuda', dtype=torch.float32): | |
| out = module.backbone(x_cat, sigma=None) | |
| out[:, :, module.mask_index] = module.neg_infinity | |
| suffix_out = out[:, len(prefix) - 1:, :] | |
| suffix_logits = torch.log_softmax(suffix_out, dim=-1) | |
| index = suffix[None, :, None].to(module.device) | |
| nll = torch.gather(-suffix_logits, dim=-1, | |
| index=index).mean() | |
| return float(nll.cpu()) | |
| def eval_suffix_nll_diffusion(config, module, prefix, suffix, | |
| batch_size, num_samples, loss_avg_mode): | |
| all_losses = [] | |
| generator = _eval_suffix_nll_generators(config, module, | |
| prefix, suffix, batch_size, num_samples, loss_avg_mode) | |
| for xt, x0, t, sigma, alpha_t, dalpha_t in generator: | |
| log_x_theta = module(xt, sigma, labels=None) | |
| token_nll = module.nll_per_token(log_x_theta, xt, x0, | |
| alpha_t, dalpha_t) | |
| if loss_avg_mode == 'full': | |
| loss = float(token_nll.mean()) | |
| elif loss_avg_mode == 'suffix': | |
| loss = float(token_nll[:, len(prefix):].mean()) | |
| all_losses.append(loss) | |
| return float(np.mean(all_losses)) | |
| class DiscreteDiffusionHarness(LM): | |
| def __init__(self, pretrained="NONE", max_length=1024, | |
| num_mc_samples=1024, batch_size=64, device="cuda", | |
| checkpoint_path=None, num_proc=8, loss_avg_mode='full', | |
| *args, **kwargs): | |
| super().__init__() | |
| # Whether to use the full sequence, or suffix only to | |
| # approximate the NLL. Full should be the correct way. | |
| assert loss_avg_mode in ('full', 'suffix') | |
| ckpt = torch.load(checkpoint_path, map_location='cpu', | |
| weights_only=False) | |
| config = ckpt['hyper_parameters']['config'] | |
| # Backfill missing keys into legacy checkpoints | |
| if not hasattr(config.training, 'class_dropout_p'): | |
| OmegaConf.set_struct(config, False) | |
| config.training.class_dropout_p = 0.0 | |
| OmegaConf.set_struct(config, True) | |
| self.tokenizer = dataloader.get_tokenizer(config) | |
| if config.algo.name == 'mdlm': | |
| self.model = algo.MDLM(config, self.tokenizer) | |
| elif config.algo.name in ('duo', 'duo_base', | |
| 'distillation', 'ot-finetune'): | |
| self.model = algo.DUO_BASE(config, self.tokenizer) | |
| elif config.algo.name == 'ar': | |
| self.model = algo.AR(config, self.tokenizer) | |
| else: | |
| raise ValueError(f'Implement for {config.algo.name}') | |
| self.config = config | |
| self.num_proc = num_proc | |
| self.num_mc_samples = num_mc_samples | |
| self.batch_size = int(batch_size) | |
| self.device = device | |
| self.loss_avg_mode = loss_avg_mode | |
| self.model.load_state_dict(ckpt['state_dict']) | |
| self.model.to(device) | |
| self.model.eval() | |
| def suffix_greedy_prediction(self, prefix, target): | |
| if self.config.algo.name == 'mdlm': | |
| return self._suffix_greedy_prediction_mdlm(prefix, | |
| target) | |
| elif self.config.algo.name in ('duo', 'duo_base', | |
| 'distillation', 'ot-finetune'): | |
| return self._suffix_greedy_prediction_duo_base(prefix, | |
| target) | |
| elif self.config.algo.name == 'ar': | |
| return self._suffix_greedy_prediction_ar(prefix, target) | |
| else: | |
| raise ValueError(self.config.algo.name) | |
| def _suffix_greedy_prediction_ar(self, prefix, target): | |
| x_cat = torch.cat([prefix, target[:-1]], | |
| dim=-1).reshape(1, -1).to(self.device) | |
| # Follows generate_samples in AR (algo.py) | |
| out = self.model.backbone(x_cat, sigma=None) | |
| out[:, :, self.model.mask_index] = self.model.neg_infinity | |
| out = out.log_softmax(-1) | |
| preds_suffix = out[:, len(prefix) - 1:, :] | |
| greedy_preds = preds_suffix.argmax(-1).flatten() | |
| return (greedy_preds.cpu() == target).all().item() | |
| def _suffix_greedy_prediction_mdlm(self, prefix, target): | |
| mask_idx = self.model.mask_index | |
| eos_idx = self.tokenizer.eos_token_id | |
| # Note: because of the preprocessing, we know that the | |
| # last token is an eos token. | |
| noisy_target = [mask_idx] * (len(target) - 1) + [eos_idx] | |
| noisy_target = torch.tensor(noisy_target, | |
| device=self.device) | |
| prefix = prefix.to(self.device) | |
| seq = torch.concatenate([prefix, noisy_target], | |
| dim=-1).reshape(1, -1) | |
| sigma = torch.zeros(size=(seq.shape[0], 1), | |
| device=self.device) | |
| logits = self.model(seq, sigma, labels=None) | |
| assert logits.shape[0] == 1 | |
| suffix_logits = logits[0, len(prefix):] | |
| target_preds = suffix_logits.argmax(-1).cpu() | |
| correct = target_preds == target | |
| correct = correct.all() | |
| return bool(correct) | |
| def _suffix_greedy_prediction_duo_base(self, prefix, target): | |
| noisy_suffix = torch.randint( | |
| 0, self.model.vocab_size, size=target.shape, | |
| dtype=target.dtype, device=self.device) | |
| prefix = prefix.to(self.device) | |
| # shape: (1, len) | |
| noisy_seq = torch.concatenate([prefix, noisy_suffix])[None, :] | |
| # Set the EOS token at the end | |
| noisy_seq[0, -1] = target[-1] | |
| clean_seq = torch.concatenate([prefix, target.to(self.device)])[None, :] | |
| alpha_t = (noisy_seq == clean_seq).float().mean(dim=1)[:, None] | |
| sigma = self.model._sigma_from_alphat(alpha_t) | |
| t = self.model.noise.get_t_for_alpha(alpha_t) | |
| logits = self.model(noisy_seq, sigma, labels=None) | |
| assert logits.shape[0] == 1 | |
| suffix_logits = logits[0, len(prefix):] | |
| target_preds = suffix_logits.argmax(-1).cpu() | |
| correct = target_preds == target | |
| correct = correct.all() | |
| return bool(correct) | |
| def loglikelihood(self, requests: list[Instance]) \ | |
| -> list[tuple[float, bool]]: | |
| dataset = requests_to_dataset(self.config, requests, | |
| self.tokenizer, self.num_proc) | |
| out = [] | |
| for elem in tqdm(dataset, 'Computing likelihood...'): | |
| prefix = elem['prefix'] | |
| target = elem['target'] | |
| if len(prefix) + len(target) > self.model.config.model.length: | |
| # If the request is too long, skip it. | |
| ll = 0.0 | |
| is_target_greedy_dec = False | |
| out.append((ll, is_target_greedy_dec)) | |
| print("SKIPPING") | |
| continue | |
| ll = -eval_suffix_nll(self.config, self.model, prefix, | |
| target, self.batch_size, self.num_mc_samples, | |
| self.loss_avg_mode) | |
| is_target_greedy_dec = self.suffix_greedy_prediction( | |
| prefix, target) | |
| out.append((ll, bool(is_target_greedy_dec))) | |
| return out | |
| def loglikelihood_rolling( | |
| self, requests: list[Instance] | |
| ) -> list[tuple[float, bool]]: | |
| raise NotImplementedError | |
| def generate_until(self, context, max_length, stop, | |
| **generation_kwargs): | |
| raise NotImplementedError | |
| if __name__ == "__main__": | |
| cli_evaluate() |