b7fd69815e
At 8 workers the GPU sat at ~5% util / <5GB VRAM — the pipeline is I/O-bound (downloading + decoding images over HTTP), so the GPU starves until many workers overlap that I/O. Raise MAX_CONCURRENCY 8→32 and make the UI worker control a number input (reaching 32 by ±1 was tedious); the cap is reported via /status so the UI clamps to it. Also size the shared requests pool (pool_maxsize=64) — the default 10 would have throttled 32 workers + spammed "connection pool is full". Verified by running; watch GPU util/VRAM climb as you dial up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
118 lines
4.5 KiB
Python
118 lines
4.5 KiB
Python
"""FastAPI control surface for the agent (served on localhost).
|
||
|
||
Start / stop the worker pool, tune the worker count live (trades desktop
|
||
responsiveness for throughput), and watch GPU load + progress + the server-side
|
||
queue. Config is env-seeded; the worker count is adjustable here on the fly.
|
||
"""
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.responses import HTMLResponse, JSONResponse
|
||
|
||
from .config import Config
|
||
from .gpu import read_gpu
|
||
from .worker import Worker
|
||
|
||
cfg = Config.from_env()
|
||
worker = Worker(cfg)
|
||
app = FastAPI(title="FabledCurator GPU agent")
|
||
|
||
|
||
@app.get("/", response_class=HTMLResponse)
|
||
def index() -> str:
|
||
return _PAGE
|
||
|
||
|
||
@app.post("/start")
|
||
def start():
|
||
worker.start()
|
||
return JSONResponse(worker.status())
|
||
|
||
|
||
@app.post("/stop")
|
||
def stop():
|
||
worker.stop()
|
||
return JSONResponse(worker.status())
|
||
|
||
|
||
@app.post("/concurrency")
|
||
async def concurrency(request: Request):
|
||
body = await request.json()
|
||
worker.set_concurrency(int(body.get("value", 1)))
|
||
return JSONResponse(worker.status())
|
||
|
||
|
||
@app.get("/status")
|
||
def status():
|
||
s = worker.status()
|
||
s["fc_url"] = cfg.fc_url
|
||
s["configured"] = bool(cfg.token)
|
||
s["gpu"] = read_gpu()
|
||
try:
|
||
s["queue"] = worker.client.queue_status()
|
||
except Exception:
|
||
s["queue"] = None
|
||
return JSONResponse(s)
|
||
|
||
|
||
_PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
||
<title>FabledCurator GPU agent</title>
|
||
<style>
|
||
body{font:14px system-ui;margin:2rem;max-width:680px;background:#14171a;color:#e8e8e8}
|
||
h1{font-size:18px} button{font:14px system-ui;padding:.5rem 1rem;border:0;border-radius:6px;
|
||
margin-right:.5rem;cursor:pointer;color:#fff} .start{background:#2e7d32}.stop{background:#b3261e}
|
||
.step{background:#33373b;padding:.4rem .7rem;font-weight:700}
|
||
.stat{display:inline-block;margin-right:1.5rem;vertical-align:top}
|
||
.n{font-size:22px;font-weight:700} code{background:#222;padding:2px 6px;border-radius:4px}
|
||
.q,.gpu{margin-top:1rem;color:#9aa} .bar{height:8px;border-radius:4px;background:#222;overflow:hidden;
|
||
max-width:320px;margin-top:4px} .bar>i{display:block;height:100%;background:#3f7d3f}
|
||
.row{margin:.8rem 0}
|
||
</style></head><body>
|
||
<h1>FabledCurator GPU agent</h1>
|
||
<p>FC: <code id=fc>—</code> · token <code id=cfg>—</code></p>
|
||
<div class=row>
|
||
<button class=start onclick=act('start')>Start</button>
|
||
<button class=stop onclick=act('stop')>Stop</button>
|
||
</div>
|
||
<div class=row>
|
||
workers
|
||
<button class=step onclick=setc(-1)>−</button>
|
||
<input id=conc type=number min=1 value=1
|
||
style="width:3.5rem;font:700 16px system-ui;text-align:center;background:#222;color:#e8e8e8;border:1px solid #444;border-radius:6px;padding:.3rem"
|
||
onchange="setv(this.value)">
|
||
<button class=step onclick=setc(1)>+</button>
|
||
<span class=cap style=color:#9aa>(more = overlap I/O, fill the GPU) max <b id=capn>8</b></span>
|
||
</div>
|
||
<div class=row>
|
||
<span class=stat><span class=n id=state>stopped</span><br>state</span>
|
||
<span class=stat><span class=n id=active>0</span><br>active now</span>
|
||
<span class=stat><span class=n id=done>0</span><br>processed</span>
|
||
<span class=stat><span class=n id=err>0</span><br>errors</span>
|
||
</div>
|
||
<div class=gpu id=gpu>GPU — …</div>
|
||
<div class=bar><i id=gpubar style=width:0%></i></div>
|
||
<div class=q id=queue></div>
|
||
<script>
|
||
let CAP=8
|
||
async function act(p){await fetch('/'+p,{method:'POST'});refresh()}
|
||
function setc(d){ setv((parseInt(conc.value||'1'))+d) }
|
||
async function setv(v){
|
||
v=Math.max(1,Math.min(CAP,parseInt(v)||1)); conc.value=v
|
||
await fetch('/concurrency',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({value:v})});refresh()
|
||
}
|
||
async function refresh(){
|
||
const s=await (await fetch('/status')).json()
|
||
CAP=s.max_concurrency||8; capn.textContent=CAP
|
||
state.textContent=s.state; active.textContent=s.active; done.textContent=s.processed
|
||
err.textContent=s.errors; fc.textContent=s.fc_url
|
||
if(document.activeElement!==conc) conc.value=s.concurrency
|
||
conc.max=CAP
|
||
cfg.textContent=s.configured?'set':'MISSING'
|
||
if(s.gpu){
|
||
gpu.textContent=`GPU — ${s.gpu.util_pct}% util · VRAM ${s.gpu.mem_used_mb}/${s.gpu.mem_total_mb} MB · ${s.gpu.temp_c}°C`
|
||
gpubar.style.width=Math.round(100*s.gpu.mem_used_mb/s.gpu.mem_total_mb)+'%'
|
||
} else { gpu.textContent='GPU — n/a (CPU fallback?)'; gpubar.style.width='0%' }
|
||
queue.textContent=s.queue?`queue — pending ${s.queue.pending} · in flight ${s.queue.leased} · done ${s.queue.done} · errored ${s.queue.error}`:'queue — unreachable'
|
||
}
|
||
refresh(); setInterval(refresh,3000)
|
||
</script></body></html>"""
|