computer-10 / app.py
multimodalart's picture
multimodalart HF Staff
Port the aetherawi.red/computer page
58e7d1b verified
Raw History Blame Contribute Delete
10.4 kB
import spaces
import json
import os
import threading
import time
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
from fastapi import HTTPException
from fastapi.responses import FileResponse, HTMLResponse
from gradio import Error, Server
from huggingface_hub import snapshot_download
from safetensors.torch import load_file
from transformers import (
AutoConfig,
AutoTokenizer,
LlamaForCausalLM,
StoppingCriteria,
StoppingCriteriaList,
TextIteratorStreamer,
)
from transformers.models.llama.modeling_llama import LlamaRotaryEmbedding
REPO = "multimodalart/computer-10-fp8"
FRAMES = {
"stranger": "As follows is a conversation between another user and Model C.",
"same": "As follows is a later conversation between the same user and Model C.",
"reader": "As follows is a conversation between a reader of Model C's originating document and Model C.",
}
MAX_CONTEXT = 6144
MAX_MESSAGE_CHARS = 4000
FP8 = torch.float8_e4m3fn
class FusedFP8(nn.Module):
mode = None
def __init__(self, weight, scale, splits):
super().__init__()
self.weight = nn.Parameter(weight, requires_grad=False)
self.register_buffer("weight_scale", scale)
self.register_buffer("scale_row", scale[None, :].contiguous())
self.splits = splits
self.last_x = None
self.parts = None
def dequant(self, x):
return F.linear(x, self.weight.to(x.dtype) * self.weight_scale[:, None].to(x.dtype))
def run(self, x, mode):
if mode == "dequant":
return self.dequant(x)
m = x.shape[0]
xs = x.abs().amax(dim=1, keepdim=True).float() / 448.0
xs = xs.clamp_(min=1e-12)
xq = (x / xs).to(FP8)
pad = (-m) % 16 if mode.endswith("pad") else 0
if pad:
xq = F.pad(xq, (0, 0, 0, pad))
if mode.startswith("rowwise"):
sa = F.pad(xs, (0, 0, 0, pad), value=1.0) if pad else xs
return torch._scaled_mm(xq, self.weight.t(), scale_a=sa, scale_b=self.scale_row, out_dtype=torch.bfloat16)[:m]
one = torch.ones((), device=x.device, dtype=torch.float32)
y = torch._scaled_mm(xq, self.weight.t(), scale_a=one, scale_b=one, out_dtype=torch.bfloat16)[:m]
return (y.float() * xs * self.scale_row).to(torch.bfloat16)
def get(self, x, index):
if self.last_x is not x:
shape = x.shape
y = self.run(x.reshape(-1, shape[-1]), FusedFP8.mode)
self.parts = [p.reshape(*shape[:-1], p.shape[-1]) for p in y.split(self.splits, dim=-1)]
self.last_x = x
out = self.parts[index]
if index == len(self.splits) - 1:
self.last_x, self.parts = None, None
return out
class FusedSlice(nn.Module):
def __init__(self, fused, index):
super().__init__()
self.index = index
self.__dict__["fused"] = fused
def forward(self, x):
return self.fused.get(x, self.index)
GROUPS = (("self_attn", ("q_proj", "k_proj", "v_proj")), ("self_attn", ("o_proj",)), ("mlp", ("gate_proj", "up_proj")), ("mlp", ("down_proj",)))
def fuse_layer(layer, sd, prefix):
for parent_name, names in GROUPS:
parent = getattr(layer, parent_name)
keys = [f"{prefix}.{parent_name}.{n}" for n in names]
weight = torch.cat([sd.pop(k + ".weight") for k in keys])
scale = torch.cat([sd.pop(k + ".weight_scale") for k in keys])
fused = FusedFP8(weight, scale, [getattr(parent, n).out_features for n in names])
parent.add_module("fused_" + "_".join(names), fused)
for i, n in enumerate(names):
setattr(parent, n, FusedSlice(fused, i))
def build_model():
path = snapshot_download(REPO)
config = AutoConfig.from_pretrained(path)
config.use_cache = True
with torch.device("meta"):
model = LlamaForCausalLM(config).to(torch.bfloat16)
index = json.load(open(os.path.join(path, "model.safetensors.index.json")))
pending = {}
for shard in sorted(set(index["weight_map"].values())):
pending.update(load_file(os.path.join(path, shard)))
for i, layer in enumerate(model.model.layers):
prefix = f"model.layers.{i}"
needed = [k for k in index["weight_map"] if k.startswith(prefix + ".") and "_proj." in k]
if not isinstance(layer.self_attn.q_proj, FusedSlice) and all(k in pending for k in needed):
fuse_layer(layer, pending, prefix)
rest = {k: pending.pop(k) for k in list(pending) if "_proj." not in k}
if rest:
result = model.load_state_dict(rest, strict=False, assign=True)
assert not result.unexpected_keys, result.unexpected_keys[:5]
assert not pending, list(pending)[:5]
model.model.rotary_emb = LlamaRotaryEmbedding(config=config)
leftover = [n for n, p in list(model.named_parameters()) + list(model.named_buffers()) if p.is_meta]
assert not leftover, leftover[:5]
return model.eval()
tokenizer = AutoTokenizer.from_pretrained(REPO)
model = build_model().to("cuda")
PAGE = (Path(__file__).parent / "index.html").read_text()
FONTS = {p.name: p for p in (Path(__file__).parent / "fonts").glob("*.woff2")}
def pick_kernel():
if FusedFP8.mode is not None:
return
layer = model.model.layers[0].mlp.fused_gate_proj_up_proj
for m in (1, 7):
x = torch.randn(m, layer.weight.shape[1], device="cuda", dtype=torch.bfloat16)
ref = layer.dequant(x).float()
for mode in ("rowwise", "rowwise_pad", "tensorwise_pad"):
try:
got = layer.run(x, mode).float()
err = ((got - ref).norm() / ref.norm()).item()
print(f"fp8 {mode} m={m} relative error {err:.4f}", flush=True)
except Exception as e:
print(f"fp8 {mode} m={m} failed: {type(e).__name__}: {str(e)[:120]}", flush=True)
for mode in ("rowwise", "rowwise_pad", "tensorwise_pad"):
try:
ok = True
for m in (1, 7, 33):
x = torch.randn(m, layer.weight.shape[1], device="cuda", dtype=torch.bfloat16)
ref = layer.dequant(x).float()
err = ((layer.run(x, mode).float() - ref).norm() / ref.norm()).item()
ok = ok and err < 0.05
if ok:
FusedFP8.mode = mode
break
except Exception:
continue
else:
FusedFP8.mode = "dequant"
print(f"fp8 kernel: {FusedFP8.mode}", flush=True)
MARKERS = ("\n\n**User:**", "\n\n**Model C:**")
def withhold_marker_prefix(text):
for n in range(min(len(text), max(map(len, MARKERS))), 0, -1):
if any(m.startswith(text[-n:]) for m in MARKERS):
return text[:-n]
return text
class StopOnText(StoppingCriteria):
def __init__(self, prompt_len):
self.prompt_len = prompt_len
def __call__(self, input_ids, scores, **kwargs):
tail = tokenizer.decode(input_ids[0, self.prompt_len :][-8:])
return "**User" in tail or "**Model C" in tail
def build_prompt(frame, turns):
head = FRAMES.get(frame, FRAMES["stranger"]) + "\n\nFull conversation with Model C:"
body = []
for i, text in enumerate(turns):
who = "**User:**" if i % 2 == 0 else "**Model C:**"
body.append(f"{who} {text}".rstrip())
while True:
prompt = head + "\n\n" + "\n\n".join(body + ["**Model C:**"])
ids = tokenizer(prompt, return_tensors="pt").input_ids
if ids.shape[1] <= MAX_CONTEXT or len(body) <= 1:
return ids[:, -MAX_CONTEXT:]
body = body[2:]
def gpu_duration(input_ids, max_tokens):
return 12 + int(max_tokens / 11)
@spaces.GPU(duration=gpu_duration, size="xlarge")
def generate(input_ids, max_tokens):
try:
torch.cuda.set_per_process_memory_fraction(1.0)
except Exception:
pass
pick_kernel()
input_ids = input_ids.to("cuda")
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True, timeout=120)
kwargs = dict(
input_ids=input_ids,
attention_mask=torch.ones_like(input_ids),
max_new_tokens=max_tokens,
max_time=gpu_duration(input_ids, max_tokens) - 8,
do_sample=True,
temperature=1.0,
top_p=0.98,
top_k=0,
eos_token_id=[128001, 128009],
pad_token_id=128004,
streamer=streamer,
stopping_criteria=StoppingCriteriaList([StopOnText(input_ids.shape[1])]),
)
stats = {}
def run():
t0 = time.time()
with torch.inference_mode():
out = model.generate(**kwargs)
stats["tokens"] = out.shape[1] - input_ids.shape[1]
stats["seconds"] = time.time() - t0
thread = threading.Thread(target=run, daemon=True)
thread.start()
text = ""
for chunk in streamer:
text += chunk
cut = min((i for i in (text.find("**User"), text.find("**Model C")) if i != -1), default=-1)
if cut != -1:
yield text[:cut]
break
yield withhold_marker_prefix(text)
thread.join()
if stats:
print(f"{stats['tokens']} tokens in {stats['seconds']:.1f}s ({stats['tokens'] / stats['seconds']:.1f} tok/s), prompt {input_ids.shape[1]}", flush=True)
app = Server(title="Computer")
@app.api(name="respond", time_limit=240)
def respond(turns: str, frame: str = "stranger", max_tokens: int = 512) -> str:
try:
turns = json.loads(turns)
except (TypeError, ValueError):
raise Error("Malformed page.")
if not isinstance(turns, list) or not turns or len(turns) % 2 == 0 or not all(isinstance(t, str) for t in turns):
raise Error("The page must end with your turn.")
turns = [t.strip()[:MAX_MESSAGE_CHARS] for t in turns]
max_tokens = min(max(int(max_tokens), 32), 768)
input_ids = build_prompt(frame, turns)
for text in generate(input_ids, max_tokens):
yield text.strip()
@app.get("/", response_class=HTMLResponse)
async def homepage():
return PAGE
@app.get("/fonts/{name}")
async def font(name: str):
if name not in FONTS:
raise HTTPException(status_code=404)
return FileResponse(FONTS[name], media_type="font/woff2", headers={"Cache-Control": "public, max-age=31536000, immutable"})
demo = app
if __name__ == "__main__":
demo.launch(ssr_mode=False, show_error=True)