- 深度学习
- 机器学习
- 人工智能
【免费下载链接】mxnet
Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more
本文围绕 contrib/clojure-package/examples/profiler 这一官方示例,讲解如何用 MXNet Clojure 包的 Profiler 对矩阵乘法(dot)算子的执行过程进行剖析:从lein run一键运行、生成profile-matmul-20iter.json剖析文件,到逐步拆解配置参数、剖析状态控制逻辑,并深入 Clojure → Scala → C/C++ 的完整调用链,帮助你掌握在 Clojure 应用中定位算子性能瓶颈的完整方法。
示例概览:一个最小可复现的算子级剖析示例
profiler示例是整个 Clojure 包中专门演示性能剖析(profiling)的工程,它的核心目的非常聚焦:通过 Profiler 记录一次矩阵乘法算子在前向计算中的执行耗时,并输出为标准 JSON 格式的剖析文件。该示例的目录结构如下:
contrib/clojure-package/examples/profiler/ ├── README.md # 使用说明:lein run 运行,生成 profile-matmul-20iter.json ├── project.clj # Leiningen 工程定义与依赖 ├── src/profiler/core.clj # 示例主逻辑:符号构建、数据准备、剖析控制 └── test/core_test.clj # 测试:运行示例并验证剖析文件是否生成README.md 给出的使用方式极为简洁,全部流程只有两步:
- 在示例目录下执行
lein run; - 运行结束后,当前目录下会生成剖析结果文件
profile-matmul-20iter.json。
尽管说明只有两句话,但其背后包含了配置剖析模式、控制剖析起止时机、构造执行图、运行迭代等一系列可复用的代码模式,下文将逐一拆解。
运行示例:依赖、命令与产物
工程依赖
project.clj 定义了该示例的工程信息:
(defproject profiler "0.1.0-SNAPSHOT" :plugins [[lein-cljfmt "0.5.7"]] :dependencies [[org.clojure/clojure "1.9.0"] [org.apache.mxnet.contrib.clojure/clojure-mxnet "1.6.0-SNAPSHOT"]] :main profiler.core)要点包括:
- 依赖
org.apache.mxnet.contrib.clojure/clojure-mxnet(本仓库中的 Clojure 包,源码位于 contrib/clojure-package/src),这是访问 MXNet 核心能力(NDArray、Symbol、Executor、Profiler 等)的入口; :main profiler.core声明了主命名空间,因此lein run会调用profiler.core/-main执行剖析;lein-cljfmt仅用于代码格式检查,与剖析逻辑无关。
运行与产物
在 contrib/clojure-package/examples/profiler 目录下执行:
lein run运行结束后,示例会在output-path(默认为当前目录.)下生成名为profile-matmul-20iter.json的剖析文件。该文件由 MXNet Profiler 在程序退出时自动 dump 输出,记录了剖析期间算子/API 调用的时间线数据。
需要说明的是:文件名中的20iter暗示了迭代规模,而当前 core.clj 中iter-num的取值为5,二者存在出入,可以推断这是示例演进过程中留下的命名差异——文件名仅代表输出标识,实际剖析的迭代次数以代码中的iter-num为准(下文会解释其影响)。
逐段解析 core.clj:剖析示例的主逻辑
示例的全部剖析逻辑集中在 src/profiler/core.clj 的run函数中。先看它定义的全局配置常量(L27-L33):
(def profiler-mode "symbolic") ;; can be symbolic, imperative, api, mem (def output-path ".") ;; the profile file output directory (def profiler-name "profile-matmul-20iter.json") (def iter-num 5) (def begin-profiling-iter 0) (def end-profiling-iter 1) (def gpu? false)每个常量的含义与作用如下:
| 常量 | 默认值 | 作用 |
|---|---|---|
profiler-mode | "symbolic" | 剖析模式,注释标明可选symbolic、imperative、api、mem,对应不同剖析维度 |
output-path | "." | 剖析输出文件所在目录 |
profiler-name | "profile-matmul-20iter.json" | 剖析结果文件名 |
iter-num | 5 | 前向计算的迭代总次数 |
begin-profiling-iter | 0 | 从第几次迭代开始记录剖析数据 |
end-profiling-iter | 1 | 到第几次迭代停止记录剖析数据 |
gpu? | false | 是否使用 GPU 上下文(默认为 CPU) |
计算图与数据准备
run函数(L35-L56)的第一步是构建一个 4096×4096 的矩阵乘法执行计划:
(defn run [] (let [shape [4096 4096] path (str output-path "/" profiler-name) ctx (if gpu? (context/gpu) (context/cpu)) kwargs {:filename path (keyword (str "profile-" profiler-mode)) 1} C (sym/dot "dot" [(sym/variable "A") (sym/variable "B")]) a (random/uniform -1.0 1.0 shape {:ctx ctx}) b (random/uniform -1.0 1.0 shape {:ctx ctx}) exec (sym/bind C ctx {"A" [a] "B" [b]})] ...))sym/dot创建名为dot的矩阵乘法符号,输入是两个占位变量A、B;random/uniform在[-1.0, 1.0]区间内随机生成两个 4096×4096 的 NDArray 作为输入数据;sym/bind将符号C绑定到具体数据上,得到可执行的Executor(对应底层MXExecutorBind)。
剖析配置:动态构造参数键
kwargs的构造是本示例最精巧的部分:
kwargs {:filename path (keyword (str "profile-" profiler-mode)) 1}它把剖析模式拼成关键字键名:当profiler-mode为"symbolic"时,生成的 map 是{:filename "..." :profile-symbolic 1}。这个 map 随后被传给 profiler.clj 中的profiler-set-config:
(defn profiler-set-config " Set up the configure of profiler. -mode, optional Indicting whether to enable the profiler, can be symbolic or all. Default is symbolic. -fileName, optional The name of output trace file. Default is profile.json." [kwargs] (Profiler/profilerSetConfig (util/convert-io-map kwargs)))这里调用了 util.clj 的convert-io-map,其内部通过convert-map与keyword->snake-case把 Clojure 关键字键转换为 C 层约定的小写下划线键名——例如:profile-symbolic→"profile_symbolic"。这正是 Clojure 侧:profile-symbolic能正确映射到 C APIMXSetProfilerConfig中profile_symbolic参数的原因。
迭代循环与剖析起止控制
核心剖析控制逻辑在doseq循环中:
(profiler/profiler-set-config kwargs) (doseq [i (range iter-num)] (when (= i begin-profiling-iter) (profiler/profiler-set-state "run")) (when (= i end-profiling-iter) (profiler/profiler-set-state "stop")) (-> exec (executor/forward) (executor/outputs) (first) (ndarray/wait-to-read)))执行顺序可以理解为:
- 先调用
profiler-set-config完成配置(指定输出文件与剖析模式); - 进入
iter-num(5)次迭代,每次迭代执行一次executor/forward前向计算,并通过ndarray/wait-to-read同步等待结果就绪,确保计算真正完成; - 当
i = begin-profiling-iter(0)时调用profiler-set-state "run"启动剖析; - 当
i = end-profiling-iter(1)时调用profiler-set-state "stop"停止剖析。
由于begin-profiling-iter与end-profiling-iter分别取值 0 和 1,实际只有第 0 次迭代的前向计算被完整记录在剖析文件中——这是刻意为之:在正式记录前先跑若干轮"预热"(warm-up)迭代,可以避免将上下文初始化等一次性开销计入性能数据。你可以通过调整这两个常量改变预热轮数与采样窗口。
剖析模式与配置参数详解
四种剖析模式
core.clj 注释中列出的symbolic、imperative、api、mem四种模式,与底层 src/profiler/profiler.h 中定义的ProfilerMode位标志一一对应:
enum ProfilerMode { kSymbolic = 1, // 符号(声明式)算子执行 kImperative = 2, // 命令式(Gluon 风格)算子执行 kAPI = 4, // C API 调用本身 kMemory = 8 // 内存分配/释放 };由于这是位标志,理论上可组合开启多种模式;本示例使用默认的"symbolic"模式,即只记录符号图算子(dot)的执行耗时。在 profiler.h 中可以看到 Profiler 内部默认的mode_初始化为kSymbolic | kAPI | kMemory,说明即使不显式配置,符号算子、C API 调用与内存行为也是默认重点关注的维度。
完整配置参数表
Clojure 的profiler-set-config最终会进入 C API src/c_api/c_api_profile.cc 中的ProfileConfigParam解析。该结构用DMLC_DECLARE_PARAMETER声明了全部可配置字段,供你按需传入:
| 参数键 | 默认值 | 说明 |
|---|---|---|
profile_all | false | 是否开启全部剖析维度 |
profile_symbolic | true | 剖析符号(声明式)算子 |
profile_imperative | true | 剖析命令式算子 |
profile_memory | true | 剖析内存行为 |
profile_api | true | 剖析 C API 调用 |
filename | "profile.json" | 剖析结果输出文件名 |
continuous_dump | true | 运行期间是否周期性 dump(追加)剖析数据 |
dump_period | 1.0f | 开启持续 dump 时,两次 dump 之间的间隔(秒) |
aggregate_stats | false | 是否维护聚合统计(MXDumpAggregateStats需要,会带来性能开销) |
profile_process | "worker" | 剖析哪个进程:单机训练恒为worker;分布式训练可设为server |
在 Clojure 侧构造配置时,只需把这些键写成关键字(如:filename、:profile-symbolic)放入 map 传给profiler-set-config即可,键名会自动转换为snake_case。
输出文件的持续写入与主动 dump
c_api_profile.cc 提供了MXDumpProfile接口,其 Clojure 封装是 profiler.clj 的dump-profile:
(defn dump-profile " Dump profile and stop profiler. Use this to save profile in advance in case your program cannot exit normally." ([finished] (Profiler/dumpProfile (int finished))) ([] (dump-profile 1)))dump-profile用于在程序可能无法正常退出(如崩溃、超时)时提前保存剖析结果,finished参数为 1 表示 dump 后停止剖析并结束。正常路径下,程序退出时 Profiler 会自动完成 dump,因此示例主逻辑中并未显式调用它。
剖析状态机:run 与 stop 的底层语义
profiler-set-state是控制剖析开关的关键函数(profiler.clj):
(defn profiler-set-state "Set up the profiler state to record operator. -state, optional - Indicting whether to run the profiler, can be stop or run. Default is stop." ([state] (Profiler/profilerSetState state)) ([] (profiler-set-state "stop")))它最终调用 C APIMXSetProfilerState,对应 profiler.h 中定义的二元状态:
enum ProfilerState { kNotRunning = 0, // 对应 "stop" kRunning = 1 // 对应 "run" };在 c_api_profile.cc 的MXSetProfilerState实现中可以看到,状态切换除了设置 Profiler 内部状态外,还会同步调用vtune_pause/vtune_resume,以兼容 Intel VTune 等外部剖析工具的启停。
结合Profiler::IsProfiling(profiler.h)的实现可以推断:只有当状态为kRunning且当前模式位被开启时,算子的计时数据才会被记录。因此示例中"先 run、后 stop"的写法,本质上是在迭代循环内精确划定了一段记录窗口——窗口外的迭代(如预热迭代与收尾迭代)不会污染剖析数据。
从 Clojure 到 C++ 的完整调用链
将整个示例串起来,一次剖析请求的完整调用链如下:
profiler.core/run (core.clj) └─ profiler/profiler-set-config {..} └─ util/convert-io-map → 键名 snake_case 化 └─ Profiler/profilerSetConfig (Java) └─ MXSetProfilerConfig (c_api_profile.cc) └─ ProfileConfigParam::Init 参数解析 └─ Profiler::Get()->SetConfig(...) └─ profiler/profiler-set-state "run"/"stop" └─ Profiler/profilerSetState (Java) └─ MXSetProfilerState (c_api_profile.cc) └─ Profiler::Get()->SetState(...)在 C API 层,api模式的计时由 c_api_profile.cc 中的on_enter_api/on_exit_api完成:每个线程维护一个ProfilingThreadData(内含可嵌套的APICallTimingData调用栈),在进入 C API 时创建/复用ProfileTask并start(),退出时stop()并弹出栈顶,从而得到每次 C API 调用的耗时;同文件中的IgnoreProfileCallScope(RAII)则用于排除剖析自身调用造成的干扰。
对以性能调优为目标的读者而言,理解这条链路的价值在于:你可以据此判断一个"性能问题"究竟出在算子本身(symbolic/imperative模式)、C API 包装层(api模式)还是内存分配(mem模式),从而避免盲目优化。
测试验证:剖析文件是否真的生成
示例工程自带了测试 test/core_test.clj,用于验证剖析流程确实产出了文件:
(defn count-lines[file] (count (line-seq (io/reader (io/as-file file))))) (deftest run-profiler (profiler/run) (let [new-file (clojure.java.io/as-file profiler/profiler-name)] (is (.exists new-file))))测试直接调用profiler/run完整跑一遍示例逻辑,然后断言profile-matmul-20iter.json文件存在。这既验证了示例代码的可用性,也给出了一个可复用的模式:任何基于 Profiler 的剖析流程,都应把"输出文件生成"作为最基本的正确性判据。运行该测试的方式与标准 Leiningen 工程一致:
lein test扩展:把剖析示例改造成自己的基准
基于本示例,你可以通过少量改动将其用于自己的模型或算子:
- 切换剖析维度:将
profiler-mode改为"imperative"、"api"或"mem",观察不同层面的耗时分布;也可以按位组合(例如同时关注符号算子和 C API); - 调整采样窗口:增大
iter-num并合理设置begin-profiling-iter/end-profiling-iter,让预热迭代更充分、记录窗口覆盖更多稳定迭代; - 切换计算设备:将
gpu?设为true(需 GPU 版 MXNet 环境),对比 CPU/GPU 上的算子耗时; - 更换算子:把
sym/dot替换为卷积、全连接等其他符号,构造针对性的算子基准; - 改输出位置:修改
output-path将剖析文件输出到指定目录; - 异常退出保护:若程序运行路径存在提前退出风险,可在关键节点调用
profiler/dump-profile主动保存数据。
剖析完成后,profile-matmul-20iter.json中的时间线数据即可用于分析算子耗时、定位热点,为后续优化(如算子融合、内存复用、设备选择)提供量化依据。
小结
本示例虽然只有寥寥数行的 README,却浓缩了 MXNet Clojure Profiler 的核心用法:profiler-set-config配置模式与输出、profiler-set-state划定记录窗口、迭代循环驱动前向计算、退出时自动 dump JSON 剖析文件。配合 profiler.clj、c_api_profile.cc 与 profiler.h 的源码,你可以把这条链路复用到任意 Clojure 深度学习代码中,用数据驱动的方式完成算子级性能调优。
- 深度学习
- 机器学习
- 人工智能
【免费下载链接】mxnet
Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more
相关推荐
Bevy 压力测试(Stress Tests)示例运行与基准剖析:以 stress-test profile 还原真实性能
Bevy 压力测试(Stress Tests)示例运行与基准剖析:以 stress test profile 还原真实性能 本指南围绕 Bevy 官方仓库中的
游戏开发图形学TreeSheets终极方案:用分层电子表格解决复杂数据组织难题
TreeSheets终极方案:用分层电子表格解决复杂数据组织难题 面对信息爆炸时代,你是否常常陷入这样的困境:传统电子表格难以处理多维度数据,思维导图缺乏结构化
桌面应用MXNet Clojure 目标检测实战:用 clojure-mxnet infer 包运行 ResNet50-SSD 图像检测
MXNet Clojure 目标检测实战:用 clojure mxnet infer 包运行 ResNet50 SSD 图像检测 本篇技术指南以 MXNet 仓
深度学习机器学习人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考