Compare commits
2 Commits
d8899c50ce
...
7ba98b6087
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ba98b6087 | |||
| cb89549334 |
221
docs/aituner-maas-collab-overview-20260703.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# AITuner:MaaS Serving Config 自动调优 — 合作概述
|
||||
|
||||
> 面向:配置调优团队
|
||||
> 目的:介绍 AITuner 是什么、为什么它比纯 LLM loop 可靠、以及我们建议的 pilot 合作方式。
|
||||
> 日期:2026-07-03
|
||||
|
||||
## 一句话总结
|
||||
|
||||
AITuner 把 LLM serving engine 的 config tuning 从"人工试错 / LLM 黑盒瞎猜"变成
|
||||
**基于真实测量、按 bottleneck 机制分类、由 validator 把关的自动实验循环**。
|
||||
我们希望在贵团队的真实环境上跑通 1-2 个 case,验证它能否成为你们的日常工具。
|
||||
|
||||
## 1. 问题背景
|
||||
|
||||
MaaS 场景下 tuning 的现状:
|
||||
|
||||
- 平台上有**数百个模型**,模型本身还在持续迭代;
|
||||
- 硬件平台在更新,同一个模型在不同硬件上的最优 config 不同;
|
||||
- 每个 (model, hardware, workload, SLO) 组合都是一个独立的 tuning case;
|
||||
- 人力远远覆盖不了所有 case,大量 case 只能用默认或粗调的 config 上线,
|
||||
留下吞吐和成本上的浪费。
|
||||
|
||||
AITuner 的目标:**自动化 tune 这些没人力覆盖的 case**,输出满足 SLO 的
|
||||
engine config,并附带可审计的实验证据。
|
||||
|
||||
## 2. 为什么"纯 LLM 自动调参"不够
|
||||
|
||||
直接让 LLM 在循环里提 config、跑一轮、再提 config,实践中有两个硬伤:
|
||||
|
||||
**(a) 缺 domain-specific 知识和内部 context。**
|
||||
|
||||
- LLM 会误读 engine knob 的语义,典型例子是 vLLM 的 DP:LLM 常把它当成
|
||||
"免费加吞吐"的开关,忽略它改变的是 replica 数和 per-GPU 效率,
|
||||
在 per-GPU 指标下盲目 scale-out 反而变差;
|
||||
- 内部平台(如 dash)有大量内部环境变量和 launch 约束,LLM 完全没有这部分
|
||||
context,提出的 config 经常直接 launch failure 或 OOM。
|
||||
|
||||
**(b) 缺 bottleneck breakdown,不会像专家一样理解系统。**
|
||||
|
||||
- LLM 拿到的往往只是"pass rate 低了"这样的 scalar 结果,
|
||||
它无法区分瓶颈在 prefill、decode、admission/queueing 还是 memory;
|
||||
- 没有瓶颈归因,proposal 就退化成 knob space 里的随机游走:
|
||||
重复已试过的配置、在错误的 knob family 上反复消耗 GPU trial。
|
||||
|
||||
AITuner 的设计就是补上这两块:把系统知识和瓶颈分析放进一个 **harness**,
|
||||
LLM 只负责在 harness 给出的合法候选里做排序和取舍。
|
||||
|
||||
## 3. AITuner 是怎么工作的(概述)
|
||||
|
||||
一句话:AITuner 是一个自动实验循环——对目标 case 做真实压测,
|
||||
从结果做瓶颈归因,按系统机制生成下一个 config 候选,
|
||||
经 validator 把关后执行下一轮测量,直到证据表明继续实验不再值得。
|
||||
|
||||
```text
|
||||
真实压测 ──> 瓶颈归因 ──> 机制化候选 ──> Validator 把关 ──> 下一轮压测 / 停止
|
||||
```
|
||||
|
||||
对使用方来说,需要知道的只有四点:
|
||||
|
||||
1. **每一步决策都来自真实测量**,不是 LLM 的自然语言猜测——每轮以
|
||||
SLO verdict、pass rate、`request_rate_per_gpu`、launch/OOM 状态为准;
|
||||
2. **像专家一样先归因再动手**:把 SLO 违约症状聚合成 prefill / decode /
|
||||
admission / memory 瓶颈假设,候选 config 只从对应的机制
|
||||
(拓扑切分、prefill 调度、并发准入、KV cache 余量等)中生成,
|
||||
不在 knob 空间里盲搜;
|
||||
3. **Validator 挡住不合法和重复的实验**:engine 参数合法性、硬件/拓扑约束、
|
||||
内部平台 launch 约束、已测配置查重、失败记忆——包括 LLM 在内的任何
|
||||
proposal 来源都必须过这一关;
|
||||
4. **知道什么时候该停**:验证充分或触及测量上限时确定性停止,
|
||||
不多烧 GPU,也不会静默宣称"已经最优"。
|
||||
|
||||
架构与模块细节见 `docs/aituner-harness-design-contract.md`,
|
||||
pilot 阶段可按需深入,这里不展开。
|
||||
|
||||
## 4. 关键性质:不被单一 LLM 绑死
|
||||
|
||||
Harness 是 **planner-agnostic** 的:LLM、确定性 heuristic 甚至 BO/bandit
|
||||
都只是在同一个 CandidateSet 上做排序。目前已经验证:
|
||||
|
||||
- **No-LLM 模式**:在没有任何 LLM endpoint 的情况下,harness 可以作为
|
||||
deterministic planner 完成整轮 tuning(baseline → 假设 → 候选 → 打分 →
|
||||
proposal/stop),已有 Qwen30B 真实轨迹;
|
||||
- 高分确定性候选存在时根本不调 LLM,LLM 只在候选需要复杂 tradeoff
|
||||
排序时介入。
|
||||
|
||||
这意味着**换 LLM 供应商的风险是可控的**:tuning 的正确性来自
|
||||
harness 的证据编译和 validator,而不是某个特定模型的能力。
|
||||
|
||||
## 5. 已有证据(内部实验,3 个 case)
|
||||
|
||||
对照组均为"纯 LLM loop"(同一 LLM、同一压测框架,只关闭 harness),
|
||||
指标为满足 SLO 的 `request_rate_per_gpu`(每 GPU 可承载请求率,越高越好)。
|
||||
|
||||
### Case 1:qwen27b chat 0-8k,dash0 内部 vLLM,H20
|
||||
|
||||
真实 trace 窗口回放(`chat_w20260311_1000`),SLO:95% pass rate、
|
||||
TTFT 2s/4s/6s 分档、TPOT ≤ 50ms。
|
||||
详见 `docs/qwen27b-chat-0-8k-current-config-fig18-20260506.md`。
|
||||
|
||||
| | 纯 LLM loop | AITuner |
|
||||
| --- | --- | --- |
|
||||
| 最终最优 config | TP2/DP1,**0.2025** req/s/GPU | TP4,**0.4429** req/s/GPU(**约 2.2x**) |
|
||||
| baseline(起点相同) | TP1/DP1,0.0350 | TP1/DP1,0.0350 |
|
||||
| 搜索路径 | 第 2/3 轮先选 DP2、DP4,per-GPU 吞吐反而回落;第 4 轮才到 TP2 | 瓶颈归因判定 TTFT/prefill 主导,第 2 轮直接 TP2(0.2142),第 4 轮 TP4(0.4429) |
|
||||
| tuning 开销 | 跑满 12 轮 GPU trial,其中第 5-12 轮全部是无可行点的 runtime probe(纯浪费) | 第 8 轮确定性 stop,实际执行 4 次 GPU trial,全程约 2.5 小时 |
|
||||
|
||||
两个值得注意的点:
|
||||
|
||||
- 纯 LLM loop 的前几轮正是第 2 节所说的 DP 误读实例——LLM 把 DP scale-out
|
||||
当成免费吞吐,per-GPU 效率被稀释,绕了 3 轮弯路;
|
||||
- 单轮真实 trial(engine launch + 多个二分 probe)约 1 小时,跑满 12 轮
|
||||
意味着 10 小时以上的 GPU 占用;AITuner 在拿到约 2.2x 的 config 的同时,
|
||||
把整个 tuning 过程压到约 2.5 小时。
|
||||
|
||||
### Case 2:qwen235b thinking prefill(大模型,TP4 baseline)
|
||||
|
||||
详见 `docs/qwen235b-thinking-prefill-harness-20260427.md`。
|
||||
|
||||
| | 纯 LLM loop | AITuner |
|
||||
| --- | --- | --- |
|
||||
| 最优 config | TP8,**0.3794** req/s/GPU,第 **10** 轮才找到 | TP8,**0.3863** req/s/GPU,第 **2** 轮即超过对照组 12 轮的最优值 |
|
||||
| 搜索路径 | 中途浪费在 DP2、EP4 等失败探索上 | 从 baseline 直接跳到 TP8/DP1,跳过对照组踩过的失败方向 |
|
||||
| tuning 开销 | 12 轮预算 | 到达最优的迭代数从 10 降到 2(**5x**) |
|
||||
|
||||
大模型 case 上单轮 trial 更贵,少跑 8 轮的绝对 GPU 成本节省也更大。
|
||||
|
||||
### Case 3:Qwen3-30B-A3B,社区 vLLM 0.20(非内部环境同样适用)
|
||||
|
||||
详见 `docs/qwen30b-community-vllm020/harness-early-stop-ablation-20260502.md`。
|
||||
两个子实验:
|
||||
|
||||
- **测量上限识别**(search 上限较低时):默认 config 已触及压测搜索上限。
|
||||
AITuner 只执行 1 次 GPU trial 就识别出"当前压测范围测不出更优配置",
|
||||
停止并明确报告原因;纯 LLM loop 把 12 轮预算烧完——第 2 轮 DP2 使
|
||||
per-GPU 吞吐减半,第 3-12 轮**连续 10 轮 launch failure**。
|
||||
净节省 11 轮 GPU 占用。
|
||||
- **放宽上限后的真实搜索**:AITuner 第 4 轮到达最优 config family
|
||||
(TP2 + runtime 精调)并在第 5 轮 stop;纯 LLM loop 经历 EP launch
|
||||
failure、不可行 DP probe 等弯路后第 7 轮才到同一 family(单次测值差
|
||||
约 1.5%,在重复压测噪声内)。
|
||||
|
||||
### 汇总
|
||||
|
||||
| Case | config 性能(vs 纯 LLM loop 最优) | 到达最优迭代数 | 省下的无效 GPU trial |
|
||||
| --- | --- | --- | --- |
|
||||
| qwen27b 内部 vLLM | **约 2.2x**(0.4429 vs 0.2025) | 4 vs 4,但对照组随后 8 轮全部无效 | 8 轮 infeasible probe |
|
||||
| qwen235b thinking | 持平略优(0.3863 vs 0.3794) | **2 vs 10(5x)** | 8 轮失败/弱探索 |
|
||||
| qwen30b 社区 vLLM | 同一 config family(±1.5%) | 4 vs 7;上限场景 1 vs 12 | 最多 11 轮(含 10 轮 launch failure) |
|
||||
|
||||
诚实说明:以上是有限 case 上的证据,qwen30b 子实验使用了有界压缩回放
|
||||
(固定输出长度)做收敛性测试,不等同生产 benchmark;结论是"更快收敛到
|
||||
同等或更好的 config、大幅减少无效 GPU trial",不是全局最优性证明。
|
||||
这正是我们想通过 pilot 在你们的真实 case 上进一步验证的。
|
||||
|
||||
## 6. 你们会得到什么
|
||||
|
||||
对每个 case,AITuner 的产出不只是一个 config:
|
||||
|
||||
1. **满足 SLO 的 engine config**(以 `request_rate_per_gpu` 为主要
|
||||
跨拓扑指标);
|
||||
2. **完整可审计的实验轨迹**:每个 trial 的假设、预期效果、真实测量、
|
||||
confirm/reject 结论,以及 probe 级别的明细(`probe_details.jsonl`);
|
||||
3. **瓶颈归因报告**:这个 case 的限制因素是 prefill、decode、admission
|
||||
还是 memory,为什么;
|
||||
4. **明确的 stop 理由**:是候选耗尽、验证充分,还是 measurement 上限
|
||||
(比如 search 范围)不够——不会静默糊弄。
|
||||
|
||||
这些轨迹本身对你们的人工 tuning 经验沉淀也有价值。
|
||||
|
||||
## 7. 建议的合作方式(pilot)
|
||||
|
||||
我们目前缺少的是贵团队每个 case 的真实环境和硬件。建议直接在你们的
|
||||
环境上做,分四步:
|
||||
|
||||
**Phase 0 — 选 case、对齐输入(约 1 周)**
|
||||
- 双方选定 1-2 个有代表性的 case;
|
||||
- 每个 case 需要:模型 + 硬件规格、SLO 定义(TTFT/TPOT/pass rate)、
|
||||
workload trace 或可复现的流量描述、当前人工 config(作对照基线);
|
||||
- 我们把 dash 平台的内部环境变量 / launch 约束接入 harness 的
|
||||
launch-feasibility 层和 validator。
|
||||
|
||||
**Phase 1 — 在你们环境跑通(1-2 周)**
|
||||
- 部署 AITuner,先跑 baseline 建立测量基线,再跑完整 tune loop;
|
||||
- 我们负责跑通和调试,你们提供环境访问和平台侧支持。
|
||||
|
||||
**Phase 2 — 对比评估**
|
||||
- AITuner 结果 vs 你们的人工 config:SLO 达标情况、`request_rate_per_gpu`、
|
||||
消耗的 GPU trial 数、时间成本;
|
||||
- 全部结果附实验轨迹,可复查。
|
||||
|
||||
**Phase 3 — 决策**
|
||||
- 若 pilot 达标,讨论扩展到更多 case 的方式(接入流程、权限、
|
||||
运行成本、维护分工)。
|
||||
|
||||
## 8. 当前依赖与风险(如实说明)
|
||||
|
||||
1. **LLM 依赖**:当前 planner 使用 gpt-5.5。计划切换到百炼
|
||||
qwen3.7-max 的 dog-fooding API,并做同 case 的效果对比。
|
||||
风险缓冲:harness 的 no-LLM deterministic 路径已经能独立完成
|
||||
相当一部分 tuning(见第 4 节),planner 模型的能力差距被 harness
|
||||
部分补偿,切换成本预计可控——但对比数据出来之前这是一个待验证项。
|
||||
2. **Engine 适配**:当前 mechanism families 主要针对 vLLM 的 knob
|
||||
语义;SGLang 等其他 engine 需要一层 adapter 把 knobs 映射到相同的
|
||||
mechanism vocabulary(架构上已预留,工作量取决于目标 engine)。
|
||||
3. **平台适配**:dash 内部环境变量和 launch 约束需要在 Phase 0 一次性
|
||||
接入,之后由 failure memory 持续积累。
|
||||
4. **边界**:AITuner 保证的是结构化、可审计、测量高效的搜索;
|
||||
它不证明全局最优,瓶颈分类是 symptom-based 的启发式归因而非
|
||||
完美因果诊断。对生产决策来说,可审计比"号称最优"更重要。
|
||||
|
||||
## 9. 我们需要贵团队提供的
|
||||
|
||||
- 1-2 个 case 的测试环境和硬件访问(或由你们的同学代跑,我们远程支持);
|
||||
- 每个 case 的 SLO 定义和 workload trace;
|
||||
- dash 平台内部环境变量 / launch 约束的文档或对接人;
|
||||
- 百炼 qwen3.7-max dog-fooding API 的配额(用于 LLM 切换对比)。
|
||||
|
||||
---
|
||||
|
||||
附:更完整的设计语义见 `docs/aituner-harness-design-contract.md`,
|
||||
harness 各机制与实验证据见 `docs/aituner-harness-summary.md`。
|
||||
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/harness-ablation/figures/knob-conditional-delta-summary.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
2481
docs/harness-ablation/figures/knob-conditional-delta-summary.svg
Normal file
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 227 KiB |
3090
docs/harness-ablation/figures/knob-oat-counterexample-c1-qwen30b.svg
Normal file
|
After Width: | Height: | Size: 102 KiB |
192
docs/harness-ablation/knob-conditional-effects-20260705.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Knob conditional effect 证据整理
|
||||
|
||||
本文整理 2026-07-01 到 2026-07-03 在 `dash1` 上跑的 interaction screening 结果,用来支持一个具体论点:
|
||||
|
||||
> Serving tuning knobs 不是彼此独立的。一个 knob 的收益方向和收益大小依赖当前 topology、admission/concurrency 和 scheduler context,因此不能假设“逐个 knob tune 到最好”一定可靠。
|
||||
|
||||
## Presentation review: 应该怎么展示
|
||||
|
||||
原来的 delta summary 能证明 `Delta_knob(context)` 不同,但它不够直观,因为它没有展示 tuning algorithm 会怎么失败。更适合作为 paper 主图的是:
|
||||
|
||||
1. **主图:OAT path counterexample**
|
||||
在同一个 measured response surface 上画两条 one-knob-at-a-time 路径。读者能直接看到:同一个起点、不同单维 tuning 顺序,会停在不同点,而且其中一个是 coordinate-wise local optimum。
|
||||
2. **补充图:interaction residual**
|
||||
用 additive model residual 形式说明:如果 TP 和 MNS 是独立贡献,残差应接近 0;实际残差有结构性正负块。
|
||||
3. **补充图:delta/context summary**
|
||||
保留为形式化证据,但不作为主图,因为它不能直接展示 OAT 的路径依赖。
|
||||
|
||||
因此本文推荐把 `knob-oat-counterexample-c1-qwen30b` 作为主文图,把 C3 crossed lines 和 residual/delta 放在 appendix 或机制分析图中。
|
||||
|
||||
## 图 1:OAT path counterexample
|
||||
|
||||

|
||||
|
||||
数据来源:
|
||||
|
||||
- `interaction-mixed-qwen30b-tp-mns-surface-high1-dash1-d8899c5-20260701T095858Z`
|
||||
- `interaction-mixed-qwen30b-tp4-mns-nocap-qps20-dash1-d8899c5-20260701T161900Z`
|
||||
|
||||
这张图直接展示为什么“逐个维度独立 tune”不可靠。我们从同一个起点 `TP=1, MNS=8` 出发:
|
||||
|
||||
| Strategy | Path | Final req/s/GPU |
|
||||
|---|---|---:|
|
||||
| tune MNS first, then TP | `TP1,MNS8 -> TP1,MNS16 -> TP4,MNS16` | `2.44` |
|
||||
| tune TP first, then MNS | `TP1,MNS8 -> TP2,MNS8 -> TP2,MNS32` | `3.28` |
|
||||
|
||||
`TP4,MNS16` 是一个 measured coordinate-wise local optimum:
|
||||
|
||||
- 固定 `TP=4` 调 `MNS`:`MNS16/32/64` 都是 `2.44`,没有 strictly improving move;
|
||||
- 固定 `MNS=16` 调 `TP`:`TP4=2.44` 高于 `TP1=2.35` 和 `TP2=2.27`;
|
||||
- 但全局最好点 `TP2,MNS32=3.28` 比它高 `25.6%`。
|
||||
|
||||
这比单纯说 “MNS 的 delta 依赖 TP” 更有力:它展示了一个实际 tuning path 如何被独立维度假设带到次优点。要从 `TP4,MNS16` 逃到 `TP2,MNS32`,tuner 必须允许非独立的 context-aware move,或者至少维护 frontier/plateau 上的反事实 anchor;单维 greedy OAT 不够。
|
||||
|
||||
这里的结论不是“所有 workload 都有强 interaction”,而是更严格地说:
|
||||
|
||||
1. 在真实 case 中确实存在明显 conditional effect;
|
||||
2. 这个现象足以否定 naive one-knob-at-a-time/OAT 作为通用 tuning strategy;
|
||||
3. harness 需要维护 mechanism-aware context,而不是把 knobs 当作独立维度。
|
||||
|
||||
## Formal definition
|
||||
|
||||
记某个 engine config 的 SLO-feasible objective 为:
|
||||
|
||||
```text
|
||||
f(config) = max request_rate_per_gpu subject to pass_rate >= target
|
||||
```
|
||||
|
||||
对 knob `x` 的一个 intervention `x_low -> x_high`,在 context `c` 下的效果定义为:
|
||||
|
||||
```text
|
||||
Delta_x(c) = f(x_high, c) - f(x_low, c)
|
||||
```
|
||||
|
||||
如果存在两个 context `c1, c2`,使得:
|
||||
|
||||
```text
|
||||
Delta_x(c1) != Delta_x(c2)
|
||||
```
|
||||
|
||||
则说明 knob `x` 存在 conditional effect。若符号也变化,比如一个 context 下提升、另一个 context 下降,则是更强的 interaction。
|
||||
|
||||
## 图 2:C1 Qwen30B mixed workload surface
|
||||
|
||||

|
||||
|
||||
数据来源:
|
||||
|
||||
- `interaction-mixed-qwen30b-tp-mns-surface-high1-dash1-d8899c5-20260701T095858Z`
|
||||
- `interaction-mixed-qwen30b-tp4-mns-nocap-qps20-dash1-d8899c5-20260701T161900Z`
|
||||
|
||||
关键观察:
|
||||
|
||||
| Context | `MNS=8 -> 32` 的 req/s/GPU 变化 |
|
||||
|---|---:|
|
||||
| `TP=1` | `2.10 -> 2.28`, `+8.7%` |
|
||||
| `TP=2` | `2.28 -> 3.28`, `+44.3%` |
|
||||
| `TP=4` | `1.28 -> 2.44`, `+90.3%` |
|
||||
|
||||
这说明 `max-num-seqs` 的收益强烈依赖 `tensor-parallel-size`。同一个 `MNS` 调整在 `TP=1` 下只是小幅提升,在 `TP=2/4` 下变成决定性能上限的关键 knob。
|
||||
|
||||
反过来看,`TP` 的收益也依赖 `MNS`:
|
||||
|
||||
- 在 `MNS=8` 时,`TP=4` 是坏点,只有 `1.28 req/s/GPU`;
|
||||
- 在 `MNS=32` 时,`TP=2` 变成全局最优附近,达到 `3.28 req/s/GPU`。
|
||||
|
||||
因此,如果 tuner 固定 `MNS=8` 去判断 topology,会错误低估 `TP=4`,也会无法看到 `TP=2 + MNS=32` 的最佳区域;如果固定 `TP=1` 去调 `MNS`,又会低估更高 TP 下 concurrency knob 的价值。这就是 OAT order sensitivity。
|
||||
|
||||
## 图 3:C1 additive residual
|
||||
|
||||

|
||||
|
||||
如果 `TP` 和 `MNS` 可以独立建模,一个简单 additive model:
|
||||
|
||||
```text
|
||||
f(TP, MNS) ~= base + effect(TP) + effect(MNS)
|
||||
```
|
||||
|
||||
应该留下接近 0 的 residual。实际 residual 最大达到约 `0.46 req/s/GPU`,而且呈现结构性模式:
|
||||
|
||||
- `TP2,MNS32/64` 是正 residual,说明这个组合比独立效应相加更好;
|
||||
- `TP2,MNS16` 和 `TP4,MNS8` 是强负 residual,说明某些组合显著低于独立假设预测。
|
||||
|
||||
这张图适合放在机制/appendix 中,用数学形式支持“不是独立 knob effect”。
|
||||
|
||||
## 图 4:C3 Qwen235B decode workload
|
||||
|
||||

|
||||
|
||||
数据来源:
|
||||
|
||||
- `interaction-qwen235b-decode-c3-topo-mns-mbt-fixed-dash1-d8899c5-20260703T022514Z`
|
||||
|
||||
完整 8 点结果:
|
||||
|
||||
| Config | req/s/GPU | pass rate |
|
||||
|---|---:|---:|
|
||||
| `TP4 DP2 EP8 MNS64 MBT256` | `0.0535` | `1.0000` |
|
||||
| `TP4 DP2 EP8 MNS64 MBT384` | `0.0535` | `0.9922` |
|
||||
| `TP4 DP2 EP8 MNS128 MBT256` | `0.0590` | `0.9929` |
|
||||
| `TP4 DP2 EP8 MNS128 MBT384` | `0.0590` | `0.9929` |
|
||||
| `TP2 DP4 EP8 MNS64 MBT256` | `0.0590` | `0.9753` |
|
||||
| `TP2 DP4 EP8 MNS64 MBT384` | `0.0535` | `0.9961` |
|
||||
| `TP2 DP4 EP8 MNS128 MBT256` | `0.0590` | `0.9788` |
|
||||
| `TP2 DP4 EP8 MNS128 MBT384` | `0.0590` | `0.9823` |
|
||||
|
||||
关键观察:
|
||||
|
||||
- `MBT 256 -> 384` 在 `TP4/DP2 + MNS64` 下没有收益;
|
||||
- 同一个 `MBT 256 -> 384` 在 `TP2/DP4 + MNS64` 下反而下降约 `9.2%`;
|
||||
- `MNS 64 -> 128` 在 `TP4/DP2` 下提升约 `10.1%`;
|
||||
- 同一个 `MNS 64 -> 128` 在 `TP2/DP4 + MBT256` 下没有收益,但在 `TP2/DP4 + MBT384` 下恢复约 `10.1%`。
|
||||
|
||||
这说明 runtime knobs 的作用不是单调独立的。`MBT` 是否有害取决于 topology 和 `MNS`;`MNS` 是否有用也取决于 topology 和 `MBT`。
|
||||
|
||||
## 图 5:Delta 形式的直接证据
|
||||
|
||||

|
||||
|
||||
这张图把上面的论证直接转成 `Delta_x(context)`:
|
||||
|
||||
- C1 中,同样是 `MNS 8 -> 32`,收益从 `+8.7%` 到 `+90.3%` 不等;
|
||||
- C3 中,同样是 `MBT 256 -> 384`,有的 context 是 `0%`,有的 context 是 `-9.2%`;
|
||||
- C3 中,同样是 `MNS 64 -> 128`,有的 context 是 `0%`,有的 context 是 `+10.1%`。
|
||||
|
||||
这就是 conditional effect 的直接测量证据。
|
||||
|
||||
## C2 是边界案例,不是反例
|
||||
|
||||
C2 Qwen235B prefill tight SLO 的结果更弱:
|
||||
|
||||
- `TP4` family: `0.1067~0.1175 req/s/GPU`;
|
||||
- `TP8` family: `0.1727 req/s/GPU`;
|
||||
- 在测过的 `MNS={64,128}`、`MBT={8192,16384}` 网格里 runtime knobs 基本平。
|
||||
|
||||
这个 case 说明并不是每个 workload 都会在 runtime knobs 上表现出强 interaction。它的主要结论是 topology 主导:`TP8` 相比 `TP4` 约 `+47% req/s/GPU`。
|
||||
|
||||
这对 paper framing 反而有用:我们的 claim 不应该是“所有 knobs 总是强耦合”,而应该是:
|
||||
|
||||
> Tuning system 不能预设 knobs 独立;它必须通过 measured response 判断当前 case 是 topology-dominant、runtime-interaction-dominant,还是 flat/noisy。Harness 的作用是把这些 measured evidence 维护成 search context。
|
||||
|
||||
## 对 harness 设计的含义
|
||||
|
||||
这些图支持我们当前 framing:
|
||||
|
||||
1. Harness 不应该只做单 knob local search。它需要保留 topology/runtime context,并允许 joint or projected interventions。
|
||||
2. Candidate generation 不能只说“把某个 knob 调大/调小”,而要说明这个 intervention 所依赖的 context。
|
||||
3. Validator 不能只比较 raw request rate;必须比较 SLO-feasible `request_rate_per_gpu`,并保存 negative evidence。
|
||||
4. LLM/planner 的价值不应被描述成“猜一个更好的 knob 值”,而是基于 harness 提供的 measured context 去提出 plausible joint moves。
|
||||
|
||||
## 复现图
|
||||
|
||||
```bash
|
||||
python3 scripts/plot_knob_conditional_effects.py
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
- `docs/harness-ablation/figures/knob-oat-counterexample-c1-qwen30b.png`
|
||||
- `docs/harness-ablation/figures/knob-interaction-residual-c1-qwen30b.png`
|
||||
- `docs/harness-ablation/figures/knob-conditional-c1-qwen30b-surface.png`
|
||||
- `docs/harness-ablation/figures/knob-conditional-c3-qwen235b-decode-lines.png`
|
||||
- `docs/harness-ablation/figures/knob-conditional-delta-summary.png`
|
||||
381
scripts/plot_knob_conditional_effects.py
Normal file
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plot measured knob conditional effects for the AITuner harness study."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from matplotlib.lines import Line2D
|
||||
from matplotlib.patches import Patch
|
||||
|
||||
|
||||
OUT = Path("docs/harness-ablation/figures")
|
||||
|
||||
|
||||
def save(fig: plt.Figure, name: str) -> None:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
fig.savefig(OUT / f"{name}.png", dpi=220, bbox_inches="tight")
|
||||
fig.savefig(OUT / f"{name}.svg", bbox_inches="tight")
|
||||
|
||||
|
||||
def plot_c1_surface() -> None:
|
||||
# Qwen30B mixed workload, TP x MNS screen. Values are req/s/GPU.
|
||||
# Source runs:
|
||||
# - interaction-mixed-qwen30b-tp-mns-surface-high1-dash1-d8899c5-20260701T095858Z
|
||||
# - interaction-mixed-qwen30b-tp4-mns-nocap-qps20-dash1-d8899c5-20260701T161900Z
|
||||
mns = np.array([8, 16, 32, 64])
|
||||
tp = np.array([1, 2, 4])
|
||||
values = np.array(
|
||||
[
|
||||
[2.1000, 2.3500, 2.2833, 2.2833],
|
||||
[2.2750, 2.2750, 3.2833, 3.2583],
|
||||
[1.2833, 2.4417, 2.4417, 2.4417],
|
||||
]
|
||||
)
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(12.8, 4.8), gridspec_kw={"width_ratios": [1.05, 1.2]})
|
||||
ax = axes[0]
|
||||
im = ax.imshow(values, cmap="YlGnBu", aspect="auto", vmin=1.2, vmax=3.35)
|
||||
ax.set_xticks(range(len(mns)), labels=mns)
|
||||
ax.set_yticks(range(len(tp)), labels=[f"TP={x}" for x in tp])
|
||||
ax.set_xlabel("max-num-seqs (MNS)")
|
||||
ax.set_ylabel("tensor-parallel-size")
|
||||
ax.set_title("C1 response surface: req/s/GPU")
|
||||
for i in range(values.shape[0]):
|
||||
for j in range(values.shape[1]):
|
||||
color = "white" if values[i, j] > 2.75 else "black"
|
||||
ax.text(j, i, f"{values[i, j]:.2f}", ha="center", va="center", color=color, fontsize=10)
|
||||
cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
||||
cbar.set_label("req/s/GPU")
|
||||
|
||||
ax = axes[1]
|
||||
colors = {1: "#4E79A7", 2: "#59A14F", 4: "#E15759"}
|
||||
for idx, t in enumerate(tp):
|
||||
ax.plot(mns, values[idx], marker="o", linewidth=2.4, color=colors[int(t)], label=f"TP={t}")
|
||||
ax.set_xscale("log", base=2)
|
||||
ax.set_xticks(mns, labels=mns)
|
||||
ax.set_xlabel("max-num-seqs (MNS)")
|
||||
ax.set_ylabel("req/s/GPU")
|
||||
ax.set_title("Non-parallel lines imply interaction")
|
||||
ax.grid(True, axis="y", alpha=0.28)
|
||||
ax.legend(frameon=False)
|
||||
ax.annotate(
|
||||
"TP=2 only becomes best\nwhen MNS reaches 32",
|
||||
xy=(32, 3.2833),
|
||||
xytext=(20, 3.05),
|
||||
arrowprops={"arrowstyle": "->", "lw": 1.2},
|
||||
fontsize=9,
|
||||
)
|
||||
ax.annotate(
|
||||
"TP=4 is bad at MNS=8\nbut recovers at MNS>=16",
|
||||
xy=(8, 1.2833),
|
||||
xytext=(10, 1.55),
|
||||
arrowprops={"arrowstyle": "->", "lw": 1.2},
|
||||
fontsize=9,
|
||||
)
|
||||
fig.suptitle("Knob effects are conditional: MNS effect depends on TP", fontsize=14, y=1.02)
|
||||
fig.tight_layout()
|
||||
save(fig, "knob-conditional-c1-qwen30b-surface")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def plot_c1_oat_counterexample() -> None:
|
||||
# C1 Qwen30B: one-knob-at-a-time tuning gets trapped at a coordinate-wise
|
||||
# local optimum 25.6% below the measured global best. The right panel zooms
|
||||
# into the trap's neighbourhood so the reader can SEE that every single-knob
|
||||
# move from the trap is worse or flat, instead of having to read a caption.
|
||||
mns = [8, 16, 32, 64]
|
||||
tp = [1, 2, 4]
|
||||
values = np.array(
|
||||
[
|
||||
[2.1000, 2.3500, 2.2833, 2.2833],
|
||||
[2.2750, 2.2750, 3.2833, 3.2583],
|
||||
[1.2833, 2.4417, 2.4417, 2.4417],
|
||||
]
|
||||
)
|
||||
idx = {(t, s): (mns.index(s), tp.index(t)) for t in tp for s in mns}
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(14.0, 6.2), gridspec_kw={"width_ratios": [1.5, 1.0]})
|
||||
ax = axes[0]
|
||||
im = ax.imshow(values, cmap="YlGnBu", aspect="auto", vmin=1.2, vmax=3.35)
|
||||
ax.set_xticks(range(len(mns)), labels=mns)
|
||||
ax.set_yticks(range(len(tp)), labels=[f"TP={x}" for x in tp])
|
||||
ax.set_xlabel("max-num-seqs (MNS)", fontsize=11)
|
||||
ax.set_ylabel("tensor-parallel-size", fontsize=11)
|
||||
ax.set_title("Two OAT paths from the same start", fontsize=12, loc="left")
|
||||
for i in range(values.shape[0]):
|
||||
for j in range(values.shape[1]):
|
||||
color = "white" if values[i, j] > 2.75 else "black"
|
||||
ax.text(j, i, f"{values[i, j]:.2f}", ha="center", va="center", color=color, fontsize=11, weight="bold")
|
||||
|
||||
def draw_path(path: list[tuple[int, int]], color: str, labels: list[str]) -> None:
|
||||
for (a, b) in zip(path, path[1:]):
|
||||
x0, y0 = idx[a]
|
||||
x1, y1 = idx[b]
|
||||
ax.annotate(
|
||||
"",
|
||||
xy=(x1, y1),
|
||||
xytext=(x0, y0),
|
||||
arrowprops={"arrowstyle": "->", "lw": 3.2, "color": color, "shrinkA": 22, "shrinkB": 22},
|
||||
)
|
||||
for (a, b), lbl in zip(zip(path, path[1:]), labels):
|
||||
x0, y0 = idx[a]
|
||||
x1, y1 = idx[b]
|
||||
mx, my = (x0 + x1) / 2, (y0 + y1) / 2
|
||||
if x0 == x1: # vertical move -> label to the side
|
||||
ax.text(
|
||||
mx + 0.30,
|
||||
my,
|
||||
lbl,
|
||||
color=color,
|
||||
fontsize=9.5,
|
||||
ha="left",
|
||||
va="center",
|
||||
weight="bold",
|
||||
bbox={"boxstyle": "round,pad=0.2", "facecolor": "white", "edgecolor": "none", "alpha": 0.85},
|
||||
)
|
||||
else: # horizontal move -> label above
|
||||
ax.text(
|
||||
mx,
|
||||
my - 0.32,
|
||||
lbl,
|
||||
color=color,
|
||||
fontsize=9.5,
|
||||
ha="center",
|
||||
va="bottom",
|
||||
weight="bold",
|
||||
bbox={"boxstyle": "round,pad=0.2", "facecolor": "white", "edgecolor": "none", "alpha": 0.85},
|
||||
)
|
||||
|
||||
# Red: tune MNS first, then TP -> walks into a coordinate-wise local optimum.
|
||||
draw_path([(1, 8), (1, 16), (4, 16)], "#C0392B", ["tune MNS", "tune TP"])
|
||||
# Green: tune TP first, then MNS -> reaches the measured global best.
|
||||
draw_path([(1, 8), (2, 8), (2, 32)], "#2E7D32", ["tune TP", "tune MNS"])
|
||||
|
||||
# start / trap / best markers (explained by the legend below the grid)
|
||||
sx, sy = idx[(1, 8)]
|
||||
ax.scatter([sx], [sy], marker="o", s=210, facecolors="none", edgecolors="black", linewidths=2.2, zorder=5)
|
||||
trap = (4, 16)
|
||||
tx, ty = idx[trap]
|
||||
ax.add_patch(plt.Rectangle((tx - 0.5, ty - 0.5), 1, 1, fill=False, edgecolor="#C0392B", linewidth=3.4))
|
||||
best = (2, 32)
|
||||
bx, by = idx[best]
|
||||
ax.add_patch(plt.Rectangle((bx - 0.5, by - 0.5), 1, 1, fill=False, edgecolor="#2E7D32", linewidth=3.4))
|
||||
|
||||
legend_elements = [
|
||||
Line2D([0], [0], marker="o", color="w", markerfacecolor="none", markeredgecolor="black", markeredgewidth=2, markersize=10, label="start TP1,MNS8 = 2.10"),
|
||||
Patch(facecolor="none", edgecolor="#C0392B", linewidth=2.4, label="OAT trap TP4,MNS16 = 2.44 (no improving single-knob move)"),
|
||||
Patch(facecolor="none", edgecolor="#2E7D32", linewidth=2.4, label="global best TP2,MNS32 = 3.28"),
|
||||
]
|
||||
ax.legend(handles=legend_elements, loc="upper center", bbox_to_anchor=(0.5, -0.11), ncol=1, frameon=False, fontsize=9.5)
|
||||
cbar = fig.colorbar(im, ax=ax, fraction=0.038, pad=0.035)
|
||||
cbar.set_label("req/s/GPU")
|
||||
|
||||
# ---- right panel: why the red path stops (trap neighbourhood zoom) ----
|
||||
ax2 = axes[1]
|
||||
ax2.set_xlim(-0.55, 3.55)
|
||||
ax2.set_ylim(-0.65, 3.7)
|
||||
ax2.set_aspect("equal")
|
||||
ax2.set_axis_off()
|
||||
ax2.set_title("Why the red path stops here", fontsize=12, loc="left")
|
||||
|
||||
# 3x3 neighbourhood of the trap (TP4,MNS16). Rows match left panel:
|
||||
# top=TP2, middle=TP4(trap), bottom=TP8(not measured). Cols: MNS 8/16/32.
|
||||
# (col, row) with row 0 at bottom.
|
||||
zoom_cells = {
|
||||
(1, 2): ("2.275", "TP2", "dead"), # up neighbour
|
||||
(0, 1): ("1.28", "TP4", "dead"), # left neighbour
|
||||
(1, 1): ("2.44", "TP4", "trap"), # the trap
|
||||
(2, 1): ("2.44", "TP4", "flat"), # right neighbour (flat, not strictly improving)
|
||||
(1, 0): ("—", "TP8", "oob"), # down neighbour (not measured)
|
||||
}
|
||||
for (col, row), (val, tplabel, kind) in zoom_cells.items():
|
||||
x, y = col, row
|
||||
if kind == "trap":
|
||||
fc, ec, lw = "#FDECEA", "#C0392B", 3.2
|
||||
elif kind == "oob":
|
||||
fc, ec, lw = "#F2F2F2", "#CCCCCC", 1.0
|
||||
else:
|
||||
fc, ec, lw = "#FDF2F2", "#E6B0AA", 1.4
|
||||
ax2.add_patch(plt.Rectangle((x, y), 1, 1, facecolor=fc, edgecolor=ec, linewidth=lw))
|
||||
if kind == "oob":
|
||||
ax2.text(x + 0.5, y + 0.5, "no data", ha="center", va="center", fontsize=9, color="#999", style="italic")
|
||||
else:
|
||||
val_color = "#C0392B" if kind in ("dead", "flat") else "#222"
|
||||
ax2.text(x + 0.5, y + 0.62, val, ha="center", va="center", fontsize=13, weight="bold", color=val_color)
|
||||
sublabel = tplabel
|
||||
if kind == "trap":
|
||||
sublabel = f"{tplabel} · trap"
|
||||
elif kind == "flat":
|
||||
sublabel = f"{tplabel} · flat"
|
||||
ax2.text(x + 0.5, y + 0.28, sublabel, ha="center", va="center", fontsize=8.5, color="#666")
|
||||
if kind in ("dead", "flat"):
|
||||
ax2.text(x + 0.87, y + 0.87, "✗", ha="center", va="center", color="#C0392B", fontsize=18, weight="bold")
|
||||
|
||||
# axis labels for the zoom
|
||||
ax2.text(-0.18, 2.5, "TP=2", ha="right", va="center", fontsize=9, color="#666")
|
||||
ax2.text(-0.18, 1.5, "TP=4", ha="right", va="center", fontsize=9, color="#666")
|
||||
ax2.text(-0.18, 0.5, "TP=8", ha="right", va="center", fontsize=9, color="#999")
|
||||
ax2.text(0.5, -0.18, "MNS=8", ha="center", va="top", fontsize=9, color="#666")
|
||||
ax2.text(1.5, -0.18, "MNS=16", ha="center", va="top", fontsize=9, color="#666")
|
||||
ax2.text(2.5, -0.18, "MNS=32", ha="center", va="top", fontsize=9, color="#666")
|
||||
|
||||
ax2.text(
|
||||
1.5,
|
||||
3.45,
|
||||
"Every measured single-knob move from the trap\nis worse or flat → coordinate ascent is stuck",
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=10.5,
|
||||
color="#222",
|
||||
bbox={"boxstyle": "round,pad=0.35", "facecolor": "white", "edgecolor": "#C0392B"},
|
||||
)
|
||||
|
||||
fig.suptitle("One-knob-at-a-time tuning gets trapped: 25.6% throughput gap between two tuning orders", fontsize=14, y=1.02)
|
||||
fig.tight_layout()
|
||||
save(fig, "knob-oat-counterexample-c1-qwen30b")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def plot_c1_interaction_residual() -> None:
|
||||
# If TP and MNS were independent additive effects, this residual matrix would be near zero.
|
||||
mns = [8, 16, 32, 64]
|
||||
tp = [1, 2, 4]
|
||||
values = np.array(
|
||||
[
|
||||
[2.1000, 2.3500, 2.2833, 2.2833],
|
||||
[2.2750, 2.2750, 3.2833, 3.2583],
|
||||
[1.2833, 2.4417, 2.4417, 2.4417],
|
||||
]
|
||||
)
|
||||
residual = values - values.mean(axis=1, keepdims=True) - values.mean(axis=0, keepdims=True) + values.mean()
|
||||
fig, ax = plt.subplots(figsize=(7.2, 4.8))
|
||||
limit = float(np.abs(residual).max())
|
||||
im = ax.imshow(residual, cmap="RdBu", aspect="auto", vmin=-limit, vmax=limit)
|
||||
ax.set_xticks(range(len(mns)), labels=mns)
|
||||
ax.set_yticks(range(len(tp)), labels=[f"TP={x}" for x in tp])
|
||||
ax.set_xlabel("max-num-seqs (MNS)")
|
||||
ax.set_ylabel("tensor-parallel-size")
|
||||
ax.set_title("C1 non-additive interaction residual")
|
||||
for i in range(residual.shape[0]):
|
||||
for j in range(residual.shape[1]):
|
||||
ax.text(j, i, f"{residual[i, j]:+.2f}", ha="center", va="center", fontsize=10)
|
||||
cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
||||
cbar.set_label("req/s/GPU residual")
|
||||
fig.suptitle("Independent-knob additive model leaves large structured residuals", fontsize=13, y=1.02)
|
||||
fig.tight_layout()
|
||||
save(fig, "knob-interaction-residual-c1-qwen30b")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def plot_c3_lines() -> None:
|
||||
# Qwen235B decode C3, topology x MNS x MBT screen.
|
||||
# Source run:
|
||||
# interaction-qwen235b-decode-c3-topo-mns-mbt-fixed-dash1-d8899c5-20260703T022514Z
|
||||
data = {
|
||||
("TP4/DP2/EP8", 64, 256): 0.05354166666666667,
|
||||
("TP4/DP2/EP8", 64, 384): 0.05354166666666667,
|
||||
("TP4/DP2/EP8", 128, 256): 0.058958333333333335,
|
||||
("TP4/DP2/EP8", 128, 384): 0.058958333333333335,
|
||||
("TP2/DP4/EP8", 64, 256): 0.058958333333333335,
|
||||
("TP2/DP4/EP8", 64, 384): 0.05354166666666667,
|
||||
("TP2/DP4/EP8", 128, 256): 0.058958333333333335,
|
||||
("TP2/DP4/EP8", 128, 384): 0.058958333333333335,
|
||||
}
|
||||
mbt = [256, 384]
|
||||
topologies = ["TP4/DP2/EP8", "TP2/DP4/EP8"]
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(11.5, 4.6), sharey=True)
|
||||
for ax, topo in zip(axes, topologies):
|
||||
for mns, color in [(64, "#4E79A7"), (128, "#F28E2B")]:
|
||||
vals = [data[(topo, mns, b)] for b in mbt]
|
||||
ax.plot(mbt, vals, marker="o", linewidth=2.6, color=color, label=f"MNS={mns}")
|
||||
for x, y in zip(mbt, vals):
|
||||
ax.text(x, y + 0.0007, f"{y:.4f}", ha="center", fontsize=9)
|
||||
ax.set_title(topo)
|
||||
ax.set_xlabel("max-num-batched-tokens (MBT)")
|
||||
ax.set_xticks(mbt)
|
||||
ax.grid(True, axis="y", alpha=0.28)
|
||||
ax.set_ylim(0.050, 0.062)
|
||||
axes[0].set_ylabel("req/s/GPU")
|
||||
axes[1].legend(frameon=False, loc="lower right")
|
||||
fig.suptitle("C3: MBT effect depends on topology and MNS", fontsize=14, y=1.02)
|
||||
fig.tight_layout()
|
||||
save(fig, "knob-conditional-c3-qwen235b-decode-lines")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def plot_delta_summary() -> None:
|
||||
c1_base = {
|
||||
"TP=1": (2.2833 - 2.1000) / 2.1000 * 100.0,
|
||||
"TP=2": (3.2833 - 2.2750) / 2.2750 * 100.0,
|
||||
"TP=4": (2.4417 - 1.2833) / 1.2833 * 100.0,
|
||||
}
|
||||
c3_mbt = {
|
||||
"TP4/DP2\nMNS=64": 0.0,
|
||||
"TP4/DP2\nMNS=128": 0.0,
|
||||
"TP2/DP4\nMNS=64": (0.05354166666666667 - 0.058958333333333335)
|
||||
/ 0.058958333333333335
|
||||
* 100.0,
|
||||
"TP2/DP4\nMNS=128": 0.0,
|
||||
}
|
||||
c3_mns = {
|
||||
"TP4/DP2\nMBT=256": (0.058958333333333335 - 0.05354166666666667)
|
||||
/ 0.05354166666666667
|
||||
* 100.0,
|
||||
"TP4/DP2\nMBT=384": (0.058958333333333335 - 0.05354166666666667)
|
||||
/ 0.05354166666666667
|
||||
* 100.0,
|
||||
"TP2/DP4\nMBT=256": 0.0,
|
||||
"TP2/DP4\nMBT=384": (0.058958333333333335 - 0.05354166666666667)
|
||||
/ 0.05354166666666667
|
||||
* 100.0,
|
||||
}
|
||||
|
||||
panels = [
|
||||
("C1: MNS 8->32\nunder different TP", c1_base, "#59A14F"),
|
||||
("C3: MBT 256->384\nunder different context", c3_mbt, "#E15759"),
|
||||
("C3: MNS 64->128\nunder different context", c3_mns, "#4E79A7"),
|
||||
]
|
||||
fig, axes = plt.subplots(1, 3, figsize=(16, 5.2))
|
||||
for ax, (title, vals, color) in zip(axes, panels):
|
||||
labels = list(vals.keys())
|
||||
y = np.arange(len(labels))
|
||||
x = list(vals.values())
|
||||
colors = [color if v >= 0 else "#B07AA1" for v in x]
|
||||
ax.barh(y, x, color=colors)
|
||||
ax.axvline(0, color="black", linewidth=0.8)
|
||||
lo = min(x)
|
||||
hi = max(x)
|
||||
pad = max(2.0, (hi - lo) * 0.12)
|
||||
ax.set_xlim(lo - pad, hi + pad)
|
||||
ax.set_yticks(y, labels=labels, fontsize=8)
|
||||
ax.invert_yaxis()
|
||||
ax.set_xlabel("relative change in req/s/GPU (%)")
|
||||
ax.set_title(title)
|
||||
ax.grid(True, axis="x", alpha=0.25)
|
||||
for yi, xi in zip(y, x):
|
||||
ha = "left" if xi >= 0 else "right"
|
||||
offset = 0.7 if xi >= 0 else -0.7
|
||||
ax.text(xi + offset, yi, f"{xi:+.1f}%", va="center", ha=ha, fontsize=8)
|
||||
fig.suptitle("The same knob intervention has context-dependent effect size", fontsize=14, y=1.02)
|
||||
fig.tight_layout()
|
||||
save(fig, "knob-conditional-delta-summary")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
plot_c1_oat_counterexample()
|
||||
plot_c1_interaction_residual()
|
||||
plot_c1_surface()
|
||||
plot_c3_lines()
|
||||
plot_delta_summary()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||