chad-code 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.
- chad/__init__.py +7 -0
- chad/agent.py +1093 -0
- chad/base_engine.py +109 -0
- chad/bench.py +243 -0
- chad/cli.py +469 -0
- chad/compaction.py +108 -0
- chad/config.py +63 -0
- chad/diag.py +100 -0
- chad/engine.py +883 -0
- chad/guardrails.py +373 -0
- chad/ignore.py +18 -0
- chad/lsp.py +323 -0
- chad/mcp.py +719 -0
- chad/mcp_oauth.py +329 -0
- chad/openai_engine.py +252 -0
- chad/prompt.py +305 -0
- chad/render.py +462 -0
- chad/repomap.py +767 -0
- chad/session.py +281 -0
- chad/skills.py +407 -0
- chad/symbols.py +360 -0
- chad/syntaxgate.py +105 -0
- chad/toolcall_parse.py +115 -0
- chad/tools.py +962 -0
- chad/tui.py +921 -0
- chad/validate.py +361 -0
- chad_code-0.1.0.dist-info/METADATA +370 -0
- chad_code-0.1.0.dist-info/RECORD +32 -0
- chad_code-0.1.0.dist-info/WHEEL +5 -0
- chad_code-0.1.0.dist-info/entry_points.txt +4 -0
- chad_code-0.1.0.dist-info/licenses/LICENSE +21 -0
- chad_code-0.1.0.dist-info/top_level.txt +1 -0
chad/base_engine.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""The engine seam: `GenStats` + the `BaseEngine` protocol (plan 046).
|
|
2
|
+
|
|
3
|
+
`BaseEngine` is the *de facto* interface `Agent` already drives on `Engine` — extracted
|
|
4
|
+
verbatim into a `typing.Protocol` so a second backend can implement it without touching
|
|
5
|
+
the agent loop, and so mypy enforces the boundary. It **documents the seam that exists**;
|
|
6
|
+
it does not redesign it. `Engine` (the MLX crown jewel in `engine.py`) satisfies it
|
|
7
|
+
unchanged, and `OpenAIEngine` (`openai_engine.py`) is the spike adapter that proves a
|
|
8
|
+
different backend can plug into the same slot.
|
|
9
|
+
|
|
10
|
+
Why a Protocol (not an ABC): zero runtime cost, structural (no base-class edit to
|
|
11
|
+
`Engine`), and it keeps `GenStats` — a pure dataclass with no MLX dependency — importable
|
|
12
|
+
by a backend that never loads mlx. `GenStats` lives here (not `engine.py`) precisely so
|
|
13
|
+
the OpenAI adapter can build one without dragging in `mlx.core`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Callable, Optional, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class GenStats:
|
|
22
|
+
prompt_tokens: int = 0 # tokens actually prefilled this turn
|
|
23
|
+
cached_tokens: int = 0 # tokens served from the prefix cache
|
|
24
|
+
generated_tokens: int = 0
|
|
25
|
+
prefill_s: float = 0.0
|
|
26
|
+
gen_s: float = 0.0
|
|
27
|
+
forwards: int = 0 # model forward passes (PLD: < generated_tokens)
|
|
28
|
+
draft_proposed: int = 0 # PLD: n-gram tokens proposed
|
|
29
|
+
draft_accepted: int = 0 # PLD: n-gram tokens accepted
|
|
30
|
+
stop_condition_fired: bool = False # generation halted by the caller's stop_condition
|
|
31
|
+
# (plan 039 soft think-cap), not by EOS / max_tokens
|
|
32
|
+
approximate: bool = False # stats are best-effort, not exact (plan 046): an
|
|
33
|
+
# OpenAI-style backend can't report cached_tokens /
|
|
34
|
+
# per-forward accounting, so it sets this and callers
|
|
35
|
+
# know the throughput/prefill numbers are estimates.
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def tok_per_s(self) -> float:
|
|
39
|
+
return self.generated_tokens / self.gen_s if self.gen_s > 0 else 0.0
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def accept_rate(self) -> float:
|
|
43
|
+
return self.draft_accepted / self.draft_proposed if self.draft_proposed else 0.0
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def tokens_per_forward(self) -> float:
|
|
47
|
+
return self.generated_tokens / self.forwards if self.forwards else 0.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@runtime_checkable
|
|
51
|
+
class BaseEngine(Protocol):
|
|
52
|
+
"""The interface `Agent` drives on an engine (plan 046). Every member here is one
|
|
53
|
+
`Agent` (or the TUI/REPL) already touches on `Engine` today — nothing aspirational.
|
|
54
|
+
|
|
55
|
+
The two backends that satisfy it:
|
|
56
|
+
- `engine.Engine` — MLX, in-process, persistent prefix KV cache (the default).
|
|
57
|
+
- `openai_engine.OpenAIEngine` — an OpenAI-compatible `/v1/chat/completions` client.
|
|
58
|
+
|
|
59
|
+
Data members (`tok`, `model_id`, `effective_ctx`, `cache_dir`, `_cached_ids`) are read
|
|
60
|
+
by the agent loop; the methods are the calls it makes each turn. The warm-prefix /
|
|
61
|
+
cache-quarantine members are honored by the MLX engine and no-op'd by a stateless
|
|
62
|
+
backend (see `OpenAIEngine`), which is exactly why they're part of the documented seam
|
|
63
|
+
rather than hidden inside `Engine`.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# --- data members the agent loop reads -------------------------------
|
|
67
|
+
tok: Any # tokenizer: apply_chat_template / decode / detokenizer
|
|
68
|
+
model_id: str
|
|
69
|
+
effective_ctx: int # usable context window (tokens)
|
|
70
|
+
cache_dir: Optional[str] # on-disk KV checkpoint dir; None disables warm-start
|
|
71
|
+
_cached_ids: list # tokens currently resident in the prefix cache
|
|
72
|
+
|
|
73
|
+
# --- generation ------------------------------------------------------
|
|
74
|
+
def generate(
|
|
75
|
+
self,
|
|
76
|
+
prompt_ids: list,
|
|
77
|
+
max_tokens: int = 2048,
|
|
78
|
+
on_token: Optional[Callable[[str], None]] = None,
|
|
79
|
+
stop_texts: Optional[list] = None,
|
|
80
|
+
should_stop: Optional[Callable[[], bool]] = None,
|
|
81
|
+
on_prefill: Optional[Callable[[int, int], None]] = None,
|
|
82
|
+
on_prefill_progress: Optional[Callable[[int, int], None]] = None,
|
|
83
|
+
stop_condition: Optional[Callable[[str, int], bool]] = None,
|
|
84
|
+
) -> tuple[str, GenStats]:
|
|
85
|
+
"""Generate a completion for the already-templated `prompt_ids`, streaming decoded
|
|
86
|
+
text to `on_token`. Returns `(text, GenStats)`. Signature mirrors `Engine.generate`
|
|
87
|
+
exactly so `Engine` satisfies this protocol unchanged."""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
# --- cache lifecycle -------------------------------------------------
|
|
91
|
+
def reset(self) -> None:
|
|
92
|
+
"""Drop any conversational state / prefix cache (public alias of the MLX engine's
|
|
93
|
+
`_reset_cache`). Called on `/reset` and by the governor's fresh-turn relaunch."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
def warm_prefix(self, prefix_ids: list, should_stop: Optional[Callable[[], bool]] = None
|
|
97
|
+
) -> tuple[str, int]:
|
|
98
|
+
"""Warm-start the stable system+tools prefix from disk (MLX) or no-op ('skip', 0)
|
|
99
|
+
on a stateless backend. Returns (status, n_tokens)."""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
def push_cache(self) -> None:
|
|
103
|
+
"""Quarantine the live cache so a sub-agent can run isolated (MLX); no-op on a
|
|
104
|
+
stateless backend, which has no cache to protect."""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
def pop_cache(self) -> None:
|
|
108
|
+
"""Restore the cache stashed by `push_cache` (MLX); no-op on a stateless backend."""
|
|
109
|
+
...
|
chad/bench.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Throughput benchmark — the numbers behind docs/benchmarks.md, reproducible locally.
|
|
2
|
+
|
|
3
|
+
Run it: `uv run chad-bench` (downloads the same model `chad` uses on first run).
|
|
4
|
+
|
|
5
|
+
It drives the *real* `Engine` on the *real* model and reports the three numbers that
|
|
6
|
+
actually decide whether a local agent feels responsive:
|
|
7
|
+
|
|
8
|
+
1. Prefill (cold) — tok/s the model reads a fresh prompt at. The bill in a naive loop.
|
|
9
|
+
2. Decode — tok/s it writes new tokens at. Memory-bandwidth bound, ~constant.
|
|
10
|
+
3. Warm step — the agentic-loop number: how few tokens a *follow-up* turn has to
|
|
11
|
+
prefill once the persistent prefix cache is warm. This is the whole
|
|
12
|
+
point — a real turn pays ~0.1 s of prefill, not the cold-prefill time.
|
|
13
|
+
|
|
14
|
+
Everything here uses only public code and the public model, so the headline numbers in
|
|
15
|
+
the docs are something you can reproduce on your own Mac, not pass-rates from a private
|
|
16
|
+
eval suite.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
# Reuse the exact model selection chad itself uses (RAM-aware default, local-dir-preferred,
|
|
24
|
+
# HF fallback) so the benchmark measures the model you'd actually run.
|
|
25
|
+
from .cli import _ensure_model, _pick_model
|
|
26
|
+
from .engine import Engine
|
|
27
|
+
|
|
28
|
+
# A chunk of plausible code/transcript text. We tile it to the requested token budget so
|
|
29
|
+
# the prefill measurement runs over realistic content (not a single repeated token, which
|
|
30
|
+
# the n-gram path could short-circuit). Decode speed is content-independent (bandwidth).
|
|
31
|
+
_FILLER = '''
|
|
32
|
+
def process(records, *, limit=None, strict=False):
|
|
33
|
+
"""Normalize and validate a batch of records, returning (ok, errors)."""
|
|
34
|
+
ok, errors = [], []
|
|
35
|
+
for i, rec in enumerate(records):
|
|
36
|
+
if limit is not None and len(ok) >= limit:
|
|
37
|
+
break
|
|
38
|
+
try:
|
|
39
|
+
name = rec["name"].strip()
|
|
40
|
+
value = int(rec.get("value", 0))
|
|
41
|
+
except (KeyError, ValueError, AttributeError) as exc:
|
|
42
|
+
errors.append((i, str(exc)))
|
|
43
|
+
if strict:
|
|
44
|
+
raise
|
|
45
|
+
continue
|
|
46
|
+
ok.append({"name": name, "value": value, "index": i})
|
|
47
|
+
return ok, errors
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _build_prompt(tok, target_tokens: int) -> list:
|
|
52
|
+
"""Tile filler text and encode to roughly `target_tokens` token ids."""
|
|
53
|
+
chunk_ids = tok.encode(_FILLER)
|
|
54
|
+
if not chunk_ids:
|
|
55
|
+
chunk_ids = tok.encode("the quick brown fox jumps over the lazy dog ")
|
|
56
|
+
reps = max(1, target_tokens // len(chunk_ids) + 1)
|
|
57
|
+
ids = (chunk_ids * reps)[:target_tokens]
|
|
58
|
+
return ids
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _encode_suffix(tok, text: str) -> list:
|
|
62
|
+
"""Encode a short follow-up turn without a leading BOS (it's mid-sequence)."""
|
|
63
|
+
try:
|
|
64
|
+
return tok.encode(text, add_special_tokens=False)
|
|
65
|
+
except TypeError:
|
|
66
|
+
ids = tok.encode(text)
|
|
67
|
+
# Drop a leading BOS if the tokenizer added one.
|
|
68
|
+
bos = getattr(tok, "bos_token_id", None)
|
|
69
|
+
if bos is not None and ids and ids[0] == bos:
|
|
70
|
+
ids = ids[1:]
|
|
71
|
+
return ids
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _render(tok, messages, thinking=True) -> list:
|
|
75
|
+
"""Render a transcript exactly as Agent._render does (chat template + tool schemas
|
|
76
|
+
+ generation prompt), so the benchmark exercises the same token stream the real
|
|
77
|
+
agentic loop feeds the engine."""
|
|
78
|
+
from .tools import active_schemas
|
|
79
|
+
return tok.apply_chat_template(
|
|
80
|
+
messages, tools=active_schemas(), add_generation_prompt=True,
|
|
81
|
+
enable_thinking=thinking)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _agentic(model_id: str, why: str, context_tokens: int, apply_fix: bool):
|
|
85
|
+
"""Drive the REAL engine through a scripted agentic session that forces a
|
|
86
|
+
truncated (token-cap) turn at large context, and measure the prefill the FOLLOWING
|
|
87
|
+
step pays. This is the cache-miss the logs show: a turn cut off mid-`<think>` leaves
|
|
88
|
+
the stored assistant turn unable to re-tokenize into a prefix of the non-trimmable
|
|
89
|
+
KV cache, so the next step re-prefills the whole transcript (0 cached).
|
|
90
|
+
|
|
91
|
+
With `apply_fix`, the dangling think block is closed before the turn is stored
|
|
92
|
+
(chad.agent.close_unclosed_think) — the same normalization run_turn now applies — so
|
|
93
|
+
the cached tokens stay a prefix and the next step prefills only a handful of tokens.
|
|
94
|
+
|
|
95
|
+
Returns (miss_prefill_tokens, miss_prefill_s, cached_tokens)."""
|
|
96
|
+
from .agent import build_system_prompt, close_unclosed_think
|
|
97
|
+
from .engine import Engine
|
|
98
|
+
|
|
99
|
+
eng = Engine(model_id=model_id, draft_id=None, cache_dir=None)
|
|
100
|
+
eng.load()
|
|
101
|
+
tok = eng.tok
|
|
102
|
+
|
|
103
|
+
# Seed a large, realistic context: system + a user task that pastes a big code blob,
|
|
104
|
+
# padded to roughly `context_tokens` so the re-prefill cost is at the scale the user
|
|
105
|
+
# hit (tens of thousands of tokens).
|
|
106
|
+
blob = (_FILLER * max(1, context_tokens // max(1, len(tok.encode(_FILLER)))))
|
|
107
|
+
messages = [
|
|
108
|
+
{"role": "system", "content": build_system_prompt()},
|
|
109
|
+
{"role": "user", "content": "Review this module and fix the validation bug.\n\n"
|
|
110
|
+
"```python\n" + blob + "\n```"},
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# The render up to the generation prompt: the template auto-opens `<think>\n` here, so
|
|
114
|
+
# the model's turn continues INSIDE the think block.
|
|
115
|
+
p1 = _render(tok, messages)
|
|
116
|
+
# A turn that hit the token cap MID-THINK: reasoning with no closing `</think>` (the
|
|
117
|
+
# `hit_cap=True` truncation the logs flag). Constructed deterministically rather than
|
|
118
|
+
# generated, so the benchmark reliably exercises the divergence (a short stochastic
|
|
119
|
+
# generation may close `</think>` within the cap and hide it).
|
|
120
|
+
# Ends on punctuation (not bare whitespace), as a real cap-hit truncation does — a
|
|
121
|
+
# token ending in a trailing space would merge under BPE on re-render and cost a
|
|
122
|
+
# 1-token tail divergence that no text-level close can avoid on a non-trimmable cache.
|
|
123
|
+
reasoning = (("Let me trace the validation path and reason about the boundary "
|
|
124
|
+
"conditions step by step. ") * 24).strip()
|
|
125
|
+
gen_ids = tok.encode(reasoning, add_special_tokens=False)
|
|
126
|
+
# Put the engine in the EXACT state generate() leaves after that truncated turn:
|
|
127
|
+
# the live KV cache holds prompt + the raw (unclosed) reasoning tokens.
|
|
128
|
+
cache_ids = list(p1) + gen_ids
|
|
129
|
+
eng._reset_cache()
|
|
130
|
+
eng._prefill(cache_ids)
|
|
131
|
+
eng._cached_ids = cache_ids
|
|
132
|
+
|
|
133
|
+
# Store the assistant turn the way run_turn does — with or without the fix — then
|
|
134
|
+
# append a canned tool result (as the loop would after a tool call).
|
|
135
|
+
stored = close_unclosed_think(reasoning, True) if apply_fix else reasoning
|
|
136
|
+
messages.append({"role": "assistant", "content": stored})
|
|
137
|
+
messages.append({"role": "tool", "name": "read",
|
|
138
|
+
"content": "def validate(x):\n return x is not None\n"})
|
|
139
|
+
|
|
140
|
+
# The FOLLOWING render. Its prefill is the cache miss we're measuring: with the fix
|
|
141
|
+
# off the unclosed think makes the stored turn re-tokenize so it is no longer a prefix
|
|
142
|
+
# of the cache -> non-trimmable reset -> full re-prefill. With the fix on the cache
|
|
143
|
+
# stays warm and only the appended tokens prefill.
|
|
144
|
+
p2 = _render(tok, messages)
|
|
145
|
+
_, s2 = eng.generate(p2, max_tokens=8)
|
|
146
|
+
return s2.prompt_tokens, s2.prefill_s, s2.cached_tokens
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _run_agentic(model_id: str, why: str, context_tokens: int) -> int:
|
|
150
|
+
w = 72
|
|
151
|
+
print(f"\nloading {model_id} [{why}] ...", flush=True)
|
|
152
|
+
# Fresh process-cold cache for each variant (separate Engine in _agentic).
|
|
153
|
+
off_new, off_s, off_cached = _agentic(model_id, why, context_tokens, apply_fix=False)
|
|
154
|
+
on_new, on_s, on_cached = _agentic(model_id, why, context_tokens, apply_fix=True)
|
|
155
|
+
ctx = on_new + on_cached # actual rendered transcript size at the measured step
|
|
156
|
+
print("=" * w)
|
|
157
|
+
print(f"agentic prefill — truncated-turn cache miss @ {ctx:,} ctx tokens")
|
|
158
|
+
print("=" * w)
|
|
159
|
+
print(f"{'':22}{'new prefill':>14}{'cached':>10}{'prefill s':>12}")
|
|
160
|
+
print("-" * w)
|
|
161
|
+
print(f"{'truncation, fix OFF':22}{off_new:>14,}{off_cached:>10,}{off_s:>12.2f}")
|
|
162
|
+
print(f"{'truncation, fix ON':22}{on_new:>14,}{on_cached:>10,}{on_s:>12.2f}")
|
|
163
|
+
print("-" * w)
|
|
164
|
+
saved = off_new - on_new
|
|
165
|
+
speedup = (off_s / on_s) if on_s else float("inf")
|
|
166
|
+
print(f"fix avoids re-prefilling {saved:,} tokens on the step after a truncation "
|
|
167
|
+
f"(~{off_s - on_s:.1f}s, {speedup:.0f}x faster prefill).")
|
|
168
|
+
print("=" * w + "\n")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main(argv=None) -> int:
|
|
173
|
+
ap = argparse.ArgumentParser(
|
|
174
|
+
prog="chad-bench",
|
|
175
|
+
description="Measure chad's prefill / decode / warm-step throughput on this machine.",
|
|
176
|
+
)
|
|
177
|
+
ap.add_argument("--prefill-tokens", type=int, default=5000,
|
|
178
|
+
help="size of the cold prompt to prefill (default: 5000)")
|
|
179
|
+
ap.add_argument("--gen-tokens", type=int, default=128,
|
|
180
|
+
help="tokens to decode for the decode measurement (default: 128)")
|
|
181
|
+
ap.add_argument("--agentic", action="store_true",
|
|
182
|
+
help="run the agentic-session prefill benchmark: reproduce the "
|
|
183
|
+
"truncated-turn cache miss and the fix, measuring the re-prefill "
|
|
184
|
+
"the next step pays (with/without the think-close fix)")
|
|
185
|
+
ap.add_argument("--context-tokens", type=int, default=24000,
|
|
186
|
+
help="seeded context size for --agentic (default: 24000)")
|
|
187
|
+
args = ap.parse_args(argv)
|
|
188
|
+
|
|
189
|
+
if args.agentic:
|
|
190
|
+
model_id, why = _pick_model()
|
|
191
|
+
_ensure_model(model_id)
|
|
192
|
+
return _run_agentic(model_id, why, args.context_tokens)
|
|
193
|
+
|
|
194
|
+
model_id, why = _pick_model()
|
|
195
|
+
_ensure_model(model_id)
|
|
196
|
+
|
|
197
|
+
# cache_dir=None so the on-disk warm-prefix can't pre-load and skew the *cold* prefill
|
|
198
|
+
# number; this benchmark measures from a genuinely cold cache.
|
|
199
|
+
eng = Engine(model_id=model_id, draft_id=None, cache_dir=None)
|
|
200
|
+
sys.stderr.write(f"loading {model_id} [{why}] ...\n")
|
|
201
|
+
load_s = eng.load()
|
|
202
|
+
|
|
203
|
+
prompt_ids = _build_prompt(eng.tok, args.prefill_tokens)
|
|
204
|
+
|
|
205
|
+
# 1 + 2: cold prefill, then decode. One generate() call does both; GenStats splits the
|
|
206
|
+
# time into prefill_s (reading the prompt) and gen_s (writing new tokens).
|
|
207
|
+
_, stats = eng.generate(prompt_ids, max_tokens=args.gen_tokens)
|
|
208
|
+
prefill_tps = stats.prompt_tokens / stats.prefill_s if stats.prefill_s else 0.0
|
|
209
|
+
decode_tps = stats.tok_per_s
|
|
210
|
+
|
|
211
|
+
# 3: warm step. The cache now holds the whole prompt + what we just generated. A real
|
|
212
|
+
# agentic turn appends a small user/tool message and regenerates — a strict extension,
|
|
213
|
+
# so only the appended tokens prefill. Build that follow-up from the live cached ids.
|
|
214
|
+
suffix = _encode_suffix(eng.tok, "\n\nNow add a docstring to process() and rerun the tests.\n")
|
|
215
|
+
warm_prompt = list(eng._cached_ids) + suffix
|
|
216
|
+
_, wstats = eng.generate(warm_prompt, max_tokens=8)
|
|
217
|
+
|
|
218
|
+
w = 64
|
|
219
|
+
print("\n" + "=" * w)
|
|
220
|
+
print(f"chad throughput — {model_id}")
|
|
221
|
+
print(f" {why}")
|
|
222
|
+
print("=" * w)
|
|
223
|
+
print(f"model load {load_s:6.1f} s")
|
|
224
|
+
print("-" * w)
|
|
225
|
+
print(f"1. prefill (cold) {stats.prompt_tokens:6d} tok in {stats.prefill_s:6.2f} s"
|
|
226
|
+
f" -> {prefill_tps:6.0f} tok/s")
|
|
227
|
+
print(f"2. decode {stats.generated_tokens:6d} tok in {stats.gen_s:6.2f} s"
|
|
228
|
+
f" -> {decode_tps:6.1f} tok/s")
|
|
229
|
+
print(f"3. warm step {wstats.prompt_tokens:6d} new tok prefilled "
|
|
230
|
+
f"({wstats.cached_tokens} cached)")
|
|
231
|
+
print(f" {wstats.prefill_s:6.2f} s of prefill for the follow-up turn")
|
|
232
|
+
print("-" * w)
|
|
233
|
+
cold_est = (stats.prompt_tokens / prefill_tps) if prefill_tps else 0.0
|
|
234
|
+
print(f"the agentic-loop win: a follow-up turn prefills {wstats.prompt_tokens} tokens "
|
|
235
|
+
f"(~{wstats.prefill_s:.2f} s),")
|
|
236
|
+
print(f"not the full {wstats.cached_tokens + wstats.prompt_tokens} "
|
|
237
|
+
f"(~{cold_est:.1f} s) a cache-less backend would re-read every step.")
|
|
238
|
+
print("=" * w + "\n")
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
raise SystemExit(main())
|