Spaces:
Sleeping
Sleeping
Devdan Schretlen
Claude Opus 4.8
Prime chat prompts with few-shot examples for shorter replies
1219ad2 Download server/inference.py from DevSchretl/microGPT: direct link, hf CLI and curl.
- Browser
- Download file 5.53 kB
-
https://huggingface.co/spaces/DevSchretl/microGPT/resolve/main/server/inference.py
- Command line
-
hf download hf://spaces/DevSchretl/microGPT/server/inference.py
-
curl -L -o inference.py https://huggingface.co/spaces/DevSchretl/microGPT/resolve/main/server/inference.py
5.53 kB
| """ | |
| Inference wrapper around the fine-tuned chat model for the web backend. | |
| Reuses the same model construction and chat template as the sample_chat.py CLI | |
| so the web server generates replies exactly the way the CLI does, but keeps the | |
| model loaded across requests and streams tokens out as they are produced. | |
| """ | |
| import threading | |
| import torch | |
| from torch.nn import functional as F | |
| from common import build_model, get_device | |
| from models import SPECIAL_TOKENS, get_encoding | |
| # Real GPT-2 vocab is 0..50256; 50257+ are the chat special tokens (and padding | |
| # up to the model's padded vocab_size), which have no printable byte form. | |
| FIRST_SPECIAL_TOKEN = 50257 | |
| # Few-shot priming: a couple of short example turns are prepended to every prompt | |
| # so the (small, 254M) model imitates the concise pattern instead of rambling -- | |
| # demonstration works far better than a natural-language "be brief" instruction | |
| # for a model this size. The examples are prompt-only (never shown in the UI), and | |
| # the length of these answers is the knob for how long real replies come out: | |
| # keep them concise but complete (one full sentence) to avoid terse non-answers. | |
| FEWSHOT_EXAMPLES = [ | |
| ("What is the capital of France?", "The capital of France is Paris."), | |
| ("Why is the sky blue?", "Sunlight scatters off air molecules, and blue scatters the most."), | |
| ("Any tips for staying focused?", "Work in short blocks and put your phone out of reach."), | |
| ] | |
| class ChatModel: | |
| """A loaded chat model that builds prompts from history and streams replies.""" | |
| def __init__(self, arch, checkpoint, device=None): | |
| self.device = device or get_device() | |
| self.model = build_model(arch).to(self.device) | |
| state = torch.load(checkpoint, map_location=self.device) | |
| state = {k.replace("_orig_mod.", ""): v for k, v in state.items()} | |
| self.model.load_state_dict(state) | |
| self.model.eval() | |
| self.enc = get_encoding() | |
| self.bos = SPECIAL_TOKENS["<|bos|>"] | |
| self.user_start = SPECIAL_TOKENS["<|user_start|>"] | |
| self.user_end = SPECIAL_TOKENS["<|user_end|>"] | |
| self.asst_start = SPECIAL_TOKENS["<|assistant_start|>"] | |
| self.asst_end = SPECIAL_TOKENS["<|assistant_end|>"] | |
| self.block_size = self.model.config.block_size | |
| # one model on one device can't run two generations at once | |
| self._lock = threading.Lock() | |
| # Encode the few-shot priming turns once; prepended to every prompt below. | |
| self.fewshot_ids = [] | |
| for q, a in FEWSHOT_EXAMPLES: | |
| self.fewshot_ids += self._encode_turn("user", q) + self._encode_turn("assistant", a) | |
| def _encode_turn(self, role, content): | |
| body = self.enc.encode_ordinary(content) | |
| if role == "user": | |
| return [self.user_start, *body, self.user_end] | |
| return [self.asst_start, *body, self.asst_end] | |
| def build_prompt(self, messages, max_new_tokens): | |
| """Tokenize the conversation, dropping oldest whole turns to fit the window.""" | |
| # leave room for <|bos|>, the trailing <|assistant_start|>, and the | |
| # always-kept few-shot priming turns | |
| budget = self.block_size - max_new_tokens - 2 - len(self.fewshot_ids) | |
| turns = [self._encode_turn(m["role"], m["content"]) for m in messages] | |
| kept, total = [], 0 | |
| for turn in reversed(turns): # keep the most recent turns that fit | |
| if total + len(turn) > budget: | |
| break | |
| kept.append(turn) | |
| total += len(turn) | |
| kept.reverse() | |
| ids = [self.bos, *self.fewshot_ids] # prime with the concise examples | |
| for turn in kept: | |
| ids.extend(turn) | |
| ids.append(self.asst_start) # hand the floor to the assistant | |
| return ids | |
| def generate_stream(self, messages, max_new_tokens=200, top_k=50): | |
| """Yield decoded text chunks of the assistant's reply as tokens are sampled.""" | |
| with self._lock: | |
| ids = self.build_prompt(messages, max_new_tokens) | |
| x = torch.tensor(ids, dtype=torch.long, device=self.device).unsqueeze(0) | |
| byte_buf = b"" # holds bytes of an in-progress multibyte character | |
| for _ in range(max_new_tokens): | |
| with torch.no_grad(): | |
| idx_cond = x[:, -self.block_size:] | |
| logits, _ = self.model(idx_cond) | |
| logits = logits[:, -1, :] | |
| probs = F.softmax(logits, dim=-1) | |
| topk_probs, topk_indices = torch.topk(probs, top_k, dim=-1) | |
| ix = torch.multinomial(topk_probs, 1) | |
| next_id = torch.gather(topk_indices, -1, ix) | |
| tok = next_id.item() | |
| if tok >= FIRST_SPECIAL_TOKEN: # <|assistant_end|> or any special token: stop | |
| break | |
| x = torch.cat((x, next_id), dim=1) | |
| # decode safely: only emit once we have complete UTF-8 characters | |
| byte_buf += self.enc.decode_single_token_bytes(tok) | |
| try: | |
| text = byte_buf.decode("utf-8") | |
| except UnicodeDecodeError: | |
| continue # wait for the rest of a multibyte character | |
| byte_buf = b"" | |
| if text: | |
| yield text | |
| if byte_buf: # flush any trailing bytes, ignoring an incomplete tail | |
| tail = byte_buf.decode("utf-8", errors="ignore") | |
| if tail: | |
| yield tail | |