# Import before torch/CUDA initialization on Hugging Face Spaces. import spaces import gc import queue import re import threading import traceback import gradio as gr import torch from transformers import ( AutoConfig, AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer, ) # ============================================================ # CONFIGURATION # ============================================================ MODELS = { "BrtGPT-1-Pre": "Bertug1911/BrtGPT-1-Pre", "BrtGPT-1-Pre-Code": "Bertug1911/BrtGPT-1-Pre-Code", "BrtGPT-1-0719": "HuggingFaceTB/SmolLM2-1.7B-Instruct", "BrtGPT-1-0906": "Bertug1911/BrtGPT-1-0906", "BrtGPT-Conversation-1": "Bertug1911/BrtGPT-Conversation-1", "BrtGPT-Conversation-1.5": "Bertug1911/BrtGPT-Conversation-1.5", "BrtGPT-Conversation-2": "Bertug1911/BrtGPT-Conversation-2", "CodePiegon": "Bertug1911/CodePiegon", } DEFAULT_MODEL = "BrtGPT-1-Pre" DEVICE = "cuda" if torch.cuda.is_available() else "cpu" DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32 REPETITION_PENALTY = 1.1 # Per-model maximums. Global minimums: top_p=0.1, top_k=1, # temperature=0.1, max_new_tokens=1. Adjust these limits as desired. MODEL_LIMITS = { "BrtGPT-1-Pre": {"top_p_max": 1.0, "top_k_max": 50, "temperature_max": 2.0, "max_new_tokens_max": 768}, "BrtGPT-1-Pre-Code": {"top_p_max": 1.0, "top_k_max": 50, "temperature_max": 2.0, "max_new_tokens_max": 768}, "BrtGPT-1-0719": {"top_p_max": 1.0, "top_k_max": 50, "temperature_max": 2.0, "max_new_tokens_max": 768}, "BrtGPT-1-0906": {"top_p_max": 1.0, "top_k_max": 50, "temperature_max": 2.0, "max_new_tokens_max": 768}, "BrtGPT-Conversation-1": {"top_p_max": 1.0, "top_k_max": 50, "temperature_max": 2.0, "max_new_tokens_max": 768}, "BrtGPT-Conversation-1.5": {"top_p_max": 1.0, "top_k_max": 50, "temperature_max": 2.0, "max_new_tokens_max": 768}, "BrtGPT-Conversation-2": {"top_p_max": 1.0, "top_k_max": 50, "temperature_max": 2.0, "max_new_tokens_max": 768}, "CodePiegon": {"top_p_max": 1.0, "top_k_max": 50, "temperature_max": 2.0, "max_new_tokens_max": 128}, } DEFAULTS = {"top_p": 0.75, "top_k": 12, "temperature": 0.7, "max_new_tokens": 128} _current_model_id = None _current_model = None _current_tokenizer = None _model_lock = threading.Lock() def get_limits(model_name): return MODEL_LIMITS.get(model_name, MODEL_LIMITS[DEFAULT_MODEL]) def update_model_controls(model_name): limits = get_limits(model_name) return ( gr.update(minimum=0.1, maximum=limits["top_p_max"], value=min(DEFAULTS["top_p"], limits["top_p_max"])), gr.update(minimum=1, maximum=limits["top_k_max"], value=min(DEFAULTS["top_k"], limits["top_k_max"]), step=1), gr.update(minimum=0.1, maximum=limits["temperature_max"], value=min(DEFAULTS["temperature"], limits["temperature_max"])), gr.update(minimum=1, maximum=limits["max_new_tokens_max"], value=min(DEFAULTS["max_new_tokens"], limits["max_new_tokens_max"]), step=1), ) # ============================================================ # MODEL LOADING + TOKEN ID SAFETY CHECKS # ============================================================ def load_model(model_name): global _current_model_id, _current_model, _current_tokenizer if model_name not in MODELS: raise ValueError(f"Unknown model: {model_name}") model_id = MODELS[model_name] if _current_model_id == model_id and _current_model is not None: return _current_tokenizer, _current_model # Release the previous model before loading another one. if _current_model is not None: del _current_model _current_model = None _current_tokenizer = None _current_model_id = None gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() print(f"Loading tokenizer: {model_id}") tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True) config = AutoConfig.from_pretrained(model_id) # Important: if the tokenizer has more IDs than the model config, # set the config vocabulary size BEFORE loading weights. This avoids # the common 32000-vs-32001 embedding shape mismatch when the checkpoint # actually contains the extra embedding row. tokenizer_size = len(tokenizer) if tokenizer_size > int(config.vocab_size): print(f"Adjusting config vocab_size: {config.vocab_size} -> {tokenizer_size}") config.vocab_size = tokenizer_size model = AutoModelForCausalLM.from_pretrained( model_id, config=config, torch_dtype=DTYPE, low_cpu_mem_usage=True, ) embedding_count = model.get_input_embeddings().num_embeddings if tokenizer_size > embedding_count: # Defensive fallback for architectures whose embedding table was not # sized from config. New rows are initialized; see note in UI errors. model.resize_token_embeddings(tokenizer_size) embedding_count = model.get_input_embeddings().num_embeddings # Validate all tokenizer IDs before the first CUDA operation. vocab = tokenizer.get_vocab() invalid = [(token, idx) for token, idx in vocab.items() if idx < 0 or idx >= embedding_count] if invalid: del model gc.collect() raise ValueError( f"Tokenizer/model vocabulary mismatch for {model_name}: {len(invalid)} token IDs " f"are outside embedding range [0, {embedding_count - 1}]. " "Check that this model repository's tokenizer and weights come from the same revision." ) if tokenizer.pad_token_id is None: if tokenizer.eos_token_id is not None: tokenizer.pad_token = tokenizer.eos_token else: tokenizer.add_special_tokens({"pad_token": "<|pad|>"}) if len(tokenizer) > model.get_input_embeddings().num_embeddings: model.resize_token_embeddings(len(tokenizer)) model.to(DEVICE) model.eval() _current_tokenizer = tokenizer _current_model = model _current_model_id = model_id print( f"Model ready: {model_id} on {DEVICE} | tokenizer={len(tokenizer)} | " f"embeddings={model.get_input_embeddings().num_embeddings}" ) return tokenizer, model # ============================================================ # OUTPUT FORMATTING # ============================================================ CODE_BLOCK_PATTERN = re.compile(r"```([^\n`]*)\n(.*?)```", re.DOTALL) def format_output(text): def replace_code_block(match): language = match.group(1).strip() or "text" code = match.group(2).rstrip("\n") longest_run = max((len(run) for run in re.findall(r"`+", code)), default=0) fence = "`" * max(3, longest_run + 1) return f"\n\n### `<{language.upper()}>`\n\n{fence}{language}\n{code}\n{fence}\n\n" return CODE_BLOCK_PATTERN.sub(replace_code_block, text).strip() or "The model returned an empty response." # Preserve the original marker cleanup behavior, but only when those markers # actually appear in the model output. def normalize_token_markers(text): if "Ġ" in text or "Ċ" in text: text = text.replace(" ", "") text = text.replace("Ġ", " ").replace("Ċ", "\n") return text # ============================================================ # GENERATION # ============================================================ @spaces.GPU def generate(prompt, model_name, top_p, top_k, temperature, max_new_tokens): if not prompt or not prompt.strip(): yield "Please enter a message." return if model_name not in MODELS: yield "Please select a valid model." return limits = get_limits(model_name) # Clamp all UI values again server-side; browser inputs are not trusted. top_p = max(0.1, min(float(top_p), limits["top_p_max"])) top_k = max(1, min(int(top_k), limits["top_k_max"])) temperature = max(0.1, min(float(temperature), limits["temperature_max"])) max_new_tokens = max(1, min(int(max_new_tokens), limits["max_new_tokens_max"])) yield "Loading model and generating..." try: # Keep the lock for the complete operation; only one request/model # generation runs at once, avoiding unsafe concurrent CUDA use. with _model_lock: tokenizer, model = load_model(model_name) messages = [{"role": "user", "content": prompt.strip()}] if getattr(tokenizer, "chat_template", None): inputs = tokenizer.apply_chat_template( messages, tokenize=True, add_generation_prompt=True, return_tensors="pt", return_dict=True, ) else: inputs = tokenizer(prompt.strip(), return_tensors="pt") inputs = {key: value.to(DEVICE) for key, value in inputs.items()} input_ids = inputs.get("input_ids") if input_ids is None or input_ids.numel() == 0: raise ValueError("Tokenizer returned empty input_ids.") embedding_count = model.get_input_embeddings().num_embeddings min_id = int(input_ids.min().item()) max_id = int(input_ids.max().item()) if min_id < 0 or max_id >= embedding_count: raise ValueError( f"Input token ID range [{min_id}, {max_id}] exceeds model embedding range " f"[0, {embedding_count - 1}]. Tokenizer and model files are incompatible." ) model_context = getattr(model.config, "max_position_embeddings", None) if model_context is None: model_context = getattr(model.config, "n_positions", None) if isinstance(model_context, int) and model_context > 1: # Keep the prompt inside the model context and leave room for generation. # This also prevents position-embedding index errors on very long prompts. max_input_tokens = model_context - 1 if input_ids.shape[-1] > max_input_tokens: for key in ("input_ids", "attention_mask", "token_type_ids"): if key in inputs and inputs[key].ndim == 2: inputs[key] = inputs[key][:, -max_input_tokens:] input_ids = inputs["input_ids"] print( f"Prompt truncated to {max_input_tokens} tokens for model context " f"limit {model_context}." ) max_new_tokens = min(max_new_tokens, max(1, model_context - input_ids.shape[-1])) pad_token_id = tokenizer.pad_token_id if pad_token_id is None: pad_token_id = tokenizer.eos_token_id generation_kwargs = { "max_new_tokens": max_new_tokens, "do_sample": True, "temperature": temperature, "top_p": top_p, "top_k": top_k, "repetition_penalty": REPETITION_PENALTY, "pad_token_id": pad_token_id, "use_cache": True, } if tokenizer.eos_token_id is not None: generation_kwargs["eos_token_id"] = tokenizer.eos_token_id streamer = TextIteratorStreamer( tokenizer, skip_prompt=True, skip_special_tokens=True, timeout=0.5, ) worker_errors = [] def generation_worker(): try: with torch.inference_mode(): model.generate(**inputs, streamer=streamer, **generation_kwargs) except Exception as exc: worker_errors.append(exc) try: streamer.on_finalized_text("", stream_end=True) except Exception: pass worker = threading.Thread(target=generation_worker, daemon=True) worker.start() raw_response = "" while True: try: chunk = next(streamer) except StopIteration: break except queue.Empty: if not worker.is_alive(): if worker_errors: raise worker_errors[0] break continue if chunk: raw_response += chunk yield format_output(normalize_token_markers(raw_response)) worker.join() if worker_errors: raise worker_errors[0] yield format_output(normalize_token_markers(raw_response)) except torch.cuda.OutOfMemoryError: gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() yield "GPU memory is insufficient. Select a smaller model or reduce Max New Tokens." except Exception as exc: print("Generation error:", repr(exc)) traceback.print_exc() message = str(exc) if "device-side assert" in message.lower() or "cuda error" in message.lower(): yield ( "CUDA generation failed. The app checked token IDs before generation, so this may be " "a model/checkpoint incompatibility or a CUDA kernel issue. Restart the Space runtime " "and verify that the model weights and tokenizer are from the same repository revision." ) else: yield f"Generation error ({type(exc).__name__}): {message}" # ============================================================ # DARK THEME # ============================================================ CSS = """ .gradio-container { max-width: 940px !important; margin: 0 auto !important; background: #0b0d12 !important; } #app-header { text-align: center; padding: 22px 0 8px 0; } #app-header h1 { font-size: 30px; font-weight: 750; letter-spacing: -1px; color: #f1f5f9; } #app-subtitle { text-align: center; color: #929bad; margin-bottom: 22px; } #model-selector, #settings-panel { border: 1px solid #292e3b; border-radius: 12px; background: #11141c; } #input-area textarea { background: #11141c !important; border: 1px solid #292e3b !important; border-radius: 12px !important; color: #f1f5f9 !important; font-size: 15px !important; line-height: 1.7 !important; } #output-area { border: 1px solid #292e3b; border-radius: 12px; background: #11141c; padding: 18px; } #output-area h3 { color: #a5b4fc; font-size: 12px; font-weight: 700; letter-spacing: 1px; margin-bottom: 10px; } #output-area pre { border: 1px solid #292e3b !important; border-radius: 10px !important; background: #090b10 !important; padding: 16px !important; overflow-x: auto !important; } #output-area code { font-size: 13px !important; line-height: 1.7 !important; } #generate-button { border-radius: 10px; min-height: 46px; font-weight: 650; } footer { display: none !important; } """ with gr.Blocks( theme=gr.themes.Base( primary_hue="indigo", secondary_hue="slate", neutral_hue="slate", ).set( body_background_fill="#0b0d12", body_text_color="#e5e7eb", block_background_fill="#11141c", block_border_color="#292e3b", input_background_fill="#11141c", button_primary_background_fill="#6366f1", button_primary_background_fill_hover="#4f46e5", ), css=CSS, title="LLM Playground", ) as demo: gr.Markdown("# LLM Playground", elem_id="app-header") gr.Markdown("A simple, modern interface for language models.", elem_id="app-subtitle") model_selector = gr.Dropdown( choices=list(MODELS.keys()), value=DEFAULT_MODEL, label="MODEL", info="Choose which model to run.", elem_id="model-selector", interactive=True, ) prompt_input = gr.Textbox( label="INPUT", placeholder="Ask a question, write code, or enter a prompt...", lines=5, max_lines=12, elem_id="input-area", ) with gr.Accordion("Generation settings", open=True, elem_id="settings-panel"): with gr.Row(): top_p_input = gr.Slider( minimum=0.1, maximum=get_limits(DEFAULT_MODEL)["top_p_max"], value=DEFAULTS["top_p"], step=0.05, label="Top P", info="Minimum 0.1; maximum depends on selected model." ) top_k_input = gr.Slider( minimum=1, maximum=get_limits(DEFAULT_MODEL)["top_k_max"], value=DEFAULTS["top_k"], step=1, label="Top K", info="Minimum 1; maximum depends on selected model." ) with gr.Row(): temperature_input = gr.Slider( minimum=0.1, maximum=get_limits(DEFAULT_MODEL)["temperature_max"], value=DEFAULTS["temperature"], step=0.05, label="Temperature", info="Minimum 0.1; maximum depends on selected model." ) max_new_tokens_input = gr.Slider( minimum=1, maximum=get_limits(DEFAULT_MODEL)["max_new_tokens_max"], value=DEFAULTS["max_new_tokens"], step=1, label="Max New Tokens", info="Minimum 1; maximum depends on selected model." ) generate_button = gr.Button("Generate", variant="primary", elem_id="generate-button") output = gr.Markdown(value="Your model's response will appear here.", label="OUTPUT", elem_id="output-area") model_selector.change( fn=update_model_controls, inputs=[model_selector], outputs=[top_p_input, top_k_input, temperature_input, max_new_tokens_input], ) generation_inputs = [ prompt_input, model_selector, top_p_input, top_k_input, temperature_input, max_new_tokens_input, ] generate_button.click(fn=generate, inputs=generation_inputs, outputs=output) prompt_input.submit(fn=generate, inputs=generation_inputs, outputs=output) if __name__ == "__main__": demo.queue(default_concurrency_limit=1).launch( server_name="0.0.0.0", server_port=7860, )