One owner for "may heavy GPU work start": numbers-only samples (full-screen
state, sustained 3D use, host free VRAM) in, { inUse, since, checkedAt,
probeOk } out; fail closed at start and when the probe goes stale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
4.5 KiB
JavaScript
143 lines
4.5 KiB
JavaScript
// GPU tenancy gate: the one owner of "may heavy GPU work start now", from the host's view.
|
|
// It reads numbers only (full-screen state, 3D-engine use, host free VRAM), keeps a yes/no and a
|
|
// timestamp, and never learns which program is running. It refuses new work; it never stops work.
|
|
|
|
export const RETRY_AFTER_SECONDS = 60;
|
|
const QUNS_BUSY = 2;
|
|
const QUNS_RUNNING_D3D_FULL_SCREEN = 3;
|
|
|
|
export function createGpuTenancy({
|
|
now,
|
|
sampleIntervalMs = 15000,
|
|
clearHoldMs = 180000,
|
|
minFreeMiB = 10240,
|
|
util3dBusyPct = 20,
|
|
staleAfterMs = 3 * sampleIntervalMs,
|
|
}) {
|
|
const startedAt = now();
|
|
let inUse = true;
|
|
let since = startedAt;
|
|
let checkedAt = null;
|
|
let lastSeenAt = startedAt;
|
|
let clearSince = null;
|
|
let previousUtil3d = null;
|
|
|
|
const staleSince = () => (now() - lastSeenAt > staleAfterMs ? lastSeenAt + staleAfterMs : null);
|
|
|
|
function observe(sample) {
|
|
const t = now();
|
|
const staleAt = staleSince();
|
|
if (staleAt !== null) {
|
|
if (!inUse) {
|
|
inUse = true;
|
|
since = staleAt;
|
|
}
|
|
clearSince = null;
|
|
previousUtil3d = null;
|
|
}
|
|
const sustained3d = sample.util3d > util3dBusyPct && previousUtil3d !== null && previousUtil3d > util3dBusyPct;
|
|
const busy = sample.quns === QUNS_BUSY
|
|
|| sample.quns === QUNS_RUNNING_D3D_FULL_SCREEN
|
|
|| sample.freeMiB < minFreeMiB
|
|
|| sustained3d;
|
|
previousUtil3d = sample.util3d;
|
|
lastSeenAt = t;
|
|
checkedAt = t;
|
|
if (busy) {
|
|
clearSince = null;
|
|
if (!inUse) {
|
|
inUse = true;
|
|
since = t;
|
|
}
|
|
return;
|
|
}
|
|
if (clearSince === null) clearSince = t;
|
|
if (inUse && t - clearSince >= clearHoldMs) {
|
|
inUse = false;
|
|
since = t;
|
|
}
|
|
}
|
|
|
|
function status() {
|
|
const staleAt = staleSince();
|
|
if (staleAt !== null) {
|
|
return { inUse: true, since: inUse ? since : staleAt, checkedAt, probeOk: false };
|
|
}
|
|
return { inUse, since, checkedAt, probeOk: true };
|
|
}
|
|
|
|
return { observe, status };
|
|
}
|
|
|
|
// A probe line is JSON; only the three finite numbers survive, so nothing else can reach the gate.
|
|
export function parseSample(line) {
|
|
let value;
|
|
try {
|
|
value = JSON.parse(line);
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (value === null || typeof value !== "object") return null;
|
|
const { quns, freeMiB, util3d } = value;
|
|
for (const field of [quns, freeMiB, util3d]) {
|
|
if (typeof field !== "number" || !Number.isFinite(field)) return null;
|
|
}
|
|
return { quns, freeMiB, util3d };
|
|
}
|
|
|
|
// Checks the gate once. A busy gate refuses without calling the job; a free gate runs the job to
|
|
// its end and returns its result: the gate never holds, pauses or cancels a job.
|
|
export async function admitThen(gate, job) {
|
|
const gpu = gate.status();
|
|
if (gpu.inUse) return { ok: false, code: "gpu-busy", source: "admission", gpu };
|
|
return job();
|
|
}
|
|
|
|
// The encoder child's placement refusal reaches the daemon as a typed code on the worker's error body;
|
|
// it is never read out of error text. Returns the typed 503 body, or null for any other outcome.
|
|
export function placementRefusal(workerBody, gpu, jobId) {
|
|
if (workerBody?.code !== "gpu-busy") return null;
|
|
return { ok: false, code: "gpu-busy", source: "placement", gpu, jobId };
|
|
}
|
|
|
|
// One long-lived hidden PowerShell child feeds samples. It exits with the daemon (the script watches
|
|
// the parent pid); if it exits on its own it is respawned at most once per interval.
|
|
export function startProbe({ spawn, scriptPath, intervalMs, onSample, parentPid = process.pid }) {
|
|
let stopped = false;
|
|
let lastSpawnAt = 0;
|
|
|
|
function launch() {
|
|
if (stopped) return;
|
|
lastSpawnAt = Date.now();
|
|
const child = spawn("powershell.exe", [
|
|
"-NoProfile",
|
|
"-NonInteractive",
|
|
"-ExecutionPolicy", "Bypass",
|
|
"-File", scriptPath,
|
|
"-ParentPid", String(parentPid),
|
|
"-IntervalMs", String(intervalMs),
|
|
], { windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
let pending = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
pending += chunk;
|
|
const lines = pending.split("\n");
|
|
pending = lines.pop();
|
|
for (const line of lines) {
|
|
const sample = parseSample(line.trim());
|
|
if (sample) onSample(sample);
|
|
}
|
|
});
|
|
let respawned = false;
|
|
const respawn = () => {
|
|
if (respawned) return;
|
|
respawned = true;
|
|
setTimeout(launch, Math.max(0, lastSpawnAt + intervalMs - Date.now())).unref();
|
|
};
|
|
child.on("exit", respawn);
|
|
child.on("error", respawn);
|
|
}
|
|
|
|
launch();
|
|
return { stop: () => { stopped = true; } };
|
|
}
|