File size: 5,913 Bytes
4131912
 
 
 
 
 
 
 
 
ccc8ac4
4131912
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccc8ac4
4131912
 
ccc8ac4
4131912
 
 
 
 
ccc8ac4
4131912
 
 
 
 
 
ccc8ac4
2373965
 
 
 
 
 
550b9a8
2373965
 
 
 
 
 
5fe1541
550b9a8
 
 
2373965
550b9a8
2373965
 
 
 
 
 
5fe1541
550b9a8
 
2373965
 
550b9a8
2373965
 
 
550b9a8
 
 
 
 
 
2373965
 
 
 
 
4131912
 
 
 
 
5fe1541
98ad17b
4131912
 
 
98ad17b
 
4131912
550b9a8
4131912
 
 
 
 
 
 
ccc8ac4
4131912
 
 
 
 
ccc8ac4
4131912
ccc8ac4
4131912
 
ccc8ac4
4131912
 
 
 
 
 
 
ccc8ac4
4131912
 
 
ccc8ac4
4131912
 
 
 
 
 
 
 
 
ccc8ac4
4131912
 
 
 
 
ccc8ac4
4131912
 
 
ccc8ac4
4131912
 
 
 
 
 
ccc8ac4
 
 
4131912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
"""
Gradio chat interface for nanochat models on Hugging Face Spaces.
Downloads the model from HF Hub and serves a streaming chat UI.
"""

import os
import json
import random
import torch
import gradio as gr
from huggingface_hub import snapshot_download

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
MODEL_REPO = os.environ.get("MODEL_REPO", "Slothwolf/MiniChat-0.9B")
MODEL_STEP = int(os.environ.get("MODEL_STEP", "486"))
DEFAULT_TEMPERATURE = float(os.environ.get("DEFAULT_TEMPERATURE", "0.8"))
DEFAULT_MAX_TOKENS = int(os.environ.get("DEFAULT_MAX_TOKENS", "1024"))
DEFAULT_TOP_K = int(os.environ.get("DEFAULT_TOP_K", "50"))

# ---------------------------------------------------------------------------
# Download and load model
# ---------------------------------------------------------------------------
print(f"Downloading model from {MODEL_REPO}...")
model_dir = snapshot_download(MODEL_REPO)

# Point nanochat at the downloaded directory so get_tokenizer() finds tokenizer/
os.environ["NANOCHAT_BASE_DIR"] = model_dir

from nanochat.checkpoint_manager import build_model
from nanochat.engine import Engine

print(f"Loading model (step {MODEL_STEP}) on CPU...")
device = torch.device("cpu")
model, tokenizer, meta = build_model(model_dir, MODEL_STEP, device, "eval")
engine = Engine(model, tokenizer)
print("Model loaded and ready!")

# Grab special token ids once
bos_id = tokenizer.get_bos_token_id()
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|>")




def normalize_history(history):
    if not history:
        return []

    if isinstance(history[0], dict):
        pairs = []
        user_msg = None
        for msg in history:
            role = msg.get("role")
            content = msg.get("content", "")

            if not isinstance(content, str):
                content = str(content) if content is not None else ""

            if role == "user":
                if user_msg is not None:
                    pairs.append([user_msg, None])
                user_msg = content
            elif role == "assistant":
                if user_msg is not None:
                    pairs.append([user_msg, content])
                    user_msg = None
                else:  
                    pairs.append(["", content])
        if user_msg is not None:
            pairs.append([user_msg, None])
        return pairs

    normalized = []
    for turn in history:
        if isinstance(turn, (list, tuple)):
            user = turn[0] if len(turn) > 0 else None
            assistant = turn[1] if len(turn) > 1 else None

            user = str(user) if user is not None else ""
            assistant = str(assistant) if assistant is not None else ""
            normalized.append([user, assistant])
        else:
            continue
    return normalized


# ---------------------------------------------------------------------------
# Chat function (streaming)
# ---------------------------------------------------------------------------
def chat(message, history, temperature, max_tokens, top_k):
    """Generate a streaming response for the chat interface."""

    history = normalize_history(history)
    # Build the full token sequence from conversation history
    # history is a list of [user_msg, assistant_msg] tuples
    tokens = [bos_id]


    for user_msg, assistant_msg in history:
        user_text = user_msg if isinstance(user_msg, str) else str(user_msg) if user_msg is not None else ""
        tokens.append(user_start)
        tokens.extend(tokenizer.encode(user_msg))
        tokens.append(user_end)
        if assistant_msg:
            tokens.append(assistant_start)
            tokens.extend(tokenizer.encode(assistant_msg))
            tokens.append(assistant_end)

    # Add the current user message and prime the assistant
    tokens.append(user_start)
    tokens.extend(tokenizer.encode(message))
    tokens.append(user_end)
    tokens.append(assistant_start)

    # Generate tokens with streaming
    response = ""
    accumulated_tokens = []
    last_clean_text = ""

    for token_column, token_masks in engine.generate(
        tokens,
        num_samples=1,
        max_tokens=int(max_tokens),
        temperature=float(temperature),
        top_k=int(top_k) if int(top_k) > 0 else None,
        seed=random.randint(0, 2**31 - 1),
    ):
        token = token_column[0]
        if token == assistant_end or token == bos_id:
            break

        # Accumulate tokens and decode, handling multi-byte UTF-8
        accumulated_tokens.append(token)
        current_text = tokenizer.decode(accumulated_tokens)
        if not current_text.endswith("\ufffd"):
            new_text = current_text[len(last_clean_text):]
            if new_text:
                response += new_text
                last_clean_text = current_text
                yield response

# ---------------------------------------------------------------------------
# Gradio UI
# ---------------------------------------------------------------------------
demo = gr.ChatInterface(
    chat,
    additional_inputs=[
        gr.Slider(0.0, 2.0, value=DEFAULT_TEMPERATURE, step=0.1, label="Temperature"),
        gr.Slider(1, 2048, value=DEFAULT_MAX_TOKENS, step=1, label="Max Tokens"),
        gr.Slider(0, 200, value=DEFAULT_TOP_K, step=1, label="Top-K (0 = disabled)"),
    ],
    title="MiniChat",
    description=(
        "Chat with a **0.9B parameter** GPT-like model trained with "
        "[nanochat](https://github.com/karpathy/nanochat) by Andrej Karpathy. "
        "Running on CPU — generation will be slow."
    ),
)

if __name__ == "__main__":
    demo.launch()