:Storage Levels —— 用 `store_at` / `store_root` 与滑动窗口优化内存与带宽)
编译器图像处理编程语言高性能计算【免费下载链接】Halidea language for fast, portable>项目地址https://gitcode.com/gh_mirrors/ha/Halide点击查看免费下载导读本篇技术指南围绕 Halide 调度体系中最关键也最容易被忽视的一环展开存储层级Storage Levels。在 Halide 中在哪里计算compute level与在哪里分配缓冲区store level本是两件事而store_at、store_root正是把二者拆开的核心指令。读完本文你将掌握store_at/store_root/hoist_storage/store_in四个指令的语义与用法理解 Halide 滑动窗口sliding window优化背后的内存复用机制并能结合print_loop_nest()输出的store节点判断调度是否生效。本文是 Halide 调度指南 系列中 第 15 章 Storage Levels 的深度展开相关实操模式可配合 第 10 章 Recipes 一起阅读。一、从 compute level 到 store level把计算位置与存储位置解耦在 Halide 中一个 Func 的调度层级placement同时决定了两件事在哪里计算它compute level以及在哪里分配它的存储store level即缓冲区分配的循环层级。默认情况下store level 与 compute level 是重合的——例如f.compute_at(g, var)会在g的var循环内既计算f又分配f的存储。但这两件事本质上是可以分离的。f.store_at(g, v)把f的存储分配放在g的v循环处而f.store_root()把存储放到最外层root。这两种指令只改变存储分配的位置不移动任何计算这正是 Halide 滑动窗口优化的基础。从源码看这些指令在 src/Func.h 中均有对应的重载与文档Func store_at(const Func f, const Var var); // 在 f 的 var 循环内分配存储 Func store_at(const Func f, const RVar var); // 支持以 RDom 维度为站点 Func store_at(LoopLevel loop_level); // 以 LoopLevel 形式指定 Func store_root(); // 分配到最外层循环注意store_at的调用形式与compute_at完全对称第一个参数是宿主 Func第二个参数是宿主 Func 的某个Var或RVar。你也可以用LoopLevel形式精确指定站点。二、为什么要拆分滑动窗口的内存折叠拆分 compute/store 的动机非常朴素在内层循环计算、在外层循环存储可以让内层某次迭代算出的值在后续迭代中被复用。这就是 Halide 的滑动窗口优化——它让缓冲区从整块完整分配折叠成小而滚动的形态。这带来两个直接效果减少了重复计算相邻迭代共享的中间结果只计算一次大幅缩小了缓冲区尺寸以行扫描为例原本要保存整幅图像的中间数据现在只需保存少数几行。需要强调的是store_at改变的是哪些值被重算、缓冲区有多大它并不改变 produce/consume/for的整体结构。从print_loop_nest()的角度看它唯一的可见效果是多出一个store节点。三、读懂print_loop_nest()中的store节点print_loop_nest()是验证调度是否生效的首要工具详见 第 12 章 Reading a Loop Nest。关于store节点的规则仅当f的 store level 与 compute level 不一致时才会打印store f:当二者一致默认情况也包括store_root().compute_root()的组合时没有store行一旦出现store f:位于 store level其下包裹着从该层到f的produce/consumecompute level之间的所有内容produce/consume以及每一个for循环的位置与单独使用compute_at时完全一致store_at只是在其外层额外加上store g:这一层。来看原文档中的经典示例g在output的内层循环x处计算但存储分配在output的外层循环y处Var x(x), y(y); ImageParam in(type_ofuint8_t(), 2, in); Func g(g); g(x, y) in(x, y); Func output(output); output(x, y) g(x, y) g(x 1, y) g(x, y 1) g(x 1, y 1); g.compute_at(output, x).store_at(output, y); output.print_loop_nest();打印结果produce output: for y: store g: # 位于 store leveloutput 的 y 循环 for x: # store level 与 compute level 之间的循环 produce g: # 位于 compute leveloutput 的 x 循环 for y: for x: g(...) ... consume g: output(...) ...几个细节值得注意store g:出现在for y:内部、for x:外部表示缓冲区在整个y循环内只分配一次store节点与produce/consume一样按 site-func 的每个 stage 分别跟随只出现在实际计算f的 stage 中store_root()则把节点放在最外层包裹整个 pipeline 主体。四、滑动窗口实战可分离模糊separable blurstore/compute 拆分是 stencil 滑动窗口的引擎。以可分离模糊为例把生产者producer存储在 strip 层每次迭代只计算一条扫描线producer.store_at(out, yo) // 每个 strip 只分配一次存储 .compute_at(out, yi); // 每次迭代只计算一条扫描线Halide 会为每个 strip 维护一个小型滚动缓冲区rolling buffer of scanlines。每个新的yi迭代只计算新需要的行旧行在缓冲区中滚动复用。若想强制使用固定大小的环形缓冲区追加.fold_storage(y, K)其中K为槽位数量——对应 src/Func.h 中的声明Func fold_storage(const Var dim, const Expr extent, bool fold_forward true);完整的滑动窗口 分块 并行 向量化配方见 第 10 章 Recipesconst int vec natural_vector_sizefloat(); Var yo, yi; out.split(y, yo, yi, 32).parallel(yo).vectorize(x, vec); producer.store_at(out, yo) // 在 strip 层分配 .compute_at(out, yi) // 每次计算一条扫描线 .vectorize(x, vec);滑动窗口的实测证据仓库中的 test/correctness/sliding_window.cpp 对滑动窗口做了大量行为验证。例如最基础的一维滑动f(x) call_counter(x, 0)g(x) f(x) f(x - 1)调度为f.store_root().compute_at(g, x)对 100 个像素 realize 后断言f只被调用101 次而不是每个输出点都重算见该文件第 26-44 行f(x) call_counter(x, 0); g(x) f(x) f(x - 1); f.store_root().compute_at(g, x).store_in(store_in); // ... Bufferint im g.realize({100}); // f should be able to tell that it only needs to compute each value once if (count ! 101) { ... }该测试还覆盖了多生产者共享消费者、两级滑动窗口级联、含归约reduction的滑动、多维滑动、矢量化的滑动窗口、固定 footprint 时只在首次迭代计算全部值count 6、每三次迭代才需要新值count 34等场景——它们共同证明了 store/compute 拆分确实只重算必要的新值。一个易错点同一 var 上的store_at(f, v).compute_at(f, v)等于什么都没做store_at(f, v).compute_at(f, v)站点 var 相同与单独compute_at(f, v)完全等价。store_at只有在严格外于compute_at的循环时才有意义。五、hoist_storage只调整分配位置不开启滑动窗口第三个层级是hoist-storage level提升存储层级由f.hoist_storage(g, v)或f.hoist_storage_root()设置。它与store_at的本质区别见 src/Func.h 的注释hoist_storage simply moves an actual allocation to a given loop level and doesnt trigger any of the optimizations such as sliding window.即store_at把存储分配外移并开启滑动窗口式的值复用hoist_storage只把实际的内存分配动作alloc/free进一步外移以跳过在循环内部反复重新分配不开启滑动窗口复用默认情况下 hoist-storage level 与 store level 重合。打印输出中不可见hoist_storage在print_loop_nest()的输出中是不可见的——带hoist_storage的调度打印出的 loop nest 与不带它的完全一样。它只影响分配位置与缓冲区尺寸。典型用法想获得细粒度compute_at的新鲜感局部性又不想每轮迭代都付出一次 alloc/free 的开销f.compute_at(g, inner).hoist_storage(g, parallel_var);并行循环的红色警戒线只能把存储提升到包含并行循环的那层循环绝不能越过它。如果g是parallel(yo)则分配必须留在yo内部保证每个线程拥有自己的缓冲区一旦把分配提升到并行循环之上缓冲区就变成了所有线程的共享状态产生数据竞争race。因此hoist_storage_root()只有在没有任何外层循环是 parallel时才是安全的从源码角度看这一约束体现在 test/correctness/hoist_storage.cpp 的大量子场景中。该测试通过自定义custom_malloc/custom_free精确断言 malloc 次数与总大小例如f.compute_at(g, x).hoist_storage(g, Var::outermost())对 128×128 输出仅分配1 次、大小为3*3*sizeof(int32_t)第 28-70 行证明分配被提升到了最外层而把hoist_storage设在yo分块外层时malloc 次数变为8第 213-257 行——即每个 tile 一行各分配一次lifting after sliding window场景第 466-511 行证明store_at与hoist_storage可以叠加使用分配大小被折叠为4*3*sizeof(int32_t)。这些断言清晰地展示了 hoist 层级对分配次数、分配大小的量化影响。六、合法性规则Legalitystore_at、hoist_storage相关调度有明确的合法性约束违反会在 realize/compile 时抛错store level 必须包含enclosecompute level二者可以是同一循环或 store 在外层把存储放在 compute 循环内部是非法调度。设置了 store level 的 Func 必须有一个非内联的 compute level对内联inlineFunc 调用store_at、store_root或hoist_storage是非法的必须先给它显式的compute_at或compute_root。仓库中的 test/correctness/store_at_without_compute_at.cpp 正是这条规则的回归测试其期望的错误信息为Func g is scheduled store_at(), but is inlined. Funcs that use store_at must also call compute_at.见该文件第 8-19 行的error_test用法。hoist-storage level 必须包含 store levelstore level 必须包含 compute level把存储提升到 compute 循环内部的循环是非法的。store level与 compute level 一样必须包含f的所有使用点use这与 第 14 章 中 compute level 的所选层级必须包含对f的每一次读取这一合法性总原则一致。七、存储位置store_in与 MemoryTypef.store_in(MemoryType::Stack)把存储分配在栈上src/Func.h。它的特点速度快但只适用于小尺寸的定长分配这是**存储类型memory type**的选择与 store level 相互独立它不会改变print_loop_nest()的输出。仓库测试中常见的取值还有MemoryType::Heap堆与MemoryType::Register寄存器例如 sliding_window.cpp 对两种类型分别验证滑动窗口行为在寄存器滑动场景下两级滑动级联的期望调用次数为 103 而非 102见该文件第 78-83 行。MemoryType::Auto则由编译器自动决策。完整的调度指令速查表见 第 20 章 Directive Reference。八、小结三层存储语义速查指令作用影响 loop nest 输出是否开启滑动窗口f.store_at(g, v)把f的存储分配放到g的v循环增加store f:节点是f.store_root()把存储分配到最外层增加最外层的store f:是f.hoist_storage(g, v)/f.hoist_storage_root()只外移实际 alloc/free不改计算不可见否f.fold_storage(var, K)固定 K 槽的环形缓冲区不改变结构配合使用f.store_in(MemoryType::Stack)栈上分配小定长不可见否一句话总结compute level 决定算多少、何时算store level 决定存多久、存多大hoist-storage level 决定在哪 alloc/free。把三者分开调度正是 Halide 在保持局部性的同时压缩内存带宽占用的核心手段。下一步可以继续阅读 第 16 章 Reshaping Loops 与 第 17 章 Loop Types把存储层级与循环形状、循环类型parallel/vectorized组合出完整的实战调度。赞分享编译器图像处理编程语言高性能计算【免费下载链接】Halidea language for fast, portable>项目地址https://gitcode.com/gh_mirrors/ha/Halide点击查看免费下载相关推荐pydictor工具集详解合并、去重、比较、统计与筛选工具pydictor工具集详解合并、去重、比较、统计与筛选工具 pydictor是一款强大的黑客字典生成工具专为暴力破解攻击设计。本文将详细介绍其工具集中的合并isle-portable性能调优内存带宽与缓存优化技巧isle portable性能调优内存带宽与缓存优化技巧 在LEGO Island 1997 的现代化项目isle portable中内存带宽和缓存优化是提突破同步瓶颈Syncthing内存与带宽深度优化指南突破同步瓶颈Syncthing内存与带宽深度优化指南 引言同步困境与性能瓶颈 你是否遇到过Syncthing同步大型文件夹时内存占用飙升至GB级别是否在家网络通信存储上一篇如何快速上手 PilotDeck从白皮书生成到小游戏开发的4个实战案例下一篇Lorien 版本演进深度解析从 v0.1.0 到 v0.7.0 的无限画布应用迭代全记录创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考