Skip to content

[BUG] Warp truncates double-precision constants #485

Closed

Description

@shi-eric

Bug Description

Double-precision constants get truncated to single-precision unless done in a specific manner.

Example

import warp as wp


@wp.kernel
def test_precision():
    pi = wp.float64(wp.PI)
    wp.printf("pi (kernel) = %.16g\n", pi)


print(f"pi (python) = {wp.PI}")

wp.launch(test_precision, dim=(1,), inputs=[])

This produces the output:

pi (python) = 3.141592653589793
pi (kernel) = 3.141592741012573

We can see what's going on in the generated CUDA C++ code:

extern "C" __global__ void test_precision_d1c1f7f9_cuda_kernel_forward(
    wp::launch_bounds_t dim)
{
    for (size_t _idx = static_cast<size_t>(blockDim.x) * static_cast<size_t>(blockIdx.x) + static_cast<size_t>(threadIdx.x);
         _idx < dim.size;
         _idx += static_cast<size_t>(blockDim.x) * static_cast<size_t>(gridDim.x))
    {
        // reset shared memory allocator
        wp::tile_alloc_shared(0, true);

        //---------
        // primal vars
        const wp::float32 var_0 = 3.141592653589793;
        const wp::float32 var_1 = 3.141592653589793;
        wp::float64 var_2;
        const wp::str var_3 = "pi (kernel) = %.16g\n";
        //---------
        // forward
        // def test_precision():                                                                  <L 7>
        // pi = wp.float64(wp.PI)                                                                 <L 8>
        var_2 = wp::float64(var_1);
        // wp.printf("pi (kernel) = %.16g\n", pi)                                                 <L 9>
        printf(var_3, var_2);
    }
}

The right double-precision value is being captured in the kernel, but it's assigned to a wp::float32 first, causing the truncation.

Current Workaround

A workaround is to define the variable as a wp::float64 outside the kernel:

import warp as wp

pi_fp64 = wp.float64(wp.PI)


@wp.kernel
def test_precision():
    pi = wp.float64(pi_fp64)
    wp.printf("pi (kernel) = %.16g\n", pi)


print(f"pi (python) = {pi_fp64.value}")

wp.launch(test_precision, dim=(1,), inputs=[])

This produces the expected output:

pi (python) = 3.141592653589793
pi (kernel) = 3.141592653589793

The generated code in this case is:

extern "C" __global__ void test_precision_ae841f27_cuda_kernel_forward(
    wp::launch_bounds_t dim)
{
    for (size_t _idx = static_cast<size_t>(blockDim.x) * static_cast<size_t>(blockIdx.x) + static_cast<size_t>(threadIdx.x);
         _idx < dim.size;
         _idx += static_cast<size_t>(blockDim.x) * static_cast<size_t>(gridDim.x))
    {
        // reset shared memory allocator
        wp::tile_alloc_shared(0, true);

        //---------
        // primal vars
        const wp::float64 var_0 = 3.141592653589793;
        wp::float64 var_1;
        const wp::str var_2 = "pi (kernel) = %.16g\n";
        //---------
        // forward
        // def test_precision():                                                                  <L 7>
        // pi = wp.float64(pi_fp64)                                                               <L 8>
        var_1 = wp::float64(var_0);
        // wp.printf("pi (kernel) = %.16g\n", pi)                                                 <L 9>
        printf(var_2, var_1);
    }
}

System Information

No response

Activity

  1. added this to the Backlog milestone on Apr 3, 2025
  2. self-assigned this
    on Jan 13, 2026
  3. modified the milestones: Backlog, 1.11.1 on Jan 13, 2026
  4. c0d1f1ed commented on Jan 13, 2026

    @c0d1f1ed
    Contributor

    Eric reported that this also happens for integer literals. Specifically, code like h = h * wp.uint64(0xFF51AFD7ED558CCD) becomes

    const wp::int32 var_5 = 18397679294719823053;
    wp::uint64 var_6;
    // ...
    // h = h * wp.uint64(0xFF51AFD7ED558CCD)                                                  <L 58>
    var_6 = wp::uint64(var_5);
    var_7 = wp::mul(var_4, var_6);
    

    We might be able to address this by emitting const auto var_5 = ... instead.

  5. modified the milestones: 1.11.1, 1.12.0 on Jan 23, 2026
  6. c0d1f1ed commented on Jan 29, 2026

    @c0d1f1ed
    Contributor

    This turns out to be a more complicated than expected. Just writing stuff down to sort my thoughts:

    • In Python all floating-point numbers (float type) are double-precision. Even though we have wp.float32 and wp.float64 to discern between single- and double-precision at the Warp level, there is no single-precision literal representation.
    • We have lots of existing kernel code like x * 2.0 where x is single-precision and 2.0 is interpreted as single-precision. In contrast, CUDA would implicitly promote x to double-precision and perform a double-precision multiplication. So the standard recommends using the f suffix where applicable.
    • Warp does not support any implicit conversion at the moment.

    We have a few design options:

    • Keep things as-is. The behavior makes sense when accepting that float means single-precision in kernels and double-precision in Python. Only explicit use of wp.float32 and wp.float64 ensures a fixed format. While consistent and tractable, it's too confusing.
    • Interpret literals as double precision and implement implicit conversion, requiring a cast to wp.float32 to stick to single precision arithmetic. While this would be closest to CUDA behavior, it's not a realistic option due to causing terrible performance in many cases.
    • Treat values representable in single-precision as single-precision, otherwise double-precision. This would mean 2.0 is single-precision. wp.PI would be double-precision, and we could have a single-precision variant. The issue with this is that it's hard to tell for a given expression containing literals or constants whether it produces a single- or double-precision result.
    • Treat float literals and constants as single-precision except when casting them with wp.float64(). Basically use the cast as the inverse of the f-suffix. This is tricky because in wp.float64(2.0 / 3.0) should the division be performed in single- or double-precision? The expectation is probably double-precision but this requires either performing constant folding as part of codegen or ensuring each literal gets stored in a double at the CUDA level.
    • Treat float literals and constants as double-precision but when used in arithmetic involving single-precision variables automatically cast them to single-precision.

    The last option is what NumPy does: Promotion rules for Python scalars. Essentially float is considered a "weak" type and it only becomes strongly typed to match the precision of the arithmetic operations. It makes a lot of sense from a developer's perspective, with minimal surprising behavior.

    It might be feasible to implement this using a C++ weak_float class which can cast to a float or double based on context...

  7. modified the milestones: 1.12.0, 1.12.1, 1.13.0 on Feb 23, 2026
  8. modified the milestones: 1.13.0, 1.12.1 on Mar 9, 2026
  9. shi-eric commented on Mar 9, 2026

    @shi-eric
    ContributorAuthor

    The scope was reduced and the precision loss issue was addressed in 0d3e1ea, planned for the 1.12.1 release.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions