diff --git a/.claude/skills/record-tui-gif/SKILL.md b/.agents/skills/record-tui-gif/SKILL.md similarity index 69% rename from .claude/skills/record-tui-gif/SKILL.md rename to .agents/skills/record-tui-gif/SKILL.md index a998d8f..39790db 100644 --- a/.claude/skills/record-tui-gif/SKILL.md +++ b/.agents/skills/record-tui-gif/SKILL.md @@ -1,6 +1,6 @@ --- name: record-tui-gif -description: Record the actop TUI as an animated GIF with vhs, optionally under a real GPU workload so the gauges move (for the README hero / launch demos). Covers the vhs tape (sizing, off-camera warm-up so the first frame is already live, on-camera glyph/layout/process toggles), the ollama-router GPU driver that lights up GPU/ANE/bandwidth/power, and the record.sh orchestrator that starts the workload, records, and always stops it. Use when refreshing the hero GIF after a TUI/layout change or producing a launch capture. For a *still* ASCII diagram in the docs use capture-tui-diagram instead. +description: Record the actop TUI as an animated GIF with vhs, optionally under a real GPU workload so the gauges move (for the README hero / launch demos). Covers the vhs tape (sizing, off-camera warm-up so the first frame is already live, on-camera glyph/layout/process toggles), the llama.cpp / ollama GPU driver that lights up GPU/ANE/bandwidth/power, and the record.sh orchestrator that starts the workload, records, and always stops it. Use when refreshing the hero GIF after a TUI/layout change or producing a launch capture. For a *still* ASCII diagram in the docs use capture-tui-diagram instead. --- # record-tui-gif @@ -30,10 +30,11 @@ tape. actop monitors the whole system, so that's fine. From anywhere in the repo: ```bash -bash .claude/skills/record-tui-gif/record.sh +bash .agents/skills/record-tui-gif/record.sh ``` -This drives a GPU workload against the ollama-router, records the bundled +This drives a GPU workload against the llama.cpp router (llamacpp-first default; +the ollama-router is the fully supported fallback), records the bundled `actop-demo.tape` → `images/actop-demo.gif`, and **always stops the workload** on exit (EXIT/INT/TERM trap — verified no orphans). Useful env overrides: @@ -41,8 +42,9 @@ exit (EXIT/INT/TERM trap — verified no orphans). Useful env overrides: |---|---|---| | `TAPES` | bundled `actop-demo.tape` | space-separated tapes to record in one session | | `SKIP_WORKLOAD` | `0` | `1` = record with idle gauges (no router needed) | -| `ROUTER_URL` | `http://localhost:11433` | ollama-router base URL | -| `MODEL` | `qwen3.6:35b-a3b-agentic` | model to drive | +| `ROUTER_URL` | `http://localhost:9040` | llamacpp router base URL (`:9040`); ollama fallback `:11433` | +| `MODEL` | `qwen3.6:35b-a3b` | model to drive (llamacpp serves the original weights) | +| `API` | `openai` | wire protocol — llama.cpp needs `openai`; ollama uses native `ollama` | | `CONCURRENCY` | `2` | parallel streams — match the router's backend count | | `NUM_PREDICT` | `4096` | tokens/request; long = sustained load | | `RAMP_SECONDS` | `8` | GPU spin-up wait before recording | @@ -50,26 +52,40 @@ exit (EXIT/INT/TERM trap — verified no orphans). Useful env overrides: Examples: ```bash +# Default: llama.cpp router, OpenAI protocol, original qwen model: +bash .agents/skills/record-tui-gif/record.sh # Heavier load, longer generations: -CONCURRENCY=3 NUM_PREDICT=8192 bash .claude/skills/record-tui-gif/record.sh +CONCURRENCY=3 NUM_PREDICT=8192 bash .agents/skills/record-tui-gif/record.sh +# Ollama fallback (native /api/generate, agentic profile): +ROUTER_URL=http://localhost:11433 MODEL=qwen3.6:35b-a3b-agentic API=ollama \ + bash .agents/skills/record-tui-gif/record.sh # Record a custom tape with idle gauges (e.g. a quick layout check): -SKIP_WORKLOAD=1 TAPES=tmp/mytape.tape bash .claude/skills/record-tui-gif/record.sh +SKIP_WORKLOAD=1 TAPES=tmp/mytape.tape bash .agents/skills/record-tui-gif/record.sh ``` ## The GPU workload driver -`gpu_workload.py` (bundled) drives an Ollama-compatible endpoint to keep the GPU -busy. Stdlib-only (urllib), so no venv installs. It streams long generations -across N concurrent workers until Ctrl-C or `--duration`. +`gpu_workload.py` (bundled) drives an Ollama- or OpenAI-compatible endpoint to +keep the GPU busy. Stdlib-only (urllib), so no venv installs. It streams long +generations across N concurrent workers until Ctrl-C or `--duration`. ```bash -.venv/bin/python .claude/skills/record-tui-gif/gpu_workload.py \ +# llama.cpp (default): OpenAI protocol against the llamacpp router +.venv/bin/python .agents/skills/record-tui-gif/gpu_workload.py \ + --url http://localhost:9040 --model qwen3.6:35b-a3b \ + --api openai --concurrency 2 --num-predict 4096 +# ollama fallback: native /api/generate (llama.cpp 404s on this path) +.venv/bin/python .agents/skills/record-tui-gif/gpu_workload.py \ --url http://localhost:11433 --model qwen3.6:35b-a3b-agentic \ - --concurrency 2 --num-predict 4096 + --api ollama --concurrency 2 --num-predict 4096 ``` Notes: -- **Check the router first**: `(cd ~/workspace_genai/snippets-genai && .venv/bin/python llm_ollama/ollama_router.py status)` — the router fans across backends on 11434/11435, so `--concurrency 2` lights up both. +- **Check the router first**: llamacpp — `./router.sh status` in + `~/workspace_genai/snippets-genai/llm_llamacpp`; ollama — + `.venv/bin/python llm_ollama/ollama_router.py status`. llama.cpp only serves + the original `qwen3.6-35b-a3b` weights (sampling tuned to the agentic profile); + the ollama router additionally exposes the `-agentic` model tag. - With large `--num-predict`, the stats line can read `requests=0` for the whole window — that's expected: each generation runs longer than the capture, so both workers stay mid-generation (GPU pegged the entire time). `errors=0` is the diff --git a/.claude/skills/record-tui-gif/actop-demo.tape b/.agents/skills/record-tui-gif/actop-demo.tape similarity index 100% rename from .claude/skills/record-tui-gif/actop-demo.tape rename to .agents/skills/record-tui-gif/actop-demo.tape diff --git a/.claude/skills/record-tui-gif/gpu_workload.py b/.agents/skills/record-tui-gif/gpu_workload.py similarity index 100% rename from .claude/skills/record-tui-gif/gpu_workload.py rename to .agents/skills/record-tui-gif/gpu_workload.py diff --git a/.claude/skills/record-tui-gif/record.sh b/.agents/skills/record-tui-gif/record.sh similarity index 74% rename from .claude/skills/record-tui-gif/record.sh rename to .agents/skills/record-tui-gif/record.sh index 3c3a6b0..c9f8518 100755 --- a/.claude/skills/record-tui-gif/record.sh +++ b/.agents/skills/record-tui-gif/record.sh @@ -3,20 +3,22 @@ # Record the actop TUI to a GIF with vhs, optionally under a real GPU workload # so the gauges actually move. Orchestrates the full sequence and ALWAYS stops # the workload on exit (success, error, or Ctrl-C): -# 1. start a GPU workload against the ollama-router (unless SKIP_WORKLOAD=1) +# 1. start a GPU workload against the llama.cpp router (unless SKIP_WORKLOAD=1) +# — the ollama-router is the fully supported fallback, see env below # 2. wait for the GPU to ramp # 3. record each tape with vhs (retries the ttyd-startup race under load) # 4. stop recording (vhs exits on its own) # 5. stop the GPU workload # # Run from anywhere inside the repo: -# bash .claude/skills/record-tui-gif/record.sh +# bash .agents/skills/record-tui-gif/record.sh # # Config via env: # TAPES="tmp/a.tape tmp/b.tape" tapes to record (default: bundled actop-demo.tape) # SKIP_WORKLOAD=1 record without driving the GPU (idle gauges) -# ROUTER_URL=http://localhost:11433 ollama-router base URL +# ROUTER_URL=http://localhost:11433 router base URL (ollama:11433, llamacpp:9040) # MODEL=qwen3.6:35b-a3b-agentic model to drive +# API=ollama|openai wire protocol (llama.cpp backends need openai) # CONCURRENCY=2 NUM_PREDICT=4096 workload knobs (match router backend count) # RAMP_SECONDS=8 GPU spin-up wait before recording # @@ -27,8 +29,12 @@ REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)" cd "$REPO_ROOT" # ---- Config (override via env) -------------------------------------------- -ROUTER_URL="${ROUTER_URL:-http://localhost:11433}" -MODEL="${MODEL:-qwen3.6:35b-a3b-agentic}" +# Defaults are llama.cpp-first (OpenAI wire protocol on the llamacpp router at +# :9040, original-model weights). The ollama-router fallback is fully supported: +# ROUTER_URL=http://localhost:11433 MODEL=qwen3.6:35b-a3b-agentic API=ollama +ROUTER_URL="${ROUTER_URL:-http://localhost:9040}" +MODEL="${MODEL:-qwen3.6:35b-a3b}" +API="${API:-openai}" # openai (/v1/chat/completions, llama.cpp) or ollama (/api/generate) CONCURRENCY="${CONCURRENCY:-2}" NUM_PREDICT="${NUM_PREDICT:-4096}" # long generations = sustained GPU load RAMP_SECONDS="${RAMP_SECONDS:-8}" # let the model start generating before vhs @@ -62,16 +68,23 @@ if [[ "$SKIP_WORKLOAD" == 1 ]]; then echo ">> SKIP_WORKLOAD=1 — recording with idle gauges." else [[ -x "$PY" ]] || { echo "!! .venv python missing at $PY (activate/create the repo venv)"; exit 1; } - if ! curl -sf "$ROUTER_URL/api/tags" >/dev/null 2>&1; then - echo "!! ollama-router not reachable at $ROUTER_URL" - echo " start it, or re-run with SKIP_WORKLOAD=1 to record idle." - exit 1 + # Health probe: try the native Ollama /api/tags first, then fall back to + # the router-level /health (the llama.cpp router's catch-all proxy 400s on + # body-less GETs, so /v1/models is not a usable probe). + HEALTH_URL="$ROUTER_URL/api/tags" + if ! curl -sf "$HEALTH_URL" >/dev/null 2>&1; then + HEALTH_URL="$ROUTER_URL/health" + if ! curl -sf "$HEALTH_URL" >/dev/null 2>&1; then + echo "!! router not reachable at $ROUTER_URL" + echo " start it, or re-run with SKIP_WORKLOAD=1 to record idle." + exit 1 + fi fi echo " router OK @ $ROUTER_URL, model=$MODEL" echo ">> Starting GPU workload (concurrency=$CONCURRENCY, num_predict=$NUM_PREDICT)…" set -m # job control → child gets its own process group "$PY" "$WORKLOAD" \ - --url "$ROUTER_URL" --model "$MODEL" \ + --url "$ROUTER_URL" --model "$MODEL" --api "$API" \ --concurrency "$CONCURRENCY" --num-predict "$NUM_PREDICT" \ >"$WORKLOAD_LOG" 2>&1 & WORKLOAD_PID=$! diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3185c..4761787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ This project follows a Keep a Changelog-style format and uses version tags for r ## [Unreleased] +### Changed +- **README hero GIF re-recorded** (`images/actop-demo.gif`) — refreshed after the + GPU% process-table column landed; recorded under a live llama.cpp workload + (OpenAI wire protocol on the llamacpp router, original `qwen3.6-35b-a3b` + weights) instead of the ollama-router. `record-tui-gif` skill moved from + `.claude/skills/` to `.agents/skills/`; `record.sh` now defaults to the + llama.cpp router (`API=openai`), with the ollama-router preserved as a + documented fallback (`API=ollama` + native `/api/generate`). Cover + (`cover/`) re-deployed with the new GIF. + ## [1.6.5] - 2026-08-09 ### Added diff --git a/README.md b/README.md index b16bb9e..62ae106 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ and thermals — plus a **Python API** (`Monitor` / `Profiler`, `to_pandas()`) s can instrument your *own* local LLM / MLX / CoreML inference and training runs with SoC-accurate power and energy context. - + *Above: actop live under a local LLM run — the `stack` layout with braille then block sparklines, then the process panel (`t`) with its watt-attributed `PWR` column. Below: the default two-column `grid` layout (`l` cycles between them).* diff --git a/cover/actop-demo.gif b/cover/actop-demo.gif new file mode 100644 index 0000000..06a329e Binary files /dev/null and b/cover/actop-demo.gif differ diff --git a/cover/index.html b/cover/index.html new file mode 100644 index 0000000..4e7c7ac --- /dev/null +++ b/cover/index.html @@ -0,0 +1,274 @@ + + +
+ + ++ Watch your Apple Silicon Mac the way it actually works — sudoless, + in-process, for M1–M4. +
+
+
+ actop reads Apple Silicon power, frequency, and residency metrics directly from
+ IOReport via Python ctypes — the same library powermetrics uses,
+ minus the sudo, subprocesses, and temp files.
+
Per-cluster and per-core utilization, DVFS P-state frequency, and power for the P-CPU, E-CPU, GPU, and Neural Engine.
+Live GB/s against the SoC's peak, with a bandwidth-saturation alert and a MEM-BOUND indicator.
+CPU/GPU die temperatures from the Apple SMC and the macOS thermal state, with a THROTTLING indicator.
+A watt-attributed PWR column — per-process power and energy attribution, unique among the sudoless *top tools.
+Monitor / Profiler with to_pandas() — instrument your own LLM / MLX / CoreML runs from Python.
16 built-in M1–M4 profiles with reference wattage and bandwidth for stable, cross-session chart scaling.
+Then run actop.
brew tap binlecode/actop
+brew install binlecode/actop/actop
+ uv tool install \
+ git+https://github.com/binlecode/actop.git
+ pip install \
+ git+https://github.com/binlecode/actop.git
+ + Wrap any workload to get a pandas frame of power, frequency, residency, and + cumulative session energy — no TUI needed. +
+from actop import Profiler
+
+with Profiler() as p:
+ run_my_inference() # your workload
+
+df = p.to_pandas() # rows = samples; cols = power/freq/residency/energy
+ Follows the Unix *top lineage: the sudoless, programmable, Python-native monitor for Apple Silicon.