Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -30,46 +30,62 @@ 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:

| Env | Default | Purpose |
|---|---|---|
| `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 |

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand All @@ -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
Expand Down Expand Up @@ -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=$!
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

![actop live during an Ollama inference run: the single-column stack layout switching chart glyphs and revealing the process panel, with live E-CPU/P-CPU/GPU/ANE utilization, per-core frequency, memory bandwidth, power charts, and a watt-attributed PWR column on Apple Silicon](images/actop-demo.gif)
![actop live during a llama.cpp inference run: the single-column stack layout switching chart glyphs and revealing the process panel, with live E-CPU/P-CPU/GPU/ANE utilization, per-core frequency, memory bandwidth, power charts, and a watt-attributed PWR column on Apple Silicon](images/actop-demo.gif)

*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).*

Expand Down
Binary file added cover/actop-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading