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 backendArray conventions#
Dr.Jit (rayd.drjit) | PyTorch (rayd.torch) | |
|---|---|---|
| Positions, directions | drjit.cuda.Array3f (structure of arrays: Array3f(xs, ys, zs)) | CUDA float32 tensor, shape (N, 3), contiguous |
| Triangle indices | drjit.cuda.Array3i | CUDA int32 tensor, shape (F, 3) |
| Scalars per ray | drjit.cuda.Float, drjit.cuda.Int | CUDA tensor, shape (N,) |
| Masks | drjit.cuda.Bool | CUDA torch.bool tensor, shape (N,) |
| Differentiable inputs | AD types from drjit.cuda.ad with dr.enable_grad | tensors with requires_grad=True or forward-AD duals |
| Rays | Ray (detached) or RayAD (differentiable) | one Ray class |
| Infinite ray | tmax defaults to inf | tmax=None (stored as an empty tensor) |
| Transforms | drjit.cuda.ad.Matrix4f | (4, 4) float32 tensor |
| Results | records of Dr.Jit arrays, flattened per lane | frozen 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.drjit | Only in rayd.torch |
|---|---|
Scene.shadow_test | Camera pinhole helper |
Scene.edge_info, edge_topology, edge_bvh_stats | Scene(devices=...), MultiDeviceOptions, calibrate_devices |
Scene.mesh_face_offsets, mesh_edge_offsets, mesh_vertex_offsets | Scene.set_instance_transform |
Scene.triangle_edge_indices, edge_adjacent_faces | SdfGridBatch and MixedScene.add_sdf_batch |
Scene.set_mesh_transform, append_mesh_transform | free function sdf_intersect |
Mesh.secondary_edges, Mesh.edge_indices | SurfelScene.transmittance |
SurfelScene.render, SurfelAppearance, SurfelGeometry | torch.compile support |
Scene.build_dfr_coherent_tx_states, build_dfr_coherent_higher_candidates | DfrPathLayout 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 Differentiationbackend_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.