用设计文档取代代码:SMART重塑ML性能建模

发布时间:2026/9/9 6:17:58
用设计文档取代代码:SMART重塑ML性能建模
1. 开篇当“写代码”不再是 ML 性能建模的核心这两年做大模型和 ML 系统优化的人基本都撞上过同一个痛点性能建模库。说白了就是用一个可计算的模型去预估某个算子、某个 kernel、某段融合逻辑在真实硬件上的运行时间、显存占用或者访存行为。这类库在编译器优化、AUTOTUNING、架构探索、容量规划里都起着核心作用但因为要模拟硬件调度、访存规律、指令级并行这类底层细节建库的门槛一直很高维护起来也极其痛苦。传统路子是这样算法工程师把算子行为写成数学公式或伪代码系统工程师再一步步翻译成 C/CUDA 或者专门的 DSL 描述一旦硬件换代、算子形态变化整个库要跟着大改。后来大家开始尝试让大语言模型直接生成性能模型代码省掉人工翻译环节。但实测下来问题非常明显——LLM 生成的代码结构灵活、注释缺失、验证困难经常一个看似合理的“性能模型”存在编译错误或逻辑错误而且调试时你根本搞不清是模型算错了还是代码写错了。Google DeepMind 和 MIT 联合发布的 SMART 论文把这个逻辑彻底反转了一下核心产物不再是代码而是一份“设计文档”。系统先把算子意图转成结构化、带注释的中间表示再由可验证的编译流水线把它翻译成可执行的性能模型。这么做的好处是文档是人类可读的可以逐行审查代码是从文档派生出来的可以批量验证出问题时你能定位到“设计层”而不是“实现层”。我一开始看到这个标题时也带着怀疑设计文档取代代码是不是又一种“听起来很合理但落不了地”的学术口号但仔细读完 SMART 的实现细节并动手把论文里的思路套到我手头一个算子性能预估场景里之后觉得这个方向确实值得参考。这篇博文我就从“为什么文档能取代代码”“实现路径怎么走”“实际使用中要注意什么”三个角度把 SMART 的做法拆开讲一遍适合正在做 ML 性能建模、编译器优化或者大模型推理调优的朋友参考。2. 整个思路的解构为什么“设计文档”比“代码”更适合当核心产物2.1 传统性能建模库的痛点不只是“写代码累”先把底层的困境说清楚。我做过几年 GPU kernel 优化也维护过内部性能建模工具最深的感觉是性能建模库真正难的点不是“实现某个公式”而是“让模型的抽象层级和真实硬件行为对齐”。举个例子一个卷积算子你可以用 FLOPs、访存量、并行度这些粗粒度指标估算耗时但真实 GPU 上卷积还要考虑显存带宽竞争、L2 cache 命中率、寄存器阻塞register blocking策略、warp 调度顺序、甚至不同架构的时钟频率差异。想让性能模型预测精度达到 10% 以内你得把访存模式、循环分块、向量化宽度这些细节都表达出来。这种表达一旦落到具体代码里就会变得极难维护。CUDA 代码本身就是命令式逻辑夹杂大量循环、索引计算、同步原语性能建模的关注点访存模式、计算密度、依赖链长度反而被淹没在实现细节里。你改一个 tile 大小可能连带改十几处索引计算换一个硬件架构整个访存模型又要重写。代码就是“怎么算”的详细说明它把“模型应该是什么样”和“模型怎么被表达”死死绑在一起这是传统建模库折腾人的根源。再从团队协作角度说代码也不是一个好“契约”。搞算法的人用公式和心理模型思考搞系统的人用数据结构和执行流程思考代码作为衔接物一方面太啰嗦另一方面又太容易产生歧义。代码对了但读代码的人不一定能说出“这个模型假设了几级缓存”“这个模型忽略了哪些依赖项”。2.2 SMART 的核心反转用“结构化的设计文档”作为表达语言SMART 的核心思路说到底就是换一种中间表达方式不再让 LLM 直接输出可执行代码而是先输出一份带注释的结构化设计文档。这个“设计文档”不是我们日常写的 Word 版架构说明而更像一种高度结构化的中间表示。它描述的是一个算子或者一个建模场景的“意图”包括要建模的算子是什么比如矩阵乘、卷积、逐元素算子关键的访存模式比如按行访问、按列访问、分块访问、连续访问计算密度和并行度如何分配硬件特性假设比如每个 SM 支持多少线程、cache 大小、带宽上限输入尺寸和分块策略的描述这份文档不包含一行可执行代码但它包含了足够的信息让后续的编译流水线能把它“翻译”成代码。最关键的是文档本身是人类可读、可验证、可修改的。你在 review 一份性能模型时直接看文档就能发现“这个模型对 L2 cache 命中的假设是不是错了”“这个模型是不是忘了把共享内存 bank conflict 算进去”。我觉得这个设计高明在它把“建模的知识表达”和“代码的机械生成”做了解耦。文档层负责承载领域知识代码层只是一个自动生成物。如果你发现预测结果不准你优先修改的是文档里对模型行为的描述而不是去翻几百行 CUDA 代码找索引 bug。这就像工程师画电路原理图和生产 PCB 版图的关系——原理图决定功能版图生成是流程的一部分。这里有一个关键的取舍值得展开为什么不干脆让 LLM 直接生成一个 DSL 代码比如 Halide 或者 TVM 那样区别在于SMART 的设计文档不仅是“前端描述”它还充当了验证和转换的中心枢纽。论文里提到生成的设计文档会经过“模块化编译流水线”处理每一步都有检查和转换边界这样每个环节都能单独验证。直接生成 DSL 代码虽然语义层级高了一些但 DSL 本身也是代码LLM 的幻觉风险依然直接作用于最终产物验证起来还是很难。2.3 “可验证性”是整个文档驱动思路的灵魂前面反复提到“验证”这是 SMART 区别于“让 LLM 写代码”的最本质点。在直接生成代码的模式里LLM 的 output 就是一个代码片段你唯一能做的验证就是编译一下、跑一跑看看有没有报错和真实硬件对比一下精度。问题在于性能模型的“对错”不像普通程序那样非黑即白——代码能编译、能运行并不代表它建模是准确的。一个模型可能算出的时间是真实耗时的 10 倍但代码依然在正常运行。SMART 把验证前移了它在“设计文档”阶段就进行结构验证和一致性检查。论文里举了个例子SMART 用带注释的中间表示来描述存储访问模式这种表示和应用二进制接口结构对齐能实现精确定位。在真实模型实验中99.4% 的合成样本可以被真实模型代表正确解释。这个数字很关键——它说明设计文档的表达能力已经足够强绝大多数算子行为都能被结构化捕获而不是需要自由形式的自然语言描述。3. 实操视角SMART 的一套设计文档到底长什么样怎么落地3.1 从算子意图到结构化设计文档的转换流程为了不变成纯理论空谈我把 SMART 在实际使用中的流程还原成一个可操作的描述。假设我们要给一个常见的 GEMM矩阵乘算子写性能模型传统方式是写一个 CUDA 风格的 kernel 并统计耗时SMART 的思路是先构建一份设计文档。第一步是定义算子的输入输出规格。矩阵形状 M、N、K数据类型FP16/BF16/FP32是否需要转置这些信息构成了文档的基础字段。第二步是描述访存模式。比如矩阵 A 是否按行主序连续访问矩阵 B 是否按列主序访问结果矩阵 C 是否原地更新是否采用分块策略将 A 和 B 的 tile 加载到共享内存。注意这些描述用的是结构化字段和注释不是代码。第三步是描述计算和并行策略。GPU 上有多少个线程块每个块内有多少线程一个线程算几个输出元素循环展开因子是多少是否使用向量化加载比如 float4 逐个加载。第四步是描述硬件假设。比如 L2 cache 容量、HBM 带宽、SM 数量、时钟频率。论文里提到LLM 生成的模型可能会用微调的硬件模型去匹配真实芯片数据这提醒我们硬件参数最好是文档中明确标注可选项而不是让后续生成过程自己修改。完成这四步你就得到了一个结构化的设计文档。这份文档可以被 LLM 直接翻译成 Python/CUDA 代码用来生成一个性能预测脚本也可以被编译流水线转换成更底层的仿真接口还可以直接拿来当团队评审材料。3.2 模块化编译流水线文档是如何一步步变成可执行模型的SMART 最有工程价值的部分是它设计了一套模块化编译流水线把设计文档逐步翻译成可执行模型。我把它拆成几个环节语法和语义检查检查设计文档是否完整、字段是否合法。比如你要建模矩阵乘文档里却没有 M、N、K 的任何定义这个环节就会直接报错。中间表示生成把设计文档翻译成一种更接近代码的中间表示这一步还会补充一些默认值和处理逻辑比如某些未声明的硬件参数从默认配置里继承。代码合成把中间表示映射到目标语言。论文里重点讨论了两种映射一种是对应 CUDA/C 内核的性能模型另一种是对应高层运行时配置的模型。反向模式验证这一步很有意思系统会把生成的代码反推回设计文档检查两者是否一致。这个“可逆性”检查能捕获很多传统方法发现不了的问题。我在自己的一个小实验里实际模拟了这个流程我准备了一个非常简单的逐元素向量加法算子描述让 LLM 生成设计文档然后手动把文档翻译成 Python 的预测函数。翻译时我发现文档里的“访存模式连续访问每线程处理 4 个元素”这个字段直接决定了预测函数里应该用顺序循环还是按 float4 向量化。如果没有这个文档层级的信息光看“逐元素算子”这个描述很难防止生成代码做出错误的假设。3.3 实操中遇到的几个关键参数和选择逻辑在实际使用这个思路时有几个参数和选择很关键值得单独拿出来讲。第一个是“文档粒度”的选择。太粗的设计文档比如“给 GEMM 建模型”只有一句话LLM 生成时依然要靠猜太细的文档比如完整描述每一行循环索引和每一处同步逻辑那和你直接写代码也没区别。我个人的经验是把粒度控制在“一次函数调用或者一个 kernel 启动”的级别。对于 GEMM 这种算子文档粒度到“描述 register blocking、tile size、访存顺序”就够了不需要具体到每一行代码。第二个是硬件参数是否开放给生成过程。论文里提到在实验中使用 LoRA 微调过的计算模型并让生成过程匹配真实芯片数据。这其实是一条重要经验如果不给 LLM 指定硬件参数它倾向于生成一个“通用但符合直觉”的模型比如认为带宽是无限、访存延迟处处相等这种模型在常识场景下表现还行但一碰真实硬件就偏差很大。第三个是“验证阈值”怎么定。设计文档生成的代码不能用“能不能跑”来判断好坏而要和真实硬件数据对比相对误差。论文报告的性能提升是 6 倍以上指的是在预测精度上比 LLM 直接生成代码的模型强了 6 倍多。我在实验中给自己定了一个实操线相对误差在 15% 以内的模型视为可用超出范围就要回头修改文档而不是去改代码。第四个是编译流程要不要区分“训练模式”和“推理模式”。在训练模式下你允许文档生成后反复迭代、微调在推理模式下文档已经定型直接用编译流水线产出代码即可。这种区分在性能和工程灵活性之间做了平衡也是我在实际搭建内部工具时会保留的设计。4. 与现有生态的碰撞SMART 怎么在真实系统里立足4.1 性能建模库的两大阵营SMART 站在哪一边现在的 ML 性能建模生态大致分两个方向。一个是以 TVM、Halide 为代表的搜索编译导向阵营这类工具强调用自动调度搜索去替代人工设计性能模型直接嵌入编译器后端指导自动调优器选择最佳配置。另一个是架构模拟器导向阵营比如利用周期级模拟器或简化模型去评估新硬件架构这类场景对性能和精度的需求极其苛刻。从设计理念看SMART 更接近前者的前沿延伸它的核心目标是让“模型构建过程”更可控而不是完全替代硬件仿真。但它的文档驱动思路对后者也有启发——架构探索阶段你可以用设计文档快速生成不同硬件参数下的性能模型评估各种设计选择而不用每次重写模拟器插件。有意思的是论文里呈现的实验重点不是整套编译器性能优化而是“性能建模库本身的构建质量和精度”。他们用真实硬件模型和合成样例做验证重点考察“生成的模型能不能忠实反映硬件行为”而不是“模型能不能帮一套编译器把 benchmark 跑得更快”。这说明 SMART 的定位更底层它先解决“模型构建可信度”再去谈“模型驱动的上层优化”。4.2 用 LLM 构建性能模型的已有尝试为什么效果不尽如人意这两年拿 LLM 直接生成性能模型的尝试不少。大家很快发现五个典型问题代码结构性错误LLM 生成循环时经常少一层括号、少一个索引偏移最终生成的代码可以编译但完全不符合预期行为。建模假设不透明模型里隐含了“L2 cache 无限大”“访存带宽恒定”这类假设没有任何注释用户根本不知道这些假设是否适用于自己的硬件。调试极其困难当模型预测不准时你很难定位是“代码实现有 bug”还是“模型本身的假设有问题”。两种问题混在一起排查成本极高。硬件迁移能力差一份针对 A100 写的模型代码换到 H100 上往往需要大量改动但没有任何注释告诉你哪些参数是与硬件相关的。验证维度单一大多数方案只验证“生成出来的代码能不能运行”没有做语义一致性和逻辑一致性的检查。这些问题的根源就是“设计信息”和“实现代码”被混为一谈。SMART 用文档把设计信息显性化用编译流水线把实现代码自动化让上面五个问题各自归位这就是它最核心的竞争力。4.3 我试过的一种“轻量版 SMART”落地方式文档模板先行如果你暂时不打算复刻 SMART 整套流水线可以像我一样先尝试一种轻量版在团队内部把“性能建模任务”的输入格式改造成结构化文档模板。具体做法是设计一个 JSON/YAML schema字段包括 ops 类型GEMM/Conv/Elementwise/Softmax、input_shape、data_type、access_pattern、parallelization_strategy、hardware_assumptions、target_metric。要求每个性能建模任务必须先填写这个模板再让 LLM 基于模板内容生成代码。我实际测试的效果是生成的代码可比性大幅提升因为 LLM 有了明确字段约束不会自由发挥。更重要的是反馈和 review 流程变快了——之前发现模型不准要把整个脚本从头到尾读一遍现在直接看模板里的 access_pattern 和 parallelization_strategy 两个字段就能定位问题因为大部分偏差都来自这两个字段的错误假设。这个方法虽然不是 SMART 论文的完整实现但它验证了同一个核心理念当你要让 AI 帮你构建一个需要深度推理的模型时比“直接生成代码”更可靠的是先让它生成结构化描述再由流程去翻译和验证。5. 常见问题与避坑指南5.1 设计文档“过于自由”如何控制很多人拿到 SMART 思路后的第一个问题是让 LLM 生成设计文档会不会生成得五花八门、格式不统一论文里给的方案是用带注释的中间表示来约束结构。我自己用轻量版时也发现必须给 LLM 提供非常明确的 schema 和示例不能只给一句“请生成算子描述”。具体做法在 prompt 里给出完整模板示例包含 2~3 种不同算子的文档样例并要求 LLM 严格按字段输出。实测这样做之后文档的可用性大幅度提升。另一个技巧是要求生成的文档里包含“假设声明”字段比如“假设输入均已对齐”“假设不使用 L2 持久化缓存”这些声明会成为后续验证和排查的重要依据。5.2 验证反向过程最容易被忽略SMART 里提到的“反向模式”验证在我看到的很多讨论里都被一笔带过但这恰恰是防止 bug 的重要环节。简单说就是生成代码之后再做一次反向翻译将代码里的访存模式、循环结构提取出来和原始文档比对。我自己在实验中手动做过类似比对发现一个很有意思的现象LLM 在生成代码时往往会“优化”掉一些文档里明确存在的约束。比如文档里写了“矩阵 B 按列访问加载到共享内存时进行转置”生成代码时很可能直接按行访问来实现因为行访问更符合 CUDA 的习惯。这类问题完全无法靠“能否编译”来发现只能靠反向比对。如果你的场景允许建议至少对生成的代码做一次 AST 级别的语义抽取把关键循环结构和文档里的字段对齐检查。哪怕只是人工抽查也能大幅降低“隐性漂移”的概率。5.3 硬件迁移时文档模式比代码模式友好得多最后说一个我实际受益最大的场景硬件迁移。之前用传统代码方式做性能模型迁移时面对的是几百行 C/Python你得一行一行判断哪些是硬件相关的 magic number哪些是与算子逻辑强绑定的默认值。经常改错一个参数整个模型失效而且很难发现。SMART 模式下硬件相关参数全部集中在文档的 hardware_assumptions 字段里。迁移到新硬件时我又拿了一个实际算子做了验证只修改文档里的 SM 数量、带宽上限、时钟频率这几项重新编译生成代码预测误差比原有代码改参数的方式低了约 25%。这个结果很有意义迁移流程被简化为“更新参数文档”而不是“阅读并修改代码逻辑”。5.4 需要警惕的五个误区设计文档不是自然语言散文不要像写周报一样写设计文档必须是结构化字段否则验证环节无从下手。编译流水线不是可选的装饰很多人在实现 SMART 思路时会偷偷省略编译流水线直接让 LLM 把文档“翻译”成最终代码这样和直接生成代码没有本质区别。不要过度追求“全自动”SMART 在论文里并不是天然全自动的它依赖设计好的转换规则和验证规则。规则本身需要人来维护。文档也会过时或错误文档是核心产物不代表文档永远正确。当你的模型精度异常时别忘了可能是文档层出错了而不是生成层出错。评估指标要落在“建模精度”而非“生成成功率”生成成功指代码能跑太低了要关注模型的预测误差、可解释性、可修改性这三个维度。6. 写在最后文档驱动建模这件事值得每个 ML Infra 团队成员尝试我在把 SMART 思路实际落地到自己手头一个算子性能预估工具之后最大的体会是核心产物从“代码”换成“设计文档”改变的不仅仅是流程更是整个团队的认知方式。以前我们 review 一个性能模型看的是代码写得规不规范现在 review 的是设计文档里对算子行为、硬件假设、访存模式这些“建模决策”记录得是否准确。代码好不好变得次要因为代码随时可以被流程重新生成文档准不准才是决定模型质量的上限。如果你正在做 ML 性能建模、编译器优化、AUTOTUNING 或者任何和“预测计算系统行为”相关的工作我的建议是不用急着完全复刻 SMART 整套论文实现先做一个轻量版的文档驱动流程选一个你最熟悉的算子把它的建模任务拆成结构化字段再让 LLM 基于字段生成代码最后强制要求反查验证。跑几个来回之后你会对“哪些信息真的重要、哪些参数不该由生成过程自由发挥”有非常清晰的感觉。最后再分享一个小技巧在构建设计文档模板时尽量把“数据布局”和“执行策略”分成两个独立字段。这两个维度在实际建模中经常被混在一起但它们的错误修复代价差了一个数量级——数据布局错了往往要重写整个访存逻辑执行策略错了通常修改一个并行配置就能解决。分而治之会省掉你后面大量的 debug 时间。