Compare commits

3 Commits

Author SHA1 Message Date
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
10 changed files with 1099 additions and 5 deletions

View File

@@ -141,8 +141,8 @@ New comparable studies:
| Variant | Study ID | Status |
| --- | --- | --- |
| no-harness baseline | `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu8-12iter-noharness-minprompt-gpt54-20260513` | running first |
| harness | `dash0-qwen27b-chat-0-8k-ttft4s-tpot25-gpu8-12iter-harness-profileplanner-20260513` | queued to run after baseline |
| 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:
@@ -152,4 +152,4 @@ Both specs set:
- SLO: TTFT p95 <= 4000ms, TPOT p95 <= 25ms, target pass rate 0.95
- search: full range, `inherit_incumbent_floor=false`
The no-harness baseline is running in tmux session `qwen27b-gpu8-noharness-20260513`. The harness run should only be started after the no-harness baseline finishes or reaches a sufficient early comparison point, because both need the full GPU host and should not run concurrently.
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

@@ -1,5 +1,7 @@
# 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`

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

@@ -2,6 +2,8 @@
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`)

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

BIN
paper.pdf Normal file

Binary file not shown.

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,8 +13,20 @@ from .harness import (
build_harness_stop_proposal,
)
from .job import append_job, build_trial_job
from .lca import (
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
@@ -422,6 +435,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)
@@ -490,6 +656,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

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

@@ -0,0 +1,406 @@
from __future__ import annotations
import json
import math
import statistics
from dataclasses import dataclass
from typing import Any, Sequence
from .trace import TraceRequest, WindowRecord
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 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,
}
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,6 +1,8 @@
from __future__ import annotations
import json
import io
import math
import os
import signal
import subprocess
@@ -25,6 +27,12 @@ from aituner.harness import (
build_harness_guided_proposal,
build_harness_stop_proposal,
)
from aituner.lca import (
build_workload_profile,
profile_similarity,
resolve_length_mode,
similarity_report,
)
from aituner.llm import _extract_response_text, build_prompt, parse_proposal_text, validate_proposal
from aituner.search import ThresholdProbe, binary_search_max_feasible
from aituner.slo import RequestOutcome, evaluate_request, summarize_evaluations
@@ -48,7 +56,7 @@ from aituner.worker import (
_wait_for_server_or_exit,
run_trial,
)
from aituner.trace import TraceRequest
from aituner.trace import TraceRequest, WindowRecord
REPO_ROOT = Path(__file__).resolve().parents[1]
@@ -241,6 +249,150 @@ class CoreFlowTests(unittest.TestCase):
self.assertIn("knob_harnesses", prompt)
self.assertTrue(study_root.exists())
def test_lca_workload_profile_uses_standard_10d_features(self) -> None:
window = WindowRecord(
window_id="w1",
trace_path=Path("trace.jsonl"),
trace_type="chat",
window_start=0.0,
window_end=4.0,
source_payload={"block_size": 64},
)
requests = [
TraceRequest(
row_id="r1",
arrival_s=0.0,
sampling_u=1.0,
body={},
prompt_tokens_hint=100,
completion_tokens_hint=10,
metadata={"hash_ids": [1, 2]},
),
TraceRequest(
row_id="r2",
arrival_s=1.0,
sampling_u=1.0,
body={},
prompt_tokens_hint=100,
completion_tokens_hint=20,
metadata={"hash_ids": [1, 3]},
),
]
profile = build_workload_profile(
requests,
window,
gpu_count=2,
length_mode="total",
)
self.assertEqual(len(profile.feature_names), 10)
self.assertEqual(len(profile.vector), 10)
self.assertEqual(profile.feature_names[0], "L.log_mean_length")
self.assertAlmostEqual(profile.stats["cache"]["total_hit_length"], 64.0)
self.assertAlmostEqual(profile.stats["cache"]["hit_rate"], 64.0 / 230.0)
self.assertAlmostEqual(profile.stats["cache"]["input_hit_rate"], 64.0 / 200.0)
self.assertAlmostEqual(profile.vector[3], math.log1p(32.0))
self.assertAlmostEqual(profile.vector[5], 1.0)
self.assertAlmostEqual(profile.stats["arrival"]["request_rate_per_gpu"], 0.25)
self.assertAlmostEqual(profile.stats["arrival"]["fano_1s"], 0.5)
self.assertEqual(resolve_length_mode(request_mode="decode_only"), "output")
def test_lca_similarity_matrix_separates_different_profiles(self) -> None:
window = WindowRecord(
window_id="base",
trace_path=Path("trace.jsonl"),
trace_type="chat",
window_start=0.0,
window_end=4.0,
source_payload={"block_size": 64},
)
def make_profile(window_id: str, input_tokens: int, *, arrival_gap: float) -> object:
reqs = [
TraceRequest(
row_id=f"{window_id}-1",
arrival_s=0.0,
sampling_u=1.0,
body={},
prompt_tokens_hint=input_tokens,
completion_tokens_hint=16,
metadata={"hash_ids": [window_id, 1]},
),
TraceRequest(
row_id=f"{window_id}-2",
arrival_s=arrival_gap,
sampling_u=1.0,
body={},
prompt_tokens_hint=input_tokens,
completion_tokens_hint=16,
metadata={"hash_ids": [window_id, 1, 2]},
),
]
return build_workload_profile(
reqs,
WindowRecord(
window_id=window_id,
trace_path=window.trace_path,
trace_type=window.trace_type,
window_start=window.window_start,
window_end=window.window_end,
source_payload=window.source_payload,
),
gpu_count=1,
length_mode="total",
)
p1 = make_profile("same-a", 100, arrival_gap=1.0)
p2 = make_profile("same-b", 100, arrival_gap=1.0)
p3 = make_profile("different", 10000, arrival_gap=0.1)
report = similarity_report([p1, p2, p3])
self.assertAlmostEqual(profile_similarity(p1, p2), 1.0)
self.assertGreater(report["matrix"][0][1], report["matrix"][0][2])
self.assertIn("L", report["pairs"][2]["family_similarity"])
def test_cli_profile_window_outputs_lca_profile(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
study_path = _write_study_assets(tmp_path)
stdout = io.StringIO()
with mock.patch("sys.stdout", stdout):
rc = cli_main(
[
"profile",
"window",
"--spec",
str(study_path),
"--gpu-count",
"8",
]
)
self.assertEqual(rc, 0)
payload = json.loads(stdout.getvalue())
self.assertEqual(payload["profile"]["window_id"], "chat_w1")
self.assertEqual(len(payload["profile"]["vector"]), 10)
self.assertEqual(payload["profile"]["gpu_count"], 8)
def test_cli_profile_window_does_not_resolve_llm_endpoint(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
study_path = _write_study_assets(tmp_path)
payload = json.loads(study_path.read_text(encoding="utf-8"))
payload["llm"]["endpoint"] = {
"provider": "codex",
"model": "gpt-5.4",
}
study_path.write_text(json.dumps(payload), encoding="utf-8")
stdout = io.StringIO()
with mock.patch("sys.stdout", stdout):
rc = cli_main(["profile", "window", "--spec", str(study_path)])
self.assertEqual(rc, 0)
self.assertEqual(json.loads(stdout.getvalue())["profile"]["window_id"], "chat_w1")
def test_harness_uses_latency_failures_before_generic_unrecoverable(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)