Spaces:
Running
Running
| from __future__ import annotations | |
| import os | |
| import random | |
| import re | |
| import sys | |
| from contextlib import nullcontext | |
| from pathlib import Path | |
| from typing import Any, Generator | |
| import gradio as gr | |
| import torch | |
| from huggingface_hub import snapshot_download | |
| MODEL_ID = "MarkChenX/lfm2-quantum-128m-sft-v2-reasoning" | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| MAX_CONTEXT_TOKENS = 1024 | |
| device = torch.device( | |
| "cuda" if torch.cuda.is_available() else "cpu" | |
| ) | |
| # --------------------------------------------------------------------- | |
| # Download repository | |
| # --------------------------------------------------------------------- | |
| print(f"Downloading model repository: {MODEL_ID}") | |
| model_dir = Path( | |
| snapshot_download( | |
| repo_id=MODEL_ID, | |
| token=HF_TOKEN, | |
| ignore_patterns=[ | |
| "optim_*.pt", | |
| "optimizer*.pt", | |
| "*.bin", | |
| ], | |
| ) | |
| ) | |
| print(f"Model repository: {model_dir}") | |
| # Make the bundled nanochat package importable. | |
| sys.path.insert(0, str(model_dir)) | |
| # --------------------------------------------------------------------- | |
| # Import custom architecture | |
| # --------------------------------------------------------------------- | |
| try: | |
| from nanochat.checkpoint_manager import build_model | |
| from nanochat.engine import Engine | |
| except ImportError as error: | |
| raise RuntimeError( | |
| "Could not import the bundled NanoChat code. The model repository " | |
| "must contain nanochat/checkpoint_manager.py and nanochat/engine.py." | |
| ) from error | |
| # --------------------------------------------------------------------- | |
| # Locate latest model checkpoint | |
| # --------------------------------------------------------------------- | |
| def find_latest_checkpoint( | |
| directory: Path, | |
| ) -> tuple[Path, int]: | |
| candidates: list[tuple[int, Path]] = [] | |
| for path in directory.rglob("model_*.pt"): | |
| match = re.fullmatch( | |
| r"model_(\d+)\.pt", | |
| path.name, | |
| ) | |
| if match: | |
| candidates.append( | |
| (int(match.group(1)), path) | |
| ) | |
| if not candidates: | |
| raise FileNotFoundError( | |
| f"No model_XXXXXX.pt checkpoint found under {directory}" | |
| ) | |
| candidates.sort( | |
| key=lambda item: item[0], | |
| reverse=True, | |
| ) | |
| step, checkpoint_path = candidates[0] | |
| return checkpoint_path, step | |
| checkpoint_path, checkpoint_step = find_latest_checkpoint(model_dir) | |
| checkpoint_dir = checkpoint_path.parent | |
| metadata_path = checkpoint_dir / f"meta_{checkpoint_step:06d}.json" | |
| if not metadata_path.exists(): | |
| raise FileNotFoundError( | |
| "Checkpoint metadata is missing. Expected: " | |
| f"{metadata_path}" | |
| ) | |
| print(f"Checkpoint directory: {checkpoint_dir}") | |
| print(f"Checkpoint file: {checkpoint_path}") | |
| print(f"Checkpoint step: {checkpoint_step}") | |
| print(f"Metadata file: {metadata_path}") | |
| print(f"Loading on device: {device}") | |
| # --------------------------------------------------------------------- | |
| # Load custom NanoChat model | |
| # --------------------------------------------------------------------- | |
| model, tokenizer, metadata = build_model( | |
| str(checkpoint_dir), | |
| checkpoint_step, | |
| device, | |
| "eval", | |
| ) | |
| model.eval() | |
| engine = Engine(model, tokenizer) | |
| assistant_end_token = tokenizer.encode_special( | |
| "<|assistant_end|>" | |
| ) | |
| bos_token = tokenizer.get_bos_token_id() | |
| if device.type == "cuda": | |
| autocast_context = lambda: torch.amp.autocast( | |
| device_type="cuda", | |
| dtype=torch.bfloat16, | |
| ) | |
| else: | |
| autocast_context = nullcontext | |
| print("Model and NanoChat engine loaded successfully.") | |
| # --------------------------------------------------------------------- | |
| # Gradio history conversion | |
| # --------------------------------------------------------------------- | |
| def normalize_history( | |
| history: list[Any] | None, | |
| ) -> list[dict[str, str]]: | |
| messages: list[dict[str, str]] = [] | |
| for item in history or []: | |
| # Current Gradio message format. | |
| if isinstance(item, dict): | |
| role = item.get("role") | |
| content = item.get("content") | |
| if ( | |
| role in {"user", "assistant"} | |
| and isinstance(content, str) | |
| and content.strip() | |
| ): | |
| messages.append( | |
| { | |
| "role": role, | |
| "content": content.strip(), | |
| } | |
| ) | |
| # Compatibility with older tuple-style Gradio history. | |
| elif isinstance(item, (tuple, list)) and len(item) == 2: | |
| user_content, assistant_content = item | |
| if isinstance(user_content, str) and user_content.strip(): | |
| messages.append( | |
| { | |
| "role": "user", | |
| "content": user_content.strip(), | |
| } | |
| ) | |
| if ( | |
| isinstance(assistant_content, str) | |
| and assistant_content.strip() | |
| ): | |
| messages.append( | |
| { | |
| "role": "assistant", | |
| "content": assistant_content.strip(), | |
| } | |
| ) | |
| return messages | |
| # --------------------------------------------------------------------- | |
| # Build NanoChat-native conversation tokens | |
| # --------------------------------------------------------------------- | |
| def build_conversation_tokens( | |
| message: str, | |
| history: list[Any] | None, | |
| system_message: str, | |
| ) -> list[int]: | |
| messages = normalize_history(history) | |
| current_message = message.strip() | |
| # NanoChat merges system instructions into the first user message. | |
| if system_message.strip(): | |
| current_message = ( | |
| system_message.strip() | |
| + "\n\n" | |
| + current_message | |
| ) | |
| messages.append( | |
| { | |
| "role": "user", | |
| "content": current_message, | |
| } | |
| ) | |
| tokens: list[int] = [bos_token] | |
| user_start = tokenizer.encode_special("<|user_start|>") | |
| user_end = tokenizer.encode_special("<|user_end|>") | |
| assistant_start = tokenizer.encode_special( | |
| "<|assistant_start|>" | |
| ) | |
| assistant_end = tokenizer.encode_special( | |
| "<|assistant_end|>" | |
| ) | |
| for chat_message in messages: | |
| content_tokens = tokenizer.encode( | |
| chat_message["content"] | |
| ) | |
| if chat_message["role"] == "user": | |
| tokens.append(user_start) | |
| tokens.extend(content_tokens) | |
| tokens.append(user_end) | |
| elif chat_message["role"] == "assistant": | |
| tokens.append(assistant_start) | |
| tokens.extend(content_tokens) | |
| tokens.append(assistant_end) | |
| # Prime the model to produce the next assistant message. | |
| tokens.append(assistant_start) | |
| # Leave room for generation within the model's 1024-token context. | |
| return tokens | |
| # --------------------------------------------------------------------- | |
| # Gradio generation function | |
| # --------------------------------------------------------------------- | |
| def respond( | |
| message: str, | |
| history: list[Any], | |
| system_message: str, | |
| max_new_tokens: int, | |
| temperature: float, | |
| top_k: int, | |
| ) -> Generator[str, None, None]: | |
| if not message or not message.strip(): | |
| yield "Please enter a message." | |
| return | |
| max_new_tokens = int(max_new_tokens) | |
| temperature = float(temperature) | |
| top_k = int(top_k) | |
| try: | |
| prompt_tokens = build_conversation_tokens( | |
| message=message, | |
| history=history, | |
| system_message=system_message, | |
| ) | |
| # Preserve enough context space for newly generated tokens. | |
| maximum_prompt_length = ( | |
| MAX_CONTEXT_TOKENS - max_new_tokens | |
| ) | |
| if maximum_prompt_length <= 0: | |
| yield ( | |
| "Max new tokens must be smaller than the " | |
| f"{MAX_CONTEXT_TOKENS}-token context window." | |
| ) | |
| return | |
| if len(prompt_tokens) > maximum_prompt_length: | |
| # Keep BOS and the newest conversation context. | |
| prompt_tokens = [ | |
| bos_token, | |
| *prompt_tokens[-(maximum_prompt_length - 1):], | |
| ] | |
| generated_tokens: list[int] = [] | |
| last_text = "" | |
| with autocast_context(): | |
| stream = engine.generate( | |
| prompt_tokens, | |
| num_samples=1, | |
| max_tokens=max_new_tokens, | |
| temperature=temperature, | |
| top_k=top_k, | |
| seed=random.randint(0, 2**31 - 1), | |
| ) | |
| for token_column, _token_masks in stream: | |
| token = token_column[0] | |
| if token in { | |
| assistant_end_token, | |
| bos_token, | |
| }: | |
| break | |
| generated_tokens.append(token) | |
| current_text = tokenizer.decode( | |
| generated_tokens | |
| ) | |
| # Avoid yielding incomplete UTF-8 replacement characters. | |
| if current_text.endswith("�"): | |
| continue | |
| if current_text != last_text: | |
| last_text = current_text | |
| yield current_text | |
| if not generated_tokens: | |
| yield "The model generated an empty response." | |
| except Exception as error: | |
| yield ( | |
| "Generation failed: " | |
| f"{type(error).__name__}: {error}" | |
| ) | |
| # --------------------------------------------------------------------- | |
| # Gradio interface | |
| # --------------------------------------------------------------------- | |
| with gr.Blocks( | |
| title="LFM2 Quantum 128M", | |
| ) as demo: | |
| gr.Markdown( | |
| """ | |
| # LFM2 Quantum 128M | |
| Custom LFM2-style hybrid quantum language model running with | |
| the bundled NanoChat checkpoint code. | |
| """ | |
| ) | |
| gr.ChatInterface( | |
| fn=respond, | |
| chatbot=gr.Chatbot( | |
| height=520, | |
| placeholder="Ask the model a question.", | |
| ), | |
| additional_inputs=[ | |
| gr.Textbox( | |
| value=( | |
| "You are a helpful, concise, and friendly assistant." | |
| ), | |
| label="System instruction", | |
| lines=3, | |
| ), | |
| gr.Slider( | |
| minimum=16, | |
| maximum=512, | |
| value=256, | |
| step=16, | |
| label="Max new tokens", | |
| ), | |
| gr.Slider( | |
| minimum=0.0, | |
| maximum=2.0, | |
| value=0.7, | |
| step=0.1, | |
| label="Temperature", | |
| ), | |
| gr.Slider( | |
| minimum=1, | |
| maximum=200, | |
| value=50, | |
| step=1, | |
| label="Top-k", | |
| ), | |
| ], | |
| examples=[ | |
| [ | |
| "Hello! Introduce yourself.", | |
| "You are a helpful, concise, and friendly assistant.", | |
| 256, | |
| 0.7, | |
| 50, | |
| ], | |
| [ | |
| "Explain quantum machine learning simply.", | |
| "You are a helpful AI research assistant.", | |
| 256, | |
| 0.7, | |
| 50, | |
| ], | |
| ], | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue( | |
| default_concurrency_limit=1, | |
| ).launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| ssr_mode=False, | |
| ) |