Repository navigation
[vulkan] Float64 literals are narrowed through float before SPIR-V constant emission #9569
Description
Description
Summary
Float64 literal precision is lost even with VulkanFloat64 enabled. The
inspected Vulkan emitter accepts floating constants as a C++ float,
including when instantiating its helper for double.
Tested environment
Halide 22.0.0.dev444, Windows x64, MSVC 19.51.36248, RTX 3070 Ti / NVIDIA
596.49. Host plus Vulkan, VulkanInt64 and VulkanFloat64.
Compiler evidence
In the inspected CodeGen_Vulkan_Dev.cpp:
template<typename T>
SpvId declare_constant_float(Type value_type, float value) {
const T typed_value = (T)(value);
// builder.declare_constant(value_type, &typed_value)
}Choosing T=double cannot recover the bits already narrowed by the
float value parameter. The helper declaration and definition, plus all
call sites, need review. This draft identifies the narrowing point, not a
tested upstream patch.
Impact and workaround
Double coefficients, thresholds, range reductions and very large/small
literals lose precision or range despite a supported Float64 target.
Transport the exact IEEE double representation as two UInt32 constants,
concatenate and reinterpret as Float64 inside device IR. This avoids the
floating-literal helper without reducing precision.
Suggested regression: exact bit patterns for 1 + 2^-40, subnormal/large
finite doubles, signed zero and nonfinite literals. The minimal case here
only demonstrates finite low-bit loss; do not infer independently tested
special-value behavior from it.
Reproducing case
#include "Halide.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <string>
using namespace Halide;
int run(int argc, char **argv) {
const std::string mode = "constant";
Target target = get_host_target();
if (std::getenv("HALIDE_REPRO_HOST") == nullptr) {
target = target.with_feature(Target::Vulkan)
.with_feature(Target::VulkanInt64).with_feature(Target::VulkanFloat64);
}
Var x("x"), block("block"), thread("thread");
Func output("output");
Param<int32_t> control("control");
Buffer<float> floats(33);
Buffer<float> powers(33);
powers.fill(2.0f);
Buffer<double> doubles(33);
Buffer<int32_t> integers(33);
Buffer<uint64_t> wide(33), other(33);
for (int i = 0; i < 33; ++i) {
floats(i) = (i + 1) * 0.1f;
doubles(i) = (i + 1) * 0.1;
integers(i) = (i - 16) * 16385;
wide(i) = i % 2 ? UINT64_MAX : uint64_t(1) << 63;
other(i) = i % 2 ? 1 : 3;
}
if (mode == "phi") {
auto lazy = [](Expr condition, Expr yes, Expr no) {
return Internal::Call::make(yes.type(), Internal::Call::if_then_else,
{condition, yes, no}, Internal::Call::PureIntrinsic);
};
Expr v = floats(x), p = powers(x);
output(x) = lazy(p == 2.0f, v * v,
lazy(p == 0.5f, sqrt(v),
lazy(p == 1.0f, v, pow(v, p))));
} else if (mode == "shift") {
output(x) = rounding_shift_right(integers(x), cast<uint32_t>(14));
} else if (mode == "select") {
Expr sum = wide(x) + other(x);
output(x) = select(sum < wide(x), UInt(64).max(), sum);
} else if (mode == "constant") {
output(x) = doubles(x) + Expr(1.0 + 0x1p-40);
} else if (mode == "bool") {
Param<double> scale("scale"), offset("offset");
scale.set(1.0 + 0x1p-40);
offset.set(-3.0 - 0x1p-38);
output(x) = Tuple(select(control > 0, integers(x) + 17, integers(x) * 31) +
select((control & 1) == 0, integers(x) + 43, integers(x) * 59),
strict_float(scale * cast<double>(x) + offset));
} else if (mode == "strict") {
Buffer<float> a(33), b(33), c(33);
a.fill(1.0f + 0x1p-23f);
b.fill(1.0f - 0x1p-23f);
c.fill(-1.0f);
output(x) = strict_float(a(x) * b(x) + c(x));
} else if (mode == "math") {
const std::string fn = argc > 1 ? argv[1] : "sqrt";
Expr v = doubles(x);
if (fn == "sqrt") output(x) = sqrt(v);
else if (fn == "floor") output(x) = floor(v);
else if (fn == "exp") output(x) = exp(v);
else if (fn == "log") output(x) = log(v);
else if (fn == "sin") output(x) = sin(v);
else if (fn == "cos") output(x) = cos(v);
else if (fn == "pow") output(x) = pow(v, Expr(0.3));
else if (fn == "atan2") output(x) = atan2(v, Expr(-0.7));
else return 2;
} else {
return 2;
}
if (target.has_feature(Target::Vulkan)) {
output.gpu_tile(x, block, thread, 32, TailStrategy::GuardWithIf);
} else {
output.vectorize(x, 4, TailStrategy::GuardWithIf);
}
Pipeline pipeline(output);
if (const char *path = std::getenv("HALIDE_REPRO_OBJECT")) {
pipeline.compile_to_object(path, pipeline.infer_arguments(), "upstream_repro", target);
}
if (mode == "bool") {
for (int m : {-2, -1, 0, 1, 2, 3}) {
control.set(m);
Realization values = pipeline.realize({33}, target);
Buffer<int32_t> result = values[0];
Buffer<double> precise = values[1];
result.copy_to_host();
precise.copy_to_host();
std::printf("mode=%d actual=%d expected=%d double=%.17g expected=%.17g\n",
m, result(0), (m > 0 ? integers(0) + 17 : integers(0) * 31) +
(m % 2 == 0 ? integers(0) + 43 : integers(0) * 59),
precise(0), -3.0 - 0x1p-38);
}
} else if (mode == "select") {
Buffer<uint64_t> result = pipeline.realize({33}, target);
result.copy_to_host();
std::printf("actual=%llu,%llu expected=%llu,%llu\n",
(unsigned long long)result(0), (unsigned long long)result(1),
(unsigned long long)((uint64_t(1) << 63) + 3),
(unsigned long long)UINT64_MAX);
} else if (mode == "shift") {
Buffer<int32_t> result = pipeline.realize({33}, target);
result.copy_to_host();
std::printf("actual=%d expected=-16\n", result(0));
} else if (mode == "constant" || mode == "math") {
Buffer<double> result = pipeline.realize({33}, target);
result.copy_to_host();
std::printf("actual=%.17g\n", result(0));
if (mode == "constant") {
std::printf("expected=%.17g\n", doubles(0) + (1.0 + 0x1p-40));
}
} else {
Buffer<float> result = pipeline.realize({33}, target);
result.copy_to_host();
std::printf("actual=%.17g\n", (double)result(0));
if (mode == "strict") std::printf("expected=0\n");
}
return 0;
}
int main(int argc, char **argv) {
try {
return run(argc, argv);
} catch (const Halide::Error &error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}How did you get Halide?
pip (halide)
Halide version
22.0.0.dev444
Halide commit (if known)
No response
Target
x86-64-windows-tune_znver3-avx-avx2-f16c-fma-sse41-vk_float64-vk_int64-vulkan
Operating system
Windows 11
Additional context
Reproduction
Compile and link the provided repro case, then execute the binary:
.\vulkan-float64-constants.exeRelevant operation, with doubles(0) = 0.1:
output(x) = doubles(x) + Expr(1.0 + 0x1p-40);The double buffer prevents the entire computation from being evaluated on
the host. The added literal differs from 1 by a value representable in
Float64 but not Float32.
Expected and actual
actual=1.1000000000000001
expected=1.1000000000009096
The CPU control produces 1.1000000000009096. This is not an acceptable
Float64 rounding difference: the coefficient's low bits have disappeared
before the addition.
Activity