File size: 10,427 Bytes
478f952
 
 
 
 
 
 
 
 
 
58e7d1b
 
478f952
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50cf4b5
 
478f952
50cf4b5
478f952
50cf4b5
 
 
 
 
 
478f952
 
 
 
50cf4b5
 
 
478f952
50cf4b5
 
 
 
478f952
 
50cf4b5
 
 
478f952
 
50cf4b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478f952
 
50cf4b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478f952
 
 
 
 
 
 
 
 
50cf4b5
478f952
50cf4b5
 
 
 
 
 
 
 
 
 
 
478f952
 
 
 
 
 
 
 
 
58e7d1b
478f952
 
 
50cf4b5
478f952
50cf4b5
 
 
478f952
50cf4b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478f952
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a6423b
478f952
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a6423b
478f952
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58e7d1b
 
 
 
 
 
 
478f952
 
 
 
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
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)