Halide 调度入门:算法与调度分离的编程模型与三个性能杠杆
编译器图像处理编程语言高性能计算【免费下载链接】Halidea language for fast, portable>项目地址https://gitcode.com/gh_mirrors/ha/Halide点击查看免费下载Halide 是一门面向快速、可移植数据并行计算的语言它的核心设计是把每一个程序拆成**算法Algorithm与调度Schedule**两个独立编写的部分。本文基于仓库内调度指南的引言章节讲解这两个部分各是什么、为什么这种拆分如此重要、调度究竟影响哪些性能要素并给出本指南四大部分的完整导读帮助你在编写或优化 Halide 调度时快速建立正确的思维框架。一切程序都由两部分组成Halide 程序始终被拆成两个部分分别编写而保持二者分离正是这门语言工作机制的核心第一部分是算法Algorithm。算法只描述每个值是什么完全不涉及这个值怎么被算出来。程序声明Func并把它们定义为参数Var的纯函数还可以附加可选的**更新定义update definitions**来细化这些值。在这一阶段描述就是纯粹的数学对执行方式只字未提。第二部分是调度Schedule。调度描述每个值在何时、何地哪个循环层级被计算和存储。调度由附着在每个Func上的指令directive构成例如compute_root、compute_at、vectorize等。所有性能决策都集中在调度这一层。以仓库内调度技能的核心说明.claude/skills/scheduling/SKILL.md为证Halide 将每个程序拆为算法每个值是什么与调度每个值何时何地被计算和存储两部分这一表述与指南引言完全一致。调度 API 的实际声明位于 src/Func.h 与 src/Schedule.h而真正消费这些调度的流水线则集中在 src/ScheduleFunctions.cpp、src/Bounds.cpp 与 src/BoundsInference.cpp 中。算法部分的代码形态算法部分的典型写法如下取自指南第二章 The Programming ModelFunc hist(hist); hist(x) 0; // 第 0 阶段纯初始定义 RDom r(0, N, r); hist(in(r)) 1; // 第 1 阶段更新定义初始定义加上后续的更新定义构成一个有序的阶段stage列表s0纯定义、s1等。每个阶段完成后下一个阶段才开始它们写入同一块存储。阶段属于算法的一部分它们说明这个 Func 是什么因此理解它们不需要任何调度知识。但每个阶段仍会被独立调度通过f.update(n)指定见 Reshaping Loops。调度部分的代码形态调度则是对这些Func附加指令。一个典型调度同样来自指南见 Scheduling for CPUsoutput.split(y, yo, yi, /*strip*/ 32) .reorder(x, yi, c, yo) // 向量化轴在最内层 .vectorize(x, vec) .parallel(yo); producer_1.compute_at(output, yo).vectorize(x, vec); producer_2.compute_at(output, yo).vectorize(x, vec);可以注意到算法部分一字未动调度部分通过链式调用直接修改的是怎么算而非算什么。这种拆分为什么有用调度永远不能改变结果让算法/调度分离如此有价值的根本原因在于对给定的算法和输入无论怎么调度结果都是一样的——除了舍入或溢出这类细微的浮点效应。这为性能优化留下了巨大空间计算可以被大胆重排以换取速度而不用担心悄悄改变程序的计算结果。需要精确理解的是一样的含义整数运算在任意调度下结果逐位一致浮点运算可能因求值顺序不同产生舍入、溢出等微小差异除此之外调度的自由度是无损的——这也是为什么 Halide 敢于激进地重排、融合、并行化。调度真正影响的只有三件事既然调度不改变结果那它到底改变什么引言把调度的影响归结为三个杠杆值被计算的顺序order——循环的拆分、重排、融合决定了求值次序发生了多少冗余重算redundant recomputation——compute_root会整块预计算compute_at会把中间结果内联到消费者循环里反复重算store_at则可能让滑动窗口复用避免重算流水线需要多少临时存储temporary storage——中间结果何时分配、分配多大、能否通过折叠fold_storage变成循环缓冲。这三个杠杆几乎承载了全部的性能收益。一个漫不经心的调度与一个精心设计的调度之间性能差距可以是数量级的更极端的是一个选错的调度甚至可能无法编译——但它在语义上计算的仍是完全相同的东西。这三重杠杆的概念也在 directive-cheatsheet 中得到一一对应compute_root/compute_at管计算位置store_at/store_root/hoist_storage/fold_storage管存储位置split/fuse/tile/reorder/parallel/vectorize/unroll管循环形状与执行方式。调度到底控制什么对于每一个Func以及它的每一个阶段调度需要决定四类事情在哪里计算Where its computed内联到它的消费者里在消费者的某个循环层级计算还是在最顶层一次算完Halide 称之为 root。存储在哪里Where its storage lives存储可以位于比计算更粗的循环层级上——这正是滑动窗口能够复用一个中间值的机制。注意store_at与compute_at是相互独立的两个决策存储比计算粗粒度恰恰是滑动窗口复用的来源这也是 cheatsheet 里专门点出的陷阱之一。循环如何塑形How its loops are shaped循环可以被split拆分、tile分块、reorder重排还可以被标记为parallel并行、vectorize向量化或unroll展开。存储如何布局How its storage is laid out内存中的维度顺序可以通过reorder_storage改变从而匹配访问模式。后面的章节会逐一展开这些维度。就引言而言最需要记住的结论是调度是一组彼此独立的、按 Func 逐个做出的选择——计算位置、存储位置、循环形状、存储布局互不绑定可以单独调整。从源码看这四类决策被组织为Func上的不同方法族。在 src/Func.h 中可以看到它们成组出现计算位置相关如 273–327 行的compute_at/compute_root文档示例、存储相关store_at/store_root、循环塑形split/fuse/tile/reorder、执行方式parallel/vectorize/unroll以及 1091 行的print_loop_nest()声明——它打印调度产生的循环结构是验证调度是否做对了的第一工具。一个具体的验证手段print_loop_nest()调度是否真的如你所想不要靠猜直接打印循环嵌套。指南的惯例是所有示例都构建一个小int main()调用Func::print_loop_nest()观察调度产生的循环结构见 Reading a Loop Nest。这在教程中有完整演示例如 tutorial/lesson_05_scheduling_1.cpp 从第 57 行起对默认行优先调度打印伪代码循环随后逐步叠加reorder、split、vectorize、parallel、tile等指令并反复打印循环结构直观展示每个指令对循环嵌套的改变。指南还强调Part III 会忽略print_loop_nest()输出中的两类内容——具体的循环变量名和常量循环边界。前者不影响结构后者来自**边界推断bounds inference**而非调度结构本身详见 Bounds Inference。本指南覆盖范围四大部分导读调度指南指南首页 README把全部内容组织成四个部分引言对此做了总览Part I模型The Model——被调度的对象与流水线的实现这一部分打地基回答到底在调度什么、流水线如何被实现、缓冲区与循环的边界从哪来Introduction算法与调度的关系以及调度控制什么即本文The Programming ModelFunc、阶段stages、RDom、RVar、ImageParam、Expr等对象以及它们组成的流水线图pipeline graph——Func之间通过 producer 边连接成有向无环图被realize/print_loop_nest调用的那个Func是输出所有可达对象构成流水线其余一概不算Realizing a Pipeline计算何时真正运行realize()触发 JITAOT 则编译为对象文件加 C 头文件纯 AOT 部署可只链接halide-runtime而彻底去掉编译器与 LLVM 依赖实现流程依次是边界推断 → 临时内存分配 → 线程池激活 → 计算与存储 → 释放Bounds Inference由输出请求反向推导每个输入与中间Func的所需区域——以三抽头模糊blur(x) (input(x-1)input(x)input(x1))/3.0f为例请求blur在x ∈ [0,99]推断出input必须在[-1,100]有效外部缓冲不足时由BoundaryConditions包装repeat_edge、mirror_image等保证越界安全。Part II策略Strategy——给一条流水线什么样的调度值得写这一部分是通往跑得快调度的快车道回答该修哪里、怎么看出来Scheduling for CPUs几乎每个快速 CPU 流水线最终都是同一形状——一个外层并行循环 沿 stride-1 轴向量化 每个非平凡中间量都compute_at进并行循环内部。校验方法profiler 中流水线级parallel loops应为 1Scheduling for GPUs把循环映射到 GPU 的 block 与 threadgpu_tile/gpu_blocks/gpu_threads等What to Scheduleinline 与compute_at与compute_root如何取舍Benchmarking Profiling测量并读懂 profileHL_TARGEThost-profile输出按 Func 统计的 profiler 表Reading the .stmt File核对向量化形状Recipes滑动窗口、分块、金字塔、stencil 链、直方图等经典配方Pitfalls递推recurrence、并行放置、重算倍数等常见陷阱。Part III机制Mechanics——给定一个调度它实际产出什么样的循环嵌套这一部分逐条讲解每个指令的确切效果从读Func::print_loop_nest()输出开始Reading a Loop Nestprint_loop_nest的记法以及单个 Func 的循环Defaults and Inlining默认内联以及三个计算层级Placement: compute_root and compute_at一个 Func 在哪里被构建Storage Levelsstore_at、store_root、hoist_storage与滑动窗口Reshaping Loopssplit、fuse、reorder、tileLoop Types串行、并行、向量化、展开、GPU 五类循环Advanced Directivesrfactor、in/clone_in、compute_with、specializeHow the Loop Nest Is Built循环嵌套的完整组装步骤。当 Part II 提到某个指令时其精确效果在 Part III 中逐条可查。Part IV参考Reference——紧凑的速查Directive Reference全部指令的签名与效果Checklist and Worked Example起飞前检查清单 一个端到端完整示例。此外仓库还配套一份一屏速查表 directive-cheatsheet.md按计算与存储位置、循环形状、循环类型、包装与约简、尾部策略、诊断分组列出每个指令的签名与效果适合快速查阅。如何在当前仓库中继续深入引言确立了概念框架后你可以沿着下面的仓库路径继续验证与实操教程tutorial调度概念的可运行示例集中在 tutorial/lesson_05_scheduling_1.cpp向量化、并行、展开、分块、tutorial/lesson_08_scheduling_2.cppcompute_at与compute_root、tutorial/lesson_12_using_the_gpu.cppGPU 调度与 tutorial/lesson_18_parallel_associative_reductions.cpprfactor并行化约简。编译方式见各文件头部注释g lesson_05*.cpp -g -I path/to/include -L path/to/lib -lHalide -lpthread -ldl -o lesson_05 -stdc17之后设置LD_LIBRARY_PATH并运行调度 API指令声明位于 src/Func.h 与 src/Schedule.h消费调度的关键 pass 位于 src/ScheduleFunctions.cpp、src/Bounds.cpp 与 src/BoundsInference.cpp——对应引言边界从哪来与循环嵌套怎么拼出来两个问题自动调度器Autoschedulers如果不打算手写调度可以通过Pipeline::apply_autoscheduler让 Halide 代为提出调度实现在 src/autoschedulers/ 下的mullapudi2016、adams2019、li2018、anderson2021等Python 绑定指南示例是 C但调度 API 在 Python 中完全相同——同样的指令是halide.Func上的方法C 调度可以直接映射过来Python 教程镜像位于 python_bindings/halide/tutorial/诊断环境变量HL_DEBUG_CODEGEN1调大数字获得更多细节打印关键 lowering pass 之后的 IRHL_JIT_TARGET/HL_TARGET设置目标平台内置 profilerHL_TARGEThost-profile与.stmt文件分别回答时间花在哪与热点循环是否为单位步长向量化存储。小结引言所确立的核心心智模型可以浓缩为三句话算法与调度彻底分离——算法是纯数学描述调度是性能决策二者独立编写调度不改变结果——只可能带来舍入/溢出级别的浮点差异因此可以放心激进重排性能全部来自三个杠杆——计算顺序、冗余重算、临时存储全部通过一组按Func独立做出的选择计算位置、存储位置、循环形状、存储布局来操纵。带着这个框架后续 Part I 讲解被调度的对象与流水线实现Part II 教你快速写出跑得快的调度Part III 逐条精确解释每个指令对循环嵌套的改写Part IV 则提供随取随用的索引。在编写或修改任何调度之前先读对应章节并用print_loop_nest()验证结果而不是凭假设。赞分享编译器图像处理编程语言高性能计算【免费下载链接】Halidea language for fast, portable>项目地址https://gitcode.com/gh_mirrors/ha/Halide点击查看免费下载相关推荐Halide 调度Scheduling完全指南算法与调度分离、三大性能杠杆与全套指令精讲Halide 调度Scheduling完全指南算法与调度分离、三大性能杠杆与全套指令精讲 导读 Halide 将每个程序拆分为 算法algorithm编译器图像处理编程语言高性能计算3D-Speaker开发者进阶指南自定义模型架构与训练流程开发3D Speaker开发者进阶指南自定义模型架构与训练流程开发 3D Speaker是一个专注于单模态和多模态说话人验证、识别及说话人分割的开源项目为开发者人工智能语音深度学习AI 编译器算子计算与调度从 Halide 调度树到 TVM 自动调优的深度解析AI 编译器算子计算与调度从 Halide 调度树到 TVM 自动调优的深度解析 导读 在 AI 编译器的后端优化中算子的计算定义与调度实现是两个文档教程人工智能上一篇KubeEdge日志系统详解边缘节点日志收集与云端聚合方案下一篇The Unzipper终极网页版压缩文件处理神器完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考