Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

830 lines
23 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Portable llama-server ablation runner for local inference backends.
*
* Measures target-only, MTP, TurboQuant KV, TCQ/QJL KV, and combined paths
* with the same OpenAI-compatible request shape across Metal, CUDA, ROCm, and
* CPU builds. Results are written as JSON for cross-machine comparison.
*/
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { performance } from "node:perf_hooks";
import process from "node:process";
const HOST = "127.0.0.1";
const DEFAULT_PROMPT =
"Write a dense technical paragraph about local language model inference, GPU kernels, KV cache compression, speculative decoding, and benchmarking. Keep going until the token budget is exhausted. Do not use bullets.";
const DEFAULT_VARIANTS = [
{
name: "baseline_f16_kv",
label: "target only, f16 KV",
args: [],
},
{
name: "turbo4_polar_kv",
label: "target only, Turbo/Polar KV turbo4",
args: ["--cache-type-k", "tbq4_0", "--cache-type-v", "tbq4_0"],
},
{
name: "turbo3_polar_kv",
label: "target only, Turbo/Polar KV turbo3",
args: ["--cache-type-k", "tbq3_0", "--cache-type-v", "tbq3_0"],
},
{
name: "qjl_tcq_forced",
label: "target only, QJL K + TBQ3 V",
args: ["--cache-type-k", "qjl1_256", "--cache-type-v", "tbq3_0"],
},
{
name: "mtp_only",
label: "draft-MTP, f16 KV",
needsDrafter: true,
args: ["--spec-type", "draft-mtp"],
},
{
name: "mtp_turbo4_polar",
label: "draft-MTP + Turbo/Polar KV turbo4",
needsDrafter: true,
args: [
"--spec-type",
"draft-mtp",
"--cache-type-k",
"tbq4_0",
"--cache-type-v",
"tbq4_0",
"--cache-type-k-draft",
"tbq4_0",
"--cache-type-v-draft",
"tbq4_0",
],
},
{
name: "all_mtp_qjl_tcq",
label: "draft-MTP + QJL K + TBQ3 V",
needsDrafter: true,
args: [
"--spec-type",
"draft-mtp",
"--cache-type-k",
"qjl1_256",
"--cache-type-v",
"tbq3_0",
"--cache-type-k-draft",
"qjl1_256",
"--cache-type-v-draft",
"tbq3_0",
],
},
];
function stateDir() {
return (
process.env.ELIZA_STATE_DIR?.trim() || path.join(os.homedir(), ".eliza")
);
}
function detectBackend() {
const forced = process.env.ELIZA_MTP_BACKEND?.trim().toLowerCase();
if (forced) return forced;
if (process.platform === "darwin") return "metal";
if (process.env.HIP_VISIBLE_DEVICES || process.env.ROCR_VISIBLE_DEVICES)
return "rocm";
if (
process.env.CUDA_VISIBLE_DEVICES &&
process.env.CUDA_VISIBLE_DEVICES !== "-1"
)
return "cuda";
const nvidia = spawnSync("nvidia-smi", ["-L"], {
stdio: ["ignore", "pipe", "ignore"],
encoding: "utf8",
});
if (nvidia.status === 0 && /\bGPU\s+\d+:/i.test(nvidia.stdout ?? "")) {
return "cuda";
}
return "cpu";
}
function platformKey(backend) {
return `${process.platform}-${process.arch}-${backend}`;
}
function canonicalArch(arch = process.arch) {
if (arch === "x64") return "x86_64";
return arch;
}
function canonicalPlatformKey(backend) {
return `${process.platform}-${canonicalArch()}-${backend}`;
}
function thresholdLookupKeys(hardware) {
const keys = [
hardware.platformKey,
hardware.canonicalPlatformKey,
`${hardware.platform}-${hardware.arch}`,
`${hardware.platform}-${hardware.canonicalArch}`,
hardware.backend,
];
return [...new Set(keys.filter(Boolean))];
}
function defaultBinary(backend) {
return path.join(
stateDir(),
"local-inference",
"bin",
"mtp",
platformKey(backend),
"llama-server",
);
}
const KERNEL_BY_CACHE_TYPE = new Map([
["turbo3", "turbo3"],
["tbq3_0", "turbo3"],
["turbo4", "turbo4"],
["tbq4_0", "turbo4"],
["turbo3_tcq", "turbo3_tcq"],
["tbq3_tcq", "turbo3_tcq"],
["qjl", "qjl_full"],
["qjl_full", "qjl_full"],
["qjl1_256", "qjl_full"],
["polar", "polarquant"],
["polarquant", "polarquant"],
["q4_polar", "polarquant"],
]);
function normalizeKernelArg(value) {
return String(value).trim().toLowerCase().replaceAll("-", "_");
}
function kernelForCacheType(value) {
return KERNEL_BY_CACHE_TYPE.get(normalizeKernelArg(value)) ?? null;
}
function optionValue(argv, index) {
const arg = argv[index];
const eq = arg.indexOf("=");
if (eq !== -1) return { value: arg.slice(eq + 1), consumed: 0 };
const value = argv[index + 1];
if (!value || value.startsWith("--")) return { value: null, consumed: 0 };
return { value, consumed: 1 };
}
function variantRequiredKernels(variant) {
const required = new Set();
const argv = variant.args ?? [];
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--spec-type" || arg.startsWith("--spec-type=")) {
const parsed = optionValue(argv, i);
const specType = parsed.value ? normalizeKernelArg(parsed.value) : "";
if (specType === "mtp" || specType === "draft_mtp") {
required.add("mtp");
}
i += parsed.consumed;
continue;
}
if (arg.startsWith("--cache-type")) {
const parsed = optionValue(argv, i);
if (parsed.value) {
const kernel = kernelForCacheType(parsed.value);
if (kernel) required.add(kernel);
}
i += parsed.consumed;
}
}
return [...required];
}
function loadCapabilities(binary) {
const filePath = path.join(path.dirname(binary), "CAPABILITIES.json");
if (!fs.existsSync(filePath)) return null;
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
const kernels = parsed?.kernels;
if (!kernels || typeof kernels !== "object" || Array.isArray(kernels)) {
throw new Error(`invalid CAPABILITIES.json kernels map: ${filePath}`);
}
return { path: filePath, kernels };
}
function missingVariantKernels(variant, capabilities) {
const required = variantRequiredKernels(variant);
return required.filter((kernel) => capabilities.kernels[kernel] !== true);
}
function defaultModelPath(name) {
return path.join(stateDir(), "local-inference", "models", name);
}
function defaultBundlePath(tierSlug, ...parts) {
return defaultModelPath(path.join(`eliza-1-${tierSlug}.bundle`, ...parts));
}
const QUICK_VARIANTS = [
"baseline_f16_kv",
"turbo4_polar_kv",
"mtp_only",
];
const OPTION_ALIASES = new Map([
["-b", "--batch-size"],
["-ub", "--ubatch-size"],
]);
const NUMBER_OPTIONS = new Map([
["--runs", "runs"],
["--max-tokens", "maxTokens"],
["--warmup-tokens", "warmupTokens"],
["--ctx-size", "contextSize"],
["--ctx-size-draft", "draftContextSize"],
["--draft-min", "draftMin"],
["--draft-max", "draftMax"],
["--batch-size", "batchSize"],
["--ubatch-size", "ubatchSize"],
["--gpu-layers", "gpuLayers"],
["--draft-gpu-layers", "draftGpuLayers"],
["--timeout-ms", "timeoutMs"],
["--start-timeout-ms", "startTimeoutMs"],
]);
const PATH_OPTIONS = new Map([
["--binary", "binary"],
["--model", "model"],
["--drafter", "drafter"],
["--out-dir", "outDir"],
["--config", "config"],
["--gate", "gate"],
]);
const VALUE_OPTIONS = new Set([
...NUMBER_OPTIONS.keys(),
...PATH_OPTIONS.keys(),
"--backend",
"--prompt",
"--variants",
]);
function defaultArgsForBackend(backend) {
const defaultGpuLayers = backend === "cpu" ? 0 : 99;
return {
backend,
binary: null,
model: defaultBundlePath("2b", "text", "eliza-1-2b-128k.gguf"),
drafter: defaultBundlePath("2b", "mtp", "drafter-2b.gguf"),
runs: 3,
warmupTokens: 32,
maxTokens: 256,
contextSize: 6048,
draftContextSize: 256,
draftMin: 1,
draftMax: 16,
batchSize: 256,
ubatchSize: 64,
gpuLayers: defaultGpuLayers,
draftGpuLayers: defaultGpuLayers,
timeoutMs: 180_000,
startTimeoutMs: 120_000,
prompt: DEFAULT_PROMPT,
variants: null,
outDir: path.join(process.cwd(), "artifacts", "local-inference-ablation"),
config: null,
gate: null,
requireAll: false,
};
}
function requireArgValue(argv, index, arg) {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${arg} requires a value`);
}
return value;
}
function setNumberOption(args, key, value) {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) throw new Error(`${key} must be a number`);
args[key] = parsed;
}
function applyValueOption(args, option, value) {
if (option === "--backend") {
args.backend = value;
return;
}
if (option === "--prompt") {
args.prompt = value;
return;
}
if (option === "--variants") {
args.variants = value
.split(",")
.map((v) => v.trim())
.filter(Boolean);
return;
}
const numberKey = NUMBER_OPTIONS.get(option);
if (numberKey) {
setNumberOption(args, numberKey, value);
return;
}
const pathKey = PATH_OPTIONS.get(option);
if (pathKey) {
args[pathKey] = path.resolve(value);
return;
}
throw new Error(`Unknown argument: ${option}`);
}
function applyFlagOption(args, arg) {
if (arg === "--require-all") {
args.requireAll = true;
return true;
}
if (arg === "--quick") {
args.runs = 1;
args.maxTokens = 96;
args.warmupTokens = 16;
args.variants = QUICK_VARIANTS;
return true;
}
if (arg === "--help" || arg === "-h") {
printHelp();
process.exit(0);
}
return false;
}
function parseArgs(argv) {
const args = defaultArgsForBackend(detectBackend());
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (applyFlagOption(args, arg)) continue;
const option = OPTION_ALIASES.get(arg) ?? arg;
if (!VALUE_OPTIONS.has(option)) throw new Error(`Unknown argument: ${arg}`);
const value = requireArgValue(argv, i, option);
applyValueOption(args, option, value);
i += 1;
}
args.binary = args.binary || defaultBinary(args.backend);
return args;
}
function printHelp() {
console.log(`Usage: node scripts/local-inference-ablation.mjs [options]
Options:
--backend metal|cuda|rocm|vulkan|cpu
--binary /path/to/llama-server
--model /path/to/target.gguf
--drafter /path/to/drafter.gguf
--variants baseline_f16_kv,turbo4_polar_kv,qjl_tcq_forced,mtp_only
--runs 3 --max-tokens 256 --quick
--out-dir artifacts/local-inference-ablation
--config scripts/local-inference-ablation.config.json
--gate scripts/local-inference-thresholds.json
--require-all fail (exit 1) if any variant is skipped due to a missing model
Exit codes:
0 all selected variants ran (or were cleanly skipped) and met thresholds (if --gate is set)
1 startup error, a variant failed to run, gate thresholds were violated, or a model was missing while --require-all is set
`);
}
function loadVariants(args) {
let variants = DEFAULT_VARIANTS;
if (args.config) {
const parsed = JSON.parse(fs.readFileSync(args.config, "utf8"));
if (Array.isArray(parsed.variants)) variants = parsed.variants;
}
if (args.variants) {
const wanted = new Set(args.variants);
variants = variants.filter((variant) => wanted.has(variant.name));
}
const targetExists = fs.existsSync(args.model);
const drafterExists = fs.existsSync(args.drafter);
const capabilities = loadCapabilities(args.binary);
return variants.map((variant) => {
if (!targetExists) {
return { ...variant, skipReason: `model missing: ${args.model}` };
}
if (variant.needsDrafter && !drafterExists) {
return { ...variant, skipReason: `drafter missing: ${args.drafter}` };
}
if (capabilities) {
const missing = missingVariantKernels(variant, capabilities);
if (missing.length > 0) {
return {
...variant,
skipReason: `kernel(s) not advertised by ${capabilities.path}: ${missing.join(", ")}`,
};
}
}
return variant;
});
}
function loadThresholds(filePath) {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (!parsed || typeof parsed !== "object") {
throw new Error(`invalid threshold file: ${filePath}`);
}
return parsed;
}
function thresholdFor(thresholds, backend, variantName) {
if (!thresholds) return null;
const keys = Array.isArray(backend) ? backend : [backend];
for (const key of keys) {
const byBackend = thresholds[key];
if (!byBackend || typeof byBackend !== "object") continue;
const direct = byBackend[variantName];
if (typeof direct === "number" && Number.isFinite(direct)) return direct;
const fallback = byBackend.default;
if (typeof fallback === "number" && Number.isFinite(fallback)) {
return fallback;
}
}
return null;
}
function evaluateGate(report, thresholds) {
const violations = [];
const thresholdKeys = thresholdLookupKeys(report.hardware);
for (const variant of report.variants) {
if (variant.skipped || !variant.ok) continue;
const min = thresholdFor(thresholds, thresholdKeys, variant.name);
if (min === null) continue;
variant.thresholdTokPerSec = min;
variant.thresholdKeys = thresholdKeys;
if (variant.avgTokPerSec < min) {
violations.push({
name: variant.name,
backend: report.hardware.backend,
avgTokPerSec: variant.avgTokPerSec,
thresholdTokPerSec: min,
});
}
}
return violations;
}
function runCapture(cmd, cmdArgs) {
const result = spawnSync(cmd, cmdArgs, {
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
});
return {
ok: result.status === 0,
stdout: result.stdout?.trim() ?? "",
stderr: result.stderr?.trim() ?? "",
};
}
function hardwareInfo(args) {
const info = {
platform: process.platform,
arch: process.arch,
canonicalArch: canonicalArch(),
backend: args.backend,
platformKey: platformKey(args.backend),
canonicalPlatformKey: canonicalPlatformKey(args.backend),
hostname: os.hostname(),
cpus: os.cpus().map((cpu) => cpu.model)[0],
memoryGb: Math.round(os.totalmem() / 1024 ** 3),
};
if (process.platform === "darwin") {
const chip = runCapture("sysctl", ["-n", "machdep.cpu.brand_string"]);
const gpu = runCapture("system_profiler", ["SPDisplaysDataType"]);
info.cpuBrand = chip.stdout || null;
info.gpu = (gpu.stdout.match(/Chipset Model: (.+)/) || [])[1] || null;
} else {
const nvidia = runCapture("nvidia-smi", [
"--query-gpu=name,memory.total,driver_version",
"--format=csv,noheader",
]);
const rocm = runCapture("rocminfo", []);
info.nvidiaSmi = nvidia.ok ? nvidia.stdout : null;
info.rocminfo = rocm.ok ? rocm.stdout.slice(0, 2000) : null;
}
return info;
}
async function freePort() {
return await new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on("error", reject);
server.listen(0, HOST, () => {
const address = server.address();
server.close(() => {
if (address && typeof address === "object") resolve(address.port);
else reject(new Error("failed to allocate port"));
});
});
});
}
async function sleep(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchJson(url, init, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { ...init, signal: controller.signal });
if (!response.ok) {
throw new Error(
`HTTP ${response.status}: ${await response.text().catch(() => "")}`,
);
}
return await response.json();
} finally {
clearTimeout(timer);
}
}
async function waitReady(baseUrl, state, logs, timeoutMs) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
if (state.exited) {
throw new Error(
`server exited ${state.exited.code ?? state.exited.signal}: ${logs.slice(-80).join("\n")}`,
);
}
try {
await fetchJson(`${baseUrl}/health`, { method: "GET" }, 1500);
return;
} catch {
await sleep(500);
}
}
throw new Error(
`server not ready after ${timeoutMs}ms:\n${logs.slice(-120).join("\n")}`,
);
}
async function stopServer(child, state) {
if (state.exited) return;
child.kill("SIGTERM");
await Promise.race([
new Promise((resolve) => child.once("exit", resolve)),
sleep(5000),
]);
if (!state.exited) child.kill("SIGKILL");
}
function serverArgs(args, variant, port) {
const out = [
"--model",
args.model,
"--host",
HOST,
"--port",
String(port),
"--ctx-size",
String(args.contextSize),
"--parallel",
"1",
"--metrics",
"--jinja",
"-fa",
"on",
"-b",
String(args.batchSize),
"-ub",
String(args.ubatchSize),
];
if (args.backend !== "cpu") {
out.push("--n-gpu-layers", String(args.gpuLayers));
}
if (variant.needsDrafter || variant.args?.includes("--spec-type")) {
out.push(
"-md",
args.drafter,
"--spec-draft-n-min",
String(args.draftMin),
"--spec-draft-n-max",
String(args.draftMax),
);
if (args.backend !== "cpu") {
out.push("--n-gpu-layers-draft", String(args.draftGpuLayers));
}
}
out.push(...(variant.args || []));
return out;
}
async function completion(baseUrl, args, maxTokens) {
const started = performance.now();
const json = await fetchJson(
`${baseUrl}/v1/chat/completions`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
model: "local",
messages: [{ role: "user", content: args.prompt }],
max_tokens: maxTokens,
temperature: 0.7,
top_p: 0.95,
stream: false,
}),
},
args.timeoutMs,
);
const elapsedSec = (performance.now() - started) / 1000;
const content = json?.choices?.[0]?.message?.content ?? "";
const usageTokens = Number(json?.usage?.completion_tokens);
const tokens =
Number.isFinite(usageTokens) && usageTokens > 0
? usageTokens
: Math.max(1, Math.round(String(content).length / 4));
return {
tokens,
elapsedSec,
tokPerSec: tokens / elapsedSec,
preview: String(content).slice(0, 96).replace(/\s+/g, " "),
};
}
async function runVariant(args, variant) {
const port = await freePort();
const baseUrl = `http://${HOST}:${port}`;
const logs = [];
const state = { exited: null };
const cmdArgs = serverArgs(args, variant, port);
const child = spawn(args.binary, cmdArgs, {
stdio: ["ignore", "pipe", "pipe"],
env: process.env,
});
child.on("exit", (code, signal) => {
state.exited = { code, signal };
});
const capture = (buf) => {
for (const raw of buf.toString().split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;
logs.push(line);
while (logs.length > 300) logs.shift();
}
};
child.stdout.on("data", capture);
child.stderr.on("data", capture);
const row = {
name: variant.name,
label: variant.label,
ok: false,
args: cmdArgs,
runs: [],
};
try {
await waitReady(baseUrl, state, logs, args.startTimeoutMs);
await completion(baseUrl, args, args.warmupTokens);
for (let i = 0; i < args.runs; i += 1) {
const run = await completion(baseUrl, args, args.maxTokens);
row.runs.push(run);
console.log(
`${variant.name} run ${i + 1}/${args.runs}: ${run.tokPerSec.toFixed(2)} tok/s (${run.tokens} tok, ${run.elapsedSec.toFixed(3)}s)`,
);
}
row.ok = true;
row.avgTokPerSec =
row.runs.reduce((sum, run) => sum + run.tokPerSec, 0) / row.runs.length;
row.minTokPerSec = Math.min(...row.runs.map((run) => run.tokPerSec));
row.maxTokPerSec = Math.max(...row.runs.map((run) => run.tokPerSec));
} catch (error) {
row.error = error instanceof Error ? error.message : String(error);
row.logs = logs.slice(-120);
console.log(`${variant.name} FAILED: ${row.error.split("\n")[0]}`);
} finally {
await stopServer(child, state);
}
return row;
}
function printSummary(results) {
console.log("\nAblation summary");
for (const row of results) {
if (row.skipped) {
console.log(
`${row.name.padEnd(24)} SKIPPED ${row.skipReason ?? "unknown"}`,
);
} else if (row.ok) {
const gate =
typeof row.thresholdTokPerSec === "number"
? ` >=${row.thresholdTokPerSec.toFixed(2)} tok/s gate`
: "";
console.log(
`${row.name.padEnd(24)} ${row.avgTokPerSec.toFixed(2).padStart(8)} tok/s (${row.label})${gate}`,
);
} else {
console.log(
`${row.name.padEnd(24)} FAILED ${row.error?.split("\n")[0] ?? "unknown"}`,
);
}
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const variants = loadVariants(args);
if (variants.length === 0) throw new Error("no variants selected");
const binaryMissing = !fs.existsSync(args.binary);
const _modelMissing = !fs.existsSync(args.model);
const thresholds = args.gate ? loadThresholds(args.gate) : null;
fs.mkdirSync(args.outDir, { recursive: true });
const report = {
createdAt: new Date().toISOString(),
binary: args.binary,
model: args.model,
drafter: fs.existsSync(args.drafter) ? args.drafter : null,
maxTokens: args.maxTokens,
runs: args.runs,
hardware: hardwareInfo(args),
variants: [],
gate: thresholds ? args.gate : null,
};
if (binaryMissing) {
const reason = `binary missing: ${args.binary}`;
console.log(`[ablation] skip all variants: ${reason}`);
for (const variant of variants) {
report.variants.push({
name: variant.name,
label: variant.label,
skipped: true,
skipReason: reason,
});
}
} else {
for (const variant of variants) {
if (variant.skipReason) {
console.log(`[ablation] skip ${variant.name}: ${variant.skipReason}`);
report.variants.push({
name: variant.name,
label: variant.label,
skipped: true,
skipReason: variant.skipReason,
});
continue;
}
console.log(`\n[ablation] ${variant.name}: ${variant.label}`);
report.variants.push(await runVariant(args, variant));
}
}
const violations = thresholds ? evaluateGate(report, thresholds) : [];
report.thresholdViolations = violations;
printSummary(report.variants);
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const outPath = path.join(args.outDir, `${stamp}-${args.backend}.json`);
fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`);
console.log(`\nWrote ${outPath}`);
const skipped = report.variants.filter((row) => row.skipped);
const failed = report.variants.filter((row) => !row.skipped && !row.ok);
let exitCode = 0;
if (failed.length > 0) {
console.error(
`[ablation] ${failed.length} variant(s) failed: ${failed.map((row) => row.name).join(", ")}`,
);
exitCode = 1;
}
if (args.requireAll && skipped.length > 0) {
console.error(
`[ablation] --require-all set but ${skipped.length} variant(s) skipped: ${skipped.map((row) => row.name).join(", ")}`,
);
exitCode = 1;
}
if (violations.length > 0) {
console.error("[ablation] threshold gate violations:");
for (const v of violations) {
console.error(
` ${v.backend}/${v.name}: ${v.avgTokPerSec.toFixed(2)} tok/s < ${v.thresholdTokPerSec.toFixed(2)} tok/s`,
);
}
exitCode = 1;
}
process.exit(exitCode);
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});