CHAPTER 32 / 架构、性能与并行

Reduction、scan 与数值正确性

求一个总和与求每个前缀为什么是不同算法,浮点结果又为何可能随顺序变化?

阅读与推演约 60 分钟练习时间另计

这一章要弄清楚

  • 区分 reduction 与 inclusive/exclusive scan
  • 处理非二次幂长度及单位元
  • 建立拒绝 NaN 的数值比较合同

先备知识:并行分解、分块与负载均衡

C++20 / macOS 与 Linux;本章的硬件模型只推演逻辑,不代表设备性能。

两个看似相近的需求

输入为 3、1、4、1、5。如果只要全部和 14,这是 reduction:把很多输入归并成一个输出。如果每个位置都要知道它前面的累计结果,就是 scan。Inclusive scan 包含当前位置,输出 3、4、8、9、14;exclusive scan 不包含当前位置,使用加法单位元 0 开头,输出 0、3、4、8、9。两者都可以成为构建更复杂并行算法的基础,但输出语义不同,不能交换名字。

partial_sum把inclusive结果写到哪里?(补充,另估15分钟)

<numeric>中的std::partial_sum(first,last,result)读取输入半开范围,把每一步前缀和写到从result开始的目标范围;目标至少有与输入一样多的可写元素。28-a的std::vector<long long> scan(input.size());已经建立这些元素,再传scan.begin。只reserve而size仍为0不满足写入前提。partial_sum合同

给输入[3,1,4,1,5],先以首项3建立累加值并写目标位置0;再加1写4、加4写8、加1写9、加5写14,得到[3,4,8,9,14]。返回的是目标尾后位置,不是总和14;本例无需使用这个返回值。空输入不读取首项也不写输出,返回原result;单项[5]只写[5]。这些都是给定小整数的过程推演。

这个三参数接口没有独立的初值参数,不要把accumulate里的0LL塞进第三个位置;第三项是输出位置。内部累加值的类型来自输入元素类型,本例为long long,不能只把输出改宽就声称计算过程中也已变宽。当前小输入的全部前缀和可表示;宽类型和算法名字都不自动保证任意输入无溢出。Exclusive输出另按本章定义构造,不能给这个inclusive接口换个标签。

稳定筛选是 scan 的一个直观用途:把每个元素是否保留写成 0/1 标志,对标志做 exclusive scan,得到每个保留元素的输出位置。例如标志 1、0、1、1 的 exclusive scan 为 0、1、1、2,被保留的三个元素分别写到位置 0、1、2,保持原顺序。计数、位置和真实数据移动应分开验证。

树形归并怎样缩短依赖链

串行加和形成长度 n 的依赖链。树形 reduction 先两两相加,再把局部和两两相加,约经过 log2(n) 轮;总工作量仍是线性。对五个元素,第一轮得到 4、5、5,第二轮 9、5,最后 14。没有搭档的最后一项直接传到下一轮,不能越界访问它的“右邻居”。也可以补单位元,但必须明确补多少和用什么值。

加法单位元是 0,乘法是 1,最大值需要按类型与空输入合同选择最小可接受值或单独返回空结果。操作必须适合所采用的重排。数学整数加法满足结合律;有符号机器整数溢出会破坏程序安全,所以示例限制值域并用更宽累加器。通用归约还要考虑操作是否允许改变元素顺序,尤其是非交换操作。

浮点数不是实数代数

浮点表示只有有限精度。大的数附近相邻可表示值之间有间距,小量相加可能被舍入掉。例子取 1e16、-1e16、1:先让前两个相消再加 1 得到 1;先把后两个相加,再加 1e16,可能把小的 1 丢掉而得到 0。使用 double 不会消除这个机制,只会改变出现问题的尺度。

因此同一数学和在串行、不同线程数或不同树形分组下可能不逐位相等。确定性与准确性是不同目标:固定树形可以让同一环境结果更可重复,但不自动最接近真值;更宽累加、pairwise 或 compensated summation 可能改善误差,却仍需测量成本和验证适用条件。不能随意用 fast-math 后再承诺 IEEE 特殊值行为不变。

比较之前先检查有限性

先回访18.1的有限值分类、绝对/相对容差与失败判定,另留5分钟核对区别。18.1的near_small限定了输入大小,并用双方绝对值的较大者作为相对尺度;本章采用下方以reference绝对值为尺度的另一份合同,不能直接搬用那个函数或它的数值额度。本章还处理差值或容差自身上溢的极值,极值缩放在这里进一步讲授。

常见错误判断是“若 abs(actual-reference) 大于 tolerance 就失败”。当 actual 是 NaN 时,大小比较可能为假,于是错误结果竟被放行。本章明确只接受有限结果,先检查双方 isfinite,再判断 abs(actual-reference) <= atol + rtol*abs(reference)。接近零时相对误差不能独自工作,需要绝对容差;大值时相对项帮助表达尺度相关误差。容差本身也必须有限且非负。

有限输入仍可能在计算差值或容差时上溢。若二者都变成 Inf,直接比较 Inf≤Inf 会假通过。比如 actual 是最大有限 double、reference 是它的负一半、相对容差为 2.5,真实相对差是 3,应当拒绝。下载程序在这条极值路径上除以双方绝对值的最大值,再比较缩放后的有限差值与容差;普通路径保持原式,避免不必要的下溢。这里不依赖 long double 比 double 更宽,极值回归同时覆盖应通过和应拒绝的边界。

测试应包含空输入、单元素、奇数长度、非二次幂、全部零、正负混合、大量相消与非有限值。若应用允许 Inf 或 NaN,就要另写显式合同,不应偷偷依赖比较表达式。这里的 CPU 树形模型与 scan 验证算法和数值语义,没有验证 GPU block 同步、跨 block 合并或设备性能。

动手改变 · 观察因果

把每一个加数留在归约树上

这一层有哪些配对?奇数尾项流向哪里?

有界整数相加的逻辑依赖树;不模拟 GPU 调度或浮点误差。落单值沿虚线直接传到下一层。 对应 例题 28-a;图中的代码行是步骤提示,完整可编译源码见例题。

改输入后从第一步重新推演。Tab 选择控件,Enter/空格操作按钮;图内方向键平移,手机可横向滑动。

正在准备默认算例。下方例题包含完整源码与逐步解释。

第 1 步

先预测,再前进一步

这一层有哪些配对?奇数尾项流向哪里?

输入、边界、状态变化

完整文字推演与当前数据
    静态推演与完整文字(便于对照、打印)
    观察 · 推演

    归约各层的数值摘要(完整依赖树见上方实验)

    输入 03输入 11输入 24输入 31输入 4501 / 04 · PIPELINE输入 03输入 11输入 24输入 31输入 4501 / 04 · PIPELINE
    原始五项

    各项在同一层;这里只列数值,不把同层项画成串行依赖。

    1 / 4
    阅读完整推演文字
    1. 原始五项

      输入 0:3;输入 1:1;输入 2:4;输入 3:1;输入 4:5

      各项在同一层;这里只列数值,不把同层项画成串行依赖。

    2. 第一轮

      3 + 1:4;4 + 1:5;尾项保留:5

      3+1=4,4+1=5;落单的5直接保留。

    3. 第二轮

      4 + 5:9;尾项保留:5

      4+5=9,尾项5继续保留。

    4. 最后一轮

      9 + 5:14

      9+5=14;完整树中的虚线始终对应不做加法的尾项保留。

    跟着例子,走完一遍

    例题 01C++20 · 本机可运行

    奇数长度树形归约与扫描

    输入 [3,1,4,1,5];同时检查 0..99 个元素与串行参考。

    1. 每轮相邻两项合并,落单项直接保留。
    2. 重复直到剩一项,空输入返回加法单位元0。
    3. inclusive scan 顺序记录每个新累计值。
    28-a.cpp
    下载
    #include <cstddef>
    #include <iostream>
    #include <numeric>
    #include <stdexcept>
    #include <vector>
    #include <utility>
    long long reduce(std::vector<long long> values) {
        if(values.empty()) return 0;
        while(values.size()>1) {
            std::vector<long long> next;next.reserve(values.size()/2+values.size()%2);
            for(std::size_t i=0;i<values.size();i+=2)
                next.push_back(values[i]+(i+1<values.size()?values[i+1]:0));
            values=std::move(next);
        }
        return values[0];
    }
    int main() {
        for(std::size_t n=0;n<100;++n) {
            std::vector<long long> v(n);for(std::size_t i=0;i<n;++i) v[i]=static_cast<long long>(i%11)-5;
            if(reduce(v)!=std::accumulate(v.begin(),v.end(),0LL)) throw std::runtime_error("oracle");
        }
        const std::vector<long long> input{3,1,4,1,5};std::vector<long long> scan(input.size());
        std::partial_sum(input.begin(),input.end(),scan.begin());
        if(reduce(input)!=14 || scan!=std::vector<long long>{3,4,8,9,14}) throw std::runtime_error("scan");
        std::cout<<"sum="<<reduce(input)<<" scan=";
        for(std::size_t i=0;i<scan.size();++i) std::cout<<(i?",":"")<<scan[i];std::cout<<'\n';
    }

    如何编译和运行下载的 .cpp 文件 →

    结果与解释

    sum=14 scan=3,4,8,9,14

    奇数尾部不会越界,scan 保留每一个前缀而 reduction 只保留总值。

    在本机运行这个例子

    下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。

    clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 28-a.cpp -o example && ./example

    预期标准输出:

    sum=14 scan=3,4,8,9,14
    
    例题 02C++20 · 本机可运行

    相消与非有限值拒绝

    比较两种括号顺序,并测试 NaN、Inf、接近零、合理误差与最大有限 double 的异号差值。

    1. 保持普通 IEEE double 运算,禁止 fast-math。
    2. 把容差合法性与结果有限性放在误差计算之前。
    3. 同时使用绝对和相对容差。
    28-b.cpp
    下载
    #include <algorithm>
    #include <cmath>
    #include <iostream>
    #include <limits>
    #include <stdexcept>
    bool close(double value,double reference,double atol=1e-12,double rtol=1e-9) {
        if(!std::isfinite(value)||!std::isfinite(reference)||!std::isfinite(atol)||!std::isfinite(rtol)||atol<0||rtol<0) return false;
        const double difference=std::abs(value-reference);
        const double tolerance=atol+rtol*std::abs(reference);
        if(std::isfinite(difference)) return difference<=tolerance;
        if(std::isfinite(tolerance)) return false;
        // Both overflowed: compare the same quantities in bounded units.
        // Do not rely on long double having extra range on this platform.
        const double scale=std::max(std::abs(value),std::abs(reference));
        return std::abs(value/scale-reference/scale)
            <= atol/scale+rtol*(std::abs(reference)/scale);
    }
    int main() {
        const double large=1e16,negative=-1e16,small=1;
        const double serial=(large+negative)+small,regrouped=large+(negative+small);
        const double nan=std::numeric_limits<double>::quiet_NaN(),inf=std::numeric_limits<double>::infinity();
        if(serial!=1 || regrouped!=0 || close(nan,1) || close(1,nan) || close(inf,inf)
           || !close(1+1e-10,1) || !close(1e-13,0) || close(1,0) || close(1,1,-1))
            throw std::runtime_error("numeric contract");
        const double maximum=std::numeric_limits<double>::max();
        if(close(maximum,-maximum/2,0,2.5) || !close(maximum,-maximum/2,0,3)
           || close(maximum,-maximum,0,1.5) || !close(maximum,-maximum,0,2)
           || close(maximum,-maximum,maximum,0) || !close(maximum,-maximum,maximum,1)
           || !close(maximum,maximum/2,0,2) || !close(maximum,maximum,0,0))
            throw std::runtime_error("finite extreme tolerance");
        std::cout<<"serial="<<serial<<" regrouped="<<regrouped<<" nonfinite_rejected=1\n";
    }
    结果与解释

    serial=1 regrouped=0 nonfinite_rejected=1

    先相消与先舍入产生不同结果;NaN 必须显式拒绝。若有限输入的差值与阈值同时上溢,用归一化比较避免 Inf≤Inf 假通过。

    在本机运行这个例子

    下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。

    clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 28-b.cpp -o example && ./example

    预期标准输出:

    serial=1 regrouped=0 nonfinite_rejected=1
    
    常见错误

    NaN 从误差检查里漏过

    只用 if(abs(actual-reference)>tol) 判断失败,NaN 比较为 false,错误被当成通过。

    修正思路:先验证有限值与容差合法,再用正向通过条件;明确是否允许无穷与 NaN。

    轮到你动手

    先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。

    练习 1

    对 [2,0,3,1] 写出 inclusive 和 exclusive scan;再用 [1,0,1,1] 计算稳定筛选位置。

    给我一点提示
    1. exclusive 从0开始且不含当前位置。
    2. 只有标志为1的位置才会写输出。
    查看答案与推理

    数值 inclusive=[2,2,5,6],exclusive=[0,2,2,5]。标志 exclusive=[0,1,1,2],下标0、2、3的原元素写入输出0、1、2,输出长度3。

    练习 2

    设计一个 reduction 测试集合,让只支持二次幂长度且漏 NaN 的实现失败。

    给我一点提示
    1. 覆盖0、1、3、5、块边界加一。
    2. 特殊值测试要对应明确合同。
    查看答案与推理

    测试空输入与单值,3/5/257长度正负混合,全部零和强相消。有限值合同下输入或输出NaN/Inf应拒绝;比较器单独测试NaN作为actual和reference,避免两边相同错误掩盖问题。

    把理解说出来

    先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。

    解释

    reduction 与 scan 输出差在哪?

    参考回答 / English answer

    reduction产出归并结果,scan产出每个前缀。inclusive包含当前元素,exclusive不包含。

    Reduction produces an aggregate. Scan produces each prefix, either including or excluding the current element.
    预测

    五个元素如何做两两 reduction 而不越界?

    参考回答 / English answer

    每轮检查右邻居存在,落单项保留到下一轮,或用正确单位元填充。

    An unpaired final element is carried into the next round. Padding is another option if the identity element is correct.
    解释

    树形 reduction 的工作与关键路径是多少?

    参考回答 / English answer

    常见平衡树总工作O(n),依赖深度O(log n);不包括真实调度和通信代价。

    A balanced reduction tree has linear work and logarithmic span. Runtime scheduling and communication add further cost.
    找错

    浮点加法为什么不能任意重排还要求逐位一致?

    参考回答 / English answer

    有限精度在每步舍入,不满足实数结合律。改变括号会改变被丢弃的小量。

    Floating-point addition rounds intermediate values. Reassociation can change the result even when the real-number expression is equivalent.
    追问

    绝对和相对容差为什么都需要?

    参考回答 / English answer

    接近零时相对误差失去意义,大尺度时固定绝对阈值过严或过松;两者结合并按应用选取。

    Absolute tolerance handles values near zero. Relative tolerance scales with the magnitude of the reference.
    找错

    通过固定CPU顺序就能证明GPU reduction正确吗?

    参考回答 / English answer

    只能提供算法参考;GPU需验证索引、同步、尾部、跨块合并和数值合同,设备未运行不能声称通过。

    A CPU oracle defines expected semantics. The device implementation still needs validation of indexing, synchronization, and numerical behavior.

    继续查证

    公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。