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,
|
"port": 18082,
|
||||||
"healthcheck_path": "/v1/models",
|
"healthcheck_path": "/v1/models",
|
||||||
"ready_timeout_s": 900,
|
"ready_timeout_s": 900,
|
||||||
"request_timeout_s": 900,
|
"request_timeout_s": 180,
|
||||||
"launch_args": [
|
"launch_args": [
|
||||||
"serve",
|
"serve",
|
||||||
"/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal"
|
"/home/admin/resource/model/464482ce/qwen3.5-27b/256k-0223-internal"
|
||||||
@@ -48,7 +48,8 @@
|
|||||||
"VLLM_DP_MASTER_PORT": "9528",
|
"VLLM_DP_MASTER_PORT": "9528",
|
||||||
"VLLM_RESPONSE_TIMEOUT": "300",
|
"VLLM_RESPONSE_TIMEOUT": "300",
|
||||||
"VLLM_LOG_REQ_KV_LENS": "1",
|
"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": {
|
"base_flags": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
@@ -145,20 +146,9 @@
|
|||||||
"slo": {
|
"slo": {
|
||||||
"target_pass_rate": 0.95,
|
"target_pass_rate": 0.95,
|
||||||
"ttft_rule": {
|
"ttft_rule": {
|
||||||
"kind": "step_ms",
|
"kind": "linear_ms",
|
||||||
"buckets": [
|
"intercept_ms": 4000,
|
||||||
{
|
"per_token_ms": 0.125
|
||||||
"max_input_tokens": 4096,
|
|
||||||
"threshold_ms": 2000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"max_input_tokens": 32768,
|
|
||||||
"threshold_ms": 4000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"threshold_ms": 6000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"tpot_rule": {
|
"tpot_rule": {
|
||||||
"kind": "fixed_ms",
|
"kind": "fixed_ms",
|
||||||
@@ -167,9 +157,9 @@
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"low": 0.0,
|
"low": 0.0,
|
||||||
"high": 0.25,
|
"high": 0.125,
|
||||||
"tolerance": 0.001,
|
"tolerance": 0.001,
|
||||||
"max_probes": 7,
|
"max_probes": 6,
|
||||||
"sample_seed": 20260325
|
"sample_seed": 20260325
|
||||||
},
|
},
|
||||||
"llm": {
|
"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:
|
except urllib.error.HTTPError as exc:
|
||||||
detail = exc.read().decode("utf-8", errors="replace")
|
detail = exc.read().decode("utf-8", errors="replace")
|
||||||
raise HttpClientError(f"llm_completion failed: {exc.code} {detail}") from exc
|
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(
|
def stream_text_completion(
|
||||||
@@ -232,6 +235,8 @@ def stream_text_completion(
|
|||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
detail = exc.read().decode("utf-8", errors="replace")
|
detail = exc.read().decode("utf-8", errors="replace")
|
||||||
raise HttpClientError(f"stream_text_completion failed: {exc.code} {detail}") from exc
|
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)
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
@@ -293,6 +298,10 @@ def stream_chat_completion(
|
|||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
detail = exc.read().decode("utf-8", errors="replace")
|
detail = exc.read().decode("utf-8", errors="replace")
|
||||||
raise HttpClientError(f"stream_chat_completion failed: {exc.code} {detail}") from exc
|
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
|
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:
|
if completion_tokens is None and chunk_token_count > 0:
|
||||||
completion_tokens = chunk_token_count
|
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":
|
if rule.kind == "fixed_ms":
|
||||||
assert rule.threshold_ms is not None
|
assert rule.threshold_ms is not None
|
||||||
return rule.threshold_ms
|
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":
|
if rule.kind != "step_ms":
|
||||||
raise ValueError(f"Unsupported threshold rule: {rule.kind}")
|
raise ValueError(f"Unsupported threshold rule: {rule.kind}")
|
||||||
prompt = float(prompt_tokens or 0)
|
prompt = float(prompt_tokens or 0)
|
||||||
|
|||||||
@@ -504,6 +504,8 @@ class ThresholdRule:
|
|||||||
kind: str
|
kind: str
|
||||||
threshold_ms: float | None = None
|
threshold_ms: float | None = None
|
||||||
buckets: list[dict[str, float]] = field(default_factory=list)
|
buckets: list[dict[str, float]] = field(default_factory=list)
|
||||||
|
intercept_ms: float | None = None
|
||||||
|
per_token_ms: float | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Mapping[str, Any], *, context: str) -> "ThresholdRule":
|
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"
|
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":
|
if kind == "step_ms":
|
||||||
raw = data.get("buckets")
|
raw = data.get("buckets")
|
||||||
if not isinstance(raw, list) or not raw:
|
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(
|
def _adaptive_replay_set(
|
||||||
selected: list[TraceRequest],
|
selected: list[TraceRequest],
|
||||||
*,
|
*,
|
||||||
@@ -568,6 +612,7 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
process = launch_process()
|
process = launch_process()
|
||||||
|
previous_sigterm = _install_sigterm_as_keyboardinterrupt()
|
||||||
probe_history: list[dict[str, Any]] = []
|
probe_history: list[dict[str, Any]] = []
|
||||||
failure_stage = "engine_launch"
|
failure_stage = "engine_launch"
|
||||||
try:
|
try:
|
||||||
@@ -826,4 +871,6 @@ def run_trial(trial_spec_path: Path) -> dict[str, Any]:
|
|||||||
StudyStore.write_json(Path(trial.result_path), result)
|
StudyStore.write_json(Path(trial.result_path), result)
|
||||||
return result
|
return result
|
||||||
finally:
|
finally:
|
||||||
|
_ignore_sigterm_if_main()
|
||||||
_terminate_process_tree(process, timeout_s=30.0, marker_env=trial_marker_env)
|
_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.compare import _aggregate_summary, load_compare_spec, run_compare
|
||||||
from aituner.engine import build_launch_recipe
|
from aituner.engine import build_launch_recipe
|
||||||
from aituner.http_client import (
|
from aituner.http_client import (
|
||||||
|
HttpClientError,
|
||||||
StreamMetrics,
|
StreamMetrics,
|
||||||
_auth_headers,
|
_auth_headers,
|
||||||
_openai_url,
|
_openai_url,
|
||||||
@@ -44,6 +45,7 @@ from aituner.spec import (
|
|||||||
ConfigPatch,
|
ConfigPatch,
|
||||||
LLMEndpointSpec,
|
LLMEndpointSpec,
|
||||||
Proposal,
|
Proposal,
|
||||||
|
SloSpec,
|
||||||
SpecError,
|
SpecError,
|
||||||
StudyState,
|
StudyState,
|
||||||
TrialSummary,
|
TrialSummary,
|
||||||
@@ -53,6 +55,8 @@ from aituner.store import StudyStore
|
|||||||
from aituner.trace import load_trace_requests, summarize_window
|
from aituner.trace import load_trace_requests, summarize_window
|
||||||
from aituner.worker import (
|
from aituner.worker import (
|
||||||
_adaptive_replay_set,
|
_adaptive_replay_set,
|
||||||
|
_install_sigterm_as_keyboardinterrupt,
|
||||||
|
_restore_sigterm,
|
||||||
_should_extend_on_boundary,
|
_should_extend_on_boundary,
|
||||||
_best_feasible_probe_record,
|
_best_feasible_probe_record,
|
||||||
_latency_summary,
|
_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:
|
def test_lca_similarity_matrix_separates_different_profiles(self) -> None:
|
||||||
window = WindowRecord(
|
window = WindowRecord(
|
||||||
window_id="base",
|
window_id="base",
|
||||||
|
|||||||
Reference in New Issue
Block a user