mjolnir-kernel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mjolnir/__init__.py +76 -0
- mjolnir/backend.py +934 -0
- mjolnir/banner.py +127 -0
- mjolnir/calibrate.py +182 -0
- mjolnir/draftvocab.py +150 -0
- mjolnir/kvfp6.py +326 -0
- mjolnir/kvscales.py +162 -0
- mjolnir/lmhead.py +379 -0
- mjolnir/qpack.py +132 -0
- mjolnir/runner.py +754 -0
- mjolnir/t5_decode.py +2990 -0
- mjolnir_kernel-0.1.0.dist-info/METADATA +222 -0
- mjolnir_kernel-0.1.0.dist-info/RECORD +17 -0
- mjolnir_kernel-0.1.0.dist-info/WHEEL +4 -0
- mjolnir_kernel-0.1.0.dist-info/entry_points.txt +2 -0
- mjolnir_kernel-0.1.0.dist-info/licenses/LICENSE +202 -0
- mjolnir_kernel-0.1.0.dist-info/licenses/NOTICE +64 -0
mjolnir/banner.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""The Mjolnir boot banner.
|
|
2
|
+
|
|
3
|
+
Owned by the ``mjolnir`` package (not a vLLM patch): when the Mjolnir
|
|
4
|
+
backend serves a model, ``emit()`` logs the hammer exactly once, with the
|
|
5
|
+
info block (``Mjolnir v<version>``, author, repo, and the serving config
|
|
6
|
+
name when the CLI set ``MJOLNIR_CONFIG``) placed to the art's right,
|
|
7
|
+
vertically centred. It goes through vLLM's own logger, so it follows the
|
|
8
|
+
server's formatter the same way vLLM's own boot logo does (ANSI colour
|
|
9
|
+
only under a color formatter), and it can never break a server's
|
|
10
|
+
start-up. ``python -m mjolnir.banner`` previews it without vLLM.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
__all__ = ["ART", "HEAD_START", "banner", "emit"]
|
|
19
|
+
|
|
20
|
+
# The hammer. Committed art — do not re-flow.
|
|
21
|
+
ART = [
|
|
22
|
+
" ▓██",
|
|
23
|
+
" █▒██",
|
|
24
|
+
" █ ░█▒",
|
|
25
|
+
" █ ▓█",
|
|
26
|
+
" ██ █",
|
|
27
|
+
" █ █░",
|
|
28
|
+
" ██ ▒█",
|
|
29
|
+
" █████▓ ██████",
|
|
30
|
+
" █ ▓▒ ██",
|
|
31
|
+
" ▒████ ████▓",
|
|
32
|
+
" ░███",
|
|
33
|
+
]
|
|
34
|
+
# Rows at/below this index make up the head (brand orange); above it, the handle.
|
|
35
|
+
HEAD_START = 7
|
|
36
|
+
|
|
37
|
+
_HEAD = "\033[1;93m" # bold brand orange (the hammer head)
|
|
38
|
+
_WHITE = "\033[1m" # bold default foreground (handle, author)
|
|
39
|
+
_RESET = "\033[0m"
|
|
40
|
+
_GAP = " "
|
|
41
|
+
_AUTHOR = "by Mieszko Syty (ms1design)"
|
|
42
|
+
_URL = "https://github.com/thor-arena/mjolnir"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def banner(version: str, color: bool = False, model: str | None = None) -> str:
|
|
46
|
+
"""Render the banner: the art, with the info block (version, author,
|
|
47
|
+
repo, and ``Model: <config>`` when given) to its right, vertically
|
|
48
|
+
centred against the art. The message is one block of lines (no leading
|
|
49
|
+
or trailing empty line — ``emit()`` renders those as their own empty
|
|
50
|
+
log records, the way the vLLM boot logo's template does). ``color``
|
|
51
|
+
switches on the ANSI codes (head in the brand orange, the rest bold
|
|
52
|
+
white)."""
|
|
53
|
+
info = [f"Mjolnir v{version}", _AUTHOR, _URL]
|
|
54
|
+
if model:
|
|
55
|
+
info.append(f"Model: {model}")
|
|
56
|
+
n, k = len(ART), len(info)
|
|
57
|
+
start = (n - k) // 2
|
|
58
|
+
width = max(len(line) for line in ART)
|
|
59
|
+
rows = []
|
|
60
|
+
for i, line in enumerate(ART):
|
|
61
|
+
idx = i - start
|
|
62
|
+
if not color:
|
|
63
|
+
if 0 <= idx < k:
|
|
64
|
+
rows.append(line.ljust(width) + _GAP + info[idx])
|
|
65
|
+
else:
|
|
66
|
+
rows.append(line)
|
|
67
|
+
continue
|
|
68
|
+
art = line.ljust(width) + _GAP if 0 <= idx < k else line
|
|
69
|
+
segs = [(_HEAD if i >= HEAD_START else _WHITE, art)]
|
|
70
|
+
if 0 <= idx < k:
|
|
71
|
+
if idx == 0:
|
|
72
|
+
segs += [(_HEAD, "Mjolnir"), (_WHITE, f" v{version}")]
|
|
73
|
+
else:
|
|
74
|
+
segs += [(_WHITE, info[idx])]
|
|
75
|
+
rows.append("".join(f"{code}{text}{_RESET}" for code, text in segs))
|
|
76
|
+
return "\n".join(rows)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_shown = False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def emit() -> None:
|
|
83
|
+
"""Log the banner once, through vLLM's logger when importable (its
|
|
84
|
+
formatter decides the ANSI colour, exactly like vLLM's own boot logo)
|
|
85
|
+
and a stdlib logger otherwise. The art is one multi-line record; the
|
|
86
|
+
empty lines on top and bottom are rendered as empty-string records
|
|
87
|
+
(vLLM's formatter turns them into bare prefixed lines, like the boot
|
|
88
|
+
logo's tail). The serving config name rides in ``MJOLNIR_CONFIG`` (set
|
|
89
|
+
by the ``mjolnir`` CLI's serve path). Never raises: a banner must not
|
|
90
|
+
be able to break a server's start."""
|
|
91
|
+
global _shown
|
|
92
|
+
if _shown:
|
|
93
|
+
return
|
|
94
|
+
_shown = True
|
|
95
|
+
try:
|
|
96
|
+
from mjolnir import __version__
|
|
97
|
+
|
|
98
|
+
model = os.environ.get("MJOLNIR_CONFIG", "").strip() or None
|
|
99
|
+
try:
|
|
100
|
+
from vllm.logger import current_formatter_type, init_logger
|
|
101
|
+
|
|
102
|
+
lgr = init_logger("vllm.mjolnir")
|
|
103
|
+
color = current_formatter_type(lgr) == "color"
|
|
104
|
+
except ImportError:
|
|
105
|
+
import logging
|
|
106
|
+
|
|
107
|
+
lgr = logging.getLogger("vllm.mjolnir")
|
|
108
|
+
color = sys.stderr.isatty()
|
|
109
|
+
lgr.info("")
|
|
110
|
+
lgr.info("%s", banner(__version__, color=color, model=model))
|
|
111
|
+
lgr.info("")
|
|
112
|
+
except Exception: # noqa: BLE001 - the banner is decoration, not a dependency
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__": # `python -m mjolnir.banner [--plain]`: preview without vLLM
|
|
117
|
+
import mjolnir
|
|
118
|
+
|
|
119
|
+
plain = "--plain" in sys.argv[1:]
|
|
120
|
+
msg = banner(
|
|
121
|
+
mjolnir.__version__,
|
|
122
|
+
color=sys.stdout.isatty() and not plain,
|
|
123
|
+
model=os.environ.get("MJOLNIR_CONFIG", "").strip() or None,
|
|
124
|
+
)
|
|
125
|
+
print("")
|
|
126
|
+
print(msg)
|
|
127
|
+
print("")
|
mjolnir/calibrate.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Calibrate per-(layer, KV head, channel) K/V scales for the fp6 (e2m3) KV cache.
|
|
2
|
+
|
|
3
|
+
Runs the model in-process on vLLM with the Mjolnir backend on **fp8** KV and
|
|
4
|
+
``MJOLNIR_ATTN_KV_CALIBRATE=1``: every K/V row the backend writes to the
|
|
5
|
+
cache is folded into a running per-channel amax (``mjolnir.kvscales``). After
|
|
6
|
+
the engine's own warm-up and profiling runs the accumulators are zeroed, a
|
|
7
|
+
varied prompt set is generated (prose, code, math, multilingual, long
|
|
8
|
+
contexts), and the result is written as a ``mjolnir.kv_scales/2`` file —
|
|
9
|
+
what ``MJOLNIR_ATTN_KV_SCALES`` points ``--kv-cache-dtype fp6_e2m3`` at.
|
|
10
|
+
|
|
11
|
+
python -m mjolnir.calibrate --config /configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR/config.yaml \\
|
|
12
|
+
--out /out/kv_scales.json --download-dir /data/models/huggingface
|
|
13
|
+
python -m mjolnir.calibrate --model Qwen/Qwen3-0.6B --out /tmp/scales.json # small models / tests
|
|
14
|
+
|
|
15
|
+
The scales cover every attention layer that runs on the Mjolnir backend,
|
|
16
|
+
drafters included (drafting writes its KV too): the built-in MTP head, and an
|
|
17
|
+
external drafter whose ``speculative-config.attention_backend`` is MJOLNIR
|
|
18
|
+
(DSpark: its 5 layers come out as layers 64-68). ``mjolnir calibrate`` runs this in the image and writes a config
|
|
19
|
+
with its own drafter to ``<config>.kv_scales.json``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import dataclasses
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
|
|
30
|
+
TOPICS = [
|
|
31
|
+
"Explain how a transformer's attention mechanism works, step by step, with a small numeric example.",
|
|
32
|
+
"Write a Python function that parses an ISO-8601 timestamp without external libraries, then test it.",
|
|
33
|
+
"def quicksort(xs):\n # complete this implementation and explain its complexity\n",
|
|
34
|
+
"Summarize the causes and consequences of the 1929 stock market crash in five paragraphs.",
|
|
35
|
+
"Solve: a train leaves at 9:40 at 72 km/h, another at 10:05 at 90 km/h on the same track. When do they meet?",
|
|
36
|
+
"Translate into German, French and Polish: 'The quick brown fox jumps over the lazy dog near the river bank.'",
|
|
37
|
+
"Write a SQL query that returns the top three customers by revenue per region, with window functions.",
|
|
38
|
+
"Compose a short story about a lighthouse keeper who finds a message in a bottle.",
|
|
39
|
+
"List the differences between TCP and UDP and when you would use each, as a table.",
|
|
40
|
+
"Prove that the square root of two is irrational.",
|
|
41
|
+
"Describe the architecture of the NVIDIA Blackwell GPU memory hierarchy for a systems engineer.",
|
|
42
|
+
"Rust: implement a thread-safe bounded queue with condition variables; explain the invariants.",
|
|
43
|
+
"What are the main ideas of Kant's Critique of Pure Reason? Answer for a first-year student.",
|
|
44
|
+
"日本の四季について、それぞれの季節の特徴を説明してください。",
|
|
45
|
+
"Напиши краткое руководство по выращиванию томатов на балконе.",
|
|
46
|
+
'Given the JSON {"a": [1, 2, {"b": null}], "c": "x"}, write a jq expression that extracts b and explain it.',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
LONG_SOURCE = (
|
|
50
|
+
"In the long history of computing, memory bandwidth has repeatedly set the pace of progress. "
|
|
51
|
+
"Each generation of accelerators added arithmetic faster than it added bytes per second, and "
|
|
52
|
+
"every workload that streams data once — decoding a language model token by token is one — "
|
|
53
|
+
"became a question of how few bytes it could read. Caches help only when data is reused; a "
|
|
54
|
+
"key-value cache of a long conversation is read in full for every new token, so its format "
|
|
55
|
+
"decides the speed. "
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def long_prompt(tokens: int, i: int) -> str:
|
|
60
|
+
"""A long context (~`tokens` words) ending in a question about it."""
|
|
61
|
+
words_per_rep = len(LONG_SOURCE.split())
|
|
62
|
+
reps = max(1, tokens // words_per_rep)
|
|
63
|
+
body = "".join(f"[section {k + i}] {LONG_SOURCE}" for k in range(reps))
|
|
64
|
+
return body + "\nQuestion: summarize the argument above in three sentences and name its weakest step."
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _engine_parser():
|
|
68
|
+
from vllm.engine.arg_utils import EngineArgs
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
|
72
|
+
except ImportError: # older vLLM
|
|
73
|
+
from vllm.utils import FlexibleArgumentParser
|
|
74
|
+
return EngineArgs.add_cli_args(FlexibleArgumentParser()), EngineArgs
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def engine_kwargs_from_config(path: str) -> dict:
|
|
78
|
+
"""A `vllm serve --config` YAML as LLM(...) kwargs, parsed by vLLM's own
|
|
79
|
+
argument parser — so every value gets the type `vllm serve` gives it
|
|
80
|
+
(``16K`` -> 16384, ``kv-cache-dtype-skip-layers: sliding_window`` -> a
|
|
81
|
+
list, nested dicts -> configs). Serve-only keys (port, served-model-name,
|
|
82
|
+
tool parsers, ...) are dropped; only values that differ from the engine
|
|
83
|
+
defaults are returned."""
|
|
84
|
+
import tempfile
|
|
85
|
+
|
|
86
|
+
import yaml
|
|
87
|
+
|
|
88
|
+
parser, engine_args = _engine_parser()
|
|
89
|
+
data = yaml.safe_load(open(path)) or {}
|
|
90
|
+
known = {k: v for k, v in data.items() if "--" + str(k).replace("_", "-") in parser._option_string_actions}
|
|
91
|
+
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
|
|
92
|
+
yaml.safe_dump(known, f)
|
|
93
|
+
try:
|
|
94
|
+
ns = vars(parser.parse_args(["--config", f.name]))
|
|
95
|
+
finally:
|
|
96
|
+
os.unlink(f.name)
|
|
97
|
+
base = vars(parser.parse_args([]))
|
|
98
|
+
fields = {fld.name for fld in dataclasses.fields(engine_args)}
|
|
99
|
+
kw = {}
|
|
100
|
+
for k, v in ns.items():
|
|
101
|
+
if k not in fields:
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
same = v == base.get(k)
|
|
105
|
+
except Exception: # noqa: BLE001 - uncomparable config objects: keep the value
|
|
106
|
+
same = False
|
|
107
|
+
if not same:
|
|
108
|
+
kw[k] = v
|
|
109
|
+
return kw
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main(argv=None) -> int:
|
|
113
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
114
|
+
src = ap.add_mutually_exclusive_group(required=True)
|
|
115
|
+
src.add_argument("--config", help="vllm serve YAML (the serving config to calibrate)")
|
|
116
|
+
src.add_argument("--model", help="a model id (no config: defaults for everything else)")
|
|
117
|
+
ap.add_argument("--out", required=True, help="where to write the mjolnir.kv_scales/2 JSON")
|
|
118
|
+
ap.add_argument("--download-dir", default=None)
|
|
119
|
+
ap.add_argument("--max-model-len", type=int, default=32768)
|
|
120
|
+
ap.add_argument("--gpu-mem", type=float, default=None, help="gpu_memory_utilization (default: the config's)")
|
|
121
|
+
ap.add_argument("--prompts", type=int, default=48, help="short prompts (cycled through the topic list)")
|
|
122
|
+
ap.add_argument("--long", type=int, default=6, help="long-context prompts")
|
|
123
|
+
ap.add_argument("--long-tokens", type=int, default=6000, help="approx. words per long prompt")
|
|
124
|
+
ap.add_argument("--max-tokens", type=int, default=96, help="tokens generated per prompt")
|
|
125
|
+
args = ap.parse_args(argv)
|
|
126
|
+
|
|
127
|
+
os.environ["MJOLNIR_ATTN_KV_CALIBRATE"] = "1"
|
|
128
|
+
os.environ.setdefault("VLLM_ENABLE_V1_MULTIPROCESSING", "0") # the accumulators live in this process
|
|
129
|
+
import mjolnir
|
|
130
|
+
from mjolnir import kvscales
|
|
131
|
+
|
|
132
|
+
mjolnir.register()
|
|
133
|
+
from vllm import LLM, SamplingParams
|
|
134
|
+
|
|
135
|
+
kw = engine_kwargs_from_config(args.config) if args.config else {"model": args.model, "dtype": "bfloat16"}
|
|
136
|
+
for k in ("served_model_name",):
|
|
137
|
+
kw.pop(k, None)
|
|
138
|
+
kw.update(
|
|
139
|
+
kv_cache_dtype="fp8", # calibrate on the fp8 path; the K/V are observed before quantization
|
|
140
|
+
enforce_eager=True,
|
|
141
|
+
max_model_len=min(int(kw.get("max_model_len") or args.max_model_len), args.max_model_len),
|
|
142
|
+
seed=0,
|
|
143
|
+
)
|
|
144
|
+
kw["attention_backend"] = "MJOLNIR" if mjolnir.has_native_member() else "CUSTOM"
|
|
145
|
+
kw.pop("attention_config", None)
|
|
146
|
+
if args.gpu_mem is not None:
|
|
147
|
+
kw["gpu_memory_utilization"] = args.gpu_mem
|
|
148
|
+
if args.download_dir:
|
|
149
|
+
kw["download_dir"] = args.download_dir
|
|
150
|
+
print(f"calibrate: LLM({', '.join(f'{k}={v!r}' for k, v in sorted(kw.items()))})", flush=True)
|
|
151
|
+
try:
|
|
152
|
+
llm = LLM(**kw)
|
|
153
|
+
except TypeError:
|
|
154
|
+
backend = kw.pop("attention_backend")
|
|
155
|
+
llm = LLM(attention_config={"backend": backend}, **kw)
|
|
156
|
+
|
|
157
|
+
kvscales.reset() # drop what the warm-up / profiling runs wrote
|
|
158
|
+
prompts = [TOPICS[i % len(TOPICS)] for i in range(args.prompts)]
|
|
159
|
+
budget = max(256, kw["max_model_len"] - args.max_tokens - 64)
|
|
160
|
+
prompts += [long_prompt(min(args.long_tokens, budget * 3 // 4), i) for i in range(args.long)]
|
|
161
|
+
sp = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=args.max_tokens, seed=0)
|
|
162
|
+
t0 = time.time()
|
|
163
|
+
outs = llm.generate(prompts, sp)
|
|
164
|
+
toks = sum(len(o.prompt_token_ids) + len(o.outputs[0].token_ids) for o in outs)
|
|
165
|
+
doc = kvscales.dump(args.out, model=str(kw.get("model", "")))
|
|
166
|
+
if not doc["amax"]:
|
|
167
|
+
print("calibrate: no K/V was observed — did the Mjolnir backend run?", file=sys.stderr)
|
|
168
|
+
return 1
|
|
169
|
+
print(f"calibrate: {len(prompts)} prompts, {toks} tokens in {time.time() - t0:.0f}s", flush=True)
|
|
170
|
+
for name, e in doc["amax"].items():
|
|
171
|
+
k_max, v_max = max(max(r) for r in e["k"]), max(max(r) for r in e["v"])
|
|
172
|
+
k_med = sorted(x for r in e["k"] for x in r)[len(e["k"]) * len(e["k"][0]) // 2]
|
|
173
|
+
print(
|
|
174
|
+
f" {name}: K amax {k_max:8.3f} (median channel {k_med:7.3f}) V amax {v_max:8.3f}"
|
|
175
|
+
f" ({len(e['k'])} heads x {len(e['k'][0])} channels)"
|
|
176
|
+
)
|
|
177
|
+
print(f"wrote {args.out} ({len(doc['amax'])} layers)")
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
raise SystemExit(main())
|
mjolnir/draftvocab.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Count which tokens the target model generates — the drafter's vocabulary
|
|
2
|
+
for VocabTrim (``MJOLNIR_DRAFT_VOCAB``, see ``mjolnir.lmhead``).
|
|
3
|
+
|
|
4
|
+
Runs the serving config in-process (vLLM ``LLM``), samples a varied prompt
|
|
5
|
+
set (several completions per prompt, temperature 0.7), counts the generated
|
|
6
|
+
token ids and writes them most frequent first, with the coverage — the share
|
|
7
|
+
of generated tokens inside the top-K — for a few K. Coverage is measured
|
|
8
|
+
held out (ranking from half the prompts, scored on the other half), since the
|
|
9
|
+
in-sample number is 100% as soon as K passes the distinct-token count. Ids the
|
|
10
|
+
run never generated follow in id order (BPE merge rank), so a top-K always has
|
|
11
|
+
K tokens. VocabTrim found the target's own generations the best source
|
|
12
|
+
(arXiv 2506.22694).
|
|
13
|
+
|
|
14
|
+
python -m mjolnir.draftvocab --config /configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3/config.yaml \\
|
|
15
|
+
--out /out/draft_vocab.json --download-dir /data/models/huggingface
|
|
16
|
+
|
|
17
|
+
File: ``{"format": "mjolnir.draft_vocab/1", "model": ..., "generated_tokens": N,
|
|
18
|
+
"coverage": {"32768": 0.997, ...}, "tokens": [ids, most frequent first]}``.
|
|
19
|
+
Special tokens (EOS, the chat / think markers) are always included.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from collections import Counter
|
|
30
|
+
|
|
31
|
+
FORMAT = "mjolnir.draft_vocab/1"
|
|
32
|
+
COVERAGE_K = (4096, 8192, 16384, 32768, 49152, 65536)
|
|
33
|
+
|
|
34
|
+
EXTRA_TOPICS = [
|
|
35
|
+
"Write a detailed README for a command-line tool that benchmarks GPUs.",
|
|
36
|
+
"Explain gradient descent to a high-school student, then give the math.",
|
|
37
|
+
"Write TypeScript code for a debounced search box in React, with tests.",
|
|
38
|
+
"Summarize the plot of a classic novel of your choice and analyze its themes.",
|
|
39
|
+
"Give a step-by-step recipe for sourdough bread with timings.",
|
|
40
|
+
"Compare PostgreSQL and SQLite for an embedded device; answer as a table and a recommendation.",
|
|
41
|
+
"Write a formal email declining a meeting and proposing two alternative times.",
|
|
42
|
+
"Debug this Python: `for i in range(10): print(i/0)` — explain the error and fix it.",
|
|
43
|
+
"Describe the water cycle in Spanish, then in English.",
|
|
44
|
+
"Write a C function that reverses a linked list in place; explain each line.",
|
|
45
|
+
"List ten interview questions for a senior backend engineer with model answers.",
|
|
46
|
+
"Explain how TLS 1.3 handshakes work, with a sequence of messages.",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def rank(counts: Counter, special: set, vocab: int) -> list[int]:
|
|
51
|
+
"""Special tokens, then generated ids by count, then the rest in id order."""
|
|
52
|
+
out = [t for t in sorted(special) if t not in counts] + [t for t, _ in counts.most_common()]
|
|
53
|
+
seen = set(out)
|
|
54
|
+
return out + [t for t in range(vocab) if t not in seen]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def held_out_coverage(train: Counter, test: Counter, special: set, vocab: int) -> dict[str, float]:
|
|
58
|
+
"""Share of ``test``'s tokens inside the top-K of a ranking built from ``train``."""
|
|
59
|
+
total = sum(test.values())
|
|
60
|
+
if not total:
|
|
61
|
+
return {str(k): 0.0 for k in COVERAGE_K}
|
|
62
|
+
order = rank(train, special, vocab)
|
|
63
|
+
pos = {t: i for i, t in enumerate(order)}
|
|
64
|
+
out = {}
|
|
65
|
+
for k in COVERAGE_K:
|
|
66
|
+
out[str(k)] = round(sum(c for t, c in test.items() if pos.get(t, vocab) < k) / total, 5)
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main(argv=None) -> int:
|
|
71
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
72
|
+
src = ap.add_mutually_exclusive_group(required=True)
|
|
73
|
+
src.add_argument("--config", help="vllm serve YAML")
|
|
74
|
+
src.add_argument("--model", help="a model id")
|
|
75
|
+
ap.add_argument("--out", required=True)
|
|
76
|
+
ap.add_argument("--download-dir", default=None)
|
|
77
|
+
ap.add_argument("--max-model-len", type=int, default=16384)
|
|
78
|
+
ap.add_argument("--prompts", type=int, default=192)
|
|
79
|
+
ap.add_argument("--samples", type=int, default=4, help="completions per prompt")
|
|
80
|
+
ap.add_argument("--max-tokens", type=int, default=768)
|
|
81
|
+
ap.add_argument("--long", type=int, default=4, help="long-context prompts")
|
|
82
|
+
args = ap.parse_args(argv)
|
|
83
|
+
|
|
84
|
+
os.environ.setdefault("VLLM_ENABLE_V1_MULTIPROCESSING", "0")
|
|
85
|
+
for k in ("MJOLNIR_DRAFT_VOCAB", "MJOLNIR_LM_HEAD"): # count with the plain heads
|
|
86
|
+
os.environ.pop(k, None)
|
|
87
|
+
import mjolnir
|
|
88
|
+
from mjolnir.calibrate import TOPICS, engine_kwargs_from_config, long_prompt
|
|
89
|
+
|
|
90
|
+
mjolnir.register()
|
|
91
|
+
from vllm import LLM, SamplingParams
|
|
92
|
+
|
|
93
|
+
kw = engine_kwargs_from_config(args.config) if args.config else {"model": args.model, "dtype": "bfloat16"}
|
|
94
|
+
kw.pop("served_model_name", None)
|
|
95
|
+
kw["max_model_len"] = min(int(kw.get("max_model_len") or args.max_model_len), args.max_model_len)
|
|
96
|
+
if args.download_dir:
|
|
97
|
+
kw["download_dir"] = args.download_dir
|
|
98
|
+
print(f"draftvocab: LLM({', '.join(f'{k}={v!r}' for k, v in sorted(kw.items()))})", flush=True)
|
|
99
|
+
llm = LLM(**kw)
|
|
100
|
+
topics = TOPICS + EXTRA_TOPICS
|
|
101
|
+
prompts = [topics[i % len(topics)] for i in range(args.prompts)]
|
|
102
|
+
prompts += [long_prompt(min(4000, kw["max_model_len"] // 2), i) for i in range(args.long)]
|
|
103
|
+
msgs = [[{"role": "user", "content": p}] for p in prompts]
|
|
104
|
+
sp = SamplingParams(temperature=0.7, top_p=0.95, max_tokens=args.max_tokens, n=args.samples, seed=0)
|
|
105
|
+
t0 = time.time()
|
|
106
|
+
outs = llm.chat(msgs, sp)
|
|
107
|
+
counts: Counter = Counter()
|
|
108
|
+
halves: tuple[Counter, Counter] = (Counter(), Counter())
|
|
109
|
+
for i, o in enumerate(outs):
|
|
110
|
+
for c in o.outputs:
|
|
111
|
+
counts.update(c.token_ids)
|
|
112
|
+
halves[i % 2].update(c.token_ids)
|
|
113
|
+
total = sum(counts.values())
|
|
114
|
+
if not total:
|
|
115
|
+
print("draftvocab: nothing generated", file=sys.stderr)
|
|
116
|
+
return 1
|
|
117
|
+
tok = llm.get_tokenizer()
|
|
118
|
+
special = set(getattr(tok, "all_special_ids", []) or [])
|
|
119
|
+
try:
|
|
120
|
+
special |= set(tok.get_added_vocab().values())
|
|
121
|
+
except Exception: # noqa: BLE001
|
|
122
|
+
pass
|
|
123
|
+
vocab = len(tok)
|
|
124
|
+
ranked = rank(counts, special, vocab)
|
|
125
|
+
coverage = held_out_coverage(halves[0], halves[1], special, vocab)
|
|
126
|
+
doc = {
|
|
127
|
+
"format": FORMAT,
|
|
128
|
+
"model": str(kw.get("model", "")),
|
|
129
|
+
"generated_tokens": total,
|
|
130
|
+
"distinct_tokens": len(counts),
|
|
131
|
+
"coverage": coverage,
|
|
132
|
+
"coverage_kind": "held-out",
|
|
133
|
+
"measured_tokens": len(counts),
|
|
134
|
+
"tokens": ranked,
|
|
135
|
+
}
|
|
136
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
|
|
137
|
+
with open(args.out, "w") as f:
|
|
138
|
+
json.dump(doc, f)
|
|
139
|
+
print(
|
|
140
|
+
f"draftvocab: {len(prompts)} prompts x {args.samples}, {total} tokens generated in {time.time() - t0:.0f}s, "
|
|
141
|
+
f"{len(counts)} distinct"
|
|
142
|
+
)
|
|
143
|
+
for k in COVERAGE_K:
|
|
144
|
+
print(f" top {k:>6}: {coverage[str(k)] * 100:6.2f}% of held-out generated tokens")
|
|
145
|
+
print(f"wrote {args.out}")
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
raise SystemExit(main())
|