rsumit1234 commited on
Commit
a9f3eee
·
verified ·
1 Parent(s): fbf35f5

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +213 -54
app.py CHANGED
@@ -1,69 +1,228 @@
1
- import gradio as gr
2
- from huggingface_hub import InferenceClient
 
 
 
 
 
 
 
 
3
 
 
 
 
4
 
5
- def respond(
6
- message,
7
- history: list[dict[str, str]],
8
- system_message,
9
- max_tokens,
10
- temperature,
11
- top_p,
12
- hf_token: gr.OAuthToken,
13
- ):
14
- """
15
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
16
- """
17
- client = InferenceClient(token=hf_token.token, model="openai/gpt-oss-20b")
18
 
19
- messages = [{"role": "system", "content": system_message}]
 
 
 
20
 
21
- messages.extend(history)
 
22
 
23
- messages.append({"role": "user", "content": message})
24
 
25
- response = ""
 
 
26
 
27
- for message in client.chat_completion(
28
- messages,
29
- max_tokens=max_tokens,
30
- stream=True,
31
- temperature=temperature,
32
- top_p=top_p,
33
- ):
34
- choices = message.choices
35
- token = ""
36
- if len(choices) and choices[0].delta.content:
37
- token = choices[0].delta.content
38
 
39
- response += token
40
- yield response
 
 
 
 
 
 
 
 
41
 
 
 
 
 
 
42
 
43
- """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- chatbot = gr.ChatInterface(
47
- respond,
48
- additional_inputs=[
49
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
- gr.Slider(
53
- minimum=0.1,
54
- maximum=1.0,
55
- value=0.95,
56
- step=0.05,
57
- label="Top-p (nucleus sampling)",
58
- ),
59
- ],
60
  )
61
 
62
- with gr.Blocks() as demo:
63
- with gr.Sidebar():
64
- gr.LoginButton()
65
- chatbot.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
 
68
  if __name__ == "__main__":
69
- demo.launch()
 
1
+ """
2
+ Browser chat UI for our trained LLM.
3
+
4
+ Wraps `LLM.generate()` in a Gradio ChatInterface. Streams tokens as they're
5
+ generated. Uses ChatML formatting (same as our SFT pipeline), so it works for
6
+ both base and SFT'd checkpoints — base models just continue more freely,
7
+ SFT'd models actually answer.
8
+
9
+ Usage (local):
10
+ python gradio_app.py --ckpt out/plan_b_110m/ckpt_step1000_model.pt --preset plan_b
11
 
12
+ Usage (HuggingFace Spaces):
13
+ Place this script + model.py + configs.py + the .pt file in the Space repo.
14
+ Spaces auto-runs `app.py` (rename gradio_app.py → app.py).
15
 
16
+ Args:
17
+ --ckpt path to checkpoint .pt file
18
+ --preset which model preset (smoke, plan_b, main)
19
+ --tokenizer HF repo for tokenizer (default: Llama-2 mirror)
20
+ --share create a temporary 72-hour public URL (Gradio's gradio.live)
21
+ """
22
+ import argparse
23
+ import json
24
+ import os
25
+ from pathlib import Path
 
 
 
26
 
27
+ import numpy as np
28
+ import torch
29
+ import gradio as gr
30
+ from transformers import AutoTokenizer
31
 
32
+ from configs import PRESETS
33
+ from model import LLM
34
 
 
35
 
36
+ # ChatML special markers — must match sft.py
37
+ SYS_PROMPT = "You are a helpful assistant."
38
+ B_USR, B_ASST, EOT = "<|user|>\n", "<|assistant|>\n", "<|end|>"
39
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ class Retriever:
42
+ """Tiny in-memory dense retriever over the KB built by build_kb.py."""
43
+ def __init__(self, kb_dir):
44
+ kb_dir = Path(kb_dir)
45
+ meta = json.load(open(kb_dir / "kb.meta.json"))
46
+ self.chunks = [json.loads(l) for l in open(kb_dir / "kb.jsonl")]
47
+ self.emb = np.load(kb_dir / "kb.npy") # already normalized
48
+ from sentence_transformers import SentenceTransformer
49
+ self.encoder = SentenceTransformer(meta["model"])
50
+ print(f"loaded KB: {len(self.chunks)} chunks, dim={meta['dim']}")
51
 
52
+ def topk(self, query, k=3):
53
+ q = self.encoder.encode([query], normalize_embeddings=True)[0]
54
+ scores = self.emb @ q # cosine since both normalized
55
+ idx = np.argsort(-scores)[:k]
56
+ return [(float(scores[i]), self.chunks[i]) for i in idx]
57
 
58
+
59
+ OUT_OF_SCOPE_THRESHOLD = 0.55 # cosine similarity below this → likely off-topic for our Indian KB
60
+ OUT_OF_SCOPE_MSG = (
61
+ "Sorry, this model is specialized for Indian general knowledge "
62
+ "(history, mythology, geography, sports, politics). I don't have "
63
+ "information on that topic."
 
 
 
 
 
 
 
 
 
 
 
64
  )
65
 
66
+
67
+ def build_prompt(history, user_msg, retriever=None, k=5, max_ctx_chars=2400):
68
+ """Render conversation + user message into a ChatML prompt. If a retriever
69
+ is given, retrieved chunks are inlined into the user message. Returns a
70
+ tuple (prompt, out_of_scope) — if out_of_scope is True, the caller should
71
+ short-circuit and return the OUT_OF_SCOPE_MSG."""
72
+ parts = [f"<|system|>\n{SYS_PROMPT}{EOT}\n"]
73
+ for u, a in history:
74
+ parts.append(f"{B_USR}{u}{EOT}\n")
75
+ parts.append(f"{B_ASST}{a}{EOT}\n")
76
+
77
+ out_of_scope = False
78
+ if retriever is not None:
79
+ hits = retriever.topk(user_msg, k=k)
80
+ if hits and hits[0][0] < OUT_OF_SCOPE_THRESHOLD:
81
+ out_of_scope = True
82
+ ctx = ""
83
+ for score, c in hits:
84
+ piece = f"{c['title']}: {c['text']}\n\n"
85
+ remaining = max_ctx_chars - len(ctx)
86
+ if remaining <= 0: break
87
+ if len(piece) > remaining:
88
+ piece = piece[:remaining].rstrip() + "…\n\n"
89
+ ctx += piece
90
+ break
91
+ ctx += piece
92
+ user_block = (
93
+ f"Read the passage and answer the question.\n\n"
94
+ f"{ctx.strip()}\n\nQuestion: {user_msg}"
95
+ )
96
+ else:
97
+ user_block = user_msg
98
+
99
+ parts.append(f"{B_USR}{user_block}{EOT}\n{B_ASST}")
100
+ return "".join(parts), out_of_scope
101
+
102
+
103
+ def make_chat_fn(model, tok, device, max_new_tokens=80, temperature=1.0,
104
+ top_k=1, top_p=1.0, repetition_penalty=1.1, raw_mode=False,
105
+ retriever=None):
106
+ # Defaults are GREEDY (top_k=1, temp=1.0, top_p=1.0) + light repetition penalty.
107
+ # Empirically: a 110M model with retrieved context does best with greedy
108
+ # decoding on factual Q&A. Sampling at any temperature destroys accuracy.
109
+ eos_id = tok.eos_token_id
110
+
111
+ def chat(message, history):
112
+ if raw_mode:
113
+ prompt = message
114
+ else:
115
+ prompt, oos = build_prompt(history, message, retriever=retriever)
116
+ if oos:
117
+ yield OUT_OF_SCOPE_MSG
118
+ return
119
+ ids = tok.encode(prompt, return_tensors="pt").to(device)
120
+ ctx = model.cfg.max_seq_len
121
+ if ids.shape[1] > ctx - max_new_tokens:
122
+ ids = ids[:, -(ctx - max_new_tokens):]
123
+
124
+ out = ids
125
+ is_mcq = ("(A)" in message and "(B)" in message)
126
+ with torch.no_grad():
127
+ for step in range(max_new_tokens):
128
+ logits, _ = model(out[:, -ctx:])
129
+ logits = logits[:, -1, :].float()
130
+
131
+ # Repetition penalty: shrink logits of tokens already in the output.
132
+ # Standard formulation from CTRL paper: divide if positive, mult if negative.
133
+ if repetition_penalty != 1.0:
134
+ seen = out[0].tolist()
135
+ for tid in set(seen[-200:]):
136
+ v = logits[0, tid]
137
+ logits[0, tid] = v / repetition_penalty if v > 0 else v * repetition_penalty
138
+
139
+ logits = logits / temperature
140
+
141
+ # top-k
142
+ if top_k:
143
+ v, _ = torch.topk(logits, top_k)
144
+ logits[logits < v[:, [-1]]] = -float("inf")
145
+
146
+ # top-p (nucleus): keep smallest set of tokens whose cum-prob >= top_p
147
+ if top_p < 1.0:
148
+ sorted_logits, sorted_idx = torch.sort(logits, descending=True)
149
+ cum = torch.softmax(sorted_logits, dim=-1).cumsum(dim=-1)
150
+ mask = cum > top_p
151
+ mask[..., 1:] = mask[..., :-1].clone()
152
+ mask[..., 0] = False
153
+ sorted_logits[mask] = -float("inf")
154
+ logits = torch.full_like(logits, -float("inf")).scatter(1, sorted_idx, sorted_logits)
155
+
156
+ probs = torch.softmax(logits, dim=-1)
157
+ nxt = torch.multinomial(probs, 1)
158
+ if nxt.item() == eos_id:
159
+ break
160
+ out = torch.cat([out, nxt], dim=1)
161
+
162
+ new_text = tok.decode(out[0, ids.shape[1]:].tolist(), skip_special_tokens=True)
163
+ if EOT in new_text:
164
+ yield new_text.split(EOT)[0].rstrip()
165
+ return
166
+ if is_mcq and step > 8 and (new_text.rstrip().endswith(".") or "\n" in new_text[10:]):
167
+ yield new_text.split("\n")[0].rstrip()
168
+ return
169
+ yield new_text
170
+
171
+ return chat
172
+
173
+
174
+ def main():
175
+ ap = argparse.ArgumentParser()
176
+ ap.add_argument("--ckpt", default=os.environ.get("LLM_CKPT", "model.pt"))
177
+ ap.add_argument("--preset", default=os.environ.get("LLM_PRESET", "plan_b"))
178
+ ap.add_argument("--tokenizer", default="NousResearch/Llama-2-7b-hf")
179
+ ap.add_argument("--share", action="store_true")
180
+ ap.add_argument("--raw", action="store_true")
181
+ ap.add_argument("--kb_dir", default=os.environ.get("LLM_KB_DIR", "kb"))
182
+ # On HF Spaces there are no CLI args — parse_args([]) so unknown sys.argv don't break us.
183
+ args, _ = ap.parse_known_args()
184
+
185
+ device = "cuda" if torch.cuda.is_available() else "cpu"
186
+ dtype = torch.float32 if device == "cpu" else torch.bfloat16
187
+ print(f"loading {args.ckpt} on {device}…")
188
+
189
+ mcfg, _ = PRESETS[args.preset]
190
+ model = LLM(mcfg).to(device, dtype=dtype)
191
+ state = torch.load(args.ckpt, map_location=device, weights_only=False)["model"]
192
+ state = {k.replace("_orig_mod.", ""): v for k, v in state.items()}
193
+ model.load_state_dict(state); model.eval()
194
+ print(f"loaded {model.num_params()/1e6:.1f}M params")
195
+
196
+ tok = AutoTokenizer.from_pretrained(args.tokenizer, use_fast=True)
197
+
198
+ retriever = None
199
+ if args.kb_dir and not args.raw:
200
+ retriever = Retriever(args.kb_dir)
201
+
202
+ chat_fn = make_chat_fn(model, tok, device, raw_mode=args.raw, retriever=retriever)
203
+
204
+ if args.raw:
205
+ title = "private-llm (base model) — text continuation only"
206
+ desc = "Base language model trained from scratch on FineWeb-Edu — give it the start of a sentence and it'll try to continue."
207
+ examples = [
208
+ "Once upon a time in a small village,",
209
+ "The history of India can be traced back to",
210
+ "Photosynthesis is the process by which",
211
+ ]
212
+ else:
213
+ title = "🇮🇳 private-llm — 110M Indian-GK chatbot, trained from scratch"
214
+ desc = "110M-param model trained from scratch + RAG over Indian Wikipedia. Indian-GK only. [GitHub](https://github.com/rsumit123/private-llm)"
215
+ examples = [
216
+ "Who is Rama's brother?",
217
+ "Who was the first Prime Minister of India?",
218
+ "What is the capital of India?",
219
+ "Who founded the Maratha Empire?",
220
+ "Tell me about Hanuman.",
221
+ "Who wrote the Indian National Anthem?",
222
+ ]
223
+
224
+ gr.ChatInterface(fn=chat_fn, title=title, description=desc, examples=examples).launch(share=args.share)
225
 
226
 
227
  if __name__ == "__main__":
228
+ main()