| import abc |
| import hashlib |
| import json |
| import os |
| import torch |
| import torch.nn.functional as F |
| from tqdm import tqdm |
| from typing import Iterable, List, Optional, Tuple, Union |
| from transformers import BatchEncoding |
|
|
| from lm_eval.api import utils |
|
|
|
|
| class LM(abc.ABC): |
| def __init__(self): |
| self.cache_hook = CacheHook(None) |
|
|
| @abc.abstractmethod |
| def loglikelihood( |
| self, requests: List[Tuple[str, str]] |
| ) -> List[Tuple[float, bool]]: |
| """Compute log-likelihood of generating a continuation from a context. |
| Downstream tasks should attempt to use loglikelihood instead of other |
| LM calls whenever possible. |
| |
| Args: |
| requests (List[Tuple[str, str]]): |
| 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. |
| |
| Returns: |
| A list of pairs (logprob, isgreedy): |
| logprob (float): |
| The log probability of `continuation`. |
| isgreedy (bool): |
| Whether `continuation` would be generated by greedy |
| sampling from `context`. |
| """ |
| pass |
|
|
| @abc.abstractmethod |
| def loglikelihood_rolling(self, requests: List[Tuple[str, str]]) -> List[float]: |
| """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 |
| |
| Args: |
| requests (List[Tuple[str, str]]): |
| A list of paired strings. |
| string (str): |
| String for which we are computing per-token loglikelihood. |
| |
| Returns: |
| A list of logprobs on the `continuation`. |
| """ |
| pass |
|
|
| @abc.abstractmethod |
| def greedy_until(self, requests: List[Tuple[str, dict]]) -> List[str]: |
| """Generate greedily until a stopping sequence or max generation length. |
| |
| Args: |
| requests (List[Tuple[str, dict]]): |
| A list of pairs (context, args): |
| context (str): |
| Context string. |
| args (dict): |
| A dictionary of generation arguments in the form: |
| { |
| stop_sequences: str, |
| max_generation_length: int, |
| num_fewshot: int |
| } |
| |
| Returns: |
| A list of strings continuation: |
| continuation: str |
| The generated continuation. |
| """ |
| pass |
|
|
| def set_cache_hook(self, cache_hook: "CacheHook"): |
| self.cache_hook = cache_hook |
|
|
|
|
| TokenSequence = Union[List[int], torch.LongTensor, torch.Tensor, BatchEncoding] |
|
|
|
|
| class TokenLM(LM): |
| """A language model that assumes inputs, and possibly outputs, are |
| tokenized text as opposed to language model APIs that only support |
| string-based input and output systems. |
| """ |
|
|
| @abc.abstractmethod |
| def tok_encode(self, string: str): |
| pass |
|
|
| @abc.abstractmethod |
| def tok_decode(self, tokens: Iterable[int]) -> List[str]: |
| pass |
|
|
| @property |
| @abc.abstractmethod |
| def eot_token(self) -> str: |
| pass |
|
|
| @property |
| @abc.abstractmethod |
| def eot_token_id(self) -> int: |
| pass |
|
|
| @property |
| @abc.abstractmethod |
| def max_gen_toks(self) -> int: |
| """The maximum number of tokens to generate - not including context.""" |
| pass |
|
|
| @property |
| @abc.abstractmethod |
| def max_length(self) -> int: |
| """The maximum sequence length of the model.""" |
| pass |
|
|
| @property |
| @abc.abstractmethod |
| def batch_size(self) -> int: |
| pass |
|
|
| @property |
| @abc.abstractmethod |
| def device(self) -> Union[int, str, torch.device]: |
| pass |
|
|
| def loglikelihood( |
| self, requests: List[Tuple[str, str]] |
| ) -> List[Tuple[float, bool]]: |
| new_requests = [] |
| 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_requests.append( |
| ((context, continuation), context_enc, continuation_enc) |
| ) |
| return self._loglikelihood_tokens(new_requests) |
|
|
| def loglikelihood_rolling(self, requests: List[Tuple[str, str]]) -> List[float]: |
| |
| |
| loglikelihoods = [] |
| for (string,) in tqdm(requests): |
| rolling_token_windows = list( |
| map( |
| utils.make_disjoint_window, |
| utils.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: List[Tuple[Tuple[str, str], TokenSequence, TokenSequence]], |
| disable_tqdm: Optional[bool] = False, |
| ) -> List[Tuple[float, bool]]: |
| """Helper method for computing log-likelihood of generating a |
| continuation from a context that have both been tokenized/encoded. |
| |
| Args: |
| requests (List[Tuple[Tuple[str, str], TokenSequence, TokenSequence]]): |
| A list of pairs ((context, continuation), context_enc, continuation_enc): |
| 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. |
| context_enc (TokenSequence): |
| The tokenized/encoded context. |
| continuation_enc (TokenSequence): |
| The tokenized/encoded continuation. |
| disable_tqdm (bool, optional, defaults to False): |
| Whether to disable `tqdm` progress bar. |
| |
| Returns: |
| A list of pairs (logprob, isgreedy): |
| logprob (float): |
| The log probability of `continuation`. |
| isgreedy (float): |
| Whether `continuation` would be generated by greedy sampling from `context`. |
| """ |
|
|
| def _collate(x): |
| |
| |
| |
| |
| |
| |
| tokens = x[1] + x[2] |
| return -len(tokens), tuple(tokens) |
|
|
| |
| |
| results = [] |
| reorder = utils.Reorderer(requests, _collate) |
| for chunk in utils.chunks( |
| tqdm(reorder.get_reordered(), disable=disable_tqdm), self.batch_size |
| ): |
| inputs = [] |
| input_lens = [] |
| cont_tokens_list = [] |
| 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 |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| _full_enc = context_enc + continuation_enc |
| input = torch.tensor( |
| _full_enc[-(self.max_length + 1) :][:-1], |
| dtype=torch.long, |
| ).to(self.device) |
| (input_len,) = input.shape |
|
|
| |
| padding_length = ( |
| padding_length if padding_length is not None else input_len |
| ) |
|
|
| |
| input = torch.cat( |
| [ |
| input, |
| torch.zeros(padding_length - input_len, dtype=torch.long).to( |
| input.device |
| ), |
| ], |
| dim=0, |
| ) |
| inputs.append(input.unsqueeze(0)) |
| cont_tokens_list.append(continuation_enc) |
| input_lens.append(input_len) |
|
|
| batched_inputs = torch.cat(inputs, dim=0) |
| multi_logits = F.log_softmax( |
| self._model_call(batched_inputs), dim=-1 |
| ).cpu() |
|
|
| for (cache_key, _, _), logits, input, input_len, cont_tokens in zip( |
| chunk, multi_logits, inputs, input_lens, cont_tokens_list |
| ): |
| |
| cont_len = len(cont_tokens) |
| |
| logits = logits[input_len - cont_len : input_len].unsqueeze(0) |
| |
| greedy_tokens = logits.argmax(dim=-1) |
| |
| cont_tokens = torch.tensor(cont_tokens, dtype=torch.long).unsqueeze(0) |
| max_equal = (greedy_tokens == cont_tokens).all() |
|
|
| |
| |
| |
| logits = torch.gather(logits, 2, cont_tokens.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) |
| results.append(answer) |
| return reorder.get_original(results) |
|
|
| @abc.abstractmethod |
| def _model_call( |
| self, inputs: TokenSequence, labels: Optional[TokenSequence] = None |
| ) -> TokenSequence: |
| """ |
| Args: |
| inputs (TokenSequence): |
| A list of strings or torch tensor of shape [batch, sequence] |
| the size of sequence may vary from call to call. |
| labels (TokenSequence, optional, defaults to None): |
| A list of strings or torch tensor of shape [batch, sequence] |
| useful for sequence-to-sequence language models. |
| |
| Returns: |
| A list of ints or torch tensor of shape [batch, sequence, vocab] |
| with the logits returned from the model. |
| """ |
| pass |
|
|
| @abc.abstractmethod |
| def _model_generate( |
| self, inputs: TokenSequence, max_tokens: int, stop: Optional[List[str]] = None |
| ) -> Union[TokenSequence, List[str]]: |
| """ |
| Args: |
| inputs (TokenSequence): |
| A list of strings/ints or torch tensor of shape [batch, sequence] |
| the size of sequence may vary from call to call. |
| max_tokens (int): |
| The maximum number of tokens to generate. |
| stop (List[str], optional, defaults to None): |
| A list of stopping sequences. If provided, the generation will |
| stop when any string sequence in the list is encountered. |
| |
| Returns: |
| A list of ints/strings or a torch tensor of shape [batch, sequence, vocab] |
| with continuation tokens/string of the inputs. |
| """ |
| pass |
|
|
|
|
| def hash_args(attr, args): |
| data = json.dumps([attr] + list(args)) |
| return hashlib.sha256(data.encode("utf-8")).hexdigest() |
|
|
|
|
| class CachingLM: |
| def __init__(self, lm: LM, cache_db: str): |
| """LM wrapper that returns cached results if they exist, and uses the underlying LM if not. |
| |
| Args: |
| lm (LM): |
| The underlying LM to use. |
| cache_db (str): |
| Path to the `cache` database. |
| """ |
| from sqlitedict import SqliteDict |
|
|
| self.lm = lm |
| if os.path.dirname(cache_db): |
| os.makedirs(os.path.dirname(cache_db), exist_ok=True) |
| self.cache_db = cache_db |
| self.dbdict = SqliteDict(cache_db, autocommit=True) |
| |
| lm.set_cache_hook(self.get_cache_hook()) |
|
|
| def __getattr__(self, attr): |
| def fn(requests): |
| res = [] |
| remaining_reqs = [] |
|
|
| |
| for req in requests: |
| hsh = hash_args(attr, req) |
| if hsh in self.dbdict: |
| ob = self.dbdict[hsh] |
|
|
| assert ob is not None |
| res.append(ob) |
| else: |
| res.append(None) |
| remaining_reqs.append(req) |
|
|
| |
| rem_res = getattr(self.lm, attr)(remaining_reqs) |
|
|
| |
| resptr = 0 |
| for req, r in zip(remaining_reqs, rem_res): |
| while res[resptr] is not None: |
| resptr += 1 |
|
|
| res[resptr] = r |
| |
| hsh = hash_args(attr, req) |
| self.dbdict[hsh] = r |
| self.dbdict.commit() |
| return res |
|
|
| return fn |
|
|
| def get_cache_hook(self): |
| return CacheHook(self) |
|
|
|
|
| class CacheHook: |
| def __init__(self, cachinglm: 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 |
|
|