Newer
Older
navi-1 / scripts / ngram_busy_probe.py
#!/usr/bin/env python3
"""Sample CPU and GPU load while llama-server generates.

Answers one question: during tg, is the CPU pinned (compute-bound on the
4 Ivy Bridge cores) or is everything waiting on memory/PCIe?
"""
import glob
import json
import os
import subprocess
import sys
import threading
import time
import http.client

BIN = os.path.expanduser("~/llama/llama-b11960/llama-server")
MODEL = os.path.expanduser("~/models/Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS.gguf")
PORT = 8299
NCPU = os.cpu_count()

PROMPT = ("You are a coding agent. Explain, step by step and in detail, how to "
          "diagnose why a systemd service keeps restarting on a remote Ubuntu "
          "server, which commands to run, and how to read each output. ") * 8


def read_cpu(pid):
    with open(f"/proc/{pid}/stat") as f:
        parts = f.read().split()
    return (int(parts[13]) + int(parts[14])) / os.sysconf("SC_CLK_TCK")


def gpu_busy():
    for p in glob.glob("/sys/class/drm/card*/device/gpu_busy_percent"):
        try:
            return int(open(p).read())
        except (OSError, ValueError):
            pass
    return None


def vram_mb():
    for p in glob.glob("/sys/class/drm/card*/device/mem_info_vram_used"):
        try:
            return int(open(p).read()) // (1024 * 1024)
        except (OSError, ValueError):
            pass
    return None


def freqs():
    out = []
    for p in sorted(glob.glob("/sys/devices/system/cpu/cpu[0-9]*/cpufreq/scaling_cur_freq")):
        try:
            out.append(int(open(p).read()) // 1000)
        except (OSError, ValueError):
            pass
    return out


def governor():
    try:
        return open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor").read().strip()
    except OSError:
        return "?"


def post(path, payload, timeout=1800):
    c = http.client.HTTPConnection("127.0.0.1", PORT, timeout=timeout)
    c.request("POST", path, body=json.dumps(payload),
              headers={"Content-Type": "application/json"})
    r = c.getresponse()
    body = r.read()
    c.close()
    if r.status != 200:
        raise RuntimeError(f"{path} {r.status}: {body[:300]!r}")
    return json.loads(body)


def main():
    flags = sys.argv[1:] or ["--spec-type", "draft-mtp", "--spec-draft-n-max", "1"]
    log = open(os.path.expanduser("~/ngram/smoke/busy.log"), "wb")
    proc = subprocess.Popen(
        [BIN, "-m", MODEL, "--port", str(PORT), "-c", "8192", "-ngl", "99",
         "--n-cpu-moe", "999", "-fa", "on", "-ctk", "q8_0", "-ctv", "q8_0",
         "-t", "4", "--no-webui", *flags],
        stdout=log, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
    try:
        for _ in range(600):
            if proc.poll() is not None:
                raise SystemExit("server died")
            try:
                c = http.client.HTTPConnection("127.0.0.1", PORT, timeout=5)
                c.request("GET", "/health")
                ok = c.getresponse().status == 200
                c.close()
                if ok:
                    break
            except OSError:
                pass
            time.sleep(1)

        post("/completion", {"prompt": PROMPT, "n_predict": 8,
                             "temperature": 0, "cache_prompt": False}, 600)

        print(f"nproc={NCPU} governor={governor()} freqs(MHz)={freqs()}")
        print(f"vram idle={vram_mb()} MB  gpu_busy={gpu_busy()}%")

        samples = []
        result = {}

        def run():
            result["out"] = post("/completion", {
                "prompt": PROMPT, "n_predict": 256, "temperature": 0,
                "cache_prompt": False}, 1800)

        t = threading.Thread(target=run)
        cpu0, t0 = read_cpu(proc.pid), time.time()
        t.start()
        while t.is_alive():
            time.sleep(0.25)
            samples.append((time.time(), read_cpu(proc.pid), gpu_busy(), vram_mb()))
        t.join()
        cpu1, t1 = read_cpu(proc.pid), time.time()

        tm = result["out"]["timings"]
        pred_s = tm["predicted_ms"] / 1000
        pp_s = tm["prompt_ms"] / 1000
        gen = [s for s in samples if s[0] >= t0 + pp_s]

        def stat(rows, idx):
            vals = [r[idx] for r in rows if r[idx] is not None]
            return (sum(vals) / len(vals), min(vals), max(vals)) if vals else (None,) * 3

        print(f"\nprompt {tm['prompt_n']} tok in {pp_s:.1f}s ({tm['prompt_n']/pp_s:.1f} t/s), "
              f"gen {tm['predicted_n']} tok in {pred_s:.1f}s "
              f"({tm['predicted_n']/pred_s:.2f} t/s)")
        print(f"total process cpu: {cpu1-cpu0:.1f}s over {t1-t0:.1f}s wall "
              f"= {(cpu1-cpu0)/(t1-t0)*100:.0f}% of {NCPU*100}%")

        # CPU% inside the generation window, from consecutive samples.
        rows = gen
        if len(rows) >= 2:
            dc = rows[-1][1] - rows[0][1]
            dw = rows[-1][0] - rows[0][0]
            print(f"during tg: cpu {dc/dw*100:.0f}% of {NCPU*100}%")
        for name, idx in (("gpu_busy%", 2), ("vram MB", 3)):
            avg, lo, hi = stat(rows, idx)
            if avg is not None:
                print(f"during tg: {name} avg {avg:.0f} min {lo} max {hi}")
        print(f"freqs after (MHz)={freqs()}")
    finally:
        proc.terminate()
        try:
            proc.wait(timeout=30)
        except subprocess.TimeoutExpired:
            proc.kill()
        log.close()


if __name__ == "__main__":
    main()