Spaces:
Running on Zero
Running on Zero
File size: 7,095 Bytes
78fd9bf 3a352d5 78fd9bf 25b7ba0 78fd9bf 3a352d5 78fd9bf 3a352d5 78fd9bf 3a352d5 ceb077a 78fd9bf 3a352d5 78fd9bf 3a352d5 78fd9bf 25b7ba0 78fd9bf 479242e 3a352d5 3602552 78fd9bf 3602552 3a352d5 78fd9bf 3a352d5 78fd9bf 3a352d5 78fd9bf 3a352d5 78fd9bf 3a352d5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | from __future__ import annotations
import functools
from typing import Any
try: # must precede torch/transformers imports: it patches CUDA allocation
import spaces
except ImportError: # pragma: no cover - local run
spaces = None
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
BASELINE_REPO = "yava-code/Tessera-1B-Nano-Base"
CONCEPT_REPO = "yava-code/Tessera-1B-Nano"
EXAMPLE_PROMPTS = [
"The most common metals are",
"Photosynthesis is the process used by plants",
"In the nineteenth century, transport changed because",
"A simple recipe for bread needs flour, water and",
]
DEFAULT_MAX_NEW_TOKENS = 48
MAX_NEW_TOKENS_LIMIT = 160
DEFAULT_SEED = 7
INTRO_NOTE = (
"Two copies of the same 360M-parameter language model, trained on the same billion"
" tokens in the same order. The right one also carries a small second module: it"
" compresses every four tokens into one concept code, guesses the *next* concept"
" code, and feeds that guess back into the decoder while it writes.\n\n"
"Type a prompt and watch both write. Then look at the third column: that is the"
" same concept model with its concept feedback silenced. Something changes - and"
" the loss line under the box shows how much the model minds. Now the strange"
" part, measured on held-out data: replace the concept feedback with a concept"
" predicted from a *different* sentence, and the model does not care. It reads the"
" channel constantly - silencing it hurts - but whatever it reads there is not the"
" sentence. What is it reading, then? That question is the study.\n\n"
"*(Same seed and prompt give bit-identical generations on every click; change the"
" seed to sample a different draw.)*"
)
MODELS: dict[str, tuple[AutoTokenizer, Any]] = {}
def load_models(*, gpu_dtype: bool = False) -> None:
"""Load both checkpoints on CPU; gpu_dtype selects bf16 for the ZeroGPU move."""
if MODELS:
return
dtype = torch.bfloat16 if gpu_dtype else torch.float32
for key, repo, trust in (
("base", BASELINE_REPO, False),
("concept", CONCEPT_REPO, True),
):
tokenizer = AutoTokenizer.from_pretrained(repo)
model = AutoModelForCausalLM.from_pretrained(
repo,
trust_remote_code=trust,
dtype=dtype,
)
model.eval()
MODELS[key] = (tokenizer, model)
def _device(model: Any) -> torch.device:
return next(model.parameters()).device
def _generate(
tokenizer: AutoTokenizer,
model: Any,
prompt: str,
max_new_tokens: int,
*,
seed: int,
zero_feedback: bool = False,
) -> str:
inputs = tokenizer(prompt, return_tensors="pt").to(_device(model))
if zero_feedback:
# generate() rejects unknown kwargs, so route concept_mode through forward.
model.forward = functools.partial(model.forward, concept_mode="zero")
# transformers 4.x multinomial sampling draws from the global RNG (no generator
# argument), so reproducibility needs a device-global seed reset per call.
device = _device(model)
if device.type == "cuda":
torch.cuda.manual_seed(seed)
else:
torch.manual_seed(seed)
try:
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=True,
temperature=0.8,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id,
)
finally:
if zero_feedback:
del model.forward
return tokenizer.decode(
outputs[0][inputs["input_ids"].shape[1] :],
skip_special_tokens=True,
)
def _prompt_loss(
tokenizer: AutoTokenizer,
model: Any,
prompt: str,
*,
zero_feedback: bool = False,
) -> float:
inputs = tokenizer(prompt, return_tensors="pt").to(_device(model))
with torch.no_grad():
outputs = model(**inputs, concept_mode="zero") if zero_feedback else model(**inputs)
logits = outputs.logits[:, :-1, :]
targets = inputs["input_ids"][:, 1:]
return float(
torch.nn.functional.cross_entropy(
logits.reshape(-1, logits.size(-1)),
targets.reshape(-1),
reduction="mean",
)
)
def _run(prompt: str, max_new_tokens: int, seed: int) -> tuple[str, str, str, str, str]:
if set(MODELS) != {"base", "concept"}:
raise RuntimeError("Models are still loading; retry in a minute.")
prompt = prompt.strip() or EXAMPLE_PROMPTS[0]
max_new_tokens = max(16, min(int(max_new_tokens), MAX_NEW_TOKENS_LIMIT))
seed = max(0, min(int(seed), 2**31 - 1))
base_tok, base_model = MODELS["base"]
concept_tok, concept_model = MODELS["concept"]
# Identical seeds across the arms make repeats bit-reproducible; per-arm offsets
# only decorrelate the columns, they change nothing on a repeated click.
base_text = _generate(base_tok, base_model, prompt, max_new_tokens, seed=seed)
concept_text = _generate(concept_tok, concept_model, prompt, max_new_tokens, seed=seed + 1)
zero_text = _generate(
concept_tok,
concept_model,
prompt,
max_new_tokens,
seed=seed + 2,
zero_feedback=True,
)
base_loss = _prompt_loss(base_tok, base_model, prompt)
concept_loss = _prompt_loss(concept_tok, concept_model, prompt)
zero_loss = _prompt_loss(concept_tok, concept_model, prompt, zero_feedback=True)
delta = zero_loss - concept_loss
base_note = f"teacher-forced NTP loss on the prompt: {base_loss:.3f}"
concept_note = (
f"teacher-forced NTP loss on the prompt: {concept_loss:.3f}; zeroing the concept"
f" feedback costs {delta:+.3f} nats here - the model notices (held-out average below)"
)
return base_text, base_note, concept_text, zero_text, concept_note
if spaces is not None: # ZeroGPU: models live on CPU, compute moves to a shared A100
@spaces.GPU(duration=45)
def _gpu_run(prompt: str, max_new_tokens: int, seed: int) -> tuple[str, str, str, str, str]:
# Only the device move and the math run inside the GPU window: downloads and
# CPU loading stay in the main process, or ZeroGPU kills the call on timeout.
for _, model in MODELS.values():
if next(model.parameters()).device.type != "cuda":
model.to("cuda")
return _run(prompt, max_new_tokens, seed)
def run_comparison(
prompt: str, max_new_tokens: int, seed: int
) -> tuple[str, str, str, str, str]:
load_models(gpu_dtype=True)
return _gpu_run(prompt, max_new_tokens, seed)
else: # local: plain cpu/cuda execution
def run_comparison(
prompt: str, max_new_tokens: int, seed: int
) -> tuple[str, str, str, str, str]:
load_models()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
for _, model in MODELS.values():
model.to(device)
return _run(prompt, max_new_tokens, seed)
|