ArchitSharma commited on
Commit
b799d1d
·
1 Parent(s): 8f91935

Add online predictive KV tiering experiments

Browse files
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** and can trigger host-to-HBM prefetch during the intervening tool gap. Four policies are available:
141
 
142
- 1. learn-only / no prefetch;
143
- 2. cumulative transition counts;
144
- 3. exponentially decayed transition counts;
145
- 4. an explicitly labeled oracle next-role upper bound.
146
 
147
- Three experiments separate prediction quality from systems quality:
 
 
 
 
 
 
148
 
149
- - **Prefetch Policy Study:** compare the four policies on one common shifted workflow trace;
 
 
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 and compare post-shift accuracy with downstream latency/cache outcomes.
 
 
 
 
 
152
 
153
- A perfect next-role predictor is **not assumed to be a perfect serving policy**: aggressive prefetch can still evict useful prefixes or occupy transfer/cache capacity. The UI therefore reports prediction accuracy and serving outcomes side by side and labels correlations from parameter sweeps as descriptive rather than causal.
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. Prefix hits saved ${fmt(r.prefill_tokens_saved, 0)} prefill tokens while ${fmt(r.wrong_step_prefetch_gb, 4)} GB of transfer was sent toward the wrong immediate next role. Provenance: ${escapeHtml(lookahead)}.`;
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. `validation.py` compares predictions against externally supplied measured cases.
 
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, changing the execution distribution without changing the hardware/model profile. Every candidate in a study replays the exact same generated workflows.
235
 
236
- The online transition learner stores a row of counts for each current role. A prediction is made **before** the next role is observable. The next role is added to the learner only when the tool gap ends and that step becomes ready. `transition_decay = 1` is cumulative counting; smaller values exponentially forget older transitions and can adapt faster after the regime shift.
237
 
238
- The learned top-1 prediction can trigger host-to-HBM prefetch only when its confidence exceeds `confidence_threshold`. Static role-prefix sizes are derived from the analytical KV bytes/token model and fixed role-prefix token counts. The shared prefix cache is intentionally bounded as a fraction of the five-role working set, so wrong prefetches can cause realistic opportunity cost through eviction and transfer occupancy.
239
 
240
- The simulator reports prediction metrics and serving metrics separately:
241
 
242
- - top-1 transition accuracy before/after the shift;
243
- - prefetch precision and coverage;
244
- - wrong-immediate-next-step prefetch volume;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  - prefix hit rate and prefill tokens saved;
246
  - HBM GB-seconds and pressure evictions;
247
  - p95 step TTFT and workflow E2E.
248
 
249
- The oracle next-role candidate is a **clairvoyant upper bound on transition information**, not a guaranteed upper bound on serving performance. Even a perfect local next-step prediction can be globally suboptimal when a small shared cache is polluted or when transfer activity evicts a prefix needed by another queued workflow. This distinction is intentional and is why predictor quality and systems outcomes are shown together.
250
-
251
- ### Confidence-threshold study
252
 
253
- The threshold sweep holds the learned transition model and workload trace fixed while varying how confident the system must be before acting. Low thresholds increase prefetch coverage but can increase wrong-step traffic/cache pollution; high thresholds become conservative and may miss useful overlap opportunities. Correlations reported by the UI are descriptive summaries of that simulated sweep, not causal estimators.
254
 
255
- ### Forgetting-rate study
256
 
257
- The decay sweep holds the trace and confidence threshold fixed while changing how much historical transition evidence is retained. It reports the decay with highest post-shift accuracy separately from the decay with lowest p95 TTFT. These need not match because transfer timing, cache occupancy, and other workflows mediate the value of a correct prediction.
 
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 now implements a deliberately smaller first-order transition learner over synthetic agent roles. The model is updated only after a transition is observed; learned next-role predictions can trigger conservative static-prefix prefetch into a bounded HBM cache. InferScale does not reproduce CacheScout's vLLM integration, learned survival policy, or production critical-path design.
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: expose prediction confidence as a serving-policy knob rather than automatically acting on every top-1 guess. The Confidence Threshold Study measures how conservative prefetch changes coverage, precision, wasted transfer, cache hits, and TTFT on one common shifted trace. InferScale uses a transparent first-order transition baseline rather than PBKV's richer context-aware predictor.
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 transitions</option><option value="oracle">Oracle next-role 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">fraction</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
  </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 workflow transitions, then prefetch conservatively</h2><p>Execution Learning maintains a first-order transition model over agent roles and only updates it after the next role is observed. Static role prefixes share a bounded HBM cache. Learned next-role predictions can trigger host-to-HBM prefetch during tool gaps, while confidence and forgetting sweeps expose the difference between prediction quality and downstream serving quality.</p></article>
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>Prediction-based KV management for dynamic agent workflows motivates conservative prefetch under imperfect future-step predictions.</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>
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 execution_decay_sweep, execution_prefetch_study, execution_threshold_sweep, run_execution_learning
 
 
 
 
 
 
 
 
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.8.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 _schedule_prediction(self, workflow_id: int, step_index: int, current_role: str) -> None:
 
 
 
 
 
 
 
 
 
 
366
  actual_next = self._actual_next_role(workflow_id, step_index)
367
- predicted_role = END
368
- confidence = 1.0
369
- probabilities: dict[str, float] = {target: 0.0 for target in (*AGENT_ROLES, END)}
370
- attempted = False
371
- decision_reason = "disabled"
 
 
 
372
  if self.cfg.prefetch_policy == "oracle":
373
  predicted_role = actual_next
374
  confidence = 1.0
 
375
  probabilities[predicted_role] = 1.0
376
- elif self.cfg.prefetch_policy in {"cumulative", "decayed"}:
377
- predicted_role, confidence, probabilities = self.predictor.predict(current_role)
378
- elif self.cfg.prefetch_policy == "none":
379
- # Learn-only control: the transition model still makes a prediction,
380
- # but the serving policy never acts on it. This separates the value
381
- # of prediction from the value (and cost) of prefetch.
382
- predicted_role, confidence, probabilities = self.predictor.predict(current_role)
383
-
384
- prefetch_result: dict[str, Any] = {"attempted": False, "reason": "no_prefetch", "role": predicted_role}
385
- if self.cfg.prefetch_policy != "none" and predicted_role != END:
386
- if confidence >= self.cfg.confidence_threshold or self.cfg.prefetch_policy == "oracle":
387
- prefetch_result = self._prefetch(predicted_role)
388
- attempted = bool(prefetch_result.get("attempted"))
389
- decision_reason = str(prefetch_result.get("reason", "scheduled"))
390
- else:
391
- decision_reason = "below_threshold"
392
- elif predicted_role == END:
393
- decision_reason = "predict_end"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "prefetch_attempted": attempted,
 
 
 
 
 
 
 
 
403
  "prefetch_reason": decision_reason,
404
- "prefetch_size_gb": float(prefetch_result.get("size_gb", 0.0)),
 
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
- attempted = bool(pending["prefetch_attempted"])
422
  correct = predicted_role == actual_next
423
- if attempted and correct and actual_next != END:
424
  self.prefetch_correct += 1
425
- if attempted and not correct:
426
- self.wrong_step_prefetch_bytes += float(pending.get("prefetch_size_gb", 0.0)) * 1e9
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Oracle is kept as an upper-bound candidate; it is not an online learner.
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 = self.prefetch_attempts / len(predictions) if predictions else 0.0
 
 
 
 
 
 
 
 
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": "first-order-transition-prefix-prefetch-reference",
642
  "prefetch_policy": self.cfg.prefetch_policy,
643
- "lookahead": "oracle-upper-bound" if self.cfg.prefetch_policy == "oracle" else "no-future-transition-lookahead",
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.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.8.0":
47
- errors.append(f"internal package version is {internal_version}; expected 0.8.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 execution_decay_sweep, execution_prefetch_study, execution_threshold_sweep, run_execution_learning
 
 
 
 
 
 
 
 
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.8.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 _schedule_prediction(self, workflow_id: int, step_index: int, current_role: str) -> None:
 
 
 
 
 
 
 
 
 
 
366
  actual_next = self._actual_next_role(workflow_id, step_index)
367
- predicted_role = END
368
- confidence = 1.0
369
- probabilities: dict[str, float] = {target: 0.0 for target in (*AGENT_ROLES, END)}
370
- attempted = False
371
- decision_reason = "disabled"
 
 
 
372
  if self.cfg.prefetch_policy == "oracle":
373
  predicted_role = actual_next
374
  confidence = 1.0
 
375
  probabilities[predicted_role] = 1.0
376
- elif self.cfg.prefetch_policy in {"cumulative", "decayed"}:
377
- predicted_role, confidence, probabilities = self.predictor.predict(current_role)
378
- elif self.cfg.prefetch_policy == "none":
379
- # Learn-only control: the transition model still makes a prediction,
380
- # but the serving policy never acts on it. This separates the value
381
- # of prediction from the value (and cost) of prefetch.
382
- predicted_role, confidence, probabilities = self.predictor.predict(current_role)
383
-
384
- prefetch_result: dict[str, Any] = {"attempted": False, "reason": "no_prefetch", "role": predicted_role}
385
- if self.cfg.prefetch_policy != "none" and predicted_role != END:
386
- if confidence >= self.cfg.confidence_threshold or self.cfg.prefetch_policy == "oracle":
387
- prefetch_result = self._prefetch(predicted_role)
388
- attempted = bool(prefetch_result.get("attempted"))
389
- decision_reason = str(prefetch_result.get("reason", "scheduled"))
390
- else:
391
- decision_reason = "below_threshold"
392
- elif predicted_role == END:
393
- decision_reason = "predict_end"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "prefetch_attempted": attempted,
 
 
 
 
 
 
 
 
403
  "prefetch_reason": decision_reason,
404
- "prefetch_size_gb": float(prefetch_result.get("size_gb", 0.0)),
 
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
- attempted = bool(pending["prefetch_attempted"])
422
  correct = predicted_role == actual_next
423
- if attempted and correct and actual_next != END:
424
  self.prefetch_correct += 1
425
- if attempted and not correct:
426
- self.wrong_step_prefetch_bytes += float(pending.get("prefetch_size_gb", 0.0)) * 1e9
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Oracle is kept as an upper-bound candidate; it is not an online learner.
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 = self.prefetch_attempts / len(predictions) if predictions else 0.0
 
 
 
 
 
 
 
 
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": "first-order-transition-prefix-prefetch-reference",
642
  "prefetch_policy": self.cfg.prefetch_policy,
643
- "lookahead": "oracle-upper-bound" if self.cfg.prefetch_policy == "oracle" else "no-future-transition-lookahead",
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"}