CF1953A Accuracy-Preserving Summation Algorithm
题目描述
在经典的高性能计算(HPC)领域,绝大多数计算都是用双精度 64 位浮点数(fp64,双精度,IEEE-754 binary64)进行的。深度神经网络(DNN)的兴起带来了能够处理 16 位浮点数(fp16,半精度,IEEE-754 binary16)的硬件,这些硬件在每秒浮点运算次数(flops)上最高可比 fp64 快 16 倍,在内存带宽(BW)上最高可快 4 倍。然而,fp16 的尾数和指数都很短,导致计算精度损失极快,在规模大于约 $2000$ 的归约操作中会产生错误的计算结果且无法恢复。由于 HPC 中的典型问题规模远大于 $2000$,这使得 fp16 计算几乎毫无用处。为了解决这一重大障碍,需要更智能的归约操作算法。
描述:有一个长度为 $N$ 的浮点数序列 $x_i$,以 IEEE-754 binary64(双精度,fp64)格式存储。需要将该序列求和,得到 $S = x_1 + x_2 + \ldots + x_N$。由于一般用户通常无法获得原生支持 fp16 的专业计算设备,我们建议在一个简化的模拟环境中进行操作,即用 fp64 格式进行计算,但将尾数和指数截断到 fp16 可接受的范围。具体来说,过小的值超出 fp16 可接受范围会变为零,过大的值会变为无穷大。
目标:你的目标是尽可能快且尽可能准确地对尽可能多的序列进行求和。请注意,你可以用 fp64 格式进行求和,但这样虽然准确,速度会很慢。如果用 fp16 格式直接求和,速度会很快,但对于较大的序列则会非常不准确。
输入格式
输入包含一行。首先是一个整数 $N$,表示序列的长度。接下来是 $N$ 个双精度浮点数,构成序列 $x_i$,其中 $i = 1, \ldots, N$。
变量约束:
- 序列长度:$2 \leq N \leq 1\,000\,000$。
- 序列中每个数的值:为合法的 IEEE-754 binary64 值,以十进制格式给出。
注意,实际的 binary64 值不一定与给定的十进制值完全相等。实际给定的值是 binary64 能表示的最接近的数。在读取输入时,大多数编程语言会自动完成这种转换。
保证序列中的每个数要么为 $0$,要么其绝对值在 $10^{-300}$ 到 $10^{300}$ 之间(含端点)。
输出格式
输出一行,描述求和过程。该行应包含一个编码后的求和算法。我们用这种编码来实际进行求和并报告结果,以避免必须依赖原生支持 fp16 运算的硬件。
编码算法由所用数据类型和需要用该类型求和的值列表组成。算法的结果是用指定数据类型,从左到右、按给定顺序对这些值求和。格式如下:
{type:value\_1,value\_2,...,value\_k}
如上所示,整个算法被大括号(“{”和“}”)包围。下一个字符表示三种可能的数据类型之一:
- “d” 表示 fp64 求和,
- “s” 表示 fp32 求和,
- “h” 表示 fp16 求和。
然后是一个冒号(“:”)。接下来是不为空的值列表,用逗号(“,”)分隔。注意中间没有空格。
每个值可以是以下之一:
- 一个从 $1$ 到 $N$ 的整数,表示输入序列中的位置:此时该值直接来自输入;
- 另一个算法:此时该值为该算法的结果。
一些例子:
- {d:1,2,3,4} 表示用双精度计算 $x_1 + x_2 + x_3 + x_4$;
- {h:4,3,2,1} 表示用半精度计算 $x_4 + x_3 + x_2 + x_1$;
- {d:{s:3,4},{h:2,1}} 表示用双精度计算 $y + z$,其中:
- $y$ 用单精度计算 $x_3 + x_4$,
- $z$ 用半精度计算 $x_2 + x_1$;
- {h:1,4,{d:3,2}} 表示用半精度计算 $x_1 + x_4 + y$,其中:
- $y$ 用双精度计算 $x_3 + x_2$。
每个输入值必须且只能使用一次。
计分方式
本题有 2 个样例测试和 76 个主测试。每个主测试的得分如下。
第一部分得分与准确性相关。用你的算法计算的和记为 $S_c$。我们尽可能精确地计算期望和 $S_e$,并以 binary64 格式存储。然后准确性得分计算如下:
$$
A = \left(\max\left(\frac{\left|S_c - S_e\right|}{\max\left(\left|S_e\right|, 10^{-200}\right)}, 10^{-20}\right)\right)^{0.05}
$$
例如,若计算和为 $99.0$,期望和为 $100.0$,则准确性得分为 $A = \left(\frac{|99 - 100|}{|100|}\right)^{0.05} = \left(\frac{1}{100}\right)^{0.05} = 0.794328...$。若相对误差为 $\frac{1}{1000}$,则 $A = \left(\frac{1}{1000}\right)^{0.05} = 0.707945...$。若结果完全准确,则 $A = \left(10^{-20}\right)^{0.05} = 0.1$。
第二部分得分与所用求和类型相关。我们定义算法的权重 $W$。若算法执行了 $k$ 次加法(即加了 $k+1$ 个值),其权重为:
- 半精度:$1 \cdot k$
- 单精度:$2 \cdot k$
- 双精度:$4 \cdot k$
此外,若算法包含其他算法,其权重递归计算并加到父算法权重上。
第三部分得分与内存读取惩罚相关。我们将算法中出现的 $N$ 个数字按从左到右顺序列出(忽略所有大括号)。将这些数字按每 16 个分为一组,最后一组可能不足 16 个。每组的第一个元素 $i$ 触发一次内存读取。对于该组的其他元素 $j$,若 $|j - i| > 15$,则超出了本组内存读取范围,你会受到惩罚。惩罚逐步增加:第 $x$ 次惩罚为 $x / 20\,000$。惩罚计数器 $x$ 是全局的,在不同分组间持续累加。所有惩罚之和为总惩罚 $P$。
例如,考虑如下算法:{s:1,2,3,4,5,6,7,8,9,10,{d:20,19,18,17,16},11,12,13,14,15}。它用单精度执行 15 次加法,其中有一个元素是用双精度执行 4 次加法的算法。因此其权重为 $W = 15 \cdot 2 + 4 \cdot 4 = 46$。第一个内存读取块为 1,2,3,4,5,6,7,8,9,10,20,19,18,17,16,11,初始读取在位置 1,块内距离超过 15 的位置有 20, 19, 18, 17,因此有 4 次惩罚。第二个块为 12,13,14,15,初始读取在 12,无惩罚。总惩罚为 $P = (1 + 2 + 3 + 4) / 20\,000 = 0.0005$。
将第二、三部分合并,单次操作的平均代价为:
$$
C = \frac{W + P}{N-1}
$$
然后数据得分为:
$$
D = \frac{10.0}{\sqrt{C + 0.5}}
$$
最后,综合准确性得分,单个测试的得分为:
$$
\mathit{Score} = \frac{D}{A}
$$
所有主测试的单项得分相加,得到最终得分。样例测试仅用于检查,不计入总分。
说明/提示
本题准备了两套测试数据:初步测试和最终测试。在比赛期间,每次提交会在初步测试集上评测。比赛结束后,对于每位选手:
- 评测组会选取该选手在初步测试中得分不为零的最后一次提交。
- 这次提交会在最终测试集上评测。
- 选手根据最终测试的表现进行排名。
两套测试数据设计相似,但不完全相同。
由 ChatGPT 4.1 翻译