Spaces:
Sleeping
Sleeping
Download scripts/bench_overhead.py from spitfire4794/test1111111: direct link, hf CLI and curl.
- Browser
- Download file 26 kB
-
https://huggingface.co/spaces/spitfire4794/test1111111/resolve/main/scripts/bench_overhead.py
- Command line
-
hf download hf://spaces/spitfire4794/test1111111/scripts/bench_overhead.py
-
curl -L -o bench_overhead.py https://huggingface.co/spaces/spitfire4794/test1111111/resolve/main/scripts/bench_overhead.py
26 kB
| """Bench Python harness overhead vs native decode (read-only measurement). | |
| Owns nothing but this file. Reads Engine/TUI/server shapes only; never | |
| modifies other files, never rebuilds native. Prefers the fake/tiny path so | |
| it runs in seconds: | |
| * FakeNative + FakeTokenizer (tests/test_engine.py make_engine pattern): | |
| zero-cost "native" isolates pure-Python loop overhead, compared against the | |
| production reference of ~200 tok/s (~5000 us/token). | |
| * Tiny real native model (test_native.tiny_model shape, hidden=24/2 layers) | |
| when cism._native + numpy are available: measures a real next_tokens-only | |
| baseline on the same box. Tiny != production model; the production ratio | |
| is still computed against the 5000 us/token reference. | |
| Sections: | |
| 1. Engine.generate(64) total vs native-only vs decode vs machinery, | |
| plus chunk_size=1 vs 16 amortisation. | |
| 2. Tokenizer scaling: single decode(n) vs incremental full-redecode cost. | |
| 3. TUI run() with an instant FakeEngine (tests/test_tui.py pattern). | |
| 4. Server TestClient timing if fastapi is importable, else code-only. | |
| 5. Verdict (a)/(b)/(c) with a >10% threshold for any C++ work. | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import statistics | |
| import sys | |
| import threading | |
| import time | |
| REPO_ROOT = __file__.rsplit("scripts", 1)[0] if "scripts" in __file__ else "." | |
| for _p in (REPO_ROOT + "/src", REPO_ROOT + "src", "G:/CISM/src", "G:\\CISM\\src"): | |
| if _p not in sys.path: | |
| try: | |
| sys.path.insert(0, _p) | |
| except Exception: | |
| pass | |
| from cism.engine import Engine # noqa: E402 | |
| try: | |
| from cism import tui # noqa: E402 | |
| except Exception: # pragma: no cover | |
| tui = None | |
| REFERENCE_US_PER_TOKEN = 5000.0 # ~200 tok/s production native decode | |
| NTOK = 64 | |
| # ---------------------------------------------------------------- fakes | |
| class FakeTokenizer: | |
| bos_token_id = 0 | |
| eos_token_id = None | |
| chat_template = "test template" | |
| def __init__(self, pieces=None, encoded=None, special=()): | |
| self.pieces = pieces or {} | |
| self.encoded = [1, 2] if encoded is None else encoded | |
| self.special = set(special) | |
| def encode(self, text, *, add_special_tokens): | |
| return list(self.encoded) | |
| def decode(self, tokens, *, skip_special_tokens, clean_up_tokenization_spaces): | |
| assert skip_special_tokens is True | |
| assert clean_up_tokenization_spaces is False | |
| return b"".join( | |
| self.pieces.get(t, b"x") for t in tokens if t not in self.special | |
| ).decode("utf-8", errors="replace") | |
| def apply_chat_template(self, messages, *, tokenize, add_generation_prompt): | |
| return list(self.encoded) | |
| class FakeSession: | |
| def __init__(self, output, options): | |
| self.output = list(output) | |
| self.options = options | |
| self.generated_tokens = 0 | |
| self.finish_reason = "length" if options["max_new_tokens"] == 0 else "" | |
| self.cancelled = threading.Event() | |
| self.requests = [] | |
| def next_tokens(self, count, spec_k=0): | |
| self.requests.append((count, spec_k)) | |
| batch = [] | |
| for _ in range(count): | |
| if self.finish_reason: | |
| break | |
| token = self.output[self.generated_tokens] | |
| self.generated_tokens += 1 | |
| batch.append(token) | |
| if token in self.options["eos_token_ids"]: | |
| self.finish_reason = "stop" | |
| elif self.generated_tokens == self.options["max_new_tokens"]: | |
| self.finish_reason = "length" | |
| return batch | |
| def cancel(self): | |
| self.cancelled.set() | |
| if not self.finish_reason: | |
| self.finish_reason = "cancelled" | |
| class FakeNative: | |
| info = {"precision": "fp32", "weight_bytes": 123, "threads": 1} | |
| def __init__(self, output): | |
| self.output = output | |
| self.sessions = [] | |
| def create_session(self, prompt, **options): | |
| session = FakeSession(self.output, options) | |
| self.sessions.append(session) | |
| return session | |
| def make_fake_engine(ntok=NTOK): | |
| output = tuple(3 + (i % 60) for i in range(ntok)) # ids in [3, 62] | |
| pieces = {tok: b"a" for tok in set(output)} | |
| native = FakeNative(output) | |
| tok = FakeTokenizer(pieces) | |
| engine = Engine(native, tok, { | |
| "model_type": "llama", "vocab_size": 64, "max_position_embeddings": 4096, | |
| }, "fake-model", revision="pinned") | |
| return engine, native | |
| class TuiFakeEngine: | |
| """Instant engine mirroring tests/test_tui.py FakeEngine (3 tok/prompt).""" | |
| def __init__(self, n_prompts=0): | |
| self.model_id = "fake-model" | |
| self.precision = "int8" | |
| self.threads = 2 | |
| self.weight_bytes = 4 * 1024 * 1024 | |
| self.context_length = 256 | |
| self.pieces = {3: "Hello", 4: " world", 5: "!"} | |
| self.order = [3, 4, 5] | |
| self.engine_time = 0.0 | |
| def info(self): | |
| return {"model": self.model_id, "precision": self.precision, | |
| "threads": self.threads, "weight_bytes": self.weight_bytes, | |
| "context_length": self.context_length} | |
| def stream(self, prompt, *, max_new_tokens=128, temperature=0.0, top_p=1.0, | |
| top_k=0, seed=0, stop=None, cancel=None, spec_k=0, **kw): | |
| from cism.engine import GenerationChunk | |
| t0 = time.perf_counter() | |
| try: | |
| if cancel is not None and cancel.is_set(): | |
| return | |
| n = min(max_new_tokens, len(self.order)) | |
| for i in range(n): | |
| if cancel is not None and cancel.is_set(): | |
| return | |
| tok = self.order[i] | |
| yield GenerationChunk(self.pieces[tok], [tok], | |
| "length" if i == n - 1 else None, 2, i + 1, 0, 0) | |
| finally: | |
| self.engine_time += time.perf_counter() - t0 | |
| def generate(self, prompt, **kw): | |
| from cism.engine import GenerationResult | |
| texts, ids, last = [], [], None | |
| for chunk in self.stream(prompt, **kw): | |
| texts.append(chunk.text) | |
| ids.extend(chunk.token_ids) | |
| last = chunk | |
| return GenerationResult("".join(texts), ids, last.finish_reason, | |
| last.prompt_tokens, last.completion_tokens, 0, 0) | |
| def nll(self, prompt, window=128): | |
| import math | |
| return {"nll": 0.5, "perplexity": math.exp(0.5), "scored_tokens": 2} | |
| # ---------------------------------------------------------------- helpers | |
| def med(fn, repeats=21, warmup=3): | |
| for _ in range(warmup): | |
| fn() | |
| samples = [fn() for _ in range(repeats)] | |
| return statistics.median(samples), samples | |
| def timed(fn): | |
| t0 = time.perf_counter() | |
| out = fn() | |
| return time.perf_counter() - t0, out | |
| def fmt_us(s): | |
| return f"{s * 1e6:9.1f} us" | |
| def build_tiny_engine(): | |
| """Tiny real native engine (test_native.tiny_model shape) or raise Skip.""" | |
| import numpy as np | |
| try: | |
| from cism import _native # noqa: F401 | |
| except Exception as exc: | |
| raise RuntimeError(f"no native extension ({exc})") | |
| rng = np.random.default_rng(312) | |
| hidden, inter, vocab = 24, 37, 43 | |
| config = dict(model_type="llama", hidden_size=hidden, | |
| intermediate_size=inter, num_hidden_layers=2, | |
| num_attention_heads=4, num_key_value_heads=2, head_dim=8, | |
| vocab_size=vocab, max_position_embeddings=80, | |
| rms_norm_eps=1e-5, rope_theta=10000.0, | |
| tie_word_embeddings=False, hidden_act="silu") | |
| def mat(shape): | |
| return rng.normal(0, 0.14, shape).astype(np.float32) | |
| def norm(w): | |
| return rng.uniform(0.75, 1.25, w).astype(np.float32) | |
| weights = {"model.embed_tokens.weight": mat((vocab, hidden)), | |
| "lm_head.weight": mat((vocab, hidden)), | |
| "model.norm.weight": norm(hidden)} | |
| for i in range(2): | |
| p = f"model.layers.{i}." | |
| weights[p + "input_layernorm.weight"] = norm(hidden) | |
| weights[p + "post_attention_layernorm.weight"] = norm(hidden) | |
| for name, shape in (("q_proj", (32, hidden)), ("k_proj", (16, hidden)), | |
| ("v_proj", (16, hidden)), ("o_proj", (hidden, 32))): | |
| weights[p + f"self_attn.{name}.weight"] = mat(shape) | |
| for name, shape in (("gate_proj", (inter, hidden)), | |
| ("up_proj", (inter, hidden)), | |
| ("down_proj", (hidden, inter))): | |
| weights[p + f"mlp.{name}.weight"] = mat(shape) | |
| pieces = {i: bytes([97 + (i % 26)]) for i in range(vocab)} | |
| engine = Engine.from_weights(config, weights, FakeTokenizer(pieces), | |
| model_id="tiny-llama", revision="bench") | |
| return engine | |
| # ---------------------------------------------------------------- 1: loop | |
| def section1(): | |
| print("== [1] Engine loop overhead vs native (fake path: pure-Python cost) ==") | |
| engine, _ = make_fake_engine(NTOK) | |
| def gen16(): | |
| return engine.generate([1, 2], max_new_tokens=NTOK, chunk_size=16) | |
| t_med, _ = med(lambda: timed(gen16)[0], repeats=21, warmup=3) | |
| print(f"Engine.generate({NTOK} tok, chunk=16): median {t_med * 1e3:.3f} ms " | |
| f"total = {t_med / NTOK * 1e6:.1f} us/token (fake native ~= 0 cost)") | |
| # Instrumented breakdown: wrap decode (Python obj, wrappable) + wrap | |
| # sessions at create_session (works for FakeSession; real pybind11 | |
| # sessions have no __dict__, so the real path uses a direct loop below). | |
| state = {"t_next": 0.0, "t_decode": 0.0, "n_next": 0, "n_decode": 0} | |
| orig_create = engine._native.create_session | |
| orig_decode = engine.tokenizer.decode | |
| def counting_create(prompt, **opts): | |
| sess = orig_create(prompt, **opts) | |
| orig_next = sess.next_tokens | |
| def timed_next(count, spec_k=0): | |
| t0 = time.perf_counter() | |
| try: | |
| return orig_next(count, spec_k=spec_k) | |
| finally: | |
| state["t_next"] += time.perf_counter() - t0 | |
| state["n_next"] += 1 | |
| sess.next_tokens = timed_next | |
| return sess | |
| def timed_decode(tokens, **kw): | |
| t0 = time.perf_counter() | |
| try: | |
| return orig_decode(tokens, **kw) | |
| finally: | |
| state["t_decode"] += time.perf_counter() - t0 | |
| state["n_decode"] += 1 | |
| engine._native.create_session = counting_create | |
| engine.tokenizer.decode = timed_decode | |
| # warmup then one measured generation (counters reset for the measure) | |
| engine.generate([1, 2], max_new_tokens=NTOK, chunk_size=16) | |
| state.update(t_next=0.0, t_decode=0.0, n_next=0, n_decode=0) | |
| t0 = time.perf_counter() | |
| engine.generate([1, 2], max_new_tokens=NTOK, chunk_size=16) | |
| t_total = time.perf_counter() - t0 | |
| engine._native.create_session = orig_create | |
| engine.tokenizer.decode = orig_decode | |
| t_other = t_total - state["t_next"] - state["t_decode"] | |
| per = lambda s: s / NTOK * 1e6 | |
| print(f" breakdown of one {NTOK}-tok generate ({t_total * 1e3:.3f} ms):") | |
| print(f" native next_tokens : {state['t_next'] * 1e6:9.1f} us total " | |
| f"({per(state['t_next']):7.1f} us/tok, {state['n_next']} calls, " | |
| f"{state['t_next'] / max(1, state['n_next']) * 1e6:.1f} us/call)") | |
| print(f" tokenizer.decode : {state['t_decode'] * 1e6:9.1f} us total " | |
| f"({per(state['t_decode']):7.1f} us/tok, {state['n_decode']} calls)") | |
| print(f" chunk/yield/lock : {t_other * 1e6:9.1f} us total " | |
| f"({per(t_other):7.1f} us/tok) [GenerationChunk allocs, stop-check," | |
| f" cancel-monitor thread start/join, gen lock]") | |
| print(f" python overhead/token ~= {t_total / NTOK * 1e6:.1f} us vs " | |
| f"reference native {REFERENCE_US_PER_TOKEN:.0f} us " | |
| f"({t_total / NTOK * 1e6 / REFERENCE_US_PER_TOKEN * 100:.2f}%)") | |
| # chunk_size amortisation on the fake path | |
| for cs in (1, 16): | |
| e, _ = make_fake_engine(NTOK) | |
| t, _ = med(lambda: timed( | |
| lambda: e.generate([1, 2], max_new_tokens=NTOK, chunk_size=cs))[0], | |
| repeats=15, warmup=2) | |
| sess = e._native.sessions[-1] | |
| print(f" chunk_size={cs:2d}: median {t * 1e3:7.3f} ms " | |
| f"({t / NTOK * 1e6:6.1f} us/tok), native calls={len(sess.requests)}") | |
| # cancel-monitor thread cost (per-generate constant, amortised per token) | |
| def spawn_join(): | |
| ev = threading.Event() | |
| th = threading.Thread(target=ev.wait, kwargs={"timeout": 0.0}) | |
| th.start() | |
| th.join() | |
| t_thr, _ = med(lambda: timed(spawn_join)[0], repeats=100, warmup=10) | |
| print(f" thread spawn+join microbench: ~{t_thr * 1e6:.1f} us/generate " | |
| f"(~{t_thr / NTOK * 1e6:.2f} us/tok at {NTOK} tok) -> negligible") | |
| # GenerationChunk alloc microbench | |
| from cism.engine import GenerationChunk | |
| def allocs(): | |
| for _ in range(1000): | |
| GenerationChunk("a", [3], None, 2, 1, 0, 0) | |
| t_al, _ = med(lambda: timed(allocs)[0], repeats=21, warmup=3) | |
| print(f" GenerationChunk alloc: ~{t_al / 1000 * 1e9:.0f} ns/chunk") | |
| # tiny real-native baseline (same box, NOT the production model) | |
| tiny = None | |
| try: | |
| tiny = build_tiny_engine() | |
| except Exception as exc: | |
| print(f" tiny real model: SKIPPED ({exc})") | |
| if tiny is not None: | |
| tn = 32 # 5 prompt + 32 < 80 context | |
| prompt = [2, 7, 1] | |
| t_eng, _ = med(lambda: timed( | |
| lambda: tiny.generate(prompt, max_new_tokens=tn, chunk_size=16))[0], | |
| repeats=5, warmup=1) | |
| def native_only(): | |
| s = tiny._native.create_session(prompt, max_new_tokens=tn, | |
| temperature=0.0, top_p=1.0, | |
| top_k=0, seed=0, eos_token_ids=[]) | |
| n = 0 | |
| while not s.finish_reason: | |
| n += len(s.next_tokens(16)) | |
| return n | |
| t_nat, _ = med(lambda: timed(native_only)[0], repeats=5, warmup=1) | |
| # decode-only share inside Engine.generate on the tiny path | |
| st = {"d": 0.0} | |
| od = tiny.tokenizer.decode | |
| def td(tokens, **kw): | |
| t0 = time.perf_counter() | |
| try: | |
| return od(tokens, **kw) | |
| finally: | |
| st["d"] += time.perf_counter() - t0 | |
| tiny.tokenizer.decode = td | |
| tiny.generate(prompt, max_new_tokens=tn, chunk_size=16) | |
| st["d"] = 0.0 | |
| tiny.generate(prompt, max_new_tokens=tn, chunk_size=16) | |
| tiny.tokenizer.decode = od | |
| ov = (t_eng - t_nat) / tn * 1e6 | |
| print(f" tiny real model ({tn} tok): engine {t_eng * 1e3:.2f} ms " | |
| f"({t_eng / tn * 1e6:.0f} us/tok), native-only {t_nat * 1e3:.2f} ms " | |
| f"({t_nat / tn * 1e6:.0f} us/tok)") | |
| print(f" tiny overhead ~= {ov:.1f} us/tok " | |
| f"(decode {st['d'] / tn * 1e6:.1f} us/tok); " | |
| f"vs PROD ref {REFERENCE_US_PER_TOKEN:.0f} us/tok = " | |
| f"{ov / REFERENCE_US_PER_TOKEN * 100:.2f}%") | |
| print() | |
| return {"fake_us_per_tok": t_total / NTOK * 1e6} | |
| # ---------------------------------------------------------------- 2: decode | |
| def section2(): | |
| print("== [2] Tokenizer scaling: full-redecode per chunk (O(n^2/chunk)) ==") | |
| tok = FakeTokenizer({i: b"a" for i in range(512)}) | |
| ids512 = list(range(512)) | |
| for n in (64, 128, 256, 512): | |
| ids = ids512[:n] | |
| t, _ = med(lambda: timed( | |
| lambda: tok.decode(ids, skip_special_tokens=True, | |
| clean_up_tokenization_spaces=False))[0], | |
| repeats=200, warmup=20) | |
| print(f" single decode({n:3d}): {t * 1e6:8.1f} us") | |
| # incremental totals as Engine.stream does (decode whole prefix per chunk) | |
| for cs in (1, 16): | |
| lens = list(range(cs, 513, cs)) | |
| if lens[-1] != 512: | |
| lens.append(512) | |
| def incr(): | |
| for k in lens: | |
| tok.decode(ids512[:k], skip_special_tokens=True, | |
| clean_up_tokenization_spaces=False) | |
| t, _ = med(lambda: timed(incr)[0], repeats=5, warmup=1) | |
| single512, _ = med(lambda: timed( | |
| lambda: tok.decode(ids512, skip_special_tokens=True, | |
| clean_up_tokenization_spaces=False))[0], | |
| repeats=200, warmup=20) | |
| print(f" incremental 512 tok, chunk={cs:2d}: {len(lens)} decodes, " | |
| f"total {t * 1e3:.2f} ms ({t / 512 * 1e6:.1f} us/tok) vs single " | |
| f"decode(512) {single512 * 1e6:.1f} us " | |
| f"(ratio {t / single512:.0f}x)") | |
| # what incremental-decode (delta only) would cost instead | |
| t_delta, _ = med(lambda: timed( | |
| lambda: tok.decode(ids512[:16], skip_special_tokens=True, | |
| clean_up_tokenization_spaces=False))[0], | |
| repeats=200, warmup=20) | |
| est = t_delta * (512 / 16) | |
| print(f" delta-only estimate (decode 16 new ids x32): ~{est * 1e3:.2f} ms " | |
| f"-> saves the quadratic re-scan") | |
| print() | |
| # ---------------------------------------------------------------- 3: TUI | |
| def section3(): | |
| print("== [3] TUI path overhead (instant FakeEngine => harness only) ==") | |
| if tui is None: | |
| print(" SKIPPED: cism.tui not importable") | |
| print() | |
| return {"tui_us_per_tok": float("nan")} | |
| # direct engine baseline (no TUI) | |
| eng0 = TuiFakeEngine() | |
| t_dir, _ = med(lambda: timed( | |
| lambda: list(eng0.stream("hello", max_new_tokens=3)))[0], | |
| repeats=2000, warmup=200) | |
| print(f" direct eng.stream(3 tok): {t_dir * 1e6:.1f} us " | |
| f"({t_dir / 3 * 1e6:.1f} us/tok) [engine-already-instant baseline]") | |
| # TUI loop over many prompts in one run() | |
| N_PROMPTS, TOK_PER = 50, 3 | |
| lines = ["hello"] * N_PROMPTS + ["/quit"] | |
| def tui_run(): | |
| eng = TuiFakeEngine() | |
| out, err = io.StringIO(), io.StringIO() | |
| t0 = time.perf_counter() | |
| code = tui.run("fake-model", engine=eng, | |
| stdin=io.StringIO("\n".join(lines) + "\n"), | |
| stdout=out, stderr=err) | |
| dt = time.perf_counter() - t0 | |
| assert code == 0 | |
| return dt, eng.engine_time | |
| _, _ = tui_run(), None # warmup (imports/regex compile) | |
| dts, eng_ts = [], [] | |
| for _ in range(5): | |
| dt, et = tui_run() | |
| dts.append(dt) | |
| eng_ts.append(et) | |
| dtm, etm = statistics.median(dts), statistics.median(eng_ts) | |
| total_tok = N_PROMPTS * TOK_PER | |
| harness = dtm - etm | |
| print(f" tui.run({N_PROMPTS} prompts x {TOK_PER} tok): median {dtm * 1e3:.2f} ms " | |
| f"total; engine-inside {etm * 1e3:.2f} ms") | |
| print(f" TUI harness overhead: {harness * 1e6:.0f} us / {total_tok} tok = " | |
| f"{harness / total_tok * 1e6:.1f} us/tok " | |
| f"({harness / total_tok * 1e6 / REFERENCE_US_PER_TOKEN * 100:.3f}% of " | |
| f"{REFERENCE_US_PER_TOKEN:.0f} us ref token)") | |
| print(" (tui.py run(): direct eng.stream() call per prompt, per-chunk " | |
| "out.write+flush, counters only; no HTTP/JSON.)") | |
| print() | |
| return {"tui_us_per_tok": harness / total_tok * 1e6} | |
| # ---------------------------------------------------------------- 4: server | |
| def section4(tui_us): | |
| print("== [4] Server path overhead (code + optional live TestClient) ==") | |
| print(" code path (src/cism/server.py, read-only): _Worker.submit puts a") | |
| print(" _Job on queue.Queue(maxsize=8); inference thread runs the SAME") | |
| print(" engine.stream(chunk_size=16) as the TUI; each chunk crosses the") | |
| print(" queue + asyncio.Event notify (worker._put/_run/receive), then one") | |
| print(" json.dumps per SSE frame (or one for non-stream) + pydantic") | |
| print(" _CompletionRequest validation. Expect ~10s of us extra vs TUI.") | |
| result = {"server_extra_us_per_tok": float("nan"), "live": False} | |
| try: | |
| from fastapi.testclient import TestClient | |
| from cism.engine import GenerationChunk as Chunk | |
| from cism.server import create_app | |
| except Exception as exc: | |
| print(f" live TestClient: SKIPPED ({exc})") | |
| print() | |
| return result | |
| class SrvFake: | |
| model_id = "bench-model" | |
| context_length = 512 | |
| def __init__(self, ntok=8): | |
| self.ntok = ntok | |
| def encode(self, prompt): | |
| return list(prompt) if isinstance(prompt, list) else [1, 2] | |
| def stream(self, prompt, **kw): | |
| n = min(kw.get("max_new_tokens", 8), self.ntok) | |
| for i in range(n): | |
| yield Chunk("a", [3 + (i % 40)], "length" if i == n - 1 else None, | |
| 2, i + 1, 0, 0) | |
| NTOK_S, REPS = 8, 10 | |
| try: | |
| import os | |
| os.environ.pop("CISM_API_KEY", None) | |
| app = create_app(SrvFake(NTOK_S), api_key="") | |
| with TestClient(app) as client: | |
| assert client.get("/health").json() == {"status": "ok"} | |
| body = {"model": "bench-model", "prompt": [1, 2], | |
| "max_tokens": NTOK_S} | |
| client.post("/v1/completions", json=body) # warmup | |
| t0 = time.perf_counter() | |
| for _ in range(REPS): | |
| r = client.post("/v1/completions", json=body) | |
| assert r.status_code == 200, r.text | |
| t_srv = (time.perf_counter() - t0) / REPS | |
| # streaming sample (one request) | |
| t0 = time.perf_counter() | |
| r = client.post("/v1/completions", json={**body, "stream": True}) | |
| assert r.status_code == 200, r.text | |
| t_stream = time.perf_counter() - t0 | |
| eng = SrvFake(NTOK_S) | |
| t0 = time.perf_counter() | |
| for _ in range(200): | |
| list(eng.stream([1, 2], max_new_tokens=NTOK_S)) | |
| t_dir = (time.perf_counter() - t0) / 200 | |
| extra = (t_srv - t_dir) / NTOK_S * 1e6 | |
| result = {"server_extra_us_per_tok": extra, "live": True} | |
| print(f" live TestClient non-stream: {t_srv * 1e3:.2f} ms/req " | |
| f"({t_srv / NTOK_S * 1e6:.0f} us/tok); direct stream: " | |
| f"{t_dir * 1e3:.3f} ms ({t_dir / NTOK_S * 1e6:.1f} us/tok)") | |
| print(f" server extra vs direct: ~{extra:.0f} us/tok " | |
| f"({extra / REFERENCE_US_PER_TOKEN * 100:.2f}% of ref token); " | |
| f"stream req sample {t_stream * 1e3:.1f} ms") | |
| print(" (in-process TestClient understates real TCP+uvicorn by " | |
| "~0.2-1 ms/req; per-token delta stays << native token time.)") | |
| except Exception as exc: | |
| print(f" live TestClient: SKIPPED ({type(exc).__name__}: {exc})") | |
| print() | |
| return result | |
| # ---------------------------------------------------------------- 5: verdict | |
| def section5(s1, tui_us, srv): | |
| print("== [5] Verdict: is a C++ inference API worth it? ==") | |
| overhead = s1["fake_us_per_tok"] | |
| ratio = overhead / REFERENCE_US_PER_TOKEN | |
| tui_r = (tui_us / REFERENCE_US_PER_TOKEN) if tui_us == tui_us else float("nan") | |
| print(f" Python loop overhead : {overhead:7.1f} us/tok " | |
| f"({ratio * 100:5.2f}% of {REFERENCE_US_PER_TOKEN:.0f} us ref token)") | |
| print(f" TUI harness overhead : {tui_us:7.1f} us/tok " | |
| f"({tui_r * 100:5.3f}% of ref token)") | |
| if srv["live"]: | |
| e = srv["server_extra_us_per_tok"] | |
| print(f" Server extra (live) : {e:7.1f} us/tok " | |
| f"({e / REFERENCE_US_PER_TOKEN * 100:5.2f}% of ref token)") | |
| else: | |
| print(" Server extra (live) : skipped (code-only: queue+JSON ~10s us)") | |
| print(" Threshold: C++ work only if harness overhead >10% of token time.") | |
| if ratio < 0.05 and (tui_r != tui_r or tui_r < 0.05): | |
| choice = ("(a) NOT worth it -- keep the Python Engine.stream boundary. " | |
| "Overhead is <5% of a batch-1 decode step.") | |
| elif ratio >= 0.10 or (tui_r == tui_r and tui_r >= 0.10): | |
| choice = ("(b)/(c) threshold tripped -- re-measure on the production " | |
| "model before committing to C++.") | |
| else: | |
| choice = ("(b) at most a native generate_text helper (incremental " | |
| "decode + stop-check inside one call) -- not a full C++ API.") | |
| # With the measured sub-5% numbers the evidence points to (a); the | |
| # conditional above encodes the rule, this line records the outcome. | |
| print(f" RECOMMENDATION: {choice}") | |
| if ratio < 0.05: | |
| print(" Chosen: (a). A full C++ inference API / C++ CLI buys <5% at") | |
| print(" batch-1 decode and adds ABI/build/debug cost for zero user-") | |
| print(" visible tok/s. The TUI already has no high harness overhead") | |
| print(" (direct eng.stream calls, ~10s us/tok).") | |
| print(" What WOULD help most instead (in order):") | |
| print(" 1. Incremental decode: decode only the new delta per chunk") | |
| print(" (carry pending UTF-8 bytes) instead of decode(all ids) --") | |
| print(" kills the O(n^2/chunk) re-scan from [2].") | |
| print(" 2. Keep chunk_size=16 whenever no stop strings are set (stops") | |
| print(" force token-at-a-time in engine.py); amortises one pybind11") | |
| print(" next_tokens call over 16 tokens -- see [1] cs=1 vs 16.") | |
| print(" 3. Keep the TUI on direct Engine calls (no HTTP/JSON hop) and") | |
| print(" batch terminal writes; the server path stays opt-in.") | |
| print() | |
| return choice | |
| def main(): | |
| print("bench_overhead.py -- Python harness overhead vs native decode") | |
| print(f"reference native: ~200 tok/s = {REFERENCE_US_PER_TOKEN:.0f} us/token; " | |
| f"NTOK={NTOK}") | |
| try: | |
| import cism._native as n # noqa: F401 | |
| print("native extension: present (tiny-model section enabled if numpy ok)") | |
| except Exception as exc: | |
| print(f"native extension: MISSING ({exc}); fake-path only") | |
| print() | |
| s1 = section1() | |
| section2() | |
| s3 = section3() | |
| srv = section4(s3["tui_us_per_tok"]) | |
| choice = section5(s1, s3["tui_us_per_tok"], srv) | |
| print(f"VERDICT: {choice}") | |
| if __name__ == "__main__": | |
| main() | |