David-stout commited on
Commit
7968636
·
verified ·
1 Parent(s): 07e9db5

Add official TwIL-LM3 Gradio ZeroGPU demo

Browse files
Files changed (4) hide show
  1. README.md +29 -8
  2. __pycache__/app.cpython-314.pyc +0 -0
  3. app.py +235 -0
  4. requirements.txt +2 -0
README.md CHANGED
@@ -1,13 +1,34 @@
1
  ---
2
- title: TwIL LM3
3
- emoji:
4
- colorFrom: purple
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.24.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: TwIL-LM3
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 6.22.0
 
8
  app_file: app.py
9
+ pinned: true
10
+ license: other
11
+ short_description: Official demo of TwIL-LM3, a 3B formal-logic reasoner
12
+ python_version: "3.12"
13
+ startup_duration_timeout: 30m
14
  ---
15
 
16
+ # TwIL-LM3 Official Demo
17
+
18
+ Official Hugging Face Space for
19
+ [`webAI-Official/TwIL-LM3`](https://huggingface.co/webAI-Official/TwIL-LM3),
20
+ webAI's 3B reasoning model for **formal logic**.
21
+
22
+ TwIL-LM3 is built from
23
+ [`HuggingFaceTB/SmolLM3-3B`](https://huggingface.co/HuggingFaceTB/SmolLM3-3B)
24
+ through LoRA SFT, checkpoint fusion, WiSE-FT interpolation, and entropy-weighted
25
+ GRPO. It emits a collapsible `<think>…</think>` trace before the answer.
26
+
27
+ **Temperature = 0** (greedy) matches the published evaluation protocol. Keep
28
+ max new tokens at 2048 or higher so the reasoning block is not truncated.
29
+
30
+ This Space runs on ZeroGPU and streams tokens as they are generated.
31
+
32
+ > **License:** the model is released under the *webAI Non-Commercial License
33
+ > ver. 1.0* — see the [model repository](https://huggingface.co/webAI-Official/TwIL-LM3)
34
+ > for full terms.
__pycache__/app.cpython-314.pyc ADDED
Binary file (9.19 kB). View file
 
app.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from collections.abc import Iterator
4
+ from threading import Thread
5
+
6
+ import spaces
7
+ import torch
8
+ import gradio as gr
9
+ from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
10
+
11
+ MODEL_ID = "webAI-Official/TwIL-LM3"
12
+ MAX_INPUT_TOKEN_LENGTH = int(os.getenv("MAX_INPUT_TOKEN_LENGTH", "8192"))
13
+
14
+ # Reasoning delimiters are non-special tokens (ids 128002 / 128003), so they
15
+ # survive skip_special_tokens=True and Gradio can render them as a collapsible
16
+ # section via allow_tags=["think"]. Built with chr() so the literal tags are
17
+ # not written into the source.
18
+ THINK_OPEN = chr(60) + "think" + chr(62)
19
+ THINK_CLOSE = chr(60) + chr(47) + "think" + chr(62)
20
+ _THINK_RE = re.compile(
21
+ re.escape(THINK_OPEN) + r".*?(" + re.escape(THINK_CLOSE) + r"|$)",
22
+ re.DOTALL,
23
+ )
24
+
25
+ TITLE = "# TwIL-LM3"
26
+ DESCRIPTION = f"""
27
+ Official demo of **[TwIL-LM3](https://huggingface.co/{MODEL_ID})**, webAI's 3B
28
+ reasoning model for formal logic — FOL translation, entailment, semantic
29
+ parsing, Lean formalisation and critique.
30
+
31
+ Built from [SmolLM3-3B](https://huggingface.co/HuggingFaceTB/SmolLM3-3B) via
32
+ LoRA SFT, checkpoint fusion, WiSE-FT, and entropy-weighted GRPO. It writes a
33
+ collapsible `{THINK_OPEN}` reasoning trace before the answer.
34
+
35
+ **Temperature = 0** (greedy) reproduces the published numbers. Keep max new
36
+ tokens at **2048+** so the thinking block is not cut off.
37
+ """
38
+
39
+ FOOTER = f"""
40
+ ---
41
+ Official Space for [{MODEL_ID}](https://huggingface.co/{MODEL_ID}) ·
42
+ License: [webAI Non-Commercial License ver. 1.0](https://huggingface.co/{MODEL_ID})
43
+ """
44
+
45
+ PLACEHOLDER = """
46
+ <div style="padding: 30px; text-align: center; display: flex; flex-direction: column; align-items: center;">
47
+ <h1 style="font-size: 28px; margin-bottom: 2px; opacity: 0.55;">TwIL-LM3</h1>
48
+ <p style="font-size: 18px; margin-bottom: 2px; opacity: 0.65;">Ask a formal-logic question…</p>
49
+ </div>
50
+ """
51
+
52
+ CSS = """
53
+ #col-container { max-width: 1100px; margin: 0 auto; }
54
+ .dark .gradio-container { color: var(--body-text-color); }
55
+ """
56
+
57
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
58
+ model = AutoModelForCausalLM.from_pretrained(
59
+ MODEL_ID,
60
+ dtype=torch.bfloat16,
61
+ attn_implementation="sdpa",
62
+ ).to("cuda")
63
+ model.eval()
64
+ model.generation_config.use_cache = True
65
+
66
+ EOS_TOKEN_ID = tokenizer.eos_token_id
67
+
68
+
69
+ def _strip_thinking(text: str) -> str:
70
+ """Drop the reasoning trace from a prior assistant turn before re-prompting."""
71
+ if not isinstance(text, str):
72
+ return ""
73
+ return _THINK_RE.sub("", text).strip()
74
+
75
+
76
+ def _gpu_seconds(
77
+ message,
78
+ history,
79
+ max_new_tokens=2048,
80
+ temperature=0,
81
+ top_p=0.95,
82
+ enable_thinking=True,
83
+ *args,
84
+ **kwargs,
85
+ ):
86
+ tokens = int(max_new_tokens or 2048)
87
+ return min(180, max(45, 25 + tokens // 18))
88
+
89
+
90
+ @spaces.GPU(duration=_gpu_seconds)
91
+ def chat_twil_lm3(
92
+ message: str,
93
+ history: list,
94
+ max_new_tokens: int,
95
+ temperature: float,
96
+ top_p: float,
97
+ enable_thinking: bool,
98
+ ) -> Iterator[str]:
99
+ conversation = []
100
+ for msg in history or []:
101
+ role = msg.get("role", "user")
102
+ content = msg.get("content", "")
103
+ if isinstance(content, list):
104
+ content = ""
105
+ if role == "assistant":
106
+ content = _strip_thinking(content)
107
+ conversation.append({"role": role, "content": content})
108
+ conversation.append({"role": "user", "content": message})
109
+
110
+ encoded = tokenizer.apply_chat_template(
111
+ conversation,
112
+ add_generation_prompt=True,
113
+ return_tensors="pt",
114
+ return_dict=True,
115
+ enable_thinking=enable_thinking,
116
+ )
117
+ input_ids = encoded["input_ids"]
118
+ attention_mask = encoded["attention_mask"]
119
+ if input_ids.shape[1] > MAX_INPUT_TOKEN_LENGTH:
120
+ input_ids = input_ids[:, -MAX_INPUT_TOKEN_LENGTH:]
121
+ attention_mask = attention_mask[:, -MAX_INPUT_TOKEN_LENGTH:]
122
+ gr.Warning(
123
+ f"Trimmed the conversation to the last {MAX_INPUT_TOKEN_LENGTH} tokens."
124
+ )
125
+
126
+ input_ids = input_ids.to(model.device)
127
+ attention_mask = attention_mask.to(model.device)
128
+
129
+ streamer = TextIteratorStreamer(
130
+ tokenizer, timeout=30.0, skip_prompt=True, skip_special_tokens=True
131
+ )
132
+ generate_kwargs = dict(
133
+ input_ids=input_ids,
134
+ attention_mask=attention_mask,
135
+ streamer=streamer,
136
+ max_new_tokens=int(max_new_tokens),
137
+ num_beams=1,
138
+ use_cache=True,
139
+ eos_token_id=EOS_TOKEN_ID,
140
+ pad_token_id=tokenizer.pad_token_id or EOS_TOKEN_ID,
141
+ )
142
+ if temperature == 0:
143
+ generate_kwargs["do_sample"] = False
144
+ else:
145
+ generate_kwargs["do_sample"] = True
146
+ generate_kwargs["temperature"] = float(temperature)
147
+ generate_kwargs["top_p"] = float(top_p)
148
+
149
+ Thread(target=model.generate, kwargs=generate_kwargs, daemon=True).start()
150
+
151
+ chunks: list[str] = []
152
+ for text in streamer:
153
+ chunks.append(text)
154
+ yield "".join(chunks)
155
+
156
+
157
+ chatbot = gr.Chatbot(
158
+ height=450,
159
+ placeholder=PLACEHOLDER,
160
+ label="TwIL-LM3",
161
+ allow_tags=["think"],
162
+ line_breaks=False,
163
+ )
164
+
165
+ with gr.Blocks(theme=gr.themes.Soft(), css=CSS, fill_height=True) as demo:
166
+ with gr.Column(elem_id="col-container"):
167
+ gr.Markdown(TITLE)
168
+ gr.Markdown(DESCRIPTION)
169
+ gr.ChatInterface(
170
+ fn=chat_twil_lm3,
171
+ chatbot=chatbot,
172
+ fill_height=True,
173
+ concurrency_limit=1,
174
+ additional_inputs_accordion=gr.Accordion(
175
+ label="Parameters", open=False, render=False
176
+ ),
177
+ additional_inputs=[
178
+ gr.Slider(
179
+ minimum=256,
180
+ maximum=4096,
181
+ step=256,
182
+ value=2048,
183
+ label="Max new tokens",
184
+ render=False,
185
+ info="Keep this at 2048+ so the reasoning trace is not truncated.",
186
+ ),
187
+ gr.Slider(
188
+ minimum=0,
189
+ maximum=1.5,
190
+ step=0.05,
191
+ value=0,
192
+ label="Temperature (0 = greedy)",
193
+ render=False,
194
+ info="Greedy (0) matches the published evaluation.",
195
+ ),
196
+ gr.Slider(
197
+ minimum=0.1,
198
+ maximum=1.0,
199
+ step=0.05,
200
+ value=0.95,
201
+ label="Top-p",
202
+ render=False,
203
+ info="Used only when temperature > 0.",
204
+ ),
205
+ gr.Checkbox(
206
+ value=True,
207
+ label="Enable thinking (reasoning trace)",
208
+ render=False,
209
+ info="When on, the model reasons in a hidden block before answering.",
210
+ ),
211
+ ],
212
+ examples=[
213
+ [
214
+ "Does 'All dogs are mammals. Rex is a dog.' entail 'Rex is a mammal'? "
215
+ "Answer entailment, contradiction, or neutral."
216
+ ],
217
+ [
218
+ "Translate into first-order logic: Every student who studies hard passes at least one exam."
219
+ ],
220
+ [
221
+ "Formalize in Lean 4: If n is even, then n^2 is even."
222
+ ],
223
+ [
224
+ "Is 'All birds fly. Tweety is a bird. Therefore Tweety flies.' logically valid? Explain."
225
+ ],
226
+ [
227
+ "Formalize and evaluate: If it rains, the ground is wet. The ground is not wet. Therefore it did not rain."
228
+ ],
229
+ ],
230
+ cache_examples=False,
231
+ )
232
+ gr.Markdown(FOOTER)
233
+
234
+ if __name__ == "__main__":
235
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ transformers>=5.0.0
2
+ accelerate