tliu's picture
Upload folder using huggingface_hub
c95c7b0 verified
Raw
History Blame Contribute Delete
17.6 kB
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 == "":
# End of text as 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]:
# TODO: Implement caching once we've confirmed the perplexity implementation
# TODO: Automatic batch size detection for vectorization
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]
# TODO: Extract out this call so it only gets called once and
# also somehow figure out partial caching for that.
string_nll = self._loglikelihood_tokens(
rolling_token_windows, disable_tqdm=True
)
# Discard `is_greedy`
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):
# The negative sign on len(tokens) sorts descending - this has a few advantages:
# - Time estimates will always be over not underestimates, which is more useful for planning
# - To know the size of a batch when going through the list, you know the first one is always the batch
# padded context length. this is useful to simplify the batching logic and more importantly to make
# automatic adaptive batches much easier to implement
# - Any OOMs will happen right away rather than near the end
tokens = x[1] + x[2]
return -len(tokens), tuple(tokens)
# TODO: Automatic (variable) batch size detection for vectorization
# TODO: Implement some kind of efficient-request-middleware that lumps together requests with the same context
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
# Because vectorizing is annoying, we first convert each (context, continuation) pair to padded
# tensors, then we pack them together into a batch, call the model, and then pick it all apart
# again because vectorizing is annoying
for _, context_enc, continuation_enc in chunk:
# sanity check
assert len(context_enc) > 0
assert len(continuation_enc) > 0
assert len(continuation_enc) <= self.max_length
# How this all works:
# CTX CONT
# inp 0 1 2 3|4 5 6 7 8 9 <- last token is deleted by inp[:, :-1]
# gpt2 \ \
# logits 1 2 3|4 5 6 7 8 9 <- the ctx half gets tossed out by the
# cont_tokens 4 5 6 7 8 9 [:, -len(continuation_enc):, :self.vocab_size] slice
# When too long to fit in context, truncate from the left
_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
# Since in _collate we make sure length is descending, the longest is always the first one.
padding_length = (
padding_length if padding_length is not None else input_len
)
# Pad length from seq to padding_length
input = torch.cat(
[
input, # [seq]
torch.zeros(padding_length - input_len, dtype=torch.long).to(
input.device
), # [padding_length - seq]
],
dim=0,
)
inputs.append(input.unsqueeze(0)) # [1, padding_length]
cont_tokens_list.append(continuation_enc)
input_lens.append(input_len)
batched_inputs = torch.cat(inputs, dim=0) # [batch, padding_length]
multi_logits = F.log_softmax(
self._model_call(batched_inputs), dim=-1
).cpu() # [batch, padding_length, vocab]
for (cache_key, _, _), logits, input, input_len, cont_tokens in zip(
chunk, multi_logits, inputs, input_lens, cont_tokens_list
):
# Slice to original seq length
cont_len = len(cont_tokens)
# [1, seq, vocab]
logits = logits[input_len - cont_len : input_len].unsqueeze(0)
# Check if per-token argmax is exactly equal to continuation
greedy_tokens = logits.argmax(dim=-1)
# [1, seq]
cont_tokens = torch.tensor(cont_tokens, dtype=torch.long).unsqueeze(0)
max_equal = (greedy_tokens == cont_tokens).all()
# Obtain logprobs at the corresponding continuation token indices
# last_token_slice = logits[:, -1, :].squeeze(0).tolist()
# [1, seq]
logits = torch.gather(logits, 2, cont_tokens.unsqueeze(-1)).squeeze(-1)
# Answer: (log prob, is-exact-match)
answer = (float(logits.sum()), bool(max_equal))
# Partial caching
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)
# Add hook to lm
lm.set_cache_hook(self.get_cache_hook())
def __getattr__(self, attr):
def fn(requests):
res = []
remaining_reqs = []
# Figure out which ones are cached and which ones are new
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)
# Actually run the LM on the requests that do not have cached results
rem_res = getattr(self.lm, attr)(remaining_reqs)
# Stick the new ones back into the list and also cache any of the new ones
resptr = 0
for req, r in zip(remaining_reqs, rem_res):
while res[resptr] is not None:
resptr += 1
res[resptr] = r
# Caching
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