ArchitSharma commited on
Commit
4649014
·
1 Parent(s): 9916edb

Finalize InferScale-Sim

Browse files
Files changed (10) hide show
  1. README.md +160 -348
  2. app.js +75 -45
  3. docs/architecture.md +1 -1
  4. docs/design.md +47 -0
  5. docs/methodology.md +5 -5
  6. docs/research.md +3 -3
  7. docs/validation.md +1 -1
  8. index.html +0 -0
  9. scripts/release_check.py +22 -3
  10. styles.css +537 -209
README.md CHANGED
@@ -11,426 +11,238 @@ short_description: Interactive LLM serving simulator and SLO planner
11
 
12
  # InferScale-Sim
13
 
14
- **Interactive LLM serving-systems laboratory written in Python and executed entirely in the browser.**
15
 
16
- InferScale-Sim studies a systems question:
17
 
18
- > How do workload shape, batching, scheduling, KV-cache pressure, reusable prefixes, topology, and resource allocation change tail latency, goodput, and sustainable serving capacity?
19
 
20
- The public Hugging Face Space uses **no server CPU, no GPU, no API key, and no inference provider**. Hugging Face serves static files; Pyodide executes the same Python package used by the local test suite inside a Web Worker on the visitor's ordinary CPU.
21
 
22
- > [!IMPORTANT]
23
- > InferScale-Sim ships with **analytical reference latency profiles**, not measured GPU calibration data. Queueing, scheduling, cache, transfer, SLO, and design-space behavior is simulated live. Absolute L4/A10G/A100 milliseconds must not be presented as empirical hardware benchmarks.
24
 
25
- ## Why simulation?
 
 
 
 
 
26
 
27
- Exhaustive serving-system exploration is expensive. Microsoft's **Vidur** reported finding a LLaMA2-70B deployment configuration in roughly one CPU-hour while estimating that deployment-based exploration would require about **42,000 GPU-hours (~$218K)**; the paper reports inference-latency prediction error below 9% in its evaluated settings.
28
 
29
- The research direction has continued rapidly. Recent work includes GPU-free serving emulation, heterogeneous and disaggregated simulation, stateful/agentic workloads, open-loop load replay, and SLA-aware design-space exploration. InferScale-Sim is deliberately smaller: dependency-light, inspectable Python intended to expose the reasoning behind serving-system trade-offs rather than hide them behind a production runtime.
30
 
31
- ## What is simulated
32
 
33
- ### Workloads
 
 
 
 
 
 
 
 
34
 
35
- - deterministic constant, Poisson, and bursty **open-loop** arrivals
36
- - log-normal prompt/output-length distributions
37
- - exact **CSV/JSON trace replay** using supplied arrival times and token lengths
38
- - shared seeds for controlled A/B experiments
39
- - stateful multi-turn agent sessions with ordered turn dependencies and explicit tool-call gaps
40
 
41
- Generated load is open-loop: arrivals are scheduled independently of response completion so queueing delay remains visible under overload. Trace replay accepts:
 
 
 
 
42
 
43
- ```text
44
- arrival_time,prompt_tokens,output_tokens
45
- 0.000,512,64
46
- 0.137,233,41
47
- 0.284,1024,128
48
- ```
49
-
50
- ### Serving loop
51
-
52
- - static batching baseline
53
- - continuous batching with FCFS
54
- - shortest-job-first scheduling
55
- - least-slack/SLO-aware scheduling
56
- - chunked prefill
57
- - paged KV-cache accounting and VRAM admission control
58
- - controlled exact-prefix reuse
59
- - colocated or prefill/decode-disaggregated topology
60
- - independent P/D worker counts and accelerator profiles
61
- - explicit serialized KV-transfer model
62
- - FP16 / INT8 / INT4 weight-footprint scenarios
63
-
64
- ### Metrics
65
-
66
- - TTFT, TPOT, E2E, and queue-latency percentiles
67
- - request and output-token throughput
68
- - **goodput**: SLO-compliant completed requests per simulated second
69
- - TTFT/E2E SLO attainment
70
- - peak KV usage and virtual utilization
71
- - prefix-cache hit rate / saved prefill work
72
- - P/D transfer latency / transfer volume / role utilization
73
- - heuristic bottleneck diagnoses with explicit simulator provenance
74
- - cross-turn KV hit rate, recomputed history tokens, routing locality, HBM/host GB-seconds, TTL/pressure evictions, and explicit host-tier transfer latency for stateful sessions
75
- - online tool-gap prediction MAE, oracle-action agreement, and pre/post-shift adaptation diagnostics
76
- - online agent-role transition accuracy, prefix-prefetch precision/coverage, wrong-step transfer volume, prefix-cache residency, and pre/post-shift execution-learning diagnostics
77
-
78
- ## Interactive experiments
79
-
80
- ### Serving Lab
81
-
82
- Run one workload through a colocated or P/D system and inspect request-level behavior, queue/KV timelines, latency distributions, prefix reuse, and bottleneck diagnostics.
83
-
84
- ### Scheduler Arena
85
-
86
- Run every colocated scheduler against the **same deterministic workload** and compare goodput, SLO attainment, tail latency, KV pressure, and unfinished work.
87
-
88
- ### Capacity Planner
89
-
90
- Perform repeated SLO-constrained binary search for the highest sustainable offered rate. A rate passes only if **every repetition** meets the requested SLO attainment and drains all generated requests. The search exposes mean attainment, worst repetition, target, and min/max range.
91
-
92
- ### Modern Serving Lab
93
-
94
- Compare the same workload across:
95
 
96
- 1. colocated
97
- 2. colocated + prefix reuse
98
- 3. P/D disaggregated
99
- 4. P/D disaggregated + prefix reuse
 
 
 
100
 
101
- Raw goodput and goodput-per-accelerator are reported separately so extra simulated hardware is not treated as free.
102
 
103
- ### Design Explorer
 
 
 
 
 
 
 
 
104
 
105
- Run a bounded live sweep across scheduler, batch size, prefix caching, and P/D worker splits. InferScale reports two non-dominated frontiers:
106
 
107
- - **performance:** maximize goodput while minimizing p95 TTFT
108
- - **efficiency:** maximize goodput per accelerator while minimizing p95 TTFT
109
 
110
- ### Agent Sessions
111
-
112
- Model stateful multi-turn programs rather than isolated requests. Each program has ordered LLM turns separated by tool-call gaps. The simulator tracks cross-turn KV residency on multiple replicas, routing locality, TTL expiry, pressure eviction, recomputation after cache loss, and memory-time residency.
113
-
114
- Eight live experiments are included:
115
-
116
- 1. **Session run:** inspect one retention/routing policy on a generated program trace.
117
- 2. **Session Policy Arena:** replay the exact same program trace through simple stateless/retention baselines.
118
- 3. **TTL frontier:** sweep KV retention time under common random numbers and expose the non-dominated trade-off between p95 turn TTFT and mean resident KV.
119
- 4. **Agent Memory Lab:** compare HBM retention, bounded affinity, host-memory offload, and an explicitly labeled oracle gap-aware tiering upper bound.
120
- 5. **Finite-HBM budget stress:** derive per-replica KV budgets from the trace's unconstrained working set and rerun tiering policies under deliberate memory pressure.
121
- 6. **Affinity Frontier:** sweep the queue-delay slack that bounded-affinity routing is willing to pay for cache locality.
122
- 7. **Predictive Tiering Lab:** replace the clairvoyant gap-aware policy with online global/per-tool EWMA predictors that learn only from completed tool calls, then compare them on a non-stationary program trace.
123
- 8. **Adaptation-rate sweep:** vary the EWMA update rate on the exact same shifted trace to expose the stability-versus-reactivity trade-off.
124
-
125
- Agent mode intentionally uses a serial service station per replica. The existing Serving Lab models batching; Agent Sessions isolates program dependencies, cache locality, tool gaps, and state-management decisions so those effects are interpretable rather than conflated.
126
-
127
- ### Predictive tiering without future leakage
128
-
129
- The adaptive KV policy does **not** read the sampled future tool duration when it decides whether to retain state in HBM or offload it. Retained adaptive state is also bounded by the configured KV safety TTL, so a bad short-gap prediction cannot pin HBM indefinitely. Each completed tool call emits a later observation event; only then is its duration added to the online predictor. Two transparent predictors are available:
130
-
131
- - a global EWMA over all completed tools;
132
- - a per-tool EWMA that falls back to the global estimate during warm-up.
133
-
134
- The Predictive Tiering Lab deliberately changes the duration distribution of slower external tools partway through one shared trace. It compares fixed TTL, unconditional host offload, global prediction, per-tool prediction, and an explicitly labeled oracle upper bound. Prediction error and serving outcomes are shown together so a lower forecasting error is not automatically treated as a better systems policy.
135
-
136
- ### Execution Learning
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
-
167
- ### Research Studies
168
-
169
- This is the statistical/research layer rather than another configuration dashboard.
170
-
171
- **Paired Monte Carlo A/B studies** currently support:
172
-
173
- - prefix reuse off vs on
174
- - colocated vs P/D topology
175
- - continuous FCFS vs chunked prefill
176
- - continuous FCFS vs least-slack/SLO-aware scheduling
177
-
178
- Baseline and treatment use **common random numbers**: each repetition gets the same seed and therefore the same generated workload. InferScale reports paired mean deltas, relative effects, treatment win rate, and a **95% bootstrap interval over paired deltas**.
179
-
180
- **Model-uncertainty stress testing** then perturbs analytical prefill, decode, and transfer timing scales jointly for both alternatives. The purpose is not to assign a probability distribution to real GPUs; it asks a narrower scientific question:
181
-
182
- > Does the qualitative conclusion survive plausible multiplicative error in the analytical reference profile?
183
-
184
- This makes profile uncertainty visible rather than allowing one uncalibrated proxy to silently determine the winner.
185
-
186
- ### Research Summary
187
-
188
- This is the feature-freeze/consolidation layer. It turns nominal single-trace comparisons into repeated evidence and gives the analytical latency model a direct path to external falsification.
189
-
190
- **Robust policy ranking** repeats the Execution Learning policy comparison across matched workload seeds. For every deployable policy InferScale reports:
191
-
192
- - median and mean p95 step TTFT;
193
- - a 95% bootstrap interval over seed-level TTFT;
194
- - fraction of seeds on which the policy wins TTFT;
195
- - **Pareto stability**: fraction of seeds on which it is non-dominated over TTFT, unused speculative prefetch, and mean prefix-HBM residency;
196
- - worst-seed TTFT;
197
- - regret to a bounded offline serving oracle.
198
-
199
- The offline reference is deliberately constrained. On every complete trace it exhaustively evaluates the deployable candidates plus clairvoyant future-set plans for forecast horizons `H=1..5` and prefetch widths `K=1..3`, all under the same cache capacity and transfer bandwidth. The best full-trace outcome becomes the per-seed information upper bound. It is **not** presented as proof of globally optimal action scheduling.
200
-
201
- **Measurement import and held-out calibration** accepts:
202
-
203
- - current `vllm bench serve` JSON results;
204
- - SGLang `bench_serving` JSON/JSONL results;
205
- - explicit InferScale validation-case bundles.
206
-
207
- Artifacts are normalized into simulator cases. A simple robust calibration fits global prefill/decode timing multipliers on training cases only, then reports held-out residuals and MAPE. With fewer than three cases, InferScale falls back to resubstitution and marks that weaker validation mode explicitly. Calibration can correct global timing bias; it does not prove unseen-model, unseen-hardware, or scheduler fidelity.
208
-
209
- **Markdown report export** turns the robust policy study and optional held-out calibration into a portable research artifact with protocol, ranking, oracle definition, validation results, and interpretation guardrails.
210
-
211
- `reports/reference_consolidation.md` is generated from the default analytical-reference configuration across 12 matched seeds. It is a simulator reference result, **not** measured GPU evidence.
212
-
213
- The public demo ships no invented empirical benchmark truth. External measurement data must be supplied by the user or collected independently.
214
 
215
  ## Architecture
216
 
217
  ```text
218
- Hugging Face Static Space
219
- |
220
- | serves files only
221
- v
222
- +------------------------------------------------------------------+
223
- | Browser |
224
- | |
225
- | UI / Chart.js Pyodide Web Worker |
226
- | | | |
227
- | +--------------------------------->| |
228
- | v |
229
- | Python inferscale package |
230
- | | |
231
- | +------------------+---------------+------------------+ |
232
- | | | | | |
233
- | workloads schedulers KV/cache profiles |
234
- | | | | | |
235
- | +------------------+---------------+------------------+ |
236
- | | |
237
- | +-----------------------+------------------+ |
238
- | | | |
239
- | colocated simulator P/D simulator |
240
- | | |
241
- | prefill -> transfer -> decode
242
- | | | |
243
- | +-----------------------+------------------+ |
244
- | v |
245
- | metrics / search / research / agent sessions / online prediction |
246
- +------------------------------------------------------------------+
247
- ```
248
-
249
- The discrete-event engine never sleeps for simulated compute time. If the analytical model predicts a 40 ms operation, virtual time advances by 0.040 seconds immediately.
250
-
251
- ## Core modeling choices
252
-
253
- ### Goodput
254
-
255
- ```text
256
- goodput = requests satisfying TTFT and E2E SLOs / simulated makespan
257
- ```
258
-
259
- Raw throughput can reward overload; goodput penalizes requests that complete too late to satisfy the configured objective.
260
-
261
- ### KV-cache model
262
-
263
- Per-token KV bytes are approximated as:
264
-
265
- ```text
266
- 2 x layers x KV heads x head dimension x 2 bytes
267
- ```
268
-
269
- for K and V with FP16 KV state. Paged allocation rounds live request state to configurable token blocks. With prefix reuse enabled, the shared prefix is represented once as persistent KV state and request allocations contain only the uncached suffix plus generated tokens.
270
-
271
- ### P/D transfer model
272
-
273
- After prefill, newly computed prompt KV crosses a serialized reference link:
274
-
275
- ```text
276
- transfer_time = base_latency + KV_bytes / interconnect_bandwidth
277
- ```
278
-
279
- The link is intentionally simple and explicit. It does not claim to reproduce NCCL, NIXL, RDMA, PCIe, or NVLink behavior.
280
-
281
- ### Analytical latency model
282
-
283
- `AnalyticalLatencyModel` uses model architecture, accelerator peak FP16 compute, memory bandwidth, quantization footprint, batch/context shape, and conservative efficiency factors in a roofline-style proxy. Prefill and decode are modeled separately.
284
-
285
- The research stress test can multiplicatively perturb prefill/decode/transfer timing to expose conclusions that are sensitive to analytical-model error.
286
-
287
- ## Stateful agent-session model
288
-
289
- Agent-session mode follows a program-level event trace:
290
-
291
- ```text
292
- session arrival -> turn 1 -> tool gap -> turn 2 -> ... -> completion
293
- ```
294
-
295
- When a turn becomes ready, it is routed either to the least-loaded replica or to a replica already holding that session's KV state. A cache hit prefills only newly appended tokens; a miss recomputes the accumulated history. KV can be evicted immediately, retained until the session ends, retained under a TTL, or moved to a modeled host-memory tier. Host offload/restore pays an explicit bandwidth + base-latency transfer cost. A bounded-affinity router only follows cached state while the estimated queue imbalance stays under a configurable slack; this avoids treating cache locality as unconditionally preferable. Per-replica capacity uses the same analytical KV-byte model as the request simulator and falls back to LRU-style pressure eviction when the retained working set exceeds the configured budget.
296
-
297
- The public app reports **HBM GB-seconds** and **host GB-seconds** in addition to peak/mean occupancy and transfer volume. This makes both retaining and offloading state visible rather than treating cache hits or a host tier as free.
298
-
299
- This module is inspired by recent work on agentic serving, including AGENTSERVESIM's program/tool-gap/session-routing abstraction and CacheTTL's observation that retention across variable tool gaps is a latency-vs-memory decision. It is intentionally smaller and does not claim their measured-system fidelity.
300
-
301
- ### Host-tier offload and bounded affinity
302
-
303
- For `offload` retention, idle session KV is moved from the modeled HBM tier into finite host memory. The reference transfer model is explicit:
304
-
305
- ```text
306
- host_transfer = base_latency + KV_size / host_bandwidth
307
- ```
308
-
309
- If the next agent turn returns before offload has completed, the remaining transfer time becomes exposed before restore. Host residency, offloaded/restored bytes, and p95 transfer latency are reported separately from HBM residency.
310
-
311
- `bounded_affinity` is a transparent middle ground between strict session affinity and least-load routing. InferScale follows the replica holding the session KV only while its estimated extra queue penalty is below `affinity_slack_ms`. This is meant to study the locality/load-balance tension, not reproduce a particular production scheduler.
312
-
313
- `gap_aware` tiering uses the **realized simulated tool gap** to retain short-gap state in HBM and offload longer-gap state. Because a real serving system does not know the future perfectly, InferScale labels this policy as an **oracle upper bound** rather than a deployable predictor.
314
-
315
- ## Empirical validation and calibration
316
-
317
- The public project does **not** ship invented "measured" GPU results. There are two explicit paths for external measurements.
318
-
319
- For already-normalized InferScale cases:
320
-
321
- ```bash
322
- python scripts/validate_measurements.py my_measured_cases.json --output validation_report.json
323
- ```
324
-
325
- For serving-engine artifacts:
326
-
327
- ```bash
328
- python scripts/import_measurements.py vllm-result.json --source auto --output measurement_cases.json
329
- python scripts/calibrate_profiles.py measurement_cases.json --holdout 0.33 --output calibration_result.json
330
- ```
331
-
332
- The importer recognizes vLLM serving JSON fields including request throughput/goodput and percentile TTFT/E2E metrics, plus SGLang `bench_serving` JSON/JSONL fields such as `request_throughput`, `p95_ttft_ms`, and `p95_e2e_latency_ms`. Token lengths and finite request rate are taken from the artifact when present; missing model/accelerator/precision metadata falls back to an explicit base `SimulationConfig`.
333
-
334
- Calibration fits robust global prefill/decode scale factors on training cases, then evaluates held-out cases through the normal simulator. `examples/validation_cases.schema.json` is a schema/template only; its placeholder values are not benchmark data.
335
 
336
  ## Run locally
337
 
338
- The Python simulator has **zero runtime dependencies** beyond Python 3.10+.
339
-
340
  ```bash
341
- python -m venv .venv
342
  source .venv/bin/activate
 
343
  pip install -e '.[dev]'
344
 
345
  pytest -q
346
- ruff check src tests scripts
347
  python scripts/release_check.py
348
  ```
349
 
350
- Run one simulation:
351
 
352
  ```bash
353
- python scripts/run_simulation.py
354
  ```
355
 
356
- Normalize and calibrate external serving measurements:
357
 
358
- ```bash
359
- python scripts/import_measurements.py result.json --source auto
360
- python scripts/calibrate_profiles.py measurement_cases.json --holdout 0.33
361
- ```
362
 
363
- Generate a Markdown report from exported browser artifacts:
364
 
365
  ```bash
366
- python scripts/generate_report.py robust-policy-study.json --calibration calibration_result.json
367
  ```
368
 
369
- Serve the browser application:
370
 
371
  ```bash
372
- python -m http.server 8000
373
  ```
374
 
375
- Open `http://localhost:8000`. The initial page load downloads Pyodide and Chart.js; simulations then run inside the local browser worker.
376
 
377
- ## Deploy to Hugging Face
 
 
378
 
379
- Create or reuse a **Static Space** and push this repository. Root metadata already contains:
380
 
381
- ```yaml
382
- sdk: static
383
- app_file: index.html
384
  ```
385
 
386
- No Hugging Face secret is required.
387
 
388
- Before deployment:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
  ```bash
391
- python scripts/sync_web_python.py
392
  pytest -q
393
  python scripts/release_check.py
 
 
 
394
  ```
395
 
396
- `sync_web_python.py` mirrors the canonical `src/inferscale/` package into `py/inferscale/`. CI fails if the browser mirror is stale.
397
 
398
- ## Research lineage
399
 
400
- - **Vidur: A Large-Scale Simulation Framework for LLM Inference** (MLSys 2024) - predictive profiling, simulation, and deployment search. https://arxiv.org/abs/2405.05465
401
- - **SGLang: Efficient Execution of Structured Language Model Programs** (NeurIPS 2024) - RadixAttention motivates reusable-prefix experiments. https://arxiv.org/abs/2312.07104
402
- - **TokenSim** (2025) - extensible scheduling and memory-management simulation. https://arxiv.org/abs/2503.08415
403
- - **Revati** (2026) - GPU-free time-warp emulation of serving control logic. https://arxiv.org/abs/2601.00397
404
- - **LLMServingSim 2.0** (2026) - heterogeneous/disaggregated infrastructure and runtime interactions. https://arxiv.org/abs/2602.23036
405
- - **Frontier** (May 2026) - P/D and Attention-FFN disaggregation, runtime optimizations, stateful workloads, and Pareto exploration. https://arxiv.org/abs/2605.21312
406
- - **AgentServeSim** (June 2026) - hardware-aware simulation for multi-turn agent serving, tool gaps, routing, and KV residency. https://arxiv.org/abs/2606.09613
407
- - **Vanguard / Load Testing for Machine Learning Model Serving Systems at Scale** (June 2026) - trace replay, reproducibility analysis, bootstrap intervals, and open-loop generation to avoid coordinated omission. https://arxiv.org/abs/2606.22013
408
- - **When Does Disaggregation Pay? / HeteroPanacea** (August 2026) - heterogeneous stage specialization and cross-stack disaggregated design exploration. https://arxiv.org/abs/2608.03741
409
 
410
- See `docs/research.md`, `docs/methodology.md`, and `docs/validation.md` for scope and limitations.
411
 
412
- ## Repository
 
 
 
 
413
 
414
- ```text
415
- .
416
- |-- src/inferscale/ canonical Python simulator + research utilities
417
- |-- py/inferscale/ generated browser mirror
418
- |-- tests/ deterministic unit tests
419
- |-- scripts/ runner, validation, release tooling
420
- |-- examples/ workload / validation schemas
421
- |-- reports/ analytical-reference consolidation artifacts
422
- |-- docs/ architecture, methodology, research notes
423
- |-- index.html HF Static Space entry point
424
- |-- app.js UI, studies, charts, export tooling
425
- |-- worker.mjs Pyodide Web Worker bridge
426
- `-- styles.css
427
- ```
428
 
429
- ## Feature freeze and scope boundary
430
 
431
- The simulator is intentionally **feature-frozen** at this point. The project already spans request-level serving, P/D disaggregation, prefix reuse, stateful agent KV management, online execution learning, speculative prefetch planning, repeated-seed research protocols, bounded-oracle regret, and external measurement calibration. Further mechanisms would add breadth faster than they add evidence.
432
 
433
- The remaining scientific boundary is empirical fidelity: analytical reference profiles are hypotheses until compared with independent hardware measurements. The repository therefore includes import/calibration/held-out validation tooling rather than claiming production-runtime accuracy. Production integrations, CUDA-kernel fidelity, speculative decoding, richer radix-tree policies, Attention-FFN disaggregation, and multi-tenant fairness remain explicitly out of scope.
 
 
434
 
435
  ## License
436
 
 
11
 
12
  # InferScale-Sim
13
 
14
+ A browser-based research workbench for studying LLM serving systems without provisioning a GPU.
15
 
16
+ The simulator is written in Python. Hugging Face serves a static site; Pyodide runs the same `src/inferscale` package inside a Web Worker on the visitor's CPU. There is no backend service, model download, API key, or server-side accelerator.
17
 
18
+ > **Timing scope.** The bundled L4/A10G/A100 profiles are analytical references, not measured hardware benchmarks. Queueing, scheduling, KV-cache, transfer, SLO, and policy behavior is simulated live. Hardware claims require imported measurements and held-out validation.
19
 
20
+ ## Research question
21
 
22
+ InferScale asks how serving-policy choices interact with workload shape and resource constraints:
 
23
 
24
+ - when continuous batching or chunked prefill changes tail latency;
25
+ - when prefill/decode disaggregation helps enough to justify extra accelerators and KV transfer;
26
+ - when reusable prefixes improve goodput versus consume scarce cache capacity;
27
+ - how stateful agent sessions change KV-retention and routing decisions;
28
+ - whether online prediction improves serving outcomes, not merely prediction accuracy;
29
+ - which conclusions remain stable across matched workload seeds and timing uncertainty.
30
 
31
+ The motivation is practical. Microsoft's [Vidur](https://arxiv.org/abs/2405.05465) reported an example LLaMA2-70B configuration search that took roughly one CPU-hour through simulation versus an estimated 42,000 GPU-hours (~$218K) using deployment-based exploration.
32
 
33
+ ## What is implemented
34
 
35
+ ### Stateless serving
36
 
37
+ - open-loop constant, Poisson, bursty, and exact trace-replay workloads;
38
+ - static and continuous batching;
39
+ - FCFS, shortest-job-first, least-slack/SLO-aware scheduling;
40
+ - chunked prefill;
41
+ - paged KV-cache accounting and memory admission;
42
+ - controlled shared-prefix reuse;
43
+ - colocated and prefill/decode-disaggregated topologies;
44
+ - independent P/D worker counts, accelerator profiles, and serialized KV-transfer cost;
45
+ - TTFT, TPOT, E2E, queueing, throughput, goodput, SLO attainment, and KV telemetry.
46
 
47
+ ### Stateful sessions
 
 
 
 
48
 
49
+ - multi-turn session dependencies separated by sampled tool gaps;
50
+ - HBM retention, TTL expiry, host-memory offload/restore, and pressure eviction;
51
+ - least-load, strict-affinity, and bounded-affinity routing;
52
+ - cross-turn cache hits, history recomputation, HBM/host GB-seconds, and session SLOs;
53
+ - online global/per-tool EWMA tool-gap prediction under a controlled distribution shift.
54
 
55
+ ### Execution learning
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ - online first-order role-transition learning with no future-trace lookahead;
58
+ - cumulative and decayed transition models;
59
+ - confidence-gated top-1 prefetch;
60
+ - multi-step transition rollout and top-k prefetch;
61
+ - utility-aware prefetch using transfer and forecast-weighted eviction costs;
62
+ - Brier score, log loss, ECE, reliability diagrams, future-role recall@K, prefetch utilization, and speculative-transfer waste;
63
+ - confidence, forgetting-rate, forecast-horizon, and cache-budget studies.
64
 
65
+ ### Research consolidation
66
 
67
+ - common-random-number A/B experiments;
68
+ - paired bootstrap intervals;
69
+ - timing-model sensitivity analysis;
70
+ - repeated-seed policy ranking;
71
+ - Pareto stability across latency, HBM residency, and unused speculative transfer;
72
+ - a bounded full-trace serving oracle over a declared policy/action-plan family;
73
+ - per-policy regret to that bounded reference;
74
+ - vLLM/SGLang-style measurement import, calibration, and held-out validation hooks;
75
+ - Markdown/JSON/CSV/PNG research-artifact export.
76
 
77
+ ## Reference result
78
 
79
+ The repository includes a deterministic 12-seed analytical-reference consolidation study in [`reports/reference_consolidation.md`](reports/reference_consolidation.md).
 
80
 
81
+ | Policy | Median p95 TTFT | TTFT wins | Pareto stability | Median oracle regret |
82
+ |---|---:|---:|---:|---:|
83
+ | Top-1 decayed | 6,474.9 ms | 41.7% | 75.0% | 59.3 ms |
84
+ | No prefetch | 6,490.7 ms | 41.7% | **91.7%** | **34.7 ms** |
85
+ | Utility-aware multi-step | 6,507.5 ms | 8.3% | 58.3% | 133.1 ms |
86
+ | Multi-step top-k | 6,640.3 ms | 8.3% | 83.3% | 215.0 ms |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ `Top-1 decayed` is the robust TTFT winner under the configured ranking protocol, while `No prefetch` is Pareto-stable on more seeds and has lower median regret to the bounded oracle. The point is not to collapse the study to one score: prediction quality, latency, speculative transfer, and HBM pressure can prefer different policies.
89
 
90
+ The bounded oracle searches 19 candidate plans per seed and has median p95 TTFT **6,408.6 ms** in this reference study. It is an information upper bound over that declared family, not a proof of globally optimal cache scheduling.
91
 
92
+ ## Interface map
 
 
 
 
 
93
 
94
+ The public Space is organized as a research tool rather than a product dashboard:
95
 
96
+ | Section | Purpose |
97
+ |---|---|
98
+ | **Serving** | Inspect one workload and request-level behavior. |
99
+ | **Schedulers** | Compare colocated schedulers on an identical trace. |
100
+ | **Capacity** | Find the highest repeated SLO-compliant offered load. |
101
+ | **P/D + Cache** | Compare colocated/P-D serving with and without prefix reuse. |
102
+ | **Design Space** | Sweep bounded configurations and inspect performance/efficiency Pareto fronts. |
103
+ | **A/B Studies** | Run paired bootstrap and timing-sensitivity experiments. |
104
+ | **Stateful Sessions** | Study KV retention, host offload, affinity, and tool-gap prediction. |
105
+ | **Execution Model** | Study transition learning, multi-step prefetch, calibration, and cache pressure. |
106
+ | **Evidence** | Run repeated-seed consolidation and import external measurements. |
107
+ | **Methods** | Read the simulator assumptions and research lineage. |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  ## Architecture
110
 
111
  ```text
112
+ Hugging Face Static Space
113
+ |
114
+ | serves HTML / CSS / JS / Python sources
115
+ v
116
+ Browser
117
+ |
118
+ +-- UI + Chart.js
119
+ |
120
+ +-- Web Worker
121
+ |
122
+ +-- Pyodide
123
+ |
124
+ +-- src/inferscale
125
+ |
126
+ +-- discrete-event simulation
127
+ +-- research protocols
128
+ +-- calibration / reporting
129
+ ```
130
+
131
+ The browser mirror under `py/inferscale/` is generated from `src/inferscale/`; `scripts/release_check.py` fails if the copies diverge.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  ## Run locally
134
 
 
 
135
  ```bash
136
+ python3 -m venv .venv
137
  source .venv/bin/activate
138
+ python -m pip install -U pip
139
  pip install -e '.[dev]'
140
 
141
  pytest -q
 
142
  python scripts/release_check.py
143
  ```
144
 
145
+ For the static UI, serve the repository root with any local HTTP server:
146
 
147
  ```bash
148
+ python -m http.server 8000
149
  ```
150
 
151
+ The deployed Space needs no secrets.
152
 
153
+ ## Command-line examples
 
 
 
154
 
155
+ Run a single simulation:
156
 
157
  ```bash
158
+ python scripts/run_simulation.py examples/balanced.json
159
  ```
160
 
161
+ Import a serving-benchmark artifact:
162
 
163
  ```bash
164
+ python scripts/import_measurements.py path/to/benchmark.json --format auto
165
  ```
166
 
167
+ Calibrate analytical timing scales and evaluate held-out cases:
168
 
169
+ ```bash
170
+ python scripts/calibrate_profiles.py measurements.json --holdout 0.25
171
+ ```
172
 
173
+ Generate a research report:
174
 
175
+ ```bash
176
+ python scripts/generate_report.py robust-policy-study.json \
177
+ --calibration calibration_result.json
178
  ```
179
 
180
+ ## Repository layout
181
 
182
+ ```text
183
+ src/inferscale/ canonical Python simulator and research code
184
+ py/inferscale/ browser mirror loaded by Pyodide
185
+ index.html static workbench structure
186
+ styles.css project-specific design system
187
+ app.js UI, charts, exports, worker orchestration
188
+ worker.mjs Pyodide bootstrap and Python action bridge
189
+ scripts/ release, import, calibration, report utilities
190
+ tests/ deterministic unit/integration coverage
191
+ docs/ architecture, methodology, validation, research notes
192
+ examples/ workload and validation schemas
193
+ reports/ reference analytical study
194
+ ```
195
+
196
+ ## Validation
197
+
198
+ The release suite checks both simulation behavior and deployment invariants, including:
199
+
200
+ - stateless and P/D smoke tests;
201
+ - trace replay;
202
+ - prefix reuse and design-space Pareto logic;
203
+ - stateful-session, host-tier, HBM-pressure, and affinity experiments;
204
+ - predictive tiering and execution-learning studies;
205
+ - paired bootstrap, timing sensitivity, repeated-seed consolidation, oracle regret;
206
+ - measurement import/calibration/report generation;
207
+ - source/browser Python parity;
208
+ - HF metadata and public provenance guardrails;
209
+ - DOM-reference integrity and chart-export controls.
210
+
211
+ Run:
212
 
213
  ```bash
 
214
  pytest -q
215
  python scripts/release_check.py
216
+ python -m compileall -q src scripts
217
+ node --check app.js
218
+ node --check worker.mjs
219
  ```
220
 
221
+ ## Design and implementation choices
222
 
223
+ The interface deliberately avoids a product/SaaS visual language. It uses one restrained accent, flat surfaces, square geometry, system typography, and editorial hierarchy instead of gradients, floating cards, badge-heavy status UI, or decorative marketing sections. The rationale and reusable tokens are documented in [`docs/design.md`](docs/design.md).
224
 
225
+ The simulator core is dependency-light Python rather than a simulation framework. Events, requests, schedulers, caches, predictors, and studies remain inspectable from source and testable outside the browser.
 
 
 
 
 
 
 
 
226
 
227
+ ## Limitations
228
 
229
+ 1. Bundled device timings are analytical reference profiles.
230
+ 2. The agent-session simulator intentionally isolates state/routing effects from the full dynamic-batching model used in stateless serving.
231
+ 3. Transfer models are simplified serialized bandwidth + base-latency abstractions, not packet/NCCL/NIXL simulators.
232
+ 4. Oracle policies are bounded information references over declared action families.
233
+ 5. Calibration can correct global timing bias; it does not establish fidelity on unseen models, hardware, schedulers, or workload regimes.
234
 
235
+ These limitations are surfaced in the UI and exported reports rather than hidden.
236
+
237
+ ## Research lineage
 
 
 
 
 
 
 
 
 
 
 
238
 
239
+ InferScale is informed by work on serving simulation, disaggregation, stateful agent workloads, and predictive cache management. See [`docs/research.md`](docs/research.md) for the annotated list and the precise distinction between implemented abstractions and cited systems.
240
 
241
+ Key starting points include:
242
 
243
+ - [Vidur](https://arxiv.org/abs/2405.05465) predictive profiling and workload-aware serving simulation.
244
+ - [SGLang / RadixAttention](https://arxiv.org/abs/2312.07104) — structured prefix reuse and scheduling motivation.
245
+ - Recent 2026 work discussed in `docs/research.md` on P/D disaggregation, GPU-free emulation, agent-session serving, KV tiering, routing locality, and predictive prefetch.
246
 
247
  ## License
248
 
app.js CHANGED
@@ -32,14 +32,14 @@ let measurementFileContent = "";
32
  let traceRequests = [];
33
 
34
  const COLORS = {
35
- blue: "#79a7ff",
36
- blue2: "#5d8fe9",
37
- steel: "#92a2b5",
38
- green: "#69c99a",
39
- amber: "#dfb966",
40
- red: "#df7d89",
41
- purple: "#9b8cff",
42
- gray: "#667384",
43
  grid: "rgba(140,155,175,.14)",
44
  };
45
 
@@ -135,12 +135,19 @@ function stamp() {
135
  const pad = (v) => String(v).padStart(2, "0");
136
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
137
  }
138
- function showToast(message) {
139
  const toast = $("toast");
140
  toast.textContent = message;
 
141
  toast.classList.add("show");
142
  if (toastTimer) clearTimeout(toastTimer);
143
- toastTimer = setTimeout(() => toast.classList.remove("show"), 1800);
 
 
 
 
 
 
144
  }
145
  async function copyText(text, message) {
146
  try {
@@ -210,7 +217,7 @@ Chart.defaults.color = "#929dab";
210
  Chart.defaults.borderColor = COLORS.grid;
211
  Chart.defaults.font.family = getComputedStyle(document.body).fontFamily;
212
  Chart.defaults.font.size = 13;
213
- Chart.defaults.animation.duration = 160;
214
 
215
  function commonChartOptions() {
216
  return { responsive: true, maintainAspectRatio: false };
@@ -394,7 +401,7 @@ $("runBtn").addEventListener("click", async () => {
394
  } catch (error) {
395
  state.textContent = "Error";
396
  state.className = "tag bad";
397
- alert(`Simulation failed: ${error.message}`);
398
  } finally {
399
  button.textContent = "Run simulation";
400
  button.disabled = false;
@@ -435,7 +442,7 @@ $("arenaBtn").addEventListener("click", async () => {
435
  button.disabled = true;
436
  button.textContent = "Comparing...";
437
  try { renderArena((await callPython("compare", { config: configFromUI() })).rows); }
438
- catch (error) { alert(`Scheduler comparison failed: ${error.message}`); }
439
  finally { button.disabled = false; button.textContent = "Compare schedulers"; }
440
  });
441
  const arenaHeaders = ["Scheduler", "Goodput", "SLO attainment", "p95 TTFT", "p95 E2E", "KV peak", "Unfinished", "Diagnosis"];
@@ -509,7 +516,7 @@ $("capacityBtn").addEventListener("click", async () => {
509
  } catch (error) {
510
  state.textContent = "Error";
511
  state.className = "tag bad";
512
- alert(`Capacity search failed: ${error.message}`);
513
  } finally {
514
  button.disabled = false;
515
  button.textContent = "Find sustainable capacity";
@@ -563,7 +570,7 @@ $("topologyBtn").addEventListener("click", async () => {
563
  button.disabled = true;
564
  button.textContent = "Comparing...";
565
  try { renderTopology((await callPython("topology_compare", { config: configFromUI() })).rows); }
566
- catch (error) { alert(`Topology comparison failed: ${error.message}`); }
567
  finally { button.disabled = false; button.textContent = "Compare 4 scenarios"; }
568
  });
569
  const topologyHeaders = ["Scenario", "GPU instances", "Goodput", "Goodput / GPU", "SLO attainment", "p95 TTFT", "p95 E2E", "p95 KV transfer", "Cache hit", "Prefill saved", "Diagnosis"];
@@ -629,7 +636,7 @@ $("designBtn").addEventListener("click", async () => {
629
  button.disabled = true;
630
  button.textContent = "Exploring...";
631
  try { renderDesign(await callPython("design_space", { config: configFromUI(), include_disaggregated: $("includePd").checked })); }
632
- catch (error) { alert(`Design-space exploration failed: ${error.message}`); }
633
  finally { button.disabled = false; button.textContent = "Explore design space"; }
634
  });
635
  const designHeaders = ["Candidate", "Perf Pareto", "Efficiency Pareto", "SLO pass", "GPU instances", "Goodput", "Goodput / GPU", "p95 TTFT", "p95 E2E", "KV peak", "Diagnosis"];
@@ -712,7 +719,7 @@ $("pairedStudyBtn").addEventListener("click", async () => {
712
  } catch (error) {
713
  $("pairedState").textContent = "Error";
714
  $("pairedState").className = "tag bad";
715
- alert(`Paired study failed: ${error.message}`);
716
  } finally {
717
  button.disabled = false;
718
  button.textContent = "Run paired study";
@@ -765,7 +772,7 @@ $("robustStudyBtn").addEventListener("click", async () => {
765
  } catch (error) {
766
  $("robustState").textContent = "Error";
767
  $("robustState").className = "tag bad";
768
- alert(`Robustness study failed: ${error.message}`);
769
  } finally {
770
  button.disabled = false;
771
  button.textContent = "Stress-test selected hypothesis";
@@ -863,7 +870,7 @@ $("agentRunBtn").addEventListener("click", async () => {
863
  $("agentRunState").textContent = "Running...";
864
  $("agentRunState").className = "tag neutral";
865
  try { renderAgentRun(await callPython("agent_simulate", { config: agentConfigFromUI() })); }
866
- catch (error) { $("agentRunState").textContent = "Error"; $("agentRunState").className = "tag bad"; alert(`Agent session simulation failed: ${error.message}`); }
867
  finally { button.disabled = false; button.textContent = "Run stateful session simulation"; }
868
  });
869
  $("agentRunCopyJson").addEventListener("click", () => { if (lastAgentRun) copyText(JSON.stringify(lastAgentRun, null, 2), "Agent-session JSON copied"); });
@@ -889,7 +896,7 @@ function renderAgentCompare(result) {
889
  $("agentCompareBtn").addEventListener("click", async () => {
890
  const button = $("agentCompareBtn"); button.disabled = true; button.textContent = "Comparing...";
891
  try { renderAgentCompare(await callPython("agent_compare", { config: agentConfigFromUI() })); }
892
- catch (error) { alert(`Agent policy comparison failed: ${error.message}`); }
893
  finally { button.disabled = false; button.textContent = "Compare 4 policies"; }
894
  });
895
  $("agentCompareCopy").addEventListener("click", () => { if (lastAgentCompare) copyText(tableText(agentCompareHeaders, agentCompareTableRows(lastAgentCompare)), "Agent policy table copied"); });
@@ -919,7 +926,7 @@ function renderAgentTtl(result) {
919
  $("agentTtlBtn").addEventListener("click", async () => {
920
  const button = $("agentTtlBtn"); button.disabled = true; button.textContent = "Sweeping TTL...";
921
  try { renderAgentTtl(await callPython("agent_ttl_sweep", { config: agentConfigFromUI() })); }
922
- catch (error) { alert(`TTL sweep failed: ${error.message}`); }
923
  finally { button.disabled = false; button.textContent = "Run TTL sweep"; }
924
  });
925
  $("agentTtlCopy").addEventListener("click", () => { if (lastAgentTtl) copyText(tableText(agentTtlHeaders, agentTtlTableRows(lastAgentTtl)), "TTL sweep copied"); });
@@ -948,7 +955,7 @@ function renderAgentMemory(result) {
948
  $("agentMemoryCompareBtn").addEventListener("click", async () => {
949
  const button = $("agentMemoryCompareBtn"); button.disabled = true; button.textContent = "Comparing policies...";
950
  try { renderAgentMemory(await callPython("agent_memory_compare", { config: agentConfigFromUI() })); }
951
- catch (error) { alert(`Memory policy comparison failed: ${error.message}`); }
952
  finally { button.disabled = false; button.textContent = "Compare memory policies"; }
953
  });
954
  $("agentMemoryCopy").addEventListener("click", () => { if (lastAgentMemory) copyText(tableText(agentMemoryHeaders, agentMemoryTableRows(lastAgentMemory)), "Memory-policy table copied"); });
@@ -978,7 +985,7 @@ function renderAgentBudget(result) {
978
  $("agentBudgetBtn").addEventListener("click", async () => {
979
  const button = $("agentBudgetBtn"); button.disabled = true; button.textContent = "Stressing HBM budget...";
980
  try { renderAgentBudget(await callPython("agent_memory_sweep", { config: agentConfigFromUI() })); }
981
- catch (error) { alert(`HBM budget study failed: ${error.message}`); }
982
  finally { button.disabled = false; button.textContent = "Stress HBM budget"; }
983
  });
984
  $("agentBudgetCopy").addEventListener("click", () => { if (lastAgentBudget) copyText(tableText(agentBudgetHeaders, agentBudgetTableRows(lastAgentBudget)), "HBM budget table copied"); });
@@ -1007,7 +1014,7 @@ function renderAgentAffinity(result) {
1007
  $("agentAffinityBtn").addEventListener("click", async () => {
1008
  const button = $("agentAffinityBtn"); button.disabled = true; button.textContent = "Sweeping affinity...";
1009
  try { renderAgentAffinity(await callPython("agent_affinity_sweep", { config: agentConfigFromUI() })); }
1010
- catch (error) { alert(`Affinity sweep failed: ${error.message}`); }
1011
  finally { button.disabled = false; button.textContent = "Run affinity sweep"; }
1012
  });
1013
  $("agentAffinityCopy").addEventListener("click", () => { if (lastAgentAffinity) copyText(tableText(agentAffinityHeaders, agentAffinityTableRows(lastAgentAffinity)), "Affinity sweep copied"); });
@@ -1081,7 +1088,7 @@ function predictivePayload() {
1081
  $("predictiveCompareBtn").addEventListener("click", async () => {
1082
  const button = $("predictiveCompareBtn"); button.disabled = true; button.textContent = "Running predictive study...";
1083
  try { renderPredictiveStudy(await callPython("agent_predictive_tiering", predictivePayload())); }
1084
- catch (error) { alert(`Predictive tiering study failed: ${error.message}`); }
1085
  finally { button.disabled = false; button.textContent = "Compare predictive policies"; }
1086
  });
1087
  $("predictiveCopy").addEventListener("click", () => { if (lastPredictiveStudy) copyText(tableText(predictiveHeaders, predictiveTableRows(lastPredictiveStudy)), "Predictive-tiering table copied"); });
@@ -1114,7 +1121,7 @@ $("predictiveAlphaBtn").addEventListener("click", async () => {
1114
  const button = $("predictiveAlphaBtn"); button.disabled = true; button.textContent = "Sweeping adaptation rate...";
1115
  const payload = predictivePayload();
1116
  try { renderPredictiveAlpha(await callPython("agent_adaptive_alpha_sweep", payload)); }
1117
- catch (error) { alert(`Adaptation-rate sweep failed: ${error.message}`); }
1118
  finally { button.disabled = false; button.textContent = "Sweep adaptation rate"; }
1119
  });
1120
  $("predictiveAlphaCopy").addEventListener("click", () => { if (lastPredictiveAlpha) copyText(tableText(predictiveAlphaHeaders, predictiveAlphaTableRows(lastPredictiveAlpha)), "Adaptation-rate table copied"); });
@@ -1225,7 +1232,7 @@ $("execRunBtn").addEventListener("click", async () => {
1225
  button.disabled = true; button.textContent = "Running execution model...";
1226
  $("execRunState").textContent = "Running..."; $("execRunState").className = "tag neutral";
1227
  try { renderExecutionRun(await callPython("execution_learning_run", { config: executionConfigFromUI() })); }
1228
- catch (error) { $("execRunState").textContent = "Error"; $("execRunState").className = "tag bad"; alert(`Execution-learning run failed: ${error.message}`); }
1229
  finally { button.disabled = false; button.textContent = "Run execution-learning simulation"; }
1230
  });
1231
  $("execRunCopyJson").addEventListener("click", () => { if (lastExecutionRun) copyText(JSON.stringify(lastExecutionRun, null, 2), "Execution-learning JSON copied"); });
@@ -1251,7 +1258,7 @@ function renderExecutionCompare(result) {
1251
  $("execCompareBtn").addEventListener("click", async () => {
1252
  const button = $("execCompareBtn"); button.disabled = true; button.textContent = "Comparing policies...";
1253
  try { renderExecutionCompare(await callPython("execution_prefetch_study", { config: executionConfigFromUI() })); }
1254
- catch (error) { alert(`Execution prefetch study failed: ${error.message}`); }
1255
  finally { button.disabled = false; button.textContent = "Compare 4 policies"; }
1256
  });
1257
  $("execCompareCopy").addEventListener("click", () => { if (lastExecutionCompare) copyText(tableText(execCompareHeaders, execCompareTableRows(lastExecutionCompare)), "Execution policy table copied"); });
@@ -1286,7 +1293,7 @@ function renderExecutionThreshold(result) {
1286
  $("execThresholdBtn").addEventListener("click", async () => {
1287
  const button = $("execThresholdBtn"); button.disabled = true; button.textContent = "Sweeping threshold...";
1288
  try { renderExecutionThreshold(await callPython("execution_threshold_sweep", { config: executionConfigFromUI() })); }
1289
- catch (error) { alert(`Execution threshold sweep failed: ${error.message}`); }
1290
  finally { button.disabled = false; button.textContent = "Sweep confidence threshold"; }
1291
  });
1292
  $("execThresholdCopy").addEventListener("click", () => { if (lastExecutionThreshold) copyText(tableText(execThresholdHeaders, execThresholdTableRows(lastExecutionThreshold)), "Execution threshold table copied"); });
@@ -1319,7 +1326,7 @@ function renderExecutionDecay(result) {
1319
  $("execDecayBtn").addEventListener("click", async () => {
1320
  const button = $("execDecayBtn"); button.disabled = true; button.textContent = "Sweeping decay...";
1321
  try { renderExecutionDecay(await callPython("execution_decay_sweep", { config: executionConfigFromUI() })); }
1322
- catch (error) { alert(`Execution decay sweep failed: ${error.message}`); }
1323
  finally { button.disabled = false; button.textContent = "Sweep transition decay"; }
1324
  });
1325
  $("execDecayCopy").addEventListener("click", () => { if (lastExecutionDecay) copyText(tableText(execDecayHeaders, execDecayTableRows(lastExecutionDecay)), "Execution decay table copied"); });
@@ -1355,7 +1362,7 @@ function renderExecutionPlanning(result) {
1355
  $("execPlanningBtn").addEventListener("click", async () => {
1356
  const button = $("execPlanningBtn"); button.disabled = true; button.textContent = "Comparing planners...";
1357
  try { renderExecutionPlanning(await callPython("execution_planning_study", { config: executionConfigFromUI() })); }
1358
- catch (error) { alert(`Execution planning study failed: ${error.message}`); }
1359
  finally { button.disabled = false; button.textContent = "Compare planning policies"; }
1360
  });
1361
  $("execPlanningCopy").addEventListener("click", () => { if (lastExecutionPlanning) copyText(tableText(execPlanningHeaders, execPlanningTableRows(lastExecutionPlanning)), "Planning table copied"); });
@@ -1389,7 +1396,7 @@ function renderExecutionHorizon(result) {
1389
  $("execHorizonBtn").addEventListener("click", async () => {
1390
  const button = $("execHorizonBtn"); button.disabled = true; button.textContent = "Sweeping horizons...";
1391
  try { renderExecutionHorizon(await callPython("execution_horizon_sweep", { config: executionConfigFromUI() })); }
1392
- catch (error) { alert(`Forecast horizon sweep failed: ${error.message}`); }
1393
  finally { button.disabled = false; button.textContent = "Sweep forecast horizon"; }
1394
  });
1395
  $("execHorizonCopy").addEventListener("click", () => { if (lastExecutionHorizon) copyText(tableText(execHorizonHeaders, execHorizonTableRows(lastExecutionHorizon)), "Horizon table copied"); });
@@ -1417,7 +1424,7 @@ function renderExecutionBudget(result) {
1417
  $("execBudgetBtn").addEventListener("click", async () => {
1418
  const button = $("execBudgetBtn"); button.disabled = true; button.textContent = "Sweeping cache budget...";
1419
  try { renderExecutionBudget(await callPython("execution_budget_sweep", { config: executionConfigFromUI() })); }
1420
- catch (error) { alert(`Cache budget study failed: ${error.message}`); }
1421
  finally { button.disabled = false; button.textContent = "Sweep cache budget"; }
1422
  });
1423
  $("execBudgetCopy").addEventListener("click", () => { if (lastExecutionBudget) copyText(tableText(execBudgetHeaders, execBudgetTableRows(lastExecutionBudget)), "Cache budget table copied"); });
@@ -1470,7 +1477,7 @@ $("consRunBtn").addEventListener("click", async () => {
1470
  renderConsolidation(await callPython("consolidation_study", {
1471
  config: executionConfigFromUI(), repetitions: num("consSeeds"), bootstrap_samples: num("consBootstrap"),
1472
  }));
1473
- } catch (error) { alert(`Robust policy study failed: ${error.message}`); }
1474
  finally { button.disabled = false; button.textContent = "Run robust policy study"; }
1475
  });
1476
  $("consCopy").addEventListener("click", () => { if (lastConsolidation) copyText(tableText(consHeaders, consTableRows(lastConsolidation)), "Robust policy table copied"); });
@@ -1486,7 +1493,7 @@ $("measurementFile").addEventListener("change", async (event) => {
1486
  $("measurementCalibrateBtn").disabled = !runtimePill.classList.contains("ready");
1487
  } catch (error) {
1488
  measurementFileContent = ""; $("measurementCalibrateBtn").disabled = true;
1489
- $("measurementStatus").textContent = "File read failed"; alert(`Measurement file failed: ${error.message}`);
1490
  }
1491
  });
1492
 
@@ -1529,7 +1536,7 @@ $("measurementCalibrateBtn").addEventListener("click", async () => {
1529
  cases: imported.cases, holdout_fraction: num("measurementHoldout"), seed: num("seed"),
1530
  });
1531
  renderCalibration(imported, calibrated);
1532
- } catch (error) { alert(`Measurement calibration failed: ${error.message}`); }
1533
  finally { button.disabled = false; button.textContent = "Import and calibrate"; }
1534
  });
1535
  $("measurementCopy").addEventListener("click", () => { if (lastCalibration) copyText(tableText(measurementHeaders, measurementTableRows(lastCalibration)), "Validation table copied"); });
@@ -1541,7 +1548,7 @@ $("consReportBtn").addEventListener("click", async () => {
1541
  try {
1542
  const result = await callPython("research_report", { robust: lastConsolidation, calibration: lastCalibration });
1543
  downloadTextFile(`inferscale_research-consolidation_${stamp()}.md`, result.markdown, "text/markdown;charset=utf-8");
1544
- } catch (error) { alert(`Report generation failed: ${error.message}`); }
1545
  finally { button.disabled = false; button.textContent = "Download Markdown report"; }
1546
  });
1547
 
@@ -1612,7 +1619,7 @@ $("traceFile").addEventListener("change", async (event) => {
1612
  const file = event.target.files?.[0];
1613
  if (!file) return;
1614
  try { await loadTraceFile(file); }
1615
- catch (error) { traceRequests = []; $("traceStatus").textContent = "Trace rejected"; alert(`Trace load failed: ${error.message}`); }
1616
  });
1617
  $("traceClearBtn").addEventListener("click", () => {
1618
  traceRequests = [];
@@ -1641,13 +1648,36 @@ $("prefixCache").addEventListener("change", syncConditionalControls);
1641
  $("arrival").addEventListener("change", syncConditionalControls);
1642
  syncConditionalControls();
1643
 
1644
- for (const tab of document.querySelectorAll(".tab")) {
1645
- tab.addEventListener("click", () => {
1646
- document.querySelectorAll(".tab").forEach((item) => item.classList.remove("active"));
1647
- document.querySelectorAll(".tab-panel").forEach((panel) => panel.classList.remove("active"));
1648
- tab.classList.add("active");
1649
- $(tab.dataset.tab).classList.add("active");
1650
- setTimeout(() => Object.values(charts).forEach((chart) => chart.resize()), 20);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1651
  });
1652
  }
1653
 
 
32
  let traceRequests = [];
33
 
34
  const COLORS = {
35
+ blue: "#78a1db",
36
+ blue2: "#5f86bf",
37
+ steel: "#91a0b2",
38
+ green: "#69c39a",
39
+ amber: "#d2b36a",
40
+ red: "#d97f89",
41
+ purple: "#9288c7",
42
+ gray: "#687585",
43
  grid: "rgba(140,155,175,.14)",
44
  };
45
 
 
135
  const pad = (v) => String(v).padStart(2, "0");
136
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
137
  }
138
+ function showToast(message, kind = "info", timeoutMs = 2200) {
139
  const toast = $("toast");
140
  toast.textContent = message;
141
+ toast.classList.toggle("error", kind === "error");
142
  toast.classList.add("show");
143
  if (toastTimer) clearTimeout(toastTimer);
144
+ toastTimer = setTimeout(() => {
145
+ toast.classList.remove("show", "error");
146
+ }, timeoutMs);
147
+ }
148
+ function reportError(context, error) {
149
+ console.error(context, error);
150
+ showToast(`${context}: ${error.message}`, "error", 5200);
151
  }
152
  async function copyText(text, message) {
153
  try {
 
217
  Chart.defaults.borderColor = COLORS.grid;
218
  Chart.defaults.font.family = getComputedStyle(document.body).fontFamily;
219
  Chart.defaults.font.size = 13;
220
+ Chart.defaults.animation.duration = 0;
221
 
222
  function commonChartOptions() {
223
  return { responsive: true, maintainAspectRatio: false };
 
401
  } catch (error) {
402
  state.textContent = "Error";
403
  state.className = "tag bad";
404
+ reportError("Simulation failed", error);
405
  } finally {
406
  button.textContent = "Run simulation";
407
  button.disabled = false;
 
442
  button.disabled = true;
443
  button.textContent = "Comparing...";
444
  try { renderArena((await callPython("compare", { config: configFromUI() })).rows); }
445
+ catch (error) { reportError("Scheduler comparison failed", error); }
446
  finally { button.disabled = false; button.textContent = "Compare schedulers"; }
447
  });
448
  const arenaHeaders = ["Scheduler", "Goodput", "SLO attainment", "p95 TTFT", "p95 E2E", "KV peak", "Unfinished", "Diagnosis"];
 
516
  } catch (error) {
517
  state.textContent = "Error";
518
  state.className = "tag bad";
519
+ reportError("Capacity search failed", error);
520
  } finally {
521
  button.disabled = false;
522
  button.textContent = "Find sustainable capacity";
 
570
  button.disabled = true;
571
  button.textContent = "Comparing...";
572
  try { renderTopology((await callPython("topology_compare", { config: configFromUI() })).rows); }
573
+ catch (error) { reportError("Topology comparison failed", error); }
574
  finally { button.disabled = false; button.textContent = "Compare 4 scenarios"; }
575
  });
576
  const topologyHeaders = ["Scenario", "GPU instances", "Goodput", "Goodput / GPU", "SLO attainment", "p95 TTFT", "p95 E2E", "p95 KV transfer", "Cache hit", "Prefill saved", "Diagnosis"];
 
636
  button.disabled = true;
637
  button.textContent = "Exploring...";
638
  try { renderDesign(await callPython("design_space", { config: configFromUI(), include_disaggregated: $("includePd").checked })); }
639
+ catch (error) { reportError("Design-space sweep failed", error); }
640
  finally { button.disabled = false; button.textContent = "Explore design space"; }
641
  });
642
  const designHeaders = ["Candidate", "Perf Pareto", "Efficiency Pareto", "SLO pass", "GPU instances", "Goodput", "Goodput / GPU", "p95 TTFT", "p95 E2E", "KV peak", "Diagnosis"];
 
719
  } catch (error) {
720
  $("pairedState").textContent = "Error";
721
  $("pairedState").className = "tag bad";
722
+ reportError("Paired study failed", error);
723
  } finally {
724
  button.disabled = false;
725
  button.textContent = "Run paired study";
 
772
  } catch (error) {
773
  $("robustState").textContent = "Error";
774
  $("robustState").className = "tag bad";
775
+ reportError("Sensitivity study failed", error);
776
  } finally {
777
  button.disabled = false;
778
  button.textContent = "Stress-test selected hypothesis";
 
870
  $("agentRunState").textContent = "Running...";
871
  $("agentRunState").className = "tag neutral";
872
  try { renderAgentRun(await callPython("agent_simulate", { config: agentConfigFromUI() })); }
873
+ catch (error) { $("agentRunState").textContent = "Error"; $("agentRunState").className = "tag bad"; reportError("Agent session simulation failed", error); }
874
  finally { button.disabled = false; button.textContent = "Run stateful session simulation"; }
875
  });
876
  $("agentRunCopyJson").addEventListener("click", () => { if (lastAgentRun) copyText(JSON.stringify(lastAgentRun, null, 2), "Agent-session JSON copied"); });
 
896
  $("agentCompareBtn").addEventListener("click", async () => {
897
  const button = $("agentCompareBtn"); button.disabled = true; button.textContent = "Comparing...";
898
  try { renderAgentCompare(await callPython("agent_compare", { config: agentConfigFromUI() })); }
899
+ catch (error) { reportError("Agent policy comparison failed", error); }
900
  finally { button.disabled = false; button.textContent = "Compare 4 policies"; }
901
  });
902
  $("agentCompareCopy").addEventListener("click", () => { if (lastAgentCompare) copyText(tableText(agentCompareHeaders, agentCompareTableRows(lastAgentCompare)), "Agent policy table copied"); });
 
926
  $("agentTtlBtn").addEventListener("click", async () => {
927
  const button = $("agentTtlBtn"); button.disabled = true; button.textContent = "Sweeping TTL...";
928
  try { renderAgentTtl(await callPython("agent_ttl_sweep", { config: agentConfigFromUI() })); }
929
+ catch (error) { reportError("TTL sweep failed", error); }
930
  finally { button.disabled = false; button.textContent = "Run TTL sweep"; }
931
  });
932
  $("agentTtlCopy").addEventListener("click", () => { if (lastAgentTtl) copyText(tableText(agentTtlHeaders, agentTtlTableRows(lastAgentTtl)), "TTL sweep copied"); });
 
955
  $("agentMemoryCompareBtn").addEventListener("click", async () => {
956
  const button = $("agentMemoryCompareBtn"); button.disabled = true; button.textContent = "Comparing policies...";
957
  try { renderAgentMemory(await callPython("agent_memory_compare", { config: agentConfigFromUI() })); }
958
+ catch (error) { reportError("Memory policy comparison failed", error); }
959
  finally { button.disabled = false; button.textContent = "Compare memory policies"; }
960
  });
961
  $("agentMemoryCopy").addEventListener("click", () => { if (lastAgentMemory) copyText(tableText(agentMemoryHeaders, agentMemoryTableRows(lastAgentMemory)), "Memory-policy table copied"); });
 
985
  $("agentBudgetBtn").addEventListener("click", async () => {
986
  const button = $("agentBudgetBtn"); button.disabled = true; button.textContent = "Stressing HBM budget...";
987
  try { renderAgentBudget(await callPython("agent_memory_sweep", { config: agentConfigFromUI() })); }
988
+ catch (error) { reportError("HBM budget study failed", error); }
989
  finally { button.disabled = false; button.textContent = "Stress HBM budget"; }
990
  });
991
  $("agentBudgetCopy").addEventListener("click", () => { if (lastAgentBudget) copyText(tableText(agentBudgetHeaders, agentBudgetTableRows(lastAgentBudget)), "HBM budget table copied"); });
 
1014
  $("agentAffinityBtn").addEventListener("click", async () => {
1015
  const button = $("agentAffinityBtn"); button.disabled = true; button.textContent = "Sweeping affinity...";
1016
  try { renderAgentAffinity(await callPython("agent_affinity_sweep", { config: agentConfigFromUI() })); }
1017
+ catch (error) { reportError("Affinity sweep failed", error); }
1018
  finally { button.disabled = false; button.textContent = "Run affinity sweep"; }
1019
  });
1020
  $("agentAffinityCopy").addEventListener("click", () => { if (lastAgentAffinity) copyText(tableText(agentAffinityHeaders, agentAffinityTableRows(lastAgentAffinity)), "Affinity sweep copied"); });
 
1088
  $("predictiveCompareBtn").addEventListener("click", async () => {
1089
  const button = $("predictiveCompareBtn"); button.disabled = true; button.textContent = "Running predictive study...";
1090
  try { renderPredictiveStudy(await callPython("agent_predictive_tiering", predictivePayload())); }
1091
+ catch (error) { reportError("Predictive tiering study failed", error); }
1092
  finally { button.disabled = false; button.textContent = "Compare predictive policies"; }
1093
  });
1094
  $("predictiveCopy").addEventListener("click", () => { if (lastPredictiveStudy) copyText(tableText(predictiveHeaders, predictiveTableRows(lastPredictiveStudy)), "Predictive-tiering table copied"); });
 
1121
  const button = $("predictiveAlphaBtn"); button.disabled = true; button.textContent = "Sweeping adaptation rate...";
1122
  const payload = predictivePayload();
1123
  try { renderPredictiveAlpha(await callPython("agent_adaptive_alpha_sweep", payload)); }
1124
+ catch (error) { reportError("Adaptation-rate sweep failed", error); }
1125
  finally { button.disabled = false; button.textContent = "Sweep adaptation rate"; }
1126
  });
1127
  $("predictiveAlphaCopy").addEventListener("click", () => { if (lastPredictiveAlpha) copyText(tableText(predictiveAlphaHeaders, predictiveAlphaTableRows(lastPredictiveAlpha)), "Adaptation-rate table copied"); });
 
1232
  button.disabled = true; button.textContent = "Running execution model...";
1233
  $("execRunState").textContent = "Running..."; $("execRunState").className = "tag neutral";
1234
  try { renderExecutionRun(await callPython("execution_learning_run", { config: executionConfigFromUI() })); }
1235
+ catch (error) { $("execRunState").textContent = "Error"; $("execRunState").className = "tag bad"; reportError("Execution-learning run failed", error); }
1236
  finally { button.disabled = false; button.textContent = "Run execution-learning simulation"; }
1237
  });
1238
  $("execRunCopyJson").addEventListener("click", () => { if (lastExecutionRun) copyText(JSON.stringify(lastExecutionRun, null, 2), "Execution-learning JSON copied"); });
 
1258
  $("execCompareBtn").addEventListener("click", async () => {
1259
  const button = $("execCompareBtn"); button.disabled = true; button.textContent = "Comparing policies...";
1260
  try { renderExecutionCompare(await callPython("execution_prefetch_study", { config: executionConfigFromUI() })); }
1261
+ catch (error) { reportError("Execution prefetch study failed", error); }
1262
  finally { button.disabled = false; button.textContent = "Compare 4 policies"; }
1263
  });
1264
  $("execCompareCopy").addEventListener("click", () => { if (lastExecutionCompare) copyText(tableText(execCompareHeaders, execCompareTableRows(lastExecutionCompare)), "Execution policy table copied"); });
 
1293
  $("execThresholdBtn").addEventListener("click", async () => {
1294
  const button = $("execThresholdBtn"); button.disabled = true; button.textContent = "Sweeping threshold...";
1295
  try { renderExecutionThreshold(await callPython("execution_threshold_sweep", { config: executionConfigFromUI() })); }
1296
+ catch (error) { reportError("Execution threshold sweep failed", error); }
1297
  finally { button.disabled = false; button.textContent = "Sweep confidence threshold"; }
1298
  });
1299
  $("execThresholdCopy").addEventListener("click", () => { if (lastExecutionThreshold) copyText(tableText(execThresholdHeaders, execThresholdTableRows(lastExecutionThreshold)), "Execution threshold table copied"); });
 
1326
  $("execDecayBtn").addEventListener("click", async () => {
1327
  const button = $("execDecayBtn"); button.disabled = true; button.textContent = "Sweeping decay...";
1328
  try { renderExecutionDecay(await callPython("execution_decay_sweep", { config: executionConfigFromUI() })); }
1329
+ catch (error) { reportError("Execution decay sweep failed", error); }
1330
  finally { button.disabled = false; button.textContent = "Sweep transition decay"; }
1331
  });
1332
  $("execDecayCopy").addEventListener("click", () => { if (lastExecutionDecay) copyText(tableText(execDecayHeaders, execDecayTableRows(lastExecutionDecay)), "Execution decay table copied"); });
 
1362
  $("execPlanningBtn").addEventListener("click", async () => {
1363
  const button = $("execPlanningBtn"); button.disabled = true; button.textContent = "Comparing planners...";
1364
  try { renderExecutionPlanning(await callPython("execution_planning_study", { config: executionConfigFromUI() })); }
1365
+ catch (error) { reportError("Execution planning study failed", error); }
1366
  finally { button.disabled = false; button.textContent = "Compare planning policies"; }
1367
  });
1368
  $("execPlanningCopy").addEventListener("click", () => { if (lastExecutionPlanning) copyText(tableText(execPlanningHeaders, execPlanningTableRows(lastExecutionPlanning)), "Planning table copied"); });
 
1396
  $("execHorizonBtn").addEventListener("click", async () => {
1397
  const button = $("execHorizonBtn"); button.disabled = true; button.textContent = "Sweeping horizons...";
1398
  try { renderExecutionHorizon(await callPython("execution_horizon_sweep", { config: executionConfigFromUI() })); }
1399
+ catch (error) { reportError("Forecast horizon sweep failed", error); }
1400
  finally { button.disabled = false; button.textContent = "Sweep forecast horizon"; }
1401
  });
1402
  $("execHorizonCopy").addEventListener("click", () => { if (lastExecutionHorizon) copyText(tableText(execHorizonHeaders, execHorizonTableRows(lastExecutionHorizon)), "Horizon table copied"); });
 
1424
  $("execBudgetBtn").addEventListener("click", async () => {
1425
  const button = $("execBudgetBtn"); button.disabled = true; button.textContent = "Sweeping cache budget...";
1426
  try { renderExecutionBudget(await callPython("execution_budget_sweep", { config: executionConfigFromUI() })); }
1427
+ catch (error) { reportError("Cache budget study failed", error); }
1428
  finally { button.disabled = false; button.textContent = "Sweep cache budget"; }
1429
  });
1430
  $("execBudgetCopy").addEventListener("click", () => { if (lastExecutionBudget) copyText(tableText(execBudgetHeaders, execBudgetTableRows(lastExecutionBudget)), "Cache budget table copied"); });
 
1477
  renderConsolidation(await callPython("consolidation_study", {
1478
  config: executionConfigFromUI(), repetitions: num("consSeeds"), bootstrap_samples: num("consBootstrap"),
1479
  }));
1480
+ } catch (error) { reportError("Robust policy study failed", error); }
1481
  finally { button.disabled = false; button.textContent = "Run robust policy study"; }
1482
  });
1483
  $("consCopy").addEventListener("click", () => { if (lastConsolidation) copyText(tableText(consHeaders, consTableRows(lastConsolidation)), "Robust policy table copied"); });
 
1493
  $("measurementCalibrateBtn").disabled = !runtimePill.classList.contains("ready");
1494
  } catch (error) {
1495
  measurementFileContent = ""; $("measurementCalibrateBtn").disabled = true;
1496
+ $("measurementStatus").textContent = "File read failed"; reportError("Measurement file failed", error);
1497
  }
1498
  });
1499
 
 
1536
  cases: imported.cases, holdout_fraction: num("measurementHoldout"), seed: num("seed"),
1537
  });
1538
  renderCalibration(imported, calibrated);
1539
+ } catch (error) { reportError("Measurement calibration failed", error); }
1540
  finally { button.disabled = false; button.textContent = "Import and calibrate"; }
1541
  });
1542
  $("measurementCopy").addEventListener("click", () => { if (lastCalibration) copyText(tableText(measurementHeaders, measurementTableRows(lastCalibration)), "Validation table copied"); });
 
1548
  try {
1549
  const result = await callPython("research_report", { robust: lastConsolidation, calibration: lastCalibration });
1550
  downloadTextFile(`inferscale_research-consolidation_${stamp()}.md`, result.markdown, "text/markdown;charset=utf-8");
1551
+ } catch (error) { reportError("Report generation failed", error); }
1552
  finally { button.disabled = false; button.textContent = "Download Markdown report"; }
1553
  });
1554
 
 
1619
  const file = event.target.files?.[0];
1620
  if (!file) return;
1621
  try { await loadTraceFile(file); }
1622
+ catch (error) { traceRequests = []; $("traceStatus").textContent = "Trace rejected"; reportError("Trace load failed", error); }
1623
  });
1624
  $("traceClearBtn").addEventListener("click", () => {
1625
  traceRequests = [];
 
1648
  $("arrival").addEventListener("change", syncConditionalControls);
1649
  syncConditionalControls();
1650
 
1651
+ const tabButtons = [...document.querySelectorAll(".tab")];
1652
+ function activateTab(tab) {
1653
+ for (const item of tabButtons) {
1654
+ const active = item === tab;
1655
+ item.classList.toggle("active", active);
1656
+ item.setAttribute("aria-selected", active ? "true" : "false");
1657
+ item.tabIndex = active ? 0 : -1;
1658
+ }
1659
+ document.querySelectorAll(".tab-panel").forEach((panel) => {
1660
+ panel.classList.remove("active");
1661
+ panel.setAttribute("aria-hidden", "true");
1662
+ });
1663
+ const targetPanel = $(tab.dataset.tab);
1664
+ targetPanel.classList.add("active");
1665
+ targetPanel.setAttribute("aria-hidden", "false");
1666
+ setTimeout(() => Object.values(charts).forEach((chart) => chart.resize()), 20);
1667
+ }
1668
+ for (const tab of tabButtons) {
1669
+ tab.addEventListener("click", () => activateTab(tab));
1670
+ tab.addEventListener("keydown", (event) => {
1671
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
1672
+ event.preventDefault();
1673
+ const current = tabButtons.indexOf(tab);
1674
+ let next = current;
1675
+ if (event.key === "ArrowLeft") next = (current - 1 + tabButtons.length) % tabButtons.length;
1676
+ if (event.key === "ArrowRight") next = (current + 1) % tabButtons.length;
1677
+ if (event.key === "Home") next = 0;
1678
+ if (event.key === "End") next = tabButtons.length - 1;
1679
+ tabButtons[next].focus();
1680
+ activateTab(tabButtons[next]);
1681
  });
1682
  }
1683
 
docs/architecture.md CHANGED
@@ -104,7 +104,7 @@ without rewriting workload generation, scheduling, cache logic, P/D orchestratio
104
 
105
  ## Agent memory tiering
106
 
107
- Stateful Agent Sessions has an additional memory path that is independent from the stateless/P-D simulator:
108
 
109
  ```text
110
  turn completes
 
104
 
105
  ## Agent memory tiering
106
 
107
+ Stateful Stateful Sessions has an additional memory path that is independent from the stateless/P-D simulator:
108
 
109
  ```text
110
  turn completes
docs/design.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Interface design notes
2
+
3
+ InferScale is a research workbench, not a SaaS product. The UI is intentionally quiet so the experiment is the visual focus.
4
+
5
+ ## Design rules
6
+
7
+ - **One job per screen.** Controls define an experiment; results explain that experiment. Secondary actions stay visually subordinate.
8
+ - **One accent color.** Blue marks the selected tab, primary action, and key chart series. Status colors are reserved for pass/fail/warning semantics.
9
+ - **Flat hierarchy.** Panels and charts use thin borders and square geometry. Avoid nested rounded cards, glass effects, gradients, glowing shadows, and decorative badges.
10
+ - **Editorial type.** System sans-serif for prose and controls; monospace only for evidence/provenance snippets. No decorative display font is required.
11
+ - **Spacing over decoration.** Use consistent 8/12/16/24 px rhythms before adding borders or backgrounds.
12
+ - **Labels are literal.** Prefer `Scheduler comparison`, `Cache budget`, and `Held-out calibration` over promotional names such as “arena”, “command center”, or “insights”.
13
+ - **Status is text, not chip soup.** Runtime/pass/fail state should be legible without turning every fact into a badge.
14
+ - **Progressive disclosure.** Prefix, trace, P/D, predictive, and calibration controls appear only when relevant.
15
+ - **No company chrome.** No promotional footer, testimonials, pricing-style cards, or fake product navigation.
16
+ - **Motion has a job.** Only the runtime header retracts after initialization; charts do not animate. Reduced-motion preferences are honored.
17
+
18
+ ## Tokens
19
+
20
+ The public CSS defines a small neutral palette:
21
+
22
+ ```text
23
+ background #080c11
24
+ surface #0c1219
25
+ inset surface #090e14
26
+ border #27313d
27
+ text #e3e8ee
28
+ muted text #929dab
29
+ accent #6f96cf
30
+ success #68c394
31
+ error #df7d89
32
+ warning #d2b36a
33
+ ```
34
+
35
+ Corners are 0–2 px for most surfaces and controls. The interface does not use gradients or pill-shaped containers.
36
+
37
+ ## Maintenance checklist
38
+
39
+ Before shipping a UI change:
40
+
41
+ 1. Does the element help configure, inspect, compare, or export an experiment?
42
+ 2. Can an existing treatment be reused instead of introducing another card/button/badge style?
43
+ 3. Is the copy literal and specific?
44
+ 4. Is information duplicated between a heading, kicker, chip, and paragraph?
45
+ 5. Does the layout still work at 720 px and 1100 px breakpoints?
46
+ 6. Is the control keyboard-focusable and does focus remain visible?
47
+ 7. Does `scripts/release_check.py` still pass the public-design guardrails?
docs/methodology.md CHANGED
@@ -134,7 +134,7 @@ The perturbation distribution is deliberately described as a **sensitivity analy
134
 
135
  Agent mode is a separate program-level discrete-event model. Session arrivals are open-loop. Each session receives a deterministic number of turns, token increments, outputs, and tool gaps from a seeded trace. A later turn cannot become ready until the prior turn completes and its tool gap elapses.
136
 
137
- Each simulated replica is a serial analytical service station in this mode. This is deliberate: dynamic batching remains covered by Serving Lab, while Agent Sessions isolates four stateful effects:
138
 
139
  1. **cross-turn reuse:** a resident KV entry means the next turn prefills only appended tokens;
140
  2. **tool-gap residency:** retained KV occupies memory while the program waits outside the model;
@@ -201,7 +201,7 @@ This sequencing is important: directly updating the predictor at LLM-turn comple
201
 
202
  ## Non-stationary predictive-tiering study
203
 
204
- The Predictive Tiering Lab generates one common program trace and replays it across fixed and adaptive policies. At a configured fraction of the arrival horizon, the duration scale of slower external tools (`database` and `remote_api`) is multiplied by a user-selected factor. Faster local-style tools remain unchanged.
205
 
206
  The study compares:
207
 
@@ -231,7 +231,7 @@ The EWMA alpha sweep replays the exact same shifted trace for every candidate al
231
 
232
  ## Online execution-transition learning
233
 
234
- 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.
235
 
236
  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.
237
 
@@ -276,14 +276,14 @@ This distinction matters because multi-step prefetch can be useful even when a p
276
 
277
  ### Controlled studies
278
 
279
- 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.
280
 
281
  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.
282
 
283
 
284
  ## Research consolidation and robust policy ranking
285
 
286
- The final research layer deliberately stops adding new serving mechanisms and asks whether policy conclusions survive workload variation. `repeated_seed_policy_study` generates matched workflow traces across multiple deterministic seeds and replays every deployable Execution Learning policy on each seed. It reports:
287
 
288
  - median and mean p95 step TTFT;
289
  - a bootstrap interval over seed-level TTFT;
 
134
 
135
  Agent mode is a separate program-level discrete-event model. Session arrivals are open-loop. Each session receives a deterministic number of turns, token increments, outputs, and tool gaps from a seeded trace. A later turn cannot become ready until the prior turn completes and its tool gap elapses.
136
 
137
+ Each simulated replica is a serial analytical service station in this mode. This is deliberate: dynamic batching remains covered by Serving section, while Stateful Sessions isolates four stateful effects:
138
 
139
  1. **cross-turn reuse:** a resident KV entry means the next turn prefills only appended tokens;
140
  2. **tool-gap residency:** retained KV occupies memory while the program waits outside the model;
 
201
 
202
  ## Non-stationary predictive-tiering study
203
 
204
+ The Predictive tiering experiment generates one common program trace and replays it across fixed and adaptive policies. At a configured fraction of the arrival horizon, the duration scale of slower external tools (`database` and `remote_api`) is multiplied by a user-selected factor. Faster local-style tools remain unchanged.
205
 
206
  The study compares:
207
 
 
231
 
232
  ## Online execution-transition learning
233
 
234
+ Execution Model models a different reuse object from Stateful Sessions. Stateful Sessions tracks one user's growing cross-turn KV state. Execution Model tracks reusable *static agent prefixes* (for example planner/retriever/reasoner system context) that can be shared across workflows.
235
 
236
  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.
237
 
 
276
 
277
  ### Controlled studies
278
 
279
+ The confidence-threshold sweep varies when one-step learned predictions are acted on. The transition-decay sweep varies transition decay. The prefetch-planning comparison compares top-1, multi-step top-k, utility-aware multi-step, and a clairvoyant realized-future-set information bound. The forecast-horizon sweep varies rollout depth on a common trace. The prefix-cache budget sweep runs top-1, multi-step, and utility-aware planners at multiple shared-prefix working-set budgets.
280
 
281
  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.
282
 
283
 
284
  ## Research consolidation and robust policy ranking
285
 
286
+ The final research layer deliberately stops adding new serving mechanisms and asks whether policy conclusions survive workload variation. `repeated_seed_policy_study` generates matched workflow traces across multiple deterministic seeds and replays every deployable Execution Model policy on each seed. It reports:
287
 
288
  - median and mean p95 step TTFT;
289
  - a bootstrap interval over seed-level TTFT;
docs/research.md CHANGED
@@ -68,7 +68,7 @@ Vanguard highlights two methodology points that directly influence InferScale:
68
  1. **open-loop replay** avoids coordinated omission by scheduling requests independently of response time;
69
  2. repeated load-test analysis benefits from reproducibility statistics and bootstrap confidence intervals.
70
 
71
- InferScale's generated workloads are open-loop, trace replay preserves externally supplied arrivals, and Research Studies use repeated paired simulations with bootstrap intervals.
72
 
73
  Source: https://arxiv.org/abs/2606.22013
74
 
@@ -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 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
 
@@ -159,7 +159,7 @@ Source: https://arxiv.org/abs/2605.06472
159
 
160
  A recent runtime-layer proposal formalizes agent-aware serving policies around observe/score/predict/act primitives and demonstrates online transition learning plus between-step KV prefetch as one concrete policy family.
161
 
162
- InferScale inspiration: keep observation and action timing explicit. In Execution Learning, the prediction is made before the next role is revealed, the transition model is updated only when that role becomes observable, and the oracle policy remains separately tagged as a clairvoyant upper bound.
163
 
164
  Source: https://arxiv.org/abs/2605.27744
165
 
 
68
  1. **open-loop replay** avoids coordinated omission by scheduling requests independently of response time;
69
  2. repeated load-test analysis benefits from reproducibility statistics and bootstrap confidence intervals.
70
 
71
+ InferScale's generated workloads are open-loop, trace replay preserves externally supplied arrivals, and A/B Studies use repeated paired simulations with bootstrap intervals.
72
 
73
  Source: https://arxiv.org/abs/2606.22013
74
 
 
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 Model** 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
 
 
159
 
160
  A recent runtime-layer proposal formalizes agent-aware serving policies around observe/score/predict/act primitives and demonstrates online transition learning plus between-step KV prefetch as one concrete policy family.
161
 
162
+ InferScale inspiration: keep observation and action timing explicit. In Execution Model, the prediction is made before the next role is revealed, the transition model is updated only when that role becomes observable, and the oracle policy remains separately tagged as a clairvoyant upper bound.
163
 
164
  Source: https://arxiv.org/abs/2605.27744
165
 
docs/validation.md CHANGED
@@ -84,7 +84,7 @@ The calibration script fits only on the training cases and reports baseline/cali
84
  - radix-tree prefix-cache eviction/scheduling
85
  - speculative decoding
86
  - Attention-FFN disaggregation
87
- - production-fidelity agentic serving or dynamic batching inside Agent Sessions
88
  - real tool execution or production agent-runtime integration
89
  - claims that the bounded offline oracle is globally optimal
90
  - claims that simulator bootstrap intervals quantify real-hardware error
 
84
  - radix-tree prefix-cache eviction/scheduling
85
  - speculative decoding
86
  - Attention-FFN disaggregation
87
+ - production-fidelity agentic serving or dynamic batching inside Stateful Sessions
88
  - real tool execution or production agent-runtime integration
89
  - claims that the bounded offline oracle is globally optimal
90
  - claims that simulator bootstrap intervals quantify real-hardware error
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
scripts/release_check.py CHANGED
@@ -90,9 +90,28 @@ if "Download PNG" not in index or ".chart-download" not in app:
90
  errors.append("chart PNG export controls are missing")
91
  if "Worst repetition" not in index or "Target" not in index:
92
  errors.append("capacity evidence columns are missing")
93
- 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", "Research Summary", "Robust policy ranking", "Held-out calibration", "Download Markdown report", "Download JSON"]:
94
- if expected not in index:
95
- errors.append(f"UI is missing research/trace feature: {expected}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  # Every $("id") lookup in app.js should resolve to a static DOM id.
98
  app_ids = set(re.findall(r'\$\("([A-Za-z0-9_-]+)"\)', app))
 
90
  errors.append("chart PNG export controls are missing")
91
  if "Worst repetition" not in index or "Target" not in index:
92
  errors.append("capacity evidence columns are missing")
93
+ required_ui_ids = [
94
+ "traceFile", "pairedStudyBtn", "robustStudyBtn", "agentRunBtn", "agentCompareBtn", "agentTtlBtn",
95
+ "agentMemoryCompareBtn", "agentBudgetBtn", "agentAffinityBtn", "predictiveCompareBtn", "predictiveAlphaBtn",
96
+ "execRunBtn", "execCompareBtn", "execThresholdBtn", "execDecayBtn", "execPlanningBtn", "execHorizonBtn",
97
+ "execBudgetBtn", "consRunBtn", "measurementCalibrateBtn", "consReportBtn", "exportBtn", "agentRunJson",
98
+ ]
99
+ for expected_id in required_ui_ids:
100
+ if f'id="{expected_id}"' not in index:
101
+ errors.append(f"UI is missing required control: {expected_id}")
102
+
103
+ # Design guardrails for the public research-workbench UI.
104
+ css = (ROOT / "styles.css").read_text()
105
+ if "gradient(" in css.lower():
106
+ errors.append("UI should not use decorative CSS gradients")
107
+ if re.search(r"border-radius:\s*(?:999|9999|50)%?px?", css, re.IGNORECASE):
108
+ errors.append("UI contains pill-style over-rounding")
109
+ if "feature freeze" in index.lower() or "feature freeze" in README.lower():
110
+ errors.append("public UI/docs should not mention the release process")
111
+ if 'class="subtitle"' in index:
112
+ errors.append("topbar should remain a compact tool header, not a product masthead")
113
+ if re.search(r"\.section-kicker\s*\{[^}]*text-transform:\s*uppercase", css, re.DOTALL | re.IGNORECASE):
114
+ errors.append("section labels should not use repetitive AI-style uppercase kickers")
115
 
116
  # Every $("id") lookup in app.js should resolve to a static DOM id.
117
  app_ids = set(re.findall(r'\$\("([A-Za-z0-9_-]+)"\)', app))
styles.css CHANGED
@@ -1,18 +1,20 @@
1
  :root {
2
- --bg: #090d12;
3
- --surface: #0d1219;
4
- --surface-2: #0a0f15;
5
- --surface-3: #111822;
6
- --line: #26313f;
7
- --line-soft: #1b2430;
8
- --text: #e4e9ef;
 
9
  --muted: #929dab;
10
- --faint: #697586;
11
- --accent: #6f9fe8;
12
- --accent-2: #9ab8df;
13
- --good: #6fd0a0;
14
- --danger: #ee7f8d;
15
- --amber: #d8b96e;
 
16
  }
17
 
18
  * { box-sizing: border-box; }
@@ -21,314 +23,640 @@ body {
21
  margin: 0;
22
  background: var(--bg);
23
  color: var(--text);
24
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
25
  font-size: 16px;
 
 
26
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
 
28
  .topbar {
29
  position: fixed;
30
  inset: 0 0 auto 0;
31
  z-index: 80;
32
- height: 64px;
33
  padding: 0 24px;
34
  display: flex;
35
  align-items: center;
36
  justify-content: space-between;
37
  gap: 16px;
38
- background: #090d12;
39
  border-bottom: 1px solid var(--line);
40
- transition: transform .18s ease;
41
  }
42
- body.runtime-ready .topbar { transform: translateY(-58px); }
43
  body.runtime-ready .topbar:hover,
44
  body.runtime-ready .topbar:focus-within { transform: translateY(0); }
45
- .brand-wrap { display: flex; align-items: center; min-width: 0; }
46
- .brand { font-size: 15px; font-weight: 760; letter-spacing: -.01em; }
47
- .subtitle { margin-top: 2px; color: var(--faint); font-size: 12px; }
48
  .runtime-pill {
49
  display: inline-flex;
50
  align-items: center;
51
- gap: 7px;
52
  color: var(--muted);
53
- border: 1px solid var(--line);
54
- background: var(--surface-2);
55
- border-radius: 4px;
56
- padding: 6px 9px;
57
- font-size: 11.5px;
58
  white-space: nowrap;
59
  }
60
- .runtime-pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--amber); }
 
 
 
 
 
61
  .runtime-pill.ready .dot { background: var(--good); }
62
  .runtime-pill.error .dot { background: var(--danger); }
63
 
 
64
  .shell {
65
- width: min(1460px, calc(100% - 40px));
66
  margin: 0 auto;
67
- padding: 94px 0 44px;
68
- transition: padding-top .18s ease;
69
  }
70
- body.runtime-ready .shell { padding-top: 34px; }
71
 
72
  .intro {
73
- display: grid;
74
- grid-template-columns: minmax(0, 1.4fr) minmax(320px, .6fr);
75
- gap: 46px;
76
- align-items: end;
77
- padding: 16px 0 18px;
78
- }
79
- .eyebrow, .section-kicker {
80
- color: var(--accent-2);
81
- font-size: 10.5px;
82
- line-height: 1.25;
83
- text-transform: uppercase;
84
- letter-spacing: .1em;
85
- font-weight: 760;
86
  }
87
  .intro h1 {
88
- max-width: 900px;
89
- margin: 8px 0 9px;
90
  color: #edf1f5;
91
- font-size: clamp(28px, 3vw, 43px);
92
- line-height: 1.06;
93
- letter-spacing: -.038em;
94
  font-weight: 720;
95
  }
96
- .intro p { max-width: 920px; margin: 0; color: var(--muted); font-size: 14.5px; line-height: 1.65; }
97
- .project-facts { margin: 0; border-top: 1px solid var(--line); }
98
- .project-facts div { display: grid; grid-template-columns: 120px 1fr; gap: 14px; padding: 9px 0; border-bottom: 1px solid var(--line-soft); }
99
- .project-facts dt { color: var(--faint); font-size: 11.5px; }
100
- .project-facts dd { margin: 0; color: #bdc7d2; font-size: 12.5px; }
 
 
 
 
 
 
 
 
101
  .reference-note {
102
- border-left: 2px solid #6c6041;
103
- background: #11120f;
104
- color: #bcb5a5;
105
- padding: 10px 12px;
 
106
  font-size: 12.5px;
107
  line-height: 1.55;
108
  }
 
109
 
 
110
  .tabs {
111
  display: flex;
112
- gap: 2px;
113
- padding: 22px 0 10px;
 
114
  overflow-x: auto;
115
- border-bottom: 1px solid var(--line-soft);
116
- margin-bottom: 14px;
117
  }
118
  .tab {
 
119
  border: 0;
120
  border-bottom: 2px solid transparent;
121
- color: var(--muted);
122
  background: transparent;
123
- padding: 9px 11px;
 
124
  cursor: pointer;
125
- font-weight: 650;
126
- font-size: 13px;
127
  white-space: nowrap;
128
  }
129
- .tab:hover { color: var(--text); }
130
  .tab.active { color: var(--text); border-bottom-color: var(--accent); }
131
  .tab-panel { display: none; }
132
  .tab-panel.active { display: block; }
133
 
134
- .workspace { display: grid; grid-template-columns: 430px minmax(0, 1fr); gap: 14px; align-items: start; }
135
- .planner-grid { grid-template-columns: 390px minmax(0, 1fr); }
136
- .panel { background: var(--surface); border: 1px solid var(--line); border-radius: 6px; }
137
- .controls-panel { padding: 18px; position: sticky; top: 16px; }
138
- .result-panel, .wide-panel { padding: 20px; }
139
- .panel-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 16px; }
140
- .panel-title-row h2 { margin: 0; font-size: 19px; letter-spacing: -.02em; }
141
- .panel-title-row p { margin: 6px 0 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  .tag {
143
  display: inline-flex;
144
  align-items: center;
145
- border: 1px solid var(--line);
146
- background: var(--surface-2);
147
  color: var(--muted);
148
- border-radius: 4px;
149
- padding: 5px 7px;
 
150
  font-size: 11.5px;
151
  white-space: nowrap;
152
  }
153
- .tag.good { color: var(--good); border-color: #315e4a; }
154
- .tag.bad { color: var(--danger); border-color: #643943; }
155
 
156
- label { display: block; color: #b5bfcb; font-size: 12.5px; font-weight: 650; position: relative; }
157
- select, input[type="number"] {
 
 
 
 
 
 
 
 
158
  width: 100%;
159
- margin-top: 7px;
160
  height: 40px;
 
161
  border: 1px solid var(--line);
162
- background: #0a0f15;
 
163
  color: var(--text);
164
- border-radius: 4px;
165
- padding: 0 9px;
166
  outline: none;
167
  font-size: 14px;
168
  }
169
- select:focus, input[type="number"]:focus { border-color: #5475a8; box-shadow: 0 0 0 2px rgba(84,117,168,.16); }
170
- .unit { position: absolute; right: 9px; bottom: 12px; color: #697687; font-size: 10.5px; pointer-events: none; }
171
- .field-grid { display: grid; gap: 10px; margin-bottom: 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  .field-grid.two { grid-template-columns: 1fr 1fr; }
173
  .field-grid.four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
174
- .subcontrols { margin: 12px 0 4px; padding: 12px; border: 1px solid var(--line-soft); background: #0a0f15; }
 
 
 
 
 
175
  .subcontrols .section-kicker { margin-bottom: 10px; }
176
- hr { border: 0; border-top: 1px solid var(--line-soft); margin: 17px 0; }
177
- .section-kicker { margin-bottom: 10px; }
178
  .hidden { display: none !important; }
 
179
 
180
- button.primary, button.secondary {
181
- height: 43px;
182
- border-radius: 4px;
183
- font-weight: 700;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  cursor: pointer;
185
- transition: .12s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
187
- button.primary { width: 100%; border: 1px solid #668fd5; background: #4f7fce; color: white; margin-top: 11px; }
188
- button.primary:hover:not(:disabled) { background: #5b8bd8; }
189
- button.secondary { border: 1px solid var(--line); background: #0b1016; color: #c4ccd7; }
190
  button.secondary:hover:not(:disabled),
191
  .mini-button:hover:not(:disabled),
192
  .chart-expand:hover,
193
- .chart-download:hover { border-color: #536174; background: #151b24; color: #d9e0e8; }
194
- button:disabled { opacity: .42; cursor: not-allowed; }
195
- .compact { width: auto !important; min-width: 178px; padding: 0 17px; margin-top: 0 !important; }
 
 
 
 
 
 
 
 
 
196
  .button-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
197
- .action-stack { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
198
- .checkline { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; font-weight: 600; }
 
 
 
 
 
 
 
 
 
199
  .checkline input { accent-color: var(--accent); }
200
 
201
- .empty-state { min-height: 380px; display: grid; place-content: center; text-align: center; color: var(--muted); padding: 30px; }
202
- .empty-state.small { min-height: 240px; }
203
- .empty-state h3 { color: #ccd4de; margin: 0 0 6px; font-size: 17px; }
204
- .empty-state p { max-width: 620px; margin: 0; line-height: 1.6; font-size: 13.5px; }
205
- .metric-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; margin-bottom: 12px; }
206
- .metric-grid.four { grid-template-columns: repeat(4, 1fr); }
207
- .metric { min-height: 82px; padding: 13px; border: 1px solid var(--line-soft); background: var(--surface-2); }
208
- .metric span { display: block; color: var(--muted); font-size: 11.5px; margin-bottom: 9px; }
209
- .metric strong { font-size: 20px; letter-spacing: -.025em; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  .metric strong.metric-small { font-size: 14.5px; line-height: 1.35; }
211
- .metric.emphasis { border-color: #486da4; background: #101824; }
212
 
213
- .diagnostic-card { margin-bottom: 12px; border: 1px solid var(--line); background: #0d131b; padding: 14px; }
 
 
 
 
 
214
  .diagnostic-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
215
- .diagnostic-head span { color: var(--muted); font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em; }
216
  .diagnostic-head strong { font-size: 16px; }
217
- .diagnostic-card p { margin: 8px 0 0; color: #b8c1cd; font-size: 13.5px; line-height: 1.55; }
218
  .diagnostic-card .diagnostic-action { color: var(--muted); }
219
- .evidence-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
220
- .evidence-row span { border: 1px solid var(--line-soft); background: #090e14; padding: 5px 7px; color: #8390a0; font: 11px/1.25 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
 
 
 
 
 
 
221
 
222
- .chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
223
- .chart-card { border: 1px solid var(--line); background: var(--surface-2); min-height: 300px; position: relative; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  .chart-card.full { margin-top: 10px; }
225
- .chart-head { height: 42px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 12px; border-bottom: 1px solid var(--line-soft); }
226
- .chart-title { color: #c7d0db; font-size: 12.5px; font-weight: 700; }
 
 
 
 
 
 
 
 
227
  .chart-actions { display: flex; gap: 6px; }
228
- .chart-expand, .chart-download, .mini-button { border: 1px solid var(--line); background: #0a0f15; color: var(--muted); border-radius: 3px; cursor: pointer; height: 31px; padding: 0 10px; font-size: 11.5px; }
229
- .chart-body { height: 260px; padding: 10px 12px 12px; }
 
 
 
 
 
 
 
 
 
 
 
230
  .chart-body.large { height: 350px; }
231
- .chart-card.chart-expanded { position: fixed; inset: 16px; z-index: 100; background: #0a0f15; border-color: #4c596b; box-shadow: 0 0 0 9999px rgba(0,0,0,.76); min-height: 0; margin: 0; }
232
- .chart-card.chart-expanded .chart-body { height: calc(100vh - 80px); }
 
 
 
 
 
 
 
 
 
 
233
  body.chart-open { overflow: hidden; }
234
  canvas { width: 100% !important; height: 100% !important; max-height: none; }
235
 
236
- .warnings { margin-top: 10px; border-left: 3px solid #7b6840; padding: 10px 12px; background: #15140f; color: #c4bca9; font-size: 12.5px; line-height: 1.5; }
237
- .muted { color: var(--muted); font-size: 13.5px; line-height: 1.55; }
238
- .table-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; color: #aab4c1; font-size: 12.5px; }
 
 
 
 
 
 
 
239
  .table-toolbar > div { display: flex; gap: 6px; }
240
- .table-wrap { overflow: auto; margin-top: 7px; border: 1px solid var(--line); }
241
- table { width: 100%; border-collapse: collapse; font-size: 13.5px; min-width: 900px; }
242
- th, td { padding: 10px 11px; text-align: right; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
243
- th:first-child, td:first-child { text-align: left; }
244
- th { color: #8e9aab; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; background: #090e14; position: sticky; top: 0; }
245
- td { color: #c7d0db; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  tbody tr:last-child td { border-bottom: 0; }
247
- tbody tr:hover td { background: #111821; }
248
  .pass { color: var(--good); }
249
  .fail { color: var(--danger); }
250
- .best-label, .pareto-label { display: inline-block; margin-left: 7px; border: 1px solid #4a6386; color: #9ab8df; padding: 2px 5px; border-radius: 2px; font-size: 10.5px; text-transform: uppercase; }
251
- .planner-note { color: var(--muted); border-left: 2px solid #445a77; padding: 9px 11px; margin-bottom: 2px; font-size: 12.5px; line-height: 1.5; }
 
 
 
 
 
 
 
 
252
 
253
- .method-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
254
- .prose { padding: 22px; }
255
- .prose h2 { font-size: 20px; margin: 8px 0 11px; }
256
- .prose p { color: var(--muted); line-height: 1.7; font-size: 14.5px; }
257
- .prose code { color: #a8bfe4; }
258
- .formula { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #b7c3d2; padding: 10px; border: 1px solid var(--line); background: #0a0f15; font-size: 12.5px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  .wide-method { grid-column: 1 / -1; }
260
- .paper-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 18px; }
261
- .paper-grid > div { padding: 12px; border: 1px solid var(--line-soft); background: #0c1219; }
262
- .paper-grid strong { display: block; font-size: 12.5px; margin-bottom: 7px; }
263
- .paper-grid span { color: var(--muted); font-size: 11.5px; line-height: 1.55; display: block; }
264
- .toast { position: fixed; right: 20px; bottom: 20px; z-index: 130; background: #17202b; border: 1px solid #465467; color: #d5dce5; padding: 9px 12px; border-radius: 4px; font-size: 12.5px; opacity: 0; transform: translateY(8px); pointer-events: none; transition: .16s ease; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  .toast.show { opacity: 1; transform: translateY(0); }
266
 
 
 
 
 
 
267
  @media (max-width: 1100px) {
268
- .intro { grid-template-columns: 1fr; gap: 24px; }
269
- .workspace, .planner-grid { grid-template-columns: 1fr; }
270
- .controls-panel { position: static; }
 
 
 
 
271
  .metric-grid { grid-template-columns: repeat(3, 1fr); }
272
  .paper-grid { grid-template-columns: 1fr 1fr; }
273
  }
274
-
 
 
275
  @media (max-width: 720px) {
276
- .topbar { padding: 0 15px; }
277
- .subtitle { display: none; }
278
- .runtime-pill { max-width: 170px; }
279
- .shell { width: calc(100% - 20px); padding-top: 88px; }
280
  body.runtime-ready .shell { padding-top: 20px; }
281
- .intro { padding-top: 10px; }
282
- .project-facts div { grid-template-columns: 100px 1fr; }
283
- .field-grid.two, .chart-grid, .method-grid { grid-template-columns: 1fr; }
284
- .metric-grid, .metric-grid.four { grid-template-columns: 1fr 1fr; }
 
 
 
 
 
 
 
285
  .paper-grid { grid-template-columns: 1fr; }
286
  .panel-title-row { flex-direction: column; }
287
- .arena-title-row .compact { width: 100% !important; }
288
- .action-stack { width: 100%; align-items: stretch; }
 
 
289
  .wide-method { grid-column: auto; }
290
  .chart-card.chart-expanded { inset: 6px; }
291
  }
292
-
293
-
294
- /* Research-oriented readability and trace controls */
295
- .tab { font-size: 13.5px; }
296
- button.primary, button.secondary { font-size: 13.5px; }
297
- input[type="file"] {
298
- width: 100%; margin-top: 8px; color: #c4ccd7; font-size: 12.5px;
299
- border: 1px solid var(--line); background: #090e14; padding: 9px; border-radius: 4px;
300
  }
301
- input[type="file"]::file-selector-button {
302
- border: 1px solid #425168; background: #111925; color: #d4dbe4; padding: 7px 10px;
303
- margin-right: 10px; border-radius: 3px; cursor: pointer;
304
- }
305
- .trace-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 10px; color: #b8c1cc; font-size: 12.5px; }
306
- .control-help { margin: 10px 0 0; color: var(--muted); font-size: 12px; line-height: 1.55; }
307
- .research-grid { display: grid; grid-template-columns: 390px minmax(0, 1fr); gap: 14px; align-items: start; }
308
- .research-controls { padding: 20px; position: sticky; top: 16px; }
309
- .research-results { display: grid; gap: 14px; min-width: 0; }
310
- .research-panel { padding: 20px; }
311
- .research-secondary { width: 100%; margin-top: 10px; }
312
- .study-summary { border-left: 2px solid #516d92; background: #0a1017; padding: 11px 13px; color: #b9c3cf; font-size: 13px; line-height: 1.6; margin-bottom: 10px; }
313
- .study-summary strong { color: #e1e7ee; }
314
- .research-chart { height: 310px; }
315
- @media (max-width: 1100px) { .research-grid { grid-template-columns: 1fr; } .research-controls { position: static; } }
316
-
317
- /* Stateful agent-session experiments */
318
- .agent-layout { display: grid; grid-template-columns: 420px minmax(0, 1fr); gap: 14px; align-items: start; }
319
- .agent-controls { padding: 20px; position: sticky; top: 16px; }
320
- .agent-results { display: grid; gap: 14px; min-width: 0; }
321
- .metric-grid.six-agent { grid-template-columns: repeat(6, minmax(0, 1fr)); }
322
- @media (max-width: 1250px) { .metric-grid.six-agent { grid-template-columns: repeat(3, 1fr); } }
323
- @media (max-width: 1100px) { .agent-layout { grid-template-columns: 1fr; } .agent-controls { position: static; } }
324
- @media (max-width: 720px) { .metric-grid.six-agent { grid-template-columns: 1fr 1fr; } }
325
- .field-disabled { opacity: .55; }
326
- .metric-grid.eight-agent { grid-template-columns: repeat(4, minmax(0, 1fr)); }
327
- .stacked-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
328
- .stacked-actions .compact { min-width: 168px; }
329
- .horizontal-actions { margin: 10px 0 14px; justify-content: flex-start; }
330
- .predictive-controls { margin-top: 12px; }
331
- @media (max-width: 1250px) { .metric-grid.eight-agent { grid-template-columns: repeat(2, 1fr); } }
332
- @media (max-width: 960px) { .field-grid.four { grid-template-columns: 1fr 1fr; } }
333
- @media (max-width: 720px) { .metric-grid.eight-agent { grid-template-columns: 1fr 1fr; } .field-grid.four { grid-template-columns: 1fr; } .stacked-actions { width: 100%; } .stacked-actions .compact { width: 100% !important; } }
334
- .experiment-subsection { margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--line); }
 
1
  :root {
2
+ --bg: #080c11;
3
+ --surface: #0c1219;
4
+ --surface-raised: #101720;
5
+ --surface-inset: #090e14;
6
+ --line: #27313d;
7
+ --line-soft: #1c2530;
8
+ --text: #e3e8ee;
9
+ --text-soft: #c2cad4;
10
  --muted: #929dab;
11
+ --faint: #6f7b8a;
12
+ --accent: #6f96cf;
13
+ --accent-strong: #4d79b8;
14
+ --good: #68c394;
15
+ --danger: #df7d89;
16
+ --amber: #d2b36a;
17
+ --focus: rgba(111, 150, 207, 0.24);
18
  }
19
 
20
  * { box-sizing: border-box; }
 
23
  margin: 0;
24
  background: var(--bg);
25
  color: var(--text);
26
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
27
  font-size: 16px;
28
+ line-height: 1.45;
29
+ text-rendering: optimizeLegibility;
30
  }
31
+ button, input, select { font: inherit; }
32
+ button, a, input, select { -webkit-tap-highlight-color: transparent; }
33
+ button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible {
34
+ outline: 2px solid var(--accent);
35
+ outline-offset: 2px;
36
+ }
37
+
38
+ .skip-link {
39
+ position: fixed;
40
+ left: 12px;
41
+ top: 10px;
42
+ z-index: 200;
43
+ transform: translateY(-160%);
44
+ padding: 8px 10px;
45
+ background: var(--text);
46
+ color: var(--bg);
47
+ text-decoration: none;
48
+ }
49
+ .skip-link:focus { transform: translateY(0); }
50
 
51
+ /* Runtime header --------------------------------------------------------- */
52
  .topbar {
53
  position: fixed;
54
  inset: 0 0 auto 0;
55
  z-index: 80;
56
+ height: 58px;
57
  padding: 0 24px;
58
  display: flex;
59
  align-items: center;
60
  justify-content: space-between;
61
  gap: 16px;
62
+ background: rgba(8, 12, 17, 0.98);
63
  border-bottom: 1px solid var(--line);
64
+ transition: transform 150ms ease;
65
  }
66
+ body.runtime-ready .topbar { transform: translateY(-52px); }
67
  body.runtime-ready .topbar:hover,
68
  body.runtime-ready .topbar:focus-within { transform: translateY(0); }
69
+ .brand-wrap { min-width: 0; }
70
+ .brand { font-size: 14px; font-weight: 700; letter-spacing: -0.01em; }
 
71
  .runtime-pill {
72
  display: inline-flex;
73
  align-items: center;
74
+ gap: 8px;
75
  color: var(--muted);
76
+ font-size: 12px;
 
 
 
 
77
  white-space: nowrap;
78
  }
79
+ .runtime-pill .dot {
80
+ width: 7px;
81
+ height: 7px;
82
+ border-radius: 50%;
83
+ background: var(--amber);
84
+ }
85
  .runtime-pill.ready .dot { background: var(--good); }
86
  .runtime-pill.error .dot { background: var(--danger); }
87
 
88
+ /* Page frame ------------------------------------------------------------- */
89
  .shell {
90
+ width: min(1500px, calc(100% - 44px));
91
  margin: 0 auto;
92
+ padding: 86px 0 46px;
93
+ transition: padding-top 150ms ease;
94
  }
95
+ body.runtime-ready .shell { padding-top: 28px; }
96
 
97
  .intro {
98
+ padding: 13px 0 16px;
99
+ max-width: 1040px;
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
  .intro h1 {
102
+ margin: 0 0 10px;
 
103
  color: #edf1f5;
104
+ font-size: clamp(26px, 2.4vw, 34px);
105
+ line-height: 1.1;
106
+ letter-spacing: -0.035em;
107
  font-weight: 720;
108
  }
109
+ .intro p {
110
+ margin: 0;
111
+ max-width: 980px;
112
+ color: var(--muted);
113
+ font-size: 15px;
114
+ line-height: 1.65;
115
+ }
116
+ .intro-meta {
117
+ margin-top: 10px !important;
118
+ color: var(--faint) !important;
119
+ font-size: 12.5px !important;
120
+ letter-spacing: 0.005em;
121
+ }
122
  .reference-note {
123
+ margin: 2px 0 0;
124
+ padding: 10px 0;
125
+ border-top: 1px solid var(--line-soft);
126
+ border-bottom: 1px solid var(--line-soft);
127
+ color: #aaa596;
128
  font-size: 12.5px;
129
  line-height: 1.55;
130
  }
131
+ .reference-note strong { color: #c6bea9; }
132
 
133
+ /* Navigation ------------------------------------------------------------- */
134
  .tabs {
135
  display: flex;
136
+ gap: 0;
137
+ margin: 12px 0 18px;
138
+ padding: 0;
139
  overflow-x: auto;
140
+ border-bottom: 1px solid var(--line);
141
+ scrollbar-width: thin;
142
  }
143
  .tab {
144
+ flex: 0 0 auto;
145
  border: 0;
146
  border-bottom: 2px solid transparent;
 
147
  background: transparent;
148
+ color: var(--muted);
149
+ padding: 12px 11px 10px;
150
  cursor: pointer;
151
+ font-size: 13.5px;
152
+ font-weight: 620;
153
  white-space: nowrap;
154
  }
155
+ .tab:hover { color: var(--text-soft); }
156
  .tab.active { color: var(--text); border-bottom-color: var(--accent); }
157
  .tab-panel { display: none; }
158
  .tab-panel.active { display: block; }
159
 
160
+ /* Layout ---------------------------------------------------------------- */
161
+ .workspace,
162
+ .agent-layout,
163
+ .research-grid {
164
+ display: grid;
165
+ grid-template-columns: 410px minmax(0, 1fr);
166
+ gap: 16px;
167
+ align-items: start;
168
+ }
169
+ .planner-grid { grid-template-columns: 370px minmax(0, 1fr); }
170
+ .results-column,
171
+ .agent-results,
172
+ .research-results { min-width: 0; display: grid; gap: 14px; }
173
+
174
+ .panel {
175
+ background: var(--surface);
176
+ border: 1px solid var(--line);
177
+ border-radius: 2px;
178
+ }
179
+ .controls-panel,
180
+ .agent-controls,
181
+ .research-controls {
182
+ padding: 20px;
183
+ position: sticky;
184
+ top: 14px;
185
+ }
186
+ .result-panel,
187
+ .wide-panel,
188
+ .research-panel { padding: 20px; }
189
+ .panel-title-row {
190
+ display: flex;
191
+ align-items: flex-start;
192
+ justify-content: space-between;
193
+ gap: 18px;
194
+ margin-bottom: 16px;
195
+ }
196
+ .panel-title-row h2 {
197
+ margin: 0;
198
+ font-size: 19px;
199
+ line-height: 1.25;
200
+ letter-spacing: -0.018em;
201
+ font-weight: 700;
202
+ }
203
+ .panel-title-row p { margin: 5px 0 0; }
204
+
205
+ /* Small labels: intentionally plain, not eyebrow/kicker styling. */
206
+ .section-kicker {
207
+ margin: 0 0 9px;
208
+ color: #a4afbc;
209
+ font-size: 12.5px;
210
+ line-height: 1.3;
211
+ font-weight: 650;
212
+ letter-spacing: 0;
213
+ text-transform: none;
214
+ }
215
+ .panel-title-row .section-kicker { display: none; }
216
  .tag {
217
  display: inline-flex;
218
  align-items: center;
 
 
219
  color: var(--muted);
220
+ padding: 2px 0;
221
+ border: 0;
222
+ background: transparent;
223
  font-size: 11.5px;
224
  white-space: nowrap;
225
  }
226
+ .tag.good { color: var(--good); }
227
+ .tag.bad { color: var(--danger); }
228
 
229
+ /* Controls --------------------------------------------------------------- */
230
+ label {
231
+ display: block;
232
+ position: relative;
233
+ color: #b9c2cd;
234
+ font-size: 12.5px;
235
+ font-weight: 620;
236
+ }
237
+ select,
238
+ input[type="number"] {
239
  width: 100%;
 
240
  height: 40px;
241
+ margin-top: 7px;
242
  border: 1px solid var(--line);
243
+ border-radius: 2px;
244
+ background: var(--surface-inset);
245
  color: var(--text);
246
+ padding: 0 10px;
 
247
  outline: none;
248
  font-size: 14px;
249
  }
250
+ select:hover,
251
+ input[type="number"]:hover { border-color: #374352; }
252
+ select:focus,
253
+ input[type="number"]:focus {
254
+ border-color: var(--accent);
255
+ box-shadow: 0 0 0 2px var(--focus);
256
+ }
257
+ .unit {
258
+ position: absolute;
259
+ right: 9px;
260
+ bottom: 12px;
261
+ color: var(--faint);
262
+ font-size: 10.5px;
263
+ pointer-events: none;
264
+ }
265
+ .field-grid { display: grid; gap: 11px; margin-bottom: 11px; }
266
  .field-grid.two { grid-template-columns: 1fr 1fr; }
267
  .field-grid.four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
268
+ .subcontrols {
269
+ margin: 13px 0 5px;
270
+ padding: 13px;
271
+ border: 1px solid var(--line-soft);
272
+ background: var(--surface-inset);
273
+ }
274
  .subcontrols .section-kicker { margin-bottom: 10px; }
275
+ hr { border: 0; border-top: 1px solid var(--line-soft); margin: 18px 0; }
 
276
  .hidden { display: none !important; }
277
+ .field-disabled { opacity: 0.5; }
278
 
279
+ input[type="file"] {
280
+ width: 100%;
281
+ margin-top: 8px;
282
+ padding: 8px;
283
+ border: 1px solid var(--line);
284
+ border-radius: 2px;
285
+ background: var(--surface-inset);
286
+ color: var(--text-soft);
287
+ font-size: 12.5px;
288
+ }
289
+ input[type="file"]::file-selector-button {
290
+ margin-right: 10px;
291
+ padding: 7px 10px;
292
+ border: 1px solid #3b4858;
293
+ border-radius: 2px;
294
+ background: var(--surface-raised);
295
+ color: var(--text-soft);
296
+ cursor: pointer;
297
+ }
298
+ .trace-row {
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: space-between;
302
+ gap: 10px;
303
+ margin-top: 10px;
304
+ color: var(--text-soft);
305
+ font-size: 12.5px;
306
+ }
307
+ .control-help,
308
+ .tiny-note {
309
+ margin: 9px 0 0;
310
+ color: var(--muted);
311
+ font-size: 12px;
312
+ line-height: 1.55;
313
+ }
314
+
315
+ button.primary,
316
+ button.secondary {
317
+ min-height: 41px;
318
+ border-radius: 2px;
319
+ font-size: 13.5px;
320
+ font-weight: 680;
321
  cursor: pointer;
322
+ transition: background-color 110ms ease, border-color 110ms ease, color 110ms ease;
323
+ }
324
+ button.primary {
325
+ width: 100%;
326
+ margin-top: 11px;
327
+ border: 1px solid #6489c0;
328
+ background: var(--accent-strong);
329
+ color: #fff;
330
+ }
331
+ button.primary:hover:not(:disabled) { background: #5885c5; }
332
+ button.secondary {
333
+ border: 1px solid var(--line);
334
+ background: transparent;
335
+ color: var(--text-soft);
336
  }
 
 
 
337
  button.secondary:hover:not(:disabled),
338
  .mini-button:hover:not(:disabled),
339
  .chart-expand:hover,
340
+ .chart-download:hover {
341
+ border-color: #465466;
342
+ background: var(--surface-raised);
343
+ color: var(--text);
344
+ }
345
+ button:disabled { opacity: 0.4; cursor: not-allowed; }
346
+ .compact {
347
+ width: auto !important;
348
+ min-width: 160px;
349
+ padding: 0 16px;
350
+ margin-top: 0 !important;
351
+ }
352
  .button-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
353
+ .action-stack,
354
+ .stacked-actions {
355
+ display: flex;
356
+ flex-wrap: wrap;
357
+ justify-content: flex-end;
358
+ align-items: flex-end;
359
+ gap: 8px;
360
+ }
361
+ .stacked-actions .compact { min-width: 166px; }
362
+ .horizontal-actions { margin: 10px 0 14px; justify-content: flex-start; }
363
+ .checkline { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12.5px; font-weight: 600; }
364
  .checkline input { accent-color: var(--accent); }
365
 
366
+ /* Results ---------------------------------------------------------------- */
367
+ .empty-state {
368
+ min-height: 330px;
369
+ display: grid;
370
+ place-content: center;
371
+ padding: 28px;
372
+ text-align: center;
373
+ color: var(--muted);
374
+ }
375
+ .empty-state.small { min-height: 210px; }
376
+ .empty-state h3 { margin: 0 0 6px; color: var(--text-soft); font-size: 16px; font-weight: 650; }
377
+ .empty-state p { max-width: 620px; margin: 0; font-size: 13.5px; line-height: 1.6; }
378
+
379
+ /* Metrics read as a strip, not a grid of product cards. */
380
+ .metric-grid {
381
+ display: grid;
382
+ grid-template-columns: repeat(6, minmax(0, 1fr));
383
+ gap: 0;
384
+ margin-bottom: 13px;
385
+ border: 1px solid var(--line-soft);
386
+ background: var(--surface-inset);
387
+ }
388
+ .metric-grid.four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
389
+ .metric-grid.six-agent { grid-template-columns: repeat(6, minmax(0, 1fr)); }
390
+ .metric-grid.eight-agent { grid-template-columns: repeat(4, minmax(0, 1fr)); }
391
+ .metric {
392
+ min-height: 84px;
393
+ padding: 13px 14px;
394
+ border: 0;
395
+ border-right: 1px solid var(--line-soft);
396
+ background: transparent;
397
+ }
398
+ .metric:last-child { border-right: 0; }
399
+ .metric span { display: block; margin-bottom: 8px; color: var(--muted); font-size: 11.5px; }
400
+ .metric strong { font-size: 20px; line-height: 1.2; letter-spacing: -0.025em; }
401
  .metric strong.metric-small { font-size: 14.5px; line-height: 1.35; }
402
+ .metric.emphasis { box-shadow: inset 0 2px 0 var(--accent); background: rgba(111, 150, 207, 0.055); }
403
 
404
+ .diagnostic-card {
405
+ margin-bottom: 13px;
406
+ padding: 14px 15px;
407
+ border: 1px solid var(--line);
408
+ background: transparent;
409
+ }
410
  .diagnostic-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
411
+ .diagnostic-head span { color: var(--muted); font-size: 11.5px; }
412
  .diagnostic-head strong { font-size: 16px; }
413
+ .diagnostic-card p { margin: 8px 0 0; color: var(--text-soft); font-size: 13.5px; line-height: 1.55; }
414
  .diagnostic-card .diagnostic-action { color: var(--muted); }
415
+ .evidence-row { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 10px; }
416
+ .evidence-row span {
417
+ padding: 0;
418
+ border: 0;
419
+ background: transparent;
420
+ color: #8390a0;
421
+ font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
422
+ }
423
 
424
+ .study-summary {
425
+ margin-bottom: 11px;
426
+ padding: 9px 0 9px 12px;
427
+ border-left: 2px solid #526f96;
428
+ background: transparent;
429
+ color: #b9c3cf;
430
+ font-size: 13px;
431
+ line-height: 1.6;
432
+ }
433
+ .study-summary strong { color: var(--text); }
434
+ .warnings,
435
+ .planner-note {
436
+ margin-top: 10px;
437
+ padding: 9px 0 9px 11px;
438
+ border-left: 2px solid #756542;
439
+ background: transparent;
440
+ color: #bbb5a7;
441
+ font-size: 12.5px;
442
+ line-height: 1.55;
443
+ }
444
+ .planner-note { border-left-color: #496284; color: var(--muted); }
445
+ .muted { color: var(--muted); font-size: 13.5px; line-height: 1.55; }
446
+
447
+ /* Charts ----------------------------------------------------------------- */
448
+ .chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
449
+ .chart-card {
450
+ position: relative;
451
+ min-height: 300px;
452
+ border: 1px solid var(--line);
453
+ border-radius: 0;
454
+ background: var(--surface-inset);
455
+ }
456
  .chart-card.full { margin-top: 10px; }
457
+ .chart-head {
458
+ min-height: 43px;
459
+ display: flex;
460
+ align-items: center;
461
+ justify-content: space-between;
462
+ gap: 12px;
463
+ padding: 7px 11px 7px 13px;
464
+ border-bottom: 1px solid var(--line-soft);
465
+ }
466
+ .chart-title { color: var(--text-soft); font-size: 12.5px; font-weight: 650; }
467
  .chart-actions { display: flex; gap: 6px; }
468
+ .chart-expand,
469
+ .chart-download,
470
+ .mini-button {
471
+ height: 31px;
472
+ padding: 0 10px;
473
+ border: 1px solid var(--line);
474
+ border-radius: 2px;
475
+ background: transparent;
476
+ color: var(--muted);
477
+ cursor: pointer;
478
+ font-size: 11.5px;
479
+ }
480
+ .chart-body { height: 260px; padding: 11px 12px 12px; }
481
  .chart-body.large { height: 350px; }
482
+ .research-chart { height: 310px; }
483
+ .chart-card.chart-expanded {
484
+ position: fixed;
485
+ inset: 14px;
486
+ z-index: 100;
487
+ min-height: 0;
488
+ margin: 0;
489
+ border-color: #4a5665;
490
+ background: #080d13;
491
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.78);
492
+ }
493
+ .chart-card.chart-expanded .chart-body { height: calc(100vh - 78px); }
494
  body.chart-open { overflow: hidden; }
495
  canvas { width: 100% !important; height: 100% !important; max-height: none; }
496
 
497
+ /* Tables ----------------------------------------------------------------- */
498
+ .table-toolbar {
499
+ display: flex;
500
+ align-items: center;
501
+ justify-content: space-between;
502
+ gap: 12px;
503
+ margin-top: 16px;
504
+ color: #a8b2bf;
505
+ font-size: 12.5px;
506
+ }
507
  .table-toolbar > div { display: flex; gap: 6px; }
508
+ .table-wrap {
509
+ overflow: auto;
510
+ margin-top: 7px;
511
+ border-top: 1px solid var(--line);
512
+ border-bottom: 1px solid var(--line);
513
+ }
514
+ table { width: 100%; min-width: 900px; border-collapse: collapse; font-size: 13.5px; }
515
+ th,
516
+ td {
517
+ padding: 10px 11px;
518
+ text-align: right;
519
+ border-bottom: 1px solid var(--line-soft);
520
+ white-space: nowrap;
521
+ }
522
+ th:first-child,
523
+ td:first-child { text-align: left; }
524
+ th {
525
+ position: sticky;
526
+ top: 0;
527
+ z-index: 1;
528
+ background: #090e14;
529
+ color: #95a1af;
530
+ font-size: 11.5px;
531
+ font-weight: 650;
532
+ text-transform: none;
533
+ letter-spacing: 0;
534
+ }
535
+ td { color: #c8d0da; }
536
  tbody tr:last-child td { border-bottom: 0; }
537
+ tbody tr:hover td { background: #0f161e; }
538
  .pass { color: var(--good); }
539
  .fail { color: var(--danger); }
540
+ .best-label,
541
+ .pareto-label {
542
+ display: inline;
543
+ margin-left: 7px;
544
+ padding: 0;
545
+ border: 0;
546
+ color: #9db6d8;
547
+ font-size: 11px;
548
+ text-transform: none;
549
+ }
550
 
551
+ /* Methodology: editorial layout instead of cards-in-cards. */
552
+ .method-grid {
553
+ display: grid;
554
+ grid-template-columns: 1fr 1fr;
555
+ column-gap: 34px;
556
+ row-gap: 0;
557
+ }
558
+ .prose.panel {
559
+ padding: 22px 0 24px;
560
+ border: 0;
561
+ border-top: 1px solid var(--line);
562
+ background: transparent;
563
+ }
564
+ .prose h2 { margin: 0 0 10px; font-size: 19px; letter-spacing: -0.015em; }
565
+ .prose p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.7; }
566
+ .prose code { color: #a9bfdc; }
567
+ .formula {
568
+ margin-top: 14px;
569
+ padding: 9px 11px;
570
+ border-left: 2px solid #435a79;
571
+ background: var(--surface-inset);
572
+ color: #b7c3d2;
573
+ font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
574
+ }
575
  .wide-method { grid-column: 1 / -1; }
576
+ .paper-grid {
577
+ display: grid;
578
+ grid-template-columns: repeat(3, minmax(0, 1fr));
579
+ gap: 0 22px;
580
+ margin-top: 18px;
581
+ }
582
+ .paper-grid > div {
583
+ padding: 11px 0 13px;
584
+ border: 0;
585
+ border-top: 1px solid var(--line-soft);
586
+ background: transparent;
587
+ }
588
+ .paper-grid strong { display: block; margin-bottom: 5px; font-size: 12.5px; }
589
+ .paper-grid span { display: block; color: var(--muted); font-size: 11.5px; line-height: 1.55; }
590
+
591
+ /* Experiment sections ---------------------------------------------------- */
592
+ .experiment-subsection { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--line); }
593
+ .predictive-controls { margin-top: 12px; }
594
+ .research-secondary { width: 100%; margin-top: 10px; }
595
+
596
+ .toast {
597
+ position: fixed;
598
+ right: 20px;
599
+ bottom: 20px;
600
+ z-index: 130;
601
+ max-width: min(480px, calc(100vw - 40px));
602
+ padding: 10px 12px;
603
+ border: 1px solid #465466;
604
+ border-radius: 2px;
605
+ background: #141c25;
606
+ color: #d6dde5;
607
+ font-size: 12.5px;
608
+ opacity: 0;
609
+ transform: translateY(8px);
610
+ pointer-events: none;
611
+ transition: opacity 140ms ease, transform 140ms ease;
612
+ }
613
+ .toast.error { border-color: #70424a; color: #efb0b8; }
614
  .toast.show { opacity: 1; transform: translateY(0); }
615
 
616
+ /* Responsive ------------------------------------------------------------- */
617
+ @media (max-width: 1250px) {
618
+ .metric-grid.six-agent { grid-template-columns: repeat(3, 1fr); }
619
+ .metric-grid.eight-agent { grid-template-columns: repeat(2, 1fr); }
620
+ }
621
  @media (max-width: 1100px) {
622
+ .workspace,
623
+ .planner-grid,
624
+ .agent-layout,
625
+ .research-grid { grid-template-columns: 1fr; }
626
+ .controls-panel,
627
+ .agent-controls,
628
+ .research-controls { position: static; }
629
  .metric-grid { grid-template-columns: repeat(3, 1fr); }
630
  .paper-grid { grid-template-columns: 1fr 1fr; }
631
  }
632
+ @media (max-width: 960px) {
633
+ .field-grid.four { grid-template-columns: 1fr 1fr; }
634
+ }
635
  @media (max-width: 720px) {
636
+ .topbar { padding: 0 14px; }
637
+ .runtime-pill { max-width: 180px; }
638
+ .shell { width: calc(100% - 20px); padding-top: 80px; }
 
639
  body.runtime-ready .shell { padding-top: 20px; }
640
+ .intro { padding-top: 8px; }
641
+ .tabs { margin-top: 8px; }
642
+ .field-grid.two,
643
+ .field-grid.four,
644
+ .chart-grid,
645
+ .method-grid { grid-template-columns: 1fr; }
646
+ .metric-grid,
647
+ .metric-grid.four,
648
+ .metric-grid.six-agent,
649
+ .metric-grid.eight-agent { grid-template-columns: 1fr 1fr; }
650
+ .metric { border-bottom: 1px solid var(--line-soft); }
651
  .paper-grid { grid-template-columns: 1fr; }
652
  .panel-title-row { flex-direction: column; }
653
+ .arena-title-row .compact,
654
+ .stacked-actions .compact { width: 100% !important; }
655
+ .action-stack,
656
+ .stacked-actions { width: 100%; align-items: stretch; }
657
  .wide-method { grid-column: auto; }
658
  .chart-card.chart-expanded { inset: 6px; }
659
  }
660
+ @media (prefers-reduced-motion: reduce) {
661
+ *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
 
 
 
 
 
 
662
  }