Spaces:
Running
Running
Commit ·
b799d1d
1
Parent(s): 8f91935
Add online predictive KV tiering experiments
Browse files- README.md +19 -9
- app.js +123 -2
- docs/architecture.md +2 -1
- docs/methodology.md +35 -13
- docs/research.md +2 -2
- index.html +69 -4
- py/inferscale/__init__.py +13 -2
- py/inferscale/api.py +10 -0
- py/inferscale/execution.py +406 -38
- pyproject.toml +1 -1
- scripts/release_check.py +18 -3
- src/inferscale/__init__.py +13 -2
- src/inferscale/api.py +10 -0
- src/inferscale/execution.py +406 -38
- tests/test_execution.py +56 -0
README.md
CHANGED
|
@@ -137,20 +137,30 @@ The Predictive Tiering Lab deliberately changes the duration distribution of slo
|
|
| 137 |
|
| 138 |
Model dynamic multi-agent workflows as a sequence of reusable agent roles rather than only a sequence of tool durations. The default synthetic workflow has structured transitions among `planner`, `retriever`, `reasoner`, `verifier`, and `writer`, then deliberately changes its transition matrix partway through the study.
|
| 139 |
|
| 140 |
-
The simulator maintains a bounded shared HBM cache for each role's reusable static prefix. A first-order transition model is updated **only after the next role is observed**
|
| 141 |
|
| 142 |
-
|
| 143 |
-
2. cumulative transition counts;
|
| 144 |
-
3. exponentially decayed transition counts;
|
| 145 |
-
4. an explicitly labeled oracle next-role upper bound.
|
| 146 |
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
|
|
|
|
|
|
|
| 150 |
- **Confidence Threshold Study:** trade prefetch coverage against precision, wrong-step traffic, cache pollution, and TTFT;
|
| 151 |
-
- **Forgetting-Rate Study:** vary how quickly old transitions are forgotten after the workflow changes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
-
|
| 154 |
|
| 155 |
Single Agent Session runs, Predictive Tiering studies, adaptation sweeps, and Execution Learning studies all support JSON artifact export in addition to CSV/table export where applicable.
|
| 156 |
|
|
|
|
| 137 |
|
| 138 |
Model dynamic multi-agent workflows as a sequence of reusable agent roles rather than only a sequence of tool durations. The default synthetic workflow has structured transitions among `planner`, `retriever`, `reasoner`, `verifier`, and `writer`, then deliberately changes its transition matrix partway through the study.
|
| 139 |
|
| 140 |
+
The simulator maintains a bounded shared HBM cache for each role's reusable static prefix. A first-order transition model is updated **only after the next role is observed**. The learned matrix can now be rolled forward for several steps without reading the future trace. Single-run policies include learn-only, cumulative/decayed top-1 prefetch, multi-step top-k prefetch, a utility-aware multi-step planner, and explicitly labeled oracle information bounds.
|
| 141 |
|
| 142 |
+
The utility-aware planner estimates, for each candidate role:
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
+
```text
|
| 145 |
+
expected discounted reuse benefit
|
| 146 |
+
- host-to-HBM transfer time
|
| 147 |
+
- forecast-weighted recomputation cost of prefixes that would be evicted
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
This is intentionally a transparent reference objective rather than a learned black-box policy.
|
| 151 |
|
| 152 |
+
Six controlled experiments separate prediction quality from systems quality:
|
| 153 |
+
|
| 154 |
+
- **Prefetch Policy Study:** compare no prefetch, cumulative counts, decayed counts, and a clairvoyant next-role information bound on one common shifted workflow trace;
|
| 155 |
- **Confidence Threshold Study:** trade prefetch coverage against precision, wrong-step traffic, cache pollution, and TTFT;
|
| 156 |
+
- **Forgetting-Rate Study:** vary how quickly old transitions are forgotten after the workflow changes;
|
| 157 |
+
- **Prefetch Planning Study:** compare one-step, multi-step top-k, utility-aware multi-step, and a clairvoyant future-set information bound;
|
| 158 |
+
- **Forecast Horizon Study:** sweep how many future workflow steps the online planner rolls forward;
|
| 159 |
+
- **Cache Budget Study:** compare top-1, multi-step, and utility-aware planning under multiple working-set-relative HBM budgets.
|
| 160 |
+
|
| 161 |
+
Prediction diagnostics now include top-1 accuracy, Brier score, multiclass log loss, expected calibration error (ECE), and pre/post-shift calibration. Serving diagnostics separately track forecast-set recall, immediate next-step precision, eventual prefetch utilization, unused prefetch volume, cache hits, HBM residency, and p95 latency. A predictor can therefore become better calibrated or more accurate without automatically being declared a better serving policy.
|
| 162 |
|
| 163 |
+
The oracle candidates are **clairvoyant information bounds**, not guaranteed upper bounds on serving performance: perfect local future information can still trigger globally poor cache or transfer decisions. That distinction is intentional.
|
| 164 |
|
| 165 |
Single Agent Session runs, Predictive Tiering studies, adaptation sweeps, and Execution Learning studies all support JSON artifact export in addition to CSV/table export where applicable.
|
| 166 |
|
app.js
CHANGED
|
@@ -22,6 +22,9 @@ let lastExecutionRun = null;
|
|
| 22 |
let lastExecutionCompare = null;
|
| 23 |
let lastExecutionThreshold = null;
|
| 24 |
let lastExecutionDecay = null;
|
|
|
|
|
|
|
|
|
|
| 25 |
let traceRequests = [];
|
| 26 |
|
| 27 |
const COLORS = {
|
|
@@ -45,7 +48,7 @@ worker.addEventListener("message", (event) => {
|
|
| 45 |
if (data.type === "ready") {
|
| 46 |
runtimePill.classList.add("ready");
|
| 47 |
runtimeText.textContent = "Python runtime ready";
|
| 48 |
-
["runBtn", "arenaBtn", "capacityBtn", "topologyBtn", "designBtn", "pairedStudyBtn", "robustStudyBtn", "agentRunBtn", "agentCompareBtn", "agentTtlBtn", "agentMemoryCompareBtn", "agentBudgetBtn", "agentAffinityBtn", "predictiveCompareBtn", "predictiveAlphaBtn", "execRunBtn", "execCompareBtn", "execThresholdBtn", "execDecayBtn"].forEach((id) => { $(id).disabled = false; });
|
| 49 |
syncConditionalControls();
|
| 50 |
window.setTimeout(() => document.body.classList.add("runtime-ready"), 650);
|
| 51 |
return;
|
|
@@ -1122,6 +1125,11 @@ function executionConfigFromUI(overrides = {}) {
|
|
| 1122 |
transition_decay: num("execDecay"),
|
| 1123 |
confidence_threshold: num("execThreshold"),
|
| 1124 |
host_bandwidth_gbps: num("execBandwidth"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1125 |
...overrides,
|
| 1126 |
};
|
| 1127 |
}
|
|
@@ -1152,10 +1160,14 @@ function renderExecutionRun(result) {
|
|
| 1152 |
$("execPrecision").textContent = r.prefetch_attempts ? pct(r.prefetch_precision) : "N/A";
|
| 1153 |
$("execCoverage").textContent = pct(r.prefetch_coverage);
|
| 1154 |
$("execWaste").textContent = `${fmt(r.wrong_step_prefetch_gb, 4)} GB`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
$("execRunState").textContent = `${s.workflows_completed}/${s.workflows_generated} workflows`;
|
| 1156 |
$("execRunState").className = `tag ${s.workflow_completion_rate === 1 ? "good" : "bad"}`;
|
| 1157 |
const lookahead = result.provenance.lookahead === "oracle-upper-bound" ? "oracle upper bound" : "online/no-lookahead";
|
| 1158 |
-
$("execRunSummary").innerHTML = `<strong>${escapeHtml(result.config.prefetch_policy.replaceAll("_", " "))} policy.</strong> ${p.count} observed transitions; top-1 accuracy ${pct(p.top1_accuracy)} overall and ${pct(p.post_shift_accuracy)} after the workflow shift.
|
| 1159 |
|
| 1160 |
const timeline = result.timeline || [];
|
| 1161 |
destroyChart("execTimeline");
|
|
@@ -1181,6 +1193,17 @@ function renderExecutionRun(result) {
|
|
| 1181 |
data: { datasets },
|
| 1182 |
options: { ...commonChartOptions(), parsing: false, plugins: { legend: lineLegend() }, scales: { x: { type: "linear", beginAtZero: true, title: { display: true, text: "Observed transitions" } }, y: { min: 0, max: 100, title: { display: true, text: "Rolling top-1 accuracy (%)" } } } },
|
| 1183 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1184 |
}
|
| 1185 |
|
| 1186 |
$("execRunBtn").addEventListener("click", async () => {
|
|
@@ -1289,6 +1312,104 @@ $("execDecayCopy").addEventListener("click", () => { if (lastExecutionDecay) cop
|
|
| 1289 |
$("execDecayCsv").addEventListener("click", () => { if (lastExecutionDecay) downloadCsv(`inferscale_execution-decay-sweep_${stamp()}.csv`, execDecayHeaders, execDecayTableRows(lastExecutionDecay)); });
|
| 1290 |
$("execDecayJson").addEventListener("click", () => { if (lastExecutionDecay) downloadNamedJson("inferscale_execution-decay-sweep", lastExecutionDecay); });
|
| 1291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1292 |
function syncAgentControls() {
|
| 1293 |
const retention = $("agentRetention").value;
|
| 1294 |
const routing = $("agentRouting").value;
|
|
|
|
| 22 |
let lastExecutionCompare = null;
|
| 23 |
let lastExecutionThreshold = null;
|
| 24 |
let lastExecutionDecay = null;
|
| 25 |
+
let lastExecutionPlanning = null;
|
| 26 |
+
let lastExecutionHorizon = null;
|
| 27 |
+
let lastExecutionBudget = null;
|
| 28 |
let traceRequests = [];
|
| 29 |
|
| 30 |
const COLORS = {
|
|
|
|
| 48 |
if (data.type === "ready") {
|
| 49 |
runtimePill.classList.add("ready");
|
| 50 |
runtimeText.textContent = "Python runtime ready";
|
| 51 |
+
["runBtn", "arenaBtn", "capacityBtn", "topologyBtn", "designBtn", "pairedStudyBtn", "robustStudyBtn", "agentRunBtn", "agentCompareBtn", "agentTtlBtn", "agentMemoryCompareBtn", "agentBudgetBtn", "agentAffinityBtn", "predictiveCompareBtn", "predictiveAlphaBtn", "execRunBtn", "execCompareBtn", "execThresholdBtn", "execDecayBtn", "execPlanningBtn", "execHorizonBtn", "execBudgetBtn"].forEach((id) => { $(id).disabled = false; });
|
| 52 |
syncConditionalControls();
|
| 53 |
window.setTimeout(() => document.body.classList.add("runtime-ready"), 650);
|
| 54 |
return;
|
|
|
|
| 1125 |
transition_decay: num("execDecay"),
|
| 1126 |
confidence_threshold: num("execThreshold"),
|
| 1127 |
host_bandwidth_gbps: num("execBandwidth"),
|
| 1128 |
+
forecast_horizon: num("execHorizon"),
|
| 1129 |
+
prefetch_top_k: num("execTopK"),
|
| 1130 |
+
forecast_discount: num("execDiscount"),
|
| 1131 |
+
forecast_min_score: num("execMinScore"),
|
| 1132 |
+
utility_threshold_ms: num("execUtility"),
|
| 1133 |
...overrides,
|
| 1134 |
};
|
| 1135 |
}
|
|
|
|
| 1160 |
$("execPrecision").textContent = r.prefetch_attempts ? pct(r.prefetch_precision) : "N/A";
|
| 1161 |
$("execCoverage").textContent = pct(r.prefetch_coverage);
|
| 1162 |
$("execWaste").textContent = `${fmt(r.wrong_step_prefetch_gb, 4)} GB`;
|
| 1163 |
+
$("execEce").textContent = pct(p.calibration?.ece || 0);
|
| 1164 |
+
$("execForecastRecall").textContent = pct(r.forecast_recall || 0);
|
| 1165 |
+
$("execUtilization").textContent = r.prefetch_attempts ? pct(r.prefetch_utilization || 0) : "N/A";
|
| 1166 |
+
$("execUnused").textContent = `${fmt(r.unused_prefetch_gb || 0, 4)} GB`;
|
| 1167 |
$("execRunState").textContent = `${s.workflows_completed}/${s.workflows_generated} workflows`;
|
| 1168 |
$("execRunState").className = `tag ${s.workflow_completion_rate === 1 ? "good" : "bad"}`;
|
| 1169 |
const lookahead = result.provenance.lookahead === "oracle-upper-bound" ? "oracle upper bound" : "online/no-lookahead";
|
| 1170 |
+
$("execRunSummary").innerHTML = `<strong>${escapeHtml(result.config.prefetch_policy.replaceAll("_", " "))} policy.</strong> ${p.count} observed transitions; top-1 accuracy ${pct(p.top1_accuracy)} overall and ${pct(p.post_shift_accuracy)} after the workflow shift. Calibration ECE is ${pct(p.calibration?.ece || 0)}. Forecast-set recall is ${pct(r.forecast_recall || 0)}; ${r.prefetch_attempts || 0} prefetch attempts achieved ${pct(r.prefetch_utilization || 0)} eventual utilization and left ${fmt(r.unused_prefetch_gb || 0, 4)} GB unused before eviction/end. Provenance: ${escapeHtml(lookahead)}.`;
|
| 1171 |
|
| 1172 |
const timeline = result.timeline || [];
|
| 1173 |
destroyChart("execTimeline");
|
|
|
|
| 1193 |
data: { datasets },
|
| 1194 |
options: { ...commonChartOptions(), parsing: false, plugins: { legend: lineLegend() }, scales: { x: { type: "linear", beginAtZero: true, title: { display: true, text: "Observed transitions" } }, y: { min: 0, max: 100, title: { display: true, text: "Rolling top-1 accuracy (%)" } } } },
|
| 1195 |
});
|
| 1196 |
+
|
| 1197 |
+
const calibrationBins = p.calibration?.bins || [];
|
| 1198 |
+
destroyChart("execCalibration");
|
| 1199 |
+
charts.execCalibration = new Chart($("execCalibrationChart"), {
|
| 1200 |
+
type: "line",
|
| 1201 |
+
data: { datasets: [
|
| 1202 |
+
{ label: "Observed accuracy", data: calibrationBins.map((b) => ({ x: b.mean_confidence * 100, y: b.accuracy * 100 })), borderColor: COLORS.blue, backgroundColor: COLORS.blue, pointRadius: 5, tension: .08, fill: false },
|
| 1203 |
+
{ label: "Perfect calibration", data: [{ x: 0, y: 0 }, { x: 100, y: 100 }], borderColor: COLORS.steel, backgroundColor: COLORS.steel, borderDash: [6, 5], pointRadius: 0, fill: false },
|
| 1204 |
+
] },
|
| 1205 |
+
options: { ...commonChartOptions(), parsing: false, plugins: { legend: lineLegend() }, scales: { x: { type: "linear", min: 0, max: 100, title: { display: true, text: "Mean predicted confidence (%)" } }, y: { min: 0, max: 100, title: { display: true, text: "Observed top-1 accuracy (%)" } } } },
|
| 1206 |
+
});
|
| 1207 |
}
|
| 1208 |
|
| 1209 |
$("execRunBtn").addEventListener("click", async () => {
|
|
|
|
| 1312 |
$("execDecayCsv").addEventListener("click", () => { if (lastExecutionDecay) downloadCsv(`inferscale_execution-decay-sweep_${stamp()}.csv`, execDecayHeaders, execDecayTableRows(lastExecutionDecay)); });
|
| 1313 |
$("execDecayJson").addEventListener("click", () => { if (lastExecutionDecay) downloadNamedJson("inferscale_execution-decay-sweep", lastExecutionDecay); });
|
| 1314 |
|
| 1315 |
+
|
| 1316 |
+
const execPlanningHeaders = ["Policy", "p95 TTFT", "Forecast recall", "Prefix hit", "Utilization", "Next-step precision", "Mean HBM", "Unused prefetch", "Saved prefill", "ECE"];
|
| 1317 |
+
function execPlanningTableRows(result) {
|
| 1318 |
+
return result.rows.map((r) => [
|
| 1319 |
+
r.label, `${fmt(r.p95_step_ttft_ms)} ms`, pct(r.forecast_recall), pct(r.prefix_hit_rate),
|
| 1320 |
+
r.prefetch_coverage ? pct(r.prefetch_utilization) : "N/A", r.prefetch_coverage ? pct(r.prefetch_precision) : "N/A",
|
| 1321 |
+
`${fmt(r.mean_hbm_gb, 4)} GB`, `${fmt(r.unused_prefetch_gb, 4)} GB`, `${fmt(r.prefill_tokens_saved, 0)} tok`, pct(r.ece),
|
| 1322 |
+
]);
|
| 1323 |
+
}
|
| 1324 |
+
function renderExecutionPlanning(result) {
|
| 1325 |
+
lastExecutionPlanning = result;
|
| 1326 |
+
$("execPlanningEmpty").classList.add("hidden"); $("execPlanningContent").classList.remove("hidden");
|
| 1327 |
+
["execPlanningCopy", "execPlanningCsv", "execPlanningJson"].forEach((id) => { $(id).disabled = false; });
|
| 1328 |
+
$("execPlanningBestTtft").textContent = result.best_ttft_policy || "N/A";
|
| 1329 |
+
$("execPlanningBestEfficiency").textContent = result.best_prefill_per_hbm_policy || "N/A";
|
| 1330 |
+
$("execPlanningHorizon").textContent = `${num("execHorizon")} steps`;
|
| 1331 |
+
$("execPlanningTopK").textContent = `${num("execTopK")} roles`;
|
| 1332 |
+
$("execPlanningRows").innerHTML = execPlanningTableRows(result).map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(String(cell))}</td>`).join("")}</tr>`).join("");
|
| 1333 |
+
const palette = [COLORS.steel, COLORS.blue, COLORS.green, COLORS.amber];
|
| 1334 |
+
destroyChart("execPlanning");
|
| 1335 |
+
charts.execPlanning = new Chart($("execPlanningChart"), {
|
| 1336 |
+
type: "scatter",
|
| 1337 |
+
data: { datasets: result.rows.map((r, i) => ({ label: r.label, data: [{ x: r.mean_hbm_gb, y: r.p95_step_ttft_ms }], backgroundColor: palette[i], borderColor: palette[i], pointRadius: 7, pointHoverRadius: 9 })) },
|
| 1338 |
+
options: { ...commonChartOptions(), plugins: { legend: pointLegend() }, scales: { x: { beginAtZero: true, title: { display: true, text: "Mean prefix HBM (GB)" } }, y: { beginAtZero: true, title: { display: true, text: "p95 step TTFT (ms)" } } } },
|
| 1339 |
+
});
|
| 1340 |
+
}
|
| 1341 |
+
$("execPlanningBtn").addEventListener("click", async () => {
|
| 1342 |
+
const button = $("execPlanningBtn"); button.disabled = true; button.textContent = "Comparing planners...";
|
| 1343 |
+
try { renderExecutionPlanning(await callPython("execution_planning_study", { config: executionConfigFromUI() })); }
|
| 1344 |
+
catch (error) { alert(`Execution planning study failed: ${error.message}`); }
|
| 1345 |
+
finally { button.disabled = false; button.textContent = "Compare planning policies"; }
|
| 1346 |
+
});
|
| 1347 |
+
$("execPlanningCopy").addEventListener("click", () => { if (lastExecutionPlanning) copyText(tableText(execPlanningHeaders, execPlanningTableRows(lastExecutionPlanning)), "Planning table copied"); });
|
| 1348 |
+
$("execPlanningCsv").addEventListener("click", () => { if (lastExecutionPlanning) downloadCsv(`inferscale_execution-planning-study_${stamp()}.csv`, execPlanningHeaders, execPlanningTableRows(lastExecutionPlanning)); });
|
| 1349 |
+
$("execPlanningJson").addEventListener("click", () => { if (lastExecutionPlanning) downloadNamedJson("inferscale_execution-planning-study", lastExecutionPlanning); });
|
| 1350 |
+
|
| 1351 |
+
const execHorizonHeaders = ["Horizon", "p95 TTFT", "Forecast recall", "Prefix hit", "Utilization", "Unused prefetch", "Mean HBM", "Saved prefill"];
|
| 1352 |
+
function execHorizonTableRows(result) {
|
| 1353 |
+
return result.rows.map((r) => [r.horizon, `${fmt(r.p95_step_ttft_ms)} ms`, pct(r.forecast_recall), pct(r.prefix_hit_rate), r.prefetch_coverage ? pct(r.prefetch_utilization) : "N/A", `${fmt(r.unused_prefetch_gb, 4)} GB`, `${fmt(r.mean_hbm_gb, 4)} GB`, `${fmt(r.prefill_tokens_saved, 0)} tok`]);
|
| 1354 |
+
}
|
| 1355 |
+
function renderExecutionHorizon(result) {
|
| 1356 |
+
lastExecutionHorizon = result;
|
| 1357 |
+
$("execHorizonEmpty").classList.add("hidden"); $("execHorizonContent").classList.remove("hidden");
|
| 1358 |
+
["execHorizonCopy", "execHorizonCsv", "execHorizonJson"].forEach((id) => { $(id).disabled = false; });
|
| 1359 |
+
$("execHorizonBestTtft").textContent = `${result.best_ttft_horizon ?? "N/A"} steps`;
|
| 1360 |
+
$("execHorizonBestUtil").textContent = `${result.best_utilization_horizon ?? "N/A"} steps`;
|
| 1361 |
+
$("execHorizonTopK").textContent = `${num("execTopK")} roles`;
|
| 1362 |
+
$("execHorizonBudget").textContent = `${fmt(num("execCacheBudget"), 2)}x`;
|
| 1363 |
+
$("execHorizonRows").innerHTML = execHorizonTableRows(result).map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(String(cell))}</td>`).join("")}</tr>`).join("");
|
| 1364 |
+
destroyChart("execHorizon");
|
| 1365 |
+
charts.execHorizon = new Chart($("execHorizonChart"), {
|
| 1366 |
+
type: "line",
|
| 1367 |
+
data: { datasets: [
|
| 1368 |
+
{ label: "p95 step TTFT", data: result.rows.map((r) => ({ x: r.horizon, y: r.p95_step_ttft_ms })), borderColor: COLORS.blue, backgroundColor: COLORS.blue, pointRadius: 4, yAxisID: "yLatency", fill: false },
|
| 1369 |
+
{ label: "Prefetch utilization", data: result.rows.map((r) => ({ x: r.horizon, y: r.prefetch_utilization * 100 })), borderColor: COLORS.green, backgroundColor: COLORS.green, pointRadius: 4, yAxisID: "yPct", fill: false },
|
| 1370 |
+
{ label: "Forecast recall", data: result.rows.map((r) => ({ x: r.horizon, y: r.forecast_recall * 100 })), borderColor: COLORS.amber, backgroundColor: COLORS.amber, pointRadius: 4, borderDash: [6, 4], yAxisID: "yPct", fill: false },
|
| 1371 |
+
] },
|
| 1372 |
+
options: { ...commonChartOptions(), parsing: false, plugins: { legend: lineLegend() }, scales: { x: { type: "linear", min: 1, max: 5, ticks: { stepSize: 1 }, title: { display: true, text: "Forecast horizon (steps)" } }, yLatency: { position: "left", beginAtZero: true, title: { display: true, text: "p95 TTFT (ms)" } }, yPct: { position: "right", min: 0, max: 100, grid: { drawOnChartArea: false }, title: { display: true, text: "Recall / utilization (%)" } } } },
|
| 1373 |
+
});
|
| 1374 |
+
}
|
| 1375 |
+
$("execHorizonBtn").addEventListener("click", async () => {
|
| 1376 |
+
const button = $("execHorizonBtn"); button.disabled = true; button.textContent = "Sweeping horizons...";
|
| 1377 |
+
try { renderExecutionHorizon(await callPython("execution_horizon_sweep", { config: executionConfigFromUI() })); }
|
| 1378 |
+
catch (error) { alert(`Forecast horizon sweep failed: ${error.message}`); }
|
| 1379 |
+
finally { button.disabled = false; button.textContent = "Sweep forecast horizon"; }
|
| 1380 |
+
});
|
| 1381 |
+
$("execHorizonCopy").addEventListener("click", () => { if (lastExecutionHorizon) copyText(tableText(execHorizonHeaders, execHorizonTableRows(lastExecutionHorizon)), "Horizon table copied"); });
|
| 1382 |
+
$("execHorizonCsv").addEventListener("click", () => { if (lastExecutionHorizon) downloadCsv(`inferscale_execution-horizon-sweep_${stamp()}.csv`, execHorizonHeaders, execHorizonTableRows(lastExecutionHorizon)); });
|
| 1383 |
+
$("execHorizonJson").addEventListener("click", () => { if (lastExecutionHorizon) downloadNamedJson("inferscale_execution-horizon-sweep", lastExecutionHorizon); });
|
| 1384 |
+
|
| 1385 |
+
const execBudgetHeaders = ["Budget", "Policy", "p95 TTFT", "Forecast recall", "Prefix hit", "Utilization", "Mean HBM", "Unused prefetch", "Pressure evictions"];
|
| 1386 |
+
function execBudgetTableRows(result) {
|
| 1387 |
+
return result.rows.map((r) => [`${fmt(r.budget, 2)}x`, r.policy_label, `${fmt(r.p95_step_ttft_ms)} ms`, pct(r.forecast_recall), pct(r.prefix_hit_rate), r.prefetch_coverage ? pct(r.prefetch_utilization) : "N/A", `${fmt(r.mean_hbm_gb, 4)} GB`, `${fmt(r.unused_prefetch_gb, 4)} GB`, fmt(r.pressure_evictions, 0)]);
|
| 1388 |
+
}
|
| 1389 |
+
function renderExecutionBudget(result) {
|
| 1390 |
+
lastExecutionBudget = result;
|
| 1391 |
+
$("execBudgetEmpty").classList.add("hidden"); $("execBudgetContent").classList.remove("hidden");
|
| 1392 |
+
["execBudgetCopy", "execBudgetCsv", "execBudgetJson"].forEach((id) => { $(id).disabled = false; });
|
| 1393 |
+
$("execBudgetRows").innerHTML = execBudgetTableRows(result).map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(String(cell))}</td>`).join("")}</tr>`).join("");
|
| 1394 |
+
const policyColors = { "Top-1": COLORS.steel, "Multi-step": COLORS.blue, "Utility-aware": COLORS.green };
|
| 1395 |
+
const policies = ["Top-1", "Multi-step", "Utility-aware"];
|
| 1396 |
+
destroyChart("execBudget");
|
| 1397 |
+
charts.execBudget = new Chart($("execBudgetChart"), {
|
| 1398 |
+
type: "line",
|
| 1399 |
+
data: { datasets: policies.map((policy) => ({ label: policy, data: result.rows.filter((r) => r.policy_label === policy).map((r) => ({ x: r.budget, y: r.p95_step_ttft_ms })), borderColor: policyColors[policy], backgroundColor: policyColors[policy], pointRadius: 4, tension: .08, fill: false })) },
|
| 1400 |
+
options: { ...commonChartOptions(), parsing: false, plugins: { legend: lineLegend() }, scales: { x: { type: "linear", min: .25, max: 1.05, title: { display: true, text: "Prefix-cache budget (working-set x)" } }, y: { beginAtZero: true, title: { display: true, text: "p95 step TTFT (ms)" } } } },
|
| 1401 |
+
});
|
| 1402 |
+
}
|
| 1403 |
+
$("execBudgetBtn").addEventListener("click", async () => {
|
| 1404 |
+
const button = $("execBudgetBtn"); button.disabled = true; button.textContent = "Sweeping cache budget...";
|
| 1405 |
+
try { renderExecutionBudget(await callPython("execution_budget_sweep", { config: executionConfigFromUI() })); }
|
| 1406 |
+
catch (error) { alert(`Cache budget study failed: ${error.message}`); }
|
| 1407 |
+
finally { button.disabled = false; button.textContent = "Sweep cache budget"; }
|
| 1408 |
+
});
|
| 1409 |
+
$("execBudgetCopy").addEventListener("click", () => { if (lastExecutionBudget) copyText(tableText(execBudgetHeaders, execBudgetTableRows(lastExecutionBudget)), "Cache budget table copied"); });
|
| 1410 |
+
$("execBudgetCsv").addEventListener("click", () => { if (lastExecutionBudget) downloadCsv(`inferscale_execution-cache-budget-sweep_${stamp()}.csv`, execBudgetHeaders, execBudgetTableRows(lastExecutionBudget)); });
|
| 1411 |
+
$("execBudgetJson").addEventListener("click", () => { if (lastExecutionBudget) downloadNamedJson("inferscale_execution-cache-budget-sweep", lastExecutionBudget); });
|
| 1412 |
+
|
| 1413 |
function syncAgentControls() {
|
| 1414 |
const retention = $("agentRetention").value;
|
| 1415 |
const routing = $("agentRouting").value;
|
docs/architecture.md
CHANGED
|
@@ -15,7 +15,8 @@ InferScale-Sim separates **serving-system logic**, **analytical latency estimati
|
|
| 15 |
9. `research.py` implements paired common-seed A/B studies, bootstrap intervals, and analytical-model sensitivity analysis.
|
| 16 |
10. `agentic.py` models multi-turn programs, tool gaps, session routing, KV retention/TTL eviction, host tiering, and online tool-gap prediction.
|
| 17 |
11. `execution.py` models online agent-role transition learning and bounded static-prefix prefetch under a shifting workflow distribution.
|
| 18 |
-
12. `
|
|
|
|
| 19 |
13. `api.py` exposes JSON-like actions to local Python and Pyodide.
|
| 20 |
|
| 21 |
## Colocated path
|
|
|
|
| 15 |
9. `research.py` implements paired common-seed A/B studies, bootstrap intervals, and analytical-model sensitivity analysis.
|
| 16 |
10. `agentic.py` models multi-turn programs, tool gaps, session routing, KV retention/TTL eviction, host tiering, and online tool-gap prediction.
|
| 17 |
11. `execution.py` models online agent-role transition learning and bounded static-prefix prefetch under a shifting workflow distribution.
|
| 18 |
+
12. `execution.py` also contains online transition calibration, multi-step forecast planning, and cache-budget/horizon study helpers.
|
| 19 |
+
13. `validation.py` compares predictions against externally supplied measured cases.
|
| 20 |
13. `api.py` exposes JSON-like actions to local Python and Pyodide.
|
| 21 |
|
| 22 |
## Colocated path
|
docs/methodology.md
CHANGED
|
@@ -231,27 +231,49 @@ The EWMA alpha sweep replays the exact same shifted trace for every candidate al
|
|
| 231 |
|
| 232 |
Execution Learning models a different reuse object from Agent Sessions. Agent Sessions tracks one user's growing cross-turn KV state. Execution Learning tracks reusable *static agent prefixes* (for example planner/retriever/reasoner system context) that can be shared across workflows.
|
| 233 |
|
| 234 |
-
A synthetic workflow is generated from a first-order role-transition matrix over five agent roles plus an `END` state. At `shift_fraction` of the arrival horizon a second transition matrix becomes active
|
| 235 |
|
| 236 |
-
The
|
| 237 |
|
| 238 |
-
|
| 239 |
|
| 240 |
-
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
- prefix hit rate and prefill tokens saved;
|
| 246 |
- HBM GB-seconds and pressure evictions;
|
| 247 |
- p95 step TTFT and workflow E2E.
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
### Confidence-threshold study
|
| 252 |
|
| 253 |
-
|
| 254 |
|
| 255 |
-
|
| 256 |
|
| 257 |
-
|
|
|
|
| 231 |
|
| 232 |
Execution Learning models a different reuse object from Agent Sessions. Agent Sessions tracks one user's growing cross-turn KV state. Execution Learning tracks reusable *static agent prefixes* (for example planner/retriever/reasoner system context) that can be shared across workflows.
|
| 233 |
|
| 234 |
+
A synthetic workflow is generated from a first-order role-transition matrix over five agent roles plus an `END` state. At `shift_fraction` of the arrival horizon a second transition matrix becomes active. Every candidate in a controlled study replays the exact same workflows.
|
| 235 |
|
| 236 |
+
The learner stores a transition row for each current role. Prediction occurs **before** the next role is observable, and the model is updated only when that next role becomes ready after the tool gap. `transition_decay = 1` is cumulative counting; smaller values exponentially forget older evidence.
|
| 237 |
|
| 238 |
+
### Multi-step forecast
|
| 239 |
|
| 240 |
+
For a current role, InferScale rolls the learned transition matrix forward for `forecast_horizon` steps. Future-step distributions are accumulated with geometric discount `forecast_discount`. This produces an expected discounted future-visit score for each role. No realized future workflow step is read during online policies.
|
| 241 |
|
| 242 |
+
`multistep` chooses up to `prefetch_top_k` roles from that ranking when their normalized score exceeds `forecast_min_score`. `utility` uses the same learned forecast but requires positive decision value above `utility_threshold_ms`.
|
| 243 |
+
|
| 244 |
+
The reference utility for role `r` is:
|
| 245 |
+
|
| 246 |
+
```text
|
| 247 |
+
forecast_score(r) * avoided_static_prefix_prefill_ms(r)
|
| 248 |
+
- host_transfer_ms(r)
|
| 249 |
+
- forecast_weighted_eviction_recompute_ms
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
The eviction term estimates the opportunity cost of removing currently cached prefixes that the same forecast considers likely to be needed. This is a transparent heuristic objective, not a claim of globally optimal cache control.
|
| 253 |
+
|
| 254 |
+
### Prediction calibration and action quality
|
| 255 |
+
|
| 256 |
+
InferScale keeps prediction and action metrics separate. The transition model reports:
|
| 257 |
+
|
| 258 |
+
- top-1 accuracy before/after the shift;
|
| 259 |
+
- multiclass Brier score;
|
| 260 |
+
- multiclass log loss;
|
| 261 |
+
- top-1 expected calibration error (ECE).
|
| 262 |
+
|
| 263 |
+
The serving policy reports:
|
| 264 |
+
|
| 265 |
+
- forecast-set recall over the configured horizon;
|
| 266 |
+
- immediate next-step prefetch precision;
|
| 267 |
+
- eventual prefetch utilization (a prefetched prefix was actually consumed before eviction/end);
|
| 268 |
+
- unused prefetch volume;
|
| 269 |
- prefix hit rate and prefill tokens saved;
|
| 270 |
- HBM GB-seconds and pressure evictions;
|
| 271 |
- p95 step TTFT and workflow E2E.
|
| 272 |
|
| 273 |
+
This distinction matters because multi-step prefetch can be useful even when a prefetched role is not the immediate next step, while a highly accurate top-1 predictor can still make poor resource decisions.
|
|
|
|
|
|
|
| 274 |
|
| 275 |
+
### Controlled studies
|
| 276 |
|
| 277 |
+
The Confidence Threshold Study varies when one-step learned predictions are acted on. The Forgetting-Rate Study varies transition decay. The Prefetch Planning Study compares top-1, multi-step top-k, utility-aware multi-step, and a clairvoyant realized-future-set information bound. The Forecast Horizon Study varies rollout depth on a common trace. The Cache Budget Study runs top-1, multi-step, and utility-aware planners at multiple shared-prefix working-set budgets.
|
| 278 |
|
| 279 |
+
Oracle next-role and oracle future-set candidates are **clairvoyant information bounds**, not guaranteed serving-performance upper bounds. Perfect future information may still produce poor global resource behavior when multiple workflows share a small cache and serialized transfer path.
|
docs/research.md
CHANGED
|
@@ -143,7 +143,7 @@ Source: https://arxiv.org/abs/2604.26968
|
|
| 143 |
|
| 144 |
CacheScout argues that future KV reuse in multi-agent systems is governed by execution semantics rather than recency alone. It learns agent execution transitions online without requiring predefined workflow graphs or offline training, then uses the learned model for cache eviction and proactive prefetching.
|
| 145 |
|
| 146 |
-
InferScale inspiration: the **Execution Learning** lab
|
| 147 |
|
| 148 |
Source: https://arxiv.org/abs/2608.14624
|
| 149 |
|
|
@@ -151,7 +151,7 @@ Source: https://arxiv.org/abs/2608.14624
|
|
| 151 |
|
| 152 |
PBKV predicts several future agent invocations in dynamic workflows and uses those predictions conservatively for cache retention and prefetching. Its motivation is particularly relevant when execution paths are input-dependent rather than a fixed workflow graph.
|
| 153 |
|
| 154 |
-
InferScale inspiration:
|
| 155 |
|
| 156 |
Source: https://arxiv.org/abs/2605.06472
|
| 157 |
|
|
|
|
| 143 |
|
| 144 |
CacheScout argues that future KV reuse in multi-agent systems is governed by execution semantics rather than recency alone. It learns agent execution transitions online without requiring predefined workflow graphs or offline training, then uses the learned model for cache eviction and proactive prefetching.
|
| 145 |
|
| 146 |
+
InferScale inspiration: the **Execution Learning** lab implements a deliberately smaller online transition learner over synthetic agent roles. The model is updated only after a transition is observed; learned forecasts can drive one-step or multi-step static-prefix prefetch into a bounded HBM cache. InferScale also measures calibration and downstream action quality separately. It does not reproduce CacheScout's vLLM integration, survival policy, or production critical-path design.
|
| 147 |
|
| 148 |
Source: https://arxiv.org/abs/2608.14624
|
| 149 |
|
|
|
|
| 151 |
|
| 152 |
PBKV predicts several future agent invocations in dynamic workflows and uses those predictions conservatively for cache retention and prefetching. Its motivation is particularly relevant when execution paths are input-dependent rather than a fixed workflow graph.
|
| 153 |
|
| 154 |
+
InferScale inspiration: PBKV's multi-step view motivates rolling the learned transition matrix forward rather than acting only on top-1 next-role guesses. InferScale adds a transparent top-k forecast and a reference utility rule that subtracts host-transfer and forecast-weighted eviction cost from expected prefill savings. Confidence, forecast-horizon, and cache-budget studies expose how conservative planning changes coverage, utilization, wasted transfer, HBM pressure, and TTFT. InferScale remains much simpler than PBKV's context-aware predictor and production cache manager.
|
| 155 |
|
| 156 |
Source: https://arxiv.org/abs/2605.06472
|
| 157 |
|
index.html
CHANGED
|
@@ -548,11 +548,17 @@
|
|
| 548 |
<hr />
|
| 549 |
<div class="section-kicker">Prediction and prefetch</div>
|
| 550 |
<div class="field-grid two">
|
| 551 |
-
<label>Prefetch policy<select id="execPolicy"><option value="none">No prefetch</option><option value="cumulative">Cumulative transitions</option><option value="decayed" selected>Decayed
|
| 552 |
<label>Transition decay<input id="execDecay" type="number" min="0.2" max="1" step="0.05" value="0.85" /><span class="unit">fraction</span></label>
|
| 553 |
-
<label>Confidence threshold<input id="execThreshold" type="number" min="0" max="1" step="0.05" value="0.55" /><span class="unit">
|
| 554 |
<label>Host bandwidth<input id="execBandwidth" type="number" min="0.1" step="1" value="32" /><span class="unit">GB/s</span></label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
</div>
|
|
|
|
| 556 |
<button id="execRunBtn" class="primary" disabled>Run execution-learning simulation</button>
|
| 557 |
<div class="button-row"><button id="execRunCopyJson" class="secondary" disabled>Copy JSON</button><button id="execRunJson" class="secondary" disabled>Download JSON</button></div>
|
| 558 |
</aside>
|
|
@@ -571,6 +577,10 @@
|
|
| 571 |
<div class="metric"><span>Prefetch precision</span><strong id="execPrecision">N/A</strong></div>
|
| 572 |
<div class="metric"><span>Prefetch coverage</span><strong id="execCoverage">N/A</strong></div>
|
| 573 |
<div class="metric"><span>Wrong-step prefetch</span><strong id="execWaste">N/A</strong></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
</div>
|
| 575 |
<div class="study-summary" id="execRunSummary"></div>
|
| 576 |
<div class="chart-grid">
|
|
@@ -583,6 +593,10 @@
|
|
| 583 |
<div class="chart-body research-chart"><canvas id="execLearningChart"></canvas></div>
|
| 584 |
</div>
|
| 585 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
</div>
|
| 587 |
</section>
|
| 588 |
|
|
@@ -636,6 +650,57 @@
|
|
| 636 |
<div class="table-wrap"><table><thead><tr><th>Decay</th><th>Pre-shift top-1</th><th>Post-shift top-1</th><th>p95 TTFT</th><th>Prefix hit</th><th>Precision</th><th>Coverage</th><th>Wrong prefetch</th></tr></thead><tbody id="execDecayRows"></tbody></table></div>
|
| 637 |
</div>
|
| 638 |
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
</div>
|
| 640 |
</div>
|
| 641 |
</section>
|
|
@@ -647,11 +712,11 @@
|
|
| 647 |
<article class="panel prose"><div class="section-kicker">P/D disaggregation</div><h2>Role-specific resources and transfer cost</h2><p>Prefill and decode have separate accelerator profiles and worker counts. Prompt KV state crosses a modeled interconnect before decode admission. The simulator reports role utilization, transfer latency, and transfer pressure so the benefit of isolation can be weighed against data-movement overhead.</p></article>
|
| 648 |
<article class="panel prose"><div class="section-kicker">Stateful agent sessions</div><h2>Tool gaps turn KV into a residency and routing decision</h2><p>Agent Sessions preserves program identity and turn order, materializes tool-induced gaps, and tracks HBM retention, TTL expiry, host-memory offload/restore, recomputation, and pressure eviction. Strict affinity, least-load, and bounded-affinity routing expose the tension between cache locality and hot-replica queueing. The per-replica service model remains intentionally serial so state-management effects are not confounded with dynamic batching.</p></article>
|
| 649 |
<article class="panel prose"><div class="section-kicker">Online adaptive tiering</div><h2>Prediction without future-gap leakage</h2><p>Adaptive tiering learns an exponentially weighted estimate from tool calls only after they complete. Global and per-tool predictors can be evaluated on the same non-stationary program trace, while an oracle policy that sees the realized future gap is kept separate as an upper bound. A shift experiment changes slower external-tool durations mid-trace to expose the stability-versus-adaptation trade-off.</p></article>
|
| 650 |
-
<article class="panel prose"><div class="section-kicker">Execution learning</div><h2>Learn
|
| 651 |
<article class="panel prose"><div class="section-kicker">Design search</div><h2>Pareto, not one magic configuration</h2><p>The Design Explorer reports both a raw-performance frontier and a resource-normalized frontier using goodput per accelerator. That prevents a multi-GPU P/D layout from looking unconditionally better merely because it uses more simulated hardware.</p></article>
|
| 652 |
<article class="panel prose"><div class="section-kicker">Trace methodology</div><h2>Generated load or exact replay</h2><p>Poisson, constant, and bursty workloads are open-loop: arrivals are scheduled independently of response completion, so queueing delay is not hidden by client backpressure. Exact CSV/JSON traces can be replayed with their original arrival times and token lengths.</p></article>
|
| 653 |
<article class="panel prose"><div class="section-kicker">Research protocol</div><h2>Paired conclusions, not one lucky seed</h2><p>Research Studies use common random numbers: baseline and treatment receive identical seeds, reducing workload variance in the paired difference. A bootstrap interval summarizes the repeated effect, while a separate sensitivity study perturbs analytical latency scales to reveal conclusions that depend too strongly on one reference profile.</p></article>
|
| 654 |
-
<article class="panel prose wide-method"><div class="section-kicker">Research lineage</div><h2>Research lineage and scope</h2><p>Vidur established the value of simulation for avoiding expensive deployment sweeps. Recent systems have pushed toward heterogeneous and disaggregated serving, communication-aware modeling, stateful workloads, trace replay, and SLA-dependent design-space exploration. InferScale-Sim remains intentionally smaller and inspectable, with paired experiments and sensitivity analysis built into the workflow.</p><div class="paper-grid"><div><strong>Vidur / 2024</strong><span>Predictive profiling, workload-aware serving simulation, configuration search.</span></div><div><strong>TokenSim / 2025</strong><span>Extensible scheduling and memory-management simulation.</span></div><div><strong>Revati / 2026</strong><span>GPU-free time-warp emulation of serving control logic.</span></div><div><strong>LLMServingSim 2.0 / 2026</strong><span>Heterogeneous and disaggregated infrastructure, memory and communication.</span></div><div><strong>Frontier / May 2026</strong><span>P/D disaggregation, runtime optimizations, stateful workloads, Pareto exploration.</span></div><div><strong>HeteroPanacea / Aug 2026</strong><span>Heterogeneous stage specialization motivates resource-aware P/D comparison.</span></div><div><strong>Vanguard / Jun 2026</strong><span>Open-loop replay avoids coordinated omission when studying latency under load.</span></div><div><strong>AgentServeSim / Jun 2026</strong><span>Stateful multi-turn serving motivates session-aware workload modeling.</span></div><div><strong>IdleKV / Jun 2026</strong><span>Tool-call idle windows motivate explicit HBM-to-host KV offload experiments.</span></div><div><strong>SMetric / Jul 2026</strong><span>Cache-local routing can overload hot replicas, motivating bounded affinity.</span></div><div><strong>Continuum / May 2026</strong><span>Per-tool duration history and bounded TTL motivate adaptive retention without clairvoyance.</span></div><div><strong>CacheScout / Jul 2026</strong><span>Online learning of agent execution transitions and between-step prefetch motivates the Execution Learning experiments.</span></div><div><strong>PBKV / May 2026</strong><span>
|
| 655 |
</div>
|
| 656 |
</section>
|
| 657 |
</main>
|
|
|
|
| 548 |
<hr />
|
| 549 |
<div class="section-kicker">Prediction and prefetch</div>
|
| 550 |
<div class="field-grid two">
|
| 551 |
+
<label>Prefetch policy<select id="execPolicy"><option value="none">No prefetch</option><option value="cumulative">Cumulative transitions</option><option value="decayed" selected>Decayed top-1</option><option value="multistep">Multi-step top-k</option><option value="utility">Utility-aware multi-step</option><option value="oracle">Oracle next-role upper bound</option><option value="oracle_horizon">Oracle future-set upper bound</option></select></label>
|
| 552 |
<label>Transition decay<input id="execDecay" type="number" min="0.2" max="1" step="0.05" value="0.85" /><span class="unit">fraction</span></label>
|
| 553 |
+
<label>Confidence threshold<input id="execThreshold" type="number" min="0" max="1" step="0.05" value="0.55" /><span class="unit">top-1</span></label>
|
| 554 |
<label>Host bandwidth<input id="execBandwidth" type="number" min="0.1" step="1" value="32" /><span class="unit">GB/s</span></label>
|
| 555 |
+
<label id="execHorizonLabel">Forecast horizon<input id="execHorizon" type="number" min="1" max="6" step="1" value="3" /><span class="unit">steps</span></label>
|
| 556 |
+
<label id="execTopKLabel">Prefetch top-k<input id="execTopK" type="number" min="1" max="5" step="1" value="2" /><span class="unit">roles</span></label>
|
| 557 |
+
<label id="execDiscountLabel">Forecast discount<input id="execDiscount" type="number" min="0.05" max="1" step="0.05" value="0.75" /><span class="unit">fraction</span></label>
|
| 558 |
+
<label id="execMinScoreLabel">Min forecast score<input id="execMinScore" type="number" min="0" max="1" step="0.05" value="0.10" /><span class="unit">normalized</span></label>
|
| 559 |
+
<label id="execUtilityLabel">Utility threshold<input id="execUtility" type="number" step="1" value="0" /><span class="unit">ms</span></label>
|
| 560 |
</div>
|
| 561 |
+
<p class="muted tiny-note">Multi-step policies roll the learned transition matrix forward without reading the future trace. Utility-aware planning estimates saved prefill time minus host-transfer and forecast-weighted eviction cost.</p>
|
| 562 |
<button id="execRunBtn" class="primary" disabled>Run execution-learning simulation</button>
|
| 563 |
<div class="button-row"><button id="execRunCopyJson" class="secondary" disabled>Copy JSON</button><button id="execRunJson" class="secondary" disabled>Download JSON</button></div>
|
| 564 |
</aside>
|
|
|
|
| 577 |
<div class="metric"><span>Prefetch precision</span><strong id="execPrecision">N/A</strong></div>
|
| 578 |
<div class="metric"><span>Prefetch coverage</span><strong id="execCoverage">N/A</strong></div>
|
| 579 |
<div class="metric"><span>Wrong-step prefetch</span><strong id="execWaste">N/A</strong></div>
|
| 580 |
+
<div class="metric"><span>Calibration ECE</span><strong id="execEce">N/A</strong></div>
|
| 581 |
+
<div class="metric"><span>Forecast recall</span><strong id="execForecastRecall">N/A</strong></div>
|
| 582 |
+
<div class="metric"><span>Prefetch utilization</span><strong id="execUtilization">N/A</strong></div>
|
| 583 |
+
<div class="metric"><span>Unused prefetch</span><strong id="execUnused">N/A</strong></div>
|
| 584 |
</div>
|
| 585 |
<div class="study-summary" id="execRunSummary"></div>
|
| 586 |
<div class="chart-grid">
|
|
|
|
| 593 |
<div class="chart-body research-chart"><canvas id="execLearningChart"></canvas></div>
|
| 594 |
</div>
|
| 595 |
</div>
|
| 596 |
+
<div class="chart-card full" data-chart-card data-chart-name="execution-transition-calibration">
|
| 597 |
+
<div class="chart-head"><div class="chart-title">Transition confidence reliability</div><div class="chart-actions"><button class="chart-download" type="button">Download PNG</button><button class="chart-expand" type="button">Expand</button></div></div>
|
| 598 |
+
<div class="chart-body research-chart"><canvas id="execCalibrationChart"></canvas></div>
|
| 599 |
+
</div>
|
| 600 |
</div>
|
| 601 |
</section>
|
| 602 |
|
|
|
|
| 650 |
<div class="table-wrap"><table><thead><tr><th>Decay</th><th>Pre-shift top-1</th><th>Post-shift top-1</th><th>p95 TTFT</th><th>Prefix hit</th><th>Precision</th><th>Coverage</th><th>Wrong prefetch</th></tr></thead><tbody id="execDecayRows"></tbody></table></div>
|
| 651 |
</div>
|
| 652 |
</section>
|
| 653 |
+
|
| 654 |
+
<section class="panel research-panel">
|
| 655 |
+
<div class="panel-title-row"><div><div class="section-kicker">Multi-step decision quality</div><h2>Prefetch Planning Study</h2><p class="muted">Compare one-step prediction with multi-step forecasting, a utility-aware planner, and a clairvoyant future-set information bound on one common shifted workflow trace.</p></div><button id="execPlanningBtn" class="primary compact" disabled>Compare planning policies</button></div>
|
| 656 |
+
<div id="execPlanningEmpty" class="empty-state small"><h3>No planning study yet</h3><p>This experiment separates next-role accuracy from forecast-set recall, cache utilization, unused prefetch traffic, and actual serving latency.</p></div>
|
| 657 |
+
<div id="execPlanningContent" class="hidden">
|
| 658 |
+
<div class="metric-grid four">
|
| 659 |
+
<div class="metric emphasis"><span>Lowest p95 TTFT</span><strong id="execPlanningBestTtft">N/A</strong></div>
|
| 660 |
+
<div class="metric"><span>Best prefill / HBM</span><strong id="execPlanningBestEfficiency">N/A</strong></div>
|
| 661 |
+
<div class="metric"><span>Forecast horizon</span><strong id="execPlanningHorizon">N/A</strong></div>
|
| 662 |
+
<div class="metric"><span>Top-k</span><strong id="execPlanningTopK">N/A</strong></div>
|
| 663 |
+
</div>
|
| 664 |
+
<div class="chart-card full" data-chart-card data-chart-name="execution-planning-tradeoff">
|
| 665 |
+
<div class="chart-head"><div class="chart-title">Mean HBM residency vs p95 TTFT</div><div class="chart-actions"><button class="chart-download" type="button">Download PNG</button><button class="chart-expand" type="button">Expand</button></div></div>
|
| 666 |
+
<div class="chart-body research-chart"><canvas id="execPlanningChart"></canvas></div>
|
| 667 |
+
</div>
|
| 668 |
+
<div class="table-toolbar"><span>Common-trace planning results</span><div><button id="execPlanningCopy" class="mini-button" disabled>Copy table</button><button id="execPlanningCsv" class="mini-button" disabled>Download CSV</button><button id="execPlanningJson" class="mini-button" disabled>Download JSON</button></div></div>
|
| 669 |
+
<div class="table-wrap"><table><thead><tr><th>Policy</th><th>p95 TTFT</th><th>Forecast recall</th><th>Prefix hit</th><th>Utilization</th><th>Next-step precision</th><th>Mean HBM</th><th>Unused prefetch</th><th>Saved prefill</th><th>ECE</th></tr></thead><tbody id="execPlanningRows"></tbody></table></div>
|
| 670 |
+
</div>
|
| 671 |
+
</section>
|
| 672 |
+
|
| 673 |
+
<section class="panel research-panel">
|
| 674 |
+
<div class="panel-title-row"><div><div class="section-kicker">How far ahead?</div><h2>Forecast Horizon Study</h2><p class="muted">Sweep a utility-aware planner from one-step prediction to longer rollout horizons. Longer forecasts can reveal reuse but can also waste bandwidth or evict nearer-term prefixes.</p></div><button id="execHorizonBtn" class="primary compact" disabled>Sweep forecast horizon</button></div>
|
| 675 |
+
<div id="execHorizonEmpty" class="empty-state small"><h3>No horizon sweep yet</h3><p>Each horizon replays the exact same shifted workflow trace and uses the same cache budget and learned transition model.</p></div>
|
| 676 |
+
<div id="execHorizonContent" class="hidden">
|
| 677 |
+
<div class="metric-grid four">
|
| 678 |
+
<div class="metric emphasis"><span>Best TTFT horizon</span><strong id="execHorizonBestTtft">N/A</strong></div>
|
| 679 |
+
<div class="metric"><span>Best utilization horizon</span><strong id="execHorizonBestUtil">N/A</strong></div>
|
| 680 |
+
<div class="metric"><span>Configured top-k</span><strong id="execHorizonTopK">N/A</strong></div>
|
| 681 |
+
<div class="metric"><span>Cache budget</span><strong id="execHorizonBudget">N/A</strong></div>
|
| 682 |
+
</div>
|
| 683 |
+
<div class="chart-card full" data-chart-card data-chart-name="execution-forecast-horizon">
|
| 684 |
+
<div class="chart-head"><div class="chart-title">Forecast horizon vs latency and prefetch utilization</div><div class="chart-actions"><button class="chart-download" type="button">Download PNG</button><button class="chart-expand" type="button">Expand</button></div></div>
|
| 685 |
+
<div class="chart-body research-chart"><canvas id="execHorizonChart"></canvas></div>
|
| 686 |
+
</div>
|
| 687 |
+
<div class="table-toolbar"><span>Horizon sweep</span><div><button id="execHorizonCopy" class="mini-button" disabled>Copy table</button><button id="execHorizonCsv" class="mini-button" disabled>Download CSV</button><button id="execHorizonJson" class="mini-button" disabled>Download JSON</button></div></div>
|
| 688 |
+
<div class="table-wrap"><table><thead><tr><th>Horizon</th><th>p95 TTFT</th><th>Forecast recall</th><th>Prefix hit</th><th>Utilization</th><th>Unused prefetch</th><th>Mean HBM</th><th>Saved prefill</th></tr></thead><tbody id="execHorizonRows"></tbody></table></div>
|
| 689 |
+
</div>
|
| 690 |
+
</section>
|
| 691 |
+
|
| 692 |
+
<section class="panel research-panel">
|
| 693 |
+
<div class="panel-title-row"><div><div class="section-kicker">Scarce-cache regime</div><h2>Cache Budget Study</h2><p class="muted">Compare top-1, multi-step, and utility-aware planning while shrinking or expanding the shared prefix-cache budget. The useful policy can change when speculative prefixes compete for HBM.</p></div><button id="execBudgetBtn" class="primary compact" disabled>Sweep cache budget</button></div>
|
| 694 |
+
<div id="execBudgetEmpty" class="empty-state small"><h3>No cache-budget sweep yet</h3><p>The study executes 12 controlled simulations: three planning policies across four working-set-relative HBM budgets.</p></div>
|
| 695 |
+
<div id="execBudgetContent" class="hidden">
|
| 696 |
+
<div class="chart-card full" data-chart-card data-chart-name="execution-cache-budget-policy">
|
| 697 |
+
<div class="chart-head"><div class="chart-title">Cache budget vs p95 TTFT by planning policy</div><div class="chart-actions"><button class="chart-download" type="button">Download PNG</button><button class="chart-expand" type="button">Expand</button></div></div>
|
| 698 |
+
<div class="chart-body research-chart"><canvas id="execBudgetChart"></canvas></div>
|
| 699 |
+
</div>
|
| 700 |
+
<div class="table-toolbar"><span>Cache-budget policy sweep</span><div><button id="execBudgetCopy" class="mini-button" disabled>Copy table</button><button id="execBudgetCsv" class="mini-button" disabled>Download CSV</button><button id="execBudgetJson" class="mini-button" disabled>Download JSON</button></div></div>
|
| 701 |
+
<div class="table-wrap"><table><thead><tr><th>Budget</th><th>Policy</th><th>p95 TTFT</th><th>Forecast recall</th><th>Prefix hit</th><th>Utilization</th><th>Mean HBM</th><th>Unused prefetch</th><th>Pressure evictions</th></tr></thead><tbody id="execBudgetRows"></tbody></table></div>
|
| 702 |
+
</div>
|
| 703 |
+
</section>
|
| 704 |
</div>
|
| 705 |
</div>
|
| 706 |
</section>
|
|
|
|
| 712 |
<article class="panel prose"><div class="section-kicker">P/D disaggregation</div><h2>Role-specific resources and transfer cost</h2><p>Prefill and decode have separate accelerator profiles and worker counts. Prompt KV state crosses a modeled interconnect before decode admission. The simulator reports role utilization, transfer latency, and transfer pressure so the benefit of isolation can be weighed against data-movement overhead.</p></article>
|
| 713 |
<article class="panel prose"><div class="section-kicker">Stateful agent sessions</div><h2>Tool gaps turn KV into a residency and routing decision</h2><p>Agent Sessions preserves program identity and turn order, materializes tool-induced gaps, and tracks HBM retention, TTL expiry, host-memory offload/restore, recomputation, and pressure eviction. Strict affinity, least-load, and bounded-affinity routing expose the tension between cache locality and hot-replica queueing. The per-replica service model remains intentionally serial so state-management effects are not confounded with dynamic batching.</p></article>
|
| 714 |
<article class="panel prose"><div class="section-kicker">Online adaptive tiering</div><h2>Prediction without future-gap leakage</h2><p>Adaptive tiering learns an exponentially weighted estimate from tool calls only after they complete. Global and per-tool predictors can be evaluated on the same non-stationary program trace, while an oracle policy that sees the realized future gap is kept separate as an upper bound. A shift experiment changes slower external-tool durations mid-trace to expose the stability-versus-adaptation trade-off.</p></article>
|
| 715 |
+
<article class="panel prose"><div class="section-kicker">Execution learning</div><h2>Learn transitions, forecast multiple steps, then spend cache carefully</h2><p>Execution Learning updates a first-order role-transition model only after the next role is observed. The learned matrix can be rolled forward for multi-step forecasts without future-trace access. A utility-aware planner weighs expected prefill savings against host-transfer cost and forecast-weighted eviction cost, while calibration, horizon, confidence, forgetting, and cache-budget studies expose where better predictions do or do not improve serving outcomes.</p></article>
|
| 716 |
<article class="panel prose"><div class="section-kicker">Design search</div><h2>Pareto, not one magic configuration</h2><p>The Design Explorer reports both a raw-performance frontier and a resource-normalized frontier using goodput per accelerator. That prevents a multi-GPU P/D layout from looking unconditionally better merely because it uses more simulated hardware.</p></article>
|
| 717 |
<article class="panel prose"><div class="section-kicker">Trace methodology</div><h2>Generated load or exact replay</h2><p>Poisson, constant, and bursty workloads are open-loop: arrivals are scheduled independently of response completion, so queueing delay is not hidden by client backpressure. Exact CSV/JSON traces can be replayed with their original arrival times and token lengths.</p></article>
|
| 718 |
<article class="panel prose"><div class="section-kicker">Research protocol</div><h2>Paired conclusions, not one lucky seed</h2><p>Research Studies use common random numbers: baseline and treatment receive identical seeds, reducing workload variance in the paired difference. A bootstrap interval summarizes the repeated effect, while a separate sensitivity study perturbs analytical latency scales to reveal conclusions that depend too strongly on one reference profile.</p></article>
|
| 719 |
+
<article class="panel prose wide-method"><div class="section-kicker">Research lineage</div><h2>Research lineage and scope</h2><p>Vidur established the value of simulation for avoiding expensive deployment sweeps. Recent systems have pushed toward heterogeneous and disaggregated serving, communication-aware modeling, stateful workloads, trace replay, and SLA-dependent design-space exploration. InferScale-Sim remains intentionally smaller and inspectable, with paired experiments and sensitivity analysis built into the workflow.</p><div class="paper-grid"><div><strong>Vidur / 2024</strong><span>Predictive profiling, workload-aware serving simulation, configuration search.</span></div><div><strong>TokenSim / 2025</strong><span>Extensible scheduling and memory-management simulation.</span></div><div><strong>Revati / 2026</strong><span>GPU-free time-warp emulation of serving control logic.</span></div><div><strong>LLMServingSim 2.0 / 2026</strong><span>Heterogeneous and disaggregated infrastructure, memory and communication.</span></div><div><strong>Frontier / May 2026</strong><span>P/D disaggregation, runtime optimizations, stateful workloads, Pareto exploration.</span></div><div><strong>HeteroPanacea / Aug 2026</strong><span>Heterogeneous stage specialization motivates resource-aware P/D comparison.</span></div><div><strong>Vanguard / Jun 2026</strong><span>Open-loop replay avoids coordinated omission when studying latency under load.</span></div><div><strong>AgentServeSim / Jun 2026</strong><span>Stateful multi-turn serving motivates session-aware workload modeling.</span></div><div><strong>IdleKV / Jun 2026</strong><span>Tool-call idle windows motivate explicit HBM-to-host KV offload experiments.</span></div><div><strong>SMetric / Jul 2026</strong><span>Cache-local routing can overload hot replicas, motivating bounded affinity.</span></div><div><strong>Continuum / May 2026</strong><span>Per-tool duration history and bounded TTL motivate adaptive retention without clairvoyance.</span></div><div><strong>CacheScout / Jul 2026</strong><span>Online learning of agent execution transitions and between-step prefetch motivates the Execution Learning experiments.</span></div><div><strong>PBKV / May 2026</strong><span>Multi-step prediction for dynamic workflows motivates forecast-horizon and utility-aware prefetch experiments.</span></div><div><strong>Predictive KV Memory / Aug 2026 rev.</strong><span>Bayesian reuse prediction and multi-tier placement motivate explicit prediction-quality experiments.</span></div><div><strong>SGLang / RadixAttention</strong><span>Automatic shared-prefix KV reuse motivates the controlled cache scenario.</span></div></div></article>
|
| 720 |
</div>
|
| 721 |
</section>
|
| 722 |
</main>
|
py/inferscale/__init__.py
CHANGED
|
@@ -9,7 +9,15 @@ from .agentic import (
|
|
| 9 |
ttl_retention_sweep,
|
| 10 |
)
|
| 11 |
from .api import execute, metadata
|
| 12 |
-
from .execution import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from .models import SimulationConfig
|
| 14 |
from .optimizer import capacity_search, compare_schedulers, compare_topologies, design_space_search
|
| 15 |
from .research import paired_study, robustness_study
|
|
@@ -29,7 +37,10 @@ __all__ = [
|
|
| 29 |
"compare_topologies",
|
| 30 |
"design_space_search",
|
| 31 |
"execute",
|
|
|
|
| 32 |
"execution_decay_sweep",
|
|
|
|
|
|
|
| 33 |
"execution_prefetch_study",
|
| 34 |
"execution_threshold_sweep",
|
| 35 |
"metadata",
|
|
@@ -44,4 +55,4 @@ __all__ = [
|
|
| 44 |
|
| 45 |
# Internal package metadata only; the public project intentionally avoids
|
| 46 |
# release/version branding in the interface and documentation.
|
| 47 |
-
__version__ = "0.
|
|
|
|
| 9 |
ttl_retention_sweep,
|
| 10 |
)
|
| 11 |
from .api import execute, metadata
|
| 12 |
+
from .execution import (
|
| 13 |
+
execution_budget_sweep,
|
| 14 |
+
execution_decay_sweep,
|
| 15 |
+
execution_horizon_sweep,
|
| 16 |
+
execution_planning_study,
|
| 17 |
+
execution_prefetch_study,
|
| 18 |
+
execution_threshold_sweep,
|
| 19 |
+
run_execution_learning,
|
| 20 |
+
)
|
| 21 |
from .models import SimulationConfig
|
| 22 |
from .optimizer import capacity_search, compare_schedulers, compare_topologies, design_space_search
|
| 23 |
from .research import paired_study, robustness_study
|
|
|
|
| 37 |
"compare_topologies",
|
| 38 |
"design_space_search",
|
| 39 |
"execute",
|
| 40 |
+
"execution_budget_sweep",
|
| 41 |
"execution_decay_sweep",
|
| 42 |
+
"execution_horizon_sweep",
|
| 43 |
+
"execution_planning_study",
|
| 44 |
"execution_prefetch_study",
|
| 45 |
"execution_threshold_sweep",
|
| 46 |
"metadata",
|
|
|
|
| 55 |
|
| 56 |
# Internal package metadata only; the public project intentionally avoids
|
| 57 |
# release/version branding in the interface and documentation.
|
| 58 |
+
__version__ = "0.9.0"
|
py/inferscale/api.py
CHANGED
|
@@ -11,7 +11,10 @@ from .agentic import (
|
|
| 11 |
ttl_retention_sweep,
|
| 12 |
)
|
| 13 |
from .execution import (
|
|
|
|
| 14 |
execution_decay_sweep,
|
|
|
|
|
|
|
| 15 |
execution_prefetch_study,
|
| 16 |
execution_threshold_sweep,
|
| 17 |
run_execution_learning,
|
|
@@ -37,6 +40,7 @@ def metadata() -> dict:
|
|
| 37 |
],
|
| 38 |
"execution_learning_modes": [
|
| 39 |
"single_run", "prefetch_policy_compare", "confidence_threshold_sweep", "transition_decay_sweep",
|
|
|
|
| 40 |
],
|
| 41 |
}
|
| 42 |
|
|
@@ -107,6 +111,12 @@ def execute(action: str, payload: dict) -> dict:
|
|
| 107 |
return execution_threshold_sweep(payload.get("config", payload), payload.get("thresholds"))
|
| 108 |
if action == "execution_decay_sweep":
|
| 109 |
return execution_decay_sweep(payload.get("config", payload), payload.get("decay_values"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
if action == "robustness_study":
|
| 111 |
config = payload.get("config", payload)
|
| 112 |
return robustness_study(
|
|
|
|
| 11 |
ttl_retention_sweep,
|
| 12 |
)
|
| 13 |
from .execution import (
|
| 14 |
+
execution_budget_sweep,
|
| 15 |
execution_decay_sweep,
|
| 16 |
+
execution_horizon_sweep,
|
| 17 |
+
execution_planning_study,
|
| 18 |
execution_prefetch_study,
|
| 19 |
execution_threshold_sweep,
|
| 20 |
run_execution_learning,
|
|
|
|
| 40 |
],
|
| 41 |
"execution_learning_modes": [
|
| 42 |
"single_run", "prefetch_policy_compare", "confidence_threshold_sweep", "transition_decay_sweep",
|
| 43 |
+
"multistep_planning", "forecast_horizon_sweep", "cache_budget_policy_sweep",
|
| 44 |
],
|
| 45 |
}
|
| 46 |
|
|
|
|
| 111 |
return execution_threshold_sweep(payload.get("config", payload), payload.get("thresholds"))
|
| 112 |
if action == "execution_decay_sweep":
|
| 113 |
return execution_decay_sweep(payload.get("config", payload), payload.get("decay_values"))
|
| 114 |
+
if action == "execution_planning_study":
|
| 115 |
+
return execution_planning_study(payload.get("config", payload))
|
| 116 |
+
if action == "execution_horizon_sweep":
|
| 117 |
+
return execution_horizon_sweep(payload.get("config", payload), payload.get("horizon_values"))
|
| 118 |
+
if action == "execution_budget_sweep":
|
| 119 |
+
return execution_budget_sweep(payload.get("config", payload), payload.get("budget_values"))
|
| 120 |
if action == "robustness_study":
|
| 121 |
config = payload.get("config", payload)
|
| 122 |
return robustness_study(
|
py/inferscale/execution.py
CHANGED
|
@@ -48,7 +48,7 @@ ROLE_GAP_MULTIPLIERS = {
|
|
| 48 |
"writer": 0.30,
|
| 49 |
}
|
| 50 |
|
| 51 |
-
PREFETCH_POLICIES = {"none", "cumulative", "decayed", "oracle"}
|
| 52 |
|
| 53 |
|
| 54 |
@dataclass
|
|
@@ -74,6 +74,11 @@ class ExecutionLearningConfig:
|
|
| 74 |
transition_decay: float = 0.85
|
| 75 |
confidence_threshold: float = 0.55
|
| 76 |
prior_strength: float = 0.35
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
timeline_points: int = 240
|
| 78 |
|
| 79 |
@classmethod
|
|
@@ -90,6 +95,11 @@ class ExecutionLearningConfig:
|
|
| 90 |
cfg.transition_decay = min(max(float(cfg.transition_decay), 0.20), 1.0)
|
| 91 |
cfg.confidence_threshold = min(max(float(cfg.confidence_threshold), 0.0), 1.0)
|
| 92 |
cfg.prior_strength = max(float(cfg.prior_strength), 0.01)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
cfg.timeline_points = max(40, min(int(cfg.timeline_points), 1000))
|
| 94 |
if cfg.prefetch_policy not in PREFETCH_POLICIES:
|
| 95 |
raise ValueError(f"Unsupported execution prefetch policy: {cfg.prefetch_policy}")
|
|
@@ -123,6 +133,7 @@ class PrefixEntry:
|
|
| 123 |
available_time: float
|
| 124 |
source: str
|
| 125 |
generation: int
|
|
|
|
| 126 |
|
| 127 |
|
| 128 |
class OnlineTransitionPredictor:
|
|
@@ -150,6 +161,59 @@ class OnlineTransitionPredictor:
|
|
| 150 |
best = max(self.targets, key=lambda target: (probabilities[target], -self.targets.index(target)))
|
| 151 |
return best, probabilities[best], probabilities
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
def observe(self, current_role: str, next_role: str) -> None:
|
| 154 |
row = self.rows[current_role]
|
| 155 |
if self.decay < 1.0:
|
|
@@ -225,6 +289,50 @@ def generate_workflows(cfg: ExecutionLearningConfig) -> list[WorkflowSpec]:
|
|
| 225 |
return workflows
|
| 226 |
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
class ExecutionLearningSimulator:
|
| 229 |
"""Reference simulator for online workflow learning and prefix prefetch.
|
| 230 |
|
|
@@ -260,7 +368,10 @@ class ExecutionLearningSimulator:
|
|
| 260 |
self.pressure_evictions = 0
|
| 261 |
self.prefetch_attempts = 0
|
| 262 |
self.prefetch_correct = 0
|
|
|
|
| 263 |
self.prefetch_bytes = 0.0
|
|
|
|
|
|
|
| 264 |
self.wrong_step_prefetch_bytes = 0.0
|
| 265 |
self.transfer_latencies_ms: list[float] = []
|
| 266 |
self.prefix_hits = 0
|
|
@@ -308,6 +419,40 @@ class ExecutionLearningSimulator:
|
|
| 308 |
if len(self.timeline) > self.cfg.timeline_points * 2:
|
| 309 |
self.timeline = self.timeline[::2]
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
def _evict_for(self, role: str, size_gb: float) -> bool:
|
| 312 |
existing = self.cache.get(role)
|
| 313 |
current_without_role = self._used_gb() - (existing.size_gb if existing else 0.0)
|
|
@@ -319,6 +464,7 @@ class ExecutionLearningSimulator:
|
|
| 319 |
return False
|
| 320 |
victim = min(candidates, key=lambda entry: (entry.last_access, entry.role))
|
| 321 |
self._integrate_memory(self.now)
|
|
|
|
| 322 |
self.cache.pop(victim.role, None)
|
| 323 |
self.prefix_source.pop(victim.role, None)
|
| 324 |
self.pressure_evictions += 1
|
|
@@ -331,6 +477,8 @@ class ExecutionLearningSimulator:
|
|
| 331 |
return None
|
| 332 |
self._integrate_memory(self.now)
|
| 333 |
previous = self.cache.get(role)
|
|
|
|
|
|
|
| 334 |
generation = (previous.generation + 1) if previous else 1
|
| 335 |
self.cache[role] = PrefixEntry(role, size_gb, self.now, available_time, source, generation)
|
| 336 |
self.prefix_source[role] = source
|
|
@@ -362,35 +510,103 @@ class ExecutionLearningSimulator:
|
|
| 362 |
return END
|
| 363 |
return workflow.steps[step_index + 1].role
|
| 364 |
|
| 365 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
actual_next = self._actual_next_role(workflow_id, step_index)
|
| 367 |
-
predicted_role =
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
| 372 |
if self.cfg.prefetch_policy == "oracle":
|
| 373 |
predicted_role = actual_next
|
| 374 |
confidence = 1.0
|
|
|
|
| 375 |
probabilities[predicted_role] = 1.0
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
predicted_role
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
self.pending_predictions[(workflow_id, step_index)] = {
|
| 396 |
"workflow_id": workflow_id,
|
|
@@ -399,14 +615,21 @@ class ExecutionLearningSimulator:
|
|
| 399 |
"predicted_role": predicted_role,
|
| 400 |
"confidence": confidence,
|
| 401 |
"probabilities": probabilities,
|
| 402 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
"prefetch_reason": decision_reason,
|
| 404 |
-
"
|
|
|
|
| 405 |
"shifted_regime": self.workflows[workflow_id].steps[step_index].shifted_regime,
|
| 406 |
}
|
| 407 |
|
| 408 |
-
# END is observable immediately at workflow completion. Non-final
|
| 409 |
-
# transitions become observable only after the tool gap has elapsed.
|
| 410 |
if actual_next == END:
|
| 411 |
self._observe_transition(workflow_id, step_index, actual_next)
|
| 412 |
|
|
@@ -418,22 +641,38 @@ class ExecutionLearningSimulator:
|
|
| 418 |
if pending is None:
|
| 419 |
return
|
| 420 |
predicted_role = str(pending["predicted_role"])
|
| 421 |
-
|
| 422 |
correct = predicted_role == actual_next
|
| 423 |
-
if
|
| 424 |
self.prefetch_correct += 1
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
self.prediction_rows.append(
|
| 428 |
pending
|
| 429 |
| {
|
| 430 |
"actual_next_role": actual_next,
|
|
|
|
| 431 |
"prediction_correct": correct,
|
|
|
|
|
|
|
|
|
|
| 432 |
"post_shift": bool(pending["shifted_regime"]),
|
| 433 |
}
|
| 434 |
)
|
| 435 |
-
|
| 436 |
-
if self.cfg.prefetch_policy != "oracle":
|
| 437 |
self.predictor.observe(current_role, actual_next)
|
| 438 |
|
| 439 |
def _start_next(self) -> None:
|
|
@@ -470,6 +709,10 @@ class ExecutionLearningSimulator:
|
|
| 470 |
self.prefetch_hits += 1
|
| 471 |
self.prefill_tokens_saved += prefix_tokens
|
| 472 |
if entry is not None:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
entry.last_access = self.now + prefix_wait_s
|
| 474 |
prefill_tokens = max(step.dynamic_prompt_tokens, 1)
|
| 475 |
else:
|
|
@@ -590,8 +833,19 @@ class ExecutionLearningSimulator:
|
|
| 590 |
def accuracy(rows: list[dict[str, Any]]) -> float:
|
| 591 |
return sum(1 for row in rows if row["prediction_correct"]) / len(rows) if rows else 0.0
|
| 592 |
|
|
|
|
|
|
|
|
|
|
| 593 |
prefetch_precision = self.prefetch_correct / self.prefetch_attempts if self.prefetch_attempts else 0.0
|
| 594 |
-
prefetch_coverage =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
workflow_count = len(self.workflows)
|
| 596 |
completed_count = len(self.workflow_completion)
|
| 597 |
summary = {
|
|
@@ -615,7 +869,12 @@ class ExecutionLearningSimulator:
|
|
| 615 |
"prefetch_correct": self.prefetch_correct,
|
| 616 |
"prefetch_precision": prefetch_precision,
|
| 617 |
"prefetch_coverage": prefetch_coverage,
|
|
|
|
|
|
|
|
|
|
| 618 |
"prefetch_gb": self.prefetch_bytes / 1e9,
|
|
|
|
|
|
|
| 619 |
"wrong_step_prefetch_gb": self.wrong_step_prefetch_bytes / 1e9,
|
| 620 |
"p95_prefetch_transfer_ms": percentile(self.transfer_latencies_ms, 0.95),
|
| 621 |
}
|
|
@@ -624,6 +883,9 @@ class ExecutionLearningSimulator:
|
|
| 624 |
"top1_accuracy": accuracy(predictions),
|
| 625 |
"pre_shift_accuracy": accuracy(pre),
|
| 626 |
"post_shift_accuracy": accuracy(post),
|
|
|
|
|
|
|
|
|
|
| 627 |
"rows": predictions[:4000],
|
| 628 |
"predictor": self.predictor.snapshot(),
|
| 629 |
}
|
|
@@ -638,9 +900,9 @@ class ExecutionLearningSimulator:
|
|
| 638 |
"simulator": "InferScale-Sim",
|
| 639 |
"mode": "online-agent-execution-learning",
|
| 640 |
"latency_profile_type": "analytical-reference",
|
| 641 |
-
"execution_model": "
|
| 642 |
"prefetch_policy": self.cfg.prefetch_policy,
|
| 643 |
-
"lookahead": "oracle-upper-bound" if self.cfg.prefetch_policy
|
| 644 |
"warning": "Execution Learning isolates cross-workflow static-prefix prediction/prefetch from session-KV retention and dynamic batching.",
|
| 645 |
},
|
| 646 |
"summary": summary,
|
|
@@ -672,10 +934,17 @@ def _row(label: str, result: dict[str, Any]) -> dict[str, Any]:
|
|
| 672 |
"prefix_hit_rate": result["resource"]["prefix_hit_rate"],
|
| 673 |
"prefetch_precision": result["resource"]["prefetch_precision"],
|
| 674 |
"prefetch_coverage": result["resource"]["prefetch_coverage"],
|
|
|
|
|
|
|
| 675 |
"mean_hbm_gb": result["resource"]["mean_prefix_hbm_gb"],
|
|
|
|
| 676 |
"wrong_step_prefetch_gb": result["resource"]["wrong_step_prefetch_gb"],
|
|
|
|
| 677 |
"prefill_tokens_saved": result["resource"]["prefill_tokens_saved"],
|
| 678 |
"top1_accuracy": result["prediction"]["top1_accuracy"],
|
|
|
|
|
|
|
|
|
|
| 679 |
"pre_shift_accuracy": result["prediction"]["pre_shift_accuracy"],
|
| 680 |
"post_shift_accuracy": result["prediction"]["post_shift_accuracy"],
|
| 681 |
}
|
|
@@ -790,3 +1059,102 @@ def execution_decay_sweep(
|
|
| 790 |
"association": association,
|
| 791 |
"note": "Better next-role prediction need not minimize serving latency because cache occupancy and transfer timing mediate the effect.",
|
| 792 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
"writer": 0.30,
|
| 49 |
}
|
| 50 |
|
| 51 |
+
PREFETCH_POLICIES = {"none", "cumulative", "decayed", "multistep", "utility", "oracle", "oracle_horizon"}
|
| 52 |
|
| 53 |
|
| 54 |
@dataclass
|
|
|
|
| 74 |
transition_decay: float = 0.85
|
| 75 |
confidence_threshold: float = 0.55
|
| 76 |
prior_strength: float = 0.35
|
| 77 |
+
forecast_horizon: int = 3
|
| 78 |
+
prefetch_top_k: int = 2
|
| 79 |
+
forecast_discount: float = 0.75
|
| 80 |
+
forecast_min_score: float = 0.10
|
| 81 |
+
utility_threshold_ms: float = 0.0
|
| 82 |
timeline_points: int = 240
|
| 83 |
|
| 84 |
@classmethod
|
|
|
|
| 95 |
cfg.transition_decay = min(max(float(cfg.transition_decay), 0.20), 1.0)
|
| 96 |
cfg.confidence_threshold = min(max(float(cfg.confidence_threshold), 0.0), 1.0)
|
| 97 |
cfg.prior_strength = max(float(cfg.prior_strength), 0.01)
|
| 98 |
+
cfg.forecast_horizon = max(1, min(int(cfg.forecast_horizon), 6))
|
| 99 |
+
cfg.prefetch_top_k = max(1, min(int(cfg.prefetch_top_k), len(AGENT_ROLES)))
|
| 100 |
+
cfg.forecast_discount = min(max(float(cfg.forecast_discount), 0.05), 1.0)
|
| 101 |
+
cfg.forecast_min_score = min(max(float(cfg.forecast_min_score), 0.0), 1.0)
|
| 102 |
+
cfg.utility_threshold_ms = float(cfg.utility_threshold_ms)
|
| 103 |
cfg.timeline_points = max(40, min(int(cfg.timeline_points), 1000))
|
| 104 |
if cfg.prefetch_policy not in PREFETCH_POLICIES:
|
| 105 |
raise ValueError(f"Unsupported execution prefetch policy: {cfg.prefetch_policy}")
|
|
|
|
| 133 |
available_time: float
|
| 134 |
source: str
|
| 135 |
generation: int
|
| 136 |
+
used: bool = False
|
| 137 |
|
| 138 |
|
| 139 |
class OnlineTransitionPredictor:
|
|
|
|
| 161 |
best = max(self.targets, key=lambda target: (probabilities[target], -self.targets.index(target)))
|
| 162 |
return best, probabilities[best], probabilities
|
| 163 |
|
| 164 |
+
def forecast(
|
| 165 |
+
self,
|
| 166 |
+
current_role: str,
|
| 167 |
+
*,
|
| 168 |
+
horizon: int = 3,
|
| 169 |
+
discount: float = 0.75,
|
| 170 |
+
) -> dict[str, Any]:
|
| 171 |
+
"""Roll the learned first-order matrix forward without future trace access.
|
| 172 |
+
|
| 173 |
+
``role_scores`` are discounted expected future visits, not calibrated
|
| 174 |
+
probabilities of at-least-one reuse. They are therefore used as a
|
| 175 |
+
ranking signal and are normalized separately for UI/decision thresholds.
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
horizon = max(1, min(int(horizon), 8))
|
| 179 |
+
discount = min(max(float(discount), 0.0), 1.0)
|
| 180 |
+
distributions: list[dict[str, float]] = []
|
| 181 |
+
current: dict[str, float] = {target: 0.0 for target in self.targets}
|
| 182 |
+
current[current_role] = 1.0
|
| 183 |
+
role_scores = {role: 0.0 for role in AGENT_ROLES}
|
| 184 |
+
|
| 185 |
+
for depth in range(1, horizon + 1):
|
| 186 |
+
nxt = {target: 0.0 for target in self.targets}
|
| 187 |
+
for source, mass in current.items():
|
| 188 |
+
if mass <= 0.0:
|
| 189 |
+
continue
|
| 190 |
+
if source == END:
|
| 191 |
+
nxt[END] += mass
|
| 192 |
+
continue
|
| 193 |
+
_, _, probs = self.predict(source)
|
| 194 |
+
for target, probability in probs.items():
|
| 195 |
+
nxt[target] += mass * probability
|
| 196 |
+
distributions.append(nxt)
|
| 197 |
+
weight = discount ** (depth - 1)
|
| 198 |
+
for role in AGENT_ROLES:
|
| 199 |
+
role_scores[role] += weight * nxt.get(role, 0.0)
|
| 200 |
+
current = nxt
|
| 201 |
+
|
| 202 |
+
total_score = sum(role_scores.values())
|
| 203 |
+
normalized = {
|
| 204 |
+
role: (score / total_score if total_score > 1e-12 else 0.0)
|
| 205 |
+
for role, score in role_scores.items()
|
| 206 |
+
}
|
| 207 |
+
ranked = sorted(AGENT_ROLES, key=lambda role: (-role_scores[role], role))
|
| 208 |
+
return {
|
| 209 |
+
"horizon": horizon,
|
| 210 |
+
"discount": discount,
|
| 211 |
+
"distributions": distributions,
|
| 212 |
+
"role_scores": role_scores,
|
| 213 |
+
"normalized_scores": normalized,
|
| 214 |
+
"ranked_roles": ranked,
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
def observe(self, current_role: str, next_role: str) -> None:
|
| 218 |
row = self.rows[current_role]
|
| 219 |
if self.decay < 1.0:
|
|
|
|
| 289 |
return workflows
|
| 290 |
|
| 291 |
|
| 292 |
+
def _prediction_calibration(rows: list[dict[str, Any]], bins: int = 10) -> dict[str, Any]:
|
| 293 |
+
if not rows:
|
| 294 |
+
return {"count": 0, "brier": 0.0, "log_loss": 0.0, "ece": 0.0, "bins": []}
|
| 295 |
+
targets = (*AGENT_ROLES, END)
|
| 296 |
+
brier_total = 0.0
|
| 297 |
+
log_total = 0.0
|
| 298 |
+
buckets: list[list[tuple[float, float]]] = [[] for _ in range(max(2, bins))]
|
| 299 |
+
for row in rows:
|
| 300 |
+
probs = row.get("probabilities", {})
|
| 301 |
+
actual = str(row.get("actual_next_role", END))
|
| 302 |
+
brier_total += sum((float(probs.get(target, 0.0)) - (1.0 if target == actual else 0.0)) ** 2 for target in targets)
|
| 303 |
+
actual_p = max(float(probs.get(actual, 0.0)), 1e-12)
|
| 304 |
+
log_total += -math.log(actual_p)
|
| 305 |
+
confidence = min(max(float(row.get("confidence", 0.0)), 0.0), 1.0)
|
| 306 |
+
correct = 1.0 if row.get("prediction_correct") else 0.0
|
| 307 |
+
index = min(int(confidence * len(buckets)), len(buckets) - 1)
|
| 308 |
+
buckets[index].append((confidence, correct))
|
| 309 |
+
ece = 0.0
|
| 310 |
+
rendered = []
|
| 311 |
+
for index, bucket in enumerate(buckets):
|
| 312 |
+
if not bucket:
|
| 313 |
+
continue
|
| 314 |
+
avg_conf = mean(value[0] for value in bucket)
|
| 315 |
+
accuracy = mean(value[1] for value in bucket)
|
| 316 |
+
weight = len(bucket) / len(rows)
|
| 317 |
+
ece += weight * abs(avg_conf - accuracy)
|
| 318 |
+
rendered.append(
|
| 319 |
+
{
|
| 320 |
+
"lower": index / len(buckets),
|
| 321 |
+
"upper": (index + 1) / len(buckets),
|
| 322 |
+
"count": len(bucket),
|
| 323 |
+
"mean_confidence": avg_conf,
|
| 324 |
+
"accuracy": accuracy,
|
| 325 |
+
}
|
| 326 |
+
)
|
| 327 |
+
return {
|
| 328 |
+
"count": len(rows),
|
| 329 |
+
"brier": brier_total / len(rows),
|
| 330 |
+
"log_loss": log_total / len(rows),
|
| 331 |
+
"ece": ece,
|
| 332 |
+
"bins": rendered,
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
|
| 336 |
class ExecutionLearningSimulator:
|
| 337 |
"""Reference simulator for online workflow learning and prefix prefetch.
|
| 338 |
|
|
|
|
| 368 |
self.pressure_evictions = 0
|
| 369 |
self.prefetch_attempts = 0
|
| 370 |
self.prefetch_correct = 0
|
| 371 |
+
self.prefetch_useful = 0
|
| 372 |
self.prefetch_bytes = 0.0
|
| 373 |
+
self.prefetch_useful_bytes = 0.0
|
| 374 |
+
self.unused_prefetch_bytes = 0.0
|
| 375 |
self.wrong_step_prefetch_bytes = 0.0
|
| 376 |
self.transfer_latencies_ms: list[float] = []
|
| 377 |
self.prefix_hits = 0
|
|
|
|
| 419 |
if len(self.timeline) > self.cfg.timeline_points * 2:
|
| 420 |
self.timeline = self.timeline[::2]
|
| 421 |
|
| 422 |
+
def _mark_unused_prefetch(self, entry: PrefixEntry) -> None:
|
| 423 |
+
if entry.source == "prefetch" and not entry.used:
|
| 424 |
+
self.unused_prefetch_bytes += entry.size_gb * 1e9
|
| 425 |
+
|
| 426 |
+
def _candidate_eviction_cost_ms(self, role: str, forecast_scores: dict[str, float]) -> float:
|
| 427 |
+
"""Approximate downstream cost of evicting forecast-relevant cached prefixes."""
|
| 428 |
+
size_gb = self.role_size_gb[role]
|
| 429 |
+
existing = self.cache.get(role)
|
| 430 |
+
current_without = self._used_gb() - (existing.size_gb if existing else 0.0)
|
| 431 |
+
overflow = max(0.0, current_without + size_gb - self.cache_capacity_gb)
|
| 432 |
+
if overflow <= 1e-12:
|
| 433 |
+
return 0.0
|
| 434 |
+
cost_ms = 0.0
|
| 435 |
+
reclaimed = 0.0
|
| 436 |
+
candidates = sorted(
|
| 437 |
+
[entry for key, entry in self.cache.items() if key != role],
|
| 438 |
+
key=lambda entry: (entry.last_access, entry.role),
|
| 439 |
+
)
|
| 440 |
+
for victim in candidates:
|
| 441 |
+
if reclaimed >= overflow - 1e-12:
|
| 442 |
+
break
|
| 443 |
+
reuse_score = max(float(forecast_scores.get(victim.role, 0.0)), 0.0)
|
| 444 |
+
recompute_ms = self.latency.prefill_seconds([ROLE_PREFIX_TOKENS[victim.role]]) * 1000.0
|
| 445 |
+
cost_ms += reuse_score * recompute_ms
|
| 446 |
+
reclaimed += victim.size_gb
|
| 447 |
+
return cost_ms
|
| 448 |
+
|
| 449 |
+
def _prefetch_utility_ms(self, role: str, forecast_scores: dict[str, float]) -> float:
|
| 450 |
+
score = max(float(forecast_scores.get(role, 0.0)), 0.0)
|
| 451 |
+
saved_ms = self.latency.prefill_seconds([ROLE_PREFIX_TOKENS[role]]) * 1000.0
|
| 452 |
+
transfer_ms = self.cfg.transfer_base_ms + self.role_size_gb[role] / self.cfg.host_bandwidth_gbps * 1000.0
|
| 453 |
+
eviction_ms = self._candidate_eviction_cost_ms(role, forecast_scores)
|
| 454 |
+
return score * saved_ms - transfer_ms - eviction_ms
|
| 455 |
+
|
| 456 |
def _evict_for(self, role: str, size_gb: float) -> bool:
|
| 457 |
existing = self.cache.get(role)
|
| 458 |
current_without_role = self._used_gb() - (existing.size_gb if existing else 0.0)
|
|
|
|
| 464 |
return False
|
| 465 |
victim = min(candidates, key=lambda entry: (entry.last_access, entry.role))
|
| 466 |
self._integrate_memory(self.now)
|
| 467 |
+
self._mark_unused_prefetch(victim)
|
| 468 |
self.cache.pop(victim.role, None)
|
| 469 |
self.prefix_source.pop(victim.role, None)
|
| 470 |
self.pressure_evictions += 1
|
|
|
|
| 477 |
return None
|
| 478 |
self._integrate_memory(self.now)
|
| 479 |
previous = self.cache.get(role)
|
| 480 |
+
if previous is not None:
|
| 481 |
+
self._mark_unused_prefetch(previous)
|
| 482 |
generation = (previous.generation + 1) if previous else 1
|
| 483 |
self.cache[role] = PrefixEntry(role, size_gb, self.now, available_time, source, generation)
|
| 484 |
self.prefix_source[role] = source
|
|
|
|
| 510 |
return END
|
| 511 |
return workflow.steps[step_index + 1].role
|
| 512 |
|
| 513 |
+
def _actual_future_roles(self, workflow_id: int, step_index: int, horizon: int) -> list[str]:
|
| 514 |
+
workflow = self.workflows[workflow_id]
|
| 515 |
+
roles = [step.role for step in workflow.steps[step_index + 1 : step_index + 1 + max(1, horizon)]]
|
| 516 |
+
return roles
|
| 517 |
+
|
| 518 |
+
def _planned_prefetch_roles(
|
| 519 |
+
self,
|
| 520 |
+
workflow_id: int,
|
| 521 |
+
step_index: int,
|
| 522 |
+
current_role: str,
|
| 523 |
+
) -> tuple[str, float, dict[str, float], dict[str, Any], list[dict[str, Any]]]:
|
| 524 |
actual_next = self._actual_next_role(workflow_id, step_index)
|
| 525 |
+
predicted_role, confidence, probabilities = self.predictor.predict(current_role)
|
| 526 |
+
forecast = self.predictor.forecast(
|
| 527 |
+
current_role,
|
| 528 |
+
horizon=self.cfg.forecast_horizon,
|
| 529 |
+
discount=self.cfg.forecast_discount,
|
| 530 |
+
)
|
| 531 |
+
plans: list[dict[str, Any]] = []
|
| 532 |
+
|
| 533 |
if self.cfg.prefetch_policy == "oracle":
|
| 534 |
predicted_role = actual_next
|
| 535 |
confidence = 1.0
|
| 536 |
+
probabilities = {target: 0.0 for target in (*AGENT_ROLES, END)}
|
| 537 |
probabilities[predicted_role] = 1.0
|
| 538 |
+
if actual_next != END:
|
| 539 |
+
plans = [{"role": actual_next, "score": 1.0, "utility_ms": None, "oracle": True}]
|
| 540 |
+
return predicted_role, confidence, probabilities, forecast, plans
|
| 541 |
+
|
| 542 |
+
if self.cfg.prefetch_policy == "oracle_horizon":
|
| 543 |
+
actual_future = self._actual_future_roles(workflow_id, step_index, self.cfg.forecast_horizon)
|
| 544 |
+
predicted_role = actual_next
|
| 545 |
+
confidence = 1.0
|
| 546 |
+
probabilities = {target: 0.0 for target in (*AGENT_ROLES, END)}
|
| 547 |
+
probabilities[predicted_role] = 1.0
|
| 548 |
+
seen: set[str] = set()
|
| 549 |
+
for role in actual_future:
|
| 550 |
+
if role not in seen:
|
| 551 |
+
plans.append({"role": role, "score": 1.0, "utility_ms": None, "oracle": True})
|
| 552 |
+
seen.add(role)
|
| 553 |
+
if len(plans) >= self.cfg.prefetch_top_k:
|
| 554 |
+
break
|
| 555 |
+
return predicted_role, confidence, probabilities, forecast, plans
|
| 556 |
+
|
| 557 |
+
if self.cfg.prefetch_policy in {"none", "cumulative", "decayed"}:
|
| 558 |
+
if self.cfg.prefetch_policy != "none" and predicted_role != END and confidence >= self.cfg.confidence_threshold:
|
| 559 |
+
plans = [{"role": predicted_role, "score": confidence, "utility_ms": None, "oracle": False}]
|
| 560 |
+
return predicted_role, confidence, probabilities, forecast, plans
|
| 561 |
+
|
| 562 |
+
ranked = forecast["ranked_roles"]
|
| 563 |
+
normalized = forecast["normalized_scores"]
|
| 564 |
+
raw_scores = forecast["role_scores"]
|
| 565 |
+
for role in ranked:
|
| 566 |
+
if len(plans) >= self.cfg.prefetch_top_k:
|
| 567 |
+
break
|
| 568 |
+
normalized_score = float(normalized.get(role, 0.0))
|
| 569 |
+
if normalized_score < self.cfg.forecast_min_score:
|
| 570 |
+
continue
|
| 571 |
+
utility_ms = self._prefetch_utility_ms(role, raw_scores)
|
| 572 |
+
if self.cfg.prefetch_policy == "utility" and utility_ms < self.cfg.utility_threshold_ms:
|
| 573 |
+
continue
|
| 574 |
+
plans.append(
|
| 575 |
+
{
|
| 576 |
+
"role": role,
|
| 577 |
+
"score": normalized_score,
|
| 578 |
+
"raw_score": float(raw_scores.get(role, 0.0)),
|
| 579 |
+
"utility_ms": utility_ms,
|
| 580 |
+
"oracle": False,
|
| 581 |
+
}
|
| 582 |
+
)
|
| 583 |
+
return predicted_role, confidence, probabilities, forecast, plans
|
| 584 |
+
|
| 585 |
+
def _schedule_prediction(self, workflow_id: int, step_index: int, current_role: str) -> None:
|
| 586 |
+
actual_next = self._actual_next_role(workflow_id, step_index)
|
| 587 |
+
predicted_role, confidence, probabilities, forecast, plans = self._planned_prefetch_roles(
|
| 588 |
+
workflow_id, step_index, current_role
|
| 589 |
+
)
|
| 590 |
+
attempted_roles: list[str] = []
|
| 591 |
+
attempt_sizes: dict[str, float] = {}
|
| 592 |
+
attempt_reasons: dict[str, str] = {}
|
| 593 |
+
|
| 594 |
+
for plan in plans:
|
| 595 |
+
role = str(plan["role"])
|
| 596 |
+
result = self._prefetch(role)
|
| 597 |
+
attempt_reasons[role] = str(result.get("reason", "unknown"))
|
| 598 |
+
if result.get("attempted"):
|
| 599 |
+
attempted_roles.append(role)
|
| 600 |
+
attempt_sizes[role] = float(result.get("size_gb", 0.0))
|
| 601 |
+
|
| 602 |
+
if self.cfg.prefetch_policy == "none":
|
| 603 |
+
decision_reason = "learn_only"
|
| 604 |
+
elif not plans:
|
| 605 |
+
decision_reason = "no_candidate"
|
| 606 |
+
elif attempted_roles:
|
| 607 |
+
decision_reason = "scheduled"
|
| 608 |
+
else:
|
| 609 |
+
decision_reason = ",".join(sorted(set(attempt_reasons.values()))) or "not_scheduled"
|
| 610 |
|
| 611 |
self.pending_predictions[(workflow_id, step_index)] = {
|
| 612 |
"workflow_id": workflow_id,
|
|
|
|
| 615 |
"predicted_role": predicted_role,
|
| 616 |
"confidence": confidence,
|
| 617 |
"probabilities": probabilities,
|
| 618 |
+
"forecast_horizon": self.cfg.forecast_horizon,
|
| 619 |
+
"forecast_discount": self.cfg.forecast_discount,
|
| 620 |
+
"forecast_role_scores": forecast["role_scores"],
|
| 621 |
+
"forecast_normalized_scores": forecast["normalized_scores"],
|
| 622 |
+
"forecast_roles": [str(plan["role"]) for plan in plans],
|
| 623 |
+
"forecast_plans": plans,
|
| 624 |
+
"prefetch_attempted": bool(attempted_roles),
|
| 625 |
+
"prefetch_roles": attempted_roles,
|
| 626 |
+
"prefetch_reasons": attempt_reasons,
|
| 627 |
"prefetch_reason": decision_reason,
|
| 628 |
+
"prefetch_sizes_gb": attempt_sizes,
|
| 629 |
+
"prefetch_size_gb": sum(attempt_sizes.values()),
|
| 630 |
"shifted_regime": self.workflows[workflow_id].steps[step_index].shifted_regime,
|
| 631 |
}
|
| 632 |
|
|
|
|
|
|
|
| 633 |
if actual_next == END:
|
| 634 |
self._observe_transition(workflow_id, step_index, actual_next)
|
| 635 |
|
|
|
|
| 641 |
if pending is None:
|
| 642 |
return
|
| 643 |
predicted_role = str(pending["predicted_role"])
|
| 644 |
+
attempted_roles = [str(role) for role in pending.get("prefetch_roles", [])]
|
| 645 |
correct = predicted_role == actual_next
|
| 646 |
+
if actual_next != END and actual_next in attempted_roles:
|
| 647 |
self.prefetch_correct += 1
|
| 648 |
+
immediate_wrong_bytes = 0.0
|
| 649 |
+
for role in attempted_roles:
|
| 650 |
+
if role != actual_next:
|
| 651 |
+
immediate_wrong_bytes += float(pending.get("prefetch_sizes_gb", {}).get(role, 0.0)) * 1e9
|
| 652 |
+
self.wrong_step_prefetch_bytes += immediate_wrong_bytes
|
| 653 |
+
|
| 654 |
+
future_roles = self._actual_future_roles(workflow_id, step_index, int(pending.get("forecast_horizon", 1)))
|
| 655 |
+
actual_future_set = set(future_roles)
|
| 656 |
+
forecast_roles = [str(role) for role in pending.get("forecast_roles", [])]
|
| 657 |
+
forecast_set = set(forecast_roles)
|
| 658 |
+
forecast_recall = (
|
| 659 |
+
len(actual_future_set & forecast_set) / len(actual_future_set)
|
| 660 |
+
if actual_future_set
|
| 661 |
+
else None
|
| 662 |
+
)
|
| 663 |
self.prediction_rows.append(
|
| 664 |
pending
|
| 665 |
| {
|
| 666 |
"actual_next_role": actual_next,
|
| 667 |
+
"actual_future_roles": future_roles,
|
| 668 |
"prediction_correct": correct,
|
| 669 |
+
"forecast_next_hit": actual_next in forecast_set if actual_next != END else predicted_role == END,
|
| 670 |
+
"forecast_recall": forecast_recall,
|
| 671 |
+
"immediate_wrong_prefetch_gb": immediate_wrong_bytes / 1e9,
|
| 672 |
"post_shift": bool(pending["shifted_regime"]),
|
| 673 |
}
|
| 674 |
)
|
| 675 |
+
if self.cfg.prefetch_policy not in {"oracle", "oracle_horizon"}:
|
|
|
|
| 676 |
self.predictor.observe(current_role, actual_next)
|
| 677 |
|
| 678 |
def _start_next(self) -> None:
|
|
|
|
| 709 |
self.prefetch_hits += 1
|
| 710 |
self.prefill_tokens_saved += prefix_tokens
|
| 711 |
if entry is not None:
|
| 712 |
+
if entry.source == "prefetch" and not entry.used:
|
| 713 |
+
entry.used = True
|
| 714 |
+
self.prefetch_useful += 1
|
| 715 |
+
self.prefetch_useful_bytes += entry.size_gb * 1e9
|
| 716 |
entry.last_access = self.now + prefix_wait_s
|
| 717 |
prefill_tokens = max(step.dynamic_prompt_tokens, 1)
|
| 718 |
else:
|
|
|
|
| 833 |
def accuracy(rows: list[dict[str, Any]]) -> float:
|
| 834 |
return sum(1 for row in rows if row["prediction_correct"]) / len(rows) if rows else 0.0
|
| 835 |
|
| 836 |
+
# Account for prefetched entries that remained unused through the end of the run.
|
| 837 |
+
for entry in list(self.cache.values()):
|
| 838 |
+
self._mark_unused_prefetch(entry)
|
| 839 |
prefetch_precision = self.prefetch_correct / self.prefetch_attempts if self.prefetch_attempts else 0.0
|
| 840 |
+
prefetch_coverage = (
|
| 841 |
+
sum(1 for row in predictions if row.get("prefetch_attempted")) / len(predictions)
|
| 842 |
+
if predictions else 0.0
|
| 843 |
+
)
|
| 844 |
+
prefetch_utilization = self.prefetch_useful / self.prefetch_attempts if self.prefetch_attempts else 0.0
|
| 845 |
+
forecast_recall_values = [
|
| 846 |
+
float(row["forecast_recall"]) for row in predictions if row.get("forecast_recall") is not None
|
| 847 |
+
]
|
| 848 |
+
forecast_recall = mean(forecast_recall_values) if forecast_recall_values else 0.0
|
| 849 |
workflow_count = len(self.workflows)
|
| 850 |
completed_count = len(self.workflow_completion)
|
| 851 |
summary = {
|
|
|
|
| 869 |
"prefetch_correct": self.prefetch_correct,
|
| 870 |
"prefetch_precision": prefetch_precision,
|
| 871 |
"prefetch_coverage": prefetch_coverage,
|
| 872 |
+
"prefetch_utilization": prefetch_utilization,
|
| 873 |
+
"prefetch_attempts_per_prediction": self.prefetch_attempts / len(predictions) if predictions else 0.0,
|
| 874 |
+
"forecast_recall": forecast_recall,
|
| 875 |
"prefetch_gb": self.prefetch_bytes / 1e9,
|
| 876 |
+
"prefetch_useful_gb": self.prefetch_useful_bytes / 1e9,
|
| 877 |
+
"unused_prefetch_gb": self.unused_prefetch_bytes / 1e9,
|
| 878 |
"wrong_step_prefetch_gb": self.wrong_step_prefetch_bytes / 1e9,
|
| 879 |
"p95_prefetch_transfer_ms": percentile(self.transfer_latencies_ms, 0.95),
|
| 880 |
}
|
|
|
|
| 883 |
"top1_accuracy": accuracy(predictions),
|
| 884 |
"pre_shift_accuracy": accuracy(pre),
|
| 885 |
"post_shift_accuracy": accuracy(post),
|
| 886 |
+
"calibration": _prediction_calibration(predictions),
|
| 887 |
+
"pre_shift_calibration": _prediction_calibration(pre),
|
| 888 |
+
"post_shift_calibration": _prediction_calibration(post),
|
| 889 |
"rows": predictions[:4000],
|
| 890 |
"predictor": self.predictor.snapshot(),
|
| 891 |
}
|
|
|
|
| 900 |
"simulator": "InferScale-Sim",
|
| 901 |
"mode": "online-agent-execution-learning",
|
| 902 |
"latency_profile_type": "analytical-reference",
|
| 903 |
+
"execution_model": "online-transition-multistep-prefix-prefetch-reference",
|
| 904 |
"prefetch_policy": self.cfg.prefetch_policy,
|
| 905 |
+
"lookahead": "oracle-upper-bound" if self.cfg.prefetch_policy in {"oracle", "oracle_horizon"} else "no-future-transition-lookahead",
|
| 906 |
"warning": "Execution Learning isolates cross-workflow static-prefix prediction/prefetch from session-KV retention and dynamic batching.",
|
| 907 |
},
|
| 908 |
"summary": summary,
|
|
|
|
| 934 |
"prefix_hit_rate": result["resource"]["prefix_hit_rate"],
|
| 935 |
"prefetch_precision": result["resource"]["prefetch_precision"],
|
| 936 |
"prefetch_coverage": result["resource"]["prefetch_coverage"],
|
| 937 |
+
"prefetch_utilization": result["resource"].get("prefetch_utilization", 0.0),
|
| 938 |
+
"forecast_recall": result["resource"].get("forecast_recall", 0.0),
|
| 939 |
"mean_hbm_gb": result["resource"]["mean_prefix_hbm_gb"],
|
| 940 |
+
"pressure_evictions": result["resource"].get("pressure_evictions", 0),
|
| 941 |
"wrong_step_prefetch_gb": result["resource"]["wrong_step_prefetch_gb"],
|
| 942 |
+
"unused_prefetch_gb": result["resource"].get("unused_prefetch_gb", 0.0),
|
| 943 |
"prefill_tokens_saved": result["resource"]["prefill_tokens_saved"],
|
| 944 |
"top1_accuracy": result["prediction"]["top1_accuracy"],
|
| 945 |
+
"brier": result["prediction"].get("calibration", {}).get("brier", 0.0),
|
| 946 |
+
"ece": result["prediction"].get("calibration", {}).get("ece", 0.0),
|
| 947 |
+
"post_shift_ece": result["prediction"].get("post_shift_calibration", {}).get("ece", 0.0),
|
| 948 |
"pre_shift_accuracy": result["prediction"]["pre_shift_accuracy"],
|
| 949 |
"post_shift_accuracy": result["prediction"]["post_shift_accuracy"],
|
| 950 |
}
|
|
|
|
| 1059 |
"association": association,
|
| 1060 |
"note": "Better next-role prediction need not minimize serving latency because cache occupancy and transfer timing mediate the effect.",
|
| 1061 |
}
|
| 1062 |
+
|
| 1063 |
+
|
| 1064 |
+
def execution_planning_study(config: dict[str, Any]) -> dict[str, Any]:
|
| 1065 |
+
"""Compare one-step, multi-step, utility-aware, and clairvoyant planning."""
|
| 1066 |
+
base = ExecutionLearningConfig.from_dict(config)
|
| 1067 |
+
workflows = _common_workflows(base)
|
| 1068 |
+
candidates = [
|
| 1069 |
+
("Top-1 decayed", "decayed"),
|
| 1070 |
+
("Multi-step top-k", "multistep"),
|
| 1071 |
+
("Utility-aware multi-step", "utility"),
|
| 1072 |
+
("Oracle future-set", "oracle_horizon"),
|
| 1073 |
+
]
|
| 1074 |
+
rows: list[dict[str, Any]] = []
|
| 1075 |
+
results: dict[str, dict[str, Any]] = {}
|
| 1076 |
+
for label, policy in candidates:
|
| 1077 |
+
cfg = ExecutionLearningConfig.from_dict(base.to_dict())
|
| 1078 |
+
cfg.prefetch_policy = policy
|
| 1079 |
+
result = run_execution_learning(cfg.to_dict(), workflows)
|
| 1080 |
+
results[label] = result
|
| 1081 |
+
rows.append(_row(label, result))
|
| 1082 |
+
best_ttft = min(rows, key=lambda row: row["p95_step_ttft_ms"]) if rows else None
|
| 1083 |
+
best_efficiency = max(
|
| 1084 |
+
rows,
|
| 1085 |
+
key=lambda row: (
|
| 1086 |
+
row["prefill_tokens_saved"] / max(row["mean_hbm_gb"], 1e-9),
|
| 1087 |
+
-row["p95_step_ttft_ms"],
|
| 1088 |
+
),
|
| 1089 |
+
) if rows else None
|
| 1090 |
+
return {
|
| 1091 |
+
"protocol": "common-shifted-agent-workflow-trace",
|
| 1092 |
+
"study": "multi-step-prefetch-planning",
|
| 1093 |
+
"rows": rows,
|
| 1094 |
+
"results": results,
|
| 1095 |
+
"best_ttft_policy": best_ttft["label"] if best_ttft else None,
|
| 1096 |
+
"best_prefill_per_hbm_policy": best_efficiency["label"] if best_efficiency else None,
|
| 1097 |
+
"note": "Oracle future-set sees realized future roles only as an information upper bound; online multi-step policies roll the learned transition matrix forward without future trace access.",
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
|
| 1101 |
+
def execution_horizon_sweep(
|
| 1102 |
+
config: dict[str, Any], horizon_values: list[int] | None = None
|
| 1103 |
+
) -> dict[str, Any]:
|
| 1104 |
+
base = ExecutionLearningConfig.from_dict(config)
|
| 1105 |
+
base.prefetch_policy = "utility"
|
| 1106 |
+
workflows = _common_workflows(base)
|
| 1107 |
+
values = horizon_values or [1, 2, 3, 4, 5]
|
| 1108 |
+
cleaned = sorted({max(1, min(int(value), 6)) for value in values})
|
| 1109 |
+
rows: list[dict[str, Any]] = []
|
| 1110 |
+
for horizon in cleaned:
|
| 1111 |
+
cfg = ExecutionLearningConfig.from_dict(base.to_dict())
|
| 1112 |
+
cfg.forecast_horizon = horizon
|
| 1113 |
+
result = run_execution_learning(cfg.to_dict(), workflows)
|
| 1114 |
+
rows.append(_row(f"horizon {horizon}", result) | {"horizon": horizon})
|
| 1115 |
+
best_ttft = min(rows, key=lambda row: (row["p95_step_ttft_ms"], row["unused_prefetch_gb"])) if rows else None
|
| 1116 |
+
best_utilization = max(rows, key=lambda row: (row["prefetch_utilization"], -row["unused_prefetch_gb"])) if rows else None
|
| 1117 |
+
return {
|
| 1118 |
+
"protocol": "common-shifted-agent-workflow-trace",
|
| 1119 |
+
"study": "forecast-horizon-sweep",
|
| 1120 |
+
"rows": rows,
|
| 1121 |
+
"best_ttft_horizon": best_ttft["horizon"] if best_ttft else None,
|
| 1122 |
+
"best_utilization_horizon": best_utilization["horizon"] if best_utilization else None,
|
| 1123 |
+
"note": "Longer forecasts can expose future reuse but may spend bandwidth and cache capacity on prefixes that are not consumed before eviction.",
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
|
| 1127 |
+
def execution_budget_sweep(
|
| 1128 |
+
config: dict[str, Any], budget_values: list[float] | None = None
|
| 1129 |
+
) -> dict[str, Any]:
|
| 1130 |
+
base = ExecutionLearningConfig.from_dict(config)
|
| 1131 |
+
workflows = _common_workflows(base)
|
| 1132 |
+
values = budget_values or [0.30, 0.50, 0.75, 1.00]
|
| 1133 |
+
cleaned = sorted({min(max(float(value), 0.10), 1.50) for value in values})
|
| 1134 |
+
policies = [
|
| 1135 |
+
("Top-1", "decayed"),
|
| 1136 |
+
("Multi-step", "multistep"),
|
| 1137 |
+
("Utility-aware", "utility"),
|
| 1138 |
+
]
|
| 1139 |
+
rows: list[dict[str, Any]] = []
|
| 1140 |
+
for budget in cleaned:
|
| 1141 |
+
for label, policy in policies:
|
| 1142 |
+
cfg = ExecutionLearningConfig.from_dict(base.to_dict())
|
| 1143 |
+
cfg.prefix_cache_budget_fraction = budget
|
| 1144 |
+
cfg.prefetch_policy = policy
|
| 1145 |
+
result = run_execution_learning(cfg.to_dict(), workflows)
|
| 1146 |
+
rows.append(_row(label, result) | {"budget": budget, "policy_label": label})
|
| 1147 |
+
winners = []
|
| 1148 |
+
for budget in cleaned:
|
| 1149 |
+
group = [row for row in rows if abs(float(row["budget"]) - budget) < 1e-12]
|
| 1150 |
+
if group:
|
| 1151 |
+
winner = min(group, key=lambda row: (row["p95_step_ttft_ms"], row["unused_prefetch_gb"]))
|
| 1152 |
+
winners.append({"budget": budget, "policy": winner["policy_label"], "p95_step_ttft_ms": winner["p95_step_ttft_ms"]})
|
| 1153 |
+
return {
|
| 1154 |
+
"protocol": "common-shifted-agent-workflow-trace",
|
| 1155 |
+
"study": "prefix-cache-budget-policy-sweep",
|
| 1156 |
+
"rows": rows,
|
| 1157 |
+
"winners": winners,
|
| 1158 |
+
"note": "All policies see the same workflows at each cache budget; the sweep exposes when broader forecasting helps versus when it increases cache pressure.",
|
| 1159 |
+
}
|
| 1160 |
+
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "inferscale-sim"
|
| 7 |
-
version = "0.
|
| 8 |
description = "Interactive LLM serving simulator and SLO capacity planner"
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.10"
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "inferscale-sim"
|
| 7 |
+
version = "0.9.0"
|
| 8 |
description = "Interactive LLM serving simulator and SLO capacity planner"
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.10"
|
scripts/release_check.py
CHANGED
|
@@ -22,6 +22,9 @@ agent_affinity_sweep = inferscale.agent_affinity_sweep
|
|
| 22 |
execution_prefetch_study = inferscale.execution_prefetch_study
|
| 23 |
execution_threshold_sweep = inferscale.execution_threshold_sweep
|
| 24 |
execution_decay_sweep = inferscale.execution_decay_sweep
|
|
|
|
|
|
|
|
|
|
| 25 |
run_execution_learning = inferscale.run_execution_learning
|
| 26 |
paired_study = inferscale.paired_study
|
| 27 |
robustness_study = inferscale.robustness_study
|
|
@@ -43,8 +46,8 @@ else:
|
|
| 43 |
|
| 44 |
if "sdk: static" not in README:
|
| 45 |
errors.append("README metadata must use sdk: static")
|
| 46 |
-
if internal_version != "0.
|
| 47 |
-
errors.append(f"internal package version is {internal_version}; expected 0.
|
| 48 |
|
| 49 |
# Public-facing release/version branding is intentionally absent. Model names
|
| 50 |
# such as Mistral-7B-v0.3 are allowed; project headings/badges are not.
|
|
@@ -83,7 +86,7 @@ if "Download PNG" not in index or ".chart-download" not in app:
|
|
| 83 |
errors.append("chart PNG export controls are missing")
|
| 84 |
if "Worst repetition" not in index or "Target" not in index:
|
| 85 |
errors.append("capacity evidence columns are missing")
|
| 86 |
-
for expected in ["Trace replay", "Research Studies", "Run paired study", "Stress-test selected hypothesis", "Agent Sessions", "Session Policy Arena", "TTL frontier", "Agent Memory Lab", "Affinity Frontier", "Stress HBM budget", "Predictive Tiering Lab", "Compare predictive policies", "Sweep adaptation rate", "Execution Learning", "Prefetch Policy Study", "Confidence Threshold Study", "Forgetting-Rate Study", "Download JSON"]:
|
| 87 |
if expected not in index:
|
| 88 |
errors.append(f"UI is missing research/trace feature: {expected}")
|
| 89 |
|
|
@@ -248,6 +251,15 @@ try:
|
|
| 248 |
execution_decay = execution_decay_sweep(execution_cfg, [0.5, 0.85, 1.0])
|
| 249 |
if len(execution_decay.get("rows", [])) != 3:
|
| 250 |
errors.append("execution transition-decay sweep is incomplete")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
except Exception as exc: # pragma: no cover
|
| 252 |
errors.append(f"execution-learning smoke test raised: {exc}")
|
| 253 |
|
|
@@ -290,5 +302,8 @@ print(f"Execution transition observations: {execution_run['prediction']['count']
|
|
| 290 |
print(f"Execution policy candidates: {len(execution_compare['rows'])}")
|
| 291 |
print(f"Execution threshold points: {len(execution_threshold['rows'])}")
|
| 292 |
print(f"Execution decay points: {len(execution_decay['rows'])}")
|
|
|
|
|
|
|
|
|
|
| 293 |
print(f"Validation observations: {validation['observation_count']}")
|
| 294 |
print(f"Profile provenance: {smoke['provenance']['latency_profile_type']}")
|
|
|
|
| 22 |
execution_prefetch_study = inferscale.execution_prefetch_study
|
| 23 |
execution_threshold_sweep = inferscale.execution_threshold_sweep
|
| 24 |
execution_decay_sweep = inferscale.execution_decay_sweep
|
| 25 |
+
execution_planning_study = inferscale.execution_planning_study
|
| 26 |
+
execution_horizon_sweep = inferscale.execution_horizon_sweep
|
| 27 |
+
execution_budget_sweep = inferscale.execution_budget_sweep
|
| 28 |
run_execution_learning = inferscale.run_execution_learning
|
| 29 |
paired_study = inferscale.paired_study
|
| 30 |
robustness_study = inferscale.robustness_study
|
|
|
|
| 46 |
|
| 47 |
if "sdk: static" not in README:
|
| 48 |
errors.append("README metadata must use sdk: static")
|
| 49 |
+
if internal_version != "0.9.0":
|
| 50 |
+
errors.append(f"internal package version is {internal_version}; expected 0.9.0")
|
| 51 |
|
| 52 |
# Public-facing release/version branding is intentionally absent. Model names
|
| 53 |
# such as Mistral-7B-v0.3 are allowed; project headings/badges are not.
|
|
|
|
| 86 |
errors.append("chart PNG export controls are missing")
|
| 87 |
if "Worst repetition" not in index or "Target" not in index:
|
| 88 |
errors.append("capacity evidence columns are missing")
|
| 89 |
+
for expected in ["Trace replay", "Research Studies", "Run paired study", "Stress-test selected hypothesis", "Agent Sessions", "Session Policy Arena", "TTL frontier", "Agent Memory Lab", "Affinity Frontier", "Stress HBM budget", "Predictive Tiering Lab", "Compare predictive policies", "Sweep adaptation rate", "Execution Learning", "Prefetch Policy Study", "Confidence Threshold Study", "Forgetting-Rate Study", "Prefetch Planning Study", "Forecast Horizon Study", "Cache Budget Study", "Download JSON"]:
|
| 90 |
if expected not in index:
|
| 91 |
errors.append(f"UI is missing research/trace feature: {expected}")
|
| 92 |
|
|
|
|
| 251 |
execution_decay = execution_decay_sweep(execution_cfg, [0.5, 0.85, 1.0])
|
| 252 |
if len(execution_decay.get("rows", [])) != 3:
|
| 253 |
errors.append("execution transition-decay sweep is incomplete")
|
| 254 |
+
execution_planning = execution_planning_study(execution_cfg | {"forecast_horizon": 3, "prefetch_top_k": 2})
|
| 255 |
+
if len(execution_planning.get("rows", [])) != 4:
|
| 256 |
+
errors.append("execution multi-step planning study is incomplete")
|
| 257 |
+
execution_horizon = execution_horizon_sweep(execution_cfg, [1, 2, 3])
|
| 258 |
+
if len(execution_horizon.get("rows", [])) != 3:
|
| 259 |
+
errors.append("execution forecast-horizon sweep is incomplete")
|
| 260 |
+
execution_budget = execution_budget_sweep(execution_cfg, [0.3, 0.6])
|
| 261 |
+
if len(execution_budget.get("rows", [])) != 6:
|
| 262 |
+
errors.append("execution cache-budget sweep is incomplete")
|
| 263 |
except Exception as exc: # pragma: no cover
|
| 264 |
errors.append(f"execution-learning smoke test raised: {exc}")
|
| 265 |
|
|
|
|
| 302 |
print(f"Execution policy candidates: {len(execution_compare['rows'])}")
|
| 303 |
print(f"Execution threshold points: {len(execution_threshold['rows'])}")
|
| 304 |
print(f"Execution decay points: {len(execution_decay['rows'])}")
|
| 305 |
+
print(f"Execution planning candidates: {len(execution_planning['rows'])}")
|
| 306 |
+
print(f"Execution horizon points: {len(execution_horizon['rows'])}")
|
| 307 |
+
print(f"Execution cache-budget rows: {len(execution_budget['rows'])}")
|
| 308 |
print(f"Validation observations: {validation['observation_count']}")
|
| 309 |
print(f"Profile provenance: {smoke['provenance']['latency_profile_type']}")
|
src/inferscale/__init__.py
CHANGED
|
@@ -9,7 +9,15 @@ from .agentic import (
|
|
| 9 |
ttl_retention_sweep,
|
| 10 |
)
|
| 11 |
from .api import execute, metadata
|
| 12 |
-
from .execution import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from .models import SimulationConfig
|
| 14 |
from .optimizer import capacity_search, compare_schedulers, compare_topologies, design_space_search
|
| 15 |
from .research import paired_study, robustness_study
|
|
@@ -29,7 +37,10 @@ __all__ = [
|
|
| 29 |
"compare_topologies",
|
| 30 |
"design_space_search",
|
| 31 |
"execute",
|
|
|
|
| 32 |
"execution_decay_sweep",
|
|
|
|
|
|
|
| 33 |
"execution_prefetch_study",
|
| 34 |
"execution_threshold_sweep",
|
| 35 |
"metadata",
|
|
@@ -44,4 +55,4 @@ __all__ = [
|
|
| 44 |
|
| 45 |
# Internal package metadata only; the public project intentionally avoids
|
| 46 |
# release/version branding in the interface and documentation.
|
| 47 |
-
__version__ = "0.
|
|
|
|
| 9 |
ttl_retention_sweep,
|
| 10 |
)
|
| 11 |
from .api import execute, metadata
|
| 12 |
+
from .execution import (
|
| 13 |
+
execution_budget_sweep,
|
| 14 |
+
execution_decay_sweep,
|
| 15 |
+
execution_horizon_sweep,
|
| 16 |
+
execution_planning_study,
|
| 17 |
+
execution_prefetch_study,
|
| 18 |
+
execution_threshold_sweep,
|
| 19 |
+
run_execution_learning,
|
| 20 |
+
)
|
| 21 |
from .models import SimulationConfig
|
| 22 |
from .optimizer import capacity_search, compare_schedulers, compare_topologies, design_space_search
|
| 23 |
from .research import paired_study, robustness_study
|
|
|
|
| 37 |
"compare_topologies",
|
| 38 |
"design_space_search",
|
| 39 |
"execute",
|
| 40 |
+
"execution_budget_sweep",
|
| 41 |
"execution_decay_sweep",
|
| 42 |
+
"execution_horizon_sweep",
|
| 43 |
+
"execution_planning_study",
|
| 44 |
"execution_prefetch_study",
|
| 45 |
"execution_threshold_sweep",
|
| 46 |
"metadata",
|
|
|
|
| 55 |
|
| 56 |
# Internal package metadata only; the public project intentionally avoids
|
| 57 |
# release/version branding in the interface and documentation.
|
| 58 |
+
__version__ = "0.9.0"
|
src/inferscale/api.py
CHANGED
|
@@ -11,7 +11,10 @@ from .agentic import (
|
|
| 11 |
ttl_retention_sweep,
|
| 12 |
)
|
| 13 |
from .execution import (
|
|
|
|
| 14 |
execution_decay_sweep,
|
|
|
|
|
|
|
| 15 |
execution_prefetch_study,
|
| 16 |
execution_threshold_sweep,
|
| 17 |
run_execution_learning,
|
|
@@ -37,6 +40,7 @@ def metadata() -> dict:
|
|
| 37 |
],
|
| 38 |
"execution_learning_modes": [
|
| 39 |
"single_run", "prefetch_policy_compare", "confidence_threshold_sweep", "transition_decay_sweep",
|
|
|
|
| 40 |
],
|
| 41 |
}
|
| 42 |
|
|
@@ -107,6 +111,12 @@ def execute(action: str, payload: dict) -> dict:
|
|
| 107 |
return execution_threshold_sweep(payload.get("config", payload), payload.get("thresholds"))
|
| 108 |
if action == "execution_decay_sweep":
|
| 109 |
return execution_decay_sweep(payload.get("config", payload), payload.get("decay_values"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
if action == "robustness_study":
|
| 111 |
config = payload.get("config", payload)
|
| 112 |
return robustness_study(
|
|
|
|
| 11 |
ttl_retention_sweep,
|
| 12 |
)
|
| 13 |
from .execution import (
|
| 14 |
+
execution_budget_sweep,
|
| 15 |
execution_decay_sweep,
|
| 16 |
+
execution_horizon_sweep,
|
| 17 |
+
execution_planning_study,
|
| 18 |
execution_prefetch_study,
|
| 19 |
execution_threshold_sweep,
|
| 20 |
run_execution_learning,
|
|
|
|
| 40 |
],
|
| 41 |
"execution_learning_modes": [
|
| 42 |
"single_run", "prefetch_policy_compare", "confidence_threshold_sweep", "transition_decay_sweep",
|
| 43 |
+
"multistep_planning", "forecast_horizon_sweep", "cache_budget_policy_sweep",
|
| 44 |
],
|
| 45 |
}
|
| 46 |
|
|
|
|
| 111 |
return execution_threshold_sweep(payload.get("config", payload), payload.get("thresholds"))
|
| 112 |
if action == "execution_decay_sweep":
|
| 113 |
return execution_decay_sweep(payload.get("config", payload), payload.get("decay_values"))
|
| 114 |
+
if action == "execution_planning_study":
|
| 115 |
+
return execution_planning_study(payload.get("config", payload))
|
| 116 |
+
if action == "execution_horizon_sweep":
|
| 117 |
+
return execution_horizon_sweep(payload.get("config", payload), payload.get("horizon_values"))
|
| 118 |
+
if action == "execution_budget_sweep":
|
| 119 |
+
return execution_budget_sweep(payload.get("config", payload), payload.get("budget_values"))
|
| 120 |
if action == "robustness_study":
|
| 121 |
config = payload.get("config", payload)
|
| 122 |
return robustness_study(
|
src/inferscale/execution.py
CHANGED
|
@@ -48,7 +48,7 @@ ROLE_GAP_MULTIPLIERS = {
|
|
| 48 |
"writer": 0.30,
|
| 49 |
}
|
| 50 |
|
| 51 |
-
PREFETCH_POLICIES = {"none", "cumulative", "decayed", "oracle"}
|
| 52 |
|
| 53 |
|
| 54 |
@dataclass
|
|
@@ -74,6 +74,11 @@ class ExecutionLearningConfig:
|
|
| 74 |
transition_decay: float = 0.85
|
| 75 |
confidence_threshold: float = 0.55
|
| 76 |
prior_strength: float = 0.35
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
timeline_points: int = 240
|
| 78 |
|
| 79 |
@classmethod
|
|
@@ -90,6 +95,11 @@ class ExecutionLearningConfig:
|
|
| 90 |
cfg.transition_decay = min(max(float(cfg.transition_decay), 0.20), 1.0)
|
| 91 |
cfg.confidence_threshold = min(max(float(cfg.confidence_threshold), 0.0), 1.0)
|
| 92 |
cfg.prior_strength = max(float(cfg.prior_strength), 0.01)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
cfg.timeline_points = max(40, min(int(cfg.timeline_points), 1000))
|
| 94 |
if cfg.prefetch_policy not in PREFETCH_POLICIES:
|
| 95 |
raise ValueError(f"Unsupported execution prefetch policy: {cfg.prefetch_policy}")
|
|
@@ -123,6 +133,7 @@ class PrefixEntry:
|
|
| 123 |
available_time: float
|
| 124 |
source: str
|
| 125 |
generation: int
|
|
|
|
| 126 |
|
| 127 |
|
| 128 |
class OnlineTransitionPredictor:
|
|
@@ -150,6 +161,59 @@ class OnlineTransitionPredictor:
|
|
| 150 |
best = max(self.targets, key=lambda target: (probabilities[target], -self.targets.index(target)))
|
| 151 |
return best, probabilities[best], probabilities
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
def observe(self, current_role: str, next_role: str) -> None:
|
| 154 |
row = self.rows[current_role]
|
| 155 |
if self.decay < 1.0:
|
|
@@ -225,6 +289,50 @@ def generate_workflows(cfg: ExecutionLearningConfig) -> list[WorkflowSpec]:
|
|
| 225 |
return workflows
|
| 226 |
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
class ExecutionLearningSimulator:
|
| 229 |
"""Reference simulator for online workflow learning and prefix prefetch.
|
| 230 |
|
|
@@ -260,7 +368,10 @@ class ExecutionLearningSimulator:
|
|
| 260 |
self.pressure_evictions = 0
|
| 261 |
self.prefetch_attempts = 0
|
| 262 |
self.prefetch_correct = 0
|
|
|
|
| 263 |
self.prefetch_bytes = 0.0
|
|
|
|
|
|
|
| 264 |
self.wrong_step_prefetch_bytes = 0.0
|
| 265 |
self.transfer_latencies_ms: list[float] = []
|
| 266 |
self.prefix_hits = 0
|
|
@@ -308,6 +419,40 @@ class ExecutionLearningSimulator:
|
|
| 308 |
if len(self.timeline) > self.cfg.timeline_points * 2:
|
| 309 |
self.timeline = self.timeline[::2]
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
def _evict_for(self, role: str, size_gb: float) -> bool:
|
| 312 |
existing = self.cache.get(role)
|
| 313 |
current_without_role = self._used_gb() - (existing.size_gb if existing else 0.0)
|
|
@@ -319,6 +464,7 @@ class ExecutionLearningSimulator:
|
|
| 319 |
return False
|
| 320 |
victim = min(candidates, key=lambda entry: (entry.last_access, entry.role))
|
| 321 |
self._integrate_memory(self.now)
|
|
|
|
| 322 |
self.cache.pop(victim.role, None)
|
| 323 |
self.prefix_source.pop(victim.role, None)
|
| 324 |
self.pressure_evictions += 1
|
|
@@ -331,6 +477,8 @@ class ExecutionLearningSimulator:
|
|
| 331 |
return None
|
| 332 |
self._integrate_memory(self.now)
|
| 333 |
previous = self.cache.get(role)
|
|
|
|
|
|
|
| 334 |
generation = (previous.generation + 1) if previous else 1
|
| 335 |
self.cache[role] = PrefixEntry(role, size_gb, self.now, available_time, source, generation)
|
| 336 |
self.prefix_source[role] = source
|
|
@@ -362,35 +510,103 @@ class ExecutionLearningSimulator:
|
|
| 362 |
return END
|
| 363 |
return workflow.steps[step_index + 1].role
|
| 364 |
|
| 365 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
actual_next = self._actual_next_role(workflow_id, step_index)
|
| 367 |
-
predicted_role =
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
| 372 |
if self.cfg.prefetch_policy == "oracle":
|
| 373 |
predicted_role = actual_next
|
| 374 |
confidence = 1.0
|
|
|
|
| 375 |
probabilities[predicted_role] = 1.0
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
predicted_role
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
self.pending_predictions[(workflow_id, step_index)] = {
|
| 396 |
"workflow_id": workflow_id,
|
|
@@ -399,14 +615,21 @@ class ExecutionLearningSimulator:
|
|
| 399 |
"predicted_role": predicted_role,
|
| 400 |
"confidence": confidence,
|
| 401 |
"probabilities": probabilities,
|
| 402 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
"prefetch_reason": decision_reason,
|
| 404 |
-
"
|
|
|
|
| 405 |
"shifted_regime": self.workflows[workflow_id].steps[step_index].shifted_regime,
|
| 406 |
}
|
| 407 |
|
| 408 |
-
# END is observable immediately at workflow completion. Non-final
|
| 409 |
-
# transitions become observable only after the tool gap has elapsed.
|
| 410 |
if actual_next == END:
|
| 411 |
self._observe_transition(workflow_id, step_index, actual_next)
|
| 412 |
|
|
@@ -418,22 +641,38 @@ class ExecutionLearningSimulator:
|
|
| 418 |
if pending is None:
|
| 419 |
return
|
| 420 |
predicted_role = str(pending["predicted_role"])
|
| 421 |
-
|
| 422 |
correct = predicted_role == actual_next
|
| 423 |
-
if
|
| 424 |
self.prefetch_correct += 1
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
self.prediction_rows.append(
|
| 428 |
pending
|
| 429 |
| {
|
| 430 |
"actual_next_role": actual_next,
|
|
|
|
| 431 |
"prediction_correct": correct,
|
|
|
|
|
|
|
|
|
|
| 432 |
"post_shift": bool(pending["shifted_regime"]),
|
| 433 |
}
|
| 434 |
)
|
| 435 |
-
|
| 436 |
-
if self.cfg.prefetch_policy != "oracle":
|
| 437 |
self.predictor.observe(current_role, actual_next)
|
| 438 |
|
| 439 |
def _start_next(self) -> None:
|
|
@@ -470,6 +709,10 @@ class ExecutionLearningSimulator:
|
|
| 470 |
self.prefetch_hits += 1
|
| 471 |
self.prefill_tokens_saved += prefix_tokens
|
| 472 |
if entry is not None:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
entry.last_access = self.now + prefix_wait_s
|
| 474 |
prefill_tokens = max(step.dynamic_prompt_tokens, 1)
|
| 475 |
else:
|
|
@@ -590,8 +833,19 @@ class ExecutionLearningSimulator:
|
|
| 590 |
def accuracy(rows: list[dict[str, Any]]) -> float:
|
| 591 |
return sum(1 for row in rows if row["prediction_correct"]) / len(rows) if rows else 0.0
|
| 592 |
|
|
|
|
|
|
|
|
|
|
| 593 |
prefetch_precision = self.prefetch_correct / self.prefetch_attempts if self.prefetch_attempts else 0.0
|
| 594 |
-
prefetch_coverage =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
workflow_count = len(self.workflows)
|
| 596 |
completed_count = len(self.workflow_completion)
|
| 597 |
summary = {
|
|
@@ -615,7 +869,12 @@ class ExecutionLearningSimulator:
|
|
| 615 |
"prefetch_correct": self.prefetch_correct,
|
| 616 |
"prefetch_precision": prefetch_precision,
|
| 617 |
"prefetch_coverage": prefetch_coverage,
|
|
|
|
|
|
|
|
|
|
| 618 |
"prefetch_gb": self.prefetch_bytes / 1e9,
|
|
|
|
|
|
|
| 619 |
"wrong_step_prefetch_gb": self.wrong_step_prefetch_bytes / 1e9,
|
| 620 |
"p95_prefetch_transfer_ms": percentile(self.transfer_latencies_ms, 0.95),
|
| 621 |
}
|
|
@@ -624,6 +883,9 @@ class ExecutionLearningSimulator:
|
|
| 624 |
"top1_accuracy": accuracy(predictions),
|
| 625 |
"pre_shift_accuracy": accuracy(pre),
|
| 626 |
"post_shift_accuracy": accuracy(post),
|
|
|
|
|
|
|
|
|
|
| 627 |
"rows": predictions[:4000],
|
| 628 |
"predictor": self.predictor.snapshot(),
|
| 629 |
}
|
|
@@ -638,9 +900,9 @@ class ExecutionLearningSimulator:
|
|
| 638 |
"simulator": "InferScale-Sim",
|
| 639 |
"mode": "online-agent-execution-learning",
|
| 640 |
"latency_profile_type": "analytical-reference",
|
| 641 |
-
"execution_model": "
|
| 642 |
"prefetch_policy": self.cfg.prefetch_policy,
|
| 643 |
-
"lookahead": "oracle-upper-bound" if self.cfg.prefetch_policy
|
| 644 |
"warning": "Execution Learning isolates cross-workflow static-prefix prediction/prefetch from session-KV retention and dynamic batching.",
|
| 645 |
},
|
| 646 |
"summary": summary,
|
|
@@ -672,10 +934,17 @@ def _row(label: str, result: dict[str, Any]) -> dict[str, Any]:
|
|
| 672 |
"prefix_hit_rate": result["resource"]["prefix_hit_rate"],
|
| 673 |
"prefetch_precision": result["resource"]["prefetch_precision"],
|
| 674 |
"prefetch_coverage": result["resource"]["prefetch_coverage"],
|
|
|
|
|
|
|
| 675 |
"mean_hbm_gb": result["resource"]["mean_prefix_hbm_gb"],
|
|
|
|
| 676 |
"wrong_step_prefetch_gb": result["resource"]["wrong_step_prefetch_gb"],
|
|
|
|
| 677 |
"prefill_tokens_saved": result["resource"]["prefill_tokens_saved"],
|
| 678 |
"top1_accuracy": result["prediction"]["top1_accuracy"],
|
|
|
|
|
|
|
|
|
|
| 679 |
"pre_shift_accuracy": result["prediction"]["pre_shift_accuracy"],
|
| 680 |
"post_shift_accuracy": result["prediction"]["post_shift_accuracy"],
|
| 681 |
}
|
|
@@ -790,3 +1059,102 @@ def execution_decay_sweep(
|
|
| 790 |
"association": association,
|
| 791 |
"note": "Better next-role prediction need not minimize serving latency because cache occupancy and transfer timing mediate the effect.",
|
| 792 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
"writer": 0.30,
|
| 49 |
}
|
| 50 |
|
| 51 |
+
PREFETCH_POLICIES = {"none", "cumulative", "decayed", "multistep", "utility", "oracle", "oracle_horizon"}
|
| 52 |
|
| 53 |
|
| 54 |
@dataclass
|
|
|
|
| 74 |
transition_decay: float = 0.85
|
| 75 |
confidence_threshold: float = 0.55
|
| 76 |
prior_strength: float = 0.35
|
| 77 |
+
forecast_horizon: int = 3
|
| 78 |
+
prefetch_top_k: int = 2
|
| 79 |
+
forecast_discount: float = 0.75
|
| 80 |
+
forecast_min_score: float = 0.10
|
| 81 |
+
utility_threshold_ms: float = 0.0
|
| 82 |
timeline_points: int = 240
|
| 83 |
|
| 84 |
@classmethod
|
|
|
|
| 95 |
cfg.transition_decay = min(max(float(cfg.transition_decay), 0.20), 1.0)
|
| 96 |
cfg.confidence_threshold = min(max(float(cfg.confidence_threshold), 0.0), 1.0)
|
| 97 |
cfg.prior_strength = max(float(cfg.prior_strength), 0.01)
|
| 98 |
+
cfg.forecast_horizon = max(1, min(int(cfg.forecast_horizon), 6))
|
| 99 |
+
cfg.prefetch_top_k = max(1, min(int(cfg.prefetch_top_k), len(AGENT_ROLES)))
|
| 100 |
+
cfg.forecast_discount = min(max(float(cfg.forecast_discount), 0.05), 1.0)
|
| 101 |
+
cfg.forecast_min_score = min(max(float(cfg.forecast_min_score), 0.0), 1.0)
|
| 102 |
+
cfg.utility_threshold_ms = float(cfg.utility_threshold_ms)
|
| 103 |
cfg.timeline_points = max(40, min(int(cfg.timeline_points), 1000))
|
| 104 |
if cfg.prefetch_policy not in PREFETCH_POLICIES:
|
| 105 |
raise ValueError(f"Unsupported execution prefetch policy: {cfg.prefetch_policy}")
|
|
|
|
| 133 |
available_time: float
|
| 134 |
source: str
|
| 135 |
generation: int
|
| 136 |
+
used: bool = False
|
| 137 |
|
| 138 |
|
| 139 |
class OnlineTransitionPredictor:
|
|
|
|
| 161 |
best = max(self.targets, key=lambda target: (probabilities[target], -self.targets.index(target)))
|
| 162 |
return best, probabilities[best], probabilities
|
| 163 |
|
| 164 |
+
def forecast(
|
| 165 |
+
self,
|
| 166 |
+
current_role: str,
|
| 167 |
+
*,
|
| 168 |
+
horizon: int = 3,
|
| 169 |
+
discount: float = 0.75,
|
| 170 |
+
) -> dict[str, Any]:
|
| 171 |
+
"""Roll the learned first-order matrix forward without future trace access.
|
| 172 |
+
|
| 173 |
+
``role_scores`` are discounted expected future visits, not calibrated
|
| 174 |
+
probabilities of at-least-one reuse. They are therefore used as a
|
| 175 |
+
ranking signal and are normalized separately for UI/decision thresholds.
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
horizon = max(1, min(int(horizon), 8))
|
| 179 |
+
discount = min(max(float(discount), 0.0), 1.0)
|
| 180 |
+
distributions: list[dict[str, float]] = []
|
| 181 |
+
current: dict[str, float] = {target: 0.0 for target in self.targets}
|
| 182 |
+
current[current_role] = 1.0
|
| 183 |
+
role_scores = {role: 0.0 for role in AGENT_ROLES}
|
| 184 |
+
|
| 185 |
+
for depth in range(1, horizon + 1):
|
| 186 |
+
nxt = {target: 0.0 for target in self.targets}
|
| 187 |
+
for source, mass in current.items():
|
| 188 |
+
if mass <= 0.0:
|
| 189 |
+
continue
|
| 190 |
+
if source == END:
|
| 191 |
+
nxt[END] += mass
|
| 192 |
+
continue
|
| 193 |
+
_, _, probs = self.predict(source)
|
| 194 |
+
for target, probability in probs.items():
|
| 195 |
+
nxt[target] += mass * probability
|
| 196 |
+
distributions.append(nxt)
|
| 197 |
+
weight = discount ** (depth - 1)
|
| 198 |
+
for role in AGENT_ROLES:
|
| 199 |
+
role_scores[role] += weight * nxt.get(role, 0.0)
|
| 200 |
+
current = nxt
|
| 201 |
+
|
| 202 |
+
total_score = sum(role_scores.values())
|
| 203 |
+
normalized = {
|
| 204 |
+
role: (score / total_score if total_score > 1e-12 else 0.0)
|
| 205 |
+
for role, score in role_scores.items()
|
| 206 |
+
}
|
| 207 |
+
ranked = sorted(AGENT_ROLES, key=lambda role: (-role_scores[role], role))
|
| 208 |
+
return {
|
| 209 |
+
"horizon": horizon,
|
| 210 |
+
"discount": discount,
|
| 211 |
+
"distributions": distributions,
|
| 212 |
+
"role_scores": role_scores,
|
| 213 |
+
"normalized_scores": normalized,
|
| 214 |
+
"ranked_roles": ranked,
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
def observe(self, current_role: str, next_role: str) -> None:
|
| 218 |
row = self.rows[current_role]
|
| 219 |
if self.decay < 1.0:
|
|
|
|
| 289 |
return workflows
|
| 290 |
|
| 291 |
|
| 292 |
+
def _prediction_calibration(rows: list[dict[str, Any]], bins: int = 10) -> dict[str, Any]:
|
| 293 |
+
if not rows:
|
| 294 |
+
return {"count": 0, "brier": 0.0, "log_loss": 0.0, "ece": 0.0, "bins": []}
|
| 295 |
+
targets = (*AGENT_ROLES, END)
|
| 296 |
+
brier_total = 0.0
|
| 297 |
+
log_total = 0.0
|
| 298 |
+
buckets: list[list[tuple[float, float]]] = [[] for _ in range(max(2, bins))]
|
| 299 |
+
for row in rows:
|
| 300 |
+
probs = row.get("probabilities", {})
|
| 301 |
+
actual = str(row.get("actual_next_role", END))
|
| 302 |
+
brier_total += sum((float(probs.get(target, 0.0)) - (1.0 if target == actual else 0.0)) ** 2 for target in targets)
|
| 303 |
+
actual_p = max(float(probs.get(actual, 0.0)), 1e-12)
|
| 304 |
+
log_total += -math.log(actual_p)
|
| 305 |
+
confidence = min(max(float(row.get("confidence", 0.0)), 0.0), 1.0)
|
| 306 |
+
correct = 1.0 if row.get("prediction_correct") else 0.0
|
| 307 |
+
index = min(int(confidence * len(buckets)), len(buckets) - 1)
|
| 308 |
+
buckets[index].append((confidence, correct))
|
| 309 |
+
ece = 0.0
|
| 310 |
+
rendered = []
|
| 311 |
+
for index, bucket in enumerate(buckets):
|
| 312 |
+
if not bucket:
|
| 313 |
+
continue
|
| 314 |
+
avg_conf = mean(value[0] for value in bucket)
|
| 315 |
+
accuracy = mean(value[1] for value in bucket)
|
| 316 |
+
weight = len(bucket) / len(rows)
|
| 317 |
+
ece += weight * abs(avg_conf - accuracy)
|
| 318 |
+
rendered.append(
|
| 319 |
+
{
|
| 320 |
+
"lower": index / len(buckets),
|
| 321 |
+
"upper": (index + 1) / len(buckets),
|
| 322 |
+
"count": len(bucket),
|
| 323 |
+
"mean_confidence": avg_conf,
|
| 324 |
+
"accuracy": accuracy,
|
| 325 |
+
}
|
| 326 |
+
)
|
| 327 |
+
return {
|
| 328 |
+
"count": len(rows),
|
| 329 |
+
"brier": brier_total / len(rows),
|
| 330 |
+
"log_loss": log_total / len(rows),
|
| 331 |
+
"ece": ece,
|
| 332 |
+
"bins": rendered,
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
|
| 336 |
class ExecutionLearningSimulator:
|
| 337 |
"""Reference simulator for online workflow learning and prefix prefetch.
|
| 338 |
|
|
|
|
| 368 |
self.pressure_evictions = 0
|
| 369 |
self.prefetch_attempts = 0
|
| 370 |
self.prefetch_correct = 0
|
| 371 |
+
self.prefetch_useful = 0
|
| 372 |
self.prefetch_bytes = 0.0
|
| 373 |
+
self.prefetch_useful_bytes = 0.0
|
| 374 |
+
self.unused_prefetch_bytes = 0.0
|
| 375 |
self.wrong_step_prefetch_bytes = 0.0
|
| 376 |
self.transfer_latencies_ms: list[float] = []
|
| 377 |
self.prefix_hits = 0
|
|
|
|
| 419 |
if len(self.timeline) > self.cfg.timeline_points * 2:
|
| 420 |
self.timeline = self.timeline[::2]
|
| 421 |
|
| 422 |
+
def _mark_unused_prefetch(self, entry: PrefixEntry) -> None:
|
| 423 |
+
if entry.source == "prefetch" and not entry.used:
|
| 424 |
+
self.unused_prefetch_bytes += entry.size_gb * 1e9
|
| 425 |
+
|
| 426 |
+
def _candidate_eviction_cost_ms(self, role: str, forecast_scores: dict[str, float]) -> float:
|
| 427 |
+
"""Approximate downstream cost of evicting forecast-relevant cached prefixes."""
|
| 428 |
+
size_gb = self.role_size_gb[role]
|
| 429 |
+
existing = self.cache.get(role)
|
| 430 |
+
current_without = self._used_gb() - (existing.size_gb if existing else 0.0)
|
| 431 |
+
overflow = max(0.0, current_without + size_gb - self.cache_capacity_gb)
|
| 432 |
+
if overflow <= 1e-12:
|
| 433 |
+
return 0.0
|
| 434 |
+
cost_ms = 0.0
|
| 435 |
+
reclaimed = 0.0
|
| 436 |
+
candidates = sorted(
|
| 437 |
+
[entry for key, entry in self.cache.items() if key != role],
|
| 438 |
+
key=lambda entry: (entry.last_access, entry.role),
|
| 439 |
+
)
|
| 440 |
+
for victim in candidates:
|
| 441 |
+
if reclaimed >= overflow - 1e-12:
|
| 442 |
+
break
|
| 443 |
+
reuse_score = max(float(forecast_scores.get(victim.role, 0.0)), 0.0)
|
| 444 |
+
recompute_ms = self.latency.prefill_seconds([ROLE_PREFIX_TOKENS[victim.role]]) * 1000.0
|
| 445 |
+
cost_ms += reuse_score * recompute_ms
|
| 446 |
+
reclaimed += victim.size_gb
|
| 447 |
+
return cost_ms
|
| 448 |
+
|
| 449 |
+
def _prefetch_utility_ms(self, role: str, forecast_scores: dict[str, float]) -> float:
|
| 450 |
+
score = max(float(forecast_scores.get(role, 0.0)), 0.0)
|
| 451 |
+
saved_ms = self.latency.prefill_seconds([ROLE_PREFIX_TOKENS[role]]) * 1000.0
|
| 452 |
+
transfer_ms = self.cfg.transfer_base_ms + self.role_size_gb[role] / self.cfg.host_bandwidth_gbps * 1000.0
|
| 453 |
+
eviction_ms = self._candidate_eviction_cost_ms(role, forecast_scores)
|
| 454 |
+
return score * saved_ms - transfer_ms - eviction_ms
|
| 455 |
+
|
| 456 |
def _evict_for(self, role: str, size_gb: float) -> bool:
|
| 457 |
existing = self.cache.get(role)
|
| 458 |
current_without_role = self._used_gb() - (existing.size_gb if existing else 0.0)
|
|
|
|
| 464 |
return False
|
| 465 |
victim = min(candidates, key=lambda entry: (entry.last_access, entry.role))
|
| 466 |
self._integrate_memory(self.now)
|
| 467 |
+
self._mark_unused_prefetch(victim)
|
| 468 |
self.cache.pop(victim.role, None)
|
| 469 |
self.prefix_source.pop(victim.role, None)
|
| 470 |
self.pressure_evictions += 1
|
|
|
|
| 477 |
return None
|
| 478 |
self._integrate_memory(self.now)
|
| 479 |
previous = self.cache.get(role)
|
| 480 |
+
if previous is not None:
|
| 481 |
+
self._mark_unused_prefetch(previous)
|
| 482 |
generation = (previous.generation + 1) if previous else 1
|
| 483 |
self.cache[role] = PrefixEntry(role, size_gb, self.now, available_time, source, generation)
|
| 484 |
self.prefix_source[role] = source
|
|
|
|
| 510 |
return END
|
| 511 |
return workflow.steps[step_index + 1].role
|
| 512 |
|
| 513 |
+
def _actual_future_roles(self, workflow_id: int, step_index: int, horizon: int) -> list[str]:
|
| 514 |
+
workflow = self.workflows[workflow_id]
|
| 515 |
+
roles = [step.role for step in workflow.steps[step_index + 1 : step_index + 1 + max(1, horizon)]]
|
| 516 |
+
return roles
|
| 517 |
+
|
| 518 |
+
def _planned_prefetch_roles(
|
| 519 |
+
self,
|
| 520 |
+
workflow_id: int,
|
| 521 |
+
step_index: int,
|
| 522 |
+
current_role: str,
|
| 523 |
+
) -> tuple[str, float, dict[str, float], dict[str, Any], list[dict[str, Any]]]:
|
| 524 |
actual_next = self._actual_next_role(workflow_id, step_index)
|
| 525 |
+
predicted_role, confidence, probabilities = self.predictor.predict(current_role)
|
| 526 |
+
forecast = self.predictor.forecast(
|
| 527 |
+
current_role,
|
| 528 |
+
horizon=self.cfg.forecast_horizon,
|
| 529 |
+
discount=self.cfg.forecast_discount,
|
| 530 |
+
)
|
| 531 |
+
plans: list[dict[str, Any]] = []
|
| 532 |
+
|
| 533 |
if self.cfg.prefetch_policy == "oracle":
|
| 534 |
predicted_role = actual_next
|
| 535 |
confidence = 1.0
|
| 536 |
+
probabilities = {target: 0.0 for target in (*AGENT_ROLES, END)}
|
| 537 |
probabilities[predicted_role] = 1.0
|
| 538 |
+
if actual_next != END:
|
| 539 |
+
plans = [{"role": actual_next, "score": 1.0, "utility_ms": None, "oracle": True}]
|
| 540 |
+
return predicted_role, confidence, probabilities, forecast, plans
|
| 541 |
+
|
| 542 |
+
if self.cfg.prefetch_policy == "oracle_horizon":
|
| 543 |
+
actual_future = self._actual_future_roles(workflow_id, step_index, self.cfg.forecast_horizon)
|
| 544 |
+
predicted_role = actual_next
|
| 545 |
+
confidence = 1.0
|
| 546 |
+
probabilities = {target: 0.0 for target in (*AGENT_ROLES, END)}
|
| 547 |
+
probabilities[predicted_role] = 1.0
|
| 548 |
+
seen: set[str] = set()
|
| 549 |
+
for role in actual_future:
|
| 550 |
+
if role not in seen:
|
| 551 |
+
plans.append({"role": role, "score": 1.0, "utility_ms": None, "oracle": True})
|
| 552 |
+
seen.add(role)
|
| 553 |
+
if len(plans) >= self.cfg.prefetch_top_k:
|
| 554 |
+
break
|
| 555 |
+
return predicted_role, confidence, probabilities, forecast, plans
|
| 556 |
+
|
| 557 |
+
if self.cfg.prefetch_policy in {"none", "cumulative", "decayed"}:
|
| 558 |
+
if self.cfg.prefetch_policy != "none" and predicted_role != END and confidence >= self.cfg.confidence_threshold:
|
| 559 |
+
plans = [{"role": predicted_role, "score": confidence, "utility_ms": None, "oracle": False}]
|
| 560 |
+
return predicted_role, confidence, probabilities, forecast, plans
|
| 561 |
+
|
| 562 |
+
ranked = forecast["ranked_roles"]
|
| 563 |
+
normalized = forecast["normalized_scores"]
|
| 564 |
+
raw_scores = forecast["role_scores"]
|
| 565 |
+
for role in ranked:
|
| 566 |
+
if len(plans) >= self.cfg.prefetch_top_k:
|
| 567 |
+
break
|
| 568 |
+
normalized_score = float(normalized.get(role, 0.0))
|
| 569 |
+
if normalized_score < self.cfg.forecast_min_score:
|
| 570 |
+
continue
|
| 571 |
+
utility_ms = self._prefetch_utility_ms(role, raw_scores)
|
| 572 |
+
if self.cfg.prefetch_policy == "utility" and utility_ms < self.cfg.utility_threshold_ms:
|
| 573 |
+
continue
|
| 574 |
+
plans.append(
|
| 575 |
+
{
|
| 576 |
+
"role": role,
|
| 577 |
+
"score": normalized_score,
|
| 578 |
+
"raw_score": float(raw_scores.get(role, 0.0)),
|
| 579 |
+
"utility_ms": utility_ms,
|
| 580 |
+
"oracle": False,
|
| 581 |
+
}
|
| 582 |
+
)
|
| 583 |
+
return predicted_role, confidence, probabilities, forecast, plans
|
| 584 |
+
|
| 585 |
+
def _schedule_prediction(self, workflow_id: int, step_index: int, current_role: str) -> None:
|
| 586 |
+
actual_next = self._actual_next_role(workflow_id, step_index)
|
| 587 |
+
predicted_role, confidence, probabilities, forecast, plans = self._planned_prefetch_roles(
|
| 588 |
+
workflow_id, step_index, current_role
|
| 589 |
+
)
|
| 590 |
+
attempted_roles: list[str] = []
|
| 591 |
+
attempt_sizes: dict[str, float] = {}
|
| 592 |
+
attempt_reasons: dict[str, str] = {}
|
| 593 |
+
|
| 594 |
+
for plan in plans:
|
| 595 |
+
role = str(plan["role"])
|
| 596 |
+
result = self._prefetch(role)
|
| 597 |
+
attempt_reasons[role] = str(result.get("reason", "unknown"))
|
| 598 |
+
if result.get("attempted"):
|
| 599 |
+
attempted_roles.append(role)
|
| 600 |
+
attempt_sizes[role] = float(result.get("size_gb", 0.0))
|
| 601 |
+
|
| 602 |
+
if self.cfg.prefetch_policy == "none":
|
| 603 |
+
decision_reason = "learn_only"
|
| 604 |
+
elif not plans:
|
| 605 |
+
decision_reason = "no_candidate"
|
| 606 |
+
elif attempted_roles:
|
| 607 |
+
decision_reason = "scheduled"
|
| 608 |
+
else:
|
| 609 |
+
decision_reason = ",".join(sorted(set(attempt_reasons.values()))) or "not_scheduled"
|
| 610 |
|
| 611 |
self.pending_predictions[(workflow_id, step_index)] = {
|
| 612 |
"workflow_id": workflow_id,
|
|
|
|
| 615 |
"predicted_role": predicted_role,
|
| 616 |
"confidence": confidence,
|
| 617 |
"probabilities": probabilities,
|
| 618 |
+
"forecast_horizon": self.cfg.forecast_horizon,
|
| 619 |
+
"forecast_discount": self.cfg.forecast_discount,
|
| 620 |
+
"forecast_role_scores": forecast["role_scores"],
|
| 621 |
+
"forecast_normalized_scores": forecast["normalized_scores"],
|
| 622 |
+
"forecast_roles": [str(plan["role"]) for plan in plans],
|
| 623 |
+
"forecast_plans": plans,
|
| 624 |
+
"prefetch_attempted": bool(attempted_roles),
|
| 625 |
+
"prefetch_roles": attempted_roles,
|
| 626 |
+
"prefetch_reasons": attempt_reasons,
|
| 627 |
"prefetch_reason": decision_reason,
|
| 628 |
+
"prefetch_sizes_gb": attempt_sizes,
|
| 629 |
+
"prefetch_size_gb": sum(attempt_sizes.values()),
|
| 630 |
"shifted_regime": self.workflows[workflow_id].steps[step_index].shifted_regime,
|
| 631 |
}
|
| 632 |
|
|
|
|
|
|
|
| 633 |
if actual_next == END:
|
| 634 |
self._observe_transition(workflow_id, step_index, actual_next)
|
| 635 |
|
|
|
|
| 641 |
if pending is None:
|
| 642 |
return
|
| 643 |
predicted_role = str(pending["predicted_role"])
|
| 644 |
+
attempted_roles = [str(role) for role in pending.get("prefetch_roles", [])]
|
| 645 |
correct = predicted_role == actual_next
|
| 646 |
+
if actual_next != END and actual_next in attempted_roles:
|
| 647 |
self.prefetch_correct += 1
|
| 648 |
+
immediate_wrong_bytes = 0.0
|
| 649 |
+
for role in attempted_roles:
|
| 650 |
+
if role != actual_next:
|
| 651 |
+
immediate_wrong_bytes += float(pending.get("prefetch_sizes_gb", {}).get(role, 0.0)) * 1e9
|
| 652 |
+
self.wrong_step_prefetch_bytes += immediate_wrong_bytes
|
| 653 |
+
|
| 654 |
+
future_roles = self._actual_future_roles(workflow_id, step_index, int(pending.get("forecast_horizon", 1)))
|
| 655 |
+
actual_future_set = set(future_roles)
|
| 656 |
+
forecast_roles = [str(role) for role in pending.get("forecast_roles", [])]
|
| 657 |
+
forecast_set = set(forecast_roles)
|
| 658 |
+
forecast_recall = (
|
| 659 |
+
len(actual_future_set & forecast_set) / len(actual_future_set)
|
| 660 |
+
if actual_future_set
|
| 661 |
+
else None
|
| 662 |
+
)
|
| 663 |
self.prediction_rows.append(
|
| 664 |
pending
|
| 665 |
| {
|
| 666 |
"actual_next_role": actual_next,
|
| 667 |
+
"actual_future_roles": future_roles,
|
| 668 |
"prediction_correct": correct,
|
| 669 |
+
"forecast_next_hit": actual_next in forecast_set if actual_next != END else predicted_role == END,
|
| 670 |
+
"forecast_recall": forecast_recall,
|
| 671 |
+
"immediate_wrong_prefetch_gb": immediate_wrong_bytes / 1e9,
|
| 672 |
"post_shift": bool(pending["shifted_regime"]),
|
| 673 |
}
|
| 674 |
)
|
| 675 |
+
if self.cfg.prefetch_policy not in {"oracle", "oracle_horizon"}:
|
|
|
|
| 676 |
self.predictor.observe(current_role, actual_next)
|
| 677 |
|
| 678 |
def _start_next(self) -> None:
|
|
|
|
| 709 |
self.prefetch_hits += 1
|
| 710 |
self.prefill_tokens_saved += prefix_tokens
|
| 711 |
if entry is not None:
|
| 712 |
+
if entry.source == "prefetch" and not entry.used:
|
| 713 |
+
entry.used = True
|
| 714 |
+
self.prefetch_useful += 1
|
| 715 |
+
self.prefetch_useful_bytes += entry.size_gb * 1e9
|
| 716 |
entry.last_access = self.now + prefix_wait_s
|
| 717 |
prefill_tokens = max(step.dynamic_prompt_tokens, 1)
|
| 718 |
else:
|
|
|
|
| 833 |
def accuracy(rows: list[dict[str, Any]]) -> float:
|
| 834 |
return sum(1 for row in rows if row["prediction_correct"]) / len(rows) if rows else 0.0
|
| 835 |
|
| 836 |
+
# Account for prefetched entries that remained unused through the end of the run.
|
| 837 |
+
for entry in list(self.cache.values()):
|
| 838 |
+
self._mark_unused_prefetch(entry)
|
| 839 |
prefetch_precision = self.prefetch_correct / self.prefetch_attempts if self.prefetch_attempts else 0.0
|
| 840 |
+
prefetch_coverage = (
|
| 841 |
+
sum(1 for row in predictions if row.get("prefetch_attempted")) / len(predictions)
|
| 842 |
+
if predictions else 0.0
|
| 843 |
+
)
|
| 844 |
+
prefetch_utilization = self.prefetch_useful / self.prefetch_attempts if self.prefetch_attempts else 0.0
|
| 845 |
+
forecast_recall_values = [
|
| 846 |
+
float(row["forecast_recall"]) for row in predictions if row.get("forecast_recall") is not None
|
| 847 |
+
]
|
| 848 |
+
forecast_recall = mean(forecast_recall_values) if forecast_recall_values else 0.0
|
| 849 |
workflow_count = len(self.workflows)
|
| 850 |
completed_count = len(self.workflow_completion)
|
| 851 |
summary = {
|
|
|
|
| 869 |
"prefetch_correct": self.prefetch_correct,
|
| 870 |
"prefetch_precision": prefetch_precision,
|
| 871 |
"prefetch_coverage": prefetch_coverage,
|
| 872 |
+
"prefetch_utilization": prefetch_utilization,
|
| 873 |
+
"prefetch_attempts_per_prediction": self.prefetch_attempts / len(predictions) if predictions else 0.0,
|
| 874 |
+
"forecast_recall": forecast_recall,
|
| 875 |
"prefetch_gb": self.prefetch_bytes / 1e9,
|
| 876 |
+
"prefetch_useful_gb": self.prefetch_useful_bytes / 1e9,
|
| 877 |
+
"unused_prefetch_gb": self.unused_prefetch_bytes / 1e9,
|
| 878 |
"wrong_step_prefetch_gb": self.wrong_step_prefetch_bytes / 1e9,
|
| 879 |
"p95_prefetch_transfer_ms": percentile(self.transfer_latencies_ms, 0.95),
|
| 880 |
}
|
|
|
|
| 883 |
"top1_accuracy": accuracy(predictions),
|
| 884 |
"pre_shift_accuracy": accuracy(pre),
|
| 885 |
"post_shift_accuracy": accuracy(post),
|
| 886 |
+
"calibration": _prediction_calibration(predictions),
|
| 887 |
+
"pre_shift_calibration": _prediction_calibration(pre),
|
| 888 |
+
"post_shift_calibration": _prediction_calibration(post),
|
| 889 |
"rows": predictions[:4000],
|
| 890 |
"predictor": self.predictor.snapshot(),
|
| 891 |
}
|
|
|
|
| 900 |
"simulator": "InferScale-Sim",
|
| 901 |
"mode": "online-agent-execution-learning",
|
| 902 |
"latency_profile_type": "analytical-reference",
|
| 903 |
+
"execution_model": "online-transition-multistep-prefix-prefetch-reference",
|
| 904 |
"prefetch_policy": self.cfg.prefetch_policy,
|
| 905 |
+
"lookahead": "oracle-upper-bound" if self.cfg.prefetch_policy in {"oracle", "oracle_horizon"} else "no-future-transition-lookahead",
|
| 906 |
"warning": "Execution Learning isolates cross-workflow static-prefix prediction/prefetch from session-KV retention and dynamic batching.",
|
| 907 |
},
|
| 908 |
"summary": summary,
|
|
|
|
| 934 |
"prefix_hit_rate": result["resource"]["prefix_hit_rate"],
|
| 935 |
"prefetch_precision": result["resource"]["prefetch_precision"],
|
| 936 |
"prefetch_coverage": result["resource"]["prefetch_coverage"],
|
| 937 |
+
"prefetch_utilization": result["resource"].get("prefetch_utilization", 0.0),
|
| 938 |
+
"forecast_recall": result["resource"].get("forecast_recall", 0.0),
|
| 939 |
"mean_hbm_gb": result["resource"]["mean_prefix_hbm_gb"],
|
| 940 |
+
"pressure_evictions": result["resource"].get("pressure_evictions", 0),
|
| 941 |
"wrong_step_prefetch_gb": result["resource"]["wrong_step_prefetch_gb"],
|
| 942 |
+
"unused_prefetch_gb": result["resource"].get("unused_prefetch_gb", 0.0),
|
| 943 |
"prefill_tokens_saved": result["resource"]["prefill_tokens_saved"],
|
| 944 |
"top1_accuracy": result["prediction"]["top1_accuracy"],
|
| 945 |
+
"brier": result["prediction"].get("calibration", {}).get("brier", 0.0),
|
| 946 |
+
"ece": result["prediction"].get("calibration", {}).get("ece", 0.0),
|
| 947 |
+
"post_shift_ece": result["prediction"].get("post_shift_calibration", {}).get("ece", 0.0),
|
| 948 |
"pre_shift_accuracy": result["prediction"]["pre_shift_accuracy"],
|
| 949 |
"post_shift_accuracy": result["prediction"]["post_shift_accuracy"],
|
| 950 |
}
|
|
|
|
| 1059 |
"association": association,
|
| 1060 |
"note": "Better next-role prediction need not minimize serving latency because cache occupancy and transfer timing mediate the effect.",
|
| 1061 |
}
|
| 1062 |
+
|
| 1063 |
+
|
| 1064 |
+
def execution_planning_study(config: dict[str, Any]) -> dict[str, Any]:
|
| 1065 |
+
"""Compare one-step, multi-step, utility-aware, and clairvoyant planning."""
|
| 1066 |
+
base = ExecutionLearningConfig.from_dict(config)
|
| 1067 |
+
workflows = _common_workflows(base)
|
| 1068 |
+
candidates = [
|
| 1069 |
+
("Top-1 decayed", "decayed"),
|
| 1070 |
+
("Multi-step top-k", "multistep"),
|
| 1071 |
+
("Utility-aware multi-step", "utility"),
|
| 1072 |
+
("Oracle future-set", "oracle_horizon"),
|
| 1073 |
+
]
|
| 1074 |
+
rows: list[dict[str, Any]] = []
|
| 1075 |
+
results: dict[str, dict[str, Any]] = {}
|
| 1076 |
+
for label, policy in candidates:
|
| 1077 |
+
cfg = ExecutionLearningConfig.from_dict(base.to_dict())
|
| 1078 |
+
cfg.prefetch_policy = policy
|
| 1079 |
+
result = run_execution_learning(cfg.to_dict(), workflows)
|
| 1080 |
+
results[label] = result
|
| 1081 |
+
rows.append(_row(label, result))
|
| 1082 |
+
best_ttft = min(rows, key=lambda row: row["p95_step_ttft_ms"]) if rows else None
|
| 1083 |
+
best_efficiency = max(
|
| 1084 |
+
rows,
|
| 1085 |
+
key=lambda row: (
|
| 1086 |
+
row["prefill_tokens_saved"] / max(row["mean_hbm_gb"], 1e-9),
|
| 1087 |
+
-row["p95_step_ttft_ms"],
|
| 1088 |
+
),
|
| 1089 |
+
) if rows else None
|
| 1090 |
+
return {
|
| 1091 |
+
"protocol": "common-shifted-agent-workflow-trace",
|
| 1092 |
+
"study": "multi-step-prefetch-planning",
|
| 1093 |
+
"rows": rows,
|
| 1094 |
+
"results": results,
|
| 1095 |
+
"best_ttft_policy": best_ttft["label"] if best_ttft else None,
|
| 1096 |
+
"best_prefill_per_hbm_policy": best_efficiency["label"] if best_efficiency else None,
|
| 1097 |
+
"note": "Oracle future-set sees realized future roles only as an information upper bound; online multi-step policies roll the learned transition matrix forward without future trace access.",
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
|
| 1101 |
+
def execution_horizon_sweep(
|
| 1102 |
+
config: dict[str, Any], horizon_values: list[int] | None = None
|
| 1103 |
+
) -> dict[str, Any]:
|
| 1104 |
+
base = ExecutionLearningConfig.from_dict(config)
|
| 1105 |
+
base.prefetch_policy = "utility"
|
| 1106 |
+
workflows = _common_workflows(base)
|
| 1107 |
+
values = horizon_values or [1, 2, 3, 4, 5]
|
| 1108 |
+
cleaned = sorted({max(1, min(int(value), 6)) for value in values})
|
| 1109 |
+
rows: list[dict[str, Any]] = []
|
| 1110 |
+
for horizon in cleaned:
|
| 1111 |
+
cfg = ExecutionLearningConfig.from_dict(base.to_dict())
|
| 1112 |
+
cfg.forecast_horizon = horizon
|
| 1113 |
+
result = run_execution_learning(cfg.to_dict(), workflows)
|
| 1114 |
+
rows.append(_row(f"horizon {horizon}", result) | {"horizon": horizon})
|
| 1115 |
+
best_ttft = min(rows, key=lambda row: (row["p95_step_ttft_ms"], row["unused_prefetch_gb"])) if rows else None
|
| 1116 |
+
best_utilization = max(rows, key=lambda row: (row["prefetch_utilization"], -row["unused_prefetch_gb"])) if rows else None
|
| 1117 |
+
return {
|
| 1118 |
+
"protocol": "common-shifted-agent-workflow-trace",
|
| 1119 |
+
"study": "forecast-horizon-sweep",
|
| 1120 |
+
"rows": rows,
|
| 1121 |
+
"best_ttft_horizon": best_ttft["horizon"] if best_ttft else None,
|
| 1122 |
+
"best_utilization_horizon": best_utilization["horizon"] if best_utilization else None,
|
| 1123 |
+
"note": "Longer forecasts can expose future reuse but may spend bandwidth and cache capacity on prefixes that are not consumed before eviction.",
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
|
| 1127 |
+
def execution_budget_sweep(
|
| 1128 |
+
config: dict[str, Any], budget_values: list[float] | None = None
|
| 1129 |
+
) -> dict[str, Any]:
|
| 1130 |
+
base = ExecutionLearningConfig.from_dict(config)
|
| 1131 |
+
workflows = _common_workflows(base)
|
| 1132 |
+
values = budget_values or [0.30, 0.50, 0.75, 1.00]
|
| 1133 |
+
cleaned = sorted({min(max(float(value), 0.10), 1.50) for value in values})
|
| 1134 |
+
policies = [
|
| 1135 |
+
("Top-1", "decayed"),
|
| 1136 |
+
("Multi-step", "multistep"),
|
| 1137 |
+
("Utility-aware", "utility"),
|
| 1138 |
+
]
|
| 1139 |
+
rows: list[dict[str, Any]] = []
|
| 1140 |
+
for budget in cleaned:
|
| 1141 |
+
for label, policy in policies:
|
| 1142 |
+
cfg = ExecutionLearningConfig.from_dict(base.to_dict())
|
| 1143 |
+
cfg.prefix_cache_budget_fraction = budget
|
| 1144 |
+
cfg.prefetch_policy = policy
|
| 1145 |
+
result = run_execution_learning(cfg.to_dict(), workflows)
|
| 1146 |
+
rows.append(_row(label, result) | {"budget": budget, "policy_label": label})
|
| 1147 |
+
winners = []
|
| 1148 |
+
for budget in cleaned:
|
| 1149 |
+
group = [row for row in rows if abs(float(row["budget"]) - budget) < 1e-12]
|
| 1150 |
+
if group:
|
| 1151 |
+
winner = min(group, key=lambda row: (row["p95_step_ttft_ms"], row["unused_prefetch_gb"]))
|
| 1152 |
+
winners.append({"budget": budget, "policy": winner["policy_label"], "p95_step_ttft_ms": winner["p95_step_ttft_ms"]})
|
| 1153 |
+
return {
|
| 1154 |
+
"protocol": "common-shifted-agent-workflow-trace",
|
| 1155 |
+
"study": "prefix-cache-budget-policy-sweep",
|
| 1156 |
+
"rows": rows,
|
| 1157 |
+
"winners": winners,
|
| 1158 |
+
"note": "All policies see the same workflows at each cache budget; the sweep exposes when broader forecasting helps versus when it increases cache pressure.",
|
| 1159 |
+
}
|
| 1160 |
+
|
tests/test_execution.py
CHANGED
|
@@ -84,3 +84,59 @@ def test_workflow_generation_has_structured_roles_and_shift() -> None:
|
|
| 84 |
assert "planner" in roles
|
| 85 |
assert len(roles) >= 3
|
| 86 |
assert any(step.shifted_regime for workflow in workflows for step in workflow.steps)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
assert "planner" in roles
|
| 85 |
assert len(roles) >= 3
|
| 86 |
assert any(step.shifted_regime for workflow in workflows for step in workflow.steps)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_multistep_forecast_is_normalized_and_no_lookahead() -> None:
|
| 90 |
+
predictor = OnlineTransitionPredictor(decay=0.85, prior_strength=0.2)
|
| 91 |
+
for _ in range(5):
|
| 92 |
+
predictor.observe("planner", "retriever")
|
| 93 |
+
forecast = predictor.forecast("planner", horizon=3, discount=0.75)
|
| 94 |
+
assert forecast["horizon"] == 3
|
| 95 |
+
assert len(forecast["distributions"]) == 3
|
| 96 |
+
assert abs(sum(forecast["normalized_scores"].values()) - 1.0) < 1e-9
|
| 97 |
+
assert forecast["ranked_roles"][0] == "retriever"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def test_multistep_and_utility_runs_report_forecast_and_calibration_metrics() -> None:
|
| 101 |
+
for policy in ("multistep", "utility"):
|
| 102 |
+
result = run_execution_learning(
|
| 103 |
+
_cfg()
|
| 104 |
+
| {
|
| 105 |
+
"prefetch_policy": policy,
|
| 106 |
+
"forecast_horizon": 3,
|
| 107 |
+
"prefetch_top_k": 2,
|
| 108 |
+
"forecast_min_score": 0.05,
|
| 109 |
+
}
|
| 110 |
+
)
|
| 111 |
+
assert result["provenance"]["lookahead"] == "no-future-transition-lookahead"
|
| 112 |
+
assert 0 <= result["resource"]["forecast_recall"] <= 1
|
| 113 |
+
assert 0 <= result["resource"]["prefetch_utilization"] <= 1
|
| 114 |
+
assert result["prediction"]["calibration"]["brier"] >= 0
|
| 115 |
+
assert 0 <= result["prediction"]["calibration"]["ece"] <= 1
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def test_planning_and_horizon_studies_return_controlled_candidates() -> None:
|
| 119 |
+
from inferscale.execution import execution_horizon_sweep, execution_planning_study
|
| 120 |
+
|
| 121 |
+
planning = execution_planning_study(_cfg())
|
| 122 |
+
assert len(planning["rows"]) == 4
|
| 123 |
+
assert planning["best_ttft_policy"] in {row["label"] for row in planning["rows"]}
|
| 124 |
+
assert {row["label"] for row in planning["rows"]} == {
|
| 125 |
+
"Top-1 decayed",
|
| 126 |
+
"Multi-step top-k",
|
| 127 |
+
"Utility-aware multi-step",
|
| 128 |
+
"Oracle future-set",
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
horizons = execution_horizon_sweep(_cfg(), [1, 2, 3])
|
| 132 |
+
assert [row["horizon"] for row in horizons["rows"]] == [1, 2, 3]
|
| 133 |
+
assert horizons["best_ttft_horizon"] in {1, 2, 3}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def test_cache_budget_sweep_compares_three_policies_per_budget() -> None:
|
| 137 |
+
from inferscale.execution import execution_budget_sweep
|
| 138 |
+
|
| 139 |
+
result = execution_budget_sweep(_cfg(), [0.3, 0.6])
|
| 140 |
+
assert len(result["rows"]) == 6
|
| 141 |
+
assert len(result["winners"]) == 2
|
| 142 |
+
assert {row["policy_label"] for row in result["rows"]} == {"Top-1", "Multi-step", "Utility-aware"}
|