MarkChenX commited on
Commit
d089c8b
·
verified ·
1 Parent(s): ccab657

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +351 -29
app.py CHANGED
@@ -1,9 +1,12 @@
1
  from __future__ import annotations
2
 
3
  import os
 
4
  import re
5
  import sys
 
6
  from pathlib import Path
 
7
 
8
  import gradio as gr
9
  import torch
@@ -13,18 +16,23 @@ from huggingface_hub import snapshot_download
13
  MODEL_ID = "MarkChenX/lfm2-quantum-128m-sft-v2"
14
  HF_TOKEN = os.getenv("HF_TOKEN")
15
 
16
- DEVICE = torch.device(
 
 
17
  "cuda" if torch.cuda.is_available() else "cpu"
18
  )
19
 
20
 
21
- print(f"Downloading custom checkpoint: {MODEL_ID}")
 
 
 
 
22
 
23
  model_dir = Path(
24
  snapshot_download(
25
  repo_id=MODEL_ID,
26
  token=HF_TOKEN,
27
- # Do not download optimizer files into the Space.
28
  ignore_patterns=[
29
  "optim_*.pt",
30
  "optimizer*.pt",
@@ -35,74 +43,388 @@ model_dir = Path(
35
 
36
  print(f"Model repository: {model_dir}")
37
 
 
38
  sys.path.insert(0, str(model_dir))
39
 
 
 
 
 
 
40
  try:
41
  from nanochat.checkpoint_manager import build_model
 
42
  except ImportError as error:
43
  raise RuntimeError(
44
- "Could not import nanochat.checkpoint_manager. "
45
- "Confirm that the model repository contains the nanochat directory."
46
  ) from error
47
 
48
 
 
 
 
 
49
  def find_latest_checkpoint(
50
- repository_dir: Path,
51
  ) -> tuple[Path, int]:
52
- checkpoint_candidates: list[tuple[int, Path]] = []
53
 
54
- for checkpoint in repository_dir.glob("model_*.pt"):
55
  match = re.fullmatch(
56
  r"model_(\d+)\.pt",
57
- checkpoint.name,
58
  )
59
 
60
  if match:
61
- step = int(match.group(1))
62
- checkpoint_candidates.append((step, checkpoint))
 
63
 
64
- if not checkpoint_candidates:
65
  raise FileNotFoundError(
66
- f"No model_XXXXXX.pt checkpoint found in {repository_dir}"
67
  )
68
 
69
- checkpoint_candidates.sort(
70
  key=lambda item: item[0],
71
  reverse=True,
72
  )
73
 
74
- latest_step, latest_path = checkpoint_candidates[0]
75
- return latest_path, latest_step
76
 
77
 
78
  checkpoint_path, checkpoint_step = find_latest_checkpoint(model_dir)
 
79
 
80
- expected_meta_path = model_dir / f"meta_{checkpoint_step:06d}.json"
81
 
82
- if not expected_meta_path.exists():
83
  raise FileNotFoundError(
84
- "The checkpoint metadata file is missing. Expected: "
85
- f"{expected_meta_path}"
86
  )
87
 
88
 
 
89
  print(f"Checkpoint file: {checkpoint_path}")
90
  print(f"Checkpoint step: {checkpoint_step}")
91
- print(f"Metadata file: {expected_meta_path}")
92
- print("Building custom nanochat model...")
93
 
94
 
 
 
 
 
95
  model, tokenizer, metadata = build_model(
96
- str(model_dir),
97
  checkpoint_step,
98
- DEVICE,
99
  "eval",
100
  )
101
 
102
  model.eval()
103
 
104
- print(f"Model loaded successfully on {DEVICE}")
105
- print(
106
- "Model config:",
107
- metadata.get("model_config", "not present"),
108
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
  import os
4
+ import random
5
  import re
6
  import sys
7
+ from contextlib import nullcontext
8
  from pathlib import Path
9
+ from typing import Any, Generator
10
 
11
  import gradio as gr
12
  import torch
 
16
  MODEL_ID = "MarkChenX/lfm2-quantum-128m-sft-v2"
17
  HF_TOKEN = os.getenv("HF_TOKEN")
18
 
19
+ MAX_CONTEXT_TOKENS = 1024
20
+
21
+ device = torch.device(
22
  "cuda" if torch.cuda.is_available() else "cpu"
23
  )
24
 
25
 
26
+ # ---------------------------------------------------------------------
27
+ # Download repository
28
+ # ---------------------------------------------------------------------
29
+
30
+ print(f"Downloading model repository: {MODEL_ID}")
31
 
32
  model_dir = Path(
33
  snapshot_download(
34
  repo_id=MODEL_ID,
35
  token=HF_TOKEN,
 
36
  ignore_patterns=[
37
  "optim_*.pt",
38
  "optimizer*.pt",
 
43
 
44
  print(f"Model repository: {model_dir}")
45
 
46
+ # Make the bundled nanochat package importable.
47
  sys.path.insert(0, str(model_dir))
48
 
49
+
50
+ # ---------------------------------------------------------------------
51
+ # Import custom architecture
52
+ # ---------------------------------------------------------------------
53
+
54
  try:
55
  from nanochat.checkpoint_manager import build_model
56
+ from nanochat.engine import Engine
57
  except ImportError as error:
58
  raise RuntimeError(
59
+ "Could not import the bundled NanoChat code. The model repository "
60
+ "must contain nanochat/checkpoint_manager.py and nanochat/engine.py."
61
  ) from error
62
 
63
 
64
+ # ---------------------------------------------------------------------
65
+ # Locate latest model checkpoint
66
+ # ---------------------------------------------------------------------
67
+
68
  def find_latest_checkpoint(
69
+ directory: Path,
70
  ) -> tuple[Path, int]:
71
+ candidates: list[tuple[int, Path]] = []
72
 
73
+ for path in directory.rglob("model_*.pt"):
74
  match = re.fullmatch(
75
  r"model_(\d+)\.pt",
76
+ path.name,
77
  )
78
 
79
  if match:
80
+ candidates.append(
81
+ (int(match.group(1)), path)
82
+ )
83
 
84
+ if not candidates:
85
  raise FileNotFoundError(
86
+ f"No model_XXXXXX.pt checkpoint found under {directory}"
87
  )
88
 
89
+ candidates.sort(
90
  key=lambda item: item[0],
91
  reverse=True,
92
  )
93
 
94
+ step, checkpoint_path = candidates[0]
95
+ return checkpoint_path, step
96
 
97
 
98
  checkpoint_path, checkpoint_step = find_latest_checkpoint(model_dir)
99
+ checkpoint_dir = checkpoint_path.parent
100
 
101
+ metadata_path = checkpoint_dir / f"meta_{checkpoint_step:06d}.json"
102
 
103
+ if not metadata_path.exists():
104
  raise FileNotFoundError(
105
+ "Checkpoint metadata is missing. Expected: "
106
+ f"{metadata_path}"
107
  )
108
 
109
 
110
+ print(f"Checkpoint directory: {checkpoint_dir}")
111
  print(f"Checkpoint file: {checkpoint_path}")
112
  print(f"Checkpoint step: {checkpoint_step}")
113
+ print(f"Metadata file: {metadata_path}")
114
+ print(f"Loading on device: {device}")
115
 
116
 
117
+ # ---------------------------------------------------------------------
118
+ # Load custom NanoChat model
119
+ # ---------------------------------------------------------------------
120
+
121
  model, tokenizer, metadata = build_model(
122
+ str(checkpoint_dir),
123
  checkpoint_step,
124
+ device,
125
  "eval",
126
  )
127
 
128
  model.eval()
129
 
130
+ engine = Engine(model, tokenizer)
131
+
132
+ assistant_end_token = tokenizer.encode_special(
133
+ "<|assistant_end|>"
134
+ )
135
+
136
+ bos_token = tokenizer.get_bos_token_id()
137
+
138
+
139
+ if device.type == "cuda":
140
+ autocast_context = lambda: torch.amp.autocast(
141
+ device_type="cuda",
142
+ dtype=torch.bfloat16,
143
+ )
144
+ else:
145
+ autocast_context = nullcontext
146
+
147
+
148
+ print("Model and NanoChat engine loaded successfully.")
149
+
150
+
151
+ # ---------------------------------------------------------------------
152
+ # Gradio history conversion
153
+ # ---------------------------------------------------------------------
154
+
155
+ def normalize_history(
156
+ history: list[Any] | None,
157
+ ) -> list[dict[str, str]]:
158
+ messages: list[dict[str, str]] = []
159
+
160
+ for item in history or []:
161
+ # Current Gradio message format.
162
+ if isinstance(item, dict):
163
+ role = item.get("role")
164
+ content = item.get("content")
165
+
166
+ if (
167
+ role in {"user", "assistant"}
168
+ and isinstance(content, str)
169
+ and content.strip()
170
+ ):
171
+ messages.append(
172
+ {
173
+ "role": role,
174
+ "content": content.strip(),
175
+ }
176
+ )
177
+
178
+ # Compatibility with older tuple-style Gradio history.
179
+ elif isinstance(item, (tuple, list)) and len(item) == 2:
180
+ user_content, assistant_content = item
181
+
182
+ if isinstance(user_content, str) and user_content.strip():
183
+ messages.append(
184
+ {
185
+ "role": "user",
186
+ "content": user_content.strip(),
187
+ }
188
+ )
189
+
190
+ if (
191
+ isinstance(assistant_content, str)
192
+ and assistant_content.strip()
193
+ ):
194
+ messages.append(
195
+ {
196
+ "role": "assistant",
197
+ "content": assistant_content.strip(),
198
+ }
199
+ )
200
+
201
+ return messages
202
+
203
+
204
+ # ---------------------------------------------------------------------
205
+ # Build NanoChat-native conversation tokens
206
+ # ---------------------------------------------------------------------
207
+
208
+ def build_conversation_tokens(
209
+ message: str,
210
+ history: list[Any] | None,
211
+ system_message: str,
212
+ ) -> list[int]:
213
+ messages = normalize_history(history)
214
+
215
+ current_message = message.strip()
216
+
217
+ # NanoChat merges system instructions into the first user message.
218
+ if system_message.strip():
219
+ current_message = (
220
+ system_message.strip()
221
+ + "\n\n"
222
+ + current_message
223
+ )
224
+
225
+ messages.append(
226
+ {
227
+ "role": "user",
228
+ "content": current_message,
229
+ }
230
+ )
231
+
232
+ tokens: list[int] = [bos_token]
233
+
234
+ user_start = tokenizer.encode_special("<|user_start|>")
235
+ user_end = tokenizer.encode_special("<|user_end|>")
236
+
237
+ assistant_start = tokenizer.encode_special(
238
+ "<|assistant_start|>"
239
+ )
240
+ assistant_end = tokenizer.encode_special(
241
+ "<|assistant_end|>"
242
+ )
243
+
244
+ for chat_message in messages:
245
+ content_tokens = tokenizer.encode(
246
+ chat_message["content"]
247
+ )
248
+
249
+ if chat_message["role"] == "user":
250
+ tokens.append(user_start)
251
+ tokens.extend(content_tokens)
252
+ tokens.append(user_end)
253
+
254
+ elif chat_message["role"] == "assistant":
255
+ tokens.append(assistant_start)
256
+ tokens.extend(content_tokens)
257
+ tokens.append(assistant_end)
258
+
259
+ # Prime the model to produce the next assistant message.
260
+ tokens.append(assistant_start)
261
+
262
+ # Leave room for generation within the model's 1024-token context.
263
+ return tokens
264
+
265
+
266
+ # ---------------------------------------------------------------------
267
+ # Gradio generation function
268
+ # ---------------------------------------------------------------------
269
+
270
+ def respond(
271
+ message: str,
272
+ history: list[Any],
273
+ system_message: str,
274
+ max_new_tokens: int,
275
+ temperature: float,
276
+ top_k: int,
277
+ ) -> Generator[str, None, None]:
278
+ if not message or not message.strip():
279
+ yield "Please enter a message."
280
+ return
281
+
282
+ max_new_tokens = int(max_new_tokens)
283
+ temperature = float(temperature)
284
+ top_k = int(top_k)
285
+
286
+ try:
287
+ prompt_tokens = build_conversation_tokens(
288
+ message=message,
289
+ history=history,
290
+ system_message=system_message,
291
+ )
292
+
293
+ # Preserve enough context space for newly generated tokens.
294
+ maximum_prompt_length = (
295
+ MAX_CONTEXT_TOKENS - max_new_tokens
296
+ )
297
+
298
+ if maximum_prompt_length <= 0:
299
+ yield (
300
+ "Max new tokens must be smaller than the "
301
+ f"{MAX_CONTEXT_TOKENS}-token context window."
302
+ )
303
+ return
304
+
305
+ if len(prompt_tokens) > maximum_prompt_length:
306
+ # Keep BOS and the newest conversation context.
307
+ prompt_tokens = [
308
+ bos_token,
309
+ *prompt_tokens[-(maximum_prompt_length - 1):],
310
+ ]
311
+
312
+ generated_tokens: list[int] = []
313
+ last_text = ""
314
+
315
+ with autocast_context():
316
+ stream = engine.generate(
317
+ prompt_tokens,
318
+ num_samples=1,
319
+ max_tokens=max_new_tokens,
320
+ temperature=temperature,
321
+ top_k=top_k,
322
+ seed=random.randint(0, 2**31 - 1),
323
+ )
324
+
325
+ for token_column, _token_masks in stream:
326
+ token = token_column[0]
327
+
328
+ if token in {
329
+ assistant_end_token,
330
+ bos_token,
331
+ }:
332
+ break
333
+
334
+ generated_tokens.append(token)
335
+
336
+ current_text = tokenizer.decode(
337
+ generated_tokens
338
+ )
339
+
340
+ # Avoid yielding incomplete UTF-8 replacement characters.
341
+ if current_text.endswith("�"):
342
+ continue
343
+
344
+ if current_text != last_text:
345
+ last_text = current_text
346
+ yield current_text
347
+
348
+ if not generated_tokens:
349
+ yield "The model generated an empty response."
350
+
351
+ except Exception as error:
352
+ yield (
353
+ "Generation failed: "
354
+ f"{type(error).__name__}: {error}"
355
+ )
356
+
357
+
358
+ # ---------------------------------------------------------------------
359
+ # Gradio interface
360
+ # ---------------------------------------------------------------------
361
+
362
+ with gr.Blocks(
363
+ title="LFM2 Quantum 128M",
364
+ ) as demo:
365
+ gr.Markdown(
366
+ """
367
+ # LFM2 Quantum 128M
368
+
369
+ Custom LFM2-style hybrid quantum language model running through
370
+ the bundled NanoChat inference engine.
371
+ """
372
+ )
373
+
374
+ chatbot = gr.ChatInterface(
375
+ fn=respond,
376
+ type="messages",
377
+ chatbot=gr.Chatbot(
378
+ type="messages",
379
+ height=520,
380
+ placeholder=(
381
+ "Ask the LFM2 Quantum model a question."
382
+ ),
383
+ ),
384
+ additional_inputs=[
385
+ gr.Textbox(
386
+ value=(
387
+ "You are a helpful, concise, and friendly assistant."
388
+ ),
389
+ label="System instruction",
390
+ lines=3,
391
+ ),
392
+ gr.Slider(
393
+ minimum=16,
394
+ maximum=512,
395
+ value=256,
396
+ step=16,
397
+ label="Max new tokens",
398
+ ),
399
+ gr.Slider(
400
+ minimum=0.0,
401
+ maximum=2.0,
402
+ value=0.7,
403
+ step=0.1,
404
+ label="Temperature",
405
+ ),
406
+ gr.Slider(
407
+ minimum=1,
408
+ maximum=200,
409
+ value=50,
410
+ step=1,
411
+ label="Top-k",
412
+ ),
413
+ ],
414
+ examples=[
415
+ ["Hello! Introduce yourself."],
416
+ ["Explain quantum machine learning simply."],
417
+ ["What is reinforcement learning?"],
418
+ ],
419
+ )
420
+
421
+
422
+ if __name__ == "__main__":
423
+ demo.queue(
424
+ default_concurrency_limit=1,
425
+ ).launch(
426
+ server_name="0.0.0.0",
427
+ server_port=7860,
428
+ share=False,
429
+ ssr_mode=False,
430
+ )