Compare commits
9 Commits
77af4ded2a
...
feat/two-s
| Author | SHA1 | Date | |
|---|---|---|---|
| f2ff0faebd | |||
| 4a64196a99 | |||
| b17b213575 | |||
| 93ce339d61 | |||
| b1b74318f6 | |||
| 2fcaf80450 | |||
| 3541065675 | |||
| 7678c7d5e8 | |||
| ed2bbe0323 |
177
configs/examples/dash0_qwen27b_stopB_loop.json
Normal file
177
configs/examples/dash0_qwen27b_stopB_loop.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
51
docs/harness-ablation/qwen27b-tp-sweep-20260616.md
Normal file
51
docs/harness-ablation/qwen27b-tp-sweep-20260616.md
Normal 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
|
||||
0–8k chat, vLLM 0.11.1, H20, `replay_time_scale=1.0` (no smoke), Stop-A enabled,
|
||||
pinned to GPUs 2–7.
|
||||
|
||||
**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).
|
||||
64
docs/harness-ablation/stop-b-e2e-27b-20260616.md
Normal file
64
docs/harness-ablation/stop-b-e2e-27b-20260616.md
Normal 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 2–7,
|
||||
`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 2–7 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).
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user