import spaces
import gradio as gr
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer, StoppingCriteria, StoppingCriteriaList
from huggingface_hub import HfApi
from threading import Thread
import gc
import os
import shutil
import torch
import psutil
import time
import queue as _queue
HF_CACHE_DIR = os.path.expanduser("~/.cache/huggingface/hub")
DEFAULT_MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
MODELS = [
"HuggingFaceTB/SmolLM2-135M-Instruct",
"HuggingFaceTB/SmolLM2-135M",
"HuggingFaceTB/SmolLM2-360M-Instruct",
"HuggingFaceTB/SmolLM2-1.7B-Instruct",
"HuggingFaceTB/SmolLM-135M",
"Qwen/Qwen3-0.6B",
"Qwen/Qwen2.5-Coder-0.5B",
"Qwen/Qwen2.5-0.5B",
"Qwen/Qwen2.5-1.5B",
"Qwen/Qwen2.5-3B",
"Qwen/Qwen3-1.7B",
"facebook/MobileLLM-R1-140M-base",
"facebook/opt-125m",
"facebook/opt-350m",
"microsoft/phi-2",
"microsoft/Phi-3.5-mini-instruct",
"microsoft/Phi-3-mini-4k-instruct",
"openai-community/gpt2",
"openai-community/gpt2-medium",
"EleutherAI/pythia-70m",
"EleutherAI/pythia-160m",
"EleutherAI/pythia-410m",
"EleutherAI/gpt-neo-125M",
"EleutherAI/gpt-neo-1.3B",
"TinyLlama/TinyLlama-1.1B-Chat-v1.0",
"stabilityai/StableLM-3b-4e1t",
"stabilityai/StableLM-Zephyr-3B",
"NousResearch/Hermes-3-Llama-3.1-8B",
"meta-llama/Llama-3.2-1B",
"SupraLabs/Supra-50M-Base",
"SupraLabs/Supra-50M-Instruct",
"SupraLabs/Supra-50M-Reasoning",
"GODELEV/Archaea-74M",
"Sandroeth/cali-0.1B",
"ThingAI/Quark-50m",
"ThingAI/Quark-135m",
"Aravindan/awesome-gpt-2-coder",
"LiquidAI/LFM2-1.2B",
"LiquidAI/LFM2-2.6B",
"LiquidAI/LFM2.5-230M",
"LiquidAI/LFM2.5-350M",
"LiquidAI/LFM2.5-1.2B-Instruct",
"LiquidAI/LFM2.5-1.2B-Thinking",
"LiquidAI/LFM2.5-8B-A1B",
]
TASK_MODES = ["Completion", "Chat", "Q&A", "Translation"]
LANGUAGES = [
"English", "Spanish", "French", "German", "Italian", "Portuguese",
"Chinese", "Japanese", "Korean", "Arabic", "Russian", "Hindi",
"Dutch", "Turkish", "Polish", "Czech", "Romanian", "Greek",
"Thai", "Vietnamese", "Indonesian", "Malay", "Finnish", "Swedish",
"Norwegian", "Danish"
]
ACTIVE_SESSIONS = {}
SESSION_TIMEOUT = 60
THINKING_PATTERNS = [
# (start_token, end_token, thinking_label, answer_label)
("<|begin_of_thought|>", "<|end_of_thought|>", "Thinking Process:", "Final Answer:"),
("", "", "Thinking:", "Answer:"),
("", "", "Thinking:", "Answer:"),
]
def live_count(request: gr.Request):
current_time = time.time()
if request:
ACTIVE_SESSIONS[request.session_hash] = current_time
expired = [s for s, t in ACTIVE_SESSIONS.items() if current_time - t > SESSION_TIMEOUT]
for s in expired:
ACTIVE_SESSIONS.pop(s, None)
return len(ACTIVE_SESSIONS)
class ModelManager:
def __init__(self):
self.model = None
self.tokenizer = None
self.model_id = None
self.stop_generation = False
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.thinking = None # (start, end, think_label, answer_label) or None
model_manager = ModelManager()
def detect_thinking(tokenizer):
if tokenizer is None:
return None
try:
special = set()
if hasattr(tokenizer, "additional_special_tokens"):
special.update(tokenizer.additional_special_tokens)
if hasattr(tokenizer, "added_tokens_decoder"):
special.update(str(v) for v in tokenizer.added_tokens_decoder.values())
for start, end, think_label, answer_label in THINKING_PATTERNS:
if start in special or start in tokenizer.get_vocab():
return (start, end, think_label, answer_label)
except Exception:
pass
return None
class StopOnFlag(StoppingCriteria):
def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool:
return model_manager.stop_generation
_search_cache = {}
_last_search_time = 0
def search_hf_models(query):
global _last_search_time
import time as _time
_last_search_time = _time.time()
if not query or len(query) < 2:
return gr.update(choices=MODELS)
q = query.strip().lower()
if q in _search_cache:
return gr.update(choices=_search_cache[q])
try:
api = HfApi()
models = list(api.list_models(search=query, limit=15, sort="downloads"))
model_ids = [m.id for m in models if m.id]
if model_ids:
_search_cache[q] = model_ids
return gr.update(choices=model_ids)
return gr.update(choices=MODELS)
except Exception:
return gr.update(choices=MODELS)
def get_system_stats(request: gr.Request = None):
mem = psutil.virtual_memory()
disk = psutil.disk_usage('/')
return (
f"CPU\t\t: \t{psutil.cpu_percent(interval=1)}%\n"
f"Mem\t\t: \t{round(mem.used / (1024**3), 2)} / {round(mem.total / (1024**3), 2)} GB\n"
f"Disk\t\t: \t{round(disk.used / (1024**3), 2)} / {round(disk.total / (1024**3), 2)} GB\n"
f"Active\t: \t{len(ACTIVE_SESSIONS) if request is None else live_count(request)} session(s)"
)
def load_new_model(model_id):
model_manager.stop_generation = True
model_manager.model = None
model_manager.tokenizer = None
model_manager.model_id = None
yield f"Loading {model_id}..."
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
try:
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_id, trust_remote_code=True, dtype=torch.float16
)
model_manager.tokenizer = tokenizer
model_manager.model = model
model_manager.model_id = model_id
model_manager.thinking = detect_thinking(tokenizer)
tag = " (thinking model)" if model_manager.thinking else ""
yield f"Loaded **{model_id}** on {model_manager.device.upper()}{tag}"
except Exception as e:
yield f"Error loading model: {str(e)}"
def update_mode_ui(mode):
show_sys = mode == "Chat"
show_ctx = mode == "Q&A"
show_src = mode == "Translation"
show_tgt = mode == "Translation"
defaults = {
"Completion": ("Prompt", "Enter your prompt here...", "Once upon a time in a digital kingdom,"),
"Chat": ("User Message", "Type your message...", "What is the capital of France?"),
"Q&A": ("Question", "Enter your question...", "How does photosynthesis work?"),
"Translation": ("Text to Translate", "Enter text to translate...", "Hello, how are you today?"),
}
label, placeholder, default = defaults.get(mode, defaults["Completion"])
return (
gr.update(visible=show_sys),
gr.update(label=label, placeholder=placeholder, value=default),
gr.update(visible=show_ctx),
gr.update(visible=show_src),
gr.update(visible=show_tgt),
)
def format_prompt(mode, prompt, system_prompt="", context="", src_lang="", tgt_lang=""):
if mode == "Completion":
return prompt
elif mode == "Chat":
if model_manager.tokenizer and hasattr(model_manager.tokenizer, "apply_chat_template"):
try:
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
return model_manager.tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True,
enable_thinking=False
)
except Exception:
pass
parts = []
if system_prompt:
parts.append(f"[SYSTEM]: {system_prompt}")
parts.append(f"[USER]: {prompt}")
parts.append("[ASSISTANT]:")
return "\n\n".join(parts)
elif mode == "Q&A":
if context and context.strip():
return f"Context:\n{context.strip()}\n\nQuestion: {prompt.strip()}\n\nAnswer:"
return f"Question: {prompt.strip()}\n\nAnswer:"
elif mode == "Translation":
src = src_lang or "English"
tgt = tgt_lang or "Spanish"
return f"Translate the following text from {src} to {tgt}:\n\n{prompt.strip()}\n\nTranslation:"
return prompt
def estimate_duration(user_prompt, max_tokens, temperature, top_k, top_p, rep_penalty, ngram_size, do_sample, show_prompt=False):
return min(max(int(max_tokens) // 20, 15), 60)
def run_inference(mode, prompt, system_prompt, context, src_lang, tgt_lang,
max_tokens, temperature, top_k, top_p, rep_penalty,
ngram_size, do_sample, gpu, stream_timeout):
formatted = format_prompt(mode, prompt, system_prompt, context, src_lang, tgt_lang)
show_prompt = mode == "Completion"
if gpu:
try:
yield from run_inference_gpu(formatted, max_tokens, temperature, top_k, top_p, rep_penalty, ngram_size, do_sample, show_prompt, stream_timeout)
except Exception as e:
yield f"GPU error: {e}\n\nClick **Generate** to retry.", "GPU Unavailable"
else:
yield from run_inference_raw(formatted, max_tokens, temperature, top_k, top_p, rep_penalty, ngram_size, do_sample, show_prompt=show_prompt, stream_timeout=stream_timeout)
@spaces.GPU(duration=estimate_duration)
def run_inference_gpu(user_prompt, max_tokens, temperature, top_k, top_p, rep_penalty, ngram_size, do_sample, show_prompt=False, stream_timeout=120):
if model_manager.model is not None:
model_manager.model = model_manager.model.to("cuda")
yield from run_inference_raw(user_prompt, max_tokens, temperature, top_k, top_p, rep_penalty, ngram_size, do_sample, use_cuda=True, show_prompt=show_prompt, stream_timeout=stream_timeout)
def run_inference_raw(user_prompt, max_tokens, temperature, top_k, top_p, rep_penalty, ngram_size, do_sample, use_cuda=False, show_prompt=False, stream_timeout=120):
if model_manager.model is None or model_manager.tokenizer is None:
yield "Please load a model first.", "Model not loaded"
return
model_manager.stop_generation = False
tokenizer = model_manager.tokenizer
model = model_manager.model
thinking = model_manager.thinking
# NOTE: Jangan paksa thinking dengan append start-token. Kalau
# enable_thinking=False sudah dipakai di format_prompt (Chat mode),
# append ini justru membatalkannya dan memaksa model berpikir.
inputs = tokenizer([user_prompt], return_tensors="pt")
if use_cuda:
inputs = {k: v.to("cuda") for k, v in inputs.items()}
else:
model = model.to("cpu")
inputs = {k: v.to("cpu") for k, v in inputs.items()}
streamer = TextIteratorStreamer(tokenizer, timeout=float(stream_timeout), skip_prompt=True, skip_special_tokens=True)
if not do_sample:
temperature = 1.0
generate_kwargs = dict(
**inputs,
streamer=streamer,
max_new_tokens=int(max_tokens),
temperature=float(temperature),
top_k=int(top_k),
top_p=float(top_p),
repetition_penalty=float(rep_penalty),
no_repeat_ngram_size=int(ngram_size),
do_sample=do_sample,
pad_token_id=tokenizer.eos_token_id,
stopping_criteria=StoppingCriteriaList([StopOnFlag()])
)
start_time = time.time()
thread = Thread(target=model.generate, kwargs=generate_kwargs)
thread.start()
if thinking:
base_display = ""
generated_text = ""
elif show_prompt:
base_display = user_prompt
generated_text = ""
else:
base_display = ""
generated_text = ""
token_count = 0
try:
for new_text in streamer:
if model_manager.stop_generation:
break
generated_text += new_text
token_count += 1
duration = time.time() - start_time
tps = token_count / duration if duration > 0 else 0
display_text = generated_text
if thinking:
start_tok, end_tok, think_label, answer_label = thinking
# Hanya bungkus sebagai thinking kalau output BENAR-BENAR
# mengandung token thinking. Kalau thinking dimatikan
# (enable_thinking=False), output adalah teks biasa (mis. JSON)
# dan harus ditampilkan apa adanya tanpa prefix "> " / "*...*".
if start_tok in generated_text or end_tok in generated_text:
clean = generated_text.replace("", "").replace("", "")
clean = clean.replace(start_tok, "").replace(end_tok, "")
clean = clean.replace("<|begin_of_solution|>", "").replace("<|end_of_solution|>", "")
if end_tok in generated_text:
parts = generated_text.split(end_tok, 1)
think_raw = parts[0].replace(start_tok, "").strip()
answer_raw = parts[1].replace("<|begin_of_solution|>", "").replace("<|end_of_solution|>", "").strip()
think_block = "\n".join("> " + line for line in think_raw.splitlines()) if think_raw else "> _thinking..._"
display_text = f"{think_block}\n\n**{answer_raw}**"
else:
think_raw = clean.strip()
think_block = "\n".join("> " + line for line in think_raw.splitlines()) if think_raw else "> _thinking..._"
display_text = f"{think_block}\n\n*...*"
device_label = "CUDA" if use_cuda else "CPU"
yield base_display + display_text, f"Speed: {tps:.2f} tokens/sec ({device_label})"
except _queue.Empty:
device_label = "CUDA" if use_cuda else "CPU"
yield base_display + generated_text, f"Stream timed out after {stream_timeout}s ({device_label})"
def clean_cache():
if os.path.exists(HF_CACHE_DIR):
shutil.rmtree(HF_CACHE_DIR)
os.makedirs(HF_CACHE_DIR)
return "Cache cleaned successfully!"
return "Cache directory not found."
with gr.Blocks(title="SLM Model Tester", css="""
#output-box {
background: var(--background-fill-secondary);
border: 1px solid var(--border-color-primary);
border-radius: var(--radius-lg);
padding: 16px;
min-height: 200px;
font-size: 15px;
line-height: 1.6;
}
""") as app:
gr.Markdown("# SLM Model Evaluation Hub")
with gr.Row():
with gr.Column(scale=1, min_width=300):
with gr.Accordion("System", open=False):
stats_output = gr.Textbox(label="Stats", show_label=False, max_lines=5)
gr.Timer(2).tick(get_system_stats, None, stats_output)
with gr.Group():
model_input = gr.Dropdown(
choices=MODELS, label="Model", value=DEFAULT_MODEL,
allow_custom_value=True,
)
search_btn = gr.Button("Search HuggingFace", variant="secondary")
with gr.Row():
load_btn = gr.Button("Load Model", variant="primary", scale=2)
clean_btn = gr.Button("Clear Cache", variant="stop", scale=1)
with gr.Group():
mode_input = gr.Dropdown(
choices=TASK_MODES, value="Completion", label="Mode"
)
use_gpu = gr.Checkbox(label="Use GPU", value=True)
with gr.Accordion("Parameters", open=False):
do_sample_input = gr.Checkbox(label="Sampling", value=True, info="Uncheck for greedy")
max_tokens_input = gr.Slider(minimum=10, maximum=102400, value=256, step=1, label="Max Tokens")
temperature_input = gr.Slider(minimum=0.1, maximum=2.0, value=0.7, step=0.1, label="Temperature")
top_k_input = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="Top-K")
top_p_input = gr.Slider(minimum=0.1, maximum=1.0, value=0.9, step=0.05, label="Top-P")
rep_penalty_input = gr.Slider(minimum=1.0, maximum=2.0, value=1.1, step=0.05, label="Rep. Penalty")
ngram_size_input = gr.Slider(minimum=0, maximum=10, value=0, step=1, label="N-Gram Size")
stream_timeout_input = gr.Slider(minimum=10, maximum=600, value=120, step=10, label="Stream Timeout (sec)")
with gr.Column(scale=3):
system_prompt_input = gr.Textbox(
label="System Prompt", value="You are a helpful assistant.",
lines=2, visible=False
)
user_prompt = gr.Textbox(
label="Prompt",
value="Once upon a time in a digital kingdom,",
placeholder="Enter your prompt here...",
lines=3
)
context_input = gr.Textbox(
label="Context", placeholder="Paste reference context here...",
lines=5, visible=False
)
src_lang_input = gr.Dropdown(
choices=LANGUAGES, value="English", label="Translate from",
visible=False
)
tgt_lang_input = gr.Dropdown(
choices=LANGUAGES, value="Spanish", label="Translate to",
visible=False
)
run_btn = gr.Button("Generate", variant="primary", size="lg")
status_output = gr.Markdown("*Ready*")
output_text = gr.Markdown(label="Output", elem_id="output-box")
search_btn.click(
fn=search_hf_models,
inputs=[model_input],
outputs=[model_input],
)
load_btn.click(
fn=load_new_model,
inputs=[model_input],
outputs=[status_output]
)
mode_input.change(
fn=update_mode_ui,
inputs=[mode_input],
outputs=[system_prompt_input, user_prompt, context_input, src_lang_input, tgt_lang_input]
)
run_btn.click(
fn=run_inference,
inputs=[
mode_input,
user_prompt,
system_prompt_input,
context_input,
src_lang_input,
tgt_lang_input,
max_tokens_input,
temperature_input,
top_k_input,
top_p_input,
rep_penalty_input,
ngram_size_input,
do_sample_input,
use_gpu,
stream_timeout_input
],
outputs=[output_text, status_output]
)
clean_btn.click(fn=clean_cache, outputs=[status_output])
if __name__ == "__main__":
app.launch(theme=gr.themes.Soft())