163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
"""How much does 4-bit cost, measured the same way on both engines.
|
|||
|
|
|
||
|
|
Absolute quality numbers are not comparable across engines -- different
|
||
|
|
tokenizers' edge cases, different chat handling. What *is* comparable is how
|
||
|
|
far each engine's own 4-bit build drifts from its own bf16 build on the same
|
||
|
|
prompts under greedy decoding. That isolates the quantization, which is the
|
||
|
|
question: is our W4A16 as faithful as llama.cpp's Q4_K_M?
|
||
|
|
"""
|
||
|
|
import argparse, json, subprocess, time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
HERE = Path(__file__).parent
|
||
|
|
|
||
|
|
PLAIN = [
|
||
|
|
"The capital of Australia is",
|
||
|
|
"In 1969, the Apollo 11 mission",
|
||
|
|
"def fibonacci(n):\n \"\"\"Return the nth Fibonacci number.\"\"\"\n",
|
||
|
|
"The three laws of thermodynamics state that",
|
||
|
|
"To make a proper espresso you need",
|
||
|
|
"The difference between TCP and UDP is",
|
||
|
|
"Photosynthesis converts",
|
||
|
|
"A binary search tree is a data structure that",
|
||
|
|
"The Treaty of Westphalia was signed in",
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def prompts():
|
||
|
|
out = []
|
||
|
|
for f, name in (("ref.json", "chat_short"), ("ref_long.json", "chat_long"),
|
||
|
|
("ref_tools.json", "chat_tools")):
|
||
|
|
p = HERE / f
|
||
|
|
if p.exists():
|
||
|
|
out.append((name, json.loads(p.read_text())["prompt"]))
|
||
|
|
out += [(f"plain_{i}", t) for i, t in enumerate(PLAIN)]
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def run_vllm(model, n_predict):
|
||
|
|
import vllm_omni # noqa: F401
|
||
|
|
from vllm import LLM, SamplingParams
|
||
|
|
|
||
|
|
llm = LLM(model=model, dtype="bfloat16", max_model_len=4096,
|
||
|
|
max_num_seqs=1, enable_prefix_caching=False,
|
||
|
|
trust_remote_code=False, disable_log_stats=True)
|
||
|
|
sp = SamplingParams(temperature=0.0, max_tokens=n_predict, ignore_eos=False)
|
||
|
|
names, texts = zip(*prompts())
|
||
|
|
outs = llm.generate(list(texts), sp, use_tqdm=False)
|
||
|
|
return {n: {"text": o.outputs[0].text,
|
||
|
|
"token_ids": list(o.outputs[0].token_ids)}
|
||
|
|
for n, o in zip(names, outs)}
|
||
|
|
|
||
|
|
|
||
|
|
def run_llamacpp(binary, gguf, n_predict, threads, port=8399):
|
||
|
|
"""Drive llama.cpp through its server's /completion endpoint.
|
||
|
|
|
||
|
|
llama-cli in this build defaults to interactive conversation mode and
|
||
|
|
refuses Spark's chat template without --jinja, so it never emits a plain
|
||
|
|
completion. The server takes the prompt as raw text, which is what a
|
||
|
|
like-for-like comparison needs, and loads the model once instead of once
|
||
|
|
per prompt.
|
||
|
|
"""
|
||
|
|
import json as _json
|
||
|
|
import urllib.request
|
||
|
|
|
||
|
|
log = open("/tmp/llama_server_quality.log", "w")
|
||
|
|
proc = subprocess.Popen(
|
||
|
|
[binary, "-m", gguf, "-t", str(threads), "-c", "4096",
|
||
|
|
"--port", str(port), "--host", "127.0.0.1", "-np", "1"],
|
||
|
|
stdout=log, stderr=subprocess.STDOUT,
|
||
|
|
)
|
||
|
|
try:
|
||
|
|
base = f"http://127.0.0.1:{port}"
|
||
|
|
for _ in range(600):
|
||
|
|
try:
|
||
|
|
urllib.request.urlopen(base + "/health", timeout=2).read()
|
||
|
|
break
|
||
|
|
except Exception:
|
||
|
|
if proc.poll() is not None:
|
||
|
|
raise RuntimeError("llama-server exited; see /tmp/llama_server_quality.log")
|
||
|
|
time.sleep(1)
|
||
|
|
else:
|
||
|
|
raise RuntimeError("llama-server did not become healthy")
|
||
|
|
|
||
|
|
res = {}
|
||
|
|
for name, text in prompts():
|
||
|
|
body = _json.dumps({
|
||
|
|
"prompt": text, "n_predict": n_predict,
|
||
|
|
"temperature": 0.0, "top_k": 1, "seed": 0,
|
||
|
|
"cache_prompt": False,
|
||
|
|
}).encode()
|
||
|
|
req = urllib.request.Request(
|
||
|
|
base + "/completion", data=body,
|
||
|
|
headers={"Content-Type": "application/json"})
|
||
|
|
out = _json.loads(urllib.request.urlopen(req, timeout=1800).read())
|
||
|
|
res[name] = {"text": out["content"], "token_ids": None}
|
||
|
|
return res
|
||
|
|
finally:
|
||
|
|
proc.terminate()
|
||
|
|
try:
|
||
|
|
proc.wait(timeout=30)
|
||
|
|
except subprocess.TimeoutExpired:
|
||
|
|
proc.kill()
|
||
|
|
log.close()
|
||
|
|
|
||
|
|
|
||
|
|
def common_prefix(a, b):
|
||
|
|
n = 0
|
||
|
|
for x, y in zip(a, b):
|
||
|
|
if x != y:
|
||
|
|
break
|
||
|
|
n += 1
|
||
|
|
return n
|
||
|
|
|
||
|
|
|
||
|
|
def compare(ref, got, label):
|
||
|
|
rows, exact, tot_pref, tot_len = [], 0, 0, 0
|
||
|
|
for name in ref:
|
||
|
|
r, g = ref[name]["text"], got[name]["text"]
|
||
|
|
cp = common_prefix(r, g)
|
||
|
|
exact += int(r == g)
|
||
|
|
tot_pref += cp
|
||
|
|
tot_len += max(len(r), 1)
|
||
|
|
rows.append({"prompt": name, "exact": r == g,
|
||
|
|
"common_prefix_chars": cp, "ref_chars": len(r)})
|
||
|
|
return {"label": label, "prompts": len(ref),
|
||
|
|
"exact_match": f"{exact}/{len(ref)}",
|
||
|
|
"mean_prefix_kept": round(tot_pref / max(tot_len, 1), 4),
|
||
|
|
"detail": rows}
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
||
|
|
g = sub.add_parser("gen")
|
||
|
|
g.add_argument("--engine", choices=["vllm", "llamacpp"], required=True)
|
||
|
|
g.add_argument("--model", default=None)
|
||
|
|
g.add_argument("--gguf", default=None)
|
||
|
|
g.add_argument("--binary",
|
||
|
|
default="/data/zhoutaichang/embedding_infer/llama.cpp/build/bin/llama-server")
|
||
|
|
g.add_argument("--threads", type=int, default=32)
|
||
|
|
g.add_argument("--n-predict", type=int, default=64)
|
||
|
|
g.add_argument("--out", required=True)
|
||
|
|
c = sub.add_parser("cmp")
|
||
|
|
c.add_argument("--ref", required=True)
|
||
|
|
c.add_argument("--got", required=True)
|
||
|
|
c.add_argument("--label", required=True)
|
||
|
|
c.add_argument("--out", required=True)
|
||
|
|
a = ap.parse_args()
|
||
|
|
|
||
|
|
if a.cmd == "gen":
|
||
|
|
res = (run_vllm(a.model, a.n_predict) if a.engine == "vllm"
|
||
|
|
else run_llamacpp(a.binary, a.gguf, a.n_predict, a.threads))
|
||
|
|
Path(a.out).write_text(json.dumps(res, indent=2))
|
||
|
|
print(f"wrote {a.out} ({len(res)} prompts)")
|
||
|
|
else:
|
||
|
|
rep = compare(json.loads(Path(a.ref).read_text()),
|
||
|
|
json.loads(Path(a.got).read_text()), a.label)
|
||
|
|
Path(a.out).write_text(json.dumps(rep, indent=2))
|
||
|
|
print(json.dumps({k: v for k, v in rep.items() if k != "detail"}, indent=2))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|