gpu-scheduler-openenv / server /custom_web.py
SulmanK's picture
Restore full project documentation and add Hugging Face Space integration
09f204f
Raw
History Blame Contribute Delete
49.1 kB
CUSTOM_WEB_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GPU Scheduler Console</title>
<style>
:root{
--bg:#0b1017;
--bg-soft:#0f1621;
--panel:#121a25;
--panel-2:#0f1722;
--panel-3:#0c141d;
--line:#243244;
--line-strong:#31506f;
--text:#e8eef5;
--muted:#8ea1b5;
--accent:#63b3ed;
--accent-soft:rgba(99,179,237,.14);
--good:#4db877;
--warn:#d4a72c;
--bad:#ef6b64;
--high:#ff8a65;
--medium:#d6b348;
--low:#67a3ff;
--shadow:0 18px 44px rgba(0,0,0,.28);
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
--sans:"Segoe UI",Inter,ui-sans-serif,system-ui,sans-serif;
}
*{box-sizing:border-box}
body{
margin:0;
font-family:var(--sans);
background:
radial-gradient(circle at top right, rgba(99,179,237,.08), transparent 28%),
linear-gradient(180deg, #0a0f16 0%, #0b1017 100%);
color:var(--text);
}
.page{max-width:1580px;margin:0 auto;padding:20px 18px 28px}
.hero,.card,.panel,.status-box,.timeline-entry,.inventory-row,.mini-card,.kpi,.detail-card{
background:linear-gradient(180deg, rgba(18,26,37,.96), rgba(13,21,31,.96));
border:1px solid var(--line);
box-shadow:var(--shadow);
}
.hero,.card,.panel{border-radius:14px}
.hero{padding:18px 20px;margin-bottom:16px}
.eyebrow{
display:inline-flex;
align-items:center;
gap:8px;
text-transform:uppercase;
letter-spacing:.12em;
font-size:.72rem;
color:var(--muted);
}
.eyebrow::before{
content:"";
width:8px;
height:8px;
border-radius:999px;
background:var(--accent);
box-shadow:0 0 0 4px rgba(99,179,237,.15);
}
.hero-head{
display:flex;
justify-content:space-between;
gap:16px;
align-items:flex-end;
}
.hero h1{margin:8px 0 6px;font-size:1.55rem;font-weight:650;letter-spacing:.01em}
.hero p,.copy,.muted{color:var(--muted)}
.hero p{margin:0;max-width:920px;line-height:1.45}
.hero-meta{
display:flex;
gap:10px;
flex-wrap:wrap;
justify-content:flex-end;
}
.layout{display:grid;grid-template-columns:296px minmax(0,1fr);gap:16px;align-items:start}
.stack{display:grid;gap:16px}
.sidebar{position:sticky;top:18px}
.card{padding:14px}
.sidebar .card{padding:13px}
.card h2,.panel h2{margin:0 0 12px;font-size:1rem;letter-spacing:.01em}
.card p{margin:0}
.row{display:grid;grid-template-columns:1fr 1fr;gap:9px}
.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:9px}
label{
display:block;
margin-bottom:5px;
color:var(--muted);
font-size:.72rem;
text-transform:uppercase;
letter-spacing:.08em;
}
input,select,button{
width:100%;
min-height:36px;
background:var(--panel-3);
color:var(--text);
border:1px solid var(--line);
border-radius:9px;
padding:8px 10px;
font:inherit;
}
input,select{font-size:.88rem}
button{
cursor:pointer;
font-weight:600;
background:linear-gradient(180deg, rgba(99,179,237,.22), rgba(99,179,237,.08));
border-color:rgba(99,179,237,.45);
}
button.secondary{
background:var(--panel-3);
border-color:var(--line);
color:var(--muted);
}
.meta-strip,.chips{display:flex;gap:8px;flex-wrap:wrap}
.tag,.chip,.tiny-pill{
display:inline-flex;
align-items:center;
gap:6px;
min-height:28px;
padding:5px 9px;
border-radius:999px;
border:1px solid var(--line);
background:rgba(255,255,255,.02);
color:var(--text);
font-size:.78rem;
white-space:nowrap;
}
.tag{color:var(--muted)}
.chip.high,.priority-dot.high,.pill-high{color:var(--high);border-color:rgba(255,138,101,.38)}
.chip.medium,.priority-dot.medium,.pill-medium{color:var(--medium);border-color:rgba(214,179,72,.38)}
.chip.low,.priority-dot.low,.pill-low{color:var(--low);border-color:rgba(103,163,255,.38)}
.good{color:var(--good);border-color:rgba(77,184,119,.38)}
.warn{color:var(--warn);border-color:rgba(212,167,44,.38)}
.bad{color:var(--bad);border-color:rgba(239,107,100,.38)}
.mono{
font-family:var(--mono);
font-variant-numeric:tabular-nums;
font-feature-settings:"tnum" 1;
}
.status-stack,.timeline{display:grid;gap:10px}
.status-box,.timeline-entry,.detail-card,.mini-card{
border-radius:12px;
padding:12px;
}
.status-box strong,.timeline-entry strong,.detail-card strong{display:block;margin-bottom:5px}
.timeline-meta{
display:flex;
justify-content:space-between;
align-items:center;
gap:8px;
margin-bottom:7px;
}
.panel{padding:16px}
.console-head{
display:flex;
justify-content:space-between;
gap:16px;
align-items:flex-start;
margin-bottom:12px;
}
.console-head h2{margin:4px 0 6px;font-size:1.1rem}
.console-subtitle{max-width:850px;color:var(--muted);line-height:1.45}
.kpi-strip{
display:grid;
grid-template-columns:repeat(6,minmax(0,1fr));
gap:8px;
margin-top:12px;
}
.kpi{
border-radius:12px;
padding:10px 11px;
min-height:78px;
}
.kpi small{
display:block;
color:var(--muted);
margin-bottom:8px;
text-transform:uppercase;
letter-spacing:.08em;
font-size:.72rem;
}
.kpi strong{
display:block;
font-size:1.18rem;
line-height:1.05;
margin-bottom:6px;
}
.kpi span{font-size:.8rem;color:var(--muted)}
.ops-grid{
display:grid;
grid-template-columns:minmax(0,1.45fr) minmax(360px,.95fr) minmax(290px,.82fr);
gap:14px;
margin-top:14px;
}
.panel-head{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:12px;
margin-bottom:12px;
}
.panel-head h3{margin:0;font-size:.96rem}
.panel-head small{color:var(--muted)}
.table-shell{
border:1px solid var(--line);
border-radius:12px;
overflow:hidden;
background:rgba(8,13,20,.55);
}
table{width:100%;border-collapse:collapse}
th,td{
padding:11px 12px;
border-bottom:1px solid rgba(36,50,68,.72);
text-align:left;
font-size:.9rem;
vertical-align:middle;
}
th{
color:var(--muted);
background:rgba(255,255,255,.02);
text-transform:uppercase;
letter-spacing:.08em;
font-size:.72rem;
font-weight:600;
}
tr:last-child td{border-bottom:none}
.ops-row{cursor:pointer}
.ops-row td,.inventory-row{transition:background .16s ease,border-color .16s ease,box-shadow .16s ease,transform .16s ease}
.ops-row:hover td,.inventory-row:hover{background:rgba(99,179,237,.07)}
.ops-row.selected,.inventory-row.selected{
background:rgba(99,179,237,.11);
box-shadow:inset 0 0 0 1px rgba(99,179,237,.42),0 0 0 1px rgba(99,179,237,.18);
}
.ops-row.selected td{background:rgba(99,179,237,.1)}
.ops-row.selected td:first-child{box-shadow:inset 3px 0 0 0 var(--accent)}
.ops-row.risk-critical td{background:rgba(239,107,100,.045)}
.ops-row.risk-warning td{background:rgba(212,167,44,.045)}
.priority-cell{display:flex;align-items:center;gap:8px}
.priority-dot{
width:8px;
height:8px;
border-radius:999px;
display:inline-block;
background:currentColor;
box-shadow:0 0 0 4px rgba(255,255,255,.04);
}
.fit-count{
display:inline-flex;
align-items:center;
gap:6px;
padding:3px 8px;
border-radius:999px;
background:rgba(255,255,255,.03);
border:1px solid var(--line);
font-size:.78rem;
color:var(--muted);
}
.slack-cell{display:grid;gap:4px}
.slack-pill,.inventory-meta-pill{
display:inline-flex;
align-items:center;
gap:6px;
padding:3px 8px;
border-radius:999px;
border:1px solid var(--line);
background:rgba(255,255,255,.03);
font-size:.76rem;
width:max-content;
}
.slack-pill.critical{color:var(--bad);border-color:rgba(239,107,100,.42);background:rgba(239,107,100,.08)}
.slack-pill.warning{color:var(--warn);border-color:rgba(212,167,44,.42);background:rgba(212,167,44,.08)}
.slack-pill.stable{color:var(--good);border-color:rgba(77,184,119,.38);background:rgba(77,184,119,.06)}
.cell-subtext{font-size:.75rem;color:var(--muted)}
.inventory{
display:grid;
gap:9px;
}
.inventory-row{
border-radius:12px;
padding:11px;
border:1px solid var(--line);
cursor:pointer;
}
.inventory-row:hover{transform:translateY(-1px);border-color:rgba(99,179,237,.28)}
.inventory-row.selected{border-color:rgba(99,179,237,.42)}
.inventory-head{
display:flex;
justify-content:space-between;
align-items:flex-start;
gap:10px;
margin-bottom:8px;
}
.inventory-head strong{display:block;font-size:.95rem}
.inventory-head small{display:block;color:var(--muted);line-height:1.4}
.capacity-track{
position:relative;
height:12px;
border-radius:999px;
background:#08111a;
border:1px solid rgba(255,255,255,.06);
overflow:hidden;
}
.capacity-fill{
height:100%;
border-radius:999px;
background:linear-gradient(90deg, rgba(99,179,237,.96), rgba(77,184,119,.78));
width:var(--fill,0%);
}
.capacity-fill.busy-hot{
background:linear-gradient(90deg, rgba(239,107,100,.96), rgba(212,167,44,.82));
}
.inventory-foot{
display:flex;
justify-content:space-between;
gap:8px;
margin-top:7px;
color:var(--muted);
font-size:.78rem;
}
.inventory-context{
margin-top:8px;
padding-top:8px;
border-top:1px solid rgba(255,255,255,.06);
display:flex;
justify-content:space-between;
gap:8px;
align-items:center;
}
.inventory-context strong{display:block;font-size:.8rem}
.inventory-context small{display:block;color:var(--muted);line-height:1.35}
.inventory-meta-pill.warn{background:rgba(212,167,44,.08)}
.inventory-meta-pill.good{background:rgba(77,184,119,.06)}
.detail-stack{display:grid;gap:12px}
.detail-label{
color:var(--muted);
text-transform:uppercase;
letter-spacing:.08em;
font-size:.72rem;
margin-bottom:5px;
display:block;
}
.detail-main{font-size:1.02rem;font-weight:620;margin-bottom:8px}
.stat-grid{
display:grid;
grid-template-columns:1fr 1fr;
gap:10px;
margin-top:10px;
}
.mini-card small{
display:block;
color:var(--muted);
margin-bottom:5px;
text-transform:uppercase;
letter-spacing:.08em;
font-size:.7rem;
}
.mini-card strong{font-size:1rem}
.signal-list{
display:grid;
gap:8px;
margin-top:10px;
}
.signal-row{
display:grid;
grid-template-columns:86px 1fr 42px;
gap:8px;
align-items:center;
font-size:.82rem;
}
.signal-row span:first-child{color:var(--muted)}
.signal-track{
height:8px;
border-radius:999px;
overflow:hidden;
background:#08111a;
border:1px solid rgba(255,255,255,.06);
}
.signal-fill{
height:100%;
border-radius:999px;
background:linear-gradient(90deg, rgba(99,179,237,.96), rgba(77,184,119,.78));
width:var(--fill,0%);
}
.spark-shell{
border:1px solid var(--line);
border-radius:12px;
background:rgba(255,255,255,.02);
padding:12px;
}
.spark-head{
display:flex;
justify-content:space-between;
gap:10px;
align-items:flex-end;
margin-bottom:10px;
}
.sparkline{
min-height:78px;
display:grid;
align-items:center;
}
.sparkline svg{width:100%;height:78px;display:block}
.spark-grid line{stroke:rgba(255,255,255,.06);stroke-width:1}
.spark-path{
fill:none;
stroke:var(--accent);
stroke-width:2.5;
stroke-linecap:round;
stroke-linejoin:round;
}
.spark-dot{fill:var(--accent)}
.spark-axis{
display:flex;
justify-content:space-between;
gap:8px;
margin-top:8px;
color:var(--muted);
font-size:.74rem;
}
.footer-grid{
display:grid;
grid-template-columns:1fr 1fr;
gap:12px;
margin-top:14px;
}
.footer-card{
border-radius:12px;
padding:10px 12px;
border:1px solid var(--line);
background:rgba(255,255,255,.02);
min-height:48px;
}
.footer-card h4{margin:0 0 6px;font-size:.84rem}
.footer-list{display:flex;flex-wrap:wrap;gap:8px}
.empty{
color:var(--muted);
font-style:italic;
}
details{
border:1px solid var(--line);
border-radius:12px;
background:var(--panel-2);
}
summary{
cursor:pointer;
padding:12px 14px;
font-weight:600;
}
pre{
margin:0;
padding:14px;
overflow:auto;
white-space:pre-wrap;
word-break:break-word;
font-family:var(--mono);
font-size:.83rem;
color:#cfd8e3;
}
hr{border:none;border-top:1px solid var(--line);margin:14px 0}
@media (max-width:1340px){
.ops-grid{grid-template-columns:minmax(0,1fr)}
.kpi-strip{grid-template-columns:repeat(3,minmax(0,1fr))}
.sidebar{position:static}
}
@media (max-width:980px){
.layout{grid-template-columns:1fr}
.kpi-strip{grid-template-columns:repeat(2,minmax(0,1fr))}
.row,.row3,.stat-grid,.footer-grid{grid-template-columns:1fr}
.hero-head,.console-head{flex-direction:column;align-items:flex-start}
}
@media (max-width:640px){
.page{padding:14px 12px 24px}
.kpi-strip{grid-template-columns:1fr}
th,td{padding:10px 8px}
}
</style>
</head>
<body>
<main class="page">
<section class="hero">
<div class="hero-head">
<div>
<div class="eyebrow">Interactive Scheduler Demo</div>
<h1>GPU Scheduler Console</h1>
<p>Operate the benchmark scenario like a compact placement console: inspect pending work, check eligible GPUs, and dispatch one placement decision at a time.</p>
</div>
<div class="hero-meta">
<span class="tag mono">Environment: openenv</span>
<span class="tag mono">Mode: manual dispatch</span>
</div>
</div>
</section>
<section class="layout">
<aside class="stack sidebar">
<article class="card">
<h2>Placement Controls</h2>
<p class="copy">Reset a scenario, load a placement decision, and dispatch one scheduling tick at a time.</p>
<div class="row" style="margin-top:12px">
<div><label for="episode">Scenario</label><select id="episode"><option value="deadline_crunch">deadline_crunch</option><option value="cost_pressure">cost_pressure</option><option value="wait_for_capacity">wait_for_capacity</option><option value="reserve_large_gpu">reserve_large_gpu</option></select></div>
<div><label for="seed">Seed</label><input id="seed" type="number" min="0" placeholder="optional" /></div>
</div>
<div class="row3" style="margin-top:10px">
<button id="reset" class="secondary">Reset</button>
<button id="refresh" class="secondary">Sync</button>
<button id="step">Dispatch</button>
</div>
<hr />
<div class="row">
<div><label for="action">Decision</label><select id="action"><option value="place">place job</option><option value="defer">defer placement</option></select></div>
<div><label for="job">Pending job</label><select id="job"></select></div>
</div>
<div class="row" style="margin-top:10px">
<div><label for="gpu">Target GPU</label><select id="gpu"></select></div>
<div><label for="rationale">Rationale</label><input id="rationale" maxlength="240" placeholder="Why this dispatch?" /></div>
</div>
</article>
<article class="card">
<h2>Last Decision</h2>
<div id="status" class="status-stack"></div>
</article>
<article class="card">
<h2>Decision Log</h2>
<div id="timeline" class="timeline"></div>
</article>
</aside>
<section class="stack">
<article class="panel">
<div class="console-head">
<div>
<div class="eyebrow">Cluster Board</div>
<h2>Placement Board</h2>
<p id="summary" class="console-subtitle">Reset the environment to start a new scenario.</p>
</div>
<div id="context" class="meta-strip"></div>
</div>
<div id="kpis" class="kpi-strip"></div>
<div class="ops-grid">
<section class="panel" style="padding:14px">
<div class="panel-head">
<div>
<h3>Pending Queue</h3>
<small>Operator view of actionable jobs and their current schedulability.</small>
</div>
<span id="queuemeta" class="chip mono">No queue data</span>
</div>
<div class="table-shell">
<table>
<thead>
<tr>
<th>Job</th>
<th>Pri</th>
<th>Eligible GPUs</th>
<th>VRAM</th>
<th>Slack</th>
<th>Age</th>
<th>Placement</th>
</tr>
</thead>
<tbody id="queueops"></tbody>
</table>
</div>
</section>
<section class="panel" style="padding:14px">
<div class="panel-head">
<div>
<h3>Cluster Inventory</h3>
<small>Capacity, placement, and schedulability by GPU.</small>
</div>
<span id="clustermeta" class="chip mono">No node data</span>
</div>
<div id="nodeinventory" class="inventory"></div>
</section>
<aside class="panel" style="padding:14px">
<div class="panel-head">
<div>
<h3>Operator Detail</h3>
<small>Current action, recommendation, and policy signals.</small>
</div>
</div>
<div class="detail-stack">
<div id="selectiondetail" class="detail-card"></div>
<div class="spark-shell">
<div class="spark-head">
<div>
<strong>Simulator Score</strong>
<div class="muted">Cumulative benchmark score across recent decisions.</div>
</div>
<span id="sparkmeta" class="chip mono">No history</span>
</div>
<div id="sparkline" class="sparkline"></div>
</div>
<div id="signals" class="detail-card"></div>
</div>
</aside>
</div>
<div class="footer-grid">
<div class="footer-card">
<h4>Completed Jobs</h4>
<div id="completeddock" class="footer-list"></div>
</div>
<div class="footer-card">
<h4>Missed Jobs</h4>
<div id="misseddock" class="footer-list"></div>
</div>
</div>
<div id="episodesummary" style="margin-top:14px"></div>
</article>
<article class="card">
<details>
<summary>Raw JSON</summary>
<pre id="raw"></pre>
</details>
</article>
</section>
</section>
</main>
<script>
const state={observation:null,reward:null,done:false,lastSelectedScenario:null,focusJobId:null,focusGpuId:null};
const el=id=>document.getElementById(id);
const f1=v=>v==null||Number.isNaN(Number(v))?"-":Number(v).toFixed(1);
const f2=v=>v==null||Number.isNaN(Number(v))?"-":Number(v).toFixed(2);
const pct=v=>v==null||Number.isNaN(Number(v))?"-":`${(Number(v)*100).toFixed(0)}%`;
const txt=v=>v==null?"":String(v);
const priorityClass=p=>txt(p).toLowerCase()||"low";
const priorityChip=p=>`<span class="chip ${priorityClass(p)}">${txt(p)}</span>`;
const priorityRank=p=>({high:0,medium:1,low:2}[priorityClass(p)] ?? 3);
const slackValue=(job, obs)=>Number(job.deadline)-Number(obs.current_tick);
function slackClass(slack){
if(slack<=2){ return "critical"; }
if(slack<=4){ return "warning"; }
return "stable";
}
function slackLabel(slack){
if(slack<0){ return `missed by ${Math.abs(slack)}`; }
if(slack===0){ return "due now"; }
if(slack<=2){ return `${slack} step left`; }
return `${slack} step buffer`;
}
function waitRiskClass(value){
if(value>=3){ return "critical"; }
if(value>=1){ return "warning"; }
return "stable";
}
function setOptions(select, options, placeholder){
const current=select.value;
select.innerHTML="";
if(!options.length){
select.innerHTML=`<option value="">${placeholder}</option>`;
select.disabled=true;
return;
}
select.innerHTML=`<option value="">${placeholder}</option>`+options.map(o=>`<option value="${o.value}">${o.label}</option>`).join("");
select.disabled=false;
if(options.some(o=>o.value===current)){ select.value=current; }
}
function parseStepDetails(details){
const raw=txt(details);
const parts=raw.split(";").map(s=>s.trim()).filter(Boolean);
return {
validation:parts[0]||"No validation details available.",
event:parts[1]||"No environment event recorded."
};
}
function normalize(payload){
return {
scenario_id:payload.scenario_id??payload.episode_id,
current_tick:payload.current_tick??payload.current_step??payload.step_count??0,
tick_limit:payload.tick_limit??payload.max_steps,
summary:payload.summary,
action_hint:payload.action_hint,
last_step_valid_action:payload.last_step_valid_action,
last_step_details:payload.last_step_details,
priority_alerts:payload.priority_alerts||[],
feasible_assignments:payload.feasible_assignments||[],
suggested_assignments:payload.suggested_assignments||[],
pending_jobs:(payload.pending_jobs ?? payload.queued_jobs) || [],
running_jobs:payload.running_jobs||[],
completed_jobs:payload.completed_jobs||[],
missed_jobs:payload.missed_jobs||[],
gpus:payload.gpus||[],
metrics:payload.metrics||{},
last_event:payload.last_event,
action_log:payload.action_log||[]
};
}
function gpuChoices(jobId){
if(!state.observation){ return []; }
const job=state.observation.pending_jobs.find(j=>j.job_id===jobId);
return state.observation.gpus
.filter(g=>!g.busy && (!job || g.vram_capacity>=job.vram_requirement))
.map(g=>({
value:g.gpu_id,
label:`${g.gpu_id} | VRAM ${g.vram_capacity} | speed ${g.speed_multiplier} | cost ${g.cost_per_step}`
}));
}
function fitCandidates(job, obs, idleOnly=true){
return (obs.gpus||[]).filter(gpu=>(!idleOnly || !gpu.busy) && gpu.vram_capacity>=job.vram_requirement);
}
function bestGpuForJob(job, obs){
const fits=fitCandidates(job, obs, true).slice().sort((a,b)=>
Number(a.cost_per_step)-Number(b.cost_per_step) ||
Number(b.speed_multiplier)-Number(a.speed_multiplier) ||
Number(a.vram_capacity)-Number(b.vram_capacity)
);
return fits[0]||null;
}
function bestJobForGpu(gpu, obs){
return (obs.pending_jobs||[])
.filter(job=>gpu.vram_capacity>=job.vram_requirement)
.slice()
.sort((a,b)=>
slackValue(a, obs)-slackValue(b, obs) ||
priorityRank(a.priority)-priorityRank(b.priority) ||
Number(b.pending_ticks)-Number(a.pending_ticks) ||
Number(b.vram_requirement)-Number(a.vram_requirement)
)[0]||null;
}
function selectJob(jobId){
if(!state.observation){ return; }
state.focusJobId=jobId||null;
el("action").value="place";
updateControls();
el("job").value=jobId||"";
setOptions(el("gpu"), gpuChoices(el("job").value), "Select an eligible GPU");
if(el("job").value && !el("gpu").value){
const job=(state.observation.pending_jobs||[]).find(item=>item.job_id===el("job").value);
const best=job?bestGpuForJob(job, state.observation):null;
if(best && [...el("gpu").options].some(option=>option.value===best.gpu_id)){ el("gpu").value=best.gpu_id; }
}
state.focusGpuId=el("gpu").value||state.focusGpuId;
if(el("job").value && el("gpu").value){
el("rationale").value=`Place ${el("job").value} on ${el("gpu").value} because it best balances fit, slack, and current pending pressure.`.slice(0,240);
}
render();
}
function selectGpu(gpuId){
if(!state.observation){ return; }
state.focusGpuId=gpuId||null;
el("action").value="place";
updateControls();
if([...el("gpu").options].some(option=>option.value===gpuId)){ el("gpu").value=gpuId; }
if(!el("job").value){
const gpu=(state.observation.gpus||[]).find(item=>item.gpu_id===gpuId);
const bestJob=gpu?bestJobForGpu(gpu, state.observation):null;
if(bestJob && [...el("job").options].some(option=>option.value===bestJob.job_id)){
el("job").value=bestJob.job_id;
setOptions(el("gpu"), gpuChoices(el("job").value), "Select an eligible GPU");
if([...el("gpu").options].some(option=>option.value===gpuId)){ el("gpu").value=gpuId; }
}
}
state.focusJobId=el("job").value||state.focusJobId;
if(el("job").value && el("gpu").value){
el("rationale").value=`Place ${el("job").value} on ${el("gpu").value} because it best balances fit, slack, and current pending pressure.`.slice(0,240);
}
render();
}
function updateControls(){
const obs=state.observation;
const queued=obs?obs.pending_jobs:[];
setOptions(
el("job"),
queued.map(j=>({
value:j.job_id,
label:`${j.job_id} | ${j.priority} | deadline ${j.deadline} | runtime ${j.remaining_runtime} | VRAM ${j.vram_requirement}`
})),
"Select a pending job"
);
setOptions(el("gpu"), gpuChoices(el("job").value), "Select an eligible GPU");
const waiting=el("action").value==="defer";
el("job").disabled=waiting||!queued.length;
el("gpu").disabled=waiting||!el("gpu").options.length;
if(waiting){
el("job").value="";
el("gpu").value="";
}
}
function pressureStats(obs){
const queued=obs.pending_jobs||[];
const allGpus=obs.gpus||[];
const urgent=queued.filter(job=>job.deadline-obs.current_tick<=2).length;
const constrained=queued.filter(job=>fitCandidates(job, obs, false).length<=1).length;
const idleFit=queued.filter(job=>fitCandidates(job, obs, true).length>0).length;
return [
{label:"Deadline", value:queued.length?(urgent/queued.length)*100:0},
{label:"Constraint", value:queued.length?(constrained/queued.length)*100:0},
{label:"Schedulable", value:queued.length?(idleFit/queued.length)*100:0},
{label:"Allocated", value:allGpus.length?(allGpus.filter(gpu=>gpu.busy).length/allGpus.length)*100:0}
];
}
function recommendation(obs){
const selectedAction=el("action").value;
const selectedJobId=el("job").value;
const selectedGpuId=el("gpu").value;
if(selectedAction==="defer"){
return {
title:"Defer placement",
body:"Defer placement for one tick. Use this when no eligible GPU can absorb high-risk jobs without burning the wrong capacity tier."
};
}
if(selectedJobId && selectedGpuId){
return {
title:`${selectedJobId} -> ${selectedGpuId}`,
body:"Loaded into the placement form. Dispatch this if the selected GPU is still the right fit for the pending job."
};
}
const suggested=(obs.suggested_assignments||[])[0];
if(suggested){
return {
title:`${txt(suggested.job_id)} -> ${txt(suggested.gpu_id)}`,
body:txt(suggested.rationale||"Top suggestion from the environment.")
};
}
const fallbackJob=(obs.pending_jobs||[])[0];
const fallbackGpu=fallbackJob?bestGpuForJob(fallbackJob, obs):null;
if(fallbackJob && fallbackGpu){
return {
title:`${fallbackJob.job_id} -> ${fallbackGpu.gpu_id}`,
body:"Lowest-cost eligible placement for the next pending job."
};
}
return {
title:"No actionable placement",
body:"Either the pending queue is empty or no eligible GPU can satisfy the current VRAM requirements."
};
}
function renderKpis(obs){
const m=obs.metrics||{};
const queued=obs.pending_jobs||[];
const running=obs.running_jobs||[];
const risk=queued.filter(job=>job.deadline-obs.current_tick<=2).length;
const constrained=queued.filter(job=>fitCandidates(job, obs, false).length<=1).length;
const allocationNow=(obs.gpus||[]).length ? ((obs.gpus||[]).filter(gpu=>gpu.busy).length/(obs.gpus||[]).length) : 0;
const costNow=(obs.gpus||[]).filter(gpu=>gpu.busy).reduce((sum,gpu)=>sum+Number(gpu.cost_per_step||0),0);
const cards=[
["Pending", queued.length, `${queued.filter(job=>fitCandidates(job, obs, true).length===0).length} unschedulable now`],
["Running", running.length, `${(obs.gpus||[]).filter(gpu=>gpu.busy).length} allocated GPUs`],
["Deadline Pressure", risk, risk?`${risk} jobs within 2 ticks`:"No immediate deadline pressure"],
["GPU Allocation", pct(allocationNow), `${txt(m.completed_jobs||0)} completed`],
["Scarce Capacity", constrained, constrained?`${constrained} jobs fit 1 GPU tier`:"No single-fit jobs right now"],
["Cost Rate", f1(costNow), `${txt((obs.gpus||[]).filter(gpu=>gpu.busy).length)} GPUs active`]
];
el("kpis").innerHTML=cards.map(([label,value,detail])=>`
<div class="kpi">
<small>${label}</small>
<strong class="mono">${value}</strong>
<span>${detail}</span>
</div>
`).join("");
}
function renderQueueBoard(obs){
const queued=obs.pending_jobs||[];
const selectedJob=state.focusJobId||el("job").value;
el("queuemeta").textContent=`${queued.length} jobs pending`;
const ranked=queued.slice().sort((a,b)=>
slackValue(a, obs)-slackValue(b, obs) ||
priorityRank(a.priority)-priorityRank(b.priority) ||
Number(b.pending_ticks)-Number(a.pending_ticks)
);
el("queueops").innerHTML=ranked.length?ranked.map(job=>{
const fits=fitCandidates(job, obs, true);
const best=bestGpuForJob(job, obs);
const slack=slackValue(job, obs);
const risk=slackClass(slack);
const waitRisk=waitRiskClass(Number(job.pending_ticks||0));
const rowClass=["ops-row", `risk-${risk}`, selectedJob===job.job_id?"selected":""].filter(Boolean).join(" ");
return `<tr class="${rowClass}" data-job="${job.job_id}">
<td class="mono">${job.job_id}</td>
<td><span class="priority-cell"><span class="priority-dot ${priorityClass(job.priority)}"></span>${txt(job.priority)}</span></td>
<td><span class="fit-count mono">${fits.length}</span></td>
<td class="mono">${job.vram_requirement}</td>
<td>
<div class="slack-cell">
<span class="slack-pill ${risk} mono">${slackLabel(slack)}</span>
<span class="cell-subtext mono">deadline d${job.deadline}</span>
</div>
</td>
<td>
<div class="slack-cell">
<span class="slack-pill ${waitRisk} mono">age ${job.pending_ticks}</span>
<span class="cell-subtext">${best?"schedulable now":"blocked now"}</span>
</div>
</td>
<td class="mono">${best?best.gpu_id:"defer"}</td>
</tr>`;
}).join(""):`<tr><td colspan="7" class="empty">No pending jobs remain.</td></tr>`;
}
function renderNodeInventory(obs){
const queued=obs.pending_jobs||[];
const running=obs.running_jobs||[];
const selectedGpu=state.focusGpuId||el("gpu").value;
const totalVram=(obs.gpus||[]).reduce((sum,gpu)=>sum+Number(gpu.vram_capacity||0),0);
el("clustermeta").textContent=`${obs.gpus.length} GPUs | ${totalVram} total VRAM`;
el("nodeinventory").innerHTML=(obs.gpus||[]).length?(obs.gpus||[]).map(gpu=>{
const runningJob=running.find(job=>job.assigned_gpu_id===gpu.gpu_id)||null;
const recommendedJob=!gpu.busy?bestJobForGpu(gpu, obs):null;
const used=runningJob?Number(runningJob.vram_requirement||0):0;
const fill=Math.max(0,Math.min(100,(used/Math.max(1,Number(gpu.vram_capacity||0)))*100));
const fitCount=queued.filter(job=>gpu.vram_capacity>=job.vram_requirement).length;
const selected=["inventory-row", selectedGpu===gpu.gpu_id?"selected":""].filter(Boolean).join(" ");
return `<div class="${selected}" data-gpu="${gpu.gpu_id}">
<div class="inventory-head">
<div>
<strong class="mono">${gpu.gpu_id}</strong>
<small>${gpu.busy?"allocated":"idle"} | speed ${f1(gpu.speed_multiplier)} | cost ${f1(gpu.cost_per_step)}</small>
</div>
<span class="chip ${gpu.busy?"warn":"good"}">${gpu.busy?"allocated":"idle"}</span>
</div>
<div class="capacity-track">
<div class="capacity-fill ${fill>=80?"busy-hot":""}" style="--fill:${fill}%"></div>
</div>
<div class="inventory-foot">
<span class="mono">${used}/${gpu.vram_capacity} VRAM used</span>
<span class="mono">${gpu.busy ? txt(runningJob?.job_id||"loaded") : `${fitCount} eligible jobs`}</span>
</div>
<div class="inventory-context">
<div>
<strong class="mono">${gpu.busy ? txt(runningJob?.job_id||"current load") : recommendedJob ? `Rec ${recommendedJob.job_id}` : "No eligible job"}</strong>
<small>${gpu.busy
? `Current workload | pr ${txt(runningJob?.priority)} | ${used}/${gpu.vram_capacity} VRAM`
: recommendedJob
? `Recommended placement | slack ${slackValue(recommendedJob, obs)} | pr ${txt(recommendedJob.priority)}`
: "No pending job fits this GPU right now."}</small>
</div>
<span class="inventory-meta-pill ${gpu.busy ? "warn" : recommendedJob ? "good" : ""} mono">${gpu.busy ? "current" : recommendedJob ? "eligible" : "idle"}</span>
</div>
</div>`;
}).join(""):`<div class="detail-card empty">No GPU state available.</div>`;
}
function renderSelectionDetail(obs){
const rec=recommendation(obs);
const selectedJob=(obs.pending_jobs||[]).find(job=>job.job_id===(state.focusJobId||el("job").value))||null;
const selectedGpu=(obs.gpus||[]).find(gpu=>gpu.gpu_id===(state.focusGpuId||el("gpu").value))||null;
const busyCount=(obs.gpus||[]).filter(gpu=>gpu.busy).length;
const idleCount=(obs.gpus||[]).length-busyCount;
let body=`
<span class="detail-label">Loaded decision</span>
<div class="detail-main mono">${rec.title}</div>
<div class="muted">${rec.body}</div>
<div class="stat-grid">
<div class="mini-card"><small>Idle GPUs</small><strong class="mono">${idleCount}</strong></div>
<div class="mini-card"><small>Allocated GPUs</small><strong class="mono">${busyCount}</strong></div>
</div>
`;
if(selectedJob){
const fits=fitCandidates(selectedJob, obs, true);
const slack=selectedJob.deadline-obs.current_tick;
body+=`
<hr />
<span class="detail-label">Selected job</span>
<div class="detail-main mono">${selectedJob.job_id}</div>
<div class="muted">Priority ${txt(selectedJob.priority)} | runtime ${f1(selectedJob.remaining_runtime)} | VRAM ${selectedJob.vram_requirement}</div>
<div class="stat-grid">
<div class="mini-card"><small>Eligible GPUs</small><strong class="mono">${fits.length}</strong></div>
<div class="mini-card"><small>Deadline slack</small><strong class="mono">${slack}</strong></div>
</div>
`;
}
if(selectedGpu){
body+=`
<hr />
<span class="detail-label">Selected GPU</span>
<div class="detail-main mono">${selectedGpu.gpu_id}</div>
<div class="muted">${selectedGpu.busy?"Currently allocated":"Eligible for placement"} | speed ${f1(selectedGpu.speed_multiplier)} | cost ${f1(selectedGpu.cost_per_step)} | VRAM ${selectedGpu.vram_capacity}</div>
`;
}
el("selectiondetail").innerHTML=body;
}
function renderSignals(obs){
const signalMarkup=pressureStats(obs).map(stat=>`
<div class="signal-row">
<span>${stat.label}</span>
<div class="signal-track"><div class="signal-fill" style="--fill:${stat.value.toFixed(0)}%"></div></div>
<span class="mono">${stat.value.toFixed(0)}%</span>
</div>
`).join("");
const hint=txt(obs.action_hint||"Use pending demand, eligible GPUs, and deadline pressure to decide whether to place now or defer.");
el("signals").innerHTML=`
<span class="detail-label">Policy signals</span>
<div class="muted">${hint}</div>
<div class="signal-list">${signalMarkup}</div>
`;
}
function renderSparkline(obs){
const history=(obs.action_log||[]).slice(-12);
el("sparkmeta").textContent=history.length?`${history.length} ticks`:"No history";
if(!history.length){
el("sparkline").innerHTML=`<div class="empty">No decision history yet.</div>`;
return;
}
let runningTotal=0;
const values=history.map(entry=>{
runningTotal+=Number((entry.score_delta ?? entry.reward_delta) || 0);
return runningTotal;
});
const min=Math.min(...values);
const max=Math.max(...values);
const width=260;
const height=78;
const padX=8;
const padY=10;
const denom=(max-min)||1;
const points=values.map((value,index)=>{
const x=padX + (index*(width-padX*2))/Math.max(1, values.length-1);
const y=height-padY-((value-min)*(height-padY*2))/denom;
return [x,y];
});
const path=points.map(([x,y],index)=>`${index===0?"M":"L"} ${x.toFixed(2)} ${y.toFixed(2)}`).join(" ");
const last=values[values.length-1];
el("sparkline").innerHTML=`
<svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" aria-hidden="true">
<g class="spark-grid">
<line x1="0" y1="${height-14}" x2="${width}" y2="${height-14}"></line>
<line x1="0" y1="${height/2}" x2="${width}" y2="${height/2}"></line>
<line x1="0" y1="14" x2="${width}" y2="14"></line>
</g>
<path class="spark-path" d="${path}"></path>
${points.map(([x,y],index)=>`<circle class="spark-dot" cx="${x.toFixed(2)}" cy="${y.toFixed(2)}" r="${index===points.length-1?3.4:2.2}"></circle>`).join("")}
</svg>
<div class="spark-axis">
<span class="mono">start</span>
<span class="mono">score ${f1(last)}</span>
</div>
`;
}
function renderFooter(obs){
const completed=obs.completed_jobs||[];
const missed=obs.missed_jobs||[];
el("completeddock").innerHTML=completed.length
? completed.slice(-8).map(job=>`<span class="tiny-pill good mono">${job.job_id}</span>`).join("")
: `<span class="empty">Nothing completed yet.</span>`;
el("misseddock").innerHTML=missed.length
? missed.slice(-8).map(job=>`<span class="tiny-pill bad mono">${job.job_id}</span>`).join("")
: `<span class="empty">No missed jobs.</span>`;
}
function renderEpisodeSummary(obs){
const summaryCard=el("episodesummary");
if(!state.done){
summaryCard.innerHTML="";
return;
}
const m=obs.metrics||{};
const completed=Number(m.completed_jobs||0);
const missed=Number(m.missed_deadlines||0);
const invalid=Number(m.invalid_actions||0);
const verdict=missed===0 && invalid===0
? "Clean finish: the scenario ended without missed deadlines or invalid moves."
: missed>0
? "The scenario finished, but deadline misses still left score on the table."
: "The scenario finished, but invalid actions reduced the total score.";
summaryCard.innerHTML=`
<div class="detail-card">
<span class="detail-label">Run summary</span>
<div class="detail-main">${verdict}</div>
<div class="stat-grid">
<div class="mini-card"><small>Simulator score</small><strong class="mono">${f1(m.total_reward)}</strong></div>
<div class="mini-card"><small>Completed jobs</small><strong class="mono">${completed}</strong></div>
<div class="mini-card"><small>Missed deadlines</small><strong class="mono">${missed}</strong></div>
<div class="mini-card"><small>Accumulated cost</small><strong class="mono">${f1(m.total_cost)}</strong></div>
</div>
</div>
`;
}
function renderStatus(obs){
const details=parseStepDetails(obs.last_step_details||obs.last_event||"Reset the environment to begin.");
el("status").innerHTML=`
<div class="status-box ${obs.last_step_valid_action==null?"":obs.last_step_valid_action?"good":"bad"}">
<strong>${obs.last_step_valid_action==null?"Ready":obs.last_step_valid_action?"Valid decision":"Invalid decision"}</strong>
<div class="muted">${details.validation}</div>
</div>
<div class="status-box">
<strong>Scheduler event</strong>
<div class="muted">${details.event}</div>
</div>
<div class="status-box">
<strong>Simulator score delta <span class="mono">${state.reward==null?"0.0":f1(state.reward)}</span></strong>
<div class="muted">Run complete: ${state.done}</div>
</div>
`;
}
function renderTimeline(obs){
const logs=(obs.action_log||[]).slice(-6).reverse();
el("timeline").innerHTML=logs.length?logs.map(entry=>`
<div class="timeline-entry">
<div class="timeline-meta">
<strong>Tick ${entry.tick_index ?? entry.step_index}</strong>
<span class="chip ${entry.valid?"good":"bad"} mono">${entry.valid?"valid":"invalid"} | score ${f1(entry.score_delta ?? entry.reward_delta)}</span>
</div>
<div class="muted">${txt(entry.details)}</div>
<div style="margin-top:8px">${txt(entry.rationale||"")}</div>
</div>
`).join(""):`<div class="detail-card empty">No decisions have been logged yet.</div>`;
}
function render(){
const obs=state.observation;
if(!obs){ return; }
el("summary").textContent=obs.summary;
el("context").innerHTML=[
`<span class="chip mono">Scenario ${txt(obs.scenario_id)}</span>`,
`<span class="chip ${state.done?"good":"warn"} mono">Run ${state.done?"complete":"active"}</span>`,
`<span class="chip mono">Tick ${txt(obs.current_tick)}/${txt(obs.tick_limit)}</span>`
].join("");
renderKpis(obs);
renderQueueBoard(obs);
renderNodeInventory(obs);
renderSelectionDetail(obs);
renderSignals(obs);
renderSparkline(obs);
renderFooter(obs);
renderEpisodeSummary(obs);
renderStatus(obs);
renderTimeline(obs);
el("raw").textContent=JSON.stringify({reward:state.reward,done:state.done,observation:obs},null,2);
updateControls();
}
async function resetEnv(){
const payload={scenario_id:el("episode").value};
if(el("seed").value.trim()){ payload.seed=Number(el("seed").value); }
const res=await fetch("/web/reset",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify(payload)
});
const body=await res.json();
const observation=body.observation;
state.reward=body.reward??0;
state.done=Boolean(body.done);
state.observation=observation;
state.focusJobId=null;
state.focusGpuId=null;
state.lastSelectedScenario=observation?.scenario_id||payload.scenario_id;
el("episode").value=observation?.scenario_id&&["deadline_crunch","cost_pressure","wait_for_capacity","reserve_large_gpu"].includes(observation.scenario_id)
? observation.scenario_id
: el("episode").value;
el("rationale").value="";
render();
}
async function refreshState(){
const res=await fetch("/web/state");
const body=await res.json();
state.reward=null;
state.done=false;
state.observation=normalize(body);
state.focusJobId=null;
state.focusGpuId=null;
render();
}
async function stepEnv(){
if(!state.observation){
await resetEnv();
return;
}
const action=el("action").value;
const wrapped={
action:{
action,
rationale:(el("rationale").value.trim()||(action==="defer"
? "Defer placement for a better scheduling window."
: "Place the selected job on the selected GPU.")).slice(0,240),
metadata:{}
}
};
if(action==="place"){
if(!el("job").value||!el("gpu").value){
el("status").innerHTML=`<div class="status-box bad"><strong>Action incomplete</strong><div class="muted">Choose both a pending job and an eligible GPU before dispatching.</div></div>`;
return;
}
wrapped.action.job_id=el("job").value;
wrapped.action.gpu_id=el("gpu").value;
}
const res=await fetch("/web/step",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify(wrapped)
});
const body=await res.json();
state.reward=body.reward;
state.done=Boolean(body.done);
state.observation=body.observation;
state.focusJobId=el("job").value||state.focusJobId;
state.focusGpuId=el("gpu").value||state.focusGpuId;
render();
}
el("action").addEventListener("change",()=>{
updateControls();
if(state.observation){ render(); }
});
el("job").addEventListener("change",()=>{
state.focusJobId=el("job").value||null;
setOptions(el("gpu"), gpuChoices(el("job").value), "Select an eligible GPU");
if(el("job").value && el("gpu").value){
el("rationale").value=`Place ${el("job").value} on ${el("gpu").value} because it best balances fit, slack, and current pending pressure.`.slice(0,240);
}
if(state.observation){ render(); }
});
el("gpu").addEventListener("change",()=>{
state.focusGpuId=el("gpu").value||null;
if(el("action").value==="place" && el("job").value && el("gpu").value){
el("rationale").value=`Place ${el("job").value} on ${el("gpu").value} because it best balances fit, slack, and current pending pressure.`.slice(0,240);
}
if(state.observation){ render(); }
});
el("episode").addEventListener("change",()=>{
if(state.observation && el("episode").value!==state.observation.scenario_id){ el("rationale").value=""; }
});
el("queueops").addEventListener("click",event=>{
const row=event.target.closest("[data-job]");
if(!row){ return; }
selectJob(row.getAttribute("data-job"));
});
el("nodeinventory").addEventListener("click",event=>{
const row=event.target.closest("[data-gpu]");
if(!row){ return; }
selectGpu(row.getAttribute("data-gpu"));
});
el("reset").addEventListener("click",resetEnv);
el("refresh").addEventListener("click",refreshState);
el("step").addEventListener("click",stepEnv);
resetEnv();
</script>
</body>
</html>"""