Spaces:
Running on Zero
Running on Zero
Gerald Corzo
fix: close mkstemp fd, absolute token_index, seed int, device fallback, friendly layer errors
c83ef18 | from __future__ import annotations | |
| import json | |
| import os | |
| import tempfile | |
| import time | |
| from pathlib import Path | |
| import gradio as gr | |
| import spaces | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| from remora_control import LayerController | |
| MODEL_ID = "LiquidAI/LFM2.5-1.2B-Instruct" | |
| MAX_OUTPUT_TOKENS = 128 | |
| DEVICE = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu") | |
| # ZeroGPU's CUDA emulation makes module-level placement correct: a real GPU is | |
| # attached only when `run_experiment` enters the decorator. | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, | |
| torch_dtype=torch.bfloat16, | |
| low_cpu_mem_usage=True, | |
| ).to(DEVICE).eval() | |
| LAYER_COUNT = len(model.model.layers) | |
| def parse_layers(text: str) -> list[int]: | |
| raw = [part.strip() for part in text.split(",") if part.strip()] | |
| try: | |
| layers = sorted(set(int(part) for part in raw)) | |
| except ValueError: | |
| raise gr.Error("Layers must be whole numbers, e.g. '2,5'.") | |
| invalid = [value for value in layers if value < 0 or value >= LAYER_COUNT] | |
| if invalid: | |
| raise gr.Error(f"Layer values must be between 0 and {LAYER_COUNT - 1}; got {invalid}.") | |
| return layers | |
| def run_experiment(prompt: str, selected_layers: str, gain: float, max_new_tokens: int, seed: int): | |
| if not prompt.strip(): | |
| raise gr.Error("Enter a prompt.") | |
| if max_new_tokens < 1 or max_new_tokens > MAX_OUTPUT_TOKENS: | |
| raise gr.Error(f"Output tokens must be 1–{MAX_OUTPUT_TOKENS}.") | |
| layers = parse_layers(selected_layers) | |
| controller = LayerController(layers, gain) | |
| seed_value = int(seed) | |
| generator = torch.Generator(device=DEVICE).manual_seed(seed_value) | |
| started = time.perf_counter() | |
| controller.attach(model) | |
| try: | |
| messages = [{"role": "user", "content": prompt}] | |
| rendered = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
| inputs = tokenizer(rendered, return_tensors="pt").to(DEVICE) | |
| with torch.inference_mode(): | |
| output_ids = model.generate( | |
| **inputs, | |
| max_new_tokens=int(max_new_tokens), | |
| do_sample=True, | |
| temperature=0.1, | |
| top_k=50, | |
| repetition_penalty=1.05, | |
| generator=generator, | |
| use_cache=True, | |
| ) | |
| finally: | |
| controller.detach() | |
| elapsed_s = time.perf_counter() - started | |
| new_tokens = output_ids[0, inputs.input_ids.shape[1] :] | |
| text = tokenizer.decode(new_tokens, skip_special_tokens=True) | |
| trace = { | |
| "schema": "remora-layer-lab/v1", | |
| "model": MODEL_ID, | |
| "layer_count": LAYER_COUNT, | |
| "selected_layers": layers, | |
| "gain": gain, | |
| "seed": seed_value, | |
| "prompt": prompt, | |
| "generated_tokens": int(new_tokens.shape[0]), | |
| "elapsed_s": round(elapsed_s, 4), | |
| "tokens_per_second": round(float(new_tokens.shape[0]) / elapsed_s, 3) if elapsed_s else None, | |
| "events": controller.json_events(), | |
| } | |
| artifact_fd, artifact_path = tempfile.mkstemp(prefix="remora-trace-", suffix=".json") | |
| os.close(artifact_fd) # close the descriptor mkstemp returns; only the path is needed | |
| Path(artifact_path).write_text(json.dumps(trace, indent=2), encoding="utf-8") | |
| status = ( | |
| f"{len(layers)} controlled layers; {len(trace['events'])} trace events; " | |
| f"{trace['generated_tokens']} tokens in {elapsed_s:.2f}s " | |
| f"({trace['tokens_per_second']} tok/s)." | |
| ) | |
| return text, status, str(artifact_path) | |
| with gr.Blocks(title="Remora Layer Lab") as demo: | |
| gr.Markdown( | |
| "# Remora Layer Lab\n" | |
| "PyTorch/ZeroGPU research adapter for controlled LFM2.5 decoder-layer experiments. " | |
| "`gain=0` is an observation-only baseline. This is not the local Rust production lane." | |
| ) | |
| with gr.Row(): | |
| prompt = gr.Textbox(label="Prompt", lines=6, value="Explain why water forecasting needs uncertainty.") | |
| with gr.Column(): | |
| selected_layers = gr.Textbox(label=f"Layers (0–{LAYER_COUNT - 1}, comma-separated)", value="2,5") | |
| gain = gr.Slider(-1.0, 1.0, value=0.0, step=0.05, label="Layer gain") | |
| max_new_tokens = gr.Slider(1, MAX_OUTPUT_TOKENS, value=32, step=1, label="Max output tokens") | |
| seed = gr.Number(value=42, precision=0, label="Random seed") | |
| run = gr.Button("Run controlled experiment", variant="primary") | |
| answer = gr.Textbox(label="Generated text", lines=8) | |
| status = gr.Textbox(label="Run statistics") | |
| trace = gr.File(label="Download trace JSON") | |
| run.click(run_experiment, [prompt, selected_layers, gain, max_new_tokens, seed], [answer, status, trace]) | |
| if __name__ == "__main__": | |
| demo.launch() | |