Spaces:
Running on Zero
Running on Zero
Gerald Corzo commited on
Commit ·
446eaf9
0
Parent(s):
feat: remora PyTorch ZeroGPU layer-control research space
Browse files- .gitignore +6 -0
- README.md +59 -0
- app.py +115 -0
- docs/plans/2026-09-02-remora-layer-lab.md +55 -0
- remora_control.py +83 -0
- requirements.txt +4 -0
- tests/test_remora_control.py +55 -0
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
.venv/
|
| 4 |
+
.env
|
| 5 |
+
.env.*
|
| 6 |
+
!.env.example
|
README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Remora Layer Lab
|
| 3 |
+
emoji: 🐟
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
python_version: "3.12"
|
| 8 |
+
sdk_version: "5.49.1"
|
| 9 |
+
app_file: app.py
|
| 10 |
+
models:
|
| 11 |
+
- LiquidAI/LFM2.5-1.2B-Instruct
|
| 12 |
+
tags:
|
| 13 |
+
- remora
|
| 14 |
+
- layer-control
|
| 15 |
+
- lfm2
|
| 16 |
+
- zerogpu
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
# Remora Layer Lab
|
| 20 |
+
|
| 21 |
+
A research-only Hugging Face ZeroGPU Space that runs `LiquidAI/LFM2.5-1.2B-Instruct` and applies explicit, reproducible controls to selected hidden-state layers.
|
| 22 |
+
|
| 23 |
+
## What this is
|
| 24 |
+
|
| 25 |
+
- A **PyTorch experimental adapter** for the existing Remora research method.
|
| 26 |
+
- Captures per-layer hidden-state statistics for each generation run.
|
| 27 |
+
- Applies a scalar gain to selected decoder-block outputs:
|
| 28 |
+
|
| 29 |
+
`controlled = baseline + strength × (baseline - token_mean)`
|
| 30 |
+
|
| 31 |
+
- `strength = 0`: observation only; output is unchanged.
|
| 32 |
+
- negative strength: damp the selected residual signal.
|
| 33 |
+
- positive strength: amplify it.
|
| 34 |
+
- Produces a JSONL trace that can be compared with the Rust/llama.cpp Remora traces.
|
| 35 |
+
|
| 36 |
+
## What this is not
|
| 37 |
+
|
| 38 |
+
This is **not** the Rust production lane and does not replace local remora on Janus. The HF ZeroGPU Space is a bounded research surface: Gradio/PyTorch only, shared GPU allocation, daily quota, and no persistent runtime disk. The production route remains octopus → local `lfm-fast`.
|
| 39 |
+
|
| 40 |
+
## Run protocol
|
| 41 |
+
|
| 42 |
+
1. Use the default `strength = 0` to create a baseline trace.
|
| 43 |
+
2. Repeat the same prompt with one selected layer and one non-zero strength.
|
| 44 |
+
3. Compare output and trace statistics outside the Space. Save the downloaded JSONL with the prompt suite and run metadata.
|
| 45 |
+
4. Keep a request below the declared GPU duration. Start with 32 output tokens.
|
| 46 |
+
|
| 47 |
+
## Cost boundary
|
| 48 |
+
|
| 49 |
+
For a PRO account, ZeroGPU has a 40-minute daily quota. Usage after the quota costs $1 per 10 minutes from pre-paid HF credits. The Space uses `large` (48 GB, one quota unit), not `xlarge`.
|
| 50 |
+
|
| 51 |
+
## Local checks
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
python -m unittest discover -s tests -v
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Deploy
|
| 58 |
+
|
| 59 |
+
Create an HF **Gradio ZeroGPU** Space called `gcorzo/remora-layer-lab`, select ZeroGPU hardware in its Settings, then push this directory to the Space repository. The HF token used for this must have Space write permission; no token is stored in this repository.
|
app.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import tempfile
|
| 5 |
+
import time
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import spaces
|
| 10 |
+
import torch
|
| 11 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 12 |
+
|
| 13 |
+
from remora_control import LayerController
|
| 14 |
+
|
| 15 |
+
MODEL_ID = "LiquidAI/LFM2.5-1.2B-Instruct"
|
| 16 |
+
MAX_OUTPUT_TOKENS = 128
|
| 17 |
+
DEVICE = "cuda"
|
| 18 |
+
|
| 19 |
+
# ZeroGPU's CUDA emulation makes module-level placement correct: a real GPU is
|
| 20 |
+
# attached only when `run_experiment` enters the decorator.
|
| 21 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 22 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 23 |
+
MODEL_ID,
|
| 24 |
+
torch_dtype=torch.bfloat16,
|
| 25 |
+
low_cpu_mem_usage=True,
|
| 26 |
+
).to(DEVICE).eval()
|
| 27 |
+
LAYER_COUNT = len(model.model.layers)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def parse_layers(text: str) -> list[int]:
|
| 31 |
+
raw = [part.strip() for part in text.split(",") if part.strip()]
|
| 32 |
+
layers = sorted(set(int(part) for part in raw))
|
| 33 |
+
invalid = [value for value in layers if value < 0 or value >= LAYER_COUNT]
|
| 34 |
+
if invalid:
|
| 35 |
+
raise gr.Error(f"Layer values must be between 0 and {LAYER_COUNT - 1}; got {invalid}.")
|
| 36 |
+
return layers
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@spaces.GPU(duration=120, size="large")
|
| 40 |
+
def run_experiment(prompt: str, selected_layers: str, gain: float, max_new_tokens: int, seed: int):
|
| 41 |
+
if not prompt.strip():
|
| 42 |
+
raise gr.Error("Enter a prompt.")
|
| 43 |
+
if max_new_tokens < 1 or max_new_tokens > MAX_OUTPUT_TOKENS:
|
| 44 |
+
raise gr.Error(f"Output tokens must be 1–{MAX_OUTPUT_TOKENS}.")
|
| 45 |
+
|
| 46 |
+
layers = parse_layers(selected_layers)
|
| 47 |
+
controller = LayerController(layers, gain)
|
| 48 |
+
generator = torch.Generator(device=DEVICE).manual_seed(int(seed))
|
| 49 |
+
started = time.perf_counter()
|
| 50 |
+
controller.attach(model)
|
| 51 |
+
try:
|
| 52 |
+
messages = [{"role": "user", "content": prompt}]
|
| 53 |
+
rendered = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 54 |
+
inputs = tokenizer(rendered, return_tensors="pt").to(DEVICE)
|
| 55 |
+
with torch.inference_mode():
|
| 56 |
+
output_ids = model.generate(
|
| 57 |
+
**inputs,
|
| 58 |
+
max_new_tokens=int(max_new_tokens),
|
| 59 |
+
do_sample=True,
|
| 60 |
+
temperature=0.1,
|
| 61 |
+
top_k=50,
|
| 62 |
+
repetition_penalty=1.05,
|
| 63 |
+
generator=generator,
|
| 64 |
+
use_cache=True,
|
| 65 |
+
)
|
| 66 |
+
finally:
|
| 67 |
+
controller.detach()
|
| 68 |
+
|
| 69 |
+
elapsed_s = time.perf_counter() - started
|
| 70 |
+
new_tokens = output_ids[0, inputs.input_ids.shape[1] :]
|
| 71 |
+
text = tokenizer.decode(new_tokens, skip_special_tokens=True)
|
| 72 |
+
trace = {
|
| 73 |
+
"schema": "remora-layer-lab/v1",
|
| 74 |
+
"model": MODEL_ID,
|
| 75 |
+
"layer_count": LAYER_COUNT,
|
| 76 |
+
"selected_layers": layers,
|
| 77 |
+
"gain": gain,
|
| 78 |
+
"seed": seed,
|
| 79 |
+
"prompt": prompt,
|
| 80 |
+
"generated_tokens": int(new_tokens.shape[0]),
|
| 81 |
+
"elapsed_s": round(elapsed_s, 4),
|
| 82 |
+
"tokens_per_second": round(float(new_tokens.shape[0]) / elapsed_s, 3) if elapsed_s else None,
|
| 83 |
+
"events": controller.json_events(),
|
| 84 |
+
}
|
| 85 |
+
artifact = Path(tempfile.mkstemp(prefix="remora-trace-", suffix=".json")[1])
|
| 86 |
+
artifact.write_text(json.dumps(trace, indent=2), encoding="utf-8")
|
| 87 |
+
status = (
|
| 88 |
+
f"{len(layers)} controlled layers; {len(trace['events'])} trace events; "
|
| 89 |
+
f"{trace['generated_tokens']} tokens in {elapsed_s:.2f}s "
|
| 90 |
+
f"({trace['tokens_per_second']} tok/s)."
|
| 91 |
+
)
|
| 92 |
+
return text, status, str(artifact)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
with gr.Blocks(title="Remora Layer Lab") as demo:
|
| 96 |
+
gr.Markdown(
|
| 97 |
+
"# Remora Layer Lab\n"
|
| 98 |
+
"PyTorch/ZeroGPU research adapter for controlled LFM2.5 decoder-layer experiments. "
|
| 99 |
+
"`gain=0` is an observation-only baseline. This is not the local Rust production lane."
|
| 100 |
+
)
|
| 101 |
+
with gr.Row():
|
| 102 |
+
prompt = gr.Textbox(label="Prompt", lines=6, value="Explain why water forecasting needs uncertainty.")
|
| 103 |
+
with gr.Column():
|
| 104 |
+
selected_layers = gr.Textbox(label=f"Layers (0–{LAYER_COUNT - 1}, comma-separated)", value="2,5")
|
| 105 |
+
gain = gr.Slider(-1.0, 1.0, value=0.0, step=0.05, label="Layer gain")
|
| 106 |
+
max_new_tokens = gr.Slider(1, MAX_OUTPUT_TOKENS, value=32, step=1, label="Max output tokens")
|
| 107 |
+
seed = gr.Number(value=42, precision=0, label="Random seed")
|
| 108 |
+
run = gr.Button("Run controlled experiment", variant="primary")
|
| 109 |
+
answer = gr.Textbox(label="Generated text", lines=8)
|
| 110 |
+
status = gr.Textbox(label="Run statistics")
|
| 111 |
+
trace = gr.File(label="Download trace JSON")
|
| 112 |
+
run.click(run_experiment, [prompt, selected_layers, gain, max_new_tokens, seed], [answer, status, trace])
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
demo.launch()
|
docs/plans/2026-09-02-remora-layer-lab.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Remora Layer Lab Implementation Plan
|
| 2 |
+
|
| 3 |
+
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
| 4 |
+
|
| 5 |
+
**Goal:** provide a bounded HF ZeroGPU laboratory for PyTorch layer-control experiments on public LFM2.5-1.2B weights, while retaining local Rust remora as the production path.
|
| 6 |
+
|
| 7 |
+
**Architecture:** The Space runs `LiquidAI/LFM2.5-1.2B-Instruct` through Transformers and attaches forward hooks to selected `model.model.layers`. A deterministic gain control captures before/after RMS per token in a downloadable JSON trace. The Space uses Gradio plus `@spaces.GPU`, which is an explicitly bounded external Python research adapter and not fleet infrastructure.
|
| 8 |
+
|
| 9 |
+
**Tech Stack:** Python 3.12, PyTorch 2.8+, Transformers 4.57.6, Gradio 5.49.1, Hugging Face ZeroGPU.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Verified inputs
|
| 14 |
+
|
| 15 |
+
- Model: `LiquidAI/LFM2.5-1.2B-Instruct`, public, Transformers-native, 16 layers (10 convolution + 6 attention).
|
| 16 |
+
- ZeroGPU: Gradio only; declare GPU work with `@spaces.GPU`; `large` offers 48 GB; PRO quota is 40 minutes/day.
|
| 17 |
+
- The account's active token cannot write a Space. Deployment requires a new token with Space-write permission.
|
| 18 |
+
|
| 19 |
+
## Acceptance criteria
|
| 20 |
+
|
| 21 |
+
- `gain=0` preserves exact hidden states while capturing a trace.
|
| 22 |
+
- Selected layer indexes are validated against the 16-layer model.
|
| 23 |
+
- Each run returns text, run speed and a downloadable JSON trace.
|
| 24 |
+
- GPU work is bounded to 120 seconds/request on a 48 GB `large` allocation.
|
| 25 |
+
- The project has unit tests, clear deployment instructions and no embedded credentials.
|
| 26 |
+
|
| 27 |
+
## Tasks
|
| 28 |
+
|
| 29 |
+
### Task 1: Core layer controller
|
| 30 |
+
|
| 31 |
+
**Files:** `remora_control.py`, `tests/test_remora_control.py`
|
| 32 |
+
|
| 33 |
+
Implement a hook controller over `model.model.layers`. Test zero-gain identity, non-zero gain effect, trace count and index validation.
|
| 34 |
+
|
| 35 |
+
**Verification:** `python -m unittest discover -s tests -v`
|
| 36 |
+
|
| 37 |
+
### Task 2: ZeroGPU application
|
| 38 |
+
|
| 39 |
+
**Files:** `app.py`, `requirements.txt`, `README.md`
|
| 40 |
+
|
| 41 |
+
Load the public 1.2B LFM model in bf16, create a Gradio prompt/control surface, decorate execution with `@spaces.GPU(duration=120, size="large")`, and return a trace artifact.
|
| 42 |
+
|
| 43 |
+
**Verification:** `python -m py_compile app.py remora_control.py`
|
| 44 |
+
|
| 45 |
+
### Task 3: Deploy and bounded benchmark
|
| 46 |
+
|
| 47 |
+
**Files:** HF Space `gcorzo/remora-layer-lab` (external)
|
| 48 |
+
|
| 49 |
+
Create a Gradio Space, choose ZeroGPU hardware in Settings, push this project and wait for build success. Run baseline and one non-zero gain with identical prompt/seed; download both traces and record runtime / generated tokens / quota use.
|
| 50 |
+
|
| 51 |
+
**Verification:** Space build is `Running`; both trace files are downloadable; HF Billing shows resulting ZeroGPU compute usage.
|
| 52 |
+
|
| 53 |
+
## Rollback
|
| 54 |
+
|
| 55 |
+
Remove the Space or switch it to CPU Basic. No local router, model server or production octopus configuration changes are made by this project.
|
remora_control.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Layer-level Remora controls for PyTorch decoder stacks.
|
| 2 |
+
|
| 3 |
+
This module is intentionally small and dependency-light. It controls the output
|
| 4 |
+
of a selected decoder block and records scalar trace data; it does not claim
|
| 5 |
+
that the output hook is identical to llama.cpp's internal MoE gate hook.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from dataclasses import asdict, dataclass
|
| 10 |
+
from typing import Any, Iterable
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from torch import nn
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True)
|
| 17 |
+
class TraceEvent:
|
| 18 |
+
layer: int
|
| 19 |
+
token_index: int
|
| 20 |
+
baseline_rms: float
|
| 21 |
+
controlled_rms: float
|
| 22 |
+
gain: float
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class LayerController:
|
| 26 |
+
"""Attach controlled-output hooks to `model.model.layers`.
|
| 27 |
+
|
| 28 |
+
The control is deterministic for fixed model weights, prompt, sampling
|
| 29 |
+
parameters, and RNG seed. A gain of zero is a strict identity operation.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self, layers: Iterable[int], gain: float) -> None:
|
| 33 |
+
self.layers = tuple(sorted(set(layers)))
|
| 34 |
+
self.gain = float(gain)
|
| 35 |
+
self.events: list[TraceEvent] = []
|
| 36 |
+
self._handles: list[Any] = []
|
| 37 |
+
|
| 38 |
+
def attach(self, model: nn.Module) -> None:
|
| 39 |
+
decoder_layers = getattr(getattr(model, "model", None), "layers", None)
|
| 40 |
+
if decoder_layers is None:
|
| 41 |
+
raise ValueError("model does not expose model.layers for layer control")
|
| 42 |
+
layer_count = len(decoder_layers)
|
| 43 |
+
invalid = [index for index in self.layers if index < 0 or index >= layer_count]
|
| 44 |
+
if invalid:
|
| 45 |
+
raise ValueError(f"layer indexes outside 0..{layer_count - 1}: {invalid}")
|
| 46 |
+
self.detach()
|
| 47 |
+
for index in self.layers:
|
| 48 |
+
self._handles.append(decoder_layers[index].register_forward_hook(self._hook(index)))
|
| 49 |
+
|
| 50 |
+
def detach(self) -> None:
|
| 51 |
+
for handle in self._handles:
|
| 52 |
+
handle.remove()
|
| 53 |
+
self._handles.clear()
|
| 54 |
+
|
| 55 |
+
def json_events(self) -> list[dict[str, float | int]]:
|
| 56 |
+
return [asdict(event) for event in self.events]
|
| 57 |
+
|
| 58 |
+
def _hook(self, layer: int):
|
| 59 |
+
def controlled_output(_module: nn.Module, _inputs: tuple[Any, ...], output: Any) -> Any:
|
| 60 |
+
hidden = output[0] if isinstance(output, tuple) else output
|
| 61 |
+
if not isinstance(hidden, torch.Tensor):
|
| 62 |
+
raise TypeError("decoder layer output must be a Tensor or tuple whose first value is a Tensor")
|
| 63 |
+
|
| 64 |
+
# [batch, sequence, hidden] -> one scalar event per generated/prompt token.
|
| 65 |
+
baseline_rms = hidden.float().pow(2).mean(dim=-1).sqrt()
|
| 66 |
+
token_mean = hidden.mean(dim=-1, keepdim=True)
|
| 67 |
+
controlled = hidden + self.gain * (hidden - token_mean)
|
| 68 |
+
controlled_rms = controlled.float().pow(2).mean(dim=-1).sqrt()
|
| 69 |
+
for token_index, (before, after) in enumerate(zip(baseline_rms[0], controlled_rms[0])):
|
| 70 |
+
self.events.append(
|
| 71 |
+
TraceEvent(
|
| 72 |
+
layer=layer,
|
| 73 |
+
token_index=token_index,
|
| 74 |
+
baseline_rms=float(before.detach().cpu()),
|
| 75 |
+
controlled_rms=float(after.detach().cpu()),
|
| 76 |
+
gain=self.gain,
|
| 77 |
+
)
|
| 78 |
+
)
|
| 79 |
+
if isinstance(output, tuple):
|
| 80 |
+
return (controlled, *output[1:])
|
| 81 |
+
return controlled
|
| 82 |
+
|
| 83 |
+
return controlled_output
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==5.49.1
|
| 2 |
+
huggingface-hub>=0.30,<1.0
|
| 3 |
+
torch>=2.8.0
|
| 4 |
+
transformers==4.57.6
|
tests/test_remora_control.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
|
| 6 |
+
from remora_control import LayerController
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ToyModel(nn.Module):
|
| 10 |
+
def __init__(self):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.model = nn.Module()
|
| 13 |
+
self.model.layers = nn.ModuleList([nn.Identity(), nn.Identity(), nn.Identity()])
|
| 14 |
+
|
| 15 |
+
def forward(self, value):
|
| 16 |
+
for layer in self.model.layers:
|
| 17 |
+
value = layer(value)
|
| 18 |
+
return value
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class LayerControllerTests(unittest.TestCase):
|
| 22 |
+
def setUp(self):
|
| 23 |
+
self.model = ToyModel()
|
| 24 |
+
self.value = torch.tensor([[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]])
|
| 25 |
+
|
| 26 |
+
def test_zero_gain_is_identity_and_records_trace(self):
|
| 27 |
+
controller = LayerController([1], gain=0.0)
|
| 28 |
+
controller.attach(self.model)
|
| 29 |
+
try:
|
| 30 |
+
actual = self.model(self.value)
|
| 31 |
+
finally:
|
| 32 |
+
controller.detach()
|
| 33 |
+
self.assertTrue(torch.equal(actual, self.value))
|
| 34 |
+
self.assertEqual(2, len(controller.events))
|
| 35 |
+
self.assertEqual(1, controller.events[0].layer)
|
| 36 |
+
self.assertEqual(controller.events[0].baseline_rms, controller.events[0].controlled_rms)
|
| 37 |
+
|
| 38 |
+
def test_nonzero_gain_changes_selected_layer(self):
|
| 39 |
+
controller = LayerController([1], gain=0.5)
|
| 40 |
+
controller.attach(self.model)
|
| 41 |
+
try:
|
| 42 |
+
actual = self.model(self.value)
|
| 43 |
+
finally:
|
| 44 |
+
controller.detach()
|
| 45 |
+
self.assertFalse(torch.equal(actual, self.value))
|
| 46 |
+
self.assertEqual(2, len(controller.events))
|
| 47 |
+
self.assertGreater(controller.events[0].controlled_rms, controller.events[0].baseline_rms)
|
| 48 |
+
|
| 49 |
+
def test_invalid_layer_is_rejected(self):
|
| 50 |
+
with self.assertRaisesRegex(ValueError, "outside"):
|
| 51 |
+
LayerController([3], 0.0).attach(self.model)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
unittest.main()
|