gpt2-summarizer-api / backend /src /inference.py
popboat1
Implement KV caching for O(N) generation and speedups
199c48b
Raw
History Blame Contribute Delete
6.25 kB
import time
import torch
import tiktoken
import torch.nn.functional as F
from .models.model import GPT, GPTConfig
def generate_text_stream(prompt, model_path, max_new_tokens=50, temperature=0.5, top_k=40, repetition_penalty=1.0, stop_on_eot=False, num_probs=0, device='cpu'):
# load checkpoint safely onto the requested hardware device
checkpoint = torch.load(model_path, map_location=device, weights_only=True)
# parse the internal state dict regardless of how the training script packaged it
if 'model_state_dict' in checkpoint:
state_dict = checkpoint['model_state_dict']
elif 'model' in checkpoint:
state_dict = checkpoint['model']
else:
state_dict = checkpoint
# strip out any compilation prefixes leftover from pytorch two point zero optimizations
unwanted_prefix = '_orig_mod.'
for k, v in list(state_dict.items()):
if k.startswith(unwanted_prefix):
state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k)
# initialize config and restore any custom saved hyper parameters
config = GPTConfig()
if 'config' in checkpoint:
for k, v in checkpoint['config'].items():
setattr(config, k, v)
# dynamically read the vocab size from the checkpoint embedding weights
# this gracefully handles our padding trick without hardcoding numbers
vocab_size = state_dict['transformer.wte.weight'].shape[0]
config.vocab_size = vocab_size
# map states to the architecture and freeze gradients
model = GPT(config)
model.load_state_dict(state_dict)
model.eval()
model.to(device)
# setup tokenizer and define structural boundary tokens
enc = tiktoken.get_encoding('gpt2')
eot_token = 50256
input_tokens = enc.encode(prompt)
idx = torch.tensor(input_tokens, dtype=torch.long, device=device).unsqueeze(0)
# cache initial input length to calculate exact generation volume later
input_len = idx.size(1)
if device == 'cuda':
torch.cuda.synchronize()
t0 = time.perf_counter()
generated_text_so_far = ""
past_key_values = None
# start the autoregressive decoding loop
with torch.no_grad():
with torch.autocast(device_type="cuda" if "cuda" in device else "cpu", dtype=torch.float16):
for i in range(max_new_tokens):
# crop context to max block size if we exceed the physical attention window
if past_key_values is not None:
idx_cond = idx[:, -1:]
else:
idx_cond = idx[:, -config.block_size:]
logits, _, past_key_values = model(idx_cond, past_key_values=past_key_values, use_cache=True)
# isolate the raw activations at the final time step
next_token_logits = logits[:, -1, :]
# apply the repetition penalty to heavily suppress tokens we already generated
if repetition_penalty != 1.0:
for token_id in set(idx[0, input_len:].tolist()):
if next_token_logits[0, token_id] > 0:
next_token_logits[0, token_id] /= repetition_penalty
else:
next_token_logits[0, token_id] *= repetition_penalty
# scale by temperature to control the deterministic boundary
next_token_logits = next_token_logits / temperature
# truncate long tail probabilities via top k filtering
if top_k is not None:
v, _ = torch.topk(next_token_logits, min(top_k, next_token_logits.size(-1)))
next_token_logits[next_token_logits < v[:, [-1]]] = -float('Inf')
probs = F.softmax(next_token_logits, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1)
top_tokens = []
if num_probs > 0:
top_k_probs, top_k_indices = torch.topk(probs, min(num_probs, probs.size(-1)))
top_k_probs_list = top_k_probs[0].tolist()
top_k_indices_list = top_k_indices[0].tolist()
for p, t_id in zip(top_k_probs_list, top_k_indices_list):
try:
token_bytes = enc.decode_single_token_bytes(t_id)
token_str = token_bytes.decode('utf-8', errors='replace')
except Exception:
token_str = str(t_id)
top_tokens.append({"token": token_str, "prob": p})
idx = torch.cat((idx, idx_next), dim=1)
if device == 'cuda':
torch.cuda.synchronize()
t1 = time.perf_counter()
actual_tokens_generated = i + 1
inference_time = t1 - t0
tokens_per_sec = actual_tokens_generated / inference_time if inference_time > 0 else 0
# slice the tensor to ONLY decode the newly generated tokens, ignoring the prompt
output_tokens = idx[0, input_len:].tolist()
current_text = enc.decode(output_tokens).replace("<|endoftext|>", "")
new_chunk = current_text[len(generated_text_so_far):]
generated_text_so_far = current_text
is_done = (i == max_new_tokens - 1) or (stop_on_eot and idx_next.item() == eot_token)
yield {
"text": new_chunk,
"time": inference_time,
"tps": tokens_per_sec,
"length": actual_tokens_generated,
"top_tokens": top_tokens,
"is_done": is_done
}
# trigger early termination if the model emits an end of text token
if stop_on_eot and idx_next.item() == eot_token:
break