felab package
The shared helper package every lesson imports.
felab/__init__.py Python · 12 lines
"""felab — the shared toolkit every lesson imports.
from felab import add_target_args, resolve, client, banner # where to send requests from felab import stream_once, astream_once, percentile, user # how to time them from felab import record, table # where the numbers go"""from .measure import Sample, astream_once, percentile, stream_once, userfrom .results import record, tablefrom .targets import TARGETS, Target, add_target_args, banner, client, resolve
__all__ = ["Sample", "astream_once", "percentile", "stream_once", "user", "record", "table", "TARGETS", "Target", "add_target_args", "banner", "client", "resolve"]felab/hardware.py Python · 54 lines
"""felab.hardware — detect the machine and know its memory bandwidth.
Why bandwidth? During decode, every new token requires reading (roughly) allactive weights from memory once. So:
decode ceiling (tok/s) ≈ memory bandwidth (GB/s) ÷ bytes read per token (GB)
That single line explains why quantization, MoE and batching matter. Lessons 04and 12 compare this ceiling with what you measure (expect 60–85% of it)."""from __future__ import annotations
import platformimport reimport subprocess
# Approximate peak unified-memory bandwidth, GB/s. Where a chip ships in two# bandwidth bins (e.g. M3 Max, M4 Max) we list the higher one — check yours.APPLE_BW = { "M1": 68, "M1 Pro": 200, "M1 Max": 400, "M1 Ultra": 800, "M2": 100, "M2 Pro": 200, "M2 Max": 400, "M2 Ultra": 800, "M3": 100, "M3 Pro": 150, "M3 Max": 400, "M3 Ultra": 819, "M4": 120, "M4 Pro": 273, "M4 Max": 546, "M5": 153,}OTHER_BW = {"DGX Spark (GB10)": 273, "RTX 4090": 1008, "H100 SXM": 3350, "H200": 4800, "B200": 8000}
def _sysctl(key: str) -> str: try: return subprocess.run(["sysctl", "-n", key], capture_output=True, text=True, timeout=3).stdout.strip() except Exception: return ""
def detect() -> dict: """Return {'chip': str, 'ram_gb': float, 'bw_gbs': int|None}.""" if platform.system() == "Darwin": chip = _sysctl("machdep.cpu.brand_string") or "Apple ?" ram = int(_sysctl("hw.memsize") or 0) / 1e9 m = re.search(r"(M\d)(\s+(Pro|Max|Ultra))?", chip) key = (m.group(1) + (" " + m.group(3) if m.group(3) else "")) if m else "" return {"chip": chip, "ram_gb": round(ram, 1), "bw_gbs": APPLE_BW.get(key)} try: ram = int(open("/proc/meminfo").read().split()[1]) / 1e6 except Exception: ram = 0.0 return {"chip": platform.processor() or platform.machine(), "ram_gb": round(ram, 1), "bw_gbs": None}
def ceiling(bw_gbs: float, gb_per_token: float) -> float: """Upper bound on single-stream decode speed.""" return bw_gbs / gb_per_token if gb_per_token else float("inf")felab/measure.py Python · 99 lines
"""felab.measure — the two numbers every inference conversation is about.
TTFT time to first token = queue + PREFILL (reading the whole prompt) ITL inter-token latency = one DECODE step (writing one more token)
Users feel TTFT as "is it thinking?" and ITL as "how fast does it type?".They are driven by different things (compute vs memory bandwidth), so weALWAYS report them separately — an average "latency" hides both.
Note: we count streamed chunks as tokens. Most servers send one token perchunk; a few batch several. For exact counts use `usage` (non-streaming)."""from __future__ import annotations
import mathimport timefrom dataclasses import dataclass, field
@dataclassclass Sample: ttft_ms: float # prompt sent → first visible token itl_ms: list[float] = field(default_factory=list) # gap between consecutive tokens tokens: int = 0 # streamed chunks with content total_s: float = 0.0 # whole request wall time text: str = ""
@property def itl_median(self) -> float: return percentile(self.itl_ms, 50) if self.itl_ms else 0.0
@property def decode_tps(self) -> float: """Tokens per second for ONE user once text starts flowing.""" return 1000.0 / self.itl_median if self.itl_median else 0.0
def percentile(xs: list[float], p: float) -> float: """Nearest-rank percentile. p95 of 10 samples is the worst one — that is deliberate: tails are what customers complain about.""" if not xs: return float("nan") s = sorted(xs) k = max(0, min(len(s) - 1, math.ceil(p / 100 * len(s)) - 1)) return s[k]
def _content(chunk) -> str: """Pull visible text out of a streamed chunk. Reasoning models may stream `reasoning_content` first; we count that too because the user waits for it.""" if not chunk.choices: return "" d = chunk.choices[0].delta return (getattr(d, "content", None) or getattr(d, "reasoning_content", None) or "")
def stream_once(cli, model: str, messages: list[dict], max_tokens: int = 192, **extra) -> Sample: """Send one streaming request and time every token (sync client).""" t0 = time.perf_counter() stamps: list[float] = [] parts: list[str] = [] for ch in cli.chat.completions.create(model=model, messages=messages, max_tokens=max_tokens, stream=True, **extra): piece = _content(ch) if piece: stamps.append(time.perf_counter()) parts.append(piece) return _to_sample(t0, stamps, parts)
async def astream_once(cli, model: str, messages: list[dict], max_tokens: int = 128, **extra) -> Sample: """Same as stream_once for an AsyncOpenAI client (used by the concurrency sweep).""" t0 = time.perf_counter() stamps: list[float] = [] parts: list[str] = [] stream = await cli.chat.completions.create(model=model, messages=messages, max_tokens=max_tokens, stream=True, **extra) async for ch in stream: piece = _content(ch) if piece: stamps.append(time.perf_counter()) parts.append(piece) return _to_sample(t0, stamps, parts)
def _to_sample(t0: float, stamps: list[float], parts: list[str]) -> Sample: end = time.perf_counter() if not stamps: # server returned nothing visible return Sample(ttft_ms=(end - t0) * 1000, total_s=end - t0) itl = [(b - a) * 1000 for a, b in zip(stamps, stamps[1:])] return Sample(ttft_ms=(stamps[0] - t0) * 1000, itl_ms=itl, tokens=len(stamps), total_s=end - t0, text="".join(parts))
def user(prompt: str, system: str | None = None) -> list[dict]: """Build a chat message list.""" msgs = [{"role": "system", "content": system}] if system else [] return msgs + [{"role": "user", "content": prompt}]felab/mock_server.py Python · 227 lines
"""felab.mock_server — a fake OpenAI-compatible LLM server with realistic *shape*.
python -m felab.mock_server # listens on :9000 python -m felab.mock_server --slots 4 --itl-ms 25
It generates no real language. What it does simulate, on purpose, is thebehaviour each lesson teaches, so you can run every script offline firstand know what "right" looks like before spending time or money:
* PREFILL cost grows with prompt length → lesson 01 (TTFT vs prompt size) * DECODE slows a little as more users share a step → lesson 03 (concurrency curve) * only N "slots" (batch size); extra users queue → lesson 03 (the TTFT knee) * a PREFIX CACHE in 64-token blocks → lesson 06 (shared prefixes) * JSON: constrained decoding always valid, prompt-only JSON sometimes breaks → lesson 07 (structured output) * a "-lora" model id classifies tickets better → lessons 09–11 (eval delta)
Standard library only. Not a benchmark of anything real — the numbers are made up."""from __future__ import annotations
import argparseimport hashlibimport jsonimport randomimport threadingimport timefrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
CFG = {"slots": 8, "itl_ms": 18.0, "prefill_tok_per_s": 2500.0, "block": 64}SLOTS: threading.SemaphoreACTIVE = 0LOCK = threading.Lock()PREFIX_CACHE: set[str] = set() # hashes of cached prompt blocks
WORDS = ("the cache keeps keys and values so each new token only reads what it needs while " "bandwidth sets the ceiling and batching shares one pass across many users").split()
# The "ground truth" the mock uses to answer ticket-triage prompts (lesson 07/09).KEYWORDS = { # checked in this order — abuse first so "fake invoices" is abuse, not billing "abuse": ["spam", "phishing", "abuse", "fraud", "stolen", "scraping"], "billing": ["invoice", "card", "charge", "refund", "billing", "payment", "declined"], "outage": ["down", "500", "timeout", "time out", "outage", "unreachable", "latency spike", "error rate"], "how-to": ["how do i", "how to", "where can i", "docs", "configure", "set up"],}
def approx_tokens(text: str) -> int: return max(1, len(text) // 4) # ~4 characters per token for English
def prompt_text(messages: list[dict]) -> str: out = [] for m in messages: c = m.get("content", "") out.append(c if isinstance(c, str) else json.dumps(c)) return "\n".join(out)
def uncached_tokens(text: str) -> int: """Walk the prompt in fixed blocks; blocks whose whole prefix was seen before are free. Mirrors how vLLM/SGLang/llama.cpp hash KV blocks. One changed character early on invalidates every block after it — that is why prompt ORDER matters.""" block_chars = CFG["block"] * 4 h = hashlib.sha1() cached_chars = 0 new_hashes = [] for i in range(0, len(text) - block_chars + 1, block_chars): h.update(text[i:i + block_chars].encode()) key = h.hexdigest() if key in PREFIX_CACHE and cached_chars == i: cached_chars = i + block_chars new_hashes.append(key) PREFIX_CACHE.update(new_hashes) return approx_tokens(text[cached_chars:]) if cached_chars < len(text) else 0
def classify(text: str, model: str) -> tuple[str, int]: """Pretend-model: finds the true label by keyword, then is sometimes wrong. Base model is right ~75% of the time, a '-lora' model ~93%. Deterministic per input.""" low = text.lower() truth = next((c for c, kws in KEYWORDS.items() if any(k in low for k in kws)), "how-to") rng = random.Random(hashlib.md5((low + model).encode()).hexdigest()) acc = 0.93 if "lora" in model else 0.75 label = truth if rng.random() < acc else rng.choice([c for c in KEYWORDS if c != truth]) sev = {"outage": 4, "abuse": 4, "billing": 3, "how-to": 2}[label] if any(w in low for w in ("urgent", "tomorrow", "all users", "production")): sev = min(5, sev + 1) return label, sev
def fake_json(messages: list[dict], model: str, schema: dict | None) -> str: user_text = next((m["content"] for m in reversed(messages) if m.get("role") == "user"), "") label, sev = classify(user_text, model) obj = {"category": label, "severity": sev, "next_action": f"route to {label} queue"} if schema and "properties" in schema: # only keep fields the schema asks for obj = {k: v for k, v in obj.items() if k in schema["properties"]} or obj return json.dumps(obj)
class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1"
def log_message(self, *a): # keep the terminal quiet pass
def _json(self, code: int, obj: dict) -> None: body = json.dumps(obj).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body)
def do_GET(self): if self.path.rstrip("/") in ("/health", "/v1/health"): return self._json(200, {"status": "ok"}) if self.path.startswith("/v1/models"): return self._json(200, {"object": "list", "data": [{"id": "mock-8b", "object": "model"}, {"id": "mock-8b-lora", "object": "model"}]}) self._json(404, {"error": "not found"})
def do_POST(self): if not self.path.endswith("/chat/completions"): return self._json(404, {"error": "only /v1/chat/completions is mocked"}) req = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}") global ACTIVE SLOTS.acquire() # queue here if all batch slots are busy with LOCK: ACTIVE += 1 try: self._complete(req) finally: with LOCK: ACTIVE -= 1 SLOTS.release()
def _complete(self, req: dict) -> None: messages = req.get("messages", []) model = req.get("model", "mock-8b") max_tokens = int(req.get("max_tokens") or 256) text = prompt_text(messages) prompt_toks = approx_tokens(text)
# ---- PREFILL: pay only for the part of the prompt that is not cached ---- todo = uncached_tokens(text) time.sleep(0.015 + todo / CFG["prefill_tok_per_s"])
# ---- choose what to "generate" ---- rf = req.get("response_format") or {} wants_json = "json" in text.lower() or rf.get("type") in ("json_object", "json_schema") if "rubric" in text.lower(): # acting as an LLM judge (lesson 09) # score high if the proposed action routes to the ticket's true category ticket = text.split("Ticket:")[-1].split("Proposed next_action:")[0].lower() action = text.split("Proposed next_action:")[-1].split("Rubric:")[0].lower() truth = next((c for c, kws in KEYWORDS.items() if any(k in ticket for k in kws)), "how-to") rng = random.Random(text) score = rng.choice([4, 5, 5]) if truth in action else rng.choice([1, 2, 3]) pieces = [json.dumps({"score": score})] elif wants_json: schema = (rf.get("json_schema") or {}).get("schema") out = fake_json(messages, model, schema) constrained = rf.get("type") in ("json_object", "json_schema") # Without constrained decoding a small model sometimes wraps or truncates JSON. if not constrained and random.Random(text).random() < 0.12: out = "Sure! Here is the JSON:\n" + out[:-3] pieces = [out[i:i + 4] for i in range(0, len(out), 4)] else: rng = random.Random(text) n = min(max_tokens, rng.randint(60, 200)) pieces = [(" " if i else "") + rng.choice(WORDS) for i in range(n)] pieces = pieces[:max_tokens]
if not req.get("stream"): time.sleep(len(pieces) * self._itl()) return self._json(200, { "id": "mock-1", "object": "chat.completion", "model": model, "choices": [{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": "".join(pieces)}}], "usage": {"prompt_tokens": prompt_toks, "completion_tokens": len(pieces), "total_tokens": prompt_toks + len(pieces), "prompt_tokens_details": {"cached_tokens": prompt_toks - todo}}})
# ---- DECODE: stream one piece per step, like a real server ---- self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.send_header("Cache-Control", "no-cache") self.send_header("Connection", "close") self.end_headers() try: for p in pieces: chunk = {"id": "mock-1", "object": "chat.completion.chunk", "model": model, "choices": [{"index": 0, "delta": {"content": p}, "finish_reason": None}]} self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode()) self.wfile.flush() time.sleep(self._itl()) done = {"id": "mock-1", "object": "chat.completion.chunk", "model": model, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]} self.wfile.write(f"data: {json.dumps(done)}\n\ndata: [DONE]\n\n".encode()) self.wfile.flush() except (BrokenPipeError, ConnectionResetError): pass self.close_connection = True
@staticmethod def _itl() -> float: """One decode step. Each extra user in the batch adds ~6% — the step is memory-bound, so sharing it is cheap. That is why batching raises throughput.""" return CFG["itl_ms"] / 1000 * (1 + 0.06 * max(0, ACTIVE - 1))
def main() -> None: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--port", type=int, default=9000) ap.add_argument("--slots", type=int, default=CFG["slots"], help="max concurrent sequences (batch size)") ap.add_argument("--itl-ms", type=float, default=CFG["itl_ms"], help="single-user decode step") a = ap.parse_args() CFG.update(slots=a.slots, itl_ms=a.itl_ms) global SLOTS SLOTS = threading.Semaphore(a.slots) print(f"mock LLM on http://localhost:{a.port}/v1 slots={a.slots} itl={a.itl_ms}ms (Ctrl-C to stop)") ThreadingHTTPServer(("0.0.0.0", a.port), Handler).serve_forever()
if __name__ == "__main__": main()felab/results.py Python · 51 lines
"""felab.results — every lesson appends its numbers to results/<lesson>.csv.
Why bother: the capstone (lesson 15) is a sizing memo built from YOUR measurements.If every run lands in a CSV with the target and model next to it, the memowrites itself and you can quote real numbers when explaining the result."""from __future__ import annotations
import csvimport datetime as dtfrom pathlib import Path
from .targets import REPO_ROOT
RESULTS = REPO_ROOT / "results"
def record(lesson: str, row: dict) -> Path: """Append one row (dict) to results/<lesson>.csv, adding a timestamp.""" RESULTS.mkdir(exist_ok=True) path = RESULTS / f"{lesson}.csv" row = {"when": dt.datetime.now().isoformat(timespec="seconds"), **row} new = not path.exists() # If columns changed since the file was created, start a fresh file rather than corrupting it. if not new: with path.open() as f: header = next(csv.reader(f), []) if header != list(row): path = path.with_name(f"{path.stem}-{dt.datetime.now():%H%M%S}.csv") new = True with path.open("a", newline="") as f: w = csv.DictWriter(f, fieldnames=list(row)) if new: w.writeheader() w.writerow(row) return path
def table(rows: list[dict], cols: list[str] | None = None) -> str: """Render rows as a fixed-width text table for the terminal.""" if not rows: return "(no rows)" cols = cols or list(rows[0]) fmt = lambda v: (f"{v:.3f}" if 0 < abs(v) < 1 else f"{v:,.1f}") if isinstance(v, float) else str(v) widths = {c: max(len(c), *(len(fmt(r.get(c, ""))) for r in rows)) for c in cols} line = " ".join(c.ljust(widths[c]) for c in cols) out = [line, " ".join("-" * widths[c] for c in cols)] for r in rows: out.append(" ".join(fmt(r.get(c, "")).rjust(widths[c]) for c in cols)) return "\n".join(out)felab/targets.py Python · 114 lines
"""felab.targets — one place that knows where every model server lives.
Every lesson script takes the same three flags:
--target mock | ollama | llamacpp | mlx | fireworks | spark --model override the target's default model id --base-url override the target's URL (rarely needed)
so the SAME script measures your laptop, Fireworks and the DGX Spark. That isthe habit the course is built on: one harness, many backends, comparable numbers.
Defaults can be changed in a `.env` file at the repo root (see .env.example)."""from __future__ import annotations
import argparseimport osfrom dataclasses import dataclassfrom pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
def load_dotenv(path: Path = REPO_ROOT / ".env") -> None: """Tiny .env reader (KEY=value per line) so we need no extra dependency. Real environment variables always win over the file.""" if not path.exists(): return for line in path.read_text().splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
load_dotenv()
@dataclassclass Target: name: str base_url: str model: str api_key: str note: str
@property def is_paid(self) -> bool: return self.name == "fireworks"
def _targets() -> dict[str, Target]: env = os.environ.get spark_ip = env("SPARK_IP", "spark.local") return { # A fake server that ships with this repo (python -m felab.mock_server). # Lets you run every lesson on a train with no model downloaded. "mock": Target("mock", env("MOCK_URL", "http://localhost:9000/v1"), "mock-8b", "x", "felab mock server — simulated latency, free, offline"), # Ollama: zero-config wrapper around llama.cpp. "ollama": Target("ollama", "http://localhost:11434/v1", env("OLLAMA_MODEL", "llama3.1:8b"), "x", "Ollama on :11434"), # llama-server (llama.cpp): every knob exposed. It ignores the model field. "llamacpp": Target("llamacpp", "http://localhost:8080/v1", "local", "x", "llama.cpp llama-server on :8080"), # mlx_lm.server: Apple-native. The model field must match what you served. "mlx": Target("mlx", env("MLX_URL", "http://localhost:8081/v1"), env("MLX_MODEL", "mlx-community/Meta-Llama-3.1-8B-Instruct-4bit"), "x", "mlx_lm.server on :8081"), # Fireworks serverless: pay per token. Check the model library for current ids. "fireworks": Target("fireworks", "https://api.fireworks.ai/inference/v1", env("FIREWORKS_MODEL", "accounts/fireworks/models/gpt-oss-120b"), env("FIREWORKS_API_KEY", ""), "Fireworks serverless (PAID)"), # vLLM / SGLang on the DGX Spark, reached over your LAN. "spark": Target("spark", env("SPARK_URL", f"http://{spark_ip}:8000/v1"), env("SPARK_MODEL", "nvidia/Llama-3.1-8B-Instruct-FP8"), "x", "vLLM/SGLang on the DGX Spark"), }
TARGETS = _targets()
def add_target_args(p: argparse.ArgumentParser, default: str | None = None) -> argparse.ArgumentParser: """Attach the standard --target/--model/--base-url flags to a script's parser.""" p.add_argument("--target", default=default or os.environ.get("TARGET", "mock"), choices=TARGETS, help="which server to talk to (default: $TARGET or mock)") p.add_argument("--model", help="override the target's default model id") p.add_argument("--base-url", help="override the target's URL") return p
def resolve(args: argparse.Namespace) -> Target: """Turn parsed flags into a Target, failing early with a helpful message.""" t = TARGETS[args.target] t = Target(t.name, args.base_url or t.base_url, args.model or t.model, t.api_key, t.note) if t.is_paid and not t.api_key: raise SystemExit("FIREWORKS_API_KEY is not set. export it, or put it in .env (see lesson 00).") return t
def client(t: Target, asynchronous: bool = False): """An OpenAI SDK client pointed at the target. Every server here speaks the OpenAI chat-completions dialect, which is exactly why one harness works everywhere.""" from openai import AsyncOpenAI, OpenAI # imported late so --help works without the SDK cls = AsyncOpenAI if asynchronous else OpenAI return cls(base_url=t.base_url, api_key=t.api_key or "x", timeout=600)
def banner(t: Target) -> None: paid = " $$ PAID ENDPOINT $$" if t.is_paid else "" print(f"→ target={t.name} model={t.model}\n {t.base_url} ({t.note}){paid}\n")felab/tickets.py Python · 68 lines
"""felab.tickets — load the triage dataset and grade answers (lessons 07–11).
Three graders, from cheapest to most expensive — the eval pattern you'll pitch: 1. parses : is it JSON at all? 2. valid : does it match the schema (right keys, allowed enum, int in range)? 3. correct : does category (and severity) match the label?(Lesson 09 adds a 4th: an LLM judge with a rubric, for fields with no single right answer.)"""from __future__ import annotations
import jsonimport refrom pathlib import Path
from .targets import REPO_ROOT
DATA = REPO_ROOT / "data"SCHEMA = json.loads((DATA / "triage_schema.json").read_text())SYSTEM = ("You triage customer support tickets. Reply with JSON only: " '{"category": "billing|outage|how-to|abuse", "severity": 1-5, "next_action": "<short>"}')
def load_test(n: int | None = None) -> list[dict]: """Held-out tickets with labels. Run `python data/make_tickets.py` if missing.""" path = DATA / "tickets" / "test.jsonl" if not path.exists(): raise SystemExit("data/tickets/test.jsonl missing — run: python data/make_tickets.py") rows = [json.loads(line) for line in path.read_text().splitlines() if line.strip()] return rows[:n] if n else rows
def parse(text: str | None) -> dict | None: """Strict: the reply must BE a JSON object (whitespace and ```json fences tolerated). Being lenient here would hide exactly the failures an integration breaks on.""" if not text: return None t = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip()) try: obj = json.loads(t) return obj if isinstance(obj, dict) else None except json.JSONDecodeError: return None
def schema_valid(obj: dict | None) -> bool: if obj is None: return False try: import jsonschema jsonschema.validate(obj, SCHEMA) return True except ImportError: # minimal fallback check return (obj.get("category") in SCHEMA["properties"]["category"]["enum"] and isinstance(obj.get("severity"), int) and 1 <= obj["severity"] <= 5 and isinstance(obj.get("next_action"), str)) except Exception: return False
def grade(reply: str | None, row: dict) -> dict: obj = parse(reply) return { "parses": obj is not None, "valid": schema_valid(obj), "category_ok": bool(obj) and obj.get("category") == row["category"], "severity_ok": bool(obj) and obj.get("severity") == row["severity"], }