Compare commits

...

2 Commits

Author SHA1 Message Date
7ba98b6087 Add knob conditional effect figures 2026-07-06 13:19:09 +08:00
cb89549334 Add MaaS collaboration overview 2026-07-06 13:19:00 +08:00
13 changed files with 11888 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
# AITunerMaaS 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 的 DPLLM 常把它当成
"免费加吞吐"的开关,忽略它改变的是 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 完成整轮 tuningbaseline → 假设 → 候选 → 打分 →
proposal/stop已有 Qwen30B 真实轨迹;
- 高分确定性候选存在时根本不调 LLMLLM 只在候选需要复杂 tradeoff
排序时介入。
这意味着**换 LLM 供应商的风险是可控的**tuning 的正确性来自
harness 的证据编译和 validator而不是某个特定模型的能力。
## 5. 已有证据内部实验3 个 case
对照组均为"纯 LLM loop"(同一 LLM、同一压测框架只关闭 harness
指标为满足 SLO 的 `request_rate_per_gpu`(每 GPU 可承载请求率,越高越好)。
### Case 1qwen27b chat 0-8kdash0 内部 vLLMH20
真实 trace 窗口回放(`chat_w20260311_1000`SLO95% 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/DP10.0350 | TP1/DP10.0350 |
| 搜索路径 | 第 2/3 轮先选 DP2、DP4per-GPU 吞吐反而回落;第 4 轮才到 TP2 | 瓶颈归因判定 TTFT/prefill 主导,第 2 轮直接 TP20.2142),第 4 轮 TP40.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 轮弯路;
- 单轮真实 trialengine launch + 多个二分 probe约 1 小时,跑满 12 轮
意味着 10 小时以上的 GPU 占用AITuner 在拿到约 2.2x 的 config 的同时,
把整个 tuning 过程压到约 2.5 小时。
### Case 2qwen235b 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 3Qwen3-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 105x** | 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. 你们会得到什么
对每个 caseAITuner 的产出不只是一个 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 你们的人工 configSLO 达标情况、`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`

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 102 KiB

View 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 或机制分析图中。
## 图 1OAT path counterexample
![OAT counterexample](figures/knob-oat-counterexample-c1-qwen30b.png)
数据来源:
- `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。
## 图 2C1 Qwen30B mixed workload surface
![C1 Qwen30B surface](figures/knob-conditional-c1-qwen30b-surface.png)
数据来源:
- `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。
## 图 3C1 additive residual
![C1 interaction residual](figures/knob-interaction-residual-c1-qwen30b.png)
如果 `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”。
## 图 4C3 Qwen235B decode workload
![C3 Qwen235B decode lines](figures/knob-conditional-c3-qwen235b-decode-lines.png)
数据来源:
- `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`
## 图 5Delta 形式的直接证据
![Delta summary](figures/knob-conditional-delta-summary.png)
这张图把上面的论证直接转成 `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`

View 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()