9 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
9 changed files with 445 additions and 18 deletions

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

@@ -20,7 +20,7 @@
"port": 18082,
"healthcheck_path": "/v1/models",
"ready_timeout_s": 900,
"request_timeout_s": 900,
"request_timeout_s": 180,
"launch_args": [
"serve",
"/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal"
@@ -48,7 +48,8 @@
"VLLM_DP_MASTER_PORT": "9528",
"VLLM_RESPONSE_TIMEOUT": "300",
"VLLM_LOG_REQ_KV_LENS": "1",
"DS_LLM_GRACEFUL_SHUTDOWN_KEEP_SECONDS": "600"
"DS_LLM_GRACEFUL_SHUTDOWN_KEEP_SECONDS": "600",
"CUDA_VISIBLE_DEVICES": "2,3,4,5,6,7"
},
"base_flags": {
"host": "127.0.0.1",
@@ -145,20 +146,9 @@
"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
}
]
"kind": "linear_ms",
"intercept_ms": 4000,
"per_token_ms": 0.125
},
"tpot_rule": {
"kind": "fixed_ms",
@@ -167,9 +157,9 @@
},
"search": {
"low": 0.0,
"high": 0.25,
"high": 0.125,
"tolerance": 0.001,
"max_probes": 7,
"max_probes": 6,
"sample_seed": 20260325
},
"llm": {

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,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

@@ -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

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

@@ -504,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":
@@ -515,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:

View File

@@ -210,6 +210,50 @@ 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],
*,
@@ -568,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:
@@ -826,4 +871,6 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
StudyStore.write_json(Path(trial.result_path), result)
return result
finally:
_ignore_sigterm_if_main()
_terminate_process_tree(process, timeout_s=30.0, marker_env=trial_marker_env)
_restore_sigterm(previous_sigterm)

View File

@@ -16,6 +16,7 @@ from aituner.cli import main as cli_main
from aituner.compare import _aggregate_summary, load_compare_spec, run_compare
from aituner.engine import build_launch_recipe
from aituner.http_client import (
HttpClientError,
StreamMetrics,
_auth_headers,
_openai_url,
@@ -44,6 +45,7 @@ from aituner.spec import (
ConfigPatch,
LLMEndpointSpec,
Proposal,
SloSpec,
SpecError,
StudyState,
TrialSummary,
@@ -53,6 +55,8 @@ from aituner.store import StudyStore
from aituner.trace import load_trace_requests, summarize_window
from aituner.worker import (
_adaptive_replay_set,
_install_sigterm_as_keyboardinterrupt,
_restore_sigterm,
_should_extend_on_boundary,
_best_feasible_probe_record,
_latency_summary,
@@ -531,6 +535,74 @@ class CoreFlowTests(unittest.TestCase):
)
)
def test_linear_ms_ttft_rule_scales_with_input_length(self) -> None:
slo = SloSpec.from_dict(
{
"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},
}
)
def ev(prompt_tokens: int, ttft_ms: float):
return evaluate_request(
RequestOutcome(
request_id="r",
success=True,
ttft_ms=ttft_ms,
tpot_ms=10.0,
prompt_tokens=prompt_tokens,
completion_tokens=8,
),
slo,
)
# threshold = 4000 + 0.125*L_in : 8k->5000ms, 0->4000ms
self.assertTrue(ev(8000, 4900).passed)
self.assertFalse(ev(8000, 5100).passed)
self.assertTrue(ev(0, 3900).passed)
self.assertFalse(ev(0, 4100).passed)
def test_streaming_socket_timeout_is_a_failed_request_not_a_crash(self) -> None:
# A request that exceeds request_timeout_s raises TimeoutError mid-stream;
# it must surface as HttpClientError (a failed request), never escape to
# crash the trial.
with mock.patch(
"aituner.http_client._urlopen", side_effect=TimeoutError("timed out")
):
with self.assertRaises(HttpClientError):
stream_chat_completion(
base_url="http://127.0.0.1:1/v1",
body={"messages": [{"role": "user", "content": "hi"}], "stream": True},
timeout_s=0.5,
)
outcome = _run_one_request(
TraceRequest(
row_id="r",
arrival_s=0.0,
sampling_u=1.0,
body={"messages": [{"role": "user", "content": "hi"}], "stream": True},
prompt_tokens_hint=10,
completion_tokens_hint=None,
),
base_url="http://127.0.0.1:1/v1",
timeout_s=0.5,
)
self.assertFalse(outcome.success)
self.assertIn("timed out", outcome.error)
def test_sigterm_is_converted_to_keyboardinterrupt(self) -> None:
# So a killed `study tune` runs the engine-teardown finally instead of
# orphaning the vLLM EngineCore workers on the GPUs.
import signal as _signal
previous = _install_sigterm_as_keyboardinterrupt()
try:
with self.assertRaises(KeyboardInterrupt):
_signal.raise_signal(_signal.SIGTERM)
finally:
_restore_sigterm(previous)
def test_lca_similarity_matrix_separates_different_profiles(self) -> None:
window = WindowRecord(
window_id="base",