我正在寻求在 JavaScript + WebAssembly 中实现 RandomX 工作量证明哈希函数。README 解释了这是不可能的:由于需要大量内存,因此 Web 挖掘不可行...
我希望在 JavaScript + WebAssembly 中实现 RandomX 工作量证明哈希函数。README 解释了为什么这是不可能的:
由于内存需求大,且 Javascript 和 WebAssembly 中缺乏对浮点运算的定向舍入支持,因此 Web 挖掘不可行。
由于广泛使用 CFROUND
在字节码中使用指令重复切换舍入模式,并且 JavaScript 无法更改舍入模式,甚至无法使用除偶数舍入之外的任何其他舍入模式,因此这是不可能的。
该 CFROUND
指令操作 fprc
RandomX 规范内的抽象寄存器。 fprc
从零开始,因此回合与偶数相关,并且在每次指令执行时似乎随机更新。
fprc
|
舍入模式
|
0
|
roundTiesToEven
|
1
|
朝负方向舍入
|
2
|
roundTowardPositive
|
3
|
朝零方向舍去
|
RandomX 使用 5 个正确舍入的运算,add、sub、mul、div 和 sqrt 保证按照 IEEE754 规范以双精度正确舍入。我的问题是这样的。 软浮点不是一种选择,我只能在舍入到偶数的情况下访问这 5 个运算的浮点算法。我需要找到一种方法来模拟双精度算法,所有其他舍入模式都以这 5 个运算的一种舍入模式实现。这比软浮点快得多。
Bruce 在 Random ASCII 博客 发表了一些言论:
如果我理解正确,那么其他三种舍入模式将产生与舍入到最近偶数相同或相差一个 ULP 的结果。如果舍入到最近偶数操作是精确的,那么所有舍入模式都会产生相同的结果。如果不精确,那么您必须弄清楚哪些结果会有所不同。我认为向负舍入或向正舍入将相差一个 ULP,但不会同时相差两个。
FMA(融合乘加)运算在 JavaScript 中不可用,但它们在 WebAssembly 中可用,但受 非确定性运算 (未得到广泛支持)的约束。这些运算是 f32x4.relaxed_*madd
和 f64x2.relaxed_*madd
。如果需要使用 FMA,这没问题,我可以实现回退。
唯一一篇稍微接近浮点舍入模式模拟的论文是 《使用舍入到最近偶数位的算法模拟舍入到最近零位的“增强”浮点运算》 。它提出了一些有趣的观点,但没有用。
附加信息 0: RandomX 使用两类抽象浮点寄存器进行运算,即组 F(加法运算)和组 E(乘法运算)。根据规范, 组 F 的绝对值永远不会超过 3.0e+14
,组 E 的近似范围是 1.7E-77
( infinity
始终为正)。浮点运算的所有结果都 绝不能 是 NaN。答案可能会考虑到这一点。
附加信息 1 (地址 @sech1 注释): 该 FSCAL_R
指令使事情变得复杂。有效执行该指令需要操作实际的浮点二进制格式:
上述数学运算相当于对二进制表示与以下值进行按位异或: 0x80F0000000000000
.
这意味着其他形式的表示(双精度、定点)已经不复存在:
由于 F 组寄存器的有限范围允许使用更有效的定点表示(80 位数字),因此 FSCAL 指令会操纵浮点格式的二进制表示,从而使这种优化更加困难。
指令可以在两个双精度数之间展开, FSCAL_R
那么双精度算术就有可能起作用
附加信息2(地址@aka.nice @sech1):
所有浮点运算均根据 fprc
寄存器的当前值进行舍入(见表 4.3.1)。由于浮点寄存器值的限制,任何运算都不会产生 NaN
或非规格化数。
我已开始执行计算,将所有输出数字限制在其范围内,以便在 E 组和 F 寄存器上执行操作(没有非正规数、没有 NaN 等),结果答案会偏离一位或导致无穷大。
我采纳了@aka.nice关于使用双产品实施操作的建议。 测试工具和失败/成功输出在这里 。摘录如下:
FAIL: [mul(FE_UPWARD)] truth(...) == 0x1.00c915254a87ep+968 != op(...) == 0x1.00c915254a87dp+968
mul(FE_UPWARD): 19/20
FAIL: [mul(FE_DOWNWARD)] truth(...) == 0x1.fffffffffffffp+1023 != op(...) == inf
FAIL: [mul(FE_DOWNWARD)] truth(...) == 0x1.00c915254a87dp+968 != op(...) == 0x1.00c915254a87cp+968
FAIL: [mul(FE_DOWNWARD)] truth(...) == 0x1.fffffffffffffp+1023 != op(...) == inf
FAIL: [mul(FE_DOWNWARD)] truth(...) == 0x1.fffffffffffffp+1023 != op(...) == inf
FAIL: [mul(FE_DOWNWARD)] truth(...) == 0x1.fffffffffffffp+1023 != op(...) == inf
FAIL: [mul(FE_DOWNWARD)] truth(...) == 0x1.fffffffffffffp+1023 != op(...) == inf
mul(FE_DOWNWARD): 14/20
FAIL: [div(FE_TOWARDZERO)] truth(...) == 0x1.71bb8430246b4p-58 != op(...) == 0x1.71bb8430246b3p-58
FAIL: [div(FE_TOWARDZERO)] truth(...) == 0x1.c254556a24a56p+73 != op(...) == 0x1.c254556a24a55p+73
FAIL: [div(FE_TOWARDZERO)] truth(...) == 0x1.a67a7c9cb8d59p+483 != op(...) == 0x1.a67a7c9cb8d58p+483
附加信息3(地址@aka.nice):
我已经取得了很大的进步,目前已经实现了加法和减法、98%的乘法和40%的除法和平方根。
我已经调整了 round_toward
函数,使其按照 OpenCL 中实现的 RandomX VM .
double round_toward(double c, double res, int fe) {
if (isinf(c)) {
// fe & 1
uint64_t inf_rnd = (2047UL << 52) - (fe == FE_DOWNWARD || fe == FE_TOWARDZERO);
return u64_double(inf_rnd);
}
...
未实施FMA的双产品实施进度落后1%:
void split64(double x, int delta, double *x_h, double *x_l) {
// https://github.com/dsiem/ErrorFreeTransforms.jl/blob/master/src/mul.jl
double f = (1UL << delta) + 1;
double c = x * f;
double h = c - (c - x);
double l = x - h;
*x_h = h;
*x_l = l;
}
// 100% success
double mul_residue(double a, double b, double c) {
return fma(a, b, -c);
}
// 99% success
double mul_residue(double a, double b, double c) {
double a_h, a_l;
double b_h, b_l;
split64(a, 27, &a_h, &a_l);
split64(b, 27, &b_h, &b_l);
return a_l * b_l - (((c - a_h * b_h) - a_l * b_h) - a_h * b_l);
}
代码输出与双产品 EFT 相同,与 FMA 的使用不匹配:
FAIL: [mul(FE_TOWARDZERO)] truth(0x1.3459aa1f9d423p-244, 0x1.47ebd8e19bd97p+1017) == 0x1.8afa9bd8f2f64p+773 != op(...) == 0x1.8afa9bd8f2f65p+773
FAIL: [mul(FE_TOWARDZERO)] truth(0x1.36a0e20b199b7p+997, 0x1.441f01389c628p-96) == 0x1.89493d0cbd95dp+901 != op(...) == 0x1.89493d0cbd95ep+901
mul(FE_TOWARDZERO): 498/500
FAIL: [mul(FE_UPWARD)] truth(0x1.4c46f791b3cb9p+1001, 0x1.a55d5da424cd1p-165) == 0x1.1174f23ab0e37p+837 != op(...) == 0x1.1174f23ab0e36p+837
FAIL: [mul(FE_UPWARD)] truth(0x1.8c3c4851d9b75p-162, 0x1.ae85675639db9p+1006) == 0x1.4d2dde5e628f5p+845 != op(...) == 0x1.4d2dde5e628f4p+845
mul(FE_UPWARD): 498/500
FAIL: [mul(FE_DOWNWARD)] truth(0x1.3459aa1f9d423p-244, 0x1.47ebd8e19bd97p+1017) == 0x1.8afa9bd8f2f64p+773 != op(...) == 0x1.8afa9bd8f2f65p+773
FAIL: [mul(FE_DOWNWARD)] truth(0x1.36a0e20b199b7p+997, 0x1.441f01389c628p-96) == 0x1.89493d0cbd95dp+901 != op(...) == 0x1.89493d0cbd95ep+901
mul(FE_DOWNWARD): 498/500
当前实现在这里。我已经非常接近了,只是一些极端情况和除法和平方根的实现。最好让函数 mul_residue
在有或没有的情况下完全相同, fma()
这样优化就不会改变输出。
附加信息4(地址@aka.nice):
当前实现 ( 附有实现的通过率和边缘情况)通过了所有测试,每个操作和舍入模式均取 50 万个浮点数样本,但仅在 mul_residue
使用 实现 fma()
和 split()
的代码 mul_residue()
如下所示:
void split64(double a, double *x_h, double *x_l) {
double f = (1UL << 26) + 1;
double c = f * a;
double x = c - (c - a);
double y = a - x;
*x_h = x;
*x_l = y;
}
double mul_residue(double a, double b, double c) {
// 100% pass rate
//return fma(a, b, -c);
// 90% pass rate
double a1, a2;
double b1, b2;
split64(a, &a1, &a2);
split64(b, &b1, &b2);
double r1 = a * b - (a1 * b1);
double r2 = r1 - (a1 * b2);
double r3 = r2 - (a2 * b1);
double res1 = a2 * b2 - r3;
double res2 = a * b - c;
return res1 + res2;
}