From 8c0438bb747521dea1f723b4815b8787d6911b07 Mon Sep 17 00:00:00 2001 From: Alazar Manakelew Date: Sun, 14 Jun 2026 20:40:58 -0400 Subject: [PATCH] updx --- SuperKittens/cli/README_logo.md | 80 ++++ SuperKittens/cli/__init__.py | 5 + SuperKittens/cli/__main__.py | 482 ++++++++++++++++++++++++ SuperKittens/cli/banner.py | 94 +++++ SuperKittens/cli/logo_preview.png | Bin 0 -> 22133 bytes SuperKittens/scripts/download_models.sh | 80 ++++ bin/sk | 44 +++ 7 files changed, 785 insertions(+) create mode 100644 SuperKittens/cli/README_logo.md create mode 100644 SuperKittens/cli/__init__.py create mode 100644 SuperKittens/cli/__main__.py create mode 100644 SuperKittens/cli/banner.py create mode 100644 SuperKittens/cli/logo_preview.png create mode 100755 SuperKittens/scripts/download_models.sh create mode 100755 bin/sk diff --git a/SuperKittens/cli/README_logo.md b/SuperKittens/cli/README_logo.md new file mode 100644 index 0000000..82f44c6 --- /dev/null +++ b/SuperKittens/cli/README_logo.md @@ -0,0 +1,80 @@ +# SK CLI logo + +The SuperKittens CLI banner (`SuperKittens/cli/banner.py`) is a purple -> pink +gradient ASCII wordmark, rendered once with [oh-my-logo](https://github.com/shinshin86/oh-my-logo) +and embedded as ANSI strings so the CLI prints it offline (no Node/npx needed at +runtime). + +## Palette + +A custom violet -> magenta -> pink gradient that mirrors SuperKitty's purple +bodysuit (see `meow.png` in the repo root) flowing into the README's pink accent: + +``` +#7c3aed (violet) +#a855f7 (purple) +#d946ef (magenta) +#ec4899 (pink) +``` + +## Regenerate + +The full wordmark (Standard figlet font, horizontal gradient): + +```bash +npx oh-my-logo "SuperKittens" \ + --palette-colors "#7c3aed,#a855f7,#d946ef,#ec4899" \ + --color -d horizontal +``` + +The compact `SK` mark for narrow terminals (filled block font): + +```bash +npx oh-my-logo "SK" \ + --palette-colors "#7c3aed,#a855f7,#d946ef,#ec4899" \ + --color -d horizontal --filled --block-font block +``` + +`--color` forces the ANSI truecolor escapes even when stdout is a pipe, so the +captured output keeps its gradient. Pipe each command into the +`BANNER` / `BANNER_COMPACT` literals in `banner.py` (escape `ESC` as `\x1b`, +trim the blank padding lines that `--filled` adds). + +## Runtime behavior + +`print_banner()` (in `banner.py`): + +- picks the full wordmark when the terminal is >= 60 cols, else the compact `SK`; +- strips color when the destination is not a TTY, or when `NO_COLOR` is set + (and honors `FORCE_COLOR` / the `force_color=` arg the other way); +- appends the MEOW tagline. + +```bash +python -c "from SuperKittens.cli.banner import print_banner; print_banner()" +``` + +## Preview + +Full wordmark (`BANNER`, ~59 cols) -- colored in a truecolor terminal: + +``` + ____ _ ___ _ _ + / ___| _ _ _ __ ___ _ __| |/ (_) |_| |_ ___ _ __ ___ + \___ \| | | | '_ \ / _ \ '__| ' /| | __| __/ _ \ '_ \/ __| + ___) | |_| | |_) | __/ | | . \| | |_| || __/ | | \__ \ + |____/ \__,_| .__/ \___|_| |_|\_\_|\__|\__\___|_| |_|___/ + |_| +Maximizing Efficient Operations per Watt — Metal kernels for Apple Silicon +``` + +Compact mark (`BANNER_COMPACT`, ~18 cols) for narrow terminals: + +``` + ███████╗ ██╗ ██╗ + ██╔════╝ ██║ ██╔╝ + ███████╗ █████╔╝ + ╚════██║ ██╔═██╗ + ███████║ ██║ ██╗ + ╚══════╝ ╚═╝ ╚═╝ +MEOW +``` diff --git a/SuperKittens/cli/__init__.py b/SuperKittens/cli/__init__.py new file mode 100644 index 0000000..b895a60 --- /dev/null +++ b/SuperKittens/cli/__init__.py @@ -0,0 +1,5 @@ +"""SuperKittens CLI helpers (banner, terminal chrome).""" + +from .banner import BANNER, BANNER_COMPACT, print_banner, render_banner + +__all__ = ["BANNER", "BANNER_COMPACT", "print_banner", "render_banner"] diff --git a/SuperKittens/cli/__main__.py b/SuperKittens/cli/__main__.py new file mode 100644 index 0000000..6137d31 --- /dev/null +++ b/SuperKittens/cli/__main__.py @@ -0,0 +1,482 @@ +"""SuperKittens CLI — `sk launch`, `sk models`, `sk `, `sk run`. + +Thin user-facing driver over the inference registry + api.load. Weights live in +a single `weights/` directory at the repo root (override with SK_WEIGHTS_ROOT); +`sk ` downloads the model there (GGUF + tokenizer); `sk run` +opens an interactive chat with live tok/s. No loader changes: weights are passed +to api.load via the adapter's `snapshot=` override. + + sk launch banner + commands + sk models list models, status, and weight paths + sk download a model's weights (if not already local) + sk run chat with the model (live tok/s) + sk talk [ ] two models converse (default qwen3-1.7b + qwen3-8b-q4km) +""" +from __future__ import annotations + +import os +import re +import sys +import time +from pathlib import Path + +from .banner import print_banner + +REPO_ROOT = Path(__file__).resolve().parents[2] + +# Keep the CLI output clean: silence huggingface_hub's unauthenticated-request +# warning + info chatter (downloads still work fine without a token). +os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1") +import logging as _logging +_logging.getLogger("huggingface_hub").setLevel(_logging.ERROR) + + +def weights_root() -> Path: + return Path(os.environ.get("SK_WEIGHTS_ROOT") or (REPO_ROOT / "weights")) + + +# GGUF artifacts live in a separate HF repo from the canonical (safetensors) +# repo. Convention: -GGUF for first-party repos (Qwen); the +# exceptions below point at community repos. Everything else derives from +# registry.hf_repo + "-GGUF". +GGUF_REPO_OVERRIDE = { + "mistral-7b-v0.3": "bartowski/Mistral-7B-Instruct-v0.3-GGUF", + "nemotron-nano-8b": "bartowski/nvidia_Llama-3.1-Nemotron-Nano-8B-v1-GGUF", +} + +_AUX_FILES = ["tokenizer.json", "tokenizer_config.json", "tokenizer.model", + "config.json", "generation_config.json", "special_tokens_map.json"] + + +# ---- small ANSI helper (no deps; strips when not a TTY / NO_COLOR) ---------- +class _C: + def __init__(self, stream): + on = bool(getattr(stream, "isatty", lambda: False)()) and not os.environ.get("NO_COLOR") + self.pink = "\x1b[38;2;236;72;153m" if on else "" + self.violet = "\x1b[38;2;168;85;247m" if on else "" + self.cyan = "\x1b[38;2;110;200;220m" if on else "" + self.dim = "\x1b[2m" if on else "" + self.bold = "\x1b[1m" if on else "" + self.reset = "\x1b[0m" if on else "" + + +def _specs(): + from SuperKittens.inference import registry + return registry.SPECS + + +def _dest(spec) -> Path: + return weights_root() / spec.weight_dir + + +def _is_downloaded(spec) -> bool: + dest = _dest(spec) + if spec.gguf_name: + return (dest / spec.gguf_name).exists() + return dest.exists() and any(dest.glob("*.safetensors")) + + +# ---- commands --------------------------------------------------------------- +def cmd_launch(_): + print_banner() + c = _C(sys.stdout) + specs = _specs() + have = sum(1 for s in specs.values() if _is_downloaded(s)) + print() + print(f" {c.bold}Commands{c.reset}") + print(f" {c.pink}sk get{c.reset} browse models by family (↑/↓) and download") + print(f" {c.pink}sk models{c.reset} list models, status, paths") + print(f" {c.pink}sk {c.reset} download a model") + print(f" {c.pink}sk run{c.reset} chat with the model (live tok/s)") + print(f" {c.pink}sk talk{c.reset} watch two models converse") + print(f"\n {c.dim}weights: {weights_root()} · {have}/{len(specs)} downloaded{c.reset}\n") + + +def cmd_models(_): + c = _C(sys.stdout) + wr = weights_root() + print(f"\n {c.bold}{'MODEL':<22}{'STATUS':<16}PATH{c.reset}") + for name, s in _specs().items(): + down = _is_downloaded(s) + mark = f"{c.pink}✓{c.reset}" if down else f"{c.dim}·{c.reset}" + status = "downloaded" if down else "not local" + col = "" if down else c.dim + print(f" {mark} {col}{name:<20}{status:<16}{wr / s.weight_dir}{c.reset}") + print() + + +# Family display order + friendly labels for the picker. +_FAMILY_ORDER = ["qwen3", "qwen2", "gemma4", "gemma4_unified", + "mamba2", "deepseek", "nemotron", "mistral"] +_FAMILY_LABEL = {"qwen3": "Qwen3", "qwen2": "Qwen2.5", "gemma4": "Gemma 4", + "gemma4_unified": "Gemma 4 (unified)", "mamba2": "Mamba-2", + "deepseek": "DeepSeek", "nemotron": "Nemotron", "mistral": "Mistral"} + + +def _quant_of(spec) -> str: + if spec.gguf_name: + m = re.search(r"(Q\d[_A-Za-z0-9]*)", spec.gguf_name) + return m.group(1) if m else "GGUF" + return "safetensors" + + +def _grouped(): + """OrderedDict family -> [(name, spec)], families in _FAMILY_ORDER.""" + from collections import OrderedDict + specs = _specs() + groups = OrderedDict() + fams = _FAMILY_ORDER + [f for f in {s.family for s in specs.values()} + if f not in _FAMILY_ORDER] + for fam in fams: + members = [(n, s) for n, s in specs.items() if s.family == fam] + if members: + groups[fam] = members + return groups + + +def _picker(): + """Arrow-key model browser grouped by family. Returns a model name or None.""" + import curses + + groups = _grouped() + rows = [] # (kind, label, spec) + for fam, members in groups.items(): + rows.append(("hdr", _FAMILY_LABEL.get(fam, fam), None)) + for name, s in members: + rows.append(("mdl", name, s)) + selectable = [i for i, r in enumerate(rows) if r[0] == "mdl"] + if not selectable: + return None + + def _run(stdscr): + curses.curs_set(0) + try: + curses.start_color(); curses.use_default_colors() + curses.init_pair(1, curses.COLOR_MAGENTA, -1) + except Exception: + pass + cur = 0 + while True: + stdscr.erase() + maxy, maxx = stdscr.getmaxyx() + title = "Select a model ↑/↓ move · Enter download · q quit" + stdscr.addnstr(0, 0, title, maxx - 1, curses.A_BOLD) + y = 2 + for i, (kind, label, s) in enumerate(rows): + if y >= maxy - 1: + break + if kind == "hdr": + try: + stdscr.addnstr(y, 2, label, maxx - 3, + curses.A_BOLD | curses.color_pair(1)) + except Exception: + stdscr.addnstr(y, 2, label, maxx - 3, curses.A_BOLD) + else: + is_cur = selectable[cur] == i + down = _is_downloaded(s) + mark = "✓" if down else " " + text = f"{mark} {label:<22}{_quant_of(s)}" + attr = curses.A_REVERSE if is_cur else (curses.A_DIM if down else curses.A_NORMAL) + stdscr.addnstr(y, 5, text, maxx - 6, attr) + y += 1 + stdscr.refresh() + k = stdscr.getch() + if k in (ord("q"), ord("Q"), 27): + return None + if k in (curses.KEY_UP, ord("k")): + cur = (cur - 1) % len(selectable) + elif k in (curses.KEY_DOWN, ord("j")): + cur = (cur + 1) % len(selectable) + elif k in (curses.KEY_ENTER, 10, 13): + return rows[selectable[cur]][1] + + try: + return curses.wrapper(_run) + except Exception as e: + print(f" (picker unavailable: {e}; use `sk models` + `sk `)", file=sys.stderr) + return None + + +def cmd_get(args): + if args: + return cmd_download(args) + sel = _picker() + if not sel: + return + _ensure(sel) + try: + ans = input(f"\n run {sel} now? [Y/n] ").strip().lower() + except (EOFError, KeyboardInterrupt): + ans = "n" + if ans in ("", "y", "yes"): + cmd_run([sel]) + + +def _fmt_size(b: float) -> str: + return f"{b/1e9:.2f} GB" if b >= 1e9 else f"{b/1e6:.0f} MB" + + +def _render_bar(label: str, done: int, total: int, elapsed: float): + c = _C(sys.stdout) + width = 30 + frac = (done / total) if total else 0.0 + fill = int(frac * width) + bar = "█" * fill + "·" * (width - fill) + spd = (done / elapsed) if elapsed > 0 else 0.0 + eta = ((total - done) / spd) if (spd > 0 and total) else 0.0 + sys.stdout.write( + f"\r {label} {c.pink}{bar}{c.reset} {frac*100:5.1f}% " + f"{_fmt_size(done)}/{_fmt_size(total)} {c.dim}{spd/1e6:5.1f} MB/s · ETA {eta:3.0f}s{c.reset} ") + sys.stdout.flush() + + +def _stream_download(url: str, dest_path: Path, label: str): + """Stream a public file with a themed live progress bar (%, size, MB/s, ETA).""" + import urllib.request + dest_path.parent.mkdir(parents=True, exist_ok=True) + tmp = str(dest_path) + ".part" + req = urllib.request.Request(url, headers={"User-Agent": "superkittens-cli"}) + with urllib.request.urlopen(req) as r: + total = int(r.headers.get("Content-Length") or 0) + done = 0 + t0 = last = time.time() + with open(tmp, "wb") as f: + while True: + chunk = r.read(1 << 20) + if not chunk: + break + f.write(chunk) + done += len(chunk) + now = time.time() + if now - last >= 0.1 or done == total: + last = now + _render_bar(label, done, total or done, now - t0) + sys.stdout.write("\n") + os.replace(tmp, dest_path) + + +def _download(name: str) -> Path: + from huggingface_hub import hf_hub_download, list_repo_files + spec = _specs()[name] + dest = _dest(spec) + dest.mkdir(parents=True, exist_ok=True) + + if spec.gguf_name: + repo = GGUF_REPO_OVERRIDE.get(name, f"{spec.hf_repo}-GGUF") + target = spec.gguf_name + try: + files = list_repo_files(repo) + except Exception as e: + print(f" ! could not list {repo}: {e}", file=sys.stderr) + files = [] + if files and target not in files: + m = re.search(r"(Q\d[_A-Za-z0-9]*)", target) + tag = m.group(1).lower() if m else "" + cand = [f for f in files if f.endswith(".gguf") and tag in f.lower()] + if cand: + target = cand[0] + print(f" (resolved GGUF filename -> {target})") + print(f"==> downloading {name} {repo}/{target}") + url = f"https://huggingface.co/{repo}/resolve/main/{target}" + try: + _stream_download(url, dest / target, name) + except Exception as e: + print(f" (stream failed: {e}; using hf fallback)", file=sys.stderr) + hf_hub_download(repo, target, local_dir=str(dest)) + else: + from huggingface_hub import snapshot_download + print(f"==> downloading {name}: full repo {spec.hf_repo}") + snapshot_download(spec.hf_repo, local_dir=str(dest), + allow_patterns=["*.safetensors", "*.json", "*.model", "*.txt"]) + + for f in _AUX_FILES: + if (dest / f).exists(): + continue + try: + hf_hub_download(spec.hf_repo, f, local_dir=str(dest)) + except Exception: + pass + print(f" -> {dest}") + return dest + + +def _ensure(name: str) -> Path: + specs = _specs() + if name not in specs: + print(f"unknown model {name!r}. run `sk models`.", file=sys.stderr) + sys.exit(2) + if not _is_downloaded(specs[name]): + _download(name) + else: + print(f" {name} already downloaded: {_dest(specs[name])}") + return _dest(specs[name]) + + +def cmd_download(args): + if not args: + print("usage: sk ", file=sys.stderr); sys.exit(2) + _ensure(args[0]) + + +def _load(name: str): + import SuperKittens.api as api + snap = _ensure(name) + print(f" loading {name} ...", flush=True) + return api.load(name, snapshot=str(snap)) + + +def _strip_think(s: str) -> str: + s = re.sub(r".*?", "", s, flags=re.S) + return re.sub(r"[ \t]+", " ", s).strip() + + +def _read_line(prompt: str): + """Read one line. In a TTY, bare Esc (or Ctrl-C/Ctrl-D) returns None so the + caller can leave the chat; arrow-key escape sequences are ignored. Falls + back to plain readline when stdin is not a TTY (piped input).""" + import termios, tty, select + sys.stdout.write(prompt); sys.stdout.flush() + if not sys.stdin.isatty(): + ln = sys.stdin.readline() + if not ln: + return None + sys.stdout.write(ln if ln.endswith("\n") else ln + "\n") + return ln.rstrip("\n") + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + buf = bytearray() + try: + tty.setcbreak(fd) + while True: + ch = os.read(fd, 1) + if ch == b"\x1b": # Esc + r, _, _ = select.select([fd], [], [], 0.06) + if r: # arrow / escape sequence -> ignore + os.read(fd, 2) + continue + sys.stdout.write("\n"); return None # bare Esc -> leave chat + if ch in (b"\x03", b"\x04"): # Ctrl-C / Ctrl-D + sys.stdout.write("\n"); return None + if ch in (b"\r", b"\n"): + sys.stdout.write("\n"); return buf.decode("utf-8", "replace") + if ch in (b"\x7f", b"\x08"): # backspace + if buf: + buf.pop() + sys.stdout.write("\b \b"); sys.stdout.flush() + continue + buf += ch + os.write(fd, ch) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + +def cmd_run(args): + import numpy as np + import termios, tty, select + name = args[0] + m = _load(name) + c = _C(sys.stdout) + tok = m.tokenizer + rng = np.random.default_rng() + stops = set() + for x in (getattr(tok, "eos_ids", None) or []): + stops.add(int(x)) + if getattr(tok, "eos_id", None) is not None: + stops.add(int(tok.eos_id)) + istty = sys.stdin.isatty() + print(f"\n {c.violet}╭─ {c.pink}{c.bold}{name}{c.reset}{c.violet} ready ─ type a message · Esc to leave · Esc mid-reply to stop ─╮{c.reset}\n") + history = [] + while True: + line = _read_line(f"{c.cyan}you ❯ {c.reset}") + if line is None: + break + line = line.strip() + if not line: + continue + if line in ("exit", "quit"): + break + history.append({"role": "user", "content": line + " /no_think"}) + ids = np.array(tok.chat(history, bos=True), dtype=np.int32) + # Prefill (TTFT) and decode are timed separately so tok/s reflects the + # steady-state decode rate, not prompt prefill. + m.reset() + t_pre = time.time() + m._forward(ids) + ttft = time.time() - t_pre + first = m._sample(m._last_logits(), 0.7, 0.9, None, rng) + out = [first] + last = first + interrupted = False + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) if istty else None + t_dec = time.time() + try: + if old is not None: + tty.setcbreak(fd) # poll for Esc between decode steps + if first not in stops: + for _ in range(255): + m._forward(np.array([last], dtype=np.int32)) + last = m._sample(m._last_logits(), 0.7, 0.9, None, rng) + out.append(last) + if last in stops: + break + if istty and select.select([fd], [], [], 0)[0]: + if os.read(fd, 1) in (b"\x1b", b"\x03"): + interrupted = True + break + finally: + if old is not None: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + dec_dt = time.time() - t_dec + n_dec = max(len(out) - 1, 1) + tps = n_dec / dec_dt if dec_dt > 0 else 0.0 + text = _strip_think(tok.decode(out, skip_special=True)) + history.append({"role": "assistant", "content": text}) + tag = f"{c.dim} (stopped){c.reset}" if interrupted else "" + print(f"{c.pink}{name} ❯{c.reset} {text}{tag}") + print(f"{c.dim} {len(out)} tok · {tps:.1f} tok/s · {ttft*1000:.0f}ms to first token{c.reset}\n") + print(f" {c.dim}bye 🐾{c.reset}") + + +def cmd_talk(args): + a_name = args[0] if len(args) > 0 else "qwen3-1.7b" + b_name = args[1] if len(args) > 1 else "qwen3-8b-q4km" + A, B = _load(a_name), _load(b_name) + cast = [(f"🐾 Pixel ({a_name})", A, + "You are Pixel, a playful curious kitten. Reply in ONE short sentence. /no_think"), + (f"🐱 Sage ({b_name})", B, + "You are Sage, a wise old cat. Reply in ONE short sentence. /no_think")] + topic = "What is the best thing about running on a Mac mini?" + print(f"\nTOPIC: {topic}\n" + "-" * 60) + last = topic + for turn in range(6): + nm, model, persona = cast[turn % 2] + prompt = f'{persona}\n\nThe other cat just said: "{last}"\nYour reply:' + txt = _strip_think(model.chat(prompt, max_new_tokens=64, + temperature=0.7, top_p=0.9, seed=turn)) + print(f"{nm}: {txt}", flush=True) + last = txt + print("-" * 60) + + +def main(argv=None): + argv = list(sys.argv[1:] if argv is None else argv) + if not argv or argv[0] in ("launch", "help", "-h", "--help"): + return cmd_launch(argv) + cmd = argv[0] + if cmd in ("models", "ls", "list"): + return cmd_models(argv) + if cmd == "talk": + return cmd_talk(argv[1:]) + if cmd in ("get", "browse", "download"): + return cmd_get(argv[1:]) + if cmd in _specs(): + if len(argv) > 1 and argv[1] == "run": + return cmd_run([cmd]) + return cmd_download([cmd]) + print(f"unknown command/model {cmd!r}. try `sk models`.", file=sys.stderr) + sys.exit(2) + + +if __name__ == "__main__": + main() diff --git a/SuperKittens/cli/banner.py b/SuperKittens/cli/banner.py new file mode 100644 index 0000000..08120a1 --- /dev/null +++ b/SuperKittens/cli/banner.py @@ -0,0 +1,94 @@ +"""SuperKittens CLI banner. + +Purple->pink gradient wordmark for the SK CLI (e.g. ``sk launch``). +The colored ASCII art was rendered with oh-my-logo and embedded here so the +banner works offline (no Node/npx at runtime). See cli/README_logo.md for the +exact command + palette and how to regenerate. + +Public API: + print_banner(stream=None, force_color=None, width=None) + BANNER -- full "SuperKittens" wordmark (ANSI color, ~59 cols) + BANNER_COMPACT -- compact "SK" mark (ANSI color, ~18 cols) +""" + +from __future__ import annotations + +import os +import re +import shutil +import sys + +# Rendered by: npx oh-my-logo "SuperKittens" \ +# --palette-colors "#7c3aed,#a855f7,#d946ef,#ec4899" --color -d horizontal +BANNER = ( + " \x1b[38;2;124;58;237m_\x1b[39m\x1b[38;2;139;67;240m_\x1b[39m\x1b[38;2;153;76;244m_\x1b[39m\x1b[38;2;168;85;247m_\x1b[39m \x1b[38;2;184;80;244m_\x1b[39m \x1b[38;2;201;75;242m_\x1b[39m\x1b[38;2;217;70;239m_\x1b[39m\x1b[38;2;223;71;210m_\x1b[39m \x1b[38;2;230;71;182m_\x1b[39m \x1b[38;2;236;72;153m_\x1b[39m \n \x1b[38;2;124;58;237m/\x1b[39m \x1b[38;2;128;60;238m_\x1b[39m\x1b[38;2;132;63;239m_\x1b[39m\x1b[38;2;136;65;240m_\x1b[39m\x1b[38;2;140;68;241m|\x1b[39m \x1b[38;2;144;70;242m_\x1b[39m \x1b[38;2;148;73;242m_\x1b[39m \x1b[38;2;152;75;243m_\x1b[39m \x1b[38;2;156;78;244m_\x1b[39m\x1b[38;2;160;80;245m_\x1b[39m \x1b[38;2;164;83;246m_\x1b[39m\x1b[38;2;168;85;247m_\x1b[39m\x1b[38;2;172;84;246m_\x1b[39m \x1b[38;2;176;83;246m_\x1b[39m \x1b[38;2;180;81;245m_\x1b[39m\x1b[38;2;184;80;244m_\x1b[39m\x1b[38;2;188;79;244m|\x1b[39m \x1b[38;2;193;78;243m|\x1b[39m\x1b[38;2;197;76;242m/\x1b[39m \x1b[38;2;201;75;242m(\x1b[39m\x1b[38;2;205;74;241m_\x1b[39m\x1b[38;2;209;73;240m)\x1b[39m \x1b[38;2;213;71;240m|\x1b[39m\x1b[38;2;217;70;239m_\x1b[39m\x1b[38;2;219;70;232m|\x1b[39m \x1b[38;2;220;70;225m|\x1b[39m\x1b[38;2;222;71;218m_\x1b[39m \x1b[38;2;223;71;210m_\x1b[39m\x1b[38;2;225;71;203m_\x1b[39m\x1b[38;2;227;71;196m_\x1b[39m \x1b[38;2;228;71;189m_\x1b[39m \x1b[38;2;230;71;182m_\x1b[39m\x1b[38;2;231;72;175m_\x1b[39m \x1b[38;2;233;72;167m_\x1b[39m\x1b[38;2;234;72;160m_\x1b[39m\x1b[38;2;236;72;153m_\x1b[39m \n \x1b[38;2;124;58;237m\\\x1b[39m\x1b[38;2;127;60;238m_\x1b[39m\x1b[38;2;131;62;239m_\x1b[39m\x1b[38;2;134;64;239m_\x1b[39m \x1b[38;2;138;66;240m\\\x1b[39m\x1b[38;2;141;68;241m|\x1b[39m \x1b[38;2;144;70;242m|\x1b[39m \x1b[38;2;148;73;242m|\x1b[39m \x1b[38;2;151;75;243m|\x1b[39m \x1b[38;2;154;77;244m'\x1b[39m\x1b[38;2;158;79;245m_\x1b[39m \x1b[38;2;161;81;245m\\\x1b[39m \x1b[38;2;165;83;246m/\x1b[39m \x1b[38;2;168;85;247m_\x1b[39m \x1b[38;2;172;84;246m\\\x1b[39m \x1b[38;2;176;83;246m'\x1b[39m\x1b[38;2;180;81;245m_\x1b[39m\x1b[38;2;184;80;244m_\x1b[39m\x1b[38;2;188;79;244m|\x1b[39m \x1b[38;2;193;78;243m'\x1b[39m \x1b[38;2;197;76;242m/\x1b[39m\x1b[38;2;201;75;242m|\x1b[39m \x1b[38;2;205;74;241m|\x1b[39m \x1b[38;2;209;73;240m_\x1b[39m\x1b[38;2;213;71;240m_\x1b[39m\x1b[38;2;217;70;239m|\x1b[39m \x1b[38;2;219;70;232m_\x1b[39m\x1b[38;2;220;70;225m_\x1b[39m\x1b[38;2;222;71;218m/\x1b[39m \x1b[38;2;223;71;210m_\x1b[39m \x1b[38;2;225;71;203m\\\x1b[39m \x1b[38;2;227;71;196m'\x1b[39m\x1b[38;2;228;71;189m_\x1b[39m \x1b[38;2;230;71;182m\\\x1b[39m\x1b[38;2;231;72;175m/\x1b[39m \x1b[38;2;233;72;167m_\x1b[39m\x1b[38;2;234;72;160m_\x1b[39m\x1b[38;2;236;72;153m|\x1b[39m\n \x1b[38;2;124;58;237m_\x1b[39m\x1b[38;2;128;60;238m_\x1b[39m\x1b[38;2;132;63;239m_\x1b[39m\x1b[38;2;136;65;240m)\x1b[39m \x1b[38;2;140;68;241m|\x1b[39m \x1b[38;2;144;70;242m|\x1b[39m\x1b[38;2;148;73;242m_\x1b[39m\x1b[38;2;152;75;243m|\x1b[39m \x1b[38;2;156;78;244m|\x1b[39m \x1b[38;2;160;80;245m|\x1b[39m\x1b[38;2;164;83;246m_\x1b[39m\x1b[38;2;168;85;247m)\x1b[39m \x1b[38;2;172;84;246m|\x1b[39m \x1b[38;2;176;83;246m_\x1b[39m\x1b[38;2;180;81;245m_\x1b[39m\x1b[38;2;184;80;244m/\x1b[39m \x1b[38;2;188;79;244m|\x1b[39m \x1b[38;2;193;78;243m|\x1b[39m \x1b[38;2;197;76;242m.\x1b[39m \x1b[38;2;201;75;242m\\\x1b[39m\x1b[38;2;205;74;241m|\x1b[39m \x1b[38;2;209;73;240m|\x1b[39m \x1b[38;2;213;71;240m|\x1b[39m\x1b[38;2;217;70;239m_\x1b[39m\x1b[38;2;219;70;232m|\x1b[39m \x1b[38;2;220;70;225m|\x1b[39m\x1b[38;2;222;71;218m|\x1b[39m \x1b[38;2;223;71;210m_\x1b[39m\x1b[38;2;225;71;203m_\x1b[39m\x1b[38;2;227;71;196m/\x1b[39m \x1b[38;2;228;71;189m|\x1b[39m \x1b[38;2;230;71;182m|\x1b[39m \x1b[38;2;231;72;175m\\\x1b[39m\x1b[38;2;233;72;167m_\x1b[39m\x1b[38;2;234;72;160m_\x1b[39m \x1b[38;2;236;72;153m\\\x1b[39m\n \x1b[38;2;124;58;237m|\x1b[39m\x1b[38;2;127;60;238m_\x1b[39m\x1b[38;2;129;61;238m_\x1b[39m\x1b[38;2;132;63;239m_\x1b[39m\x1b[38;2;134;64;239m_\x1b[39m\x1b[38;2;137;66;240m/\x1b[39m \x1b[38;2;140;68;241m\\\x1b[39m\x1b[38;2;142;69;241m_\x1b[39m\x1b[38;2;145;71;242m_\x1b[39m\x1b[38;2;147;72;242m,\x1b[39m\x1b[38;2;150;74;243m_\x1b[39m\x1b[38;2;152;75;243m|\x1b[39m \x1b[38;2;155;77;244m.\x1b[39m\x1b[38;2;158;79;245m_\x1b[39m\x1b[38;2;160;80;245m_\x1b[39m\x1b[38;2;163;82;246m/\x1b[39m \x1b[38;2;165;83;246m\\\x1b[39m\x1b[38;2;168;85;247m_\x1b[39m\x1b[38;2;171;84;247m_\x1b[39m\x1b[38;2;174;83;246m_\x1b[39m\x1b[38;2;177;82;246m|\x1b[39m\x1b[38;2;180;81;245m_\x1b[39m\x1b[38;2;182;81;245m|\x1b[39m \x1b[38;2;185;80;244m|\x1b[39m\x1b[38;2;188;79;244m_\x1b[39m\x1b[38;2;191;78;243m|\x1b[39m\x1b[38;2;194;77;243m\\\x1b[39m\x1b[38;2;197;76;242m_\x1b[39m\x1b[38;2;200;75;242m\\\x1b[39m\x1b[38;2;203;74;241m_\x1b[39m\x1b[38;2;205;74;241m|\x1b[39m\x1b[38;2;208;73;240m\\\x1b[39m\x1b[38;2;211;72;240m_\x1b[39m\x1b[38;2;214;71;239m_\x1b[39m\x1b[38;2;217;70;239m|\x1b[39m\x1b[38;2;218;70;234m\\\x1b[39m\x1b[38;2;219;70;229m_\x1b[39m\x1b[38;2;220;70;224m_\x1b[39m\x1b[38;2;221;70;219m\\\x1b[39m\x1b[38;2;223;71;214m_\x1b[39m\x1b[38;2;224;71;209m_\x1b[39m\x1b[38;2;225;71;204m_\x1b[39m\x1b[38;2;226;71;199m|\x1b[39m\x1b[38;2;227;71;193m_\x1b[39m\x1b[38;2;228;71;188m|\x1b[39m \x1b[38;2;229;71;183m|\x1b[39m\x1b[38;2;230;71;178m_\x1b[39m\x1b[38;2;232;72;173m|\x1b[39m\x1b[38;2;233;72;168m_\x1b[39m\x1b[38;2;234;72;163m_\x1b[39m\x1b[38;2;235;72;158m_\x1b[39m\x1b[38;2;236;72;153m/\x1b[39m\n \x1b[38;2;124;58;237m|\x1b[39m\x1b[38;2;168;85;247m_\x1b[39m\x1b[38;2;217;70;239m|\x1b[39m " +) + +# Rendered by: npx oh-my-logo "SK" \ +# --palette-colors "#7c3aed,#a855f7,#d946ef,#ec4899" --color -d horizontal \ +# --filled --block-font block +BANNER_COMPACT = ( + "\x1b[38;2;124;58;237m \x1b[38;2;133;63;239m█\x1b[38;2;142;69;241m█\x1b[38;2;150;74;243m█\x1b[38;2;159;80;245m█\x1b[38;2;168;85;247m█\x1b[38;2;176;83;246m█\x1b[38;2;184;80;244m█\x1b[38;2;193;78;243m╗\x1b[38;2;201;75;242m \x1b[38;2;209;73;240m█\x1b[38;2;217;70;239m█\x1b[38;2;220;70;225m╗\x1b[38;2;223;71;210m \x1b[38;2;227;71;196m \x1b[38;2;230;71;182m█\x1b[38;2;233;72;167m█\x1b[38;2;236;72;153m╗\x1b[39m\n\x1b[38;2;124;58;237m \x1b[38;2;133;63;239m█\x1b[38;2;142;69;241m█\x1b[38;2;150;74;243m╔\x1b[38;2;159;80;245m═\x1b[38;2;168;85;247m═\x1b[38;2;176;83;246m═\x1b[38;2;184;80;244m═\x1b[38;2;193;78;243m╝\x1b[38;2;201;75;242m \x1b[38;2;209;73;240m█\x1b[38;2;217;70;239m█\x1b[38;2;220;70;225m║\x1b[38;2;223;71;210m \x1b[38;2;227;71;196m█\x1b[38;2;230;71;182m█\x1b[38;2;233;72;167m╔\x1b[38;2;236;72;153m╝\x1b[39m\n\x1b[38;2;124;58;237m \x1b[38;2;133;63;239m█\x1b[38;2;142;69;241m█\x1b[38;2;150;74;243m█\x1b[38;2;159;80;245m█\x1b[38;2;168;85;247m█\x1b[38;2;176;83;246m█\x1b[38;2;184;80;244m█\x1b[38;2;193;78;243m╗\x1b[38;2;201;75;242m \x1b[38;2;209;73;240m█\x1b[38;2;217;70;239m█\x1b[38;2;220;70;225m█\x1b[38;2;223;71;210m█\x1b[38;2;227;71;196m█\x1b[38;2;230;71;182m╔\x1b[38;2;233;72;167m╝\x1b[38;2;236;72;153m \x1b[39m\n\x1b[38;2;124;58;237m \x1b[38;2;133;63;239m╚\x1b[38;2;142;69;241m═\x1b[38;2;150;74;243m═\x1b[38;2;159;80;245m═\x1b[38;2;168;85;247m═\x1b[38;2;176;83;246m█\x1b[38;2;184;80;244m█\x1b[38;2;193;78;243m║\x1b[38;2;201;75;242m \x1b[38;2;209;73;240m█\x1b[38;2;217;70;239m█\x1b[38;2;220;70;225m╔\x1b[38;2;223;71;210m═\x1b[38;2;227;71;196m█\x1b[38;2;230;71;182m█\x1b[38;2;233;72;167m╗\x1b[38;2;236;72;153m \x1b[39m\n\x1b[38;2;124;58;237m \x1b[38;2;133;63;239m█\x1b[38;2;142;69;241m█\x1b[38;2;150;74;243m█\x1b[38;2;159;80;245m█\x1b[38;2;168;85;247m█\x1b[38;2;176;83;246m█\x1b[38;2;184;80;244m█\x1b[38;2;193;78;243m║\x1b[38;2;201;75;242m \x1b[38;2;209;73;240m█\x1b[38;2;217;70;239m█\x1b[38;2;220;70;225m║\x1b[38;2;223;71;210m \x1b[38;2;227;71;196m \x1b[38;2;230;71;182m█\x1b[38;2;233;72;167m█\x1b[38;2;236;72;153m╗\x1b[39m\n\x1b[38;2;124;58;237m \x1b[38;2;133;63;239m╚\x1b[38;2;142;69;241m═\x1b[38;2;150;74;243m═\x1b[38;2;159;80;245m═\x1b[38;2;168;85;247m═\x1b[38;2;176;83;246m═\x1b[38;2;184;80;244m═\x1b[38;2;193;78;243m╝\x1b[38;2;201;75;242m \x1b[38;2;209;73;240m╚\x1b[38;2;217;70;239m═\x1b[38;2;220;70;225m╝\x1b[38;2;223;71;210m \x1b[38;2;227;71;196m \x1b[38;2;230;71;182m╚\x1b[38;2;233;72;167m═\x1b[38;2;236;72;153m╝\x1b[39m" +) + +# Minimum terminal width before falling back to the compact mark. +_FULL_MIN_WIDTH = 60 + +_ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]") + +_TAGLINE = "Maximizing Efficient Operations per Watt \u2014 Metal kernels for Apple Silicon" + + +def _strip_ansi(text: str) -> str: + return _ANSI_RE.sub("", text) + + +def _supports_color(stream, force_color) -> bool: + if force_color is not None: + return force_color + # Respect the de-facto NO_COLOR / FORCE_COLOR conventions. + if os.environ.get("NO_COLOR"): + return False + if os.environ.get("FORCE_COLOR"): + return True + return bool(getattr(stream, "isatty", lambda: False)()) + + +def _term_width(width, stream) -> int: + if width is not None: + return width + try: + return shutil.get_terminal_size().columns + except Exception: + return 80 + + +def render_banner(force_color: bool | None = None, width: int | None = None, + stream=None) -> str: + """Return the banner string (compact or full) honoring width + color. + + Colors are stripped when the destination is not a TTY (or NO_COLOR is set), + so piped / redirected output stays clean. + """ + stream = stream if stream is not None else sys.stdout + cols = _term_width(width, stream) + art = BANNER if cols >= _FULL_MIN_WIDTH else BANNER_COMPACT + if not _supports_color(stream, force_color): + art = _strip_ansi(art) + return art + + +def print_banner(stream=None, force_color: bool | None = None, + width: int | None = None) -> None: + """Print the SK banner to ``stream`` (default stdout).""" + stream = stream if stream is not None else sys.stdout + stream.write(render_banner(force_color=force_color, width=width, stream=stream)) + stream.write("\n") + try: + stream.flush() + except Exception: + pass + + +if __name__ == "__main__": + print_banner() diff --git a/SuperKittens/cli/logo_preview.png b/SuperKittens/cli/logo_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..ac43e90f00c7870d0334051ca019d33d0417cdc0 GIT binary patch literal 22133 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 literal 0 HcmV?d00001 diff --git a/SuperKittens/scripts/download_models.sh b/SuperKittens/scripts/download_models.sh new file mode 100755 index 0000000..b7e897b --- /dev/null +++ b/SuperKittens/scripts/download_models.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# SuperKittens model downloader — ONE script, ONE clear list. +# +# Mirrors what `sk ` does, as a standalone reference. Downloads land in +# weights// (override with SK_WEIGHTS_ROOT) — exactly where the +# inference registry looks — so `api.load("")` resolves with no +# symlinks. Each model also gets its tokenizer/config pulled from the canonical +# HF repo (GGUF repos usually omit those). +# +# Usage: +# scripts/download_models.sh # list models and exit +# scripts/download_models.sh qwen3-4b-q4km # download one (or several) +# scripts/download_models.sh --all # download everything +# +# Requires huggingface-cli (`pip install huggingface_hub`). Gated repos (Gemma) +# need `huggingface-cli login` first. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DEST_ROOT="${SK_WEIGHTS_ROOT:-$ROOT/weights}" + +# variant | gguf source repo | file (or *) | weight_dir +# --- Qwen3 (first-party GGUF repos: -GGUF) ------------------------------------------------------------------------- +read -r -d '' MODELS <<'EOF' || true +qwen3-0.6b | Qwen/Qwen3-0.6B-GGUF | Qwen3-0.6B-Q8_0.gguf | Qwen3-0.6B +qwen3-1.7b | Qwen/Qwen3-1.7B-GGUF | Qwen3-1.7B-Q8_0.gguf | Qwen3-1.7B-GGUF +qwen3-4b | Qwen/Qwen3-4B-GGUF | Qwen3-4B-Q8_0.gguf | Qwen3-4B-GGUF +qwen3-4b-q4km | Qwen/Qwen3-4B-GGUF | Qwen3-4B-Q4_K_M.gguf | Qwen3-4B-GGUF +qwen3-8b | Qwen/Qwen3-8B-GGUF | Qwen3-8B-Q8_0.gguf | Qwen3-8B-GGUF +qwen3-8b-q4km | Qwen/Qwen3-8B-GGUF | Qwen3-8B-Q4_K_M.gguf | Qwen3-8B-GGUF +qwen3-14b | Qwen/Qwen3-14B-GGUF | Qwen3-14B-Q8_0.gguf | Qwen3-14B-GGUF +qwen3-14b-q4km | Qwen/Qwen3-14B-GGUF | Qwen3-14B-Q4_K_M.gguf | Qwen3-14B-GGUF +qwen3-32b | Qwen/Qwen3-32B-GGUF | Qwen3-32B-Q8_0.gguf | Qwen3-32B-GGUF +mistral-7b-v0.3 | bartowski/Mistral-7B-Instruct-v0.3-GGUF | Mistral-7B-Instruct-v0.3-Q4_K_M.gguf | Mistral-7B-Instruct-v0.3-GGUF +nemotron-nano-8b | bartowski/nvidia_Llama-3.1-Nemotron-Nano-8B-v1-GGUF | nvidia_Llama-3.1-Nemotron-Nano-8B-v1-Q4_K_M.gguf | Llama-3.1-Nemotron-Nano-8B-v1-GGUF +gemma4-12b-unified | google/gemma-4-12B-it-GGUF | gemma-4-12B-it-Q4_K_M.gguf | gemma-4-12B-it-GGUF +gemma4-e2b | google/gemma-4-E2B-it | * | gemma-4-E2B-it +gemma4-e4b | google/gemma-4-E4B-it | * | gemma-4-E4B-it +mamba2-130m | AntonV/mamba2-130m-hf | * | mamba2-130m-hf +deepseek-v2-lite | deepseek-ai/DeepSeek-V2-Lite | * | DeepSeek-V2-Lite +EOF + +# canonical repo (for tokenizer/config) keyed by variant +declare -A CANON=( + [qwen3-0.6b]=Qwen/Qwen3-0.6B [qwen3-1.7b]=Qwen/Qwen3-1.7B [qwen3-4b]=Qwen/Qwen3-4B + [qwen3-4b-q4km]=Qwen/Qwen3-4B [qwen3-8b]=Qwen/Qwen3-8B [qwen3-8b-q4km]=Qwen/Qwen3-8B + [qwen3-14b]=Qwen/Qwen3-14B [qwen3-14b-q4km]=Qwen/Qwen3-14B [qwen3-32b]=Qwen/Qwen3-32B + [mistral-7b-v0.3]=mistralai/Mistral-7B-Instruct-v0.3 + [nemotron-nano-8b]=nvidia/Llama-3.1-Nemotron-Nano-8B-v1 + [gemma4-12b-unified]=google/gemma-4-12B-it ) + +if [[ $# -eq 0 ]]; then + echo "Available models (pass one or more, or --all):" + echo "$MODELS" | awk -F'|' 'NF{printf " %-20s %s\n",$1,$2}' + exit 0 +fi + +command -v huggingface-cli >/dev/null 2>&1 || { echo "ERROR: pip install huggingface_hub" >&2; exit 1; } + +WANT=("$@"); ALL=0; [[ "${1:-}" == "--all" ]] && ALL=1 +want(){ [[ $ALL -eq 1 ]] && return 0; for w in "${WANT[@]}"; do [[ "$w" == "$1" ]] && return 0; done; return 1; } + +echo "$MODELS" | while IFS='|' read -r v repo file dir; do + v="$(echo "$v"|xargs)"; [[ -z "$v" ]] && continue + repo="$(echo "$repo"|xargs)"; file="$(echo "$file"|xargs)"; dir="$(echo "$dir"|xargs)" + want "$v" || continue + out="$DEST_ROOT/$dir"; mkdir -p "$out" + echo "==> $v ($repo)" + if [[ "$file" == "*" ]]; then huggingface-cli download "$repo" --local-dir "$out" + elif [[ -f "$out/$file" ]]; then echo " have $file" + else huggingface-cli download "$repo" "$file" --local-dir "$out"; fi + canon="${CANON[$v]:-}" + if [[ -n "$canon" ]]; then + for t in tokenizer.json tokenizer_config.json config.json generation_config.json; do + [[ -f "$out/$t" ]] || huggingface-cli download "$canon" "$t" --local-dir "$out" 2>/dev/null || true + done + fi + echo " -> $out" +done +echo "Done." diff --git a/bin/sk b/bin/sk new file mode 100755 index 0000000..6bafe6f --- /dev/null +++ b/bin/sk @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# SuperKittens CLI launcher: `sk launch`, `sk `, `sk talk`. +# +# Auto-discovers the repo root (this script lives in /bin/), points the +# native runtime at the prebuilt dylib + runtime-compiled Metal kernels, and +# routes the weights dir to /weights. All env vars are overridable. +set -euo pipefail + +# Resolve this script's real path even when invoked via a PATH symlink +# (e.g. /opt/homebrew/bin/sk -> /bin/sk), so ROOT points at the repo. +SRC="${BASH_SOURCE[0]}" +while [ -L "$SRC" ]; do + DIR="$(cd "$(dirname "$SRC")" && pwd)" + SRC="$(readlink "$SRC")" + [ "${SRC#/}" = "$SRC" ] && SRC="$DIR/$SRC" +done +SELF="$(cd "$(dirname "$SRC")" && pwd)" +ROOT="${SK_HOME:-$(cd "$SELF/.." && pwd)}" +cd "$ROOT" + +PY="${SK_PYTHON:-}" +if [ -z "$PY" ]; then + if [ -x "$HOME/sk-venv/bin/python" ]; then PY="$HOME/sk-venv/bin/python" + else PY="$(command -v python3)"; fi +fi + +export SK_DYLIB="${SK_DYLIB:-$ROOT/build/libsk.dylib}" +export SK_METALLIB="${SK_METALLIB:-/nonexistent_force_source_fallback.metallib}" +export SK_WEIGHTS_ROOT="${SK_WEIGHTS_ROOT:-$ROOT/weights}" + +SKM="SuperKittens/kernels"; QW="SuperKittens/models/qwen" +export SK_METAL_SRC_FALLBACK="${SK_METAL_SRC_FALLBACK:-\ +$SKM/utils/layernorm/layernorm.metal:$SKM/utils/rmsnorm/rmsnorm_t1.metal:\ +$SKM/gemm/fp16/gemm.metal:$SKM/gemm/fp16/gemv.metal:$SKM/gemm/fp16/gemv_t.metal:\ +$SKM/gemm/fp16/gemv_t_2dtile.metal:$SKM/gemm/q8_0_matvec.metal:$SKM/gemm/q4k_matvec.metal:\ +$SKM/gemm/q6k_matvec.metal:$SKM/fusion/gemv_swiglu_m1.metal:\ +$SKM/fusion/q8_0_swiglu_prenorm_m1.metal:$SKM/gemm/q8_0_swiglu_m1.metal:\ +$SKM/fusion/add_rmsnorm.metal:$SKM/fusion/gated_mlp.metal:$SKM/fusion/silu_mul.metal:\ +$SKM/ops/split/split_packed.metal:$QW/qwen_rope.metal:$SKM/attn/attn.metal:\ +$SKM/ops/kv_cache/kv_cache.metal:$SKM/ops/add/add.metal:$SKM/ops/transpose/seq_head.metal:\ +$SKM/utils/embedding/embedding.metal:$SKM/ops/sample/sample.metal:\ +$SKM/ops/sample/argmax_2pass.metal}" + +exec "$PY" -m SuperKittens.cli "$@"