PyOptiX - Issues when rebuilding or updating GAS

Hi,

I’m currently running into an issue when trying to build a custom PyOptiX application. I started from the triangle.py example and then successfully modified the raygen and rayhit CUDA functions to what I wanted.

I then tried implementing changing vertices in the program duration by rebuilding or updating the GAS (tried both, and had the same issue with both). Strange square artefacts started appearing on my application on the first frame after GAS update. When trying to investigate, the validation mode always crashes at the first frame after GAS update. The error comes from one of the ray reflections implemented on my custom kernels, and hints at the fact that the triangle vertices are invalid (NaN in the reflexion vector coordinates).

To get back to a minimal example of the bug, I re-used the original raygen and rayhit CUDA functions so that my only modifications to the triangle.py example are the update() function that updates the GAS. The validation mode no longer crashes because there’s no secondary hit anymore, but strange artefacts appear in the geometry, which indicate that the vertex data is incorrect and would crash my more complicated closest_hit function.
Here is the picture of my scene before the GAS update (no artefact):

Here is the picture of my scene after the GAS update:


Here is how I build the Acceleration object :

class AccelState:
    def __init__(self, ctx):
        self.accel_options = optix.AccelBuildOptions(
            buildFlags = int(optix.BUILD_FLAG_ALLOW_RANDOM_VERTEX_ACCESS | optix.BUILD_FLAG_ALLOW_UPDATE),
            operation  = optix.BUILD_OPERATION_BUILD
        )

        self.vertices = cp.array( [
            -0.1, 0.1, -0.1,
            0.1, 0.00, -0.1,
            -0.1,-0.1, -0.1,

            -1.0,-1.0,-1.0,
            -1.0,-1.0, 1.0,
            -1.0, 1.0, 1.0,
            1.0, 1.0,-1.0,
            -1.0,-1.0,-1.0,
            -1.0, 1.0,-1.0,
            1.0,-1.0, 1.0,
            -1.0,-1.0,-1.0,
            1.0,-1.0,-1.0,
            1.0, 1.0,-1.0,
            1.0,-1.0,-1.0,
            -1.0,-1.0,-1.0,
            -1.0,-1.0,-1.0,
            -1.0, 1.0, 1.0,
            -1.0, 1.0,-1.0,
            1.0,-1.0, 1.0,
            -1.0,-1.0, 1.0,
            -1.0,-1.0,-1.0,
            -1.0, 1.0, 1.0,
            -1.0,-1.0, 1.0,
            1.0,-1.0, 1.0,
            1.0, 1.0, 1.0,
            1.0,-1.0,-1.0,
            1.0, 1.0,-1.0,
            1.0,-1.0,-1.0,
            1.0, 1.0, 1.0,
            1.0,-1.0, 1.0,
            1.0, 1.0, 1.0,
            1.0, 1.0,-1.0,
            -1.0, 1.0,-1.0,
            1.0, 1.0, 1.0,
            -1.0, 1.0,-1.0,
            -1.0, 1.0, 1.0,
            1.0, 1.0, 1.0,
            -1.0, 1.0, 1.0,
            1.0,-1.0, 1.0
        ], dtype = 'f4')
        self.triangle_input_flags = [ optix.GEOMETRY_FLAG_NONE, optix.GEOMETRY_FLAG_DISABLE_ANYHIT ]
        self.triangle_input = optix.BuildInputTriangleArray()
        self.triangle_input.vertexFormat  = optix.VERTEX_FORMAT_FLOAT3
        self.triangle_input.numVertices   = len( self.vertices )
        self.triangle_input.vertexBuffers = [ self.vertices.data.ptr ]
        self.triangle_input.flags         = self.triangle_input_flags
        self.triangle_input.numSbtRecords = 1;

        self.gas_buffer_sizes = ctx.accelComputeMemoryUsage( [self.accel_options], [self.triangle_input] )

        self.d_temp_buffer_gas   = cp.cuda.alloc( self.gas_buffer_sizes.tempSizeInBytes )
        self.d_gas_output_buffer = cp.cuda.alloc( self.gas_buffer_sizes.outputSizeInBytes)

        self.gas_handle = ctx.accelBuild(
            0,    # CUDA stream
            [ self.accel_options ],
            [ self.triangle_input ],
            self.d_temp_buffer_gas.ptr,
            self.gas_buffer_sizes.tempSizeInBytes,
            self.d_gas_output_buffer.ptr,
            self.gas_buffer_sizes.outputSizeInBytes,
            [] # emitted properties
        )
        #return (gas_handle, d_gas_output_buffer)

    def update(self, ctx):
        self.accel_options = optix.AccelBuildOptions(
            buildFlags = int(optix.BUILD_FLAG_ALLOW_RANDOM_VERTEX_ACCESS | optix.BUILD_FLAG_ALLOW_UPDATE),
            operation  = optix.BUILD_OPERATION_UPDATE
        )
        self.gas_handle = ctx.accelBuild(
            0,    # CUDA stream
            [ self.accel_options ],
            [ self.triangle_input ],
            self.d_temp_buffer_gas.ptr,
            self.gas_buffer_sizes.tempSizeInBytes,
            self.d_gas_output_buffer.ptr,
            self.gas_buffer_sizes.outputSizeInBytes,
            [] # emitted properties
        )

And here is how I use that object :

def main():
    triangle_cu = os.path.join(os.path.dirname(__file__), 'triangle.cu')
    triangle_ptx = compile_cuda( triangle_cu )


    ctx              = create_ctx()
    acc_state        = AccelState(ctx)
    pipeline_options = set_pipeline_options()
    module           = create_module( ctx, pipeline_options, triangle_ptx )
    prog_groups      = create_program_groups( ctx, module )
    pipeline         = create_pipeline( ctx, prog_groups, pipeline_options )
    sbt              = create_sbt( prog_groups )

    euler = np.array([0.0, 0.0, 0.0])
    r = R.from_euler('xyz', [[0, 0, 0]], degrees=True)

    pygame.init()
    screen = pygame.display.set_mode((pix_width, pix_height))
    pygame.event.set_grab(not pygame.event.get_grab())
    pygame.mouse.set_visible(False)
    i = 0
    while True:
        i = i + 1
        for event in pygame.event.get():
            if event.type == pygame.MOUSEMOTION:
                mov = pygame.mouse.get_rel()
                euler += np.array([-mov[1], mov[0], 0.0]) / 3.0
                r = R.from_euler('xyz', [euler], degrees=True)


        pix = launch( pipeline, sbt, acc_state.gas_handle, np.zeros(3), r )
        img = pygame.image.frombytes(pix.tobytes(), (pix_width, pix_height), "RGBA")
        screen.blit(img, (0, 0))
        pygame.display.flip()

        #acc_state.update(ctx)

    pygame.quit()


    #print( "Total number of log messages: {}".format( logger.num_mssgs ) )

    pix = pix.reshape( ( pix_height, pix_width, 4 ) )     # PIL expects [ y, x ] resolution
    img = ImageOps.flip( Image.fromarray( pix, 'RGBA' ) ) # PIL expects y = 0 at bottom
    img.show()
    img.save( 'my.png' )


if __name__ == "__main__":
    main()

You can find my complete main.py code in this gist.

Software versions:

➜  cudafe++ --version
cudafe: NVIDIA (R) Cuda Language Front End
Portions Copyright (c) 2005-2023 NVIDIA Corporation
Portions Copyright (c) 1988-2018 Edison Design Group Inc.
Based on Edison Design Group C/C++ Front End, version 6.3 (Jan  6 2023 16:45:23)
Cuda compilation tools, release 12.0, V12.0.140

OptiX SDK version: NVIDIA-OptiX-SDK-8.0.0-linux64-x86_64
PyOptiX bindings : got from git clone https://github.com/NVIDIA/otk-pyoptix

CMake Output from OptiX 8.0 compilation : 
-- The C compiler identification is GNU 12.3.0
-- The CXX compiler identification is GNU 12.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/gcc-12 - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/g++-12 - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Deprecation Warning at CMakeLists.txt:94 (cmake_policy):
  Compatibility with CMake < 3.5 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value or use a ...<max> suffix to tell
  CMake that the project does not need compatibility with older versions.


-- Performing Test OPTIX_CXX_ACCEPTS_NO_CPP
-- Performing Test OPTIX_CXX_ACCEPTS_NO_CPP - Success
-- Performing Test OPTIX_CXX_ACCEPTS_NO_UNUSED_RESULT
-- Performing Test OPTIX_CXX_ACCEPTS_NO_UNUSED_RESULT - Success
-- Performing Test SSE_41_AVAILABLE
-- Performing Test SSE_41_AVAILABLE - Success
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE
-- Found CUDA: /usr (found suitable version "12.0", minimum required is "5.0")
-- Unsetting values associated with OptiX code generation
-- Resetting CUDA_NVRTC_FLAGS
sutil OPTIXIR
ptx_files = /home/alex/softwares/NVIDIA-OptiX-SDK-8.0.0-linux64-x86_64/SDK/build/lib/ptx/./sutil_generated_camera.cu.optixir;/home/alex/softwares/NVIDIA-OptiX-SDK-8.0.0-linux64-x86_64/SDK/build/lib/ptx/./sutil_gen
erated_geometry.cu.optixir;/home/alex/softwares/NVIDIA-OptiX-SDK-8.0.0-linux64-x86_64/SDK/build/lib/ptx/./sutil_generated_shading.cu.optixir;/home/alex/softwares/NVIDIA-OptiX-SDK-8.0.0-linux64-x86_64/SDK/build/lib
/ptx/./sutil_generated_sphere.cu.optixir;/home/alex/softwares/NVIDIA-OptiX-SDK-8.0.0-linux64-x86_64/SDK/build/lib/ptx/./sutil_generated_whitted.cu.optixir
-- Found OpenGL: /usr/lib/x86_64-linux-gnu/libOpenGL.so
CMake Deprecation Warning at support/GLFW/CMakeLists.txt:5 (cmake_minimum_required):
  Compatibility with CMake < 3.5 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value or use a ...<max> suffix to tell
  CMake that the project does not need compatibility with older versions.


-- Found Vulkan: /usr/lib/x86_64-linux-gnu/libvulkan.so
-- Using X11 for window creation
-- Found X11: /usr/include
-- Looking for XOpenDisplay in /usr/lib/x86_64-linux-gnu/libX11.so;/usr/lib/x86_64-linux-gnu/libXext.so
-- Looking for XOpenDisplay in /usr/lib/x86_64-linux-gnu/libX11.so;/usr/lib/x86_64-linux-gnu/libXext.so - found
-- Looking for gethostbyname
-- Looking for gethostbyname - found
-- Looking for connect
-- Looking for connect - found
-- Looking for remove
-- Looking for remove - found
-- Looking for shmat
-- Looking for shmat - found
-- Configuring done (1.9s)
-- Generating done (0.1s)
-- Build files have been written to: /home/alex/softwares/NVIDIA-OptiX-SDK-8.0.0-linux64-x86_64/SDK/build

Would appreciate some competent help on fixing this, please let me know if I can give any additional information!

Thank you,
Alex

Hi,

Spent the last days trying to figure this out with no luck. Is there any way in which I could get help? I can provide any info if needed

Thank you!

Hi,

Under the suggestion of @droettger, I’ve uploaded here a minimal and complete reproducer for the issue. There’s ~three main changes from the main repo:

  • Added a few additional triangles. The artifacts do not appear with only one triangle.
  • Changed the uint_as_float calls to __uint_as_float calls to allow compilation on Cuda 12.0
  • Created an AccelState class to store acceleration state and temporary buffers, along with an update() function which, when called, creates the artefacts :

Without the update() calls:

With the update() calls:

This program runs a fresh Ubuntu 24.04 install running an Geforce RTX 2080.

  • Driver version 535.183.01
  • NVML version12.535.183.01

I was able to reproduce the behavior you see with your latest reproducer. I will take a look and report back.

I believe that the invalid vertices data warnings are the issue here. Interestingly, if I take your reproducer and just move the AccelState creation to below the pipeline creation, the original build gets invalid vertices as well. So any accel builds that occur after the pipeline create are corrupted somehow. Maybe a bug in pyoptix pipeline creation wrappers but I dont see anything yet. Still looking …

I found the issue. It is a bug in the original sample. When creating the BuildInputTriangleArray I specify the number of vertices as the following:
self.triangle_input.numVertices = len(self.vertices)
This is incorrect, as vertices is an array of floats (not float3s). It should be:
self.triangle_input.numVertices = len(self.vertices)//3
This appears to fix your simple reproducer. Could you please check that it fixes the issue in your full test case?

Hi,

I can confirm that it fixes the problem for my full test case. Thank you very much for the help!

Best regards,
Alex Toussaint