资讯动态

Mojo 编译耗时分析指南:从 `--mlir-timing` 到可执行的编译时间分解

发布时间:2026/9/10 15:28:57 来源:尧图企业网站定制
Mojo 编译耗时分析指南从--mlir-timing到可执行的编译时间分解【免费下载链接】mojoThe Modular Platform (includes MAX Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo导读当一次 Mojo 编译变得缓慢时首先要回答的问题是编译器的时间到底花在了哪个环节本文基于 Modular Mojo 仓库中的编译耗时分析文档系统讲解如何用--mlir-timing/--llvm-timing定位耗时热点、如何对真实模型如 gemma-4-31B采集并解读计时报告、如何用消化脚本把 16 万行原始日志压缩成可行动的分解结论以及如何通过仓库内置的 Kepler 基准目标把编译耗时纳入每日 CI 追踪。读完本文你将能够独立完成采集报告 → 解读结构 → 定位热点 → 长期监控的完整编译性能分析流程。一、三个核心标志答案从哪来当编译变慢第一步是让编译器自己回答时间花在哪里。Mojo 编译驱动提供三个隐藏标志通过mojo build --help-hidden或mojo run --help-hidden查看完整帮助文本标志作用--mlir-timing对每一个 MLIR pass 和分析计时--llvm-timing对每一个 LLVM pass 和分析计时--mlir-timing-display MODEtree默认按 pipeline 结构嵌套list按 pass 名称聚合、按耗时排序这些标志的定义位于 Mojo/tools/mojo/Common/CompilationOptions.td。从 TableGen 定义可以看到几个关键细节--mlir-timingIndex18的 HelpText 明确指出报告在编译结束时打印到stderr对mojo run而言打印发生在程序启动之前编译缓存命中的 pass 永远不会运行因此热缓存只会报告 parse 阶段把MODULAR_CACHE_DIR指向空目录才能对完整 pipeline 计时。--llvm-timingIndex20的 HelpText 解释了它的单线程约束LLVM 的计时器是进程全局的、对多线程不安全因此该标志会把编译器线程数设为 1并覆盖--num-threads同时 LLVM 拿不到编译缓存中已存在的目标代码所以缓存命中时 LLVM 报告为空。--mlir-timing-display的list模式会把所有 pipeline 的 pass 合并因此无法单独展示某一个 offload 目标默认的tree模式更受推荐。读任何报告前必须知道的四件事两份报告都走 stderr编译结束时输出用2重定向捕获mojo run时在程序启动前打印。--llvm-timing强制单线程编译。报告度量的是总 CPU 工作量而不是并行构建的墙钟时间。热缓存产生空报告或部分报告。缓存服务的 pass 从不运行热缓存下 MLIR 报告只有 parse 等极少量内容目标代码被缓存时 LLVM 报告完全为空。把MODULAR_CACHE_DIR指向空目录即可对完整 pipeline 计时。list模式合并所有 pipeline无法单独呈现某个 offload 目标优先用默认的tree。二、报告包含什么三个部分的结构按顺序报告包含三个部分MLIR pass timing (--mlir-timing)— MLIR pass 的树状结构。LLVM pass timing (--llvm-timing): offload triple— 每个加速器目标各一份。LLVM pass timing (--llvm-timing): host triple— 主机目标一份。为加速器编译时编译器会再次对内核运行同一套编译流程因此加速器的工作会以两种不同形态同时出现在报告的两半中。加速器 scope 的双重呈现加速器的 MLIR pass 位于 MLIR 部分以一行 scope 行打开ElaborateGenerators下的子树119.7262 ( 45.1%) ElaborateGenerators 101.8517 ( 38.4%) --- offload nvptx64-nvidia-cuda sm_100a ... (also in host) 21.9535 ( 8.3%) ElaborateGenerators 18.3007 ( 6.9%) AutomaticInline ... 0.3545 ( 0.1%) SetFastMathFlags ... -59.0020 (-22.2%) Rest 265.5347 (100.0%) Total解读要点scope 行下方缩进一层的行是加速器自己的 MLIR pass——第二个ElaborateGenerators、第二个AutomaticInline等因为这是同一套 pipeline 在内核上再次运行。scope 行与ElaborateGenerators处于同一缩进层级但(also in host)标记说明其时间被计入该 pass 内部而非并列。子树之后恢复外层缩进的行此处为SetFastMathFlags又是主机侧的工作。MLIR 部分以两行收尾Total是计时根即总编译时间Rest是根内部没有任何 pass 认领的时间MLIR 用根减去子节点之和计算因此当子节点重复计数时会变为负数本例为-59.0020。详见下文 Rest 是主机代码生成 与 每个加速器 scope 被计数两次。加速器的 LLVM pass不在该子树中它们落在独立的offload tripleLLVM 部分——尽管这些时间同样花在了ElaborateGenerators内部。因此要汇总加速器 pipeline 的总耗时需要把MLIR 子树 对应 LLVM 部分相加。三个部分并非同级它们的总和不能相加MLIR 根跨越整个编译过程含代码生成。具体后果见下文 手工解读数字。三、实测一个模型两步拿到真实报告文档用 gemma-4-31B 演示完整流程分两步。第一步获取图编译器产出的 MojoMODULAR_DEBUGir-output-dir/tmp/gemma4/out-dir \ ./bazelw run //max/python/max/_entrypoints:pipelines -- warm-cache \ --model-path google/gemma-4-31B-it --target cuda:sm_100a这条命令会运行完整 pipeline因此没有匹配的 GPU 会失败——但没关系照跑即可产出的文件会在失败之前落盘。输出目录保存每个阶段的图 IR外加两个 Mojo 文件gemma4_visiongemma4_language.mojo— 约 3 MB待编译的目标文件。gemma4_vision_constant_subgraphs.mojo— 约 12 KB常量子图单独产出。第二步用两个计时标志编译它source ./utils/start-modular.sh # 每个 shell 执行一次把 mojo 加入 PATH MODULAR_CACHE_DIR$(mktemp -d) \ mojo build --emitobject --mlir-timing --llvm-timing \ --target-acceleratorsm_100a gemma4_visiongemma4_language.mojo \ -o /dev/null 2 log.txt--target-accelerator选择要生成代码的加速器架构无需 GPU 在场只需合法架构名如sm_100a或gfx950。-o /dev/null跳过目标文件写入——本次目的就是计时。MODULAR_CACHE_DIR$(mktemp -d)保证冷缓存得到完整 pipeline 报告。记录产生日志的是哪个构建版本debug 构建与 production 构建不可比见下文 基准测试注意事项。四、消化报告把 16 万行日志压成结论模型原始日志体量巨大gemma-4 约165,000 行大部分是每个加速器模块的每个 pass 一行。仓库的mojo-compile-timingskill 负责消化它/mojo-compile-timing log.txt --top 10也可以直接运行其脚本python3 .claude/skills/mojo-compile-timing/scripts/digest_timing_log.py log.txt有用选项选项作用--compare BASELINE同时提供第二个日志输出逐行加速比第一个参数是被测对象第二个是基线--top N每组点名多少个 pass余下卷进尾部默认 5--markdown树以围栏代码块输出并附汇总表格--json机器可读输出供仪表盘使用gemma-4 debug 编译的消化输出示例MLIR root whole compile 265.5s (100.0%) │ ├─ ElaborateGenerators 119.7s ( 45.1%) │ ├─ offload nvptx64 sm_100a scope (also in host) 101.9s ( 38.4%) │ │ ├─ MLIR passes 57.2s ( 21.5%) │ │ ├─ LLVM 23.0s ( 8.6%) │ │ └─ translation to LLVM IR, object emit 21.7s ( 8.2%) │ └─ elaboration itself 17.9s ( 6.7%) │ ├─ other host MLIR passes 102.2s ( 38.5%) │ └─ Rest host code generation 42.8s ( 16.1%) ├─ LLVM 34.5s ( 13.0%) └─ translation to LLVM IR, object emit 8.4s ( 3.1%) Rolled up by compiler half MLIR 177.3s ( 66.8%) LLVM 57.4s ( 21.6%) translation, object emit, untimed 30.1s ( 11.3%) Accelerator modules compiled: ~765读图规则每个百分比都是占整个编译的比例任意深度的行可直接互比子节点是其父节点的一部分、绝不额外叠加因此只有同一缩进层级的行相加才等于其父节点。这个模型的头条结论ElaborateGenerators看似热点占 45%但其 119.7s 中只有17.9s是真正的 elaboration其余101.9s是加速器代码的完整嵌套编译由 elaborator 通过CompileOffloadOp驱动相关实现可见 Mojo/lib/Compiler/KGENCompiler.cpp 中围绕 offload 分组的编译逻辑。三、手工解读数字四个陷阱消化脚本已处理全部四个问题了解它们有助于核对脚本输出或自行编写消费程序。MLIR 根就是整个编译MLIR 根跨越解析、pass 与代码生成——因为MLIRPassTiming对象存活于build()的整个生命周期见 Mojo/tools/mojo/Build/mojo-build.cpp。某次 gemma-4 编译中根读数为 272.34s而墙钟为 272.56s。总编译时间取自根绝不把各部分相加。每个加速器 scope 被计数两次它打印在最外层、与ElaborateGenerators并列但时间同时也在其内部——这正是(also in host)的含义。同一份日志上最外层各行求和得 323.80s而真实根是 265.53s。这个盈余正是上面示例中Rest为负的原因。MLIR 把Rest计算为根减去子节点之和重复计数的时间便以符号翻转的形式落入其中行显示-59.0020而真正未归属的时间是 42.85s。把 scope 加回去即可还原-59.00 101.85 42.85秒。消费报告的代码要么跳过名称以---开头的行要么把它们减掉。Rest是主机代码生成不是噪声它是 pass pipeline 之后未计时的尾巴。compileModuleToArchive先以计时 scope 运行runKGENPipeline然后把工作交给没有计时 scope的目标代码编译器。对 gemma-442.8s 拆分为 34.5s 主机 LLVM 与 8.4s 的 LLVM IR 翻译 目标文件发射。LLVM 分组互相重叠每个 LLVM 部分最多打印四组其中只有两组是互斥的分组关系Pass execution timing reportpass 本身Analysis execution timing report分析与 pass 分离Instruction Selection and SchedulingISel pass 内部的子计时器Register AllocationRA pass 内部的子计时器后两组来自SelectionDAGISel::CodeGenAndEmitDAG与贪心分配器内部的NamedRegionTimer对象因此其时间已被 pass 报告计入。某目标上 LLVM 时间 pass execution analyses另外两组仅作为其父 pass 的细分例如 AArch64 指令选择 8.9s 中3.3s 是DAG Combining after legalize types。排名加速器 pass 时还有一个细节报告按每 pass 每模块一行输出所以InstCombinePass会出现 765 次、每次约 0.30s。必须按名称聚合并剥离#N实例后缀否则列表顶部是十行相同记录。五、追踪基准测试把编译耗时纳入每日 CI以上步骤只能测量某台机器上的某次编译。要观察编译耗时在数周内的变化趋势CI 通过utils/benchmarking/kepler/mojo_compilation/每日运行同样的测量包含两个目标目标作用make_snapshot构建冻结输入并发布到 S3bench_mojo_compile_time编译该输入并报告分解输入必须冻结编译耗时只有在固定输入下才有意义。图编译器或内核库一变产出的 Mojo 就会变两者对编译耗时的影响可能不亚于 Mojo 编译器本身。快照把一切钉住序列中的任何台阶变化都可归因。快照以源码形式保存库而非预编译的.mojoc.mojoc只能在 MLIR dialect 校验和匹配的编译器中加载而该校验和哈希的是 dialect.td文本、几乎每天变化——预编译包快照一天内即失效。源码只要语言仍接受就能编译因此基准每次调用都会用被测编译器重建所有包。这次重建不是开销它编译整个标准库和全部内核库代码量远超单个产出的模型并在报告中记为precompile.*。生成快照./bazelw run //utils/benchmarking/kepler/mojo_compilation:make_snapshot -- \ --out ~/mojo-compile-snapshot --skip-upload--skip-upload只构建并归档快照而不发布——这正是本地运行想要的同时绕过节奏检查保证快照总是重建。该运行会以与第一步获取产出 Mojo相同的方式为model_inputs.yaml中的每个模型产出 Mojo复制库源码从bazel aquery读取预编译配方、重建包一次以确认冻结树可编译并统计每个源码的 offload 内核数。产出结构~/mojo-compile-snapshot/ ├── SNAPSHOT.yaml the recipe, plus a hash and size per file ├── library/ stdlib, kernel and max sources at their repo paths ├── sources/dir/ the emitted .mojo files └── mojo_compile_snapshot.tar.gz两件预期之事图编译器运行需要 HuggingFace 访问模型、且会在当前环境无法到达的目标上失败与第一步相同其输出保留在日志中、仅当源码缺失时才展示——那才是真正的失败而--skip-emit复用--out下已有的.mojo而非再次运行图编译器在迭代 emit 之后的所有环节时快得多。本地运行基准./bazelw run //utils/benchmarking/kepler/mojo_compilation:bench_mojo_compile_time -- \ --snapshot ~/mojo-compile-snapshot \ --runs 1 \ --results /tmp/compile-time.json选项含义--list打印 manifest 声明的基准名称后退出--snapshot编译已解包快照而非拉取已发布快照--runs每个源码的重复次数每次含两次完整编译默认 3--results把结果写入文件按扩展名识别.json/.yaml--benchmark只运行指定基准可重复指定前四项是本基准自有选项--benchmark来自共享的 Kepler runner命令行上其余未列出的选项同理。先看有哪些可测./bazelw run //utils/benchmarking/kepler/mojo_compilation:bench_mojo_compile_time \ -- --listgemma-4-31b.language sm_100a gemma4/gemma4_visiongemma4_language.mojo gemma-4-31b.constant-subgraphs sm_100a gemma4/gemma4_vision_constant_subgraphs.mojo再按--list打印的精确名称测量其中一个./bazelw run //utils/benchmarking/kepler/mojo_compilation:bench_mojo_compile_time -- \ --snapshot ~/mojo-compile-snapshot \ --runs 1 \ --benchmark gemma-4-31b.language库重建无论如何都会发生——它是被测对象的输入因此过滤只省下模型编译、省不了别的。每次重复编译源码两次一次不带计时标志、用编译器默认线程数记为wall.total一次带--mlir-timing --llvm-timing获取阶段分解。只有前者是延迟指标——原因见前文标志部分后者是单线程的。省略--snapshot则通过 HTTPS 拉取最近发布的快照——这正是 CI 的行为。成本按编译次数而非分钟预算分钟随机器而异一次调用 一次库重建 每个源码每次重复两次完整编译。重建每次调用只发生一次、不随重复次数增加所有源码共享同一批包且大体量源码主导一切。从--runs 1开始。添加一个模型基准追踪的每条序列都来自两个目标旁边的model_inputs.yaml。一个模型 一次提取一次图编译器运行提取产出的文件就是它的 sources每个 source 单独编译、单独绘图。添加步骤先用第一步获取产出 Mojo的命令手动提取新模型观察输出目录里落下什么。你需要产出的文件名而它们无法从模型名推断。添加条目- name: my-model target_accelerator: sm_100a emitted_by: model_path: org/My-Model target: cuda:sm_100a command: - MODULAR_DEBUGir-output-dirout-dir ./bazelw run //max/python/max/_entrypoints:pipelines -- warm-cache --model-path org/My-Model --target cuda:sm_100a sources: - name: language file: my-model/the_emitted_file.mojo重新生成快照。emit、offload 内核计数与源码统计全部由 manifest 驱动其余无需编辑。四个必须搞对的点file是快照内部的路径不是提取时写出的位置。只有 basename 需匹配某个产出文件前面的目录由你决定作用是隔开不同模型的源码。emitted_by在基准运行时不读取。它被记录下来以保证刷新可复现——图编译器产出什么取决于这些设置——所以command必须与上方字段保持一致。name与每个 source 的name组成序列名model.source是仪表盘上该序列的身份标识改名会开启新曲线而非延续旧曲线。模型名不得含点因为 BigQuery 视图按第一个点切分序列名。每个 source 在每次 CI 运行中都要付出每次重复两次完整编译的成本所以只加能回答问题的 source而不是提取产物里的每个文件。结果落入 BigQuery 与 Looker表与仪表盘细节见 Kepler 文档docs/internal/KeplerBenchmarking.md。五、基准测试注意事项用 production 构建做基准。它是实际发布版本、更快、运行成本更低更重要的是两种构建不可比在本文测量的 gemma-4 组合上production 运行 56 个最外层 passdebug 为 60 个LowerGlobalPOPToLLVM完全缺席VerifyParameters运行两次而非五次。逐 pass 加速比在 1.2x 到 8.2x 之间——debug 测量值无法缩放成 production 估算。由于计时标志强制单线程编译这些数字是总 CPU 工作量而非用户感知的延迟。追踪用户可见编译耗时需要另做一次测量默认线程数、不带计时标志。加速器模块数是输入属性不是编译器属性。若它变化说明产出的 Mojo 形状变了——只有在其保持稳定时跨快照比较编译耗时才有意义。仍有约 10% 的时间不属于任何 pass目前测得各次编译主机侧与加速器侧的 translation 与 object emit 行。落在那里的回归只会体现在总数上。附核心资源速查资源位置三个计时标志的 TableGen 定义Mojo/tools/mojo/Common/CompilationOptions.tdMLIRPassTiming配置与根 scopeMojo/tools/mojo/Build/mojo-build.cppoffload 目标分组编译实现Mojo/lib/Compiler/KGENCompiler.cpp原文档Mojo/docs/compiler/manual/CompileTimeProfiling.md注文档中提到的utils/start-modular.sh、utils/benchmarking/kepler/mojo_compilation/与docs/internal/KeplerBenchmarking.md属于上游仓库配套内容未包含在当前仓库快照中引用时以所在分支的实际情况为准。【免费下载链接】mojoThe Modular Platform (includes MAX Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价