| import abc |
| import torch |
| import json |
| import hashlib |
| import collections |
| from tqdm import tqdm |
| from typing import Iterable |
| from abc import abstractmethod |
| from torch import nn |
| import transformers |
| import torch.nn.functional as F |
|
|
| class TruncateFunction(torch.autograd.Function): |
| @staticmethod |
| def forward(ctx, input, threshold): |
| truncated_tensor = input.clone() |
| truncated_tensor[truncated_tensor.abs() < threshold] = truncated_tensor[truncated_tensor.abs() < threshold].sign() * threshold |
| return truncated_tensor |
| |
|
|
| @staticmethod |
| def backward(ctx, grad_output): |
| grad_input = grad_output.clone() |
| return grad_input, None |
|
|
| def truncate_number(number, threshold=1e-3): |
| |
| return TruncateFunction.apply(number, threshold) |
|
|
| def find_layers(module, layers=[nn.Conv2d, nn.Linear, transformers.Conv1D], name=""): |
| if type(module) in layers: |
| return {name: module} |
| res = {} |
| for name1, child in module.named_children(): |
| res.update( |
| find_layers( |
| child, layers=layers, name=name + "." + name1 if name != "" else name1 |
| ) |
| ) |
| return res |
|
|
|
|
| class CacheHook: |
| def __init__(self, cachinglm): |
| if cachinglm is None: |
| self.dbdict = None |
| return |
|
|
| self.dbdict = cachinglm.dbdict |
|
|
| def add_partial(self, attr, req, res): |
| if self.dbdict is None: |
| return |
| hsh = hash_args(attr, req) |
| self.dbdict[hsh] = res |
|
|
|
|
| class LM(abc.ABC): |
| def __init__(self): |
| self.cache_hook = CacheHook(None) |
|
|
| @abstractmethod |
| def loglikelihood(self, requests): |
| """Compute log-likelihood of generating a continuation from a context. |
| Downstream tasks should attempt to use loglikelihood instead of other |
| LM calls whenever possible. |
| |
| :param requests: list |
| A list of pairs (context, continuation) |
| context: str |
| Context string. Implementations of LM must be able to handle an |
| empty context string. |
| continuation: str |
| The continuation over which log likelihood will be calculated. If |
| there is a word boundary, the space should be in the continuation. |
| For example, context="hello" continuation=" world" is correct. |
| :return: list |
| A list of pairs (logprob, isgreedy) |
| logprob: float |
| The log probability of `continuation` |
| isgreedy: |
| Whether `continuation` would be generated by greedy sampling from `context` |
| """ |
| pass |
|
|
| @abstractmethod |
| def loglikelihood_rolling(self, requests): |
| """Compute full log-likelihood of a string, with no truncation, for perplexity computation |
| - We will use the full max context length of the model. |
| - For inputs that exceed the max context length, we divide the tokenized string into chunks of up to |
| the max context length. |
| - IMPORTANT: Each document's loglikelihood/perplexity is computed *separately*, unlike other implementations |
| which may simply concatenate multiple documents together. |
| - IMPORTANT: We maximize the amount of context for each prediction. Specifically, for inputs that we break into |
| multiple chunks, the last input will still a full-sized context. |
| Example: |
| Input tokens: [ 0 1 2 3 4 5 6 7 8 9 ] |
| Prefix: EOT |
| Max context length: 4 |
| Resulting input/prediction pairs: |
| |
| INPUT: EOT 0 1 2 |
| PRED: 0 1 2 3 |
| |
| INPUT: 3 4 5 6 |
| PRED: 4 5 6 7 |
| |
| INPUT: 5 6 7 8 |
| PRED: 8 9 |
| |
| Observe that: |
| 1. Each token is predicted exactly once |
| 2. For the last pair, we provide the full context, but only score the last two tokens |
| |
| :param requests: list |
| A list of strings |
| string: str |
| String for which we are computing per-toke loglikelihood |
| :return: list |
| A list of pairs (logprob, isgreedy) |
| logprob: float |
| The log probability of `continuation` |
| isgreedy: |
| Whether `continuation` would be generated by greedy sampling from `context` |
| """ |
| pass |
|
|
| |
| @abstractmethod |
| def greedy_until(self, requests): |
| """Generate greedily until a stopping sequence |
| |
| :param requests: list |
| A list of pairs (context, until) |
| context: str |
| Context string |
| until: [str] |
| The string sequences to generate until. These string sequences |
| may each span across multiple tokens, or may be part of one token. |
| :return: list |
| A list of strings continuation |
| continuation: str |
| The generated continuation. |
| """ |
| pass |
|
|
| @classmethod |
| def create_from_arg_string(cls, additional_config=None): |
| additional_config = {} if additional_config is None else additional_config |
| args = {k: v for k, v in additional_config.items() if v is not None} |
| return cls(**args) |
|
|
| def set_cache_hook(self, cache_hook): |
| self.cache_hook = cache_hook |
|
|
|
|
| class BaseLM(LM): |
| @property |
| @abstractmethod |
| def eot_token_id(self): |
| pass |
|
|
| @property |
| @abstractmethod |
| def max_length(self): |
| pass |
|
|
| @property |
| @abstractmethod |
| def max_gen_toks(self): |
| pass |
|
|
| @property |
| @abstractmethod |
| def batch_size(self): |
| pass |
|
|
| @property |
| @abstractmethod |
| def device(self): |
| pass |
|
|
| @abstractmethod |
| def tok_encode(self, string: str): |
| pass |
|
|
| @abstractmethod |
| def tok_decode(self, tokens: Iterable[int]): |
| pass |
|
|
| @abstractmethod |
| def _model_generate(self, context, max_length, eos_token_id): |
| pass |
|
|
| @abstractmethod |
| def _model_call(self, inps): |
| """ |
| inps: a torch tensor of shape [batch, sequence] |
| the size of sequence may vary from call to call |
| |
| returns: a torch tensor of shape [batch, sequence, vocab] with the |
| logits returned from the model |
| """ |
| pass |
|
|
| |
| |
|
|
| def loglikelihood(self, requests): |
| new_reqs = [] |
| for context, continuation in requests: |
| if context == "": |
| |
| context_enc = [self.eot_token_id] |
| else: |
| context_enc = self.tok_encode(context) |
|
|
| continuation_enc = self.tok_encode(continuation) |
| new_reqs.append(((context, continuation), context_enc, continuation_enc)) |
|
|
| return self._loglikelihood_tokens(new_reqs) |
|
|
| def loglikelihood_rolling(self, requests): |
| |
| |
|
|
| loglikelihoods = [] |
| for (string,) in tqdm(requests): |
| rolling_token_windows = list( |
| map( |
| make_disjoint_window, |
| get_rolling_token_windows( |
| token_list=self.tok_encode(string), |
| prefix_token=self.eot_token_id, |
| max_seq_len=self.max_length, |
| context_len=1, |
| ), |
| ) |
| ) |
|
|
| rolling_token_windows = [(None,) + x for x in rolling_token_windows] |
|
|
| |
| |
| string_nll = self._loglikelihood_tokens( |
| rolling_token_windows, disable_tqdm=True |
| ) |
|
|
| |
| string_nll = [x[0] for x in string_nll] |
|
|
| string_nll = sum(string_nll) |
| loglikelihoods.append(string_nll) |
|
|
| return loglikelihoods |
|
|
| def _loglikelihood_tokens(self, requests, disable_tqdm=False): |
| |
| res = [] |
| dataset_inps = [] |
|
|
| def _collate(x): |
| |
| |
| |
| |
| |
| |
|
|
| toks = x[1] + x[2] |
| return -len(toks), tuple(toks) |
|
|
| |
| re_ord = Reorderer(requests, _collate) |
| for chunk in chunks( |
| tqdm(re_ord.get_reordered(), disable=disable_tqdm), self.batch_size |
| ): |
| inps = [] |
| cont_toks_list = [] |
| inplens = [] |
|
|
| padding_length = None |
|
|
| |
| |
| |
|
|
| for _, context_enc, continuation_enc in chunk: |
| |
| assert len(context_enc) > 0 |
| assert len(continuation_enc) > 0 |
| assert len(continuation_enc) <= self.max_length |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| inp = torch.tensor( |
| (context_enc + continuation_enc)[-(self.max_length + 1) :][:-1], |
| dtype=torch.long, |
| ).to(self.device) |
| (inplen,) = inp.shape |
|
|
| cont = continuation_enc |
|
|
| |
| padding_length = ( |
| padding_length if padding_length is not None else inplen |
| ) |
|
|
| |
| inp = torch.cat( |
| [ |
| inp, |
| torch.zeros(padding_length - inplen, dtype=torch.long).to( |
| inp.device |
| ), |
| ], |
| dim=0, |
| ) |
|
|
| inps.append(inp.unsqueeze(0)) |
| cont_toks_list.append(cont) |
| inplens.append(inplen) |
| |
| batched_inps = torch.cat(inps, dim=0).to( |
| self.device |
| ) |
|
|
| |
| multi_logits = F.log_softmax( |
| self._model_call(batched_inps), dim=-1 |
| ).cpu() |
|
|
|
|
| for (cache_key, _, _), logits, inp, inplen, cont_toks in zip( |
| chunk, multi_logits, inps, inplens, cont_toks_list |
| ): |
|
|
| |
| contlen = len(cont_toks) |
| logits = logits[inplen - contlen : inplen].unsqueeze( |
| 0 |
| ) |
|
|
| |
| greedy_tokens = logits.argmax(dim=-1) |
| cont_toks = torch.tensor(cont_toks, dtype=torch.long).unsqueeze( |
| 0 |
| ) |
| |
| max_equal = (greedy_tokens == cont_toks).all() |
|
|
| |
| |
| logits = torch.gather(logits, 2, cont_toks.unsqueeze(-1)).squeeze( |
| -1 |
| ) |
|
|
| |
| answer = (float(logits.sum()), bool(max_equal)) |
|
|
| |
| if cache_key is not None: |
| self.cache_hook.add_partial("loglikelihood", cache_key, answer) |
| res.append(answer) |
|
|
| return re_ord.get_original(res) |
|
|
| def greedy_until(self, requests): |
| print("greedy utils in base...") |
| |
| |
|
|
| |
| res = [] |
|
|
| def _collate(x): |
| toks = self.tok_encode(x[0]) |
| return len(toks), x[0] |
|
|
| re_ord = Reorderer(requests, _collate) |
|
|
| for context, until in tqdm(re_ord.get_reordered()): |
| if isinstance(until, str): |
| until = [until] |
|
|
| (primary_until,) = self.tok_encode(until[0]) |
|
|
| context_enc = torch.tensor( |
| [self.tok_encode(context)[self.max_gen_toks - self.max_length :]] |
| ).to(self.device) |
|
|
| cont = self._model_generate( |
| context_enc, context_enc.shape[1] + self.max_gen_toks, primary_until |
| ) |
|
|
| s = self.tok_decode(cont[0].tolist()[context_enc.shape[1] :]) |
|
|
| for term in until: |
| s = s.split(term)[0] |
|
|
| |
| self.cache_hook.add_partial("greedy_until", (context, until), s) |
|
|
| res.append(s) |
|
|
| return re_ord.get_original(res) |
|
|
|
|
| def make_disjoint_window(pair): |
| """Takes output from get_rolling_token_windows and makes the context not overlap with the continuation""" |
| a, b = pair |
| return a[: len(a) - (len(b) - 1)], b |
|
|
|
|
| def hash_args(attr, args): |
| dat = json.dumps([attr] + list(args)) |
| return hashlib.sha256(dat.encode("utf-8")).hexdigest() |
|
|
|
|
| def simple_parse_args_string(args_string): |
| """ |
| Parses something like |
| args1=val1,arg2=val2 |
| Into a dictionary |
| """ |
| args_string = args_string.strip() |
| if not args_string: |
| return {} |
| arg_list = args_string.split(",") |
| args_dict = {} |
| for arg in arg_list: |
| k, v = arg.split("=") |
| args_dict[k] = v |
| return args_dict |
|
|
|
|
| def get_rolling_token_windows(token_list, prefix_token, max_seq_len, context_len): |
| """ |
| - context_len allows for a rolling window context, allowing each prediction window to potentially |
| condition on some context |
| |
| :param token_list: list |
| List of tokens to be PREDICTED |
| :param max_seq_len: int |
| max_seq_len of model (or max_seq_len we want to use) |
| :param context_len: int |
| Amount of desired token context for prediction. Needs to be at least 1. |
| :param prefix_token: token |
| Dummy token like <eos> so the first token has something to condition on |
| :return: generator |
| Generator of tuples |
| (input_tokens, pred_tokens) |
| Note: Score only the last len(pred_tokens) logits of the LM |
| """ |
| assert 1 <= context_len <= max_seq_len |
| if not token_list: |
| return |
| |
| pred_len = max_seq_len - context_len + 1 |
| predicted = 0 |
|
|
| |
| first_seq_len = min(max_seq_len, len(token_list)) |
| yield ([prefix_token] + token_list[: first_seq_len - 1], token_list[:first_seq_len]) |
| predicted += first_seq_len |
|
|
| while predicted < len(token_list): |
| window_pred_len = min(len(token_list) - predicted, pred_len) |
| window_end = predicted + window_pred_len |
|
|
| yield ( |
| token_list[window_end - max_seq_len - 1 : window_end - 1], |
| token_list[window_end - window_pred_len : window_end], |
| ) |
| predicted += window_pred_len |
|
|
|
|
| class Reorderer: |
| def __init__(self, arr, fn): |
| self.size = len(arr) |
| arr = list(enumerate(arr)) |
| arr = group(arr, lambda x: fn(x[1])) |
| arr = [([y[0] for y in x], x[0][1]) for x in arr] |
| arr.sort(key=lambda x: fn(x[1])) |
|
|
| self.arr = arr |
|
|
| def get_reordered(self): |
| return [x[1] for x in self.arr] |
|
|
| def get_original(self, newarr): |
| res = [None] * self.size |
| cov = [False] * self.size |
|
|
| for (inds, _), v in zip(self.arr, newarr): |
| for ind in inds: |
| res[ind] = v |
| cov[ind] = True |
|
|
| assert all(cov) |
|
|
| return res |
|
|
|
|
| def join_iters(iters): |
| for iter in iters: |
| yield from iter |
|
|
|
|
| def chunks(iter, n): |
| arr = [] |
| for x in iter: |
| arr.append(x) |
| if len(arr) == n: |
| yield arr |
| arr = [] |
|
|
| if arr: |
| yield arr |
|
|
|
|
| def group(arr, fn): |
| res = collections.defaultdict(list) |
|
|
| for ob in arr: |
| res[fn(ob)].append(ob) |
|
|
| return list(res.values()) |
|
|