Skip to content

[vulkan] Float64 literals are narrowed through float before SPIR-V constant emission #9569

Open

Description

@derek-gerstmann

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.exe

Relevant 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions