55 Commits

Author SHA1 Message Date
f2ff0faebd Document Stop-B end-to-end on dense 27B: the improving climb + no-regression
Real gpt-5.4 agentic loop raised per-GPU TP1 0.123 -> TP2 0.2925 -> TP4 1.0012 (8.1x),
each a correctly-diagnosed real gain; then a TP4 runtime tweak measured 0.942 < 1.00
and was correctly rejected (no regression). With the 30B run (validator stop + LLM-stop
veto), all Stop-B behaviors are now validated end-to-end. The SIGTERM-teardown fix was
validated in practice (clean engine teardown, no GPU leak on stop). Efficiency finding:
at scale=1.0, infeasible high-theta probes burn the 900s elapsed cap, so a practical
loop needs a lower cap; this is why the run was stopped after iter-4 rather than driven
to an explicit Stop-B firing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:07:00 +08:00
4a64196a99 Add 27B Stop-B agentic-loop config (harness-driven, GPUs 2-7)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:08:46 +08:00
b17b213575 Tear down the engine on SIGTERM instead of orphaning it
Killing `study tune` with a default SIGTERM skipped the finally blocks, leaving the
vLLM engine and its EngineCore workers (which inherit the AITUNER_* marker env) alive
on the GPUs — twice leaking GPU memory that needed a root reset. Install a SIGTERM
handler in run_trial that raises KeyboardInterrupt so _terminate_process_tree runs,
ignore SIGTERM during teardown so a second signal can't re-orphan it, and restore the
prior handler afterward. Main-thread-guarded; unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:08:06 +08:00
93ce339d61 Document 27B TP sweep: per-GPU rises sharply with TP (dense), opposite of MoE
Under the length-aware TTFT SLO (4s + L_in/8k), dense Qwen3.5-27B per-GPU throughput:
TP1=0.065, TP2=0.2925 (4.5x), TP4>=0.908 (>=14x, ceiling-saturated). TP1 is TPOT-bound
(one H20 can't decode a 27B under 50ms/token once batched); loosening TTFT didn't move
TP1, confirming TPOT is the binding constraint. Opposite of MoE 30B-A3B where TP1 was
best per-GPU. Validates the harness + length-aware SLO produce meaningful, non-saturated
measurements (TP1/TP2). TP4 saturated -> lower bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:54:40 +08:00
b1b74318f6 Pin 27B A/B to GPUs 2-7 (route around leaked GPU0/1 memory)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:01:22 +08:00
2fcaf80450 Wrap socket/timeout errors in HTTP client as HttpClientError
stream_chat_completion (and the LLM stream/chat paths) only caught HTTPError, so a
request exceeding request_timeout_s raised a raw TimeoutError mid-stream that escaped
_run_one_request (which only catches HttpClientError), propagated through the probe,
and crashed the whole trial ("failed: timed out"). A timed-out request is a failed
request (SLO miss), not a trial crash. Catch OSError (covers TimeoutError, URLError,
ConnectionError) after HTTPError and wrap it. Exposed by lowering request_timeout_s
to 180s on the 27B run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:58:28 +08:00
3541065675 Speed up 27B TP A/B: request_timeout 180s, search.high 0.125
The wide 0.5 range made TP1 (low-capacity) waste many infeasible high-theta probes,
and the 900s request timeout made overloaded probes drain hung requests for 15min
each. Cap drain at 180s and bound the search to where the boundaries actually are.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:40:42 +08:00
7678c7d5e8 Switch 27B TP A/B to length-aware TTFT SLO (4s + L_in/8k), widen search
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:35:23 +08:00
ed2bbe0323 Add linear_ms SLO rule (length-aware TTFT budget)
threshold_ms = intercept_ms + per_token_ms * input_tokens. Lets the TTFT target
scale with prefill work, e.g. "4s + L_in/8k" => intercept_ms=4000, per_token_ms=0.125
(4s base, +1s per 8k input tokens). slo + spec + test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:35:23 +08:00
77af4ded2a Flag Stop-B e2e per-GPU trajectory as non-benchmark (saturation + smoke regime)
The reported trajectory validates the Stop-B mechanics only. TP2-DP2/TP4 saturated
the trace ceiling (best_sampling_u~0.98) so their per-GPU peak is underestimated, and
the run used the smoke regime (scale=0.1 + 512 cap). The TP1>TP2 ordering may be real
for the small-active MoE but this run cannot establish it; the 27B TP A/B is the valid
follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:40:38 +08:00
4f45b546a1 Add 27B TP A/B (deterministic ground-truth: does TP2 beat TP1 per-GPU)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:39:54 +08:00
90c3eb51c8 Document Stop-B end-to-end validation (Phase 5)
Real gpt-5.4 agentic loop on Qwen3-30B-A3B/H20 with Stop-A enabled. Validates both
Stop-B paths: search-high-saturation (validator-authorized immediate stop) and
multi-iteration convergence. The TP1 baseline stays the per-GPU incumbent (2.90
req/s/GPU); TP/DP scaling raises raw throughput but lowers per-GPU efficiency and is
correctly never adopted (no regression). The Phase-4 authority model is exercised
live: a premature LLM stop is vetoed (validator_did_not_authorize_stop), then a later
justified stop is honored after the veto budget. EP launch-failures handled as
hard-negative evidence. Auditable reason chains throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:58:44 +08:00
0b6beafeb8 Phase 5: widen search.high to 1.0 to force multi-iteration Stop-B convergence
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:12:32 +08:00
d4aff81691 Add Stop-B end-to-end config (agentic loop, Stop-A enabled)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:05:39 +08:00
f31e9ccfd5 Record Stop-A boundary-guard A/B: correct verdict, ~38% replay saved
With the guard enabled the binary search recovers best sampling_u=0.078125
(rate 2.30 req/s), identical to the full-replay baseline. The guard fired on
exactly the one feasibility-knee probe (0.08594, re-measured full -> infeasible);
the other three probes truncated to ~45-50%. Net ~38% replay saved on the trial
with no peak-rate overestimate. Stop-A + boundary guard is safe to enable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:57:53 +08:00
03e556f0ab Add Stop-A ON config (adaptive_stop enabled + boundary guard) for A/B
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:25:24 +08:00
dfc823f972 Add Stop-A SLO-boundary guard
When a truncated probe's measured pass-rate lands within trace.adaptive_stop.
boundary_delta of the SLO target, re-measure on the full window and use that
verdict. Offered-L-C-A convergence cannot see engine-state drift in the window
tail, so a near-knee truncated verdict is untrustworthy (validated: prefix 0.96
vs full 0.946 at threshold 0.08594). The guard fires only on feasibility-knee
probes, so non-boundary probes keep the Stop-A saving. Default delta=0.02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:25:24 +08:00
9f52812753 Document Stop-A validation: calibration + GPU fidelity check
CPU calibration (chat vs coder) reproduces the paper's C-slowest ordering and
shows C-convergence difficulty is driven by signal noise (low-reuse chat) not
reuse magnitude. GPU fidelity check on Qwen3-30B-A3B: truncating at the L-C-A
convergence prefix saves ~52% replay (tau_c=0.90) with 3/4 probe verdicts
preserved; the one mismatch is a boundary false-positive at the feasibility knee
(prefix 0.96 vs full 0.946), caused by second-half engine-state drift the offered
L-C-A cannot see. Argues for revisiting the SLO-boundary guard before enabling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:03:16 +08:00
958739027a Fix Stop-A validation config: system vllm, cap max-model-len
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:22:48 +08:00
0f57ee96a9 Drop LLM endpoint from Stop-A full-data config (baseline-only run)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:19:46 +08:00
43125f48cf Address review of two-stop branch
- lca._prefix_profile: anchor the prefix window to the prefix's own first arrival
  so the A-rate is measured over the prefix span (matches the design intent;
  no-op for the 0-based canonical pipeline).
- cli study tune: label file-originated stops as file_proposal rather than
  llm_after_veto_budget (the veto never applies to file proposals).
- spec.AdaptiveStopSpec: reject stable_checks > max_checks (would make
  convergence undetectable and silently disable Stop-A).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:19:08 +08:00
3af1d84ac0 Add Stop-A full-data validation config (real-time replay, no cap)
A single-config baseline run with adaptive_stop disabled and replay_time_scale=1.0,
so per-request probe_details capture the full 600s window for offline analysis of
whether truncating at the L-C-A convergence prefix preserves the feasibility verdict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:15:12 +08:00
08e53fd897 Add Stop-A calibration script (CPU-only convergence curve)
Prints the offered-L-C-A convergence curve and the stop fraction at candidate
tau_c values for a raw trace window, to calibrate Stop-A thresholds and compare
how late C converges across workloads. No serving required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:10:02 +08:00
a8f903498d Add Stop-B authority: deterministic validator overrides LLM stop
Phase 4 of the two-stop work. The harness already pre-empts the LLM with
deterministic stops and guided probes, but an LLM-originated should_stop could
still end the loop while the validator saw remaining opportunity.

Add harness._stop_authority, exposed as context["stop_authority"], whose
`authorized` mirrors the deterministic harness stop decision and whose
`opportunity_remains` flags an open topology frontier or a high-value planned
candidate. In study tune, an LLM-originated should_stop is now honored only when
the validator authorizes it; an unauthorized stop is vetoed (bounded budget) so
the loop cannot converge prematurely on the agent's say-so. File- and
harness-originated stops are unaffected, and the stop reason chain is recorded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:45:14 +08:00
51a9e4a007 Add Stop-A: offered-L-C-A convergence early-stop for replay
Phase 2 of the two-stop work. The L-C-A vector is a deterministic function of the
trace's offered metadata, so the convergence of prefix-vs-full L-C-A (the paper's
Fig. 9 curve) can be computed up front rather than monitored live, with identical
result and no per-request overhead.

- lca.find_convergence_prefix: earliest arrival-ordered prefix whose L and A family
  similarities reach tau and the slow C family reaches the stricter tau_c for
  stable_checks consecutive checkpoints. Self-similarity uses the raw log-feature
  vector (same window -> identical per-dim spread; RobustScaler is reserved for the
  cross-window Stop-C). If C never converges it reports the full set, which is the
  C-gate: no early stop on a cold/under-warmed cache. The checkpoint sims double as
  Phase 3 calibration data.
- spec.AdaptiveStopSpec (trace.adaptive_stop), disabled by default until the
  thresholds are calibrated, so existing studies are unaffected.
- worker._adaptive_replay_set truncates each probe's replay to the convergence
  prefix and records a certificate (converged, fraction, family similarity) into
  probe history and probe_details. Offered request_rate at the threshold is
  unchanged; only wall-clock replay shrinks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:23:49 +08:00
0f15bbc3f1 Make the offered-load axis session-coherent
Phase 1 of the two-stop work. Subsampling the trace by per-request uniform score
broke multi-turn sessions (a kept turn-2 could lose its turn-1), which lowered the
realized KV-cache hit rate as offered load dropped — so the feasibility boundary
was measured on a workload with a different C than production, contradicting the
paper's scale-stationary L-C-A premise.

prepare_trace_windows now resolves each row's session root via the parent_chat_id
chain in a single streaming pass and assigns sampling_u per session, so thresholding
keeps or drops whole sessions and preserves intra-session prefix reuse. Rows whose
parent fell outside the span fall back to grouping under the parent id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:16:06 +08:00
6f8e3c95c1 Unify harness L-C-A on the canonical lca.WorkloadProfile
Phase 0 of the two-stop work. The prompt block labeled `workload_lca_profile`
previously re-derived L-C-A from summarize_window's ad-hoc percentiles, diverging
from the paper's 10-dim RobustScaler vector implemented in lca.py. Make that block
authoritative: build_harness_context now accepts an optional workload_profile and
renders the canonical 10-dim vector + per-family stats when present, falling back
to the legacy rendering only when no profile is supplied (direct unit-test calls).

Real call sites (study prompt/llm-propose/tune, run_baseline_then_llm) build the
profile via lca.build_study_workload_profile and pass it through build_prompt. The
heuristic regime classifiers keep reading window_summary; that is the heuristic
layer, distinct from the similarity metric.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:12:17 +08:00
8b4116fad0 Add reference paper and qwen27b tpot25 16-iter notes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:02:30 +08:00
27d1c8fa92 Add L-C-A workload profile metric and CLI profile commands
Implement the paper's 10-dimensional L-C-A workload feature vector
(RobustScaler-normalized, sim=exp(-||dz||)) in lca.py, and wire it into
`aituner profile window` / `aituner profile similarity`. Covered by tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:02:24 +08:00
984eb1f325 Document 8-GPU harness ablation results for qwen27b and qwen235b prefill
Add completed experiment results from dash0 runs after 2026-05-13:
- qwen27b chat 0-8k: harness +118.6% over no-harness (0.2696 vs 0.1233 req/s/GPU)
- qwen235b prefill TTFT 3s/6s/9s: harness +76.8% (0.3921 vs 0.2217 req/s/GPU)

Mark old 7-GPU and pre-5/13 docs as superseded. Update implementation
log with completed run status.
2026-05-16 21:23:16 +08:00
d0c89dac48 Clean marked trial engine processes 2026-05-16 15:51:04 +08:00
cf9b8b3f68 Clean vLLM process groups after parent exit 2026-05-16 14:52:05 +08:00
5a879a8592 Fix decode harness partial probe handling 2026-05-16 14:18:07 +08:00
f18765b235 Document eight-GPU harness rerun 2026-05-13 09:04:14 +08:00
5c2958e6c1 Constrain harness topology by visible GPUs 2026-05-13 01:25:31 +08:00
fb6d74a18c Document harness v2 rerun criteria 2026-05-12 22:23:12 +08:00
e3ed775afd Fix harness SLO early-stop diagnosis 2026-05-12 22:20:01 +08:00
ef359c8eea Document profile-driven harness run 2026-05-12 21:40:19 +08:00
17e9681ca0 Add profile-driven harness planner 2026-05-12 21:28:44 +08:00
63d6a111f4 Document profile-driven harness design 2026-05-12 21:09:29 +08:00
2d03b1cd4c Add SLO-driven topology frontier harness guard 2026-05-12 21:00:49 +08:00
e1125475ae Minimize no-harness ablation prompt 2026-05-12 09:42:53 +08:00
ae756600ce Support full-range and incumbent-floor search modes 2026-05-11 12:58:46 +08:00
8516cd88c0 Use full search range for every trial 2026-05-11 12:50:22 +08:00
14259fcec9 Measure lower-range performance for infeasible trials 2026-05-10 14:30:34 +08:00
bf7c02e721 Clarify qwen27b raw per-iteration performance 2026-05-10 14:24:10 +08:00
b0325ecfd9 Clarify qwen235b raw per-iteration performance 2026-05-10 14:21:49 +08:00
4cfd3757b6 Document qwen235b prefill harness ablation 2026-05-10 13:05:49 +08:00
bdb08f6edc Handle missing streamed token metrics 2026-05-10 02:40:00 +08:00
307e2eb0e8 Document qwen27b harness ablation 2026-05-10 01:12:21 +08:00
adc4351e5d Report latency stats for infeasible baseline 2026-05-08 11:10:34 +08:00
eb137a0b62 Document TPOT40 baseline infeasible run 2026-05-08 02:57:03 +08:00
f212673f44 Stop tuning when baseline is infeasible 2026-05-08 01:07:36 +08:00
a7a5e9ad80 Make tune trial budget resumable 2026-05-07 17:18:06 +08:00
7263587cb6 clean: ci 2026-05-06 22:56:53 +08:00
37 changed files with 6685 additions and 147 deletions

View File

@@ -1,25 +0,0 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.11"
- "3.12"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: python -m pip install -e .
- name: Test
run: python -m unittest discover -s tests -v

View File

@@ -0,0 +1,177 @@
{
"study_id": "dash0-qwen27b-stopB-loop-chat-0-8k",
"hardware": {
"gpu_count": 8,
"gpu_model": "H20",
"host_candidates": [
"dash0"
]
},
"model": {
"model_id": "qwen3.5-27b-256k-0223-internal",
"served_model_name": "qwen35-27b-aituner"
},
"engine": {
"engine_name": "vllm",
"engine_version": "latest-release-on-dash0",
"exec_path": "/usr/local/bin/vllm",
"cwd": "/home/admin/cpfs/wjh/aituner/aituner",
"host": "127.0.0.1",
"port": 18082,
"healthcheck_path": "/v1/models",
"ready_timeout_s": 900,
"request_timeout_s": 180,
"launch_args": [
"serve",
"/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal"
],
"base_envs": {
"VLLM_DISABLE_COMPILE_CACHE": "1",
"DS_LLM_IGNORE_WARMUP": "1",
"DS_LLM_IGNORE_CHECK_WARMUP": "1",
"VLLM_ENABLE_MODEL_RUNNER_WARMUP": "1",
"VLLM_GDN_USE_FUSED_QKVZBA_KERNEL": "0",
"PARAM_TOTAL_MAX": "262144",
"PARAM_IN_LENGTH_MAX": "262144",
"PARAM_MAX_LENGTH_MAX": "131072",
"DS_LLM_MAX_THINK_TOKENS": "81920",
"DS_LLM_GRACEFUL_SHUTDOWN_WAIT_SECONDS": "600",
"VLLM_FP8_USE_BLADNN": "1",
"VLLM_MOE_USE_BLADNN": "1",
"VLLM_GDN_USE_BLADNN": "0",
"VLLM_USE_V1": "1",
"VLLM_IS_HYBRID_MODEL": "1",
"VLLM_ENABLE_TORCH_COMPILE": "1",
"VLLM_ATTENTION_BACKEND": "FLASH_ATTN",
"VLLM_QUANTIZE_ROUTED_EXPERTS_ONLY": "1",
"VLLM_USE_FLASHINFER_SAMPLER": "0",
"VLLM_DP_MASTER_PORT": "9528",
"VLLM_RESPONSE_TIMEOUT": "300",
"VLLM_LOG_REQ_KV_LENS": "1",
"DS_LLM_GRACEFUL_SHUTDOWN_KEEP_SECONDS": "600",
"CUDA_VISIBLE_DEVICES": "2,3,4,5,6,7"
},
"base_flags": {
"host": "127.0.0.1",
"port": 18082,
"served-model-name": "qwen35-27b-aituner",
"trust-remote-code": true,
"dtype": "bfloat16",
"gpu-memory-utilization": 0.9,
"enable-prefix-caching": true,
"mamba-cache-mode": "light",
"distributed-executor-backend": "mp",
"block-size": 64,
"enable-chunked-prefill": true,
"max-num-batched-tokens": 8192,
"disable-cascade-attn": true,
"max-model-len": 262144,
"speculative-config": "{\"method\":\"qwen3_next_vl_mtp\",\"num_speculative_tokens\":3}",
"mm-processor-cache-gb": 0,
"limit-mm-per-prompt": "{\"image\":256,\"video\":64}",
"compilation-config": "{\"cudagraph_mode\":\"FULL_AND_PIECEWISE\",\"use_inductor\":false,\"pass_config\":{\"fuse_norm_quant\":false,\"fuse_act_quant\":false,\"fuse_attn_quant\":false}}",
"mamba-cache-dtype": "float32",
"skip-mm-profiling": true,
"quantization": "fp8",
"tensor-parallel-size": 1,
"disable-log-requests": true
},
"tunable_envs": [
"VLLM_ENABLE_TORCH_COMPILE"
],
"tunable_flags": [
"tensor-parallel-size",
"data-parallel-size",
"expert-parallel-size",
"gpu-memory-utilization",
"block-size",
"max-num-batched-tokens",
"max-num-seqs",
"enable-prefix-caching",
"enable-chunked-prefill"
],
"topology_constraints": {
"require_tp_dp_product_equals_gpu_count": false,
"require_ep_size_leq_tp_dp_product": true,
"require_ep_size_divides_tp_dp_product": true,
"require_enable_expert_parallel_when_ep_gt_one": true,
"validate_cuda_graph_sizes_divisible_by_tp_when_tp_ep_reduce_scatter": true,
"allowed_tp_dp_products": [
1,
2,
4,
8
],
"allowed_tensor_parallel_sizes": [
1,
2,
4,
8
],
"allowed_data_parallel_sizes": [
1,
2,
4,
8
],
"allowed_expert_parallel_sizes": [
1
]
},
"python_executable": "python3"
},
"trace": {
"windows_path": "/home/admin/cpfs/wjh/aituner/aituner/trace_windows/windows.json",
"window_id": "chat_w20260311_1000",
"u_field": "sampling_u",
"timestamp_field": "timestamp",
"max_concurrency": 32,
"input_length_filter": {
"min_input_tokens": 0,
"max_input_tokens": 8192
},
"replay_time_scale": 1.0,
"early_stop_max_lag_s": 120.0,
"early_stop_max_elapsed_s": 900.0,
"adaptive_stop": {
"enabled": true,
"tau": 0.9,
"tau_c": 0.9,
"stable_checks": 3,
"max_checks": 20,
"min_fraction": 0.1,
"boundary_delta": 0.02
}
},
"slo": {
"target_pass_rate": 0.95,
"ttft_rule": {
"kind": "linear_ms",
"intercept_ms": 4000,
"per_token_ms": 0.125
},
"tpot_rule": {
"kind": "fixed_ms",
"threshold_ms": 50
}
},
"search": {
"low": 0.0,
"high": 0.25,
"tolerance": 0.001,
"max_probes": 6,
"sample_seed": 20260325,
"inherit_incumbent_floor": true
},
"llm": {
"system_prompt": "Propose a single engine config patch that increases the maximum feasible sampling_u under the SLO target. Favor launch-safe changes grounded in the incumbent result and only propose knobs that plausibly improve throughput above the incumbent request rate.",
"max_history_trials": 8,
"endpoint": {
"provider": "codex",
"model": "gpt-5.4",
"stream": true,
"api_key_env": "OPENAI_API_KEY",
"timeout_s": 180
}
}
}

View File

@@ -0,0 +1,176 @@
{
"study_id": "dash0-qwen27b-tp-ab-chat-0-8k",
"hardware": {
"gpu_count": 8,
"gpu_model": "H20",
"host_candidates": [
"dash0"
]
},
"model": {
"model_id": "qwen3.5-27b-256k-0223-internal",
"served_model_name": "qwen35-27b-aituner"
},
"engine": {
"engine_name": "vllm",
"engine_version": "latest-release-on-dash0",
"exec_path": "/usr/local/bin/vllm",
"cwd": "/home/admin/cpfs/wjh/aituner/aituner",
"host": "127.0.0.1",
"port": 18082,
"healthcheck_path": "/v1/models",
"ready_timeout_s": 900,
"request_timeout_s": 180,
"launch_args": [
"serve",
"/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal"
],
"base_envs": {
"VLLM_DISABLE_COMPILE_CACHE": "1",
"DS_LLM_IGNORE_WARMUP": "1",
"DS_LLM_IGNORE_CHECK_WARMUP": "1",
"VLLM_ENABLE_MODEL_RUNNER_WARMUP": "1",
"VLLM_GDN_USE_FUSED_QKVZBA_KERNEL": "0",
"PARAM_TOTAL_MAX": "262144",
"PARAM_IN_LENGTH_MAX": "262144",
"PARAM_MAX_LENGTH_MAX": "131072",
"DS_LLM_MAX_THINK_TOKENS": "81920",
"DS_LLM_GRACEFUL_SHUTDOWN_WAIT_SECONDS": "600",
"VLLM_FP8_USE_BLADNN": "1",
"VLLM_MOE_USE_BLADNN": "1",
"VLLM_GDN_USE_BLADNN": "0",
"VLLM_USE_V1": "1",
"VLLM_IS_HYBRID_MODEL": "1",
"VLLM_ENABLE_TORCH_COMPILE": "1",
"VLLM_ATTENTION_BACKEND": "FLASH_ATTN",
"VLLM_QUANTIZE_ROUTED_EXPERTS_ONLY": "1",
"VLLM_USE_FLASHINFER_SAMPLER": "0",
"VLLM_DP_MASTER_PORT": "9528",
"VLLM_RESPONSE_TIMEOUT": "300",
"VLLM_LOG_REQ_KV_LENS": "1",
"DS_LLM_GRACEFUL_SHUTDOWN_KEEP_SECONDS": "600",
"CUDA_VISIBLE_DEVICES": "2,3,4,5,6,7"
},
"base_flags": {
"host": "127.0.0.1",
"port": 18082,
"served-model-name": "qwen35-27b-aituner",
"trust-remote-code": true,
"dtype": "bfloat16",
"gpu-memory-utilization": 0.9,
"enable-prefix-caching": true,
"mamba-cache-mode": "light",
"distributed-executor-backend": "mp",
"block-size": 64,
"enable-chunked-prefill": true,
"max-num-batched-tokens": 8192,
"disable-cascade-attn": true,
"max-model-len": 262144,
"speculative-config": "{\"method\":\"qwen3_next_vl_mtp\",\"num_speculative_tokens\":3}",
"mm-processor-cache-gb": 0,
"limit-mm-per-prompt": "{\"image\":256,\"video\":64}",
"compilation-config": "{\"cudagraph_mode\":\"FULL_AND_PIECEWISE\",\"use_inductor\":false,\"pass_config\":{\"fuse_norm_quant\":false,\"fuse_act_quant\":false,\"fuse_attn_quant\":false}}",
"mamba-cache-dtype": "float32",
"skip-mm-profiling": true,
"quantization": "fp8",
"tensor-parallel-size": 1,
"disable-log-requests": true
},
"tunable_envs": [
"VLLM_ENABLE_TORCH_COMPILE"
],
"tunable_flags": [
"tensor-parallel-size",
"data-parallel-size",
"expert-parallel-size",
"gpu-memory-utilization",
"block-size",
"max-num-batched-tokens",
"max-num-seqs",
"enable-prefix-caching",
"enable-chunked-prefill"
],
"topology_constraints": {
"require_tp_dp_product_equals_gpu_count": false,
"require_ep_size_leq_tp_dp_product": true,
"require_ep_size_divides_tp_dp_product": true,
"require_enable_expert_parallel_when_ep_gt_one": true,
"validate_cuda_graph_sizes_divisible_by_tp_when_tp_ep_reduce_scatter": true,
"allowed_tp_dp_products": [
1,
2,
4,
8
],
"allowed_tensor_parallel_sizes": [
1,
2,
4,
8
],
"allowed_data_parallel_sizes": [
1,
2,
4,
8
],
"allowed_expert_parallel_sizes": [
1
]
},
"python_executable": "python3"
},
"trace": {
"windows_path": "/home/admin/cpfs/wjh/aituner/aituner/trace_windows/windows.json",
"window_id": "chat_w20260311_1000",
"u_field": "sampling_u",
"timestamp_field": "timestamp",
"max_concurrency": 32,
"input_length_filter": {
"min_input_tokens": 0,
"max_input_tokens": 8192
},
"replay_time_scale": 1.0,
"early_stop_max_lag_s": 120.0,
"early_stop_max_elapsed_s": 900.0,
"adaptive_stop": {
"enabled": true,
"tau": 0.9,
"tau_c": 0.9,
"stable_checks": 3,
"max_checks": 20,
"min_fraction": 0.1,
"boundary_delta": 0.02
}
},
"slo": {
"target_pass_rate": 0.95,
"ttft_rule": {
"kind": "linear_ms",
"intercept_ms": 4000,
"per_token_ms": 0.125
},
"tpot_rule": {
"kind": "fixed_ms",
"threshold_ms": 50
}
},
"search": {
"low": 0.0,
"high": 0.125,
"tolerance": 0.001,
"max_probes": 6,
"sample_seed": 20260325
},
"llm": {
"system_prompt": "Propose a single engine config patch that increases the maximum feasible sampling_u under the SLO target. Favor launch-safe changes grounded in the incumbent result and only propose knobs that plausibly improve throughput above the incumbent request rate.",
"max_history_trials": 8,
"endpoint": {
"provider": "codex",
"model": "gpt-5.4",
"stream": true,
"api_key_env": "OPENAI_API_KEY",
"timeout_s": 180
}
}
}

View File

@@ -0,0 +1,138 @@
{
"study_id": "dash0-qwen30b-a3b-stopA-fulldata-chat-0-8k",
"hardware": {
"gpu_count": 8,
"gpu_model": "H20",
"host_candidates": [
"dash0"
]
},
"model": {
"model_id": "Qwen/Qwen3-30B-A3B",
"served_model_name": "qwen3-30b-a3b-community"
},
"engine": {
"engine_name": "vllm",
"engine_version": "0.20.0",
"exec_path": "/usr/local/bin/vllm",
"cwd": "/home/admin/cpfs/wjh/aituner/aituner",
"host": "127.0.0.1",
"port": 18230,
"healthcheck_path": "/v1/models",
"ready_timeout_s": 900,
"request_timeout_s": 900,
"launch_args": [
"serve",
"/home/admin/cpfs/wjh/models/Qwen/Qwen3-30B-A3B"
],
"base_envs": {
"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7",
"HOME": "/tmp/wjh",
"XDG_CACHE_HOME": "/tmp/wjh/.cache"
},
"base_flags": {
"host": "127.0.0.1",
"port": 18230,
"served-model-name": "qwen3-30b-a3b-community",
"gpu-memory-utilization": 0.9,
"max-model-len": 16384,
"trust-remote-code": true,
"enable-prefix-caching": true
},
"tunable_envs": [],
"tunable_flags": [
"tensor-parallel-size",
"data-parallel-size",
"enable-expert-parallel",
"expert-parallel-size",
"gpu-memory-utilization",
"max-num-batched-tokens",
"max-num-seqs",
"block-size",
"enable-prefix-caching",
"enable-chunked-prefill"
],
"topology_constraints": {
"require_tp_dp_product_equals_gpu_count": false,
"require_ep_size_leq_tp_dp_product": true,
"require_ep_size_divides_tp_dp_product": true,
"require_enable_expert_parallel_when_ep_gt_one": true,
"validate_cuda_graph_sizes_divisible_by_tp_when_tp_ep_reduce_scatter": true,
"allowed_tp_dp_products": [
1,
2,
4,
8
],
"allowed_tensor_parallel_sizes": [
1,
2,
4,
8
],
"allowed_data_parallel_sizes": [
1,
2,
4,
8
],
"allowed_expert_parallel_sizes": [
1,
2,
4,
8
]
},
"python_executable": "/tmp/wjh/venvs/vllm-0.20.0-cu129/bin/python"
},
"trace": {
"windows_path": "/home/admin/cpfs/wjh/aituner/aituner/trace_windows/windows.json",
"window_id": "chat_w20260311_1000",
"completion_tokens_override": 128,
"u_field": "sampling_u",
"timestamp_field": "timestamp",
"max_concurrency": 64,
"input_length_filter": {
"min_input_tokens": 0,
"max_input_tokens": 8192
},
"replay_time_scale": 1.0,
"early_stop_max_lag_s": 120.0,
"early_stop_max_elapsed_s": 900.0
},
"slo": {
"target_pass_rate": 0.95,
"ttft_rule": {
"kind": "step_ms",
"buckets": [
{
"max_input_tokens": 4096,
"threshold_ms": 2000
},
{
"max_input_tokens": 32768,
"threshold_ms": 4000
},
{
"threshold_ms": 6000
}
]
},
"tpot_rule": {
"kind": "fixed_ms",
"threshold_ms": 50
}
},
"search": {
"low": 0.0,
"high": 0.125,
"tolerance": 0.001,
"max_probes": 4,
"sample_seed": 20260325
},
"llm": {
"system_prompt": "Tune community vLLM 0.20.0 serving for Qwen3-30B-A3B. Start from the default vLLM engine configuration, use only launch-safe patches, and optimize request_rate_per_gpu under the configured SLO.",
"max_history_trials": 8,
"use_harness": false
}
}

View File

@@ -0,0 +1,147 @@
{
"study_id": "dash0-qwen30b-a3b-stopA-on-chat-0-8k",
"hardware": {
"gpu_count": 8,
"gpu_model": "H20",
"host_candidates": [
"dash0"
]
},
"model": {
"model_id": "Qwen/Qwen3-30B-A3B",
"served_model_name": "qwen3-30b-a3b-community"
},
"engine": {
"engine_name": "vllm",
"engine_version": "0.20.0",
"exec_path": "/usr/local/bin/vllm",
"cwd": "/home/admin/cpfs/wjh/aituner/aituner",
"host": "127.0.0.1",
"port": 18230,
"healthcheck_path": "/v1/models",
"ready_timeout_s": 900,
"request_timeout_s": 900,
"launch_args": [
"serve",
"/home/admin/cpfs/wjh/models/Qwen/Qwen3-30B-A3B"
],
"base_envs": {
"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7",
"HOME": "/tmp/wjh",
"XDG_CACHE_HOME": "/tmp/wjh/.cache"
},
"base_flags": {
"host": "127.0.0.1",
"port": 18230,
"served-model-name": "qwen3-30b-a3b-community",
"gpu-memory-utilization": 0.9,
"max-model-len": 16384,
"trust-remote-code": true,
"enable-prefix-caching": true
},
"tunable_envs": [],
"tunable_flags": [
"tensor-parallel-size",
"data-parallel-size",
"enable-expert-parallel",
"expert-parallel-size",
"gpu-memory-utilization",
"max-num-batched-tokens",
"max-num-seqs",
"block-size",
"enable-prefix-caching",
"enable-chunked-prefill"
],
"topology_constraints": {
"require_tp_dp_product_equals_gpu_count": false,
"require_ep_size_leq_tp_dp_product": true,
"require_ep_size_divides_tp_dp_product": true,
"require_enable_expert_parallel_when_ep_gt_one": true,
"validate_cuda_graph_sizes_divisible_by_tp_when_tp_ep_reduce_scatter": true,
"allowed_tp_dp_products": [
1,
2,
4,
8
],
"allowed_tensor_parallel_sizes": [
1,
2,
4,
8
],
"allowed_data_parallel_sizes": [
1,
2,
4,
8
],
"allowed_expert_parallel_sizes": [
1,
2,
4,
8
]
},
"python_executable": "/tmp/wjh/venvs/vllm-0.20.0-cu129/bin/python"
},
"trace": {
"windows_path": "/home/admin/cpfs/wjh/aituner/aituner/trace_windows/windows.json",
"window_id": "chat_w20260311_1000",
"completion_tokens_override": 128,
"u_field": "sampling_u",
"timestamp_field": "timestamp",
"max_concurrency": 64,
"input_length_filter": {
"min_input_tokens": 0,
"max_input_tokens": 8192
},
"replay_time_scale": 1.0,
"early_stop_max_lag_s": 120.0,
"early_stop_max_elapsed_s": 900.0,
"adaptive_stop": {
"enabled": true,
"tau": 0.9,
"tau_c": 0.9,
"stable_checks": 3,
"max_checks": 20,
"min_fraction": 0.1,
"boundary_delta": 0.02
}
},
"slo": {
"target_pass_rate": 0.95,
"ttft_rule": {
"kind": "step_ms",
"buckets": [
{
"max_input_tokens": 4096,
"threshold_ms": 2000
},
{
"max_input_tokens": 32768,
"threshold_ms": 4000
},
{
"threshold_ms": 6000
}
]
},
"tpot_rule": {
"kind": "fixed_ms",
"threshold_ms": 50
}
},
"search": {
"low": 0.0,
"high": 0.125,
"tolerance": 0.001,
"max_probes": 4,
"sample_seed": 20260325
},
"llm": {
"system_prompt": "Tune community vLLM 0.20.0 serving for Qwen3-30B-A3B. Start from the default vLLM engine configuration, use only launch-safe patches, and optimize request_rate_per_gpu under the configured SLO.",
"max_history_trials": 8,
"use_harness": false
}
}

View File

@@ -0,0 +1,155 @@
{
"study_id": "dash0-qwen30b-a3b-stopB-e2e-hi-chat-0-8k",
"hardware": {
"gpu_count": 8,
"gpu_model": "H20",
"host_candidates": [
"dash0"
]
},
"model": {
"model_id": "Qwen/Qwen3-30B-A3B",
"served_model_name": "qwen3-30b-a3b-community"
},
"engine": {
"engine_name": "vllm",
"engine_version": "0.20.0",
"exec_path": "/usr/local/bin/vllm",
"cwd": "/home/admin/cpfs/wjh/aituner/aituner",
"host": "127.0.0.1",
"port": 18230,
"healthcheck_path": "/v1/models",
"ready_timeout_s": 900,
"request_timeout_s": 900,
"launch_args": [
"serve",
"/home/admin/cpfs/wjh/models/Qwen/Qwen3-30B-A3B"
],
"base_envs": {
"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7",
"HOME": "/tmp/wjh",
"XDG_CACHE_HOME": "/tmp/wjh/.cache"
},
"base_flags": {
"host": "127.0.0.1",
"port": 18230,
"served-model-name": "qwen3-30b-a3b-community",
"gpu-memory-utilization": 0.9,
"max-model-len": 16384,
"trust-remote-code": true,
"enable-prefix-caching": true
},
"tunable_envs": [],
"tunable_flags": [
"tensor-parallel-size",
"data-parallel-size",
"enable-expert-parallel",
"expert-parallel-size",
"gpu-memory-utilization",
"max-num-batched-tokens",
"max-num-seqs",
"block-size",
"enable-prefix-caching",
"enable-chunked-prefill"
],
"topology_constraints": {
"require_tp_dp_product_equals_gpu_count": false,
"require_ep_size_leq_tp_dp_product": true,
"require_ep_size_divides_tp_dp_product": true,
"require_enable_expert_parallel_when_ep_gt_one": true,
"validate_cuda_graph_sizes_divisible_by_tp_when_tp_ep_reduce_scatter": true,
"allowed_tp_dp_products": [
1,
2,
4,
8
],
"allowed_tensor_parallel_sizes": [
1,
2,
4,
8
],
"allowed_data_parallel_sizes": [
1,
2,
4,
8
],
"allowed_expert_parallel_sizes": [
1,
2,
4,
8
]
},
"python_executable": "/tmp/wjh/venvs/vllm-0.20.0-cu129/bin/python"
},
"trace": {
"windows_path": "/home/admin/cpfs/wjh/aituner/aituner/trace_windows/windows.json",
"window_id": "chat_w20260311_1000",
"completion_tokens_override": 128,
"u_field": "sampling_u",
"timestamp_field": "timestamp",
"max_concurrency": 64,
"input_length_filter": {
"min_input_tokens": 0,
"max_input_tokens": 8192
},
"max_requests_per_probe": 512,
"replay_time_scale": 0.1,
"early_stop_max_lag_s": 120.0,
"early_stop_max_elapsed_s": 900.0,
"adaptive_stop": {
"enabled": true,
"tau": 0.9,
"tau_c": 0.9,
"stable_checks": 3,
"max_checks": 20,
"min_fraction": 0.1,
"boundary_delta": 0.02
}
},
"slo": {
"target_pass_rate": 0.95,
"ttft_rule": {
"kind": "step_ms",
"buckets": [
{
"max_input_tokens": 4096,
"threshold_ms": 2000
},
{
"max_input_tokens": 32768,
"threshold_ms": 4000
},
{
"threshold_ms": 6000
}
]
},
"tpot_rule": {
"kind": "fixed_ms",
"threshold_ms": 50
}
},
"search": {
"low": 0.0,
"high": 1.0,
"tolerance": 0.001,
"max_probes": 6,
"sample_seed": 20260325
},
"llm": {
"system_prompt": "Tune community vLLM 0.20.0 serving for Qwen3-30B-A3B. Start from the default vLLM engine configuration, use only launch-safe patches, and optimize request_rate_per_gpu under the configured SLO.",
"max_history_trials": 8,
"use_harness": true,
"endpoint": {
"provider": "codex",
"model": "gpt-5.4",
"stream": true,
"api_key_env": "OPENAI_API_KEY",
"timeout_s": 240
}
}
}

View File

@@ -0,0 +1,13 @@
{
"observation": "baseline TP1 (deployed flags)",
"diagnosis": "deterministic TP A/B point",
"config_patch": {
"env_patch": {},
"flag_patch": {}
},
"expected_effects": [
"measure peak request_rate_per_gpu at this topology"
],
"why_not_previous_failures": "n/a",
"should_stop": false
}

View File

@@ -0,0 +1,15 @@
{
"observation": "TP2",
"diagnosis": "deterministic TP A/B point",
"config_patch": {
"env_patch": {},
"flag_patch": {
"tensor-parallel-size": 2
}
},
"expected_effects": [
"measure peak request_rate_per_gpu at this topology"
],
"why_not_previous_failures": "n/a",
"should_stop": false
}

View File

@@ -0,0 +1,15 @@
{
"observation": "TP4",
"diagnosis": "deterministic TP A/B point",
"config_patch": {
"env_patch": {},
"flag_patch": {
"tensor-parallel-size": 4
}
},
"expected_effects": [
"measure peak request_rate_per_gpu at this topology"
],
"why_not_previous_failures": "n/a",
"should_stop": false
}

View File

@@ -0,0 +1,250 @@
# Profile-Driven Harness Design
## Problem
The current harness is too rule-like. It can help in a narrow setup, but it can also overfit to a previous observation such as "TP=2 is enough" and then fail when the SLO or workload changes. Adding one more rule for each failure mode is not acceptable.
The harness should make AITuner behave more like a performance engineer:
1. Profile the workload and engine behavior.
2. Diagnose the active bottleneck from measurements.
3. Form hypotheses about which knob families can relieve that bottleneck.
4. Choose the next experiment that best disambiguates or improves the system.
5. Interpret the result and update the diagnosis.
6. Stop only when the measured search space is exhausted or the remaining hypotheses have low value.
The harness should not encode a fixed answer for qwen27b, TP=4, TPOT25, or any testcase-specific threshold.
## Design Principles
### 1. Harness is an evidence system, not a prompt trick
The harness should provide structured evidence and a decision protocol. It should not simply add more natural-language hints to the prompt.
The output should include:
- `profile`: measured workload and engine traits.
- `bottleneck_diagnosis`: ranked bottleneck hypotheses with evidence.
- `candidate_actions`: candidate config changes with predicted effect and risk.
- `experiment_plan`: the next config to test, why it is informative, and what outcome would confirm/refute the hypothesis.
- `stop_decision`: only if no useful experiment remains under the current measurement budget.
### 2. Separate profiling from tuning
AITuner currently learns mostly from full trial results. That is too coarse. The harness needs a profile layer that extracts bottleneck signals from every probe:
- workload profile: input/output token distributions, cache reuse, burstiness, selected-request count per sampling threshold;
- latency profile: TTFT and TPOT mean/p50/p95/p99 by threshold and optionally by input/output token bucket;
- engine profile: prefill/decode throughput, waiting/running request counts, KV cache usage, preemptions, GPU utilization if available;
- topology profile: TP/DP/EP product, per-GPU request rate, launch stability, memory pressure;
- measurement profile: search high/low/tolerance, which thresholds were measured, whether failures were early-stop artifacts or true low-load infeasibility.
This profile should be persisted per trial so later decisions do not depend on prompt memory.
### 3. Diagnose bottlenecks from counters and slopes
A professional tuning loop should not infer bottlenecks from a single failed reason. It should compare how metrics change across thresholds and configs.
Useful diagnostics:
- TTFT-bound: TTFT p95/p99 rises sharply with request rate, long prompts dominate failures, prefill throughput is saturated, decode TPOT is still healthy.
- TPOT-bound: TPOT p95/p99 fails while TTFT is acceptable, decode throughput or per-token latency dominates, high sequence concurrency or batching creates token latency tails.
- Admission/queueing-bound: waiting requests and arrival lag grow, both TTFT and TPOT may degrade, utilization may be high but service time per request is not the only issue.
- Memory/KV-bound: KV cache usage, preemptions, OOM/launch failures, or max model length/cache pressure limit throughput.
- Topology/communication-bound: increasing TP lowers per-request latency but may reduce per-GPU throughput; DP improves admission but may not reduce per-request latency.
The diagnosis should be a ranked list with confidence, not a single label:
```json
{
"bottlenecks": [
{
"name": "decode_tpot",
"confidence": 0.62,
"evidence": ["tpot_p95 fails at last infeasible probe", "ttft_p95 remains below SLO"]
},
{
"name": "prefill_ttft",
"confidence": 0.31,
"evidence": ["long prompt tail", "ttft_p99 grows near knee"]
}
]
}
```
### 4. Candidate actions come from a knob-effect model
The harness should maintain a generic model of knob effects, not case-specific rules.
Examples:
| Knob family | Expected effect | Main risks |
|---|---|---|
| Increase TP | lower per-request compute latency, may help TTFT/TPOT tails | communication overhead, lower per-GPU efficiency, fewer independent replicas |
| Increase DP | more replicas, better admission/queueing | does not reduce per-request compute latency, may waste memory |
| Increase EP | MoE expert distribution, possible decode/prefill balance | launch complexity, communication, only relevant with model/engine evidence |
| Lower max-num-seqs | reduce decode contention and tail TPOT | underutilization, lower throughput |
| Raise max-num-seqs | improve admission/concurrency | TPOT/TTFT tails, memory pressure |
| Raise max-num-batched-tokens | improve prefill batching and GPU occupancy | long-prefill HoL blocking, memory pressure |
| Lower max-num-batched-tokens | reduce long-prefill interference | underfilled prefill batches |
| Change block-size | cache fragmentation/allocator effects | launch/runtime instability, workload-specific |
The harness should score candidates with:
```text
score = expected_bottleneck_relief
+ information_gain
+ launch_safety
- regression_risk
- measurement_cost
```
This lets it choose between "exploit current incumbent" and "explore unmeasured topology" from evidence, not hardcoded order.
### 5. Use hypothesis-driven experiments
Each trial should answer a specific question.
Bad:
- "Try TP=4 because previous run needed TP=4."
Good:
- "At the current best TP=2, the last infeasible probe is TPOT-bound. Increasing TP to 4 should reduce per-token compute latency but may reduce per-GPU efficiency. This trial tests whether the SLO is compute-latency limited or topology-overhead limited."
Every candidate should have:
- hypothesis;
- expected metric movement;
- risk;
- confirm condition;
- reject condition;
- fallback next family.
### 6. Stop is a decision under measured search limits
Early stop should not mean "we found something good." It should mean:
- the configured `search.high` is saturated by a feasible incumbent; or
- the remaining high-value hypotheses are already measured or invalidated; or
- all remaining candidates have low expected information gain or high launch risk relative to the current best; or
- the baseline is infeasible even at the lowest load and the SLO is too tight.
For Fig18 raw mode, stop can be disabled or recorded as `stop` rather than filling remaining columns with best-so-far.
## Proposed Architecture
### Components
1. `Profiler`
- Input: trial result, probe history, engine logs/metrics if available, study spec.
- Output: `TrialProfile`.
2. `BottleneckAnalyzer`
- Input: recent `TrialProfile`s, workload profile, SLO.
- Output: ranked bottleneck hypotheses.
3. `KnobEffectModel`
- Generic mapping from bottleneck hypotheses to possible knob families.
- Contains no model-specific or SLO-specific constants.
4. `ExperimentPlanner`
- Generates candidate config patches.
- Scores candidates by expected relief, information gain, risk, and cost.
- Emits the next experiment and rationale.
5. `StopPolicy`
- Uses measured search coverage and remaining candidate scores.
- Does not stop just because a strong incumbent appears.
6. `PromptRenderer`
- Renders the structured plan for the LLM when LLM involvement is needed.
- The LLM can choose among candidates or refine rationale, but should not invent arbitrary directions without evidence.
### Data Flow
```text
study spec + workload trace
|
v
workload profile
|
trial result/probes/logs --> trial profile
|
v
bottleneck analyzer --> ranked hypotheses
|
v
knob effect model --> candidate action set
|
v
experiment planner --> next config / stop
|
v
AITuner evaluates config over configured search range
```
## How This Handles the Current Failure
Current fixed TTFT4s + TPOT25 data:
- TP=2 reached `0.1992 req/s/GPU`.
- Later TP=2 local runtime probes did not improve.
- Min-prompt no-harness found TP=4 with `0.2696-0.2758 req/s/GPU`.
A profile-driven harness should have reasoned:
1. The current incumbent is good but not proof of optimum.
2. The failed probes still show SLO pressure, especially token-latency/tail behavior.
3. Higher TP is a candidate action because it may reduce per-request compute latency.
4. TP=4 is an unmeasured topology hypothesis within allowed constraints.
5. Before spending many local TP=2 runtime trials or stopping, test TP=4 as a high-information experiment.
This is not because TP=4 is hardcoded. If TP=4 had already been measured and underperformed, the planner would prefer local runtime refinement or stop. If the bottleneck were admission/queueing with TTFT/TPOT healthy, the planner might prefer DP or max-num-seqs instead.
## Implementation Plan
### Phase 1: Make the current harness evidence-first
- Persist a compact `TrialProfile` for every trial.
- Build ranked bottleneck hypotheses from probe sequences.
- Replace single `active_bottleneck` with ranked hypotheses and evidence.
- Replace deterministic local refinement with candidate scoring.
- Keep existing rules only as entries in the generic `KnobEffectModel`.
### Phase 2: Candidate scoring
- Generate candidates from:
- adjacent topology moves;
- same-topology runtime moves;
- rollback/avoidance after failures;
- no-op stop.
- Score candidates by:
- whether they directly target top bottleneck;
- whether they test an unmeasured high-information direction;
- launch risk from prior failures;
- expected impact on request_rate_per_gpu;
- measurement cost.
### Phase 3: Controlled experiments
- Add ablations:
- old strong-prompt no-harness;
- min-prompt no-harness;
- current rule harness;
- profile-driven harness.
- Run each under the same SLO/workload/search.
- Report raw `perf[i]`, best-so-far, failed configs, and stop reason.
## Acceptance Criteria
The profile-driven harness is acceptable only if:
1. It does not encode model names, fixed SLO values, or known winning configs.
2. It can explain every proposal as a hypothesis tied to measured bottleneck evidence.
3. It does not early-stop while an unmeasured high-score candidate remains.
4. It improves or matches min-prompt no-harness convergence on at least the qwen27b TTFT4s/TPOT25 setup.
5. It does not regress the previous stepped TTFT/TPOT50 setup.
6. It records enough evidence for a human engineer to audit why each trial was chosen.

View File

@@ -0,0 +1,155 @@
# Profile-Driven Harness Implementation Log
Date: 2026-05-12
## Goal
The harness should accelerate AITuner as a general tuning system, not as a collection of case-specific rules. The current implementation moves the harness toward a performance-engineering loop:
1. extract a compact profile from each measured trial;
2. rank bottleneck hypotheses from workload and probe evidence;
3. generate generic candidate actions from a knob-effect model;
4. score candidates by expected bottleneck relief, information gain, launch safety, and regression risk;
5. block early stop while a high-value untested candidate remains.
This is intended to apply across qwen3.5-27b chat, qwen3-235b prefill-only, qwen3-235b decode-only, and different SLOs without encoding model names, SLO constants, or known winning configs.
## Code Changes
- `src/aituner/harness.py`
- Added `trial_profiles` to normalize trial topology, performance, probe failures, latency quantiles, and launch failure evidence.
- Added `bottleneck_hypotheses`, a ranked list instead of a single active bottleneck label.
- Added `candidate_actions`, generated from topology and runtime knob families.
- Added `experiment_plan`, which selects the next high-score candidate or declares stop readiness.
- Updated harness proposal generation to prefer the profile-driven next action before falling back to legacy deterministic proposal code.
- Updated harness stop logic so convergence/validation stop is blocked when the planner still has a high-value untested candidate.
- `tests/test_core_flow.py`
- Added coverage that a strong TP=2 incumbent with TTFT pressure still selects an unmeasured TP=4 topology candidate.
- Added coverage that decode-only TPOT pressure at max TP can prefer lowering `max-num-seqs` instead of blindly lowering TP.
## Current Scoring Model
The candidate score is intentionally generic:
```text
score = expected_bottleneck_relief * bottleneck_confidence
+ information_gain
+ launch_safety
- regression_risk
```
Examples:
- TTFT/prefill bottleneck: increasing TP and prefill batching candidates receive relief score.
- Decode TPOT bottleneck: increasing TP is useful if a higher legal TP exists; if already at high TP, lowering decode concurrency can become the higher-value candidate.
- Admission/queueing bottleneck: more DP or higher safe concurrency receives relief score.
The scores are not tied to qwen27b/qwen235b or a fixed TPOT/TTFT threshold. They are tied to the measured bottleneck class and legal tunable space.
## Verification
Local:
```bash
python3 -m compileall -q src tests
PYTHONPATH=src python3 -m unittest tests.test_core_flow
```
Result: `93` tests passed.
## Next Experiment
Run the same qwen3.5-27b chat 0-8k setup as the current ablation baseline:
- workload: chat, input length 0-8k
- SLO: TTFT p95 <= 4000ms, TPOT p95 <= 25ms, target pass rate 0.95
- search: full range, `inherit_incumbent_floor=false`
- budget: 12 total tuning iterations
- LLM model: `gpt-5.4`
- variant: harness enabled with profile-driven planner
The no-harness min-prompt baseline is already available and only needs to be reused for comparison unless the setup changes.
## Experiment Started
Started on `dash0` (`11.73.2.172`) at commit `17e9681`.
- tmux session: `qwen27b-profileplanner-harness-20260512`
- spec: `.aituner-tight/specs/dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu3skip-12iter-harness-profileplanner-20260512.json`
- study id: `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu3skip-12iter-harness-profileplanner-20260512`
- log: `.aituner-tight/logs/qwen27b-profileplanner-harness-20260512.log`
- status at launch check: `trial-0001` baseline is running under AITuner; no manual intervention in the tuning loop.
## V1 Early Stop
The first profile-planner run was stopped before accepting it as evidence. A read-only replay of its completed baseline probe history showed that the planner would choose `max-num-seqs=64` for iter2. That was a diagnosis bug:
- `slo_pass_rate_unrecoverable` is a binary-search early-stop summary, not a bottleneck class.
- The harness was counting that summary as an admission/queueing failure.
- Because this count dominated the real TTFT/TPOT failure counts, the planner selected a concurrency action instead of testing TP.
Fix commit: `e3ed775`.
The fix excludes `slo_pass_rate_unrecoverable` from the admission/queueing failure bucket. With the same baseline probe history, the planner now ranks `ttft_prefill` first and proposes `tensor-parallel-size=2` for iter2.
## V2 Experiment Started
Started on `dash0` (`11.73.2.172`) at commit `e3ed775`.
- tmux session: `qwen27b-profileplanner-v2-harness-20260512`
- spec: `.aituner-tight/specs/dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu3skip-12iter-harness-profileplanner-v2-20260512.json`
- study id: `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu3skip-12iter-harness-profileplanner-v2-20260512`
- log: `.aituner-tight/logs/qwen27b-profileplanner-v2-harness-20260512.log`
- monitor: read-only subagent `Wegener`
Acceptance for this run is based on end-to-end trial results, not unit tests. If the first four trials lag the min-prompt no-harness baseline (`0.0650`, `0.1992`, `0.2696`, then failed/NA), the run should be treated as a failed harness iteration and the harness should be optimized again.
## V2 Result And Failure
V2 was stopped early after four trials because it did not improve the no-harness baseline and made a preventable launch-risk proposal.
Raw `request_rate/GPU`:
| Variant | iter1 | iter2 | iter3 | iter4 |
| --- | ---: | ---: | ---: | --- |
| no-harness min-prompt | 0.0650 | 0.1992 | 0.2696 | 0.2696 |
| harness v2 | 0.0650 | 0.1992 | 0.2696 | failed |
Harness v2 did correctly diagnose the first bottleneck and proposed:
- iter2: `tensor-parallel-size=2`, raw `0.1992 req/s/GPU`;
- iter3: `tensor-parallel-size=4`, raw `0.2696 req/s/GPU`.
However, iter4 proposed `tensor-parallel-size=8` and failed at engine launch. The study's `hardware.gpu_count` is 8, but the launch environment sets `CUDA_VISIBLE_DEVICES=0,1,2,4,5,6,7`, which exposes only 7 GPUs. Therefore TP=8 should not have been considered launch-safe.
This is a general harness bug: topology planning must use the effective visible GPU count from the execution profile, not just the nominal hardware count.
Fix:
- parse `engine.base_envs.CUDA_VISIBLE_DEVICES`;
- compute effective GPU count as `min(hardware.gpu_count, visible_device_count)`;
- filter topology candidates and adjacent TP frontier candidates by the effective GPU count.
## GPU Visibility Correction
On 2026-05-13 we corrected the intended experiment setup: `CUDA_VISIBLE_DEVICES` should be `0,1,2,3,4,5,6,7`, not the previous `0,1,2,4,5,6,7`.
This invalidates direct comparison between the old `gpu3skip` runs and new 8-GPU runs. The old v2 failure was real under the old visible-device profile, but it was not the intended 8-card H20 setup.
New comparable studies:
| Variant | Study ID | Status |
| --- | --- | --- |
| no-harness baseline | `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu8-12iter-noharness-minprompt-gpt54-20260513` | completed |
| harness | `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu8-12iter-harness-profileplanner-20260513` | completed |
Both specs set:
- `CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7`
- model endpoint: `gpt-5.4`
- workload: qwen3.5-27b chat 0-8k
- SLO: TTFT p95 <= 4000ms, TPOT p95 <= 25ms, target pass rate 0.95
- search: full range, `inherit_incumbent_floor=false`
Results: harness best `0.2696 req/s/GPU` (TP=4, MBT=7680) vs no-harness best `0.1233 req/s/GPU` (prefix-caching=false), a **+118.6%** improvement. Full analysis in `qwen27b-chat-0-8k-ttft4s-tpot25-gpu8-20260513.md`.

View File

@@ -0,0 +1,89 @@
# qwen235b Thinking Prefill Harness Ablation, 2026-05-10
**Superseded** by `qwen235b-thinking-prefill-ttft-3s6s9s-20260514.md` (updated SLO thresholds, 8-GPU setup). This document is retained for reference only.
## Setup
- Host: `dash0`
- Engine: internal vLLM at `/usr/local/bin/vllm`
- Model: `/home/admin/resource/model/464482ce.qwen3-235b-a22b/256k-0717`
- Trace window: `thinking_w20260327_1000`
- Request mode: chat, with `completion_tokens_override=1` for prefill-only measurement
- SLO: TTFT-only stepped p95 pass target, target pass rate `0.95`
- input tokens `<=4096`: `3000 ms`
- input tokens `<=32768`: `6000 ms`
- otherwise: `9000 ms`
- Search: `sampling_u` in `[0, 0.125]`, tolerance `0.001`, max probes `6`
- Trial budget: no-harness allowed 12 GPU trials; harness allowed 12 but could stop early
- Store root: `.aituner-prefill`
The two fresh specs were identical except `study_id` and `llm.use_harness`:
- no-harness: `.aituner-prefill/specs/dash0-qwen235b-prefill-thinking-run1-ttft-harness-ablation-12iter-noharness-rerun2-20260510.json`
- harness: `.aituner-prefill/specs/dash0-qwen235b-prefill-thinking-run1-ttft-harness-ablation-12iter-harness-rerun2-20260510.json`
Both runs were launched through `python3 -m aituner.cli study tune`; no proposal or study state was edited manually during tuning.
## Result
The table below is the raw per-iteration performance for a Fig18-style plot. Use this table as `perf[i]`; do not replace missing points with `max(perf[:i+1])`.
Metric: `best_request_rate_per_gpu` from that trial's own `result.json`. `NA` means the proposed config did not produce a feasible point in the measured search range, either because the engine/probe failed or because every sampled probe was infeasible.
Important caveat: these runs were produced before the lower-range fallback fix. For same-parallel-size runtime patches, AITuner inherited the incumbent `sampling_u` as the new search floor. If the config was infeasible above that floor, the old worker wrote `NA` without searching below the floor. Therefore the `NA` entries below are not complete Fig18-quality raw performance points; they are "no feasible point above inherited floor." A rerun with the fixed worker is required to fill their true lower-load performance.
| Variant | iter1 | iter2 | iter3 | iter4 | iter5 | iter6 | iter7 | iter8 | iter9 | iter10 | iter11 | iter12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness raw `perf[i]` | 0.2029 | NA | NA | 0.3863 | NA | NA | NA | 0.3879 | 0.3892 | 0.3896 | 0.3900 | 0.3900 |
| harness raw `perf[i]` | 0.2029 | NA | 0.3863 | stop | stop | stop | stop | stop | stop | stop | stop | stop |
The raw no-harness curve is therefore not monotonic. The apparent monotonic 12-iter sequence comes only from plotting best-so-far rather than the measured performance of each proposal.
Per-trial details:
| Variant | iter1 | iter2 | iter3 | iter4 | iter5 | iter6 | iter7 | iter8 | iter9 | iter10 | iter11 | iter12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness, per-trial | 0.2029 | - | - | 0.3863 | - | - | - | 0.3879 | 0.3892 | 0.3896 | 0.3900 | 0.3900 |
| harness, per-trial | 0.2029 | - | 0.3863 | stop | stop | stop | stop | stop | stop | stop | stop | stop |
Best-so-far curve, shown only to explain final incumbent selection:
| Variant | iter1 | iter2 | iter3 | iter4 | iter5 | iter6 | iter7 | iter8 | iter9 | iter10 | iter11 | iter12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness | 0.2029 | 0.2029 | 0.2029 | 0.3863 | 0.3863 | 0.3863 | 0.3863 | 0.3879 | 0.3892 | 0.3896 | 0.3900 | 0.3900 |
| harness | 0.2029 | 0.2029 | 0.3863 | 0.3863 | 0.3863 | 0.3863 | 0.3863 | 0.3863 | 0.3863 | 0.3863 | 0.3863 | 0.3863 |
For plotting raw `perf[i]`, the failed/infeasible points should stay missing or be rendered as invalid trials. If a plotting script requires numeric values, use `0` only with an explicit label that this means "no feasible configuration under the configured SLO"; do not forward-fill from the incumbent.
Final best:
| Variant | GPU trials spent | Best trial | Best config summary | Best req/s | Best req/s/GPU | Final vs no-harness |
| --- | ---: | --- | --- | ---: | ---: | ---: |
| no-harness | 12 | `trial-0011`/`trial-0012` | TP8, DP1, EP off, `max-num-batched-tokens` 7936/8064 | 3.1200 | 0.3900 | baseline |
| harness | 3 | `trial-0003` | TP8, DP1, EP off | 3.0900 | 0.3863 | -0.96% |
Harness reached `0.38625 req/s/GPU` at iter3. No-harness first reached the same TP8 family at iter4, then spent eight more GPU trials to move from `0.38625` to `0.39000 req/s/GPU`, an absolute gain of `0.00375 req/s/GPU` or `0.97%`.
## What the Harness Did
The harness did not use a testcase-specific throughput threshold. The stop decision came from the generic search-high saturation rule:
- incumbent highest feasible probe: `sampling_u=0.123046875`
- configured `search.high`: `0.125`
- binary-search resolution: `(0.125 - 0.0) / 2^6 = 0.001953125`
- gap to search high: `0.001953125`
Because the incumbent was feasible and within one configured search resolution of `search.high`, the harness emitted `harness-stop-0004` before launching another GPU trial. This means the current study could no longer measure a materially higher workload without increasing `search.high`; it is not a claim of global engine optimality.
The harness context also made the LLM response more directed after failure:
- After baseline, it exposed the TTFT-only prefill bottleneck and the sharp queueing knee around `sampling_u=0.03515625`.
- The LLM first chose TP4/DP2 to use the idle 4 GPUs while preserving the validated TP4 shard shape. This failed with `connection refused`, matching the no-harness failure family.
- The next harness prompt included that failure, and the LLM switched to TP8/DP1 with EP off, explicitly avoiding the failed DP2 family.
- No-harness inserted an extra EP4 launch-failure trial before reaching TP8/DP1.
## Conclusion
Harness accelerated convergence mainly through early stopping, not by finding a much better final config on this setup. It reduced GPU trials from 12 to 3 while preserving 99.0% of the no-harness final throughput. It also reached the first strong TP8 point one trial earlier than no-harness.
The limitation is that the generic search-high stop guard stopped before local runtime tuning of `max-num-batched-tokens`, which no-harness used to recover a small additional `0.97%`. For this setup, that tradeoff is acceptable if the goal is fast convergence under a fixed measurement ceiling; if the goal is exact final throughput, the next study should raise `search.high` or disable search-high early stop for a local-polish phase.

View File

@@ -0,0 +1,117 @@
# qwen235b Thinking Prefill Harness Ablation (TTFT 3s/6s/9s)
Date: 2026-05-14 / 2026-05-15
Supersedes: `qwen235b-thinking-prefill-ttft-20260510.md` (different SLO thresholds).
## Setup
- Host: `dash0`
- Engine: internal vLLM at `/usr/local/bin/vllm`
- Model: `/home/admin/resource/model/464482ce.qwen3-235b-a22b/256k-0717`
- Trace window: `thinking_w20260327_1000`
- Request mode: chat, with `completion_tokens_override=1` for prefill-only measurement
- SLO: TTFT-only stepped p95 pass target, target pass rate `0.95`
- input tokens `<=4096`: `3000 ms`
- input tokens `<=32768`: `6000 ms`
- otherwise: `9000 ms`
- GPU env: `CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7` (8x H20)
- Baseline topology: `TP=4`
- LLM: `gpt-5.4`
- Code: profile-driven harness planner, post GPU-visibility fix (`5c2958e`+)
## Studies
| Variant | Study ID | search.high |
| --- | --- | ---: |
| no-harness | `dash0-qwen235b-prefill-thinking-ttft-3s6s9s-12iter-noharness-minprompt-gpt54-20260514` | 0.125 |
| harness | `dash0-qwen235b-prefill-thinking-ttft-3s6s9s-12iter-harness-profileplanner-gpt54-20260514` | 0.125 |
| harness (high=0.25) | `dash0-qwen235b-prefill-thinking-ttft-3s6s9s-high025-12iter-harness-profileplanner-gpt54-20260515` | 0.25 |
The `harness (high=0.25)` run was added to test whether raising `search.high` lets the harness find a better runtime config after reaching the search ceiling at `0.125`.
## Result
Raw per-iteration performance for Fig18-style plot. Metric: `best_request_rate_per_gpu`. `NA` means the proposed config did not produce a feasible point. `fail` means engine launch failure. `stop` means harness stopped before launching another trial.
| Variant | iter1 | iter2 | iter3 | iter4 | iter5 | iter6 | iter7 | iter8 | iter9 | iter10 | iter11 | iter12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness raw `perf[i]` | 0.1804 | fail | 0.1892 | fail | 0.1892 | 0.1804 | 0.2217 | 0.2029 | 0.2029 | 0.2029 | 0.1892 | 0.1804 |
| harness raw `perf[i]` | 0.2029 | 0.3863 | stop | stop | stop | stop | stop | stop | stop | stop | stop | stop |
| harness (high=0.25) raw `perf[i]` | 0.2029 | 0.3921 | 0.3442 | 0.3921 | 0.3821 | 0.3821 | 0.3821 | 0.3688 | 0.3821 | 0.3821 | 0.3821 | 0.3821 |
| Variant | GPU trials | Best iter | Best req/s | Best req/s/GPU | Best config summary |
| --- | ---: | ---: | ---: | ---: | --- |
| no-harness | 12 | 7 | 0.8867 | 0.2217 | TP=4, MNS=112, MBT=7168 |
| harness | 2 (stop) | 2 | 3.0900 | 0.3863 | TP=8 |
| harness (high=0.25) | 12 | 2 | 3.1367 | **0.3921** | TP=8 |
Harness reached **+74.2%** over no-harness at iter 2. With `search.high=0.25`, the harness found `0.3921 req/s/GPU` (+76.8%).
## Incumbent Curve
Best-so-far request rate per GPU after each iteration.
| Variant | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness | 0.1804 | 0.1804 | 0.1892 | 0.1892 | 0.1892 | 0.1892 | 0.2217 | 0.2217 | 0.2217 | 0.2217 | 0.2217 | 0.2217 |
| harness | 0.2029 | 0.3863 | stop | stop | stop | stop | stop | stop | stop | stop | stop | stop |
| harness (high=0.25) | 0.2029 | 0.3921 | 0.3921 | 0.3921 | 0.3921 | 0.3921 | 0.3921 | 0.3921 | 0.3921 | 0.3921 | 0.3921 | 0.3921 |
## Trial Details
No-harness:
| Iter | Result / GPU | Incumbent / GPU | Status | Config summary |
| ---: | ---: | ---: | --- | --- |
| 1 | 0.1804 | 0.1804 | completed | baseline (TP=4) |
| 2 | - | 0.1804 | launch fail | TP=4, EP=4, MNS=128 |
| 3 | 0.1892 | 0.1892 | completed | MNS=96 |
| 4 | - | 0.1892 | launch fail | TP=4, DP=2, EP off, MNS=96 |
| 5 | 0.1892 | 0.1892 | completed | MNS=112 |
| 6 | 0.1804 | 0.1892 | completed | MNS=112, MBT=9216 |
| 7 | 0.2217 | 0.2217 | completed | MNS=112, MBT=7168 |
| 8 | 0.2029 | 0.2217 | completed | MNS=112, MBT=6144 |
| 9 | 0.2029 | 0.2217 | completed | MNS=120, MBT=7168 |
| 10 | 0.2029 | 0.2217 | completed | TP=4, DP=1, EP off, MNS=108, MBT=7168 |
| 11 | 0.1892 | 0.2217 | completed | MNS=112, MBT=7680 |
| 12 | 0.1804 | 0.2217 | completed | MNS=112, MBT=6912 |
Harness (`search.high=0.125`):
| Iter | Result / GPU | Incumbent / GPU | Status | Config summary |
| ---: | ---: | ---: | --- | --- |
| 1 | 0.2029 | 0.2029 | completed | baseline (TP=4) |
| 2 | 0.3863 | 0.3863 | completed | TP=8 |
| 3 | - | - | harness stop | search-high saturation (`sampling_u=0.123` vs `search.high=0.125`) |
Harness (`search.high=0.25`):
| Iter | Result / GPU | Incumbent / GPU | Status | Config summary |
| ---: | ---: | ---: | --- | --- |
| 1 | 0.2029 | 0.2029 | completed | baseline (TP=4) |
| 2 | 0.3921 | 0.3921 | completed | TP=8 |
| 3 | 0.3442 | 0.3921 | completed | TP=8, chunked-prefill, MBT=32768 |
| 4 | 0.3921 | 0.3921 | completed | TP=8, MBT=12288 |
| 5 | 0.3821 | 0.3921 | completed | TP=8, EP off, MBT=16384 |
| 6 | 0.3821 | 0.3921 | completed | TP=8, EP off, MBT=14336 |
| 7 | 0.3821 | 0.3921 | completed | TP=8, EP off, MBT=10240 |
| 8 | 0.3688 | 0.3921 | completed | TP=8, EP off, MBT=11776 |
| 9 | 0.3821 | 0.3921 | completed | TP=8, EP off, MBT=13312 |
| 10 | 0.3821 | 0.3921 | completed | TP=8, EP off, MBT=7168 |
| 11 | 0.3821 | 0.3921 | completed | TP=8, EP off, MBT=12032 |
| 12 | 0.3821 | 0.3921 | completed | TP=8, EP off, MBT=12800 |
## Interpretation
No-harness never attempted TP=8. It stayed on the TP=4 baseline, encountered two launch failures (EP=4 and DP=2), and spent all remaining trials on runtime knob tuning within the TP=4 family. Its best finding was `MNS=112, MBT=7168` at iter 7 (`0.2217 req/s/GPU`).
Harness identified `ttft_prefill` as the dominant bottleneck from the baseline trial and immediately proposed TP=8 as the first topology move. This is the correct direction for a prefill-only workload with heavy-tail prompts (p95 ~19.7k tokens, p99 ~30k tokens).
With `search.high=0.125`, the harness stopped at iter 2 because the incumbent's best feasible `sampling_u=0.123` was within one search resolution of `search.high`. With `search.high=0.25`, the harness continued for 12 trials but the best remained iter 2 (`TP=8, default MBT`). The additional 10 trials explored MBT variations on TP=8 but none improved per-GPU throughput. This confirms the 2-trial harness result was already at or near the local optimum.
The gap between harness and no-harness (`+76.8%`) comes entirely from topology: TP=8 doubles the per-GPU prefill compute bandwidth compared to TP=4, which directly reduces TTFT and allows higher admitted request rates under the stepped TTFT SLO.
## Comparison with Previous Run (2026-05-10)
The 2026-05-10 run used different SLO thresholds and is documented in `qwen235b-thinking-prefill-ttft-20260510.md`. The core finding is consistent: harness finds TP=8 at iter 2-3 while no-harness gets stuck on TP=4 runtime tuning.

View File

@@ -0,0 +1,103 @@
# Qwen27B Chat 0-8k Harness Ablation
Date: 2026-05-10
**Superseded** by `qwen27b-chat-0-8k-ttft4s-tpot25-gpu8-20260513.md` (corrected 8-GPU setup). This document used `CUDA_VISIBLE_DEVICES=0,1,2,4,5,6,7` (7 GPUs) and is retained for reference only.
## Setup
- Host: `dash0` (`172.27.114.84`)
- Model: `/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal`
- Workload: chat, 0-8k input window
- SLO: TTFT <= 4000ms and TPOT <= 25ms, target pass rate = 0.95
- Trial budget: 12 total tuning iterations per study
- Execution: direct `python3 -m aituner.cli study tune ... --max-trials 12`
- GPU env: `CUDA_VISIBLE_DEVICES=0,1,2,4,5,6,7`
- Code commit: `adc4351`
The previous no-harness run was affected by the `dash0` migration and had many engine launch failures. This document uses the clean no-harness rerun from 2026-05-09.
## Studies
| Variant | Study ID |
| --- | --- |
| no-harness rerun | `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu3skip-12iter-noharness-rerun-20260509` |
| harness | `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu3skip-12iter-harness-20260508` |
## Result
The table below is the raw per-iteration performance for a Fig18-style plot. Use this table as `perf[i]`; do not replace missing points with `max(perf[:i+1])`.
Metric: `best_request_rate_per_gpu` from that trial's own `result.json`. `NA` means the proposed config did not produce a feasible point in the measured search range. `stop` means the harness stopped before launching another GPU trial.
Important caveat: these runs were produced before the lower-range fallback fix. For same-parallel-size runtime patches, AITuner inherited the incumbent `sampling_u` as the new search floor. If the config was infeasible above that floor, the old worker wrote `NA` without searching below the floor. Therefore the `NA` entries below are not complete Fig18-quality raw performance points; they are "no feasible point above inherited floor." A rerun with the fixed worker is required to fill their true lower-load performance.
| Variant | iter1 | iter2 | iter3 | iter4 | iter5 | iter6 | iter7 | iter8 | iter9 | iter10 | iter11 | iter12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness raw `perf[i]` | 0.0650 | 0.0617 | 0.0308 | NA | NA | NA | NA | NA | NA | 0.2025 | NA | NA |
| harness raw `perf[i]` | 0.0650 | 0.0617 | 0.2025 | NA | 0.1283 | NA | 0.2696 | 0.2742 | NA | NA | NA | stop |
The raw no-harness curve is not monotonic: iter2 and iter3 are worse than the baseline, and iter4-9 do not produce feasible configs. The monotonic curve below is best-so-far/incumbent tracking, not the measured performance of each proposal.
| Variant | Best iter | Best request rate | Best request rate / GPU | Best config summary |
| --- | ---: | ---: | ---: | --- |
| no-harness rerun | 10 | 0.4050 | 0.2025 | `tensor-parallel-size=2`, `data-parallel-size=1`, `max-num-batched-tokens=12288` |
| harness | 8 | 1.0967 | 0.2742 | `tensor-parallel-size=4`, `enable-chunked-prefill=true`, `max-num-batched-tokens=16384` |
Harness reached a higher incumbent and did so earlier. Final best request rate per GPU improved by about `35.4%` over the clean no-harness rerun.
## Incumbent Curve
Values are incumbent best request rate per GPU after each tuning iteration. This table is useful for explaining final best selection, but it should not be used as Fig18 raw `perf[i]`.
| Variant | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness rerun | 0.0650 | 0.0650 | 0.0650 | 0.0650 | 0.0650 | 0.0650 | 0.0650 | 0.0650 | 0.0650 | 0.2025 | 0.2025 | 0.2025 |
| harness | 0.0650 | 0.0650 | 0.2025 | 0.2025 | 0.2025 | 0.2025 | 0.2696 | 0.2742 | 0.2742 | 0.2742 | 0.2742 | stop |
For plotting raw `perf[i]`, keep `NA` points missing or render them as invalid trials. If a plotting script requires numeric values, use `0` only with an explicit label that this means "no feasible configuration under the configured SLO"; do not forward-fill from the incumbent.
## Trial Details
No-harness rerun:
| Iter | Trial result / GPU | Incumbent / GPU | Status | Config summary |
| ---: | ---: | ---: | --- | --- |
| 1 | 0.0650 | 0.0650 | completed | baseline |
| 2 | 0.0617 | 0.0650 | completed | `tp=1`, `dp=2`, `max-num-batched-tokens=12288` |
| 3 | 0.0308 | 0.0650 | completed | `tp=1`, `dp=4` |
| 4 | - | 0.0650 | completed, infeasible | `max-num-batched-tokens=12288` |
| 5 | - | 0.0650 | completed, infeasible | `tp=1`, `dp=2`, `max-num-batched-tokens=16384` |
| 6 | - | 0.0650 | completed, infeasible | `tp=1`, `dp=2`, `max-num-batched-tokens=12288`, `block-size=32` |
| 7 | - | 0.0650 | completed, infeasible | `max-num-batched-tokens=10240` |
| 8 | - | 0.0650 | completed, infeasible | `max-num-batched-tokens=7168` |
| 9 | - | 0.0650 | completed, infeasible | `tp=1`, `dp=2` |
| 10 | 0.2025 | 0.2025 | completed | `tp=2`, `dp=1`, `max-num-batched-tokens=12288` |
| 11 | - | 0.2025 | completed, infeasible | `tp=2`, `dp=1`, `max-num-batched-tokens=10240` |
| 12 | - | 0.2025 | completed, infeasible | `tp=2`, `dp=1`, `max-num-batched-tokens=13312` |
Harness:
| Iter | Trial result / GPU | Incumbent / GPU | Status | Config summary |
| ---: | ---: | ---: | --- | --- |
| 1 | 0.0650 | 0.0650 | completed | baseline |
| 2 | 0.0617 | 0.0650 | completed | `tp=1`, `dp=2` |
| 3 | 0.2025 | 0.2025 | completed | `tp=2`, `dp=1` |
| 4 | - | 0.2025 | completed, infeasible | `tp=2`, chunked prefill, `max-num-batched-tokens=16384` |
| 5 | 0.1283 | 0.2025 | completed | `tp=2`, `dp=2` |
| 6 | - | 0.2025 | completed, infeasible | `tp=2`, `dp=1`, `max-num-seqs=4` |
| 7 | 0.2696 | 0.2696 | completed | `tp=4`, `dp=1` |
| 8 | 0.2742 | 0.2742 | completed | `tp=4`, chunked prefill, `max-num-batched-tokens=16384` |
| 9 | - | 0.2742 | completed, infeasible | `tp=4`, chunked prefill, `max-num-batched-tokens=24576` |
| 10 | - | 0.2742 | completed, infeasible | `tp=4`, chunked prefill, `max-num-batched-tokens=16384`, `max-num-seqs=8` |
| 11 | - | 0.2742 | completed, infeasible | `tp=4`, chunked prefill, `max-num-batched-tokens=16384`, `max-num-seqs=16` |
| 12 | - | 0.2742 | harness stop | validation exhausted after strong incumbent |
## Interpretation
The clean no-harness rerun eventually found the `tp=2` topology at iter 10, so the old migration-tainted no-harness result was indeed too pessimistic. Harness still improves the process in two ways:
- It reaches the `tp=2` topology by iter 3 instead of iter 10.
- It then escalates to `tp=4` and a nearby batching refinement, reaching `0.2742 req/s/GPU`.
The harness effect is not "one iter to best"; it is directional search. It turns bottleneck evidence into topology validation probes, then validates runtime refinements around the stronger incumbent and stops when further nearby probes do not improve.

View File

@@ -0,0 +1,99 @@
# Qwen27B Chat 0-8k Harness Ablation (8-GPU)
Date: 2026-05-13
Supersedes: `qwen27b-chat-0-8k-ttft4s-tpot25-20260510.md` (7-GPU / gpu3skip setup).
## Setup
- Host: `dash0`
- Model: `/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal`
- Workload: `chat_w20260311_1000`, chat, 0-8k input window
- SLO: TTFT <= 4000ms and TPOT <= 25ms, target pass rate = 0.95
- Trial budget: 12 total tuning iterations per study
- Search: `sampling_u` in `[0, 0.0625]`, tolerance `0.001`, max probes `6`
- Execution: `python3 -m aituner.cli study tune ... --max-trials 12`
- GPU env: `CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7` (8x H20)
- Baseline topology: `TP=1`
- LLM: `gpt-5.4`
- Code: profile-driven harness planner, post GPU-visibility fix (`5c2958e`+)
## Studies
| Variant | Study ID |
| --- | --- |
| no-harness | `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu8-12iter-noharness-minprompt-gpt54-20260513` |
| harness | `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu8-12iter-harness-profileplanner-20260513` |
## Result
Raw per-iteration performance for Fig18-style plot. Metric: `best_request_rate_per_gpu` from that trial's own `result.json`. `NA` means the proposed config did not produce a feasible point. `fail` means engine launch failure.
| Variant | iter1 | iter2 | iter3 | iter4 | iter5 | iter6 | iter7 | iter8 | iter9 | iter10 | iter11 | iter12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness raw `perf[i]` | 0.0650 | fail | fail | 0.0617 | 0.0650 | 0.1233 | 0.1050 | 0.1233 | 0.0650 | 0.0650 | 0.0617 | 0.1233 |
| harness raw `perf[i]` | 0.0650 | 0.1992 | 0.2621 | 0.2056 | 0.1544 | 0.2696 | 0.2621 | 0.2621 | 0.2696 | 0.2621 | 0.2621 | 0.2621 |
| Variant | Best iter | Best request rate | Best request rate / GPU | Best config summary |
| --- | ---: | ---: | ---: | --- |
| no-harness | 6 | 0.1233 | 0.1233 | `enable-prefix-caching=false` |
| harness | 6 | 1.0783 | **0.2696** | `tensor-parallel-size=4`, `max-num-batched-tokens=7680` |
Harness final best is **+118.6%** over no-harness.
## Incumbent Curve
Best-so-far request rate per GPU after each iteration.
| Variant | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness | 0.0650 | 0.0650 | 0.0650 | 0.0650 | 0.0650 | 0.1233 | 0.1233 | 0.1233 | 0.1233 | 0.1233 | 0.1233 | 0.1233 |
| harness | 0.0650 | 0.1992 | 0.2621 | 0.2621 | 0.2621 | 0.2696 | 0.2696 | 0.2696 | 0.2696 | 0.2696 | 0.2696 | 0.2696 |
## Trial Details
No-harness:
| Iter | Result / GPU | Incumbent / GPU | Status | Config summary |
| ---: | ---: | ---: | --- | --- |
| 1 | 0.0650 | 0.0650 | completed | baseline |
| 2 | - | 0.0650 | launch fail | `gpu-memory-utilization=0.94`, `max-num-batched-tokens=16384` |
| 3 | - | 0.0650 | launch fail | `enable-chunked-prefill=false` |
| 4 | 0.0617 | 0.0650 | completed | `data-parallel-size=2` |
| 5 | 0.0650 | 0.0650 | completed | `block-size=32` |
| 6 | 0.1233 | 0.1233 | completed | `enable-prefix-caching=false` |
| 7 | 0.1050 | 0.1233 | completed | `enable-prefix-caching=false`, `block-size=32` |
| 8 | 0.1233 | 0.1233 | completed | `enable-prefix-caching=false`, `max-num-seqs=32` |
| 9 | 0.0650 | 0.1233 | completed | `enable-prefix-caching=false`, `max-num-batched-tokens=4096` |
| 10 | 0.0650 | 0.1233 | completed | `enable-prefix-caching=false`, `max-num-seqs=16` |
| 11 | 0.0617 | 0.1233 | completed | `data-parallel-size=2`, `enable-prefix-caching=false` |
| 12 | 0.1233 | 0.1233 | completed | `enable-prefix-caching=false` (+ torch compile off) |
Harness:
| Iter | Result / GPU | Incumbent / GPU | Status | Config summary |
| ---: | ---: | ---: | --- | --- |
| 1 | 0.0650 | 0.0650 | completed | baseline |
| 2 | 0.1992 | 0.1992 | completed | `tensor-parallel-size=2` |
| 3 | 0.2621 | 0.2621 | completed | `tensor-parallel-size=4` |
| 4 | 0.2056 | 0.2621 | completed | `tensor-parallel-size=8` |
| 5 | 0.1544 | 0.2621 | completed | `tensor-parallel-size=4`, `data-parallel-size=2` |
| 6 | 0.2696 | 0.2696 | completed | `tensor-parallel-size=4`, `max-num-batched-tokens=7680` |
| 7 | 0.2621 | 0.2696 | completed | `tensor-parallel-size=4`, `enable-chunked-prefill=true`, `max-num-batched-tokens=12288` |
| 8 | 0.2621 | 0.2696 | completed | `tensor-parallel-size=4`, `max-num-batched-tokens=7424` |
| 9 | 0.2696 | 0.2696 | completed | `tensor-parallel-size=4`, `max-num-batched-tokens=7680`, `max-num-seqs=64` |
| 10 | 0.2621 | 0.2696 | completed | `tensor-parallel-size=4`, `max-num-batched-tokens=7680`, `max-num-seqs=56` |
| 11 | 0.2621 | 0.2696 | completed | `tensor-parallel-size=4`, `max-num-batched-tokens=7680`, `max-num-seqs=60` |
| 12 | 0.2621 | 0.2696 | completed | `tensor-parallel-size=4`, `max-num-batched-tokens=7680`, `max-num-seqs=63` |
## Interpretation
No-harness never tested any TP change in 12 trials. It started from TP=1, encountered two early launch failures, then spent all remaining budget on runtime knobs (`enable-prefix-caching`, `block-size`, `max-num-seqs`, `max-num-batched-tokens`). Its best discovery was disabling prefix caching at iter 6, reaching only `0.1233 req/s/GPU`.
Harness systematically explored the TP frontier: iter 2 tested TP=2, iter 3 tested TP=4, iter 4 tested TP=8. The profile-driven planner identified `ttft_prefill` as the ranked bottleneck and proposed increasing TP as the primary relief action. After TP=4 proved best per-GPU, the harness tested TP=4/DP=2 (worse) then shifted to runtime refinement within the TP=4 family, settling on `max-num-batched-tokens=7680` as the marginal improvement.
The result demonstrates that topology exploration is critical for this workload: the no-harness LLM failed to discover TP>1 configurations entirely, while the harness reached the optimal TP=4 topology by iter 3 and refined it by iter 6.
## Comparison with Previous 7-GPU Run
The 7-GPU (`gpu3skip`) run from 2026-05-10 used `CUDA_VISIBLE_DEVICES=0,1,2,4,5,6,7` and is not directly comparable. The harness result on 7-GPU was `0.2742 req/s/GPU` (TP=4, chunked-prefill, MBT=16384). On 8-GPU, the harness found a similar TP=4 optimum at `0.2696 req/s/GPU` with slightly different runtime tuning. The core finding is consistent: harness accelerates topology discovery and significantly outperforms no-harness.

View File

@@ -0,0 +1,51 @@
# Qwen3.5-27B TP sweep under length-aware TTFT SLO — 2026-06-16
Branch `feat/two-stop`. Deterministic ground-truth A/B (proposal files, no LLM):
TP1 vs TP2 vs TP4 on the dense Qwen3.5-27B (internal 256k, fp8, spec-decode) at
08k chat, vLLM 0.11.1, H20, `replay_time_scale=1.0` (no smoke), Stop-A enabled,
pinned to GPUs 27.
**SLO**: TTFT ≤ `4000 + 0.125·L_in` ms (= 4s + L_in/8k), TPOT ≤ 50 ms, pass ≥ 95%.
## Result
| config | best_u | raw req/s | req/s/GPU | pass | saturated |
| --- | --- | --- | --- | --- | --- |
| TP1 | 0.00195 | 0.065 | **0.065** | 1.00 | no |
| TP2 | 0.0195 | 0.585 | **0.2925** | 0.96 | no |
| TP4 | 0.123 | 3.63 | **≥0.908** | 0.98 | **yes (best_u≈high=0.125)** |
- **Per-GPU throughput rises sharply with TP for the dense 27B**: TP2 = 4.5× TP1,
TP4 ≥ 14× TP1. Opposite of the MoE Qwen3-30B-A3B (TP1 best per-GPU) — confirms the
dense-vs-MoE distinction.
- **Mechanism**: TP1 is TPOT-bound — one H20 cannot decode a 27B under 50 ms/token
once the batch grows, so it saturates at ~0.065 req/s/GPU. Loosening TTFT (2s→4-5s)
did *not* change TP1 (still 0.065), confirming TPOT — not TTFT — is TP1's binding
constraint. Each TP doubling speeds decode+prefill enough to more than recover the
added GPUs.
- **TP4 saturated** the offered-load ceiling (`best_u=0.123 ≈ 0.125`): still feasible
after ~the whole trace, so 0.908 is a lower bound. True peak (and TP8) need a
raised `search.high` to measure.
## Process findings (fed back into the harness)
- **Bug fixed**: a request exceeding `request_timeout_s` raised a raw `TimeoutError`
mid-stream that escaped `_run_one_request` and crashed the whole trial; now wrapped
as `HttpClientError` (failed request, not failed trial). Commit `2fcaf80`.
- **Open gap**: killing a `study tune` run orphans the `VLLM::EngineCore` workers
(SIGTERM/SIGKILL of the loop doesn't tear down the engine), which twice left leaked
GPU memory on GPUs 0/1 (dead PIDs still pinning KV, only clearable via root
`nvidia-smi --gpu-reset`). Fix: SIGTERM handler in the CLI loop + make
`_terminate_process_tree` match `EngineCore` workers, not just `vllm serve`.
- Experiment hygiene: scale=1.0 makes each probe take real arrival time; `search.high`
must bracket the config's boundary (too wide wastes probes on a low-capacity config;
too low saturates a high-capacity one), and `request_timeout_s` must be modest so
overloaded probes drain fast.
## Next
- Re-measure TP4 (and TP8) with `search.high` raised (e.g. 0.5) to find the true peak
per-GPU and the TP knee.
- Run the Stop-B agentic loop on this 27B stack: unlike the 30B (baseline already
optimal), here the loop should climb TP1→TP2→TP4 and stop — a real improving
trajectory (the original Phase-5 "A" goal).

View File

@@ -0,0 +1,121 @@
# Stop-A validation (Phase 3) — 2026-06-15
Branch `feat/two-stop`. Stop-A = truncate each probe's replay once the offered
L-C-A of the replayed prefix converges to the full set (pure L-C-A criterion +
C-gate). This note records the CPU calibration and the GPU fidelity check.
## 1. Calibration (CPU, no serving)
`scripts/stop_a_calibration.py` on the dash0 0321 10:0010:10 windows:
| dim | chat (19239 req, hit≈7%) | coder (2451 req, structured reuse) |
| --- | --- | --- |
| A | ≥0.95 by frac 0.10 | fast |
| L | ≥0.96 from frac 0.05 | 0.05=0.75 (heavy tail) → ≥0.94 by 0.20 |
| **C (slowest)** | noisy, dips (0.50→0.885, 0.55→0.835), stable ≥0.92 only ~0.85 | smooth, stable ≥0.92 by ~0.70 |
Stop fraction (τ_L=τ_A=0.90, W=3):
| τ_c | chat | coder |
| --- | --- | --- |
| 0.85 | 0.45 (273s) | 0.45 (255s) |
| 0.90 | 0.70 (423s) | 0.55 (318s) |
| 0.92 | 0.85 (513s) | 0.70 (411s) |
Findings:
- **C is the slowest dimension in both workloads** — reproduces paper §5.2 / Fig 9.
- **What makes C hard to call converged is signal *noise*, not reuse magnitude.**
Low-reuse chat has a sparse/spiky ideal-hit-length series, so its C similarity
oscillates and is *harder* to stabilize than the structured, higher-reuse coder.
Consequence: a strict τ_c (0.92) gives chat only ~15% saving. A more robust C
feature for the low-reuse regime is future work.
## 2. GPU fidelity check (Qwen3-30B-A3B, vLLM 0.11.1, H20)
One full-window run (`adaptive_stop` disabled, `replay_time_scale=1.0`, window
`chat_w20260311_1000`, 08k, out=128), then `scripts/stop_a_validate.py`
recomputes each probe's convergence prefix and compares the truncated verdict to
the full verdict — so a single GPU run validates truncation fidelity (no second run).
Trial result: best feasible `sampling_u=0.078125`, request_rate **2.30 req/s**,
pass_rate 0.973.
Per-probe verdict (τ=0.9):
| τ_c | verdict matches | mean replay saved |
| --- | --- | --- |
| 0.85 | 3/4 | 54% |
| 0.90 | 3/4 | 52% |
| 0.92 | 3/4 | 38% |
The mismatch is the same probe at every τ_c — the feasibility knee `0.08594`:
```
thresh full_pass prefix_pass full_feas prefix_feas
0.08594 0.946 0.9560.961 False True <- mismatch
0.07812 0.973 0.9870.990 True True
0.06250 0.986 1.000 True True
0.09375 0.268 0.490.54 False False
```
## 3. Interpretation
- **Stop-A works and saves ~50% of replay** (vs the full 600 s window) while
preserving 3/4 probe verdicts. (The paper's ~70% is vs a 30-min fixed baseline;
our baseline is the 600 s window, so the percentages are not directly comparable.)
- **The one failure is a boundary false-positive at the feasibility knee.** At
`0.08594` the full window is 0.946 (just below the 0.95 SLO) but the prefix is
0.9560.961 (just above): the *second half* of the window degraded — engine-state
drift (KV fill / fragmentation / later-arriving harder requests) that the
*offered* L-C-A cannot see. The C-gate did not help because offered-C had
converged; the divergence is in the measured pass-rate, not in C.
- If Stop-A were enabled, the binary search would accept `0.08594`, overestimating
the peak sustainable rate by one binary step (~10%).
**This is the boundary jitter we accepted when choosing the pure-L-C-A criterion.**
The data now argues for revisiting the previously-declined **SLO-boundary guard**:
keep replaying while the measured pass-rate is within ±δ of the target, even after
L-C-A converges. It targets exactly this knee case at low extra cost (it only
extends replay on probes sitting on the feasibility boundary). Recommend adding it
as a small Stop-A enhancement before enabling Stop-A in production studies.
## 4. SLO-boundary guard (implemented + validated)
Added `trace.adaptive_stop.boundary_delta` (default 0.02): when a truncated probe's
measured pass-rate lands within ±δ of the SLO target, re-measure on the full window
and use that verdict. Re-ran the same config with `adaptive_stop` enabled
(τ=0.9, τ_c=0.90, δ=0.02):
| threshold | feasible | pass | selected | replayed | boundary_extended |
| --- | --- | --- | --- | --- | --- |
| 0.06250 | True | 1.000 | 1086 | 487 (45%) | — |
| 0.09375 | False | 0.444 | 1656 | 822 (50%) | — |
| 0.07812 | True | 0.994 | 1378 | 682 (49%) | — |
| 0.08594 | **False** | 0.947 | 1523 | **1523 (100%)** | **True** |
Result: best feasible `sampling_u=0.078125` (rate 2.30 req/s) — **identical to the
full-replay baseline**. The guard fired on exactly the one knee probe and
re-measured it to the correct infeasible verdict; the other three probes truncated
to ~4550%. Net replayed 3514/5643 requests ≈ **38% replay saved on this trial
while recovering the correct peak rate** (no one-step overestimate).
**Conclusion: Stop-A with the boundary guard is correct (verdict matches full
replay) and still saves replay time. Safe to enable.** Configs:
`dash0_qwen30b_a3b_stopA_fulldata.json` (OFF baseline) and
`dash0_qwen30b_a3b_stopA_on.json` (ON).
## Repro
```
# calibration
PYTHONPATH=src python3 scripts/stop_a_calibration.py \
--jsonl <DIR>/qwen_chat_blksz_64_032109-032111.jsonl --block-size 64 \
--window-start 3600 --window-end 4200 --gpu-count 8 --label chat
# GPU run + fidelity
PYTHONPATH=src python3 -m aituner.cli study tune \
--spec configs/examples/dash0_qwen30b_a3b_stopA_fulldata.json \
--store-root .aituner/stopA-fulldata --max-trials 1
PYTHONPATH=src python3 scripts/stop_a_validate.py \
--spec configs/examples/dash0_qwen30b_a3b_stopA_fulldata.json \
--store-root .aituner/stopA-fulldata --tau 0.9 --tau-c 0.90
```

View File

@@ -0,0 +1,86 @@
# Stop-B end-to-end validation (Phase 5) — 2026-06-15
Branch `feat/two-stop`. Real agentic loop on dash0: Qwen3-30B-A3B / vLLM 0.11.1 /
8×H20, `gpt-5.4` (via codex/prism) proposing configs, Stop-A enabled to accelerate
each evaluation, `use_harness=True` so the Stop-B deterministic validator + LLM-stop
veto are active. Config `dash0_qwen30b_a3b_stopB_e2e.json`, `search.high=1.0`,
`max_probes=6`, `--max-trials 8`.
## Two stop paths exercised
**Run A (`search.high=0.125`)** — the default config already saturates the offered-load
search range, so Stop-B fired immediately via the **search-high-saturation** path:
`stop_authorized_by: validator`, reason *"the incumbent's highest measured probe is
feasible and within the binary-search resolution of search.high."* Correct
measurement-ceiling stop (no point proposing configs when the load range, not the
config, is the bound).
**Run B (`search.high=1.0`)** — forces a real multi-iteration search:
| trial | TP | DP | EP | feasible | raw req/s | **req/s/GPU** | source |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 0001 | 1 | 1 | 1 | ✓ | 2.90 | **2.900** | baseline |
| 0002 | 2 | 1 | 1 | ✓ | 4.42 | 2.208 | harness TP-seed |
| 0003 | 2 | 1 | 2 | ✗ launch-fail | — | — | harness (EP) |
| 0004 | 1 | 2 | 1 | ✓ | 4.42 | 2.208 | LLM (after veto) |
| 0005 | 2 | 2 | 1 | ✓ | 8.37 | 2.092 | harness |
| 0006 | 2 | 2 | 2 | ✗ launch-fail | — | — | harness (EP) |
| 0007 | 4 | 1 | 1 | ✓ | 8.37 | 2.092 | LLM |
| (0008) | — | — | — | **STOP** | — | — | LLM stop, honored after veto budget |
Incumbent: **trial-0001 (TP1), 2.90 req/s/GPU — never beaten.**
> **⚠️ The per-GPU trajectory above is NOT a valid benchmark — it validates only
> the Stop-B *mechanics*.** Two confounds:
> 1. **Trace-ceiling saturation.** TP2·DP2 and TP4 reached `best_sampling_u≈0.98`
> (still feasible after consuming ~the whole window), so their *true* peak
> per-GPU is higher than the 2.09 shown — we ran out of offered load to push
> them to their boundary. Only TP1 (u=0.31), TP2 (u=0.48) and DP2 (u=0.48)
> found real boundaries. The `sampling_u` axis maxes at the full trace, so any
> config that sustains more than the window's offered rate cannot be measured.
> 2. **Smoke regime.** This run inherited `replay_time_scale=0.1` +
> `max_requests_per_probe=512` (README: convergence test, *not* a benchmark) —
> compressed arrivals distort A and the 512 cap imposes a ~8.4 req/s ceiling.
>
> The below-ceiling TP1 (2.90) > TP2 (2.21) ordering *may* be real for this model
> (Qwen3-30B-A3B is an MoE with ~3B active params → little compute per token → TP
> adds all-reduce overhead with little benefit), which differs from the dense
> Qwen3.5-27B where TP2 wins. But this run cannot establish it. A valid benchmark
> needs `scale=1.0`, no cap, and enough offered-load headroom that strong configs
> are not trace-saturated — see the 27B TP A/B follow-up.
## Phase-5 acceptance
- **No regression.** The primary metric `request_rate_per_gpu` stayed 2.90 the whole
run. Scaling TP/DP raised *raw* throughput (4.42, 8.37) but lowered per-GPU
efficiency (2.21, 2.09); the loop correctly kept the TP1 baseline as incumbent and
never adopted a worse-per-GPU config. (Matches the paper: long-prompt, low-reuse
chat prefers small TP for per-GPU efficiency.)
- **Stop-B authority validated live.** At trial 4 the LLM tried to stop
(`should_stop=true`); the deterministic validator **vetoed** it
(`validator_did_not_authorize_stop`, `continue_harness_guided_search`), forcing one
more confirmation (DP2, which also failed to beat baseline). After the budget, the
LLM's later, well-justified stop was honored (`stop_authorized_by:
llm_after_veto_budget`). The bounded veto behaved exactly as designed.
- **Auditable reason chain.** Every stop/veto carries a diagnosis grounded in the
measured evidence (e.g. *"increasing TP 1→2 lowers per-GPU efficiency even though
token latency improves … EP is explicitly blocked by launch-failure evidence"*).
- **Launch-failure robustness.** Two EP configs (trial-0003, 0006) failed to launch
under vLLM 0.11.1; the harness recorded them as hard-negative evidence and the LLM
explicitly stopped proposing EP.
## Notes / limitations
- For this workload the baseline (TP1) is already per-GPU optimal, so iterations-to-
*best* = 1; the remaining trials are the loop *confirming* no config beats baseline
before stopping. A workload with an under-tuned default would show an improving
trajectory; this run validates the stop/no-regression machinery, not a tuning win.
- The final stop came via `llm_after_veto_budget` (validator vetoed once, then
deferred), not a pure deterministic validator stop — because the deterministic
conditions (3-within-2%, saturation, validation-exhausted) did not cleanly fire
when every trial was a distinct config with a distinct per-GPU rate. The validator
acted as the *guard* (preventing premature stop), which is its designed role.
- 7 trials > the paper's 36 average, inflated by the wider search range, 2 EP
launch-failures, and the veto. Acceptable for a validation run.
- LLM token: the non-interactive shell lacks `OPENAI_API_KEY`; export it from the
codex `auth.json` (`~/.codex/auth.json`) before the run.

View File

@@ -0,0 +1,64 @@
# Stop-B end-to-end on dense Qwen3.5-27B (the improving trajectory) — 2026-06-16
Branch `feat/two-stop`. Real `gpt-5.4` agentic loop (codex/prism), Stop-A enabled,
length-aware TTFT SLO (4s + L_in/8k, TPOT ≤ 50 ms), vLLM 0.11.1, H20, GPUs 27,
`replay_time_scale=1.0`, `search.high=0.25`, `inherit_incumbent_floor=true`.
Config `dash0_qwen27b_stopB_loop.json`. Companion to the 30B run
(`stop-b-e2e-20260616.md`); together they cover all Stop-B behaviors.
## Trajectory (incumbent = TP4 @ 1.00 req/s/GPU)
| iter | proposed by | config | per_gpu | adopted? |
| --- | --- | --- | --- | --- |
| 1 | baseline | TP1 | 0.123 | incumbent |
| 2 | gpt-5.4 | TP2 | 0.2925 (2.4×) | ✅ new incumbent |
| 3 | gpt-5.4 | TP4 | **1.0012 (8.1×)** | ✅ new incumbent |
| 4 | gpt-5.4 | TP4 + chunked-prefill + mbt=16384 | 0.942 | ❌ **worse → rejected** |
| 5 | gpt-5.4 | TP2 + DP2 | (loop stopped before completing) | — |
(Run stopped manually after iter-4 — see "Why stopped" below. Incumbent preserved
at TP4.)
## What this demonstrates (the piece the 30B run could not)
- **A genuine improving climb.** `gpt-5.4` + the harness raised per-GPU throughput
TP1 → TP2 → TP4 (0.123 → 0.29 → 1.00, 8.1×), each step a correctly-diagnosed real
gain: TP1 is TPOT-bound, so the agent scaled tensor-parallelism, then — once
topology was won — pivoted to **runtime tuning on the winning family** (chunked
prefill + larger batched tokens).
- **No regression.** The runtime tweak (iter-4) measured *below* plain TP4
(0.942 < 1.00), and the harness correctly **kept TP4 as the incumbent** rather than
adopting the worse config the core Stop-B guarantee, shown live.
- Combined with the 30B run (search-high-saturation `validator`-authorized stop +
premature-LLM-stop veto), every Stop-B behavior is now validated end-to-end:
improving climb, correct bottleneck-driven proposals, no regression, deterministic
stop authority, and the LLM-stop veto.
## Process wins / findings
- **SIGTERM teardown fix validated in practice.** This loop was stopped with a plain
SIGTERM and the engine + EngineCore workers torn down cleanly GPUs 27 freed, no
orphan, no leaked memory (contrast: the pre-fix runs twice leaked GPU0/1). Commit
`b17b213`.
- **Timeout-as-failed-request fix** (`2fcaf80`) held no trial crashed on
request timeouts this run.
## Why stopped (efficiency finding — feeds next round)
The loop was stopped after iter-4 rather than run to an explicit Stop-B firing,
because each TP4-family trial took ~3 h: at `scale=1.0`, infeasible high-θ probes
each run to the **`early_stop_max_elapsed_s=900` cap** (`probe_elapsed_s>900`), and
the primary+fallback binary search doubles the probe count. Stop-A truncates a
*converged* replay but does not shortcut an *overloaded* probe that simply runs out
the clock. **For a practical agentic loop at scale=1.0, lower `early_stop_max_elapsed_s`
(≈300 s)** so overloaded probes die fast; consider also having an infeasible-and-
overloaded probe early-stop on a fast lag/throughput signal rather than the elapsed
cap. The convergence itself was already evident (iter-4's runtime tweak and the
queued TP2+DP2 were not beating TP4).
## Next
- Lower the elapsed cap and (optionally) re-run to capture the explicit Stop-B stop
on this 27B stack.
- Land the deferred items: more robust C feature for the low-reuse regime; Stop-C
cross-day retune trigger; §7 baselines (SCOOT/naive/community).

View File

@@ -0,0 +1,106 @@
# qwen27b-chat-0-8k TPOT25 16-Iter Harness Compare
## Goal
Rerun the internal vLLM Qwen3.5-27B chat 0-8k tuning comparison under a stricter
TPOT SLO:
- no-harness: 16 tuning iterations;
- harness: 16 tuning iterations, with permission to stop early if the harness
convergence guard decides no further GPU trial is needed.
Both variants must be launched directly through AITuner. No state seeding,
manual replay, or historical-result injection is allowed.
## Setup
- Host: `dash0`.
- Hardware: 8 NVIDIA H20 GPUs.
- Engine: internal vLLM at `/usr/local/bin/vllm`.
- Model:
`/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal`.
- Served model name: `qwen35-27b-aituner`.
- Workload window: `chat_w20260311_1000`.
- Trace path source: `/home/admin/cpfs/wjh/aituner/aituner/trace_windows/windows.json`.
- Request mode: `chat`.
- Input bucket: `0 <= input_length <= 8192`.
- Replay scale: `1.0`.
- Max concurrency: `32`.
- Max requests per probe: unset, so each probe uses the full selected trace
subset for its `sampling_u` threshold.
- Restart engine after early stop: `true` for both variants. This is needed
under TPOT25 because very slow infeasible probes can leave live HTTP requests
in the engine after the SLO is already unrecoverable. Restarting keeps the
next binary-search probe from being contaminated by previous in-flight work.
- Search field: `sampling_u`.
- Search range: `low=0.0`, `high=0.0625`.
- Search probes: `max_probes=6`, `tolerance=0.001`.
- Sampling seed: `20260325`.
## SLO
- Target pass rate: `0.95`.
- TTFT rule:
| Input tokens | TTFT threshold |
| ---: | ---: |
| `<=4096` | `2000 ms` |
| `<=32768` | `4000 ms` |
| otherwise | `6000 ms` |
- TPOT rule: fixed `<=25 ms`.
## Specs
Remote generated specs:
- no-harness:
`.aituner-tight/specs/dash0-qwen27b-chat-0-8k-tpot25-restart-16iter-noharness.json`
- harness:
`.aituner-tight/specs/dash0-qwen27b-chat-0-8k-tpot25-restart-16iter-harness.json`
The two specs were generated from
`configs/examples/dash0_qwen27b_tight_slo_run4_0_8k.json`. After normalizing
`study_id` and `llm.use_harness`, the JSON payloads compare equal. Therefore the
only tuning-behavior difference between the formal comparison runs is whether
the harness is enabled.
## Commands
No-harness:
```bash
PYTHONPATH=src python3 -m aituner.cli study tune \
--spec .aituner-tight/specs/dash0-qwen27b-chat-0-8k-tpot25-restart-16iter-noharness.json \
--store-root .aituner-tight \
--max-trials 16
```
Harness:
```bash
PYTHONPATH=src python3 -m aituner.cli study tune \
--spec .aituner-tight/specs/dash0-qwen27b-chat-0-8k-tpot25-restart-16iter-harness.json \
--store-root .aituner-tight \
--max-trials 16
```
## Run Log
- 2026-05-06 12:37 CST: generated both remote specs and verified that the only
normalized difference is `llm.use_harness`.
- 2026-05-06 12:37 CST: started no-harness in tmux session
`qwen27b_tpot25_noharness_16iter_20260506`.
- 2026-05-06 21:06 CST: stopped the initial no-harness pre-run before using it
for comparison. It used `restart_engine_after_early_stop=false`; the first
TP1 baseline probe already recorded `slo_pass_rate_unrecoverable`, but
unfinished requests remained live in vLLM and would contaminate the next probe.
- 2026-05-06 21:07 CST: generated the formal clean specs with
`restart_engine_after_early_stop=true` for both variants and verified the
normalized diff is still only `llm.use_harness`.
- 2026-05-06 21:09 CST: started formal no-harness run in tmux session
`qwen27b_tpot25_restart_noharness_16iter_20260506`.
## Results
Pending.

View File

@@ -0,0 +1,131 @@
# Qwen27B Chat 0-8k TPOT 40ms Baseline Infeasible Run
Date: 2026-05-07
## Goal
Re-run the internal vLLM + Qwen3.5-27B chat 0-8k tuning comparison after adding a study-level guard:
- if the automatic baseline trial has no feasible probe;
- and the lowest sampled request rate still fails the SLO target pass rate;
- then AITuner stops the whole study and reports that the SLO is too tight for the current setup.
This prevents spending the remaining tuning budget on LLM or harness proposals when the baseline itself demonstrates that the workload/SLO is infeasible at the search floor.
## Implementation
Commit: `f212673 Stop tuning when baseline is infeasible`
Changed behavior:
- `study tune` now persists `tuning_stop_reason` and `tuning_stop_diagnosis` in `state.json`.
- `study tune` also persists `tuning_stop_details`, including the lowest sampled probe's TTFT/TPOT mean, p50, p95, and p99.
- After the automatic baseline trial is ingested, AITuner checks the worker result:
- `status == completed`
- `best_request_rate is None`
- at least one probe exists
- all probes are infeasible
- If true, AITuner stops before asking the LLM or harness for any proposal.
- Re-running the same study respects the persisted stop state and does not resume tuning.
Validation:
```bash
python3 -m compileall -q src tests
PYTHONPATH=src python3 -m unittest tests.test_core_flow
```
Local and `dash0` both passed.
## Setup
Host: `dash0`
Remote repo: `/home/admin/cpfs/wjh/aituner/aituner`
Base spec: `configs/examples/dash0_qwen27b_tight_slo_run4_0_8k.json`
Model: `/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal`
Workload: chat, 0-8k input window
SLO:
- TTFT: existing step rule from the base spec
- TPOT: fixed `40ms`
- target pass rate: `0.95`
Search:
- Direct AITuner command: `python3 -m aituner.cli study tune ... --max-trials 12`
- No manual proposal/state edits during either run.
- Both variants used `CUDA_VISIBLE_DEVICES=0,1,2,4,5,6,7`; this was identical for both specs.
- The two specs were verified equal after normalizing only `study_id` and `llm.use_harness`.
Specs:
- no-harness: `.aituner-tight/specs/dash0-qwen27b-chat-0-8k-tpot40-gpu3skip-12iter-noharness-20260507.json`
- harness: `.aituner-tight/specs/dash0-qwen27b-chat-0-8k-tpot40-gpu3skip-12iter-harness-20260507.json`
## Commands
No harness:
```bash
PYTHONPATH=src python3 -m aituner.cli study tune \
--spec .aituner-tight/specs/dash0-qwen27b-chat-0-8k-tpot40-gpu3skip-12iter-noharness-20260507.json \
--store-root .aituner-tight \
--max-trials 12
```
Harness:
```bash
PYTHONPATH=src python3 -m aituner.cli study tune \
--spec .aituner-tight/specs/dash0-qwen27b-chat-0-8k-tpot40-gpu3skip-12iter-harness-20260507.json \
--store-root .aituner-tight \
--max-trials 12
```
## Results
Both runs stopped after the baseline trial. No LLM/harness proposal was evaluated because baseline had no feasible probe.
| Variant | Trials executed | Best request rate | Best request rate / GPU | Stop reason |
| --- | ---: | ---: | ---: | --- |
| no-harness | 1 | - | - | `baseline_all_infeasible` |
| harness | 1 | - | - | `baseline_all_infeasible` |
Baseline probe curve:
| sampling_u | request rate | pass rate | feasible | early stop reason |
| ---: | ---: | ---: | --- | --- |
| 0.03125 | 0.895 | 0.000000 | false | `slo_pass_rate_unrecoverable` |
| 0.015625 | 0.483333 | 0.137931 | false | `slo_pass_rate_unrecoverable` |
| 0.0078125 | 0.246667 | 0.236486 | false | `slo_pass_rate_unrecoverable` |
| 0.00390625 | 0.123333 | 0.189189 | false | `slo_pass_rate_unrecoverable` |
| 0.001953125 | 0.065000 | 0.205128 | false | `slo_pass_rate_unrecoverable` |
| 0.0009765625 | 0.035000 | 0.142857 | false | `slo_pass_rate_unrecoverable` |
Lowest request rate latency summary:
| Variant | request rate | pass rate | TTFT mean | TTFT p50 | TTFT p95 | TTFT p99 | TPOT mean | TPOT p50 | TPOT p95 | TPOT p99 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| no-harness | 0.035000 | 0.142857 | 1288.953ms | 446.586ms | 3011.814ms | 3011.814ms | 12.661ms | 13.141ms | 15.097ms | 15.097ms |
| harness | 0.035000 | 0.142857 | 1268.090ms | 445.274ms | 2889.080ms | 2889.080ms | 12.658ms | 13.170ms | 15.102ms | 15.102ms |
This shows that the TPOT threshold of `40ms` is not the binding constraint at the lowest sampled rate. The observed TPOT p99 is about `15.1ms`; failures are driven by TTFT and by the unrecoverable-pass-rate early stop after too many requests have already failed or been skipped.
Final diagnosis written by AITuner:
```text
Baseline configuration has no feasible probe under the current SLO. Stopping tuning because even the lowest sampled request rate did not meet the target pass rate. lowest_sampled_request_rate=0.035 lowest_sampling_u=0.000976562 lowest_probe_pass_rate=0.142857 early_stop_reason=slo_pass_rate_unrecoverable
```
## Interpretation
This run does not measure harness acceleration. It proves that the TPOT 40ms setup is infeasible for the current baseline and search floor: even at `0.035` aggregate request rate, only `14.29%` of requests pass the SLO, far below the `95%` target.
The correct behavior is to stop the study early and report SLO infeasibility instead of spending the remaining 11 trial slots. Harness cannot accelerate convergence when there is no feasible baseline point and no incumbent for guided tuning.
For a Fig. 18-style convergence comparison, the next setup must first have at least one feasible baseline or feasible low-rate point under the same metric definitions.

BIN
paper.pdf Normal file

Binary file not shown.

View File

@@ -92,17 +92,39 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args()
def stable_uniform(*, seed: int, window_id: str, index: int, row: dict[str, Any]) -> float:
def resolve_session_root(row: dict[str, Any], root_of: dict[Any, Any]) -> Any:
"""Resolve the session root chat_id for a trace row.
Sessions are multi-turn chains linked via parent_chat_id (turn>1 points to the
parent turn's chat_id, the root turn has parent_chat_id=-1). Because parent
turns precede their children in time, a single streaming pass that records
chat_id -> root resolves the full chain. Rows whose parent is not yet known
(e.g. it fell outside the materialized span) fall back to the parent id so
siblings still group together.
"""
chat_id = row.get("chat_id")
parent = row.get("parent_chat_id")
parent_is_root = (
parent is None
or (isinstance(parent, (int, float)) and not isinstance(parent, bool) and int(parent) < 0)
)
root = chat_id if parent_is_root else root_of.get(parent, parent)
if chat_id is not None:
root_of[chat_id] = root
return root
def session_uniform(*, seed: int, window_id: str, session_root: Any) -> float:
"""Deterministic per-session uniform score in [0, 1).
All turns of a session share one score, so thresholding sampling_u keeps or
drops whole sessions and preserves intra-session prefix (KV-cache) reuse.
"""
payload = json.dumps(
{
"seed": seed,
"window_id": window_id,
"index": index,
"timestamp": row.get("timestamp"),
"input_length": row.get("input_length"),
"output_length": row.get("output_length"),
"chat_id": row.get("chat_id"),
"turn": row.get("turn"),
"session_root": session_root,
},
sort_keys=True,
separators=(",", ":"),
@@ -241,12 +263,16 @@ def materialize_windows(
bucket = grouped[(trace_path, prompt_path)]
bucket.sort(key=lambda item: (float(item["window_start"]), str(item["window_id"])))
matched_rows = 0
root_of: dict[Any, Any] = {}
with trace_path.open() as trace_handle, prompt_path.open() as prompt_handle:
for trace_raw, prompt_raw in zip(trace_handle, prompt_handle):
trace_raw = trace_raw.strip()
if not trace_raw:
continue
trace_row = json.loads(trace_raw)
# Resolve session linkage for every row (even unmatched ones)
# so multi-turn chains crossing the window edge still group.
session_root = resolve_session_root(trace_row, root_of)
timestamp = float(trace_row.get("timestamp") or 0.0)
matched_window: dict[str, Any] | None = None
for window in bucket:
@@ -267,11 +293,11 @@ def materialize_windows(
start = float(matched_window["window_start"])
out["source_timestamp"] = timestamp
out["timestamp"] = timestamp - start
out["sampling_u"] = stable_uniform(
out["session_root"] = session_root
out["sampling_u"] = session_uniform(
seed=sample_seed,
window_id=window_id,
index=stats_by_window[window_id].num_requests,
row=merged,
session_root=session_root,
)
handles[window_id].write(json.dumps(out, ensure_ascii=False) + "\n")
stats_by_window[window_id].record(out)
@@ -311,7 +337,7 @@ def build_output_window(
output["num_excluded_too_long"] = 0
output["sampling_u_field"] = "sampling_u"
output["sampling_seed"] = int(sample_seed)
output["sampling_strategy"] = "fixed_uniform_score"
output["sampling_strategy"] = "session_coherent_uniform_score"
output["first_request_ts"] = stats.first_request_ts
output["last_request_ts"] = stats.last_request_ts
output["first_request_index"] = stats.first_request_index

View File

@@ -10,6 +10,7 @@ from aituner.llm import (
load_capability_profile,
parse_proposal_text,
)
from aituner.lca import build_study_workload_profile
from aituner.spec import load_study_spec
from aituner.store import StudyStore
from aituner.trace import load_trace_requests, summarize_window
@@ -89,6 +90,7 @@ def main() -> int:
window_summary=summarize_window(requests, window),
state=state,
capability_profile=capability_profile,
workload_profile=build_study_workload_profile(study, requests, window),
)
prompt_name = f"prompt-{state.next_trial_index:04d}"
store.write_prompt(study.study_id, prompt_name, prompt)

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Stop-A calibration: print the offered-L-C-A convergence curve for a raw trace window.
The convergence of prefix-vs-full L-C-A is a deterministic property of the trace
metadata (lengths, hash_ids, arrivals), so this runs on CPU without serving the
model. Use it to pick tau / tau_c / stable_checks and to compare how late the C
dimension converges across workloads (e.g. low-reuse chat vs high-reuse coder).
Example:
PYTHONPATH=src python3 scripts/stop_a_calibration.py \
--jsonl /dashscope/.../qwen_chat_blksz_64_032109-032111.jsonl \
--block-size 64 --window-start 3600 --window-end 4200 --gpu-count 8 --label chat
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from aituner.lca import find_convergence_prefix, resolve_length_mode
from aituner.trace import TraceRequest, WindowRecord
def _session_root(row: dict, root_of: dict) -> object:
chat_id = row.get("chat_id")
parent = row.get("parent_chat_id")
parent_is_root = parent is None or (
isinstance(parent, (int, float)) and not isinstance(parent, bool) and int(parent) < 0
)
root = chat_id if parent_is_root else root_of.get(parent, parent)
if chat_id is not None:
root_of[chat_id] = root
return root
def load_window(jsonl: Path, *, window_start: float, window_end: float) -> list[TraceRequest]:
root_of: dict = {}
requests: list[TraceRequest] = []
with jsonl.open(encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
row = json.loads(line)
_session_root(row, root_of) # keep chain complete even outside the window
ts = float(row.get("timestamp") or 0.0)
if not (window_start <= ts < window_end):
continue
hash_ids = row.get("hash_ids")
requests.append(
TraceRequest(
row_id=str(row.get("chat_id")),
arrival_s=ts - window_start,
sampling_u=1.0,
body={},
prompt_tokens_hint=int(row.get("input_length") or 0),
completion_tokens_hint=int(row.get("output_length") or 0),
metadata={"hash_ids": hash_ids if isinstance(hash_ids, list) else None},
)
)
requests.sort(key=lambda item: item.arrival_s)
return requests
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--jsonl", type=Path, required=True)
ap.add_argument("--block-size", type=int, required=True)
ap.add_argument("--window-start", type=float, default=3600.0)
ap.add_argument("--window-end", type=float, default=4200.0)
ap.add_argument("--gpu-count", type=int, default=8)
ap.add_argument("--length-mode", default="total")
ap.add_argument("--label", default="")
ap.add_argument("--tau", type=float, default=0.9)
ap.add_argument("--max-checks", type=int, default=20)
args = ap.parse_args()
requests = load_window(
args.jsonl, window_start=args.window_start, window_end=args.window_end
)
window = WindowRecord(
window_id=args.label or args.jsonl.stem,
trace_path=args.jsonl,
trace_type=args.label or "chat",
window_start=0.0,
window_end=float(args.window_end - args.window_start),
source_payload={"block_size": args.block_size},
)
mode = resolve_length_mode(length_mode=args.length_mode)
rows_with_hash = sum(1 for r in requests if r.metadata.get("hash_ids"))
print(
f"[{args.label}] requests={len(requests)} rows_with_hash_ids={rows_with_hash} "
f"window={args.window_start:.0f}-{args.window_end:.0f}s block_size={args.block_size}"
)
# Full curve (tau_c high so it never short-circuits; we read the curve directly).
point = find_convergence_prefix(
requests, window, gpu_count=args.gpu_count, length_mode=mode,
tau=args.tau, tau_c=1.01, stable_checks=10_000, max_checks=args.max_checks,
min_fraction=0.05,
)
print(" frac time_s L C A")
for c in point.checks:
s = c["family_similarity"]
print(
f" {c['fraction']:.2f} {c['time_s']:7.1f} "
f"{s['L']:.3f} {s['C']:.3f} {s['A']:.3f}"
)
# Stop fraction at candidate tau_c values (L,A >= tau, C >= tau_c, stable for W=3).
print(" -- stop fraction (tau_L=tau_A=%.2f, W=3) --" % args.tau)
for tau_c in (0.85, 0.90, 0.92, 0.95):
p = find_convergence_prefix(
requests, window, gpu_count=args.gpu_count, length_mode=mode,
tau=args.tau, tau_c=tau_c, stable_checks=3, max_checks=args.max_checks,
min_fraction=0.05,
)
verdict = (
f"stop@frac={p.fraction:.2f} t={p.stop_time_s:.0f}s"
if p.converged
else "NEVER (replays full window)"
)
print(f" tau_c={tau_c:.2f}: {verdict}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

101
scripts/stop_a_validate.py Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Validate Stop-A truncation fidelity from a full-replay trial's probe_details.
Given a completed trial that replayed the full window (adaptive_stop disabled), for
each probe recompute the L-C-A convergence prefix and compare the feasibility
verdict / pass-rate of the truncated prefix against the full probe. This answers:
"would Stop-A have changed the measured peak-sustainable-rate?" using only the one
full run (no second GPU run needed).
Example:
PYTHONPATH=src python3 scripts/stop_a_validate.py \
--spec configs/examples/dash0_qwen30b_a3b_stopA_fulldata.json \
--store-root .aituner/stopA-fulldata --tau 0.9 --tau-c 0.90
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from aituner.lca import find_convergence_prefix, resolve_length_mode
from aituner.spec import load_study_spec
from aituner.trace import load_trace_requests, select_requests_for_threshold
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--spec", type=Path, required=True)
ap.add_argument("--store-root", type=Path, required=True)
ap.add_argument("--tau", type=float, default=0.9)
ap.add_argument("--tau-c", type=float, default=0.90)
ap.add_argument("--stable-checks", type=int, default=3)
ap.add_argument("--target-pass-rate", type=float, default=0.95)
args = ap.parse_args()
study = load_study_spec(args.spec)
window, requests = load_trace_requests(study, study_spec_path=args.spec)
mode = resolve_length_mode(request_mode=study.trace.request_mode)
gpu_count = study.hardware.gpu_count
detail_files = sorted(args.store_root.glob("*/trials/*/probe_details.jsonl"))
if not detail_files:
print(f"no probe_details.jsonl under {args.store_root}")
return 1
print(f"target_pass_rate={args.target_pass_rate} tau={args.tau} tau_c={args.tau_c}")
print(
"thresh n_full stop_idx frac full_pass prefix_pass "
"full_feas prefix_feas verdict_match"
)
mismatches = 0
total = 0
saved_fractions = []
for detail_file in detail_files:
with detail_file.open(encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
probe = json.loads(line)
threshold = float(probe["threshold"])
outcomes = probe.get("outcomes") or []
# arrival-ordered outcomes that carry an arrival_s and verdict
ordered = sorted(
(o for o in outcomes if o.get("arrival_s") is not None),
key=lambda o: float(o["arrival_s"]),
)
n = len(ordered)
if n == 0:
continue
selected = select_requests_for_threshold(requests, threshold=threshold)
cp = find_convergence_prefix(
selected, window, gpu_count=gpu_count, length_mode=mode,
tau=args.tau, tau_c=args.tau_c, stable_checks=args.stable_checks,
)
# Map the convergence prefix fraction onto the replayed outcomes.
stop_n = max(1, min(n, round(cp.fraction * n)))
full_pass = sum(1 for o in ordered if o.get("evaluation")) / n
prefix_pass = sum(1 for o in ordered[:stop_n] if o.get("evaluation")) / stop_n
full_feas = full_pass >= args.target_pass_rate
prefix_feas = prefix_pass >= args.target_pass_rate
match = full_feas == prefix_feas
total += 1
mismatches += 0 if match else 1
saved_fractions.append(1.0 - cp.fraction)
print(
f"{threshold:.5f} {n:6d} {stop_n:7d} {cp.fraction:.2f} "
f"{full_pass:.3f} {prefix_pass:.3f} "
f"{str(full_feas):5s} {str(prefix_feas):5s} {match}"
)
if total:
avg_saved = sum(saved_fractions) / len(saved_fractions)
print(
f"\nverdict matches: {total - mismatches}/{total} "
f"mean replay saved: {avg_saved*100:.0f}%"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import argparse
import json
import sys
from dataclasses import replace
from pathlib import Path
from .compare import run_compare
@@ -12,13 +13,108 @@ from .harness import (
build_harness_stop_proposal,
)
from .job import append_job, build_trial_job
from .lca import (
build_study_workload_profile,
build_workload_profile,
resolve_length_mode,
similarity_report,
)
from .llm import build_prompt, call_llm_for_proposal, load_capability_profile, parse_proposal_text
from .spec import Proposal, SpecError, load_study_spec, to_jsonable
from .spec import (
Proposal,
SpecError,
StudySpec,
load_structured_file,
load_study_spec,
to_jsonable,
)
from .store import StudyStore
from .trace import load_trace_requests, summarize_window
from .worker import run_trial
def _is_empty_config_patch(proposal: Proposal) -> bool:
return not proposal.config_patch.env_patch and not proposal.config_patch.flag_patch
def _latency_percentiles(summary: object, metric: str) -> dict[str, float]:
if not isinstance(summary, dict):
return {}
payload = summary.get(metric)
if not isinstance(payload, dict):
return {}
selected: dict[str, float] = {}
for key in ("mean", "p50", "p95", "p99"):
value = payload.get(key)
if isinstance(value, (int, float)):
selected[key] = float(value)
return selected
def _format_latency_percentiles(metric: str, values: dict[str, float]) -> str:
if not values:
return ""
ordered = ", ".join(
f"{key}={values[key]:.3f}"
for key in ("mean", "p50", "p95", "p99")
if key in values
)
return f"{metric}({ordered})"
def _baseline_all_infeasible_stop(result: dict[str, object]) -> tuple[str, dict[str, object]] | None:
if result.get("status") != "completed":
return None
if isinstance(result.get("best_request_rate"), (int, float)):
return None
probes = result.get("probes")
if not isinstance(probes, list) or not probes:
return None
if any(isinstance(probe, dict) and probe.get("feasible") for probe in probes):
return None
diagnostics = result.get("all_infeasible_diagnostics")
if not isinstance(diagnostics, dict):
diagnostics = {}
lowest_rate = diagnostics.get("request_rate")
lowest_threshold = diagnostics.get("threshold")
pass_rate = diagnostics.get("pass_rate")
early_stop_reason = str(diagnostics.get("early_stop_reason") or "").strip()
latency_summary = diagnostics.get("latency_summary")
ttft = _latency_percentiles(latency_summary, "ttft_ms")
tpot = _latency_percentiles(latency_summary, "tpot_ms")
details: dict[str, object] = {
"lowest_sampled_request_rate": lowest_rate,
"lowest_sampling_u": lowest_threshold,
"lowest_probe_pass_rate": pass_rate,
"early_stop_reason": early_stop_reason,
"lowest_probe_latency_ms": {
"ttft": ttft,
"tpot": tpot,
},
"lowest_probe_latency_summary": latency_summary if isinstance(latency_summary, dict) else {},
}
pieces = [
"Baseline configuration has no feasible probe under the current SLO.",
"Stopping tuning because even the lowest sampled request rate did not meet the target pass rate.",
]
if isinstance(lowest_rate, (int, float)):
pieces.append(f"lowest_sampled_request_rate={float(lowest_rate):.6g}")
if isinstance(lowest_threshold, (int, float)):
pieces.append(f"lowest_sampling_u={float(lowest_threshold):.6g}")
if isinstance(pass_rate, (int, float)):
pieces.append(f"lowest_probe_pass_rate={float(pass_rate):.6g}")
if early_stop_reason:
pieces.append(f"early_stop_reason={early_stop_reason}")
for item in (
_format_latency_percentiles("lowest_probe_ttft_ms", ttft),
_format_latency_percentiles("lowest_probe_tpot_ms", tpot),
):
if item:
pieces.append(item)
return " ".join(pieces), details
def _study_source_path(study_root: Path) -> Path:
return Path((study_root / "study_spec.source").read_text(encoding="utf-8").strip())
@@ -45,6 +141,7 @@ def cmd_study_prompt(args: argparse.Namespace) -> int:
window_summary=summarize_window(requests, window),
state=state,
capability_profile=capability_profile,
workload_profile=build_study_workload_profile(study, requests, window),
)
prompt_name = args.prompt_name or f"prompt-{state.next_trial_index:04d}"
path = store.write_prompt(study.study_id, prompt_name, prompt)
@@ -65,8 +162,13 @@ def cmd_study_llm_propose(args: argparse.Namespace) -> int:
window_summary=summarize_window(requests, window),
state=state,
capability_profile=capability_profile,
workload_profile=build_study_workload_profile(study, requests, window),
)
proposal_text = call_llm_for_proposal(
policy=study.llm,
prompt=prompt,
use_harness=study.llm.use_harness,
)
proposal_text = call_llm_for_proposal(policy=study.llm, prompt=prompt)
proposal = parse_proposal_text(proposal_text, study)
name = args.proposal_name or f"proposal-{state.next_trial_index:04d}"
path = store.write_proposal(study.study_id, name, proposal)
@@ -124,15 +226,34 @@ def cmd_study_tune(args: argparse.Namespace) -> int:
if proposal_files and max_trials > len(proposal_files):
max_trials = len(proposal_files)
executed: list[dict[str, object]] = []
stop_vetoes = 0
max_llm_stop_vetoes = 1
for idx in range(max_trials):
state = store.load_state(study.study_id)
if state.tuning_stop_reason:
executed.append(
{
"trial_id": None,
"stopped": True,
"reason": state.tuning_stop_reason,
"diagnosis": state.tuning_stop_diagnosis,
"details": state.tuning_stop_details,
"state_best_trial_id": state.best_trial_id,
"state_best_request_rate": state.best_request_rate,
}
)
break
if state.next_trial_index > max_trials:
break
window, requests = load_trace_requests(study, study_spec_path=spec_path)
window_summary = summarize_window(requests, window)
workload_profile = build_study_workload_profile(study, requests, window)
harness_context = (
build_harness_context(
study=study,
window_summary=window_summary,
state=state,
workload_profile=workload_profile,
)
if study.llm.use_harness
else None
@@ -142,6 +263,7 @@ def cmd_study_tune(args: argparse.Namespace) -> int:
window_summary=window_summary,
state=state,
capability_profile=capability_profile,
workload_profile=workload_profile,
)
prompt_name = f"prompt-{state.next_trial_index:04d}"
store.write_prompt(study.study_id, prompt_name, prompt)
@@ -169,7 +291,10 @@ def cmd_study_tune(args: argparse.Namespace) -> int:
ensure_ascii=False,
)
elif proposal_files:
proposal_source = proposal_files[idx]
proposal_index = state.next_trial_index - 1
if proposal_index >= len(proposal_files):
break
proposal_source = proposal_files[proposal_index]
proposal_text = proposal_source.read_text(encoding="utf-8")
proposal_name = proposal_source.stem
else:
@@ -200,14 +325,45 @@ def cmd_study_tune(args: argparse.Namespace) -> int:
"No proposal files provided, study.llm.endpoint is not configured, "
"and the harness stop guard did not fire."
)
proposal_text = call_llm_for_proposal(policy=study.llm, prompt=prompt)
proposal_text = call_llm_for_proposal(
policy=study.llm,
prompt=prompt,
use_harness=study.llm.use_harness,
)
proposal_name = f"proposal-{state.next_trial_index:04d}"
raw_proposal_path = store.study_root(study.study_id) / "proposals" / f"{proposal_name}.raw.txt"
raw_proposal_path.write_text(proposal_text, encoding="utf-8")
proposal = parse_proposal_text(proposal_text, study)
store.write_proposal(study.study_id, proposal_name, proposal)
if proposal.should_stop:
if proposal_name.startswith("harness-stop-"):
is_harness_stop = proposal_name.startswith("harness-stop-")
is_llm_stop = not is_harness_stop and proposal_source is None
stop_authority = (
harness_context.get("stop_authority")
if isinstance(harness_context, dict)
else None
)
authorized = stop_authority is None or bool(stop_authority.get("authorized"))
# Stop-B authority: the deterministic validator overrides an
# LLM-originated stop. Veto an unauthorized stop (bounded) so the
# loop does not converge prematurely on the agent's say-so alone.
if is_llm_stop and not authorized and stop_vetoes < max_llm_stop_vetoes:
stop_vetoes += 1
executed.append(
{
"trial_id": None,
"proposal_name": proposal_name,
"proposal_source": "llm",
"stop_vetoed": True,
"reason": "validator_did_not_authorize_stop",
"validator_reason": (
stop_authority.get("reason") if stop_authority else None
),
"diagnosis": proposal.diagnosis,
}
)
continue
if is_harness_stop:
proposal_source_label = "harness"
else:
proposal_source_label = str(proposal_source) if proposal_source else "llm"
@@ -217,12 +373,26 @@ def cmd_study_tune(args: argparse.Namespace) -> int:
"proposal_name": proposal_name,
"proposal_source": proposal_source_label,
"stopped": True,
"stop_authorized_by": (
"validator"
if (is_harness_stop or authorized)
else "file_proposal"
if proposal_source is not None
else "llm_after_veto_budget"
),
"diagnosis": proposal.diagnosis,
"state_best_trial_id": state.best_trial_id,
"state_best_request_rate": state.best_request_rate,
}
)
break
is_auto_baseline = (
not proposal_files
and not args.skip_baseline
and state.next_trial_index == 1
and not state.trials
and _is_empty_config_patch(proposal)
)
trial, _ = store.materialize_trial(study=study, state=state, proposal=proposal)
trial_spec_path = Path(trial.artifact_dir) / "trial_spec.json"
result = run_trial(trial_spec_path)
@@ -243,6 +413,26 @@ def cmd_study_tune(args: argparse.Namespace) -> int:
"state_best_request_rate": state.best_request_rate,
}
)
if is_auto_baseline:
stop = _baseline_all_infeasible_stop(result)
if stop is not None:
diagnosis, details = stop
state.tuning_stop_reason = "baseline_all_infeasible"
state.tuning_stop_diagnosis = diagnosis
state.tuning_stop_details = details
store.save_state(state)
executed.append(
{
"trial_id": None,
"stopped": True,
"reason": state.tuning_stop_reason,
"diagnosis": diagnosis,
"details": details,
"state_best_trial_id": state.best_trial_id,
"state_best_request_rate": state.best_request_rate,
}
)
break
final_state = store.load_state(study.study_id)
print(
@@ -252,6 +442,9 @@ def cmd_study_tune(args: argparse.Namespace) -> int:
"executed_trials": executed,
"best_trial_id": final_state.best_trial_id,
"best_request_rate": final_state.best_request_rate,
"tuning_stop_reason": final_state.tuning_stop_reason,
"tuning_stop_diagnosis": final_state.tuning_stop_diagnosis,
"tuning_stop_details": final_state.tuning_stop_details,
},
ensure_ascii=False,
)
@@ -284,6 +477,159 @@ def cmd_compare_run(args: argparse.Namespace) -> int:
return 0
def _resolve_profile_gpu_count(args: argparse.Namespace, study: StudySpec) -> int:
gpu_count = args.gpu_count
if gpu_count is None:
gpu_count = study.hardware.gpu_count
if gpu_count <= 0:
raise SpecError("--gpu-count must be > 0.")
return int(gpu_count)
def _load_profile_study_spec(spec_path: Path) -> StudySpec:
payload = dict(load_structured_file(spec_path))
llm_payload = dict(payload.get("llm") or {})
llm_payload.pop("endpoint", None)
payload["llm"] = llm_payload
return StudySpec.from_dict(payload)
def _profile_current_study_window(args: argparse.Namespace) -> dict[str, object]:
spec_path = Path(args.spec).resolve()
study = _load_profile_study_spec(spec_path)
mode = resolve_length_mode(
request_mode=study.trace.request_mode,
length_mode=args.length_mode,
)
window, requests = load_trace_requests(study, study_spec_path=spec_path)
profile = build_workload_profile(
requests,
window,
gpu_count=_resolve_profile_gpu_count(args, study),
length_mode=mode,
)
return {
"profile": profile.to_dict(),
"source": {
"study_spec_path": str(spec_path),
"window_id": study.trace.window_id,
},
}
def _resolve_windows_path_for_profile(study: StudySpec, *, study_spec_path: Path) -> Path:
path = Path(study.trace.windows_path)
if not path.is_absolute():
path = (study_spec_path.parent / path).resolve()
return path
def _load_profile_windows(
study: StudySpec,
*,
study_spec_path: Path,
) -> list[dict[str, object]]:
windows_path = _resolve_windows_path_for_profile(study, study_spec_path=study_spec_path)
payload = json.loads(windows_path.read_text(encoding="utf-8"))
raw_windows = payload.get("windows") if isinstance(payload, dict) else payload
if not isinstance(raw_windows, list):
raise SpecError(f"windows payload must contain a list: {windows_path}")
return [
{str(key): value for key, value in item.items()}
for item in raw_windows
if isinstance(item, dict)
]
def _selected_profile_windows(
args: argparse.Namespace,
study: StudySpec,
*,
study_spec_path: Path,
) -> list[dict[str, object]]:
windows = _load_profile_windows(study, study_spec_path=study_spec_path)
window_ids = set(args.window_id or [])
selected: list[dict[str, object]] = []
for item in windows:
window_id = str(item.get("window_id") or "").strip()
if not window_id:
continue
if window_ids and window_id not in window_ids:
continue
if not window_ids and not args.all:
if window_id != study.trace.window_id:
continue
trace_type = str(item.get("trace_type") or "").strip()
if args.trace_type and trace_type != args.trace_type:
continue
date_value = str(item.get("date") or "").strip()
if args.date_from and date_value and date_value < args.date_from:
continue
if args.date_to and date_value and date_value > args.date_to:
continue
if args.slot_token and str(item.get("slot_token") or "").strip() != args.slot_token:
continue
selected.append(item)
selected.sort(
key=lambda item: (
str(item.get("date") or ""),
str(item.get("slot_token") or ""),
str(item.get("window_id") or ""),
)
)
if args.limit is not None:
selected = selected[: args.limit]
if not selected:
raise SpecError("No trace windows selected for profile similarity.")
return selected
def cmd_profile_window(args: argparse.Namespace) -> int:
print(json.dumps(_profile_current_study_window(args), ensure_ascii=False, indent=2))
return 0
def cmd_profile_similarity(args: argparse.Namespace) -> int:
spec_path = Path(args.spec).resolve()
study = _load_profile_study_spec(spec_path)
mode = resolve_length_mode(
request_mode=study.trace.request_mode,
length_mode=args.length_mode,
)
gpu_count = _resolve_profile_gpu_count(args, study)
profiles = []
selected = _selected_profile_windows(args, study, study_spec_path=spec_path)
for item in selected:
window_id = str(item["window_id"])
window_study = replace(study, trace=replace(study.trace, window_id=window_id))
window, requests = load_trace_requests(window_study, study_spec_path=spec_path)
profiles.append(
build_workload_profile(
requests,
window,
gpu_count=gpu_count,
length_mode=mode,
)
)
print(
json.dumps(
{
"source": {
"study_spec_path": str(spec_path),
"selected_window_count": len(profiles),
"length_mode": mode,
"gpu_count": gpu_count,
},
"profiles": [profile.to_dict() for profile in profiles],
"similarity": similarity_report(profiles),
},
ensure_ascii=False,
indent=2,
)
)
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="AITuner CLI")
subparsers = parser.add_subparsers(dest="command", required=True)
@@ -352,6 +698,50 @@ def build_parser() -> argparse.ArgumentParser:
compare_run.add_argument("--output-root")
compare_run.set_defaults(func=cmd_compare_run)
profile = subparsers.add_parser("profile")
profile_sub = profile.add_subparsers(dest="profile_command", required=True)
profile_window = profile_sub.add_parser("window")
profile_window.add_argument("--spec", required=True)
profile_window.add_argument(
"--length-mode",
default="auto",
choices=["auto", "total", "input", "output"],
help="Token length basis for the L vector. auto uses output for decode_only and total otherwise.",
)
profile_window.add_argument(
"--gpu-count",
type=int,
help="GPU denominator for per-GPU arrival rate. Defaults to hardware.gpu_count.",
)
profile_window.set_defaults(func=cmd_profile_window)
profile_similarity = profile_sub.add_parser("similarity")
profile_similarity.add_argument("--spec", required=True)
profile_similarity.add_argument("--window-id", action="append")
profile_similarity.add_argument("--trace-type")
profile_similarity.add_argument("--date-from")
profile_similarity.add_argument("--date-to")
profile_similarity.add_argument("--slot-token")
profile_similarity.add_argument("--limit", type=int)
profile_similarity.add_argument(
"--all",
action="store_true",
help="Profile all windows selected by filters. Without this or --window-id, only the study window is used.",
)
profile_similarity.add_argument(
"--length-mode",
default="auto",
choices=["auto", "total", "input", "output"],
help="Token length basis for the L vector. auto uses output for decode_only and total otherwise.",
)
profile_similarity.add_argument(
"--gpu-count",
type=int,
help="GPU denominator for per-GPU arrival rate. Defaults to hardware.gpu_count.",
)
profile_similarity.set_defaults(func=cmd_profile_similarity)
return parser

File diff suppressed because it is too large Load Diff

View File

@@ -179,6 +179,9 @@ def chat_completion(
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise HttpClientError(f"llm_completion failed: {exc.code} {detail}") from exc
except OSError as exc:
# TimeoutError (socket.timeout), URLError, ConnectionError all subclass OSError.
raise HttpClientError(f"llm_completion failed: {exc}") from exc
def stream_text_completion(
@@ -232,6 +235,8 @@ def stream_text_completion(
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise HttpClientError(f"stream_text_completion failed: {exc.code} {detail}") from exc
except OSError as exc:
raise HttpClientError(f"stream_text_completion failed: {exc}") from exc
return "".join(parts)
@@ -293,6 +298,10 @@ def stream_chat_completion(
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise HttpClientError(f"stream_chat_completion failed: {exc.code} {detail}") from exc
except OSError as exc:
# A request that exceeds request_timeout_s raises TimeoutError mid-stream;
# treat it as a failed request (SLO miss), not a crashed trial.
raise HttpClientError(f"stream_chat_completion failed: {exc}") from exc
ttft_ms = None if first_token_at is None else (first_token_at - start) * 1000.0
if completion_tokens is None and chunk_token_count > 0:
completion_tokens = chunk_token_count
@@ -310,7 +319,7 @@ def stream_chat_completion(
return StreamMetrics(
ttft_ms=ttft_ms,
tpot_ms=tpot_ms,
completion_tokens=used_tokens if used_tokens > 0 else None,
completion_tokens=used_tokens if used_tokens is not None and used_tokens > 0 else None,
completion_tokens_source=completion_tokens_source,
streamed_chunk_count=chunk_token_count,
)

577
src/aituner/lca.py Normal file
View File

@@ -0,0 +1,577 @@
from __future__ import annotations
import json
import math
import statistics
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Sequence
from .trace import TraceRequest, WindowRecord
if TYPE_CHECKING:
from .spec import StudySpec
EPSILON = 1e-9
FEATURE_NAMES = [
"L.log_mean_length",
"L.log_p95_over_mean_length",
"L.cv_length",
"C.log_mean_hit_length",
"C.log_p95_over_mean_hit_length",
"C.cv_hit_length",
"C.hit_rate",
"A.log_request_rate_per_gpu",
"A.cv_interarrival",
"A.log_fano_1s",
]
FAMILY_SLICES = {
"L": slice(0, 3),
"C": slice(3, 7),
"A": slice(7, 10),
}
LENGTH_MODES = {"total", "input", "output"}
@dataclass(frozen=True)
class WorkloadProfile:
window_id: str
trace_type: str
request_count: int
duration_s: float
gpu_count: int
length_mode: str
feature_names: list[str]
vector: list[float]
stats: dict[str, Any]
def to_dict(self) -> dict[str, Any]:
return {
"window_id": self.window_id,
"trace_type": self.trace_type,
"request_count": self.request_count,
"duration_s": self.duration_s,
"gpu_count": self.gpu_count,
"length_mode": self.length_mode,
"feature_names": self.feature_names,
"vector": self.vector,
"stats": self.stats,
}
@dataclass(frozen=True)
class RobustScale:
feature_names: list[str]
center: list[float]
scale: list[float]
def transform(self, vector: Sequence[float]) -> list[float]:
return [
(float(value) - self.center[idx]) / self.scale[idx]
for idx, value in enumerate(vector)
]
def to_dict(self) -> dict[str, Any]:
return {
"feature_names": self.feature_names,
"center": self.center,
"scale": self.scale,
}
def resolve_length_mode(*, request_mode: str | None = None, length_mode: str = "auto") -> str:
normalized = str(length_mode or "auto").strip().lower()
if normalized == "auto":
return (
"output"
if str(request_mode or "").strip().lower() == "decode_only"
else "total"
)
if normalized not in LENGTH_MODES:
raise ValueError(
"length_mode must be one of: auto, total, input, output."
)
return normalized
def build_workload_profile(
requests: list[TraceRequest],
window: WindowRecord,
*,
gpu_count: int,
length_mode: str = "total",
) -> WorkloadProfile:
if gpu_count <= 0:
raise ValueError("gpu_count must be > 0.")
if length_mode not in LENGTH_MODES:
raise ValueError(f"Unsupported length_mode: {length_mode}")
duration_s = _duration_s(requests, window)
input_lengths = [float(item.prompt_tokens_hint or 0) for item in requests]
output_lengths = [float(item.completion_tokens_hint or 0) for item in requests]
profile_lengths = [
_profile_length(input_len, output_len, length_mode=length_mode)
for input_len, output_len in zip(input_lengths, output_lengths)
]
hit_lengths, cache_stats = _ideal_cache_hit_lengths(
requests,
input_lengths=input_lengths,
block_size=_block_size(window),
)
arrival_stats = _arrival_stats(requests, duration_s=duration_s, gpu_count=gpu_count)
length_stats = _series_stats(profile_lengths)
hit_stats = _series_stats(hit_lengths)
total_profile_length = sum(profile_lengths)
total_input_length = sum(input_lengths)
total_hit_length = sum(hit_lengths)
feature_hit_rate = (
float(total_hit_length / max(total_profile_length, EPSILON))
if total_profile_length > 0
else 0.0
)
input_hit_rate = (
float(total_hit_length / max(total_input_length, EPSILON))
if total_input_length > 0
else 0.0
)
vector = [
math.log1p(length_stats["mean"]),
math.log1p(length_stats["p95"] / max(length_stats["mean"], EPSILON)),
length_stats["cv"],
math.log1p(hit_stats["mean"]),
math.log1p(hit_stats["p95"] / max(hit_stats["mean"], EPSILON)),
hit_stats["cv"],
feature_hit_rate,
math.log1p(arrival_stats["request_rate_per_gpu"]),
arrival_stats["interarrival_cv"],
math.log1p(arrival_stats["fano_1s"]),
]
return WorkloadProfile(
window_id=window.window_id,
trace_type=window.trace_type,
request_count=len(requests),
duration_s=duration_s,
gpu_count=int(gpu_count),
length_mode=length_mode,
feature_names=list(FEATURE_NAMES),
vector=[float(item) for item in vector],
stats={
"length": {
**length_stats,
"mode": length_mode,
"total": total_profile_length,
"input_total": total_input_length,
"output_total": sum(output_lengths),
},
"cache": {
**hit_stats,
**cache_stats,
"total_hit_length": total_hit_length,
"hit_rate": feature_hit_rate,
"input_hit_rate": input_hit_rate,
},
"arrival": arrival_stats,
},
)
def build_study_workload_profile(
study: "StudySpec",
requests: list[TraceRequest],
window: WindowRecord,
) -> WorkloadProfile:
"""Canonical L-C-A profile for a study's loaded window.
This is the single source of truth for the paper's 10-dimensional L-C-A
feature vector used by the harness prompt and (later) by Stop-A.
"""
mode = resolve_length_mode(
request_mode=study.trace.request_mode,
length_mode="auto",
)
return build_workload_profile(
requests,
window,
gpu_count=study.hardware.gpu_count,
length_mode=mode,
)
def fit_robust_scale(profiles: Sequence[WorkloadProfile]) -> RobustScale:
if not profiles:
raise ValueError("At least one profile is required to fit a robust scale.")
centers: list[float] = []
scales: list[float] = []
for idx in range(len(FEATURE_NAMES)):
values = [float(profile.vector[idx]) for profile in profiles]
median = _percentile(values, 50.0)
iqr = _percentile(values, 75.0) - _percentile(values, 25.0)
centers.append(float(median))
scales.append(float(iqr if abs(iqr) > EPSILON else 1.0))
return RobustScale(feature_names=list(FEATURE_NAMES), center=centers, scale=scales)
def profile_similarity(
left: WorkloadProfile,
right: WorkloadProfile,
*,
scale: RobustScale | None = None,
) -> float:
scaler = scale or fit_robust_scale([left, right])
z_left = scaler.transform(left.vector)
z_right = scaler.transform(right.vector)
return _similarity_from_z(z_left, z_right)
def similarity_report(profiles: Sequence[WorkloadProfile]) -> dict[str, Any]:
if not profiles:
raise ValueError("At least one profile is required.")
scale = fit_robust_scale(profiles)
transformed = [scale.transform(profile.vector) for profile in profiles]
rows: list[dict[str, Any]] = []
matrix: list[list[float]] = []
for i, left in enumerate(profiles):
row_values: list[float] = []
for j, right in enumerate(profiles):
sim = _similarity_from_z(transformed[i], transformed[j])
row_values.append(sim)
rows.append(
{
"left": left.window_id,
"right": right.window_id,
"similarity": sim,
"family_similarity": _family_similarity(transformed[i], transformed[j]),
}
)
matrix.append(row_values)
return {
"feature_names": list(FEATURE_NAMES),
"scaler": scale.to_dict(),
"windows": [profile.window_id for profile in profiles],
"matrix": matrix,
"pairs": rows,
}
@dataclass(frozen=True)
class ConvergencePoint:
converged: bool
stop_index: int
stop_time_s: float
fraction: float
family_similarity: dict[str, float]
checks: list[dict[str, Any]]
def to_dict(self) -> dict[str, Any]:
return {
"converged": self.converged,
"stop_index": self.stop_index,
"stop_time_s": self.stop_time_s,
"fraction": self.fraction,
"family_similarity": self.family_similarity,
"checks": self.checks,
}
def find_convergence_prefix(
requests: list[TraceRequest],
window: WindowRecord,
*,
gpu_count: int,
length_mode: str = "total",
tau: float = 0.9,
tau_c: float = 0.92,
stable_checks: int = 3,
max_checks: int = 20,
min_fraction: float = 0.1,
) -> ConvergencePoint:
"""Earliest arrival-ordered prefix whose offered L-C-A converges to the full set.
The L-C-A vector is a deterministic function of the trace metadata, so the
convergence of prefix-vs-full is itself deterministic (the paper's Fig. 9
curve). Stop-A replays only up to this prefix. A prefix counts as converged
when the L and A family similarities reach ``tau`` and the (slowest) C family
similarity reaches the stricter ``tau_c`` for ``stable_checks`` consecutive
checkpoints. If that never happens within the window the point reports the
full set (converged=False), which keeps the C-gate honest: an unconverged C
means the probe must replay the whole window rather than stop early.
"""
total = len(requests)
if total == 0:
return ConvergencePoint(
converged=False,
stop_index=0,
stop_time_s=0.0,
fraction=1.0,
family_similarity={"L": 1.0, "C": 1.0, "A": 1.0},
checks=[],
)
# Compare each arrival-ordered prefix to the whole set, both measured over
# their own elapsed span so the A (rate) dimension is comparable rather than
# diluted by the fixed window length.
target = _prefix_profile(
requests, total, window, gpu_count=gpu_count, length_mode=length_mode
)
indices = _checkpoint_indices(
total, max_checks=max_checks, min_fraction=min_fraction
)
checks: list[dict[str, Any]] = []
consecutive = 0
converged_index: int | None = None
converged_sims: dict[str, float] | None = None
for index in indices:
prefix = _prefix_profile(
requests, index, window, gpu_count=gpu_count, length_mode=length_mode
)
sims = _family_similarity(target.vector, prefix.vector)
checks.append(
{
"index": index,
"fraction": float(index / total),
"time_s": float(requests[index - 1].arrival_s),
"family_similarity": sims,
}
)
passed = sims["L"] >= tau and sims["A"] >= tau and sims["C"] >= tau_c
consecutive = consecutive + 1 if passed else 0
if consecutive >= stable_checks and converged_index is None:
converged_index = index
converged_sims = sims
break
if converged_index is None:
last_sims = checks[-1]["family_similarity"] if checks else {"L": 1.0, "C": 1.0, "A": 1.0}
return ConvergencePoint(
converged=False,
stop_index=total,
stop_time_s=float(requests[-1].arrival_s),
fraction=1.0,
family_similarity=last_sims,
checks=checks,
)
return ConvergencePoint(
converged=True,
stop_index=converged_index,
stop_time_s=float(requests[converged_index - 1].arrival_s),
fraction=float(converged_index / total),
family_similarity=converged_sims or {},
checks=checks,
)
def _prefix_profile(
requests: list[TraceRequest],
index: int,
window: WindowRecord,
*,
gpu_count: int,
length_mode: str,
) -> WorkloadProfile:
prefix = requests[:index]
start = float(prefix[0].arrival_s) if prefix else float(window.window_start)
end = float(prefix[-1].arrival_s) if prefix else float(window.window_start)
prefix_window = WindowRecord(
window_id=window.window_id,
trace_path=window.trace_path,
trace_type=window.trace_type,
window_start=start,
window_end=end,
source_payload=window.source_payload,
)
return build_workload_profile(
prefix, prefix_window, gpu_count=gpu_count, length_mode=length_mode
)
def _checkpoint_indices(total: int, *, max_checks: int, min_fraction: float) -> list[int]:
start = max(1, int(math.ceil(min_fraction * total)))
if total <= max_checks:
candidates = range(start, total + 1)
else:
step = max(1, total // max_checks)
candidates = list(range(start, total + 1, step))
if candidates and candidates[-1] != total:
candidates.append(total)
seen: list[int] = []
for value in candidates:
clamped = min(total, max(1, int(value)))
if not seen or seen[-1] != clamped:
seen.append(clamped)
return seen
def dumps_profile(profile: WorkloadProfile) -> str:
return json.dumps(profile.to_dict(), ensure_ascii=False, indent=2) + "\n"
def _duration_s(requests: list[TraceRequest], window: WindowRecord) -> float:
duration = max(float(window.window_end) - float(window.window_start), 0.0)
if duration > 0:
return duration
if len(requests) >= 2:
return max(0.0, float(requests[-1].arrival_s) - float(requests[0].arrival_s))
return 0.0
def _profile_length(input_length: float, output_length: float, *, length_mode: str) -> float:
if length_mode == "input":
return max(input_length, 0.0)
if length_mode == "output":
return max(output_length, 0.0)
return max(input_length, 0.0) + max(output_length, 0.0)
def _block_size(window: WindowRecord) -> int:
value = window.source_payload.get("block_size")
if isinstance(value, bool):
return 1
if isinstance(value, (int, float)) and value > 0:
return int(value)
if isinstance(value, str) and value.strip():
try:
parsed = int(value)
except ValueError:
return 1
return parsed if parsed > 0 else 1
return 1
def _ideal_cache_hit_lengths(
requests: list[TraceRequest],
*,
input_lengths: list[float],
block_size: int,
) -> tuple[list[float], dict[str, Any]]:
seen_hashes: set[Any] = set()
hit_lengths: list[float] = []
total_blocks = 0
repeated_blocks = 0
rows_with_hash_ids = 0
for request, input_length in zip(requests, input_lengths):
hash_ids = request.metadata.get("hash_ids")
if not isinstance(hash_ids, list):
hit_lengths.append(0.0)
continue
rows_with_hash_ids += 1
repeated_for_request = 0
for hash_id in hash_ids:
total_blocks += 1
if hash_id in seen_hashes:
repeated_blocks += 1
repeated_for_request += 1
else:
seen_hashes.add(hash_id)
hit_lengths.append(float(min(max(input_length, 0.0), repeated_for_request * block_size)))
return hit_lengths, {
"block_size": block_size,
"rows_with_hash_ids": rows_with_hash_ids,
"total_blocks": total_blocks,
"repeated_blocks": repeated_blocks,
"repeated_block_ratio": (
float(repeated_blocks / total_blocks) if total_blocks else 0.0
),
}
def _arrival_stats(
requests: list[TraceRequest],
*,
duration_s: float,
gpu_count: int,
) -> dict[str, Any]:
arrivals = [float(item.arrival_s) for item in requests]
interarrivals = [
max(0.0, arrivals[idx] - arrivals[idx - 1])
for idx in range(1, len(arrivals))
]
per_second_counts = _per_second_counts(arrivals, duration_s=duration_s)
qps = float(len(requests) / duration_s) if duration_s > 0 else 0.0
return {
"request_rate": qps,
"request_rate_per_gpu": float(qps / gpu_count) if gpu_count > 0 else 0.0,
"interarrival_cv": _cv(interarrivals),
"fano_1s": _fano(per_second_counts),
"one_second_count_mean": statistics.fmean(per_second_counts)
if per_second_counts
else 0.0,
"one_second_count_variance": statistics.pvariance(per_second_counts)
if len(per_second_counts) >= 2
else 0.0,
"one_second_bin_count": len(per_second_counts),
}
def _per_second_counts(arrivals: list[float], *, duration_s: float) -> list[float]:
if duration_s <= 0:
return [float(len(arrivals))] if arrivals else []
bin_count = max(1, int(math.ceil(duration_s)))
counts = [0.0 for _ in range(bin_count)]
for arrival in arrivals:
if arrival < 0:
continue
idx = int(math.floor(arrival))
if 0 <= idx < bin_count:
counts[idx] += 1.0
return counts
def _series_stats(values: list[float]) -> dict[str, float]:
return {
"count": float(len(values)),
"mean": statistics.fmean(values) if values else 0.0,
"p50": _percentile(values, 50.0),
"p95": _percentile(values, 95.0),
"cv": _cv(values),
}
def _cv(values: list[float]) -> float:
if not values:
return 0.0
mean = statistics.fmean(values)
if abs(mean) <= EPSILON:
return 0.0
return float(statistics.pstdev(values) / mean) if len(values) >= 2 else 0.0
def _fano(values: list[float]) -> float:
if not values:
return 0.0
mean = statistics.fmean(values)
if abs(mean) <= EPSILON:
return 0.0
return float(statistics.pvariance(values) / mean) if len(values) >= 2 else 0.0
def _percentile(values: Sequence[float], p: float) -> float:
if not values:
return 0.0
ordered = sorted(float(item) for item in values)
if len(ordered) == 1:
return ordered[0]
rank = (p / 100.0) * (len(ordered) - 1)
lower = int(math.floor(rank))
upper = int(math.ceil(rank))
if lower == upper:
return ordered[lower]
weight = rank - lower
return float(ordered[lower] * (1.0 - weight) + ordered[upper] * weight)
def _similarity_from_z(left: Sequence[float], right: Sequence[float]) -> float:
distance = math.sqrt(
sum((float(lval) - float(rval)) ** 2 for lval, rval in zip(left, right))
)
return float(math.exp(-distance))
def _family_similarity(left: Sequence[float], right: Sequence[float]) -> dict[str, float]:
result: dict[str, float] = {}
for family, family_slice in FAMILY_SLICES.items():
result[family] = _similarity_from_z(left[family_slice], right[family_slice])
return result

View File

@@ -1,13 +1,17 @@
from __future__ import annotations
import json
import time
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any
from .harness import build_harness_context, render_harness_context
from .http_client import chat_completion, stream_text_completion
from .spec import LLMPolicySpec, Proposal, SpecError, StudySpec, StudyState
if TYPE_CHECKING:
from .lca import WorkloadProfile
def _parse_bool_like(value: Any, *, context: str) -> bool:
if isinstance(value, bool):
@@ -177,6 +181,7 @@ def build_prompt(
window_summary: dict[str, Any],
state: StudyState,
capability_profile: dict[str, Any] | None,
workload_profile: "WorkloadProfile | None" = None,
) -> str:
objective_notes: list[str] = []
if study.trace.request_mode == "decode_only":
@@ -211,16 +216,102 @@ def build_prompt(
)
launch_failures = _launch_failure_history(state)
parallel_candidates = _enumerate_parallel_candidates(study)
sections = [
common_preamble = [
"You are tuning an OpenAI-compatible serving engine.",
"Return exactly one JSON object with keys: observation, diagnosis, config_patch, expected_effects, why_not_previous_failures, should_stop.",
"config_patch must contain env_patch and flag_patch.",
"expected_effects must be a JSON array of short strings, not an object.",
"should_stop must be a boolean. Use true only when the harness convergence guard says another adjacent probe is not justified.",
(
"should_stop must be a boolean. Use true only when the harness convergence guard says another adjacent probe is not justified."
if study.llm.use_harness
else "should_stop must be a boolean. Use false unless no valid config can be proposed."
),
"Only use allowed tunable env keys and allowed tunable flag keys.",
"Do not wrap the JSON in markdown fences or any extra text.",
"Do not repeat a config that previously failed to launch unless the new patch explicitly removes the failing knob.",
"Treat previous engine launch failures as hard negative evidence. If you touch TP/DP/EP, keep the proposal inside the topology constraints exactly.",
]
if not study.llm.use_harness:
sections = [
*common_preamble,
"",
"Study context:",
json.dumps(
{
"study_id": study.study_id,
"objective": "maximize feasible request_rate_per_gpu at the SLO target",
"current_best": {
"trial_id": state.best_trial_id,
"best_parallel_size": state.best_parallel_size,
"best_sampling_u": state.best_sampling_u,
"best_request_rate": state.best_request_rate,
"best_request_rate_per_gpu": state.best_request_rate_per_gpu,
},
"hardware": {
"gpu_count": study.hardware.gpu_count,
"gpu_model": study.hardware.gpu_model,
},
"model": {
"model_id": study.model.model_id,
"served_model_name": study.model.served_model_name,
},
"trace": {
"window_id": study.trace.window_id,
"request_mode": study.trace.request_mode,
"completion_tokens_override": study.trace.completion_tokens_override,
"input_length_filter": (
{
"min_input_tokens": study.trace.input_length_filter.min_input_tokens,
"max_input_tokens": study.trace.input_length_filter.max_input_tokens,
}
if study.trace.input_length_filter is not None
else None
),
},
"engine": {
"engine_name": study.engine.engine_name,
"engine_version": study.engine.engine_version,
"base_flags": study.engine.base_flags,
"base_envs": study.engine.base_envs,
"allowed_flag_keys": study.engine.tunable_flags,
"allowed_env_keys": study.engine.tunable_envs,
"topology_constraints": (
study.engine.topology_constraints.__dict__
if study.engine.topology_constraints is not None
else None
),
},
},
ensure_ascii=False,
indent=2,
),
"",
"SLO:",
json.dumps(
{
"target_pass_rate": study.slo.target_pass_rate,
"ttft_rule": study.slo.ttft_rule,
"tpot_rule": study.slo.tpot_rule,
"objective_notes": objective_notes,
},
default=lambda value: value.__dict__,
ensure_ascii=False,
indent=2,
),
"",
"Trial history:",
json.dumps(history, ensure_ascii=False, indent=2),
"",
"Known launch failures:",
json.dumps(launch_failures, ensure_ascii=False, indent=2),
"",
"Tested config signatures:",
json.dumps(_tested_config_signatures(state), ensure_ascii=False, indent=2),
]
return "\n".join(sections)
sections = [
*common_preamble,
(
"TP/DP/EP are part of the tunable space for this study. Prioritize exploring legal topology changes in parallel space before runtime-only knobs unless recent history already proves a topology variant is worse or fails to launch."
if parallel_candidates
@@ -313,42 +404,32 @@ def build_prompt(
"Tested config signatures:",
json.dumps(_tested_config_signatures(state), ensure_ascii=False, indent=2),
]
if study.llm.use_harness:
sections.extend(
[
"",
"Harnesses:",
render_harness_context(
build_harness_context(
study=study,
window_summary=window_summary,
state=state,
)
),
"",
]
)
else:
sections.extend(
[
"",
"Harnesses:",
"Disabled by llm.use_harness=false for ablation.",
"",
]
)
sections.extend(
[
"",
"Harnesses:",
render_harness_context(
build_harness_context(
study=study,
window_summary=window_summary,
state=state,
workload_profile=workload_profile,
)
),
"",
]
)
sections.extend(
[
"The primary cross-topology comparison metric is request_rate_per_gpu, not raw request_rate.",
"The proposal should beat the incumbent on request_rate_per_gpu under the 95%+ SLO target.",
"The evaluator uses the best feasible sampling_u from the same tp_dp_product group when it exists.",
"If a tp_dp_product group has no history yet, the evaluator starts from the study's original search.low and runs a full binary search for that group.",
"Do not assume a configuration with fewer GPUs should inherit the global incumbent sampling_u.",
(
"Follow the active harness. Prefer stop over a weak exploratory proposal once a good incumbent has converged."
if study.llm.use_harness
else "For this ablation, reason from the raw study stack, trial history, launch failures, and tested config signatures without harness hints."
"The evaluator may use the same tp_dp_product incumbent as the search floor when search.inherit_incumbent_floor=true."
if study.search.inherit_incumbent_floor
else "The evaluator runs each proposal over the full configured search range so raw per-iteration performance is measured directly."
),
"Do not assume a configuration with fewer GPUs should inherit the global incumbent sampling_u.",
"Follow the active harness. Prefer stop over a weak exploratory proposal once a good incumbent has converged.",
]
)
return "\n".join(sections)
@@ -600,12 +681,15 @@ def call_llm_for_proposal(
*,
policy: LLMPolicySpec,
prompt: str,
use_harness: bool = True,
) -> str:
if policy.endpoint is None:
raise RuntimeError("study.llm.endpoint is not configured")
last_error: Exception | None = None
for attempt in range(2):
max_attempts = 4
for attempt in range(max_attempts):
try:
system_prompt = policy.system_prompt if use_harness else ""
if policy.endpoint.stream:
text = stream_text_completion(
base_url=policy.endpoint.base_url,
@@ -615,7 +699,7 @@ def call_llm_for_proposal(
model=policy.endpoint.model,
messages=[{"role": "user", "content": prompt}],
timeout_s=policy.endpoint.timeout_s,
system_prompt=policy.system_prompt,
system_prompt=system_prompt,
reasoning_effort=policy.endpoint.reasoning_effort,
)
else:
@@ -627,7 +711,7 @@ def call_llm_for_proposal(
model=policy.endpoint.model,
messages=[{"role": "user", "content": prompt}],
timeout_s=policy.endpoint.timeout_s,
system_prompt=policy.system_prompt,
system_prompt=system_prompt,
reasoning_effort=policy.endpoint.reasoning_effort,
)
text = _extract_response_text(response)
@@ -636,6 +720,7 @@ def call_llm_for_proposal(
last_error = RuntimeError("LLM response content is empty")
except Exception as exc: # noqa: BLE001
last_error = exc
if attempt == 0:
if attempt < max_attempts - 1:
time.sleep(min(30.0, 2.0 * (2**attempt)))
continue
raise RuntimeError(f"LLM proposal failed after retry: {last_error}") from last_error

View File

@@ -29,6 +29,9 @@ def _rule_threshold_ms(rule: ThresholdRule, prompt_tokens: int | None) -> float:
if rule.kind == "fixed_ms":
assert rule.threshold_ms is not None
return rule.threshold_ms
if rule.kind == "linear_ms":
assert rule.intercept_ms is not None and rule.per_token_ms is not None
return float(rule.intercept_ms) + float(rule.per_token_ms) * float(prompt_tokens or 0)
if rule.kind != "step_ms":
raise ValueError(f"Unsupported threshold rule: {rule.kind}")
prompt = float(prompt_tokens or 0)

View File

@@ -321,6 +321,71 @@ class InputLengthFilterSpec:
return spec
@dataclass(frozen=True)
class AdaptiveStopSpec:
"""Stop-A: truncate per-probe replay once the offered L-C-A converges.
Disabled by default; the thresholds are calibrated per workload (Phase 3)
before being switched on, so existing studies are unaffected.
"""
enabled: bool = False
tau: float = 0.9
tau_c: float = 0.92
stable_checks: int = 3
max_checks: int = 20
min_fraction: float = 0.1
boundary_delta: float = 0.02
@classmethod
def from_dict(cls, data: Any) -> "AdaptiveStopSpec":
if data is None:
return cls()
m = _require_mapping(data, context="trace.adaptive_stop")
enabled = (
_require_bool(m.get("enabled"), context="trace.adaptive_stop.enabled")
if m.get("enabled") is not None
else False
)
tau = _require_float(m.get("tau", 0.9), context="trace.adaptive_stop.tau")
tau_c = _require_float(m.get("tau_c", 0.92), context="trace.adaptive_stop.tau_c")
stable_checks = _require_int(
m.get("stable_checks", 3), context="trace.adaptive_stop.stable_checks"
)
max_checks = _require_int(
m.get("max_checks", 20), context="trace.adaptive_stop.max_checks"
)
min_fraction = _require_float(
m.get("min_fraction", 0.1), context="trace.adaptive_stop.min_fraction"
)
boundary_delta = _require_float(
m.get("boundary_delta", 0.02), context="trace.adaptive_stop.boundary_delta"
)
for name, value in (("tau", tau), ("tau_c", tau_c), ("min_fraction", min_fraction)):
if not 0.0 < value <= 1.0:
raise SpecError(f"trace.adaptive_stop.{name} must be in (0, 1].")
if not 0.0 <= boundary_delta < 1.0:
raise SpecError("trace.adaptive_stop.boundary_delta must be in [0, 1).")
if stable_checks <= 0 or max_checks <= 0:
raise SpecError(
"trace.adaptive_stop.stable_checks and max_checks must be > 0."
)
if stable_checks > max_checks:
raise SpecError(
"trace.adaptive_stop.stable_checks must be <= max_checks, "
"otherwise convergence can never be detected."
)
return cls(
enabled=enabled,
tau=tau,
tau_c=tau_c,
stable_checks=stable_checks,
max_checks=max_checks,
min_fraction=min_fraction,
boundary_delta=boundary_delta,
)
@dataclass(frozen=True)
class TraceSpec:
windows_path: str
@@ -338,6 +403,7 @@ class TraceSpec:
early_stop_max_lag_s: float | None = None
early_stop_max_elapsed_s: float | None = None
restart_engine_after_early_stop: bool = False
adaptive_stop: AdaptiveStopSpec = AdaptiveStopSpec()
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> "TraceSpec":
@@ -429,6 +495,7 @@ class TraceSpec:
if data.get("restart_engine_after_early_stop") is not None
else request_mode == "decode_only"
),
adaptive_stop=AdaptiveStopSpec.from_dict(data.get("adaptive_stop")),
)
@@ -437,6 +504,8 @@ class ThresholdRule:
kind: str
threshold_ms: float | None = None
buckets: list[dict[str, float]] = field(default_factory=list)
intercept_ms: float | None = None
per_token_ms: float | None = None
@classmethod
def from_dict(cls, data: Mapping[str, Any], *, context: str) -> "ThresholdRule":
@@ -448,6 +517,18 @@ class ThresholdRule:
data.get("threshold_ms"), context=f"{context}.threshold_ms"
),
)
if kind == "linear_ms":
# threshold = intercept_ms + per_token_ms * input_tokens
# e.g. "4s + L_in/8k" -> intercept_ms=4000, per_token_ms=0.125
intercept_ms = _require_float(
data.get("intercept_ms"), context=f"{context}.intercept_ms"
)
per_token_ms = _require_float(
data.get("per_token_ms"), context=f"{context}.per_token_ms"
)
if intercept_ms < 0 or per_token_ms < 0:
raise SpecError(f"{context}.intercept_ms/per_token_ms must be >= 0.")
return cls(kind=kind, intercept_ms=intercept_ms, per_token_ms=per_token_ms)
if kind == "step_ms":
raw = data.get("buckets")
if not isinstance(raw, list) or not raw:
@@ -511,6 +592,7 @@ class SamplingSearchSpec:
tolerance: float
max_probes: int
sample_seed: int
inherit_incumbent_floor: bool = False
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> "SamplingSearchSpec":
@@ -524,6 +606,10 @@ class SamplingSearchSpec:
sample_seed=_require_int(
data.get("sample_seed", 20260325), context="search.sample_seed"
),
inherit_incumbent_floor=_require_bool(
data.get("inherit_incumbent_floor", False),
context="search.inherit_incumbent_floor",
),
)
@@ -764,6 +850,9 @@ class StudyState:
best_request_rate: float | None = None
best_request_rate_per_gpu: float | None = None
next_trial_index: int = 1
tuning_stop_reason: str = ""
tuning_stop_diagnosis: str = ""
tuning_stop_details: dict[str, Any] = field(default_factory=dict)
best_by_parallel_size: dict[str, dict[str, Any]] = field(default_factory=dict)
trials: list[TrialSummary] = field(default_factory=list)

View File

@@ -45,6 +45,9 @@ class StudyStore:
best_request_rate=payload.get("best_request_rate"),
best_request_rate_per_gpu=payload.get("best_request_rate_per_gpu"),
next_trial_index=int(payload.get("next_trial_index", 1)),
tuning_stop_reason=str(payload.get("tuning_stop_reason") or ""),
tuning_stop_diagnosis=str(payload.get("tuning_stop_diagnosis") or ""),
tuning_stop_details=dict(payload.get("tuning_stop_details") or {}),
best_by_parallel_size={
str(key): value
for key, value in (payload.get("best_by_parallel_size") or {}).items()
@@ -82,15 +85,21 @@ class StudyStore:
trial_root = self.study_root(study.study_id) / "trials" / trial_id
trial_root.mkdir(parents=True, exist_ok=True)
parallel_size = _parallel_size_for_proposal(study=study, proposal=proposal)
search_low = _derive_search_floor(study=study, state=state, parallel_size=parallel_size)
search = study.search
if study.search.inherit_incumbent_floor:
search = replace(
study.search,
low=_derive_search_floor(
study=study,
state=state,
parallel_size=parallel_size,
),
)
spec = TrialSpec(
study_id=study.study_id,
trial_id=trial_id,
config_patch=proposal.config_patch,
search=replace(
study.search,
low=search_low,
),
search=search,
study_spec_path=str((self.study_root(study.study_id) / "study_spec.source").resolve()),
artifact_dir=str(trial_root),
probe_log_path=str(trial_root / "probe_history.json"),

View File

@@ -8,6 +8,7 @@ import statistics
import subprocess
import threading
import time
import traceback
from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait
from dataclasses import dataclass
from pathlib import Path
@@ -15,6 +16,7 @@ from typing import Any, Callable
from .engine import build_launch_recipe
from .http_client import HttpClientError, stream_chat_completion, wait_for_server
from .lca import find_convergence_prefix, resolve_length_mode
from .search import ThresholdProbe, binary_search_max_feasible
from .slo import RequestOutcome, evaluate_request, summarize_evaluations
from .spec import ConfigPatch, SamplingSearchSpec, TrialSpec, load_study_spec, to_jsonable
@@ -208,6 +210,123 @@ def _probe_outcome_details(
}
_SIGTERM_NOT_INSTALLED = object()
def _install_sigterm_as_keyboardinterrupt() -> Any:
"""Make SIGTERM raise KeyboardInterrupt so the engine-teardown finally runs.
When `study tune` is killed, a default SIGTERM skips the finally blocks and
orphans the vLLM engine (and its EngineCore workers) on the GPUs. Converting
SIGTERM to KeyboardInterrupt lets _terminate_process_tree run. Only installable
from the main thread; returns the previous handler (or a sentinel).
"""
if threading.current_thread() is not threading.main_thread():
return _SIGTERM_NOT_INSTALLED
def _handler(signum: int, frame: Any) -> None:
raise KeyboardInterrupt()
try:
return signal.signal(signal.SIGTERM, _handler)
except (ValueError, OSError):
return _SIGTERM_NOT_INSTALLED
def _restore_sigterm(previous: Any) -> None:
if previous is _SIGTERM_NOT_INSTALLED:
return
if threading.current_thread() is not threading.main_thread():
return
try:
signal.signal(signal.SIGTERM, previous)
except (ValueError, OSError):
pass
def _ignore_sigterm_if_main() -> None:
"""Ignore SIGTERM during teardown so a second signal cannot orphan the engine."""
if threading.current_thread() is not threading.main_thread():
return
try:
signal.signal(signal.SIGTERM, signal.SIG_IGN)
except (ValueError, OSError):
pass
def _adaptive_replay_set(
selected: list[TraceRequest],
*,
study: Any,
window: Any,
) -> tuple[list[TraceRequest], dict[str, Any] | None]:
"""Stop-A: truncate the replay to the offered-L-C-A convergence prefix.
Returns the (possibly shortened) request list to replay and a certificate of
the convergence decision. When Stop-A is disabled, or C never converges, the
full selected set is replayed (the C-gate: no early stop on a cold cache).
"""
spec = study.trace.adaptive_stop
if not getattr(spec, "enabled", False) or not selected:
return selected, None
point = find_convergence_prefix(
selected,
window,
gpu_count=study.hardware.gpu_count,
length_mode=resolve_length_mode(request_mode=study.trace.request_mode),
tau=spec.tau,
tau_c=spec.tau_c,
stable_checks=spec.stable_checks,
max_checks=spec.max_checks,
min_fraction=spec.min_fraction,
)
replay = selected[: point.stop_index] if point.stop_index > 0 else selected
certificate = {
"enabled": True,
"converged": point.converged,
"stop_index": point.stop_index,
"total_selected": len(selected),
"fraction": point.fraction,
"stop_time_s": point.stop_time_s,
"family_similarity": point.family_similarity,
}
return replay, certificate
def _should_extend_on_boundary(
*,
pass_rate: float,
target_pass_rate: float,
certificate: dict[str, Any] | None,
truncated: bool,
boundary_delta: float,
) -> bool:
"""SLO-boundary guard: re-measure on the full window when a truncated probe
lands within +/- boundary_delta of the SLO target.
Offered-L-C-A convergence cannot see engine-state drift in the window's tail,
so a near-boundary truncated verdict is untrustworthy. This fires only on
probes sitting on the feasibility knee, so non-boundary probes keep the Stop-A
time saving.
"""
if certificate is None or not certificate.get("converged"):
return False
if not truncated or boundary_delta <= 0:
return False
return abs(float(pass_rate) - float(target_pass_rate)) <= float(boundary_delta)
def _best_feasible_probe_record(probe_history: list[dict[str, Any]]) -> dict[str, Any] | None:
feasible = [
item
for item in probe_history
if item.get("feasible") and isinstance(item.get("request_rate"), (int, float))
]
if not feasible:
return None
return max(feasible, key=lambda item: float(item["request_rate"]))
def _replay_requests(
requests: list[TraceRequest],
*,
@@ -389,27 +508,76 @@ def _wait_for_server_or_exit(
raise HttpClientError(f"Timed out waiting for {base_url}{healthcheck_path}: {last_error}")
def _terminate_process_tree(process: subprocess.Popen[str], *, timeout_s: float = 30.0) -> None:
if process.poll() is not None:
return
def _process_group_exists(pgid: int) -> bool:
try:
os.killpg(pgid, 0)
return True
except ProcessLookupError:
return False
def _pids_matching_env(marker_env: dict[str, str] | None) -> list[int]:
if not marker_env:
return []
expected = {
f"{key}={value}".encode()
for key, value in marker_env.items()
}
pids: list[int] = []
proc_root = Path("/proc")
for entry in proc_root.iterdir():
if not entry.name.isdigit():
continue
pid = int(entry.name)
if pid == os.getpid():
continue
try:
environ = (entry / "environ").read_bytes()
except (FileNotFoundError, PermissionError, ProcessLookupError):
continue
if expected.issubset(set(environ.split(b"\0"))):
pids.append(pid)
return sorted(pids)
def _signal_pids(pids: list[int], sig: signal.Signals) -> None:
for pid in pids:
try:
os.kill(pid, sig)
except (ProcessLookupError, PermissionError):
continue
def _terminate_process_tree(
process: subprocess.Popen[str],
*,
timeout_s: float = 30.0,
marker_env: dict[str, str] | None = None,
) -> None:
try:
pgid = os.getpgid(process.pid)
except ProcessLookupError:
return
# Children can keep the session/process group alive after the vLLM API
# server exits. In that case the group id is still the original pid
# because the process was launched with start_new_session=True.
pgid = process.pid
try:
os.killpg(pgid, signal.SIGTERM)
except ProcessLookupError:
return
pass
_signal_pids(_pids_matching_env(marker_env), signal.SIGTERM)
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
if process.poll() is not None:
if not _process_group_exists(pgid) and not _pids_matching_env(marker_env):
return
time.sleep(0.1)
try:
os.killpg(pgid, signal.SIGKILL)
except ProcessLookupError:
return
process.wait(timeout=timeout_s)
pass
_signal_pids(_pids_matching_env(marker_env), signal.SIGKILL)
if process.poll() is None:
process.wait(timeout=timeout_s)
def run_trial(trial_spec_path: Path) -> dict[str, Any]:
@@ -426,12 +594,17 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
probe_details_path = artifact_dir / "probe_details.jsonl"
if probe_details_path.exists():
probe_details_path.unlink()
trial_marker_env = {
"AITUNER_STUDY_ID": trial.study_id,
"AITUNER_TRIAL_ID": trial.trial_id,
}
with engine_log_path.open("w", encoding="utf-8") as engine_log:
def launch_process() -> subprocess.Popen[str]:
launch_env = {**recipe.env, **trial_marker_env}
return subprocess.Popen( # noqa: S603
recipe.argv,
cwd=recipe.cwd,
env=recipe.env,
env=launch_env,
stdout=engine_log,
stderr=subprocess.STDOUT,
text=True,
@@ -439,6 +612,7 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
)
process = launch_process()
previous_sigterm = _install_sigterm_as_keyboardinterrupt()
probe_history: list[dict[str, Any]] = []
failure_stage = "engine_launch"
try:
@@ -453,27 +627,49 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
def evaluator(threshold: float) -> ThresholdProbe[ProbePayload]:
nonlocal process
selected = select_requests_for_threshold(requests, threshold=threshold)
restart_after_early_stop = study.trace.restart_engine_after_early_stop
outcomes, early_stopped, early_stop_reason = _replay_requests(
selected,
base_url=recipe.base_url,
timeout_s=recipe.request_timeout_s,
max_concurrency=study.trace.max_concurrency,
target_pass_rate=study.slo.target_pass_rate,
max_lag_s=study.trace.early_stop_max_lag_s,
max_elapsed_s=study.trace.early_stop_max_elapsed_s,
evaluate_outcome=lambda outcome: evaluate_request(outcome, study.slo),
drain_inflight_on_early_stop=not restart_after_early_stop,
replay_set, adaptive_stop_certificate = _adaptive_replay_set(
selected, study=study, window=window
)
restart_after_early_stop = study.trace.restart_engine_after_early_stop
def _run(reqs: list[TraceRequest]) -> tuple[list[RequestOutcome], bool, str]:
return _replay_requests(
reqs,
base_url=recipe.base_url,
timeout_s=recipe.request_timeout_s,
max_concurrency=study.trace.max_concurrency,
target_pass_rate=study.slo.target_pass_rate,
max_lag_s=study.trace.early_stop_max_lag_s,
max_elapsed_s=study.trace.early_stop_max_elapsed_s,
evaluate_outcome=lambda outcome: evaluate_request(outcome, study.slo),
drain_inflight_on_early_stop=not restart_after_early_stop,
)
outcomes, early_stopped, early_stop_reason = _run(replay_set)
evaluations, summary = summarize_evaluations(outcomes, study.slo)
if _should_extend_on_boundary(
pass_rate=summary["slo_pass_rate"],
target_pass_rate=study.slo.target_pass_rate,
certificate=adaptive_stop_certificate,
truncated=len(replay_set) < len(selected),
boundary_delta=study.trace.adaptive_stop.boundary_delta,
):
# On the feasibility knee the truncated verdict is untrustworthy;
# re-measure the full window and use that result.
replay_set = selected
outcomes, early_stopped, early_stop_reason = _run(selected)
evaluations, summary = summarize_evaluations(outcomes, study.slo)
if adaptive_stop_certificate is not None:
adaptive_stop_certificate["boundary_extended"] = True
probe_details = _probe_outcome_details(
threshold=threshold,
selected=selected,
selected=replay_set,
outcomes=outcomes,
evaluations=evaluations,
early_stopped=early_stopped,
early_stop_reason=early_stop_reason,
)
probe_details["adaptive_stop"] = adaptive_stop_certificate
with probe_details_path.open("a", encoding="utf-8") as details_handle:
details_handle.write(
json.dumps(probe_details, ensure_ascii=False) + "\n"
@@ -514,17 +710,23 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
probe_record = {
"threshold": threshold,
"request_count": payload.request_count,
"replayed_request_count": len(replay_set),
"pass_rate": payload.pass_rate,
"request_rate": payload.request_rate,
"feasible": payload.feasible,
"early_stopped": payload.early_stopped,
"early_stop_reason": payload.early_stop_reason,
"latency_summary": payload.latency_summary,
"adaptive_stop": adaptive_stop_certificate,
}
probe_history.append(probe_record)
StudyStore.write_json(Path(trial.probe_log_path), probe_history)
if early_stopped and restart_after_early_stop:
_terminate_process_tree(process, timeout_s=30.0)
_terminate_process_tree(
process,
timeout_s=30.0,
marker_env=trial_marker_env,
)
process = launch_process()
_wait_for_server_or_exit(
process,
@@ -538,41 +740,88 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
payload=payload,
)
search = binary_search_max_feasible(
primary_search = binary_search_max_feasible(
low=trial.search.low,
high=trial.search.high,
tolerance=trial.search.tolerance,
max_probes=trial.search.max_probes,
evaluator=evaluator,
)
best = search.best_feasible_payload
search_for_best = primary_search
best = primary_search.best_feasible_payload
best_source = "primary_search"
fallback_search = None
original_search_low = float(study.search.low)
inherited_search_floor = float(trial.search.low)
if best is None and inherited_search_floor > original_search_low:
fallback_search = binary_search_max_feasible(
low=original_search_low,
high=inherited_search_floor,
tolerance=trial.search.tolerance,
max_probes=trial.search.max_probes,
evaluator=evaluator,
)
if fallback_search.best_feasible_payload is not None:
search_for_best = fallback_search
best = fallback_search.best_feasible_payload
best_source = "lower_range_fallback"
def serialize_probe(probe: ThresholdProbe[ProbePayload]) -> dict[str, Any]:
return {
"threshold": probe.threshold,
"feasible": probe.feasible,
"payload": {
"request_count": probe.payload.request_count,
"pass_rate": probe.payload.pass_rate,
"request_rate": probe.payload.request_rate,
"early_stopped": probe.payload.early_stopped,
"early_stop_reason": probe.payload.early_stop_reason,
"latency_summary": probe.payload.latency_summary,
},
}
all_probes = [
*primary_search.probes,
*((fallback_search.probes if fallback_search is not None else [])),
]
result = {
"study_id": trial.study_id,
"trial_id": trial.trial_id,
"status": "completed",
"config_patch": to_jsonable(trial.config_patch),
"best_sampling_u": search.best_threshold if best is not None else None,
"best_source": best_source,
"best_sampling_u": search_for_best.best_threshold if best is not None else None,
"best_request_rate": best.request_rate if best is not None else None,
"best_pass_rate": best.pass_rate if best is not None else None,
"best_request_count": best.request_count if best is not None else None,
"probes": [
{
"threshold": probe.threshold,
"feasible": probe.feasible,
"payload": {
"request_count": probe.payload.request_count,
"pass_rate": probe.payload.pass_rate,
"request_rate": probe.payload.request_rate,
"early_stopped": probe.payload.early_stopped,
"early_stop_reason": probe.payload.early_stop_reason,
"latency_summary": probe.payload.latency_summary,
},
}
for probe in search.probes
],
"probes": [serialize_probe(probe) for probe in all_probes],
}
if best is None and search.probes:
last_probe = search.probes[-1]
if fallback_search is not None:
result["primary_search"] = {
"low": inherited_search_floor,
"high": trial.search.high,
"best_sampling_u": primary_search.best_threshold
if primary_search.best_feasible_payload is not None
else None,
"best_request_rate": primary_search.best_feasible_payload.request_rate
if primary_search.best_feasible_payload is not None
else None,
"probes": [serialize_probe(probe) for probe in primary_search.probes],
}
result["lower_range_fallback"] = {
"triggered": True,
"low": original_search_low,
"high": inherited_search_floor,
"best_sampling_u": fallback_search.best_threshold
if fallback_search.best_feasible_payload is not None
else None,
"best_request_rate": fallback_search.best_feasible_payload.request_rate
if fallback_search.best_feasible_payload is not None
else None,
"probes": [serialize_probe(probe) for probe in fallback_search.probes],
}
if best is None and all_probes:
last_probe = all_probes[-1]
result["all_infeasible_diagnostics"] = {
"threshold": last_probe.threshold,
"request_count": last_probe.payload.request_count,
@@ -585,6 +834,26 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
StudyStore.write_json(Path(trial.result_path), result)
return result
except Exception as exc: # noqa: BLE001
partial_best = _best_feasible_probe_record(probe_history)
if partial_best is not None:
result = {
"study_id": trial.study_id,
"trial_id": trial.trial_id,
"status": "completed",
"config_patch": to_jsonable(trial.config_patch),
"best_source": "partial_probe_before_failure",
"best_sampling_u": partial_best.get("threshold"),
"best_request_rate": partial_best.get("request_rate"),
"best_pass_rate": partial_best.get("pass_rate"),
"best_request_count": partial_best.get("request_count"),
"completed_with_probe_failure": True,
"failure_stage": failure_stage,
"failure_reason": str(exc),
"failure_traceback": traceback.format_exc(),
"probes": probe_history,
}
StudyStore.write_json(Path(trial.result_path), result)
return result
result = {
"study_id": trial.study_id,
"trial_id": trial.trial_id,
@@ -596,9 +865,12 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
"best_request_count": None,
"failure_stage": failure_stage,
"failure_reason": str(exc),
"failure_traceback": traceback.format_exc(),
"probes": probe_history,
}
StudyStore.write_json(Path(trial.result_path), result)
return result
finally:
_terminate_process_tree(process, timeout_s=30.0)
_ignore_sigterm_if_main()
_terminate_process_tree(process, timeout_s=30.0, marker_env=trial_marker_env)
_restore_sigterm(previous_sigterm)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
from __future__ import annotations
import importlib.util
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
_SPEC = importlib.util.spec_from_file_location(
"prepare_trace_windows",
REPO_ROOT / "scripts" / "prepare_trace_windows.py",
)
assert _SPEC and _SPEC.loader
ptw = importlib.util.module_from_spec(_SPEC)
# Register before exec so dataclasses can resolve the module's annotations.
sys.modules[_SPEC.name] = ptw
_SPEC.loader.exec_module(ptw)
class SessionCoherentSamplingTests(unittest.TestCase):
def test_multi_hop_chain_resolves_to_root(self) -> None:
root_of: dict[object, object] = {}
# turn1 root, turn2 -> turn1, turn3 -> turn2 (multi-hop), streamed in order.
self.assertEqual(
ptw.resolve_session_root({"chat_id": 1, "parent_chat_id": -1, "turn": 1}, root_of),
1,
)
self.assertEqual(
ptw.resolve_session_root({"chat_id": 2, "parent_chat_id": 1, "turn": 2}, root_of),
1,
)
self.assertEqual(
ptw.resolve_session_root({"chat_id": 3, "parent_chat_id": 2, "turn": 3}, root_of),
1,
)
def test_unknown_parent_falls_back_to_parent_id(self) -> None:
root_of: dict[object, object] = {}
# parent never seen (fell outside the span): group siblings under the parent.
self.assertEqual(
ptw.resolve_session_root({"chat_id": 50, "parent_chat_id": 9, "turn": 2}, root_of),
9,
)
self.assertEqual(
ptw.resolve_session_root({"chat_id": 51, "parent_chat_id": 9, "turn": 2}, root_of),
9,
)
def test_all_turns_of_a_session_share_one_u(self) -> None:
root_of: dict[object, object] = {}
rows = [
{"chat_id": 1, "parent_chat_id": -1, "turn": 1},
{"chat_id": 2, "parent_chat_id": 1, "turn": 2},
{"chat_id": 3, "parent_chat_id": 2, "turn": 3},
]
us = {
ptw.session_uniform(
seed=7,
window_id="w",
session_root=ptw.resolve_session_root(row, root_of),
)
for row in rows
}
self.assertEqual(len(us), 1)
only = next(iter(us))
self.assertGreaterEqual(only, 0.0)
self.assertLess(only, 1.0)
def test_thresholding_keeps_or_drops_whole_sessions(self) -> None:
# Two distinct sessions get distinct scores; a threshold either keeps a
# session's every turn or none of them.
seed, window_id = 20260325, "chat_w_x"
sessions = {
"A": [
{"chat_id": 10, "parent_chat_id": -1},
{"chat_id": 11, "parent_chat_id": 10},
],
"B": [
{"chat_id": 20, "parent_chat_id": -1},
{"chat_id": 21, "parent_chat_id": 20},
],
}
root_of: dict[object, object] = {}
scored: list[tuple[str, float]] = []
for name, rows in sessions.items():
for row in rows:
root = ptw.resolve_session_root(row, root_of)
u = ptw.session_uniform(seed=seed, window_id=window_id, session_root=root)
scored.append((name, u))
for name in sessions:
us = {u for n, u in scored if n == name}
self.assertEqual(len(us), 1, f"session {name} turns must share one u")
for threshold in (0.0, 0.25, 0.5, 0.75, 1.0):
for name in sessions:
kept = {u <= threshold for n, u in scored if n == name}
self.assertEqual(len(kept), 1, "a session must be kept/dropped as a whole")
if __name__ == "__main__":
unittest.main()