Backends and arrays

How the Dr.Jit and PyTorch backends of RayD differ in array types, layouts, ownership, devices, and available features.

RayD has two backends that share a vocabulary but nothing else. Pick the one that matches the framework your optimization or simulator already uses. This page explains what each backend expects and returns, and where their feature sets differ.

Separate runtimes, no crossing#

rayd.drjit and rayd.torch are separate native extensions. Each one owns its scene objects, GPU allocations, CUDA stream, OptiX pipelines, acceleration structures, and autodiff graph. A rayd.drjit.Scene cannot be passed to rayd.torch, and RayD never converts between Dr.Jit arrays and Torch tensors for you. An operation that one backend does not support fails there; it is never silently routed through the other backend.

The parent package rayd is a PEP 420 namespace. It does not pick a default backend, so always import one explicitly:

import rayd.drjit as rd   # Dr.Jit backend
import rayd.torch as rt   # PyTorch backend

Array conventions#

Dr.Jit (rayd.drjit)PyTorch (rayd.torch)
Positions, directionsdrjit.cuda.Array3f (structure of arrays: Array3f(xs, ys, zs))CUDA float32 tensor, shape (N, 3), contiguous
Triangle indicesdrjit.cuda.Array3iCUDA int32 tensor, shape (F, 3)
Scalars per raydrjit.cuda.Float, drjit.cuda.IntCUDA tensor, shape (N,)
Masksdrjit.cuda.BoolCUDA torch.bool tensor, shape (N,)
Differentiable inputsAD types from drjit.cuda.ad with dr.enable_gradtensors with requires_grad=True or forward-AD duals
RaysRay (detached) or RayAD (differentiable)one Ray class
Infinite raytmax defaults to inftmax=None (stored as an empty tensor)
Transformsdrjit.cuda.ad.Matrix4f(4, 4) float32 tensor
Resultsrecords of Dr.Jit arrays, flattened per lanefrozen dataclasses of tensors, with a batch dimension

Ray directions do not need to be normalized. The hit distance t is measured in units of the direction vector, so the hit point is always o + t * d. RayD has no unit system of its own: distances come out in the units of your vertex coordinates. RF operations take a wavelength in those same units.

Result layouts#

Dr.Jit results are flat arrays. Per-slot data from multi-result queries is laid out query-major: nearest_edges(point, k) returns query_count * k entries, and trace_reflections(..., symbolic=False) returns ray_count * max_bounces slots. Torch results keep the batch dimension instead: nearest_edges returns (N, k) tensors and trace_reflections returns (N, max_bounces) tensors.

Some Torch records carry fewer fields than their Dr.Jit counterparts. For example, rayd.torch.NearestPointEdge has no point or is_boundary field, and rayd.torch.ReflectionChain exposes only valid, t, image_sources, and prim_ids. The API reference lists the exact fields for each backend.

Devices and streams#

The Dr.Jit backend follows Dr.Jit's current-thread CUDA device. Select it with rd.set_device(index) before you create meshes, scenes, or arrays. Do not reuse scenes across a device switch.

The Torch backend follows the device of its input tensors and launches work on the current Torch CUDA stream. All tensors in one query must be on the same device. A scene built on cuda:1 answers queries correctly while cuda:0 is current. The Torch backend can also replicate one scene across several GPUs with Scene(devices=[...]); see Select devices and use several GPUs.

Feature differences#

Most query families exist in both backends. These are the differences in 0.8.0:

Only in rayd.drjitOnly in rayd.torch
Scene.shadow_testCamera pinhole helper
Scene.edge_info, edge_topology, edge_bvh_statsScene(devices=...), MultiDeviceOptions, calibrate_devices
Scene.mesh_face_offsets, mesh_edge_offsets, mesh_vertex_offsetsScene.set_instance_transform
Scene.triangle_edge_indices, edge_adjacent_facesSdfGridBatch and MixedScene.add_sdf_batch
Scene.set_mesh_transform, append_mesh_transformfree function sdf_intersect
Mesh.secondary_edges, Mesh.edge_indicesSurfelScene.transmittance
SurfelScene.render, SurfelAppearance, SurfelGeometrytorch.compile support
Scene.build_dfr_coherent_tx_states, build_dfr_coherent_higher_candidatesDfrPathLayout row layouts

Ask the backend what it supports#

Both backends report their capabilities at run time:

import rayd.drjit as rd
 
rd.backend_capabilities()        # flat dict: {"backend": "drjit", "intersect": True, ...}
rd.Scene().capabilities()        # Dr.Jit only: resolved trace/edge backends for this scene
rd.api_manifest()["derivatives"] # per-operation VJP/JVP status, see Differentiation

backend_capabilities()["reverse_ad"] and ["forward_ad"] mean the backend has AD support at all. They do not mean every operation supports both modes; use api_manifest() for that.

Dr.Jit integration modes#

In the Dr.Jit backend, many queries can run in two ways. The jit_symbolic mode records the query into Dr.Jit's traced program so it can fuse with surrounding Dr.Jit code. The eager_native mode launches RayD's own CUDA kernels immediately. Scene.capabilities()["integration"] lists the modes the resolved trace backend supports. The pure-CUDA trace backend runs eagerly only.

The symbolic argument of the Dr.Jit Scene.trace_reflections also changes the result type: symbolic=True (the default) returns a per-bounce ReflectionTrace, and symbolic=False returns a flat, ray-major ReflectionChain.