Computing Systems Optimization
目标不是一项技术
此前的 AI Technology Value Stack 回答了“为什么选择昂贵、可测、可复用的 AI 系统瓶颈”;AI Optimization Stack 回答了“模型、训练、推理、运行时、Kernel、通信与硬件分别控制什么”。本文进一步回答:我要把这些层培养成怎样的一项长期能力,以及怎样带领团队共同完成它。
表面目标是提高硬件利用率,更准确的目标却是:
在正确性、模型质量、服务 SLO、功耗、可靠性与成本约束下,提高端到端关键路径上的有效工作比例。
这一定义排除了三个危险捷径:
- 忙不等于有效。 设备可以因为重试、冗余搬运、错误的 Batch、无效 Padding 或拥塞一直忙,但单位成本产出的有效结果反而更少。
- 局部满不等于系统快。 一个 Kernel 达到高算力利用率,仍可能被排队、通信、Host 调度或流水空泡吞掉端到端收益。
- 容量不是速率。 显存或内存容量决定任务能不能放下;带宽决定数据多久搬完。二者需要分别建模。
因此,我要长期拥有五种相互闭环的能力:
| 能力 | 要回答的问题 | 可验证产物 |
|---|---|---|
| 工作负载理解 | 软件真正计算什么,哪些语义不能改变? | 计算图、状态图、输入输出契约 |
| 统一性能建模 | 计算、访存、通信、容量和空泡如何共同决定上限? | 资源账本、时序模型、预测区间 |
| 优化实现 | 应拆分、重排、融合、掩盖还是改变并行策略? | 代码、Microbenchmark、端到端 A/B |
| 硬件反馈 | 哪个硬件短板的边际改善最值钱? | 工作负载分布、敏感性与规格建议 |
| 技术组织 | 怎样让多人并行工作而不丢失系统最优? | 模块合同、责任边界、集成验收 |
优化的统一对象
四本账
可扩展的模型不应从某个 Profiler 指标开始,而应先维护四本相互连接的账:
- 逻辑账: 软件的节点、依赖、状态和不可改变的语义。它回答“先做什么、后做什么,哪些分支可以并行”。
- 资源账: 每个节点需要多少计算、设备内存流量、网络流量、内存/显存容量和工作空间。它回答“消耗了什么”。
- 时序账: 每个对象何时产生、最后一次被谁使用、何时释放;每个引擎何时工作、等待或争用。它回答“为什么出现空泡和峰值”。
- 证据账: 预测值、实测值、环境、误差与边界。它回答“这项判断在什么条件下成立”。
只有逻辑账,没有资源量,就只能画流程图;只有资源账,没有时序,就会把可重叠的时间机械相加;只有时序,没有对象生命周期,就解释不了显存峰值;没有证据账,模型再精细也只是故事。
资源—图—时序模型
我把这套可复用机制称为 资源—图—时序模型。概念层的目标是提高有效工作;机制层把工作负载变成图、资源需求和可执行时序;它不是某个框架补丁,每个项目仍需把模型实例化为真实的算子、Collective、Buffer、Stream、队列和硬件拓扑。
对象与公式
公式之前先固定核心对象,避免同一个“带宽”在规格、Microbenchmark 和实际任务之间偷换含义:
| 对象 | 生产者 | 单位或形态 | 消费者 | 生命周期与状态 |
|---|---|---|---|---|
| 工作负载图 $G=(V,E)$ | 语义与依赖分解 | 节点和有向边 | 调度器、模块 Owner | 持久、版本化模型 |
| 节点需求 $F_i,B_i^{mem},B_i^{net}$ | 计算量、数据流与通信分析 | operations、bytes | 速率下界与资源账本 | 每组 Shape/精度/拓扑维护 |
| 活跃对象 $M_j(t)$ | 生产算子与状态机 | 随时间变化的 bytes | 峰值容量模型与未来消费者 | 物理对象,最后消费后才可释放 |
| 可达上限 $P_i^{att},BW_i^{att},NW_i^{att}$ | 受控 Microbenchmark | operations/s、bytes/s | 节点下界 | 硬件、Shape、访问和拓扑参数 |
| 调度 $S$ | 依赖与资源约束求解 | 开始、结束、引擎、Buffer | makespan 与生命周期 | 版本化实验产物 |
| 实测 $Y$ 与残差 $\epsilon$ | Profiler 和端到端 Benchmark | Trace、时间、bytes、计数器 | 模型校准 | 实测不可改,模型可更新 |
设工作负载图为 $G=(V,E)$。节点 $v_i$ 表示一个计算或有状态阶段,边 $e_{ij}$ 表示数据、控制或所有权依赖。对节点 $i$ 记录:
- $F_i$:有效计算量,单位为 operations;
- $B_i^{mem}$:设备内存层级之间的有效流量,单位为 bytes;
- $B_i^{net}$:设备或节点之间的通信量,单位为 bytes;
- $M_j(t)$:对象 $j$ 在时刻 $t$ 仍存活的容量,单位为 bytes。
再用同一 Shape、数据类型、访问模式和拓扑下的可达上限 $P_i^{att}$、$BW_i^{att}$、$NW_i^{att}$ 计算一个速率下界:
$$
T_i^{roof}
\ge
\max
\left(
\frac{F_i}{P_i^{att}},
\frac{B_i^{mem}}{BW_i^{att}},
\frac{B_i^{net}}{NW_i^{att}}
\right)
$$
这个式子只回答“最快不能低于多少”,不直接预测真实耗时。真实节点时间 $T_i$ 还要加入启动、同步、Shape 尾部、资源占用、协议和运行时校准;系统时间也不是 $\sum_i T_i$,而是依赖图与资源约束下的 makespan,即最后一个必要输出完成的时间。
容量则是另一条约束:
$$
M_{peak}
\max_t
\left(
\sum_{j \in live(t)} M_j(t)
- M_{workspace}(t)
- M_{allocator}(t)
- M_{margin}(t)
\right)
$$
其中 live(t) 是时刻 $t$ 仍会被未来计算消费的对象集合。一个对象大小不变,只要提前结束生命周期,也可能降低峰值;反过来,逻辑上已经“不再使用”的 View 仍引用底层 Storage 时,显存并不会释放。
在一个测量窗口内,资源 $r$ 的有效利用率可写成:
$$
U_r
\frac{D_r^{useful}}
{C_r^{att} \cdot T_{window}}
$$
$D_r^{useful}$ 是产生最终有效结果所必需的需求,$C_r^{att}$ 是实际分配给该工作负载的可达能力,而不是整机规格表峰值。对稳态流水,Little 的排队关系给出 $L=\lambda W$:平均在途任务数 $L$ 等于吞吐 $\lambda$ 乘平均停留时间 $W$。它可以检查并发量是否足以填满流水,但只有在长期平均有限、系统近似稳定时才适用。[^little]
完整伪代码
完整的教学伪代码如下。它不隐藏分解、计量、调度、校准和回写步骤:
1 | def analyze_system(workload, hardware, baseline_measurement): |
对象完整性审计的标准是:图上每个节点和边都能回到输入、输出或状态;公式中的每个 demand 都有计量方法;每个物理对象都有生产者、最后消费者与释放时刻;每个可达上限都带 Shape、精度、拓扑和环境。没有来源的对象不能靠一个“其他开销”长期隐藏。
五视图与边界
读这张图时,不能只看一条箭头:A 面板验证是否真的减少空闲;B 面板保留“新代价”;C 面板要求实测残差回写;D 面板检查等待与释放位置;E 面板确保每个资源数字都有数据对象来源。
模型如何逐层收敛
全局先看关键路径
全局模型关注的不是每个模块有多快,而是总资源如何分配、哪个依赖链决定完成时间、哪些设备在何时出现空泡。至少要同时保留:
- 依赖关键路径: 不能通过增加并行度消除的前后关系;
- 资源关键路径: 逻辑可并行,但争用同一计算单元、HBM、网络或 Host 线程;
- 容量关键路径: 某组对象必须同时存活,迫使 Chunk、Offload 或重计算;
- 队列关键路径: 请求到达、Batch 形成、抢占和长尾 Shape 造成的等待;
- 功耗关键路径: 计算、HBM 与互连并发后触发功耗墙或降频。
这一步的产物应是一张全局时序图和资源热力图。若端到端时间由通信后的 Barrier 决定,把非关键分支上的 Kernel 再优化 20% 也不会产生相同比例收益。Amdahl 早已指出,序列或 housekeeping 部分会限制并行加速;现代系统只是把“序列部分”扩展成同步、调度、数据准备与尾部等待。[^amdahl]
模块再看对象流
每个模块都要回答一个相同的问题模板:
| 视图 | 必答问题 |
|---|---|
| 计算流程图 | 做了哪些 Projection、MatMul、Reduce、Collective 或状态更新? |
| 时序图 | 输入到达多久后开始,多久后产生第一批与全部输出? |
| 逻辑图 | 哪些依赖必须保留,哪些分支可以拆开或重排? |
| 数据量图 | 输入、输出、中间对象、网络消息与工作空间各有多少 bytes? |
| 资源上限 | 计算、访存、通信和容量中的哪一项先触顶? |
| 验收图 | Microbenchmark 改变了哪个物理量,端到端是否进入关键路径? |
模块模型不是一次性 Excel。它应以 Shape、数据类型、并行拓扑、Batch/序列分布和硬件版本为参数;每次实测都留下预测、残差与解释。模型能迁移到第二个模块或第二种硬件,才开始形成长期竞争力。
上限差距不能消失
“建模估算与实际的差距”至少要拆成四类:
- 对象误差: 少算了一个 Buffer、重计算、Layout 转换、Host 同步或通信协议。
- 上限误差: 把规格峰值当成了特定 Shape、精度、访问模式和拓扑下的可达上限。
- 调度误差: 假设可以重叠,实际存在依赖、引擎冲突、Buffer 复用、Barrier 或尾部不均衡。
- 环境误差: 频率、功耗、温度、ECC、NUMA、网络拥塞、后台任务和版本变化。
因此,差距不能被彻底“避免”,但可以用五级上限梯子缩小并定位:
| 层级 | 上限来源 | 回答的问题 |
|---|---|---|
| 规格峰值 | 手册中的 FLOPS、带宽、容量 | 物理设计宣称什么? |
| 可达上限 | 同 Shape/精度/访问/拓扑的 Microbenchmark | 当前环境最多能交付什么? |
| Kernel/Collective 实测 | 单个实现的 Profiler | 实现离可达上限多远? |
| 模块实测 | 含调用、转换、同步与工作空间 | 集成开销是多少? |
| 端到端实测 | 真实请求/训练步/任务 | 是否进入关键路径并改善主指标? |
Roofline 论文的核心价值不是精确预测,而是用上界和瓶颈指导下一步。Figure 1 把计算峰值与可持续内存带宽共同画成上界;右图也显示,计算峰值提高而内存带宽不变时,转折点会右移,更多程序可能仍受内存限制。[^roofline]
论文 Table 4 更重要的证据是:不同 Kernel 在不同机器上靠近的上限并不相同。例如表中 Intel Xeon 的 SpMV 达到 11.1 GB/s,而相邻上限是 11.2 GB/s;IBM Cell 的 SpMV 达到 47.1 GB/s,而相邻上限是 47.6 GB/s。其他行则暴露 FMA、SIMD、Affinity 或 TLP 等更低一层天花板。[^roofline]
实务中应把每级差距写成残差:
1 | 端到端残差 |
每次只在证据支持时增加模型细节。若新增参数不能改变瓶颈判断、预测区间或设计决策,它只是复杂度,不是理解力。
拆分、掩盖与流水
拆分、掩盖和流水不是三个口号,而是对同一时序图的三种物理动作:
| 动作 | 改变什么 | 直接收益 | 新代价与失败条件 |
|---|---|---|---|
| 拆分 | 把大对象或长任务切成可调度 Chunk | 降低局部峰值、增加并行机会、改善尾部 | 更多启动、边界处理、通信与碎片;Chunk 太小会反向变慢 |
| 掩盖 | 让相互独立的计算、通信或搬运同时发生 | 从关键路径隐藏部分等待 | 需要独立引擎、依赖自由、双 Buffer 和足够在途工作;资源争用会让两边都变慢 |
| 流水 | 让不同阶段处理不同批次或 Micro-batch | 填补阶段空闲,提高稳态吞吐 | 启停气泡、阶段不均衡、状态数量、尾部与同步仍然存在 |
CUDA 的异步接口允许 Host、Device、内存传输和设备间操作表达并发,但官方文档也明确指出,实际并发取决于硬件能力,并需要在结果被消费前同步。[^cuda-async] 因此,“调用是 async”只能证明提交方式,不能证明 makespan 已经缩短。
一个优化动作只有同时满足下面四个条件才算成立:
- 语义可拆: 依赖、数值和状态允许切分或重排;
- 资源可并: 两条路径没有争用同一个瓶颈引擎、容量或功耗预算;
- 对象可活: Buffer 的生产、消费、复用和释放时机正确;
- 证据可见: Trace 中的空泡缩小,端到端指标在同条件 A/B 中改善。
让模型反向设计硬件
感知硬件短板不是看到某个计数器 100%,而是回答一个反事实问题:
如果只增加这一项资源,保持软件语义、其他硬件能力和成本约束不变,端到端目标能改善多少?
可以对可达计算、HBM 带宽、互连带宽、片上容量、显存容量、DMA 并发或特定指令做局部敏感性实验:
$$
S_r
-\frac{\Delta T_{end}}{\Delta C_r}
$$
$S_r$ 表示资源 $r$ 的边际时间价值。实际硬件优先级还要除以面积、功耗、成本和实现风险,并在工作负载分布上求期望,而不是只看一个模型的最好 Case:
1 | 硬件优先级 |
这里的 $S_r$ 应通过多个受控容量点做有限差分和置信区间,而不是把一次模拟结果当作全局线性导数。瓶颈移动后,继续增加同一资源的边际价值通常会下降。
这里的“软件可达性”非常关键。若编译器、运行时、指令、内存一致性或通信接口无法稳定表达目标数据流,再高的硬件峰值也不会被应用使用。Hennessy 与 Patterson 把软件推动架构创新、提升软硬件接口视为新架构机会;这支持双向反馈,却不保证任何定制单元都会商业成功。[^golden-age]
面向硬件团队的建议不应写成“需要更大带宽”,而应包含:
- 代表性工作负载、Shape 与时间分布;
- 当前关键路径和可复现基线;
- 资源增加前后的模拟或原型结果;
- 软件、编译器和运行时所需接口;
- 面积、功耗、容量、可靠性和成本边界;
- 哪些 Case 获益,哪些 Case 不变或退化。
把系统切成团队
模块边界
团队分工不能先画组织架构,再强迫软件服从。应先从软件图和资源图选择模块边界:
- 状态所有权清楚: 哪个模块创建、更新、持有和释放对象?
- 接口相对稳定: 输入输出与语义稳定,内部实现可以独立变化。
- 验证可以闭环: 模块能独立做正确性、性能和边界测试。
- 资源冲突可描述: 计算、通信、容量和功耗争用能在接口上暴露。
- 并行工作真实存在: 两个团队不必频繁修改同一状态机或等待同一决策。
Parnas 的模块化原则不是按处理步骤机械切函数,而是把可能变化的设计决策隐藏在模块内部,以降低变化传播。[^parnas] 对性能团队,还要再加一条:把需要联合优化的资源冲突暴露成共同合同,而不是藏进某一方的实现细节。
每个模块至少维护四类合同:
| 合同 | 内容 | 失败示例 |
|---|---|---|
| 功能合同 | 输入、输出、状态、正确性与精度 | 两边对 Padding、顺序或状态语义理解不同 |
| 资源合同 | FLOPs、bytes、容量、工作空间、并发引擎 | 局部优化偷偷增加全局峰值 |
| 时序合同 | 何时 ready、何时完成、同步与释放 | async 接口返回被误当作数据可用 |
| 证据合同 | 基线、环境、指标、回归与回滚 | 只给最好一次数字,无法集成复现 |
责任结构
我希望管理的不是一群孤立专家,而是一套共享模型下的模块所有者:
| 角色 | 主要责任 | 不应独占的权力 |
|---|---|---|
| 系统性能负责人 | 定义端到端目标、全局模型、关键路径和模块合同 | 不能替模块所有者写完所有详细实现 |
| 计算模块负责人 | 算子、编译、Tile、融合和计算上限 | 不能忽略数据搬运与端到端占比 |
| 通信模块负责人 | Collective、P2P、拓扑、协议与 Overlap | 不能只报告链路带宽 |
| 内存与运行时负责人 | 对象生命周期、Allocator、队列、调度和 Host 路径 | 不能把容量问题等同于带宽问题 |
| 测量与集成负责人 | Trace、基线、回归、环境控制与跨模块验收 | 不能成为事后测试员 |
| 硬件协同接口人 | 聚合工作负载、敏感性和规格建议 | 不能从单个热点直接承诺硬件收益 |
Conway 在 1968 年指出,组织结构会限制可有效探索的设计方案;任务一旦委派,也会立即产生协调问题。[^conway] 因此,团队边界必须跟随系统概念迭代。若新的 Trace 证明两个模块长期共同决定同一关键路径,就应建立联合 Owner、共同基线或重新切分,而不是坚持最初组织图。
技术管理
这也澄清了我对“管理”的真实目标:我未必想把主要精力放在人员行政、情绪协调和汇报层级上,但我需要具备技术组织能力。
技术管理不是离开技术,而是:
- 用统一对象和指标消除口径冲突;
- 让全局模型决定优先级,而不是职位或声音大小;
- 给模块 Owner 足够的设计空间,同时守住接口与证据;
- 在局部最优伤害系统最优时及时止损;
- 培养第二层负责人,使模型、实现和判断不依赖一个人。
团队每个迭代按同一个闭环运行:
- 系统负责人冻结口径: 发布当前 Baseline、前三个关键瓶颈、不可改变的语义和端到端验收。
- 模块 Owner 提交反事实: 说明改哪个对象、预期改变哪条资源或生命周期、会新增什么代价。
- 接口双方共同评审: 对跨模块 Buffer、完成事件、Shape 与回滚条件建立共同 Owner,不能把风险推到边界外。
- 测量与集成统一验收: 先验局部物理量,再验端到端关键路径;失败结果同样进入证据账。
- 复盘后重画系统: 更新残差、优先级与模块边界;瓶颈移动时及时停止旧方向。
最终,一名系统负责人不需要掌握每个模块的全部代码,却必须能审查每个模块的对象、数据量、时序、上限、残差与集成影响。团队规模扩展的本质,是让判断模型扩展,而不是让会议数量扩展。
培养路线
这条职业路线可以按责任半径逐步扩大:
| 阶段 | 核心任务 | 必须留下的资产 | 晋级证据 |
|---|---|---|---|
| 单模块 | 对一个算子、Collective 或运行时路径建立完整模型 | 对象账本、Microbenchmark、Profiler、边界测试 | 预测能解释主要残差,局部收益可复现 |
| 端到端链路 | 把多个模块放入同一关键路径与容量模型 | 全局时序、峰值账本、同语义 A/B | 能拒绝无端到端价值的局部优化 |
| 跨硬件迁移 | 在 GPU/NPU 或两代硬件上校准同一模型 | 可达上限梯子、迁移规则、失败 Case | 能区分通用机制与硬件补丁 |
| 软硬协同 | 用敏感性与原型提出硬件/接口改进 | 工作负载集、反事实结果、规格合同 | 建议能覆盖一类负载而非单点热点 |
| 团队负责人 | 让多个 Owner 在共享模型下并行交付 | 模块合同、集成节奏、培养与复盘机制 | 团队离开个人微观指挥仍能稳定闭环 |
每个项目结束时,至少沉淀五件东西:
- 工作负载图: 语义、Shape、状态和依赖;
- 资源账本: 计算、访存、通信、容量和功耗;
- 时序与生命周期: 关键路径、空泡、峰值和释放;
- 证据包: Baseline、A/B、Trace、环境与残差;
- 复用模块: 代码、工具、模型、接口、测试或硬件建议。
失败标准
下面几种状态说明路线正在偏离:
- 只追计数器: 指标漂亮,但单位成本、端到端时间、质量或 SLO 没有改善。
- 只做局部补丁: 每个新模型都重新适配,无法沉淀第二个 Case 可复用的对象和接口。
- 模型无限变复杂: 参数越来越多,却不能改变瓶颈判断、预测区间或优先级。
- 按组织切软件: 团队边界遮蔽真实依赖,接口处长期出现返工和无人负责的性能损失。
- 从热点跳到硬件: 没有反事实敏感性、覆盖率和面积功耗成本,就提出下一代规格。
- 负责人亲自承包一切: 局部质量依赖一个人,模块 Owner 没有模型与验收责任,团队无法扩展。
总结
我的长期职业目标不是绑定某个模型、框架、芯片或岗位名称,而是建立一项可迁移的系统能力:
从软件逻辑出发,把计算、访存、通信、容量、调度和功耗放进同一资源—图—时序模型;通过拆分、掩盖与流水提高关键路径上的有效硬件利用率;用实测残差收敛模型,用反事实敏感性指导下一代硬件,再把模型转化为可并行、可验收、可培养的团队责任。
这条路线的核心竞争力不是“知道答案”,而是能把复杂系统变成可解释对象,把估算误差变成下一步调查,把局部优化变成端到端证据,把个人理解变成团队共同使用的模型。
参考资料
[^roofline]: Samuel Williams, Andrew Waterman, David Patterson, Roofline: An Insightful Visual Performance Model for Multicore Architectures, Communications of the ACM 52(4), 2009. 本文图表取自 OSTI 公开 PDF。
[^amdahl]: Gene M. Amdahl, Validity of the Single Processor Approach to Achieving Large Scale Computing Capabilities, AFIPS Spring Joint Computer Conference, 1967.
[^little]: John D. C. Little, A Proof for the Queuing Formula: L = λW, Operations Research 9(3), 1961.
[^parnas]: David L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules, Communications of the ACM 15(12), 1972.
[^conway]: Melvin E. Conway, How Do Committees Invent?, Datamation, April 1968.
[^golden-age]: John L. Hennessy, David A. Patterson, A New Golden Age for Computer Architecture, Communications of the ACM 62(2), 2019.
[^cuda-async]: NVIDIA, CUDA Programming Guide: Asynchronous Execution and Asynchronous Data Copies, accessed 2026-07-31.
Computing Systems Optimization