File size: 5,338 Bytes
80300e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Reproducible timing probe for the EXLLM 5M PyTorch checkpoint."""
import argparse
import json
import platform
import statistics
import sys
import time
from pathlib import Path

import torch

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from src.infer import bad_text  # noqa: E402
from src.model import EXLLM, EXLLMConfig  # noqa: E402
from src.tokenizer import HybridTokenizer, UTF8State  # noqa: E402


def sync(device):
    if device.type == "cuda":
        torch.cuda.synchronize(device)


def load(checkpoint, device):
    data = torch.load(checkpoint, map_location="cpu", weights_only=False)
    model = EXLLM(EXLLMConfig(**data["config"]))
    model.load_state_dict(data["model"])
    return model.eval().to(device), HybridTokenizer.load(ROOT / "tokenizer.json")


@torch.inference_mode()
def generate_timed(model, tok, prompt, device, max_new=48):
    ids = tok.encode_user(prompt)
    reserve = min(max_new, model.cfg.max_seq_len // 2)
    if len(ids) > model.cfg.max_seq_len - reserve:
        keep = model.cfg.max_seq_len - reserve - 3
        ids = [tok.BOS, tok.USER, *ids[2:-1][-max(1, keep):], tok.ASSIST]

    out, state, token_ms = [], UTF8State(), []
    sync(device)
    start = time.perf_counter_ns()
    ttft_ns = None
    for _ in range(min(max_new, model.cfg.max_seq_len - len(ids))):
        x = torch.tensor([ids + out], dtype=torch.long, device=device)
        logits = model(x)[0, -1].clone()
        logits[tok.PAD] = logits[tok.BOS] = logits[tok.USER] = logits[tok.ASSIST] = -1e30
        valid = torch.zeros_like(logits, dtype=torch.bool)
        if state.complete:
            valid[tok.EOS] = True
            for token in range(256):
                if state.accepts_byte(token):
                    valid[token] = True
            if tok.chars:
                valid[256:256 + len(tok.chars)] = True
        else:
            for token in range(256):
                if state.accepts_byte(token):
                    valid[token] = True
        logits[~valid] = -1e30
        token = int(torch.argmax(logits).item())
        sync(device)
        now = time.perf_counter_ns()
        if ttft_ns is None:
            ttft_ns = now - start
        token_ms.append((now - start) / 1e6 if len(token_ms) == 0 else 0.0)
        if token == tok.EOS:
            break
        if token < 256:
            state.push(token)
        elif not state.complete:
            continue
        out.append(token)
    sync(device)
    end = time.perf_counter_ns()
    while out:
        try:
            text = tok.decode(out)
            break
        except UnicodeDecodeError:
            out.pop()
    else:
        text = ""
    total_s = (end - start) / 1e9
    generated = len(out)
    decode_s = max(0.0, total_s - (ttft_ns or 0) / 1e9)
    return {
        "prompt": prompt,
        "input_tokens": len(ids),
        "output_tokens": generated,
        "ttft_ms": round((ttft_ns or 0) / 1e6, 3),
        "total_ms": round(total_s * 1000, 3),
        "decode_tokens_per_second": round(max(0, generated - 1) / decode_s, 3) if decode_s else None,
        "text": text.strip(),
        "bad_text": bad_text(text.strip()),
    }


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--checkpoint", default=str(ROOT / "weights" / "EXLLM-v1.1-5m-release3.pt"))
    ap.add_argument("--device", choices=("cpu", "cuda"), default="cuda" if torch.cuda.is_available() else "cpu")
    ap.add_argument("--repeats", type=int, default=3)
    args = ap.parse_args()
    device = torch.device(args.device)
    if device.type == "cuda" and not torch.cuda.is_available():
        raise SystemExit("CUDA was requested but is unavailable")

    started = time.perf_counter()
    model, tok = load(args.checkpoint, device)
    sync(device)
    load_s = time.perf_counter() - started
    prompts = ["こんにちは", "あなたは何というモデルですか?", "RAMとは何ですか?", "オフラインとは何ですか?"]
    _ = generate_timed(model, tok, prompts[0], device, 16)  # warm-up
    runs = [generate_timed(model, tok, p, device) for p in prompts for _ in range(args.repeats)]
    report = {
        "schema": "exllm-benchmark-v1",
        "checkpoint": Path(args.checkpoint).name,
        "parameters": model.num_parameters(),
        "device": str(device),
        "torch": torch.__version__,
        "python": platform.python_version(),
        "load_seconds": round(load_s, 4),
        "gpu": None,
        "runs": runs,
        "summary": {
            "median_ttft_ms": round(statistics.median(x["ttft_ms"] for x in runs), 3),
            "median_total_ms": round(statistics.median(x["total_ms"] for x in runs), 3),
            "median_decode_tokens_per_second": round(statistics.median(x["decode_tokens_per_second"] for x in runs if x["decode_tokens_per_second"] is not None), 3),
        },
    }
    if device.type == "cuda":
        prop = torch.cuda.get_device_properties(device)
        report["gpu"] = {
            "name": prop.name,
            "vram_bytes": prop.total_memory,
            "compute_capability": f"{prop.major}.{prop.minor}",
            "peak_allocated_bytes": torch.cuda.max_memory_allocated(device),
        }
    print(json.dumps(report, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()