phanerozoic commited on
Commit
de4d6ae
·
verified ·
1 Parent(s): 51cee60

pathtracer-diff v1: differentiable path tracer source tree

Browse files
CARD.md ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: kernels
3
+ license: apache-2.0
4
+ ---
5
+
6
+ # pathtracer-diff
7
+
8
+ A differentiable Monte Carlo path tracer as one kernel, loadable through
9
+ `kernels`. The forward pass renders a linear-radiance image with a megakernel
10
+ unidirectional path tracer (diffuse + emissive materials, cosine-weighted BRDF
11
+ sampling, optional next-event estimation, BVH traversal); the backward pass
12
+ replays the identical paths with the identical counter-based random draws and
13
+ emits analytic gradients to per-material albedo and emission. Inverse
14
+ rendering then runs as a bare torch loop around the kernel, with no rendering
15
+ framework in the loop: render, compare to a target, `backward()`, step.
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ import torch
21
+ from kernels import get_kernel
22
+
23
+ ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
24
+
25
+ scene = ptd.Scene(vertices, faces, material_ids,
26
+ albedo=albedo, # [M, 3] float32, may require grad
27
+ emission=emission) # [M, 3] float32, may require grad
28
+ cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
29
+ vfov_deg=39.0)
30
+
31
+ img = ptd.render(scene, cam, 512, 512, spp=64, max_bounces=4) # [H, W, 3] linear
32
+ loss = (img - target).square().mean()
33
+ loss.backward() # gradients on scene.albedo / scene.emission
34
+ ```
35
+
36
+ `version` selects the release branch; `trust_remote_code` is required by
37
+ `kernels` for publishers without the trusted-publisher mark.
38
+
39
+ A fixed `seed` renders with the same paths every call, so the Monte Carlo
40
+ objective is a deterministic function of the parameters and gradient descent
41
+ sees a smooth landscape at any spp; that is the configuration an
42
+ inverse-rendering loop wants.
43
+
44
+ ## API
45
+
46
+ | Symbol | Purpose |
47
+ |---|---|
48
+ | `Scene(vertices, faces, material_ids, albedo, emission)` | triangle-mesh scene; the BVH and the emissive-face light list are built at construction |
49
+ | `Camera(position, look_at, up, vfov_deg)` | pinhole camera |
50
+ | `render(scene, camera, height, width, spp, max_bounces, nee, seed)` | differentiable render to `[H, W, 3]` f32 linear radiance; autograd to `scene.albedo` and `scene.emission` |
51
+ | `ops.pt_forward` / `ops.pt_backward` | raw kernel launches |
52
+
53
+ ## How it works
54
+
55
+ One thread owns one pixel and accumulates its spp samples serially, so the
56
+ image is bitwise deterministic with no atomics in the forward pass. Each
57
+ sample is a unidirectional path: cosine-weighted hemisphere sampling makes
58
+ the diffuse throughput multiplier exactly the albedo per bounce, and with
59
+ next-event estimation on, emission is gathered at the camera hit while every
60
+ later vertex estimates direct light by area sampling over the emissive-face
61
+ list. NEE is skipped at the final vertex so the NEE and BRDF-only estimators
62
+ cover camera-to-light paths of the same maximum length and agree in
63
+ expectation at any bounce budget, not just in the limit.
64
+
65
+ The backward pass is exact path replay. The random stream is counter-based
66
+ Philox keyed by `(seed, pixel, sample)`, and sampling directions depend only
67
+ on geometry and that stream, never on albedo or emission, so the backward
68
+ kernel re-traces the identical paths with the identical draws and no stored
69
+ path state. Each radiance term is a product of per-bounce albedo factors, a
70
+ geometry scalar, and an emission value; the replay differentiates it in
71
+ closed form with prefix/suffix exclusion products (no division, so zero
72
+ albedos are safe) and accumulates into per-material gradient buffers through
73
+ block-shared memory. Parameter gradients are exactly the derivative of the
74
+ rendered estimate: interior terms only, no visibility/silhouette gradients,
75
+ which is the correct and complete derivative for materials and emission under
76
+ fixed geometry.
77
+
78
+ ## Correctness
79
+
80
+ Measured on RTX 6000 Ada (sm89) from a source build:
81
+
82
+ - **Analytic furnace, zero variance.** In a closed uniform box with BRDF
83
+ sampling, every path returns exactly `E * sum a^k` independent of the
84
+ random walk; at `a = 0.5`, `E = 0.25`, 8 bounces the analytic value is
85
+ 0.498046875 and the worst pixel deviates 3.4e-3 (64x64, 4 spp).
86
+ - **Estimator agreement.** The NEE and BRDF-only estimators agree to 0.10%
87
+ on a diffuse room with an area light (128x128; 128 vs 512 spp).
88
+ - **Gradients match finite differences.** With a fixed seed the loss is
89
+ smooth in the parameters along the same paths; analytic replay gradients
90
+ match central differences to a maximum relative error of 3.0e-4 over nine
91
+ albedo and emission entries in a Cornell scene.
92
+ - **Inverse rendering.** Recovering a wall albedo (0.2, 0.5, 0.7) from a
93
+ rendered target by Adam through the kernel reaches a maximum absolute
94
+ error of 0.003 in 80 steps, driving the image loss from 2.6e-4 to 4.1e-8.
95
+ - **Deterministic.** Repeated forward renders are bitwise identical
96
+ (`torch.equal`); gradient buffers accumulate through float atomics and are
97
+ deterministic in expectation but not bitwise.
98
+
99
+ ## Measured
100
+
101
+ RTX 6000 Ada, Cornell box with an interior blocker (24 triangles, 4
102
+ materials), 512x512, 64 spp, 4 bounces, NEE on:
103
+
104
+ | pass | time | rate |
105
+ |---|---|---|
106
+ | forward | 8.7 ms | 1.94 Gpaths/s |
107
+ | backward (replay) | 20.1 ms | 2.32x forward |
108
+
109
+ ## Requirements and limits
110
+
111
+ - NVIDIA GPU with compute capability 8.0+; float32 throughout.
112
+ - Diffuse (Lambertian) + emissive materials; pinhole camera; at most 64
113
+ materials and 16 bounces; two-sided surfaces.
114
+ - Differentiable parameters are per-material albedo and emission only.
115
+ Geometry, camera, and per-vertex quantities are not differentiable inputs,
116
+ and there are no visibility/silhouette gradients.
117
+ - Published variants are Linux x86_64; on Windows `load_local.py` JIT-builds
118
+ the same source and exposes the identical API.
119
+
120
+ ## References
121
+
122
+ Kajiya, "The Rendering Equation" (SIGGRAPH 1986); Vicini, Speierer, Jakob,
123
+ "Path Replay Backpropagation" (SIGGRAPH 2021); Nimier-David, Speierer,
124
+ Ruiz, Jakob, "Radiative Backpropagation" (SIGGRAPH 2020); Möller and
125
+ Trumbore, "Fast, Minimum Storage Ray-Triangle Intersection" (1997); Duff et
126
+ al., "Building an Orthonormal Basis, Revisited" (JCGT 2017).
127
+
128
+ ## License
129
+
130
+ Apache-2.0.
README.md ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: kernels
3
+ license: apache-2.0
4
+ ---
5
+
6
+ # pathtracer-diff
7
+
8
+ A differentiable Monte Carlo path tracer as one kernel, loadable through
9
+ `kernels`. The forward pass renders a linear-radiance image with a megakernel
10
+ unidirectional path tracer (diffuse + emissive materials, cosine-weighted BRDF
11
+ sampling, optional next-event estimation, BVH traversal); the backward pass
12
+ replays the identical paths with the identical counter-based random draws and
13
+ emits analytic gradients to per-material albedo and emission. Inverse
14
+ rendering then runs as a bare torch loop around the kernel, with no rendering
15
+ framework in the loop: render, compare to a target, `backward()`, step.
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ import torch
21
+ from kernels import get_kernel
22
+
23
+ ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
24
+
25
+ scene = ptd.Scene(vertices, faces, material_ids,
26
+ albedo=albedo, # [M, 3] float32, may require grad
27
+ emission=emission) # [M, 3] float32, may require grad
28
+ cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
29
+ vfov_deg=39.0)
30
+
31
+ img = ptd.render(scene, cam, 512, 512, spp=64, max_bounces=4) # [H, W, 3] linear
32
+ loss = (img - target).square().mean()
33
+ loss.backward() # gradients on scene.albedo / scene.emission
34
+ ```
35
+
36
+ `version` selects the release branch; `trust_remote_code` is required by
37
+ `kernels` for publishers without the trusted-publisher mark.
38
+
39
+ A fixed `seed` renders with the same paths every call, so the Monte Carlo
40
+ objective is a deterministic function of the parameters and gradient descent
41
+ sees a smooth landscape at any spp; that is the configuration an
42
+ inverse-rendering loop wants.
43
+
44
+ ## API
45
+
46
+ | Symbol | Purpose |
47
+ |---|---|
48
+ | `Scene(vertices, faces, material_ids, albedo, emission)` | triangle-mesh scene; the BVH and the emissive-face light list are built at construction |
49
+ | `Camera(position, look_at, up, vfov_deg)` | pinhole camera |
50
+ | `render(scene, camera, height, width, spp, max_bounces, nee, seed)` | differentiable render to `[H, W, 3]` f32 linear radiance; autograd to `scene.albedo` and `scene.emission` |
51
+ | `ops.pt_forward` / `ops.pt_backward` | raw kernel launches |
52
+
53
+ ## How it works
54
+
55
+ One thread owns one pixel and accumulates its spp samples serially, so the
56
+ image is bitwise deterministic with no atomics in the forward pass. Each
57
+ sample is a unidirectional path: cosine-weighted hemisphere sampling makes
58
+ the diffuse throughput multiplier exactly the albedo per bounce, and with
59
+ next-event estimation on, emission is gathered at the camera hit while every
60
+ later vertex estimates direct light by area sampling over the emissive-face
61
+ list. NEE is skipped at the final vertex so the NEE and BRDF-only estimators
62
+ cover camera-to-light paths of the same maximum length and agree in
63
+ expectation at any bounce budget, not just in the limit.
64
+
65
+ The backward pass is exact path replay. The random stream is counter-based
66
+ Philox keyed by `(seed, pixel, sample)`, and sampling directions depend only
67
+ on geometry and that stream, never on albedo or emission, so the backward
68
+ kernel re-traces the identical paths with the identical draws and no stored
69
+ path state. Each radiance term is a product of per-bounce albedo factors, a
70
+ geometry scalar, and an emission value; the replay differentiates it in
71
+ closed form with prefix/suffix exclusion products (no division, so zero
72
+ albedos are safe) and accumulates into per-material gradient buffers through
73
+ block-shared memory. Parameter gradients are exactly the derivative of the
74
+ rendered estimate: interior terms only, no visibility/silhouette gradients,
75
+ which is the correct and complete derivative for materials and emission under
76
+ fixed geometry.
77
+
78
+ ## Correctness
79
+
80
+ Measured on RTX 6000 Ada (sm89) from a source build:
81
+
82
+ - **Analytic furnace, zero variance.** In a closed uniform box with BRDF
83
+ sampling, every path returns exactly `E * sum a^k` independent of the
84
+ random walk; at `a = 0.5`, `E = 0.25`, 8 bounces the analytic value is
85
+ 0.498046875 and the worst pixel deviates 3.4e-3 (64x64, 4 spp).
86
+ - **Estimator agreement.** The NEE and BRDF-only estimators agree to 0.10%
87
+ on a diffuse room with an area light (128x128; 128 vs 512 spp).
88
+ - **Gradients match finite differences.** With a fixed seed the loss is
89
+ smooth in the parameters along the same paths; analytic replay gradients
90
+ match central differences to a maximum relative error of 3.0e-4 over nine
91
+ albedo and emission entries in a Cornell scene.
92
+ - **Inverse rendering.** Recovering a wall albedo (0.2, 0.5, 0.7) from a
93
+ rendered target by Adam through the kernel reaches a maximum absolute
94
+ error of 0.003 in 80 steps, driving the image loss from 2.6e-4 to 4.1e-8.
95
+ - **Deterministic.** Repeated forward renders are bitwise identical
96
+ (`torch.equal`); gradient buffers accumulate through float atomics and are
97
+ deterministic in expectation but not bitwise.
98
+
99
+ ## Measured
100
+
101
+ RTX 6000 Ada, Cornell box with an interior blocker (24 triangles, 4
102
+ materials), 512x512, 64 spp, 4 bounces, NEE on:
103
+
104
+ | pass | time | rate |
105
+ |---|---|---|
106
+ | forward | 8.7 ms | 1.94 Gpaths/s |
107
+ | backward (replay) | 20.1 ms | 2.32x forward |
108
+
109
+ ## Requirements and limits
110
+
111
+ - NVIDIA GPU with compute capability 8.0+; float32 throughout.
112
+ - Diffuse (Lambertian) + emissive materials; pinhole camera; at most 64
113
+ materials and 16 bounces; two-sided surfaces.
114
+ - Differentiable parameters are per-material albedo and emission only.
115
+ Geometry, camera, and per-vertex quantities are not differentiable inputs,
116
+ and there are no visibility/silhouette gradients.
117
+ - Published variants are Linux x86_64; on Windows `load_local.py` JIT-builds
118
+ the same source and exposes the identical API.
119
+
120
+ ## References
121
+
122
+ Kajiya, "The Rendering Equation" (SIGGRAPH 1986); Vicini, Speierer, Jakob,
123
+ "Path Replay Backpropagation" (SIGGRAPH 2021); Nimier-David, Speierer,
124
+ Ruiz, Jakob, "Radiative Backpropagation" (SIGGRAPH 2020); Möller and
125
+ Trumbore, "Fast, Minimum Storage Ray-Triangle Intersection" (1997); Duff et
126
+ al., "Building an Orthonormal Basis, Revisited" (JCGT 2017).
127
+
128
+ ## License
129
+
130
+ Apache-2.0.
build.toml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [general]
2
+ name = "pathtracer-diff"
3
+ version = 1
4
+ edition = 5
5
+ license = "Apache-2.0"
6
+ backends = [
7
+ "cuda",
8
+ ]
9
+
10
+ [general.hub]
11
+ repo-id = "phanerozoic/pathtracer-diff"
12
+
13
+ [torch]
14
+ src = [
15
+ "torch-ext/torch_binding.cpp",
16
+ "torch-ext/torch_binding.h",
17
+ ]
18
+
19
+ [kernel.pathtracer-diff]
20
+ backend = "cuda"
21
+ cuda-capabilities = ["8.0", "8.6", "8.9", "9.0", "10.0", "12.0"]
22
+ depends = ["torch"]
23
+ src = [
24
+ "pathtracer_diff_cuda/pathtracer.cu",
25
+ "pathtracer_diff_cuda/pathtracer_launch.h",
26
+ ]
dev/report_local.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Measured numbers for the card, run on the local GPU from source."""
2
+ import os
3
+ import sys
4
+ import time
5
+
6
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7
+ sys.path.insert(0, ROOT)
8
+
9
+ import torch
10
+
11
+ import load_local
12
+
13
+ ptd = load_local.load()
14
+
15
+ print("GPU:", torch.cuda.get_device_name(0))
16
+
17
+
18
+ def quad(a, b, c, d):
19
+ return [[a, b, c], [a, c, d]]
20
+
21
+
22
+ def add_box(verts, faces, mat, lo, hi, mat_id):
23
+ x0, y0, z0 = lo
24
+ x1, y1, z1 = hi
25
+ base = len(verts)
26
+ verts.extend([(x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1),
27
+ (x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)])
28
+ i = lambda k: base + k
29
+ for q in [quad(i(0), i(1), i(2), i(3)), quad(i(4), i(7), i(6), i(5)),
30
+ quad(i(0), i(4), i(5), i(1)), quad(i(3), i(2), i(6), i(7)),
31
+ quad(i(0), i(3), i(7), i(4)), quad(i(1), i(5), i(6), i(2))]:
32
+ faces.extend(q)
33
+ mat.extend([mat_id, mat_id])
34
+
35
+
36
+ def cornell(with_box=True):
37
+ verts, faces, mat = [], [], []
38
+
39
+ def wall(a, b, c, d, m):
40
+ base = len(verts)
41
+ verts.extend([a, b, c, d])
42
+ faces.extend(quad(base, base + 1, base + 2, base + 3))
43
+ mat.extend([m, m])
44
+
45
+ X, Y, Z = 5.56, 5.488, 5.592
46
+ wall((0, 0, 0), (X, 0, 0), (X, 0, Z), (0, 0, Z), 0)
47
+ wall((0, Y, 0), (0, Y, Z), (X, Y, Z), (X, Y, 0), 0)
48
+ wall((0, 0, Z), (X, 0, Z), (X, Y, Z), (0, Y, Z), 0)
49
+ wall((0, 0, 0), (0, 0, Z), (0, Y, Z), (0, Y, 0), 1)
50
+ wall((X, 0, 0), (X, Y, 0), (X, Y, Z), (X, 0, Z), 2)
51
+ wall((2.13, Y - 0.001, 2.27), (3.43, Y - 0.001, 2.27),
52
+ (3.43, Y - 0.001, 3.32), (2.13, Y - 0.001, 3.32), 3)
53
+ if with_box:
54
+ add_box(verts, faces, mat, (1.3, 0.0, 0.65), (2.4, 1.65, 1.7), 0)
55
+ albedo = [[0.73, 0.73, 0.73], [0.65, 0.05, 0.05], [0.12, 0.45, 0.15],
56
+ [0.78, 0.78, 0.78]]
57
+ emission = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [18.4, 15.6, 8.0]]
58
+ return ptd.Scene(verts, faces, mat, albedo=albedo, emission=emission)
59
+
60
+
61
+ cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
62
+ vfov_deg=39.0)
63
+
64
+ # ---- furnace exactness
65
+ a, E, B = 0.5, 0.25, 8
66
+ verts, faces, mat = [], [], []
67
+ add_box(verts, faces, mat, (-2, -2, -2), (2, 2, 2), 0)
68
+ fs = ptd.Scene(verts, faces, mat, albedo=[[a] * 3], emission=[[E] * 3])
69
+ fc = ptd.Camera(position=(0, 0, 0), look_at=(0, 0, 1), vfov_deg=60.0)
70
+ img = ptd.render(fs, fc, 64, 64, spp=4, max_bounces=B, nee=False, seed=0)
71
+ exp = E * (1 - a ** B) / (1 - a)
72
+ print(f"furnace: analytic {exp:.9f}, max abs err {(img - exp).abs().max().item():.2e}")
73
+
74
+ # ---- estimator agreement
75
+ verts, faces, mat = [], [], []
76
+ add_box(verts, faces, mat, (0, 0, 0), (5, 5, 5), 0)
77
+ base = len(verts)
78
+ verts.extend([(1.0, 4.999, 1.0), (4.5, 4.999, 1.0), (4.5, 4.999, 4.5),
79
+ (1.0, 4.999, 4.5)])
80
+ faces.extend(quad(base, base + 1, base + 2, base + 3))
81
+ mat.extend([1, 1])
82
+ es = ptd.Scene(verts, faces, mat, albedo=[[0.6] * 3, [0.0] * 3],
83
+ emission=[[0] * 3, [3.0] * 3])
84
+ ec = ptd.Camera(position=(2.5, 2.5, 0.2), look_at=(2.5, 2.5, 5.0), vfov_deg=70.0)
85
+ m_nee = ptd.render(es, ec, 128, 128, spp=128, max_bounces=4, nee=True, seed=1).mean().item()
86
+ m_brdf = ptd.render(es, ec, 128, 128, spp=512, max_bounces=4, nee=False, seed=2).mean().item()
87
+ print(f"estimators: NEE {m_nee:.5f} vs BRDF {m_brdf:.5f}, rel diff {abs(m_nee-m_brdf)/m_brdf:.4%}")
88
+
89
+ # ---- FD gradient agreement
90
+ scene = cornell(with_box=False)
91
+ H = W = 48
92
+ torch.manual_seed(0)
93
+ Wr = torch.rand(H, W, 3, device="cuda")
94
+
95
+
96
+ def loss_of(alb, emi):
97
+ scene.albedo = alb
98
+ scene.emission = emi
99
+ return (ptd.render(scene, cam, H, W, spp=8, max_bounces=3, seed=7) * Wr).sum()
100
+
101
+
102
+ alb0 = scene.albedo.clone()
103
+ emi0 = scene.emission.clone()
104
+ alb = alb0.clone().requires_grad_(True)
105
+ emi = emi0.clone().requires_grad_(True)
106
+ loss_of(alb, emi).backward()
107
+ worst = 0.0
108
+ for (i, c) in [(0, 0), (0, 1), (0, 2), (1, 0), (2, 1), (2, 2)]:
109
+ h = 2e-3
110
+ ap = alb0.clone(); ap[i, c] += h
111
+ am = alb0.clone(); am[i, c] -= h
112
+ fd = (loss_of(ap, emi0) - loss_of(am, emi0)).item() / (2 * h)
113
+ worst = max(worst, abs(alb.grad[i, c].item() - fd) / abs(fd))
114
+ h = 0.05
115
+ for c in range(3):
116
+ ep = emi0.clone(); ep[3, c] += h
117
+ em = emi0.clone(); em[3, c] -= h
118
+ fd = (loss_of(alb0, ep) - loss_of(alb0, em)).item() / (2 * h)
119
+ worst = max(worst, abs(emi.grad[3, c].item() - fd) / abs(fd))
120
+ print(f"gradients vs central differences: max rel err {worst:.2e} over 9 entries")
121
+
122
+ # ---- inverse rendering
123
+ target_row = torch.tensor([0.2, 0.5, 0.7], device="cuda")
124
+ scene = cornell(with_box=False)
125
+ t_alb = scene.albedo.clone(); t_alb[1] = target_row
126
+ scene.albedo = t_alb
127
+ target = ptd.render(scene, cam, 48, 48, spp=8, max_bounces=3, seed=3).detach()
128
+ alb = t_alb.clone(); alb[1] = torch.tensor([0.5, 0.5, 0.5], device="cuda")
129
+ alb = alb.requires_grad_(True)
130
+ opt = torch.optim.Adam([alb], lr=0.05)
131
+ first = None
132
+ for it in range(80):
133
+ opt.zero_grad()
134
+ scene.albedo = alb
135
+ loss = (ptd.render(scene, cam, 48, 48, spp=8, max_bounces=3, seed=3) - target).square().mean()
136
+ loss.backward()
137
+ alb.grad[0] = 0; alb.grad[2] = 0; alb.grad[3] = 0
138
+ opt.step()
139
+ with torch.no_grad():
140
+ alb.clamp_(0.02, 0.98)
141
+ if first is None:
142
+ first = loss.item()
143
+ print(f"inverse: target {target_row.tolist()} recovered "
144
+ f"{[round(x, 3) for x in alb[1].detach().tolist()]}, "
145
+ f"max abs err {(alb[1].detach() - target_row).abs().max().item():.3f}, "
146
+ f"loss {first:.2e} -> {loss.item():.2e} in 80 steps")
147
+
148
+ # ---- throughput
149
+ scene = cornell(with_box=True)
150
+ H, W, spp, B = 512, 512, 64, 4
151
+ img = ptd.render(scene, cam, H, W, spp=spp, max_bounces=B) # warmup + autograd off
152
+ torch.cuda.synchronize()
153
+ ts = []
154
+ for _ in range(3):
155
+ t0 = time.perf_counter()
156
+ img = ptd.render(scene, cam, H, W, spp=spp, max_bounces=B)
157
+ torch.cuda.synchronize()
158
+ ts.append(time.perf_counter() - t0)
159
+ fwd = sorted(ts)[1]
160
+ paths = H * W * spp
161
+ print(f"forward: {H}x{W} spp {spp} bounces {B} ({scene.n_faces} tris): "
162
+ f"{fwd*1e3:.1f} ms, {paths/fwd/1e6:.0f} Mpaths/s")
163
+
164
+ alb = scene.albedo.clone().requires_grad_(True)
165
+ scene.albedo = alb
166
+ g = torch.ones(H, W, 3, device="cuda")
167
+ img = ptd.render(scene, cam, H, W, spp=spp, max_bounces=B)
168
+ img.backward(g) # warmup
169
+ torch.cuda.synchronize()
170
+ ts = []
171
+ for _ in range(3):
172
+ alb.grad = None
173
+ img = ptd.render(scene, cam, H, W, spp=spp, max_bounces=B)
174
+ torch.cuda.synchronize()
175
+ t0 = time.perf_counter()
176
+ img.backward(g)
177
+ torch.cuda.synchronize()
178
+ ts.append(time.perf_counter() - t0)
179
+ bwd = sorted(ts)[1]
180
+ print(f"backward (replay): {bwd*1e3:.1f} ms, {bwd/fwd:.2f}x forward")
dev/run_local.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build the extension locally and run the test suite against it, stubbing
2
+ kernels.get_kernel so the published-variant tests run natively."""
3
+ import os
4
+ import sys
5
+
6
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7
+ sys.path.insert(0, ROOT)
8
+
9
+ import load_local
10
+
11
+ mod = load_local.load(verbose="-v" in sys.argv)
12
+
13
+ import kernels
14
+
15
+ kernels.get_kernel = lambda *a, **k: mod
16
+
17
+ import pytest
18
+
19
+ sys.exit(pytest.main([os.path.join(ROOT, "tests"), "-x", "-q", "-p",
20
+ "no:cacheprovider"]))
flake.nix ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ description = "Flake for pathtracer-diff kernel";
3
+
4
+ inputs = {
5
+ builder.url = "github:huggingface/kernels";
6
+ };
7
+
8
+ outputs =
9
+ {
10
+ self,
11
+ builder,
12
+ }:
13
+ builder.lib.genKernelFlakeOutputs {
14
+ inherit self;
15
+ path = ./.;
16
+ };
17
+ }
load_local.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load pathtracer-diff natively on this machine (Windows/SAURON included).
2
+
3
+ The published Hugging Face kernel carries `*-linux` variants only, so on
4
+ Windows `get_kernel` has no matching build. The source is right here, though:
5
+ JIT-build it with `torch.utils.cpp_extension` and expose the identical
6
+ `Scene` / `Camera` / `render` API.
7
+
8
+ import load_local
9
+ ptd = load_local.load()
10
+ """
11
+ import os
12
+ import sys
13
+ import types
14
+
15
+ _cached = None
16
+
17
+
18
+ def load(verbose=False):
19
+ """Return the pathtracer_diff module, JIT-built from the local source."""
20
+ global _cached
21
+ if _cached is not None:
22
+ return _cached
23
+ from torch.utils.cpp_extension import load as _jit
24
+
25
+ root = os.path.dirname(os.path.abspath(__file__))
26
+ cuda_dir = os.path.join(root, "pathtracer_diff_cuda")
27
+ ext = _jit(name="pathtracer_diff_ext",
28
+ sources=[os.path.join(root, "local_bind.cpp"),
29
+ os.path.join(cuda_dir, "pathtracer.cu")],
30
+ extra_include_paths=[cuda_dir],
31
+ verbose=verbose)
32
+ ops_mod = types.ModuleType("pathtracer_diff._ops")
33
+ ops_mod.ops = type("_Ops", (), {
34
+ "pt_forward": staticmethod(ext.pt_forward),
35
+ "pt_backward": staticmethod(ext.pt_backward),
36
+ })()
37
+ sys.modules["pathtracer_diff._ops"] = ops_mod
38
+ sys.path.insert(0, os.path.join(root, "torch-ext"))
39
+ import pathtracer_diff
40
+ _cached = pathtracer_diff
41
+ return pathtracer_diff
local_bind.cpp ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Local development binding (torch.utils.cpp_extension JIT; Windows/MSVC).
2
+ // Mirrors the torch-ext binding's argument order exactly.
3
+ #include <torch/extension.h>
4
+
5
+ #include <ATen/cuda/CUDAContext.h>
6
+ #include <c10/cuda/CUDAGuard.h>
7
+
8
+ #include "pathtracer_launch.h"
9
+
10
+ namespace {
11
+
12
+ void pt_forward(torch::Tensor tris, torch::Tensor mat_ids,
13
+ torch::Tensor nodes_f, torch::Tensor nodes_i,
14
+ torch::Tensor light_faces, torch::Tensor light_cdf,
15
+ double total_light_area, torch::Tensor albedo,
16
+ torch::Tensor emission, torch::Tensor cam, int64_t spp,
17
+ int64_t max_bounces, bool nee, int64_t seed,
18
+ torch::Tensor image) {
19
+ TORCH_CHECK(tris.is_cuda() && tris.is_contiguous(), "tris");
20
+ TORCH_CHECK(image.is_cuda() && image.is_contiguous() && image.dim() == 3,
21
+ "image [H, W, 3]");
22
+ TORCH_CHECK(albedo.size(0) <= 64, "at most 64 materials");
23
+ TORCH_CHECK(max_bounces >= 1 && max_bounces <= 16, "bounces in [1,16]");
24
+ const at::cuda::CUDAGuard guard(tris.device());
25
+ cudaStream_t stream = at::cuda::getCurrentCUDAStream();
26
+ int L = (int)light_faces.numel();
27
+ torch::Tensor cam_h =
28
+ cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
29
+ ptd_forward_launch(
30
+ tris.const_data_ptr<float>(), mat_ids.const_data_ptr<int>(),
31
+ (int)tris.size(0), nodes_f.const_data_ptr<float>(),
32
+ nodes_i.const_data_ptr<int>(), (int)nodes_f.size(0),
33
+ L ? light_faces.const_data_ptr<int>() : nullptr,
34
+ L ? light_cdf.const_data_ptr<float>() : nullptr, L,
35
+ (float)total_light_area, albedo.const_data_ptr<float>(),
36
+ emission.const_data_ptr<float>(), (int)albedo.size(0),
37
+ cam_h.const_data_ptr<float>(), (int)image.size(0), (int)image.size(1),
38
+ (int)spp, (int)max_bounces, nee ? 1 : 0, (long long)seed,
39
+ image.data_ptr<float>(), stream);
40
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
41
+ }
42
+
43
+ void pt_backward(torch::Tensor tris, torch::Tensor mat_ids,
44
+ torch::Tensor nodes_f, torch::Tensor nodes_i,
45
+ torch::Tensor light_faces, torch::Tensor light_cdf,
46
+ double total_light_area, torch::Tensor albedo,
47
+ torch::Tensor emission, torch::Tensor cam, int64_t spp,
48
+ int64_t max_bounces, bool nee, int64_t seed,
49
+ torch::Tensor grad_image, torch::Tensor grad_albedo,
50
+ torch::Tensor grad_emission) {
51
+ TORCH_CHECK(grad_image.is_cuda() && grad_image.is_contiguous(), "grad_image");
52
+ TORCH_CHECK(albedo.size(0) <= 64, "at most 64 materials");
53
+ const at::cuda::CUDAGuard guard(tris.device());
54
+ cudaStream_t stream = at::cuda::getCurrentCUDAStream();
55
+ int L = (int)light_faces.numel();
56
+ torch::Tensor cam_h =
57
+ cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
58
+ ptd_backward_launch(
59
+ tris.const_data_ptr<float>(), mat_ids.const_data_ptr<int>(),
60
+ (int)tris.size(0), nodes_f.const_data_ptr<float>(),
61
+ nodes_i.const_data_ptr<int>(), (int)nodes_f.size(0),
62
+ L ? light_faces.const_data_ptr<int>() : nullptr,
63
+ L ? light_cdf.const_data_ptr<float>() : nullptr, L,
64
+ (float)total_light_area, albedo.const_data_ptr<float>(),
65
+ emission.const_data_ptr<float>(), (int)albedo.size(0),
66
+ cam_h.const_data_ptr<float>(), (int)grad_image.size(0),
67
+ (int)grad_image.size(1), (int)spp, (int)max_bounces, nee ? 1 : 0,
68
+ (long long)seed, grad_image.const_data_ptr<float>(),
69
+ grad_albedo.data_ptr<float>(), grad_emission.data_ptr<float>(), stream);
70
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
71
+ }
72
+
73
+ } // namespace
74
+
75
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
76
+ m.def("pt_forward", &pt_forward, "pathtracer-diff forward");
77
+ m.def("pt_backward", &pt_backward, "pathtracer-diff backward");
78
+ }
pathtracer_diff_cuda/pathtracer.cu ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // pathtracer-diff: a differentiable Monte Carlo path tracer as one kernel.
2
+ //
3
+ // Forward: a megakernel unidirectional path tracer (diffuse + emissive
4
+ // materials, cosine-weighted BRDF sampling, optional next-event estimation
5
+ // against an area-sampled light list, BVH traversal). One thread owns one
6
+ // pixel and accumulates its spp samples serially, so the image is bitwise
7
+ // deterministic.
8
+ //
9
+ // Backward: exact path replay. Sampling directions depend only on geometry
10
+ // and the counter-based Philox stream, never on the differentiable
11
+ // parameters (per-material albedo and emission), so the backward kernel
12
+ // re-traces the identical paths with the identical RNG draws and
13
+ // accumulates dLoss/dAlbedo and dLoss/dEmission analytically, storing no
14
+ // path state between the passes (path-replay backpropagation, restricted
15
+ // to interior terms: no visibility/silhouette gradients).
16
+ //
17
+ // Radiance estimator with NEE on: emission is gathered at the camera hit
18
+ // only; every later vertex estimates direct light by area sampling, so no
19
+ // contribution is double-counted. With NEE off: emission is gathered at
20
+ // every hit (with cosine sampling the diffuse throughput factor is exactly
21
+ // the albedo, which makes a closed uniform furnace a zero-variance analytic
22
+ // test case).
23
+
24
+ #include <cuda_runtime.h>
25
+ #include <curand_kernel.h>
26
+
27
+ #include <cstdint>
28
+
29
+ #include "pathtracer_launch.h"
30
+
31
+ namespace {
32
+
33
+ constexpr int kThreads = 128;
34
+ constexpr int kMaxBounces = 16;
35
+ constexpr int kMaxMats = 64;
36
+ constexpr int kStack = 64;
37
+ constexpr float kInvPi = 0.31830988618379067154f;
38
+ constexpr float kRayEps = 1e-4f;
39
+ constexpr float kShadowEps = 1e-3f;
40
+
41
+ // ---------------------------------------------------------------- float3 ops
42
+ __device__ __forceinline__ float3 f3(float x, float y, float z) {
43
+ return make_float3(x, y, z);
44
+ }
45
+ __device__ __forceinline__ float3 operator+(float3 a, float3 b) {
46
+ return f3(a.x + b.x, a.y + b.y, a.z + b.z);
47
+ }
48
+ __device__ __forceinline__ float3 operator-(float3 a, float3 b) {
49
+ return f3(a.x - b.x, a.y - b.y, a.z - b.z);
50
+ }
51
+ __device__ __forceinline__ float3 operator*(float3 a, float s) {
52
+ return f3(a.x * s, a.y * s, a.z * s);
53
+ }
54
+ __device__ __forceinline__ float3 operator*(float3 a, float3 b) {
55
+ return f3(a.x * b.x, a.y * b.y, a.z * b.z);
56
+ }
57
+ __device__ __forceinline__ float dot(float3 a, float3 b) {
58
+ return a.x * b.x + a.y * b.y + a.z * b.z;
59
+ }
60
+ __device__ __forceinline__ float3 cross(float3 a, float3 b) {
61
+ return f3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
62
+ }
63
+ __device__ __forceinline__ float3 normalize(float3 a) {
64
+ return a * rsqrtf(fmaxf(dot(a, a), 1e-30f));
65
+ }
66
+
67
+ // Duff et al. 2017, "Building an Orthonormal Basis, Revisited".
68
+ __device__ __forceinline__ void onb(float3 n, float3& t, float3& b) {
69
+ float s = copysignf(1.0f, n.z);
70
+ float a = -1.0f / (s + n.z);
71
+ float c = n.x * n.y * a;
72
+ t = f3(1.0f + s * n.x * n.x * a, s * c, -s * n.x);
73
+ b = f3(c, s + n.y * n.y * a, -n.y);
74
+ }
75
+
76
+ // ---------------------------------------------------------------- scene view
77
+ struct DevScene {
78
+ const float* tris; // [F, 9] v0 v1 v2
79
+ const int* mats; // [F]
80
+ const float* nf; // [N, 6] bbox lo.xyz hi.xyz
81
+ const int* ni; // [N, 3] (left,right,0) | (start,count,1)
82
+ int n_nodes;
83
+ const int* lf; // [L] emissive face indices
84
+ const float* lcdf; // [L] normalized area cdf
85
+ int nl;
86
+ float larea; // total emissive area
87
+ const float* alb; // [M, 3]
88
+ const float* emi; // [M, 3]
89
+ int nm;
90
+ };
91
+
92
+ struct DevCam { // pos, fwd, right*tan(v/2)*aspect, up*tan(v/2)
93
+ float p[3], f[3], r[3], u[3];
94
+ };
95
+
96
+ // ------------------------------------------------------------- intersection
97
+ __device__ __forceinline__ bool tri_hit(const float* v, float3 ro, float3 rd,
98
+ float tmin, float tmax, float& t,
99
+ float3& ng) {
100
+ float3 v0 = f3(v[0], v[1], v[2]);
101
+ float3 e1 = f3(v[3], v[4], v[5]) - v0;
102
+ float3 e2 = f3(v[6], v[7], v[8]) - v0;
103
+ float3 p = cross(rd, e2);
104
+ float det = dot(e1, p);
105
+ if (fabsf(det) < 1e-12f) return false;
106
+ float inv = 1.0f / det;
107
+ float3 s = ro - v0;
108
+ float u = dot(s, p) * inv;
109
+ if (u < -1e-6f || u > 1.0f + 1e-6f) return false;
110
+ float3 q = cross(s, e1);
111
+ float w = dot(rd, q) * inv;
112
+ if (w < -1e-6f || u + w > 1.0f + 1e-6f) return false;
113
+ float tt = dot(e2, q) * inv;
114
+ if (tt < tmin || tt > tmax) return false;
115
+ t = tt;
116
+ ng = cross(e1, e2);
117
+ return true;
118
+ }
119
+
120
+ __device__ __forceinline__ bool slab(const float* b, const float ro[3],
121
+ const float inv[3], float tmax) {
122
+ float t0 = kRayEps, t1 = tmax;
123
+ #pragma unroll
124
+ for (int a = 0; a < 3; ++a) {
125
+ float lo = (b[a] - ro[a]) * inv[a];
126
+ float hi = (b[3 + a] - ro[a]) * inv[a];
127
+ if (lo > hi) {
128
+ float tmp = lo;
129
+ lo = hi;
130
+ hi = tmp;
131
+ }
132
+ t0 = fmaxf(t0, lo);
133
+ t1 = fminf(t1, hi);
134
+ }
135
+ return t0 <= t1;
136
+ }
137
+
138
+ __device__ __forceinline__ void inv_dir(float3 rd, float inv[3]) {
139
+ float d;
140
+ d = rd.x; if (fabsf(d) < 1e-12f) d = copysignf(1e-12f, d); inv[0] = 1.0f / d;
141
+ d = rd.y; if (fabsf(d) < 1e-12f) d = copysignf(1e-12f, d); inv[1] = 1.0f / d;
142
+ d = rd.z; if (fabsf(d) < 1e-12f) d = copysignf(1e-12f, d); inv[2] = 1.0f / d;
143
+ }
144
+
145
+ __device__ int bvh_closest(const DevScene& sc, float3 ro, float3 rd,
146
+ float tmin, float& tbest, float3& ngbest) {
147
+ float roa[3] = {ro.x, ro.y, ro.z};
148
+ float inv[3];
149
+ inv_dir(rd, inv);
150
+ int stack[kStack];
151
+ int sp = 0;
152
+ stack[sp++] = 0;
153
+ int best = -1;
154
+ while (sp > 0) {
155
+ int nid = stack[--sp];
156
+ if (!slab(&sc.nf[nid * 6], roa, inv, tbest)) continue;
157
+ const int* n = &sc.ni[nid * 3];
158
+ if (n[2]) { // leaf: faces n[0] .. n[0]+n[1]
159
+ for (int f = n[0]; f < n[0] + n[1]; ++f) {
160
+ float t;
161
+ float3 ng;
162
+ if (tri_hit(&sc.tris[f * 9], ro, rd, tmin, tbest, t, ng)) {
163
+ tbest = t;
164
+ ngbest = ng;
165
+ best = f;
166
+ }
167
+ }
168
+ } else if (sp + 2 <= kStack) {
169
+ stack[sp++] = n[0];
170
+ stack[sp++] = n[1];
171
+ }
172
+ }
173
+ return best;
174
+ }
175
+
176
+ __device__ bool bvh_occluded(const DevScene& sc, float3 ro, float3 rd,
177
+ float tmax) {
178
+ float roa[3] = {ro.x, ro.y, ro.z};
179
+ float inv[3];
180
+ inv_dir(rd, inv);
181
+ int stack[kStack];
182
+ int sp = 0;
183
+ stack[sp++] = 0;
184
+ while (sp > 0) {
185
+ int nid = stack[--sp];
186
+ if (!slab(&sc.nf[nid * 6], roa, inv, tmax)) continue;
187
+ const int* n = &sc.ni[nid * 3];
188
+ if (n[2]) {
189
+ for (int f = n[0]; f < n[0] + n[1]; ++f) {
190
+ float t;
191
+ float3 ng;
192
+ if (tri_hit(&sc.tris[f * 9], ro, rd, kRayEps, tmax, t, ng)) return true;
193
+ }
194
+ } else if (sp + 2 <= kStack) {
195
+ stack[sp++] = n[0];
196
+ stack[sp++] = n[1];
197
+ }
198
+ }
199
+ return false;
200
+ }
201
+
202
+ __device__ __forceinline__ int pick_light(const float* cdf, int nl, float r) {
203
+ int lo = 0, hi = nl - 1;
204
+ while (lo < hi) {
205
+ int mid = (lo + hi) >> 1;
206
+ if (cdf[mid] < r) lo = mid + 1; else hi = mid;
207
+ }
208
+ return lo;
209
+ }
210
+
211
+ // ------------------------------------------------------------- path tracing
212
+ // One camera path: forward accumulates radiance into acc; backward (GRAD)
213
+ // accumulates parameter gradients into the shared buffers instead. The RNG
214
+ // draw order is identical in both instantiations by construction.
215
+ template <bool GRAD>
216
+ __device__ void trace_path(const DevScene& sc, float3 ro, float3 rd,
217
+ curandStatePhilox4_32_10_t& st, int B, int nee,
218
+ float3& acc, const float gs[3], float* s_ga,
219
+ float* s_ge) {
220
+ float T[3] = {1.0f, 1.0f, 1.0f}; // prefix albedo product
221
+ float albs[kMaxBounces][3]; // per-bounce albedo (GRAD replay)
222
+ int mats[kMaxBounces];
223
+ float suf[kMaxBounces + 1][3]; // suffix products workspace
224
+
225
+ for (int k = 0; k < B; ++k) {
226
+ float tbest = 3.0e38f;
227
+ float3 ng;
228
+ int face = bvh_closest(sc, ro, rd, kRayEps, tbest, ng);
229
+ if (face < 0) break;
230
+
231
+ int m = sc.mats[face];
232
+ float3 x = ro + rd * tbest;
233
+ float3 n = normalize(ng);
234
+ if (dot(n, rd) > 0.0f) n = f3(-n.x, -n.y, -n.z);
235
+
236
+ const float* am = &sc.alb[m * 3];
237
+ const float* em = &sc.emi[m * 3];
238
+
239
+ // Emission gathered at the camera hit (NEE on) or at every hit (NEE off).
240
+ if (k == 0 || !nee) {
241
+ if (!GRAD) {
242
+ acc.x += T[0] * em[0];
243
+ acc.y += T[1] * em[1];
244
+ acc.z += T[2] * em[2];
245
+ } else {
246
+ // dC/dE[m] = T; dC/da[m_j] (j<k) = E * excl_j.
247
+ for (int c = 0; c < 3; ++c)
248
+ atomicAdd(&s_ge[m * 3 + c], gs[c] * T[c]);
249
+ if (k > 0) {
250
+ suf[k][0] = suf[k][1] = suf[k][2] = 1.0f;
251
+ for (int i = k - 1; i >= 0; --i)
252
+ for (int c = 0; c < 3; ++c) suf[i][c] = suf[i + 1][c] * albs[i][c];
253
+ float pref[3] = {1.0f, 1.0f, 1.0f};
254
+ for (int j = 0; j < k; ++j) {
255
+ for (int c = 0; c < 3; ++c) {
256
+ float excl = pref[c] * suf[j + 1][c];
257
+ atomicAdd(&s_ga[mats[j] * 3 + c], gs[c] * em[c] * excl);
258
+ pref[c] *= albs[j][c];
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ if (GRAD) {
266
+ albs[k][0] = am[0];
267
+ albs[k][1] = am[1];
268
+ albs[k][2] = am[2];
269
+ mats[k] = m;
270
+ }
271
+
272
+ // Next-event estimation against the area-sampled light list. NEE at
273
+ // vertex k reaches the light at hit k+1, so it is skipped at the final
274
+ // vertex: both estimators then cover camera-to-light paths of at most
275
+ // max_bounces surface hits and agree in expectation, not just in the
276
+ // limit.
277
+ if (nee && sc.nl > 0 && k + 1 < B) {
278
+ float r1 = curand_uniform(&st);
279
+ float r2 = curand_uniform(&st);
280
+ float r3 = curand_uniform(&st);
281
+ int li = pick_light(sc.lcdf, sc.nl, r1);
282
+ int lface = sc.lf[li];
283
+ const float* lv = &sc.tris[lface * 9];
284
+ float3 lv0 = f3(lv[0], lv[1], lv[2]);
285
+ float3 le1 = f3(lv[3], lv[4], lv[5]) - lv0;
286
+ float3 le2 = f3(lv[6], lv[7], lv[8]) - lv0;
287
+ float su = sqrtf(r2);
288
+ float b0 = 1.0f - su;
289
+ float b1 = r3 * su;
290
+ float3 y = lv0 + le1 * b0 + le2 * b1; // uniform: v0 + e1*(1-sqrt(r2)) + e2*(r3*sqrt(r2))
291
+ float3 ln = normalize(cross(le1, le2));
292
+ float3 dvec = y - x;
293
+ float d2 = fmaxf(dot(dvec, dvec), 1e-12f);
294
+ float d = sqrtf(d2);
295
+ float3 wi = dvec * (1.0f / d);
296
+ if (dot(ln, dvec) > 0.0f) ln = f3(-ln.x, -ln.y, -ln.z); // face the shading point
297
+ float cx = dot(n, wi);
298
+ float cy = -dot(ln, wi);
299
+ int lm = sc.mats[lface];
300
+ if (cx > 1e-6f && cy > 1e-6f && d > kShadowEps * 2.0f) {
301
+ float3 xo = x + n * kRayEps;
302
+ if (!bvh_occluded(sc, xo, wi, d - kShadowEps)) {
303
+ float S = cx * cy / d2 * sc.larea * kInvPi;
304
+ const float* el = &sc.emi[lm * 3];
305
+ if (!GRAD) {
306
+ acc.x += T[0] * am[0] * el[0] * S;
307
+ acc.y += T[1] * am[1] * el[1] * S;
308
+ acc.z += T[2] * am[2] * el[2] * S;
309
+ } else {
310
+ // Term = prod_{j<=k} a_j * E_l * S; the vertex albedo a_k is
311
+ // one of the factors (albs[k] is already stored above).
312
+ suf[k + 1][0] = suf[k + 1][1] = suf[k + 1][2] = 1.0f;
313
+ for (int i = k; i >= 0; --i)
314
+ for (int c = 0; c < 3; ++c)
315
+ suf[i][c] = suf[i + 1][c] * albs[i][c];
316
+ for (int c = 0; c < 3; ++c)
317
+ atomicAdd(&s_ge[lm * 3 + c], gs[c] * suf[0][c] * S);
318
+ float pref[3] = {1.0f, 1.0f, 1.0f};
319
+ for (int j = 0; j <= k; ++j) {
320
+ for (int c = 0; c < 3; ++c) {
321
+ float excl = pref[c] * suf[j + 1][c];
322
+ atomicAdd(&s_ga[mats[j] * 3 + c], gs[c] * el[c] * S * excl);
323
+ pref[c] *= albs[j][c];
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ // Cosine-weighted continuation: BRDF*cos/pdf = albedo exactly.
332
+ float rd1 = curand_uniform(&st);
333
+ float rd2 = curand_uniform(&st);
334
+ float rr = sqrtf(rd1);
335
+ float phi = 6.2831853071795864769f * rd2;
336
+ float3 tang, bit;
337
+ onb(n, tang, bit);
338
+ float3 nd = tang * (rr * cosf(phi)) + bit * (rr * sinf(phi)) +
339
+ n * sqrtf(fmaxf(0.0f, 1.0f - rd1));
340
+ rd = normalize(nd);
341
+ ro = x + n * kRayEps;
342
+ T[0] *= am[0];
343
+ T[1] *= am[1];
344
+ T[2] *= am[2];
345
+ }
346
+ }
347
+
348
+ __device__ __forceinline__ float3 camera_ray(const DevCam& cam, int px, int py,
349
+ int W, int H, float jx, float jy) {
350
+ float nx = 2.0f * ((px + jx) / (float)W) - 1.0f;
351
+ float ny = 1.0f - 2.0f * ((py + jy) / (float)H);
352
+ float3 d = f3(cam.f[0] + nx * cam.r[0] + ny * cam.u[0],
353
+ cam.f[1] + nx * cam.r[1] + ny * cam.u[1],
354
+ cam.f[2] + nx * cam.r[2] + ny * cam.u[2]);
355
+ return normalize(d);
356
+ }
357
+
358
+ __global__ void k_forward(DevScene sc, DevCam cam, int H, int W, int spp,
359
+ int B, int nee, unsigned long long seed,
360
+ float* img) {
361
+ int pid = blockIdx.x * blockDim.x + threadIdx.x;
362
+ if (pid >= H * W) return;
363
+ int px = pid % W, py = pid / W;
364
+ float3 ro = f3(cam.p[0], cam.p[1], cam.p[2]);
365
+ float3 acc = f3(0.0f, 0.0f, 0.0f);
366
+ for (int s = 0; s < spp; ++s) {
367
+ curandStatePhilox4_32_10_t st;
368
+ curand_init(seed, (unsigned long long)pid * spp + s, 0, &st);
369
+ float jx = curand_uniform(&st);
370
+ float jy = curand_uniform(&st);
371
+ float3 rd = camera_ray(cam, px, py, W, H, jx, jy);
372
+ trace_path<false>(sc, ro, rd, st, B, nee, acc, nullptr, nullptr, nullptr);
373
+ }
374
+ float inv_spp = 1.0f / (float)spp;
375
+ img[pid * 3 + 0] = acc.x * inv_spp;
376
+ img[pid * 3 + 1] = acc.y * inv_spp;
377
+ img[pid * 3 + 2] = acc.z * inv_spp;
378
+ }
379
+
380
+ __global__ void k_backward(DevScene sc, DevCam cam, int H, int W, int spp,
381
+ int B, int nee, unsigned long long seed,
382
+ const float* gimg, float* ga, float* ge) {
383
+ __shared__ float s_ga[kMaxMats * 3];
384
+ __shared__ float s_ge[kMaxMats * 3];
385
+ int nm3 = sc.nm * 3;
386
+ for (int i = threadIdx.x; i < nm3; i += blockDim.x) {
387
+ s_ga[i] = 0.0f;
388
+ s_ge[i] = 0.0f;
389
+ }
390
+ __syncthreads();
391
+
392
+ int pid = blockIdx.x * blockDim.x + threadIdx.x;
393
+ if (pid < H * W) {
394
+ int px = pid % W, py = pid / W;
395
+ float inv_spp = 1.0f / (float)spp;
396
+ float gs[3] = {gimg[pid * 3 + 0] * inv_spp, gimg[pid * 3 + 1] * inv_spp,
397
+ gimg[pid * 3 + 2] * inv_spp};
398
+ float3 ro = f3(cam.p[0], cam.p[1], cam.p[2]);
399
+ float3 acc = f3(0.0f, 0.0f, 0.0f);
400
+ for (int s = 0; s < spp; ++s) {
401
+ curandStatePhilox4_32_10_t st;
402
+ curand_init(seed, (unsigned long long)pid * spp + s, 0, &st);
403
+ float jx = curand_uniform(&st);
404
+ float jy = curand_uniform(&st);
405
+ float3 rd = camera_ray(cam, px, py, W, H, jx, jy);
406
+ trace_path<true>(sc, ro, rd, st, B, nee, acc, gs, s_ga, s_ge);
407
+ }
408
+ }
409
+
410
+ __syncthreads();
411
+ for (int i = threadIdx.x; i < nm3; i += blockDim.x) {
412
+ atomicAdd(&ga[i], s_ga[i]);
413
+ atomicAdd(&ge[i], s_ge[i]);
414
+ }
415
+ }
416
+
417
+ DevScene make_scene(const float* tris, const int* mat_ids,
418
+ const float* nodes_f, const int* nodes_i, int n_nodes,
419
+ const int* light_faces, const float* light_cdf,
420
+ int n_lights, float total_light_area, const float* albedo,
421
+ const float* emission, int n_mats) {
422
+ DevScene sc;
423
+ sc.tris = tris;
424
+ sc.mats = mat_ids;
425
+ sc.nf = nodes_f;
426
+ sc.ni = nodes_i;
427
+ sc.n_nodes = n_nodes;
428
+ sc.lf = light_faces;
429
+ sc.lcdf = light_cdf;
430
+ sc.nl = n_lights;
431
+ sc.larea = total_light_area;
432
+ sc.alb = albedo;
433
+ sc.emi = emission;
434
+ sc.nm = n_mats;
435
+ return sc;
436
+ }
437
+
438
+ DevCam make_cam(const float* cam) {
439
+ DevCam c;
440
+ for (int i = 0; i < 3; ++i) {
441
+ c.p[i] = cam[i];
442
+ c.f[i] = cam[3 + i];
443
+ c.r[i] = cam[6 + i];
444
+ c.u[i] = cam[9 + i];
445
+ }
446
+ return c;
447
+ }
448
+
449
+ } // namespace
450
+
451
+ extern "C" void ptd_forward_launch(
452
+ const float* tris, const int* mat_ids, int /*n_faces*/,
453
+ const float* nodes_f, const int* nodes_i, int n_nodes,
454
+ const int* light_faces, const float* light_cdf, int n_lights,
455
+ float total_light_area, const float* albedo, const float* emission,
456
+ int n_mats, const float* cam, int H, int W, int spp, int max_bounces,
457
+ int nee, long long seed, float* image, cudaStream_t stream) {
458
+ DevScene sc = make_scene(tris, mat_ids, nodes_f, nodes_i, n_nodes,
459
+ light_faces, light_cdf, n_lights, total_light_area,
460
+ albedo, emission, n_mats);
461
+ DevCam dc = make_cam(cam);
462
+ int blocks = (H * W + kThreads - 1) / kThreads;
463
+ k_forward<<<blocks, kThreads, 0, stream>>>(
464
+ sc, dc, H, W, spp, max_bounces, nee, (unsigned long long)seed, image);
465
+ }
466
+
467
+ extern "C" void ptd_backward_launch(
468
+ const float* tris, const int* mat_ids, int /*n_faces*/,
469
+ const float* nodes_f, const int* nodes_i, int n_nodes,
470
+ const int* light_faces, const float* light_cdf, int n_lights,
471
+ float total_light_area, const float* albedo, const float* emission,
472
+ int n_mats, const float* cam, int H, int W, int spp, int max_bounces,
473
+ int nee, long long seed, const float* grad_image, float* grad_albedo,
474
+ float* grad_emission, cudaStream_t stream) {
475
+ DevScene sc = make_scene(tris, mat_ids, nodes_f, nodes_i, n_nodes,
476
+ light_faces, light_cdf, n_lights, total_light_area,
477
+ albedo, emission, n_mats);
478
+ DevCam dc = make_cam(cam);
479
+ int blocks = (H * W + kThreads - 1) / kThreads;
480
+ k_backward<<<blocks, kThreads, 0, stream>>>(
481
+ sc, dc, H, W, spp, max_bounces, nee, (unsigned long long)seed,
482
+ grad_image, grad_albedo, grad_emission);
483
+ }
pathtracer_diff_cuda/pathtracer_launch.h ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Raw-pointer launch interface for the pathtracer-diff kernels. Torch-free so
2
+ // the .cu never sees libtorch headers (required for local MSVC builds).
3
+ #pragma once
4
+
5
+ #include <cuda_runtime_api.h>
6
+
7
+ extern "C" {
8
+
9
+ void ptd_forward_launch(
10
+ const float* tris, const int* mat_ids, int n_faces,
11
+ const float* nodes_f, const int* nodes_i, int n_nodes,
12
+ const int* light_faces, const float* light_cdf, int n_lights,
13
+ float total_light_area,
14
+ const float* albedo, const float* emission, int n_mats,
15
+ const float* cam,
16
+ int H, int W, int spp, int max_bounces, int nee,
17
+ long long seed, float* image, cudaStream_t stream);
18
+
19
+ void ptd_backward_launch(
20
+ const float* tris, const int* mat_ids, int n_faces,
21
+ const float* nodes_f, const int* nodes_i, int n_nodes,
22
+ const int* light_faces, const float* light_cdf, int n_lights,
23
+ float total_light_area,
24
+ const float* albedo, const float* emission, int n_mats,
25
+ const float* cam,
26
+ int H, int W, int spp, int max_bounces, int nee,
27
+ long long seed, const float* grad_image,
28
+ float* grad_albedo, float* grad_emission, cudaStream_t stream);
29
+
30
+ } // extern "C"
tests/test_pathtracer_diff.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import torch
3
+
4
+ import kernels
5
+
6
+ ptd = kernels.get_kernel("phanerozoic/pathtracer-diff", version=1,
7
+ trust_remote_code=True)
8
+
9
+ requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(),
10
+ reason="CUDA required")
11
+
12
+
13
+ def quad(a, b, c, d):
14
+ return [[a, b, c], [a, c, d]]
15
+
16
+
17
+ def add_box(verts, faces, mat, lo, hi, mat_id):
18
+ x0, y0, z0 = lo
19
+ x1, y1, z1 = hi
20
+ base = len(verts)
21
+ verts.extend([(x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1),
22
+ (x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)])
23
+ i = lambda k: base + k
24
+ for q in [quad(i(0), i(1), i(2), i(3)), # bottom
25
+ quad(i(4), i(7), i(6), i(5)), # top
26
+ quad(i(0), i(4), i(5), i(1)), # z0
27
+ quad(i(3), i(2), i(6), i(7)), # z1
28
+ quad(i(0), i(3), i(7), i(4)), # x0
29
+ quad(i(1), i(5), i(6), i(2))]: # x1
30
+ faces.extend(q)
31
+ mat.extend([mat_id, mat_id])
32
+
33
+
34
+ def furnace_scene(albedo=0.5, emission=0.25):
35
+ verts, faces, mat = [], [], []
36
+ add_box(verts, faces, mat, (-2, -2, -2), (2, 2, 2), 0)
37
+ return ptd.Scene(verts, faces, mat,
38
+ albedo=[[albedo] * 3], emission=[[emission] * 3])
39
+
40
+
41
+ def cornell(with_box=True, albedo=None, emission=None):
42
+ verts, faces, mat = [], [], []
43
+
44
+ def wall(a, b, c, d, m):
45
+ base = len(verts)
46
+ verts.extend([a, b, c, d])
47
+ faces.extend(quad(base, base + 1, base + 2, base + 3))
48
+ mat.extend([m, m])
49
+
50
+ X, Y, Z = 5.56, 5.488, 5.592
51
+ wall((0, 0, 0), (X, 0, 0), (X, 0, Z), (0, 0, Z), 0) # floor
52
+ wall((0, Y, 0), (0, Y, Z), (X, Y, Z), (X, Y, 0), 0) # ceiling
53
+ wall((0, 0, Z), (X, 0, Z), (X, Y, Z), (0, Y, Z), 0) # back
54
+ wall((0, 0, 0), (0, 0, Z), (0, Y, Z), (0, Y, 0), 1) # left (red)
55
+ wall((X, 0, 0), (X, Y, 0), (X, Y, Z), (X, 0, Z), 2) # right (green)
56
+ wall((2.13, Y - 0.001, 2.27), (3.43, Y - 0.001, 2.27),
57
+ (3.43, Y - 0.001, 3.32), (2.13, Y - 0.001, 3.32), 3) # light
58
+ if with_box:
59
+ add_box(verts, faces, mat, (1.3, 0.0, 0.65), (2.4, 1.65, 1.7), 0)
60
+ if albedo is None:
61
+ albedo = [[0.73, 0.73, 0.73], [0.65, 0.05, 0.05],
62
+ [0.12, 0.45, 0.15], [0.78, 0.78, 0.78]]
63
+ if emission is None:
64
+ emission = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [18.4, 15.6, 8.0]]
65
+ return ptd.Scene(verts, faces, mat, albedo=albedo, emission=emission)
66
+
67
+
68
+ def cornell_camera():
69
+ return ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
70
+ vfov_deg=39.0)
71
+
72
+
73
+ @requires_cuda
74
+ @pytest.mark.kernels_ci
75
+ def test_furnace_analytic_zero_variance():
76
+ """Closed uniform box, BRDF sampling only: with cosine sampling the
77
+ diffuse throughput multiplier is exactly the albedo, so every path
78
+ returns exactly E * sum_{k<B} a^k and every pixel equals the analytic
79
+ series regardless of the random walk."""
80
+ a, E, B = 0.5, 0.25, 8
81
+ scene = furnace_scene(a, E)
82
+ cam = ptd.Camera(position=(0, 0, 0), look_at=(0, 0, 1), vfov_deg=60.0)
83
+ img = ptd.render(scene, cam, 32, 32, spp=2, max_bounces=B, nee=False,
84
+ seed=0)
85
+ expected = E * (1 - a ** B) / (1 - a)
86
+ assert torch.isfinite(img).all()
87
+ assert (img - expected).abs().max().item() < 1e-3, \
88
+ (img.min().item(), img.max().item(), expected)
89
+
90
+
91
+ @requires_cuda
92
+ @pytest.mark.kernels_ci
93
+ def test_forward_bitwise_deterministic():
94
+ scene = cornell()
95
+ cam = cornell_camera()
96
+ i1 = ptd.render(scene, cam, 96, 96, spp=8, max_bounces=4, seed=11)
97
+ i2 = ptd.render(scene, cam, 96, 96, spp=8, max_bounces=4, seed=11)
98
+ assert torch.equal(i1, i2)
99
+ i3 = ptd.render(scene, cam, 96, 96, spp=8, max_bounces=4, seed=12)
100
+ assert not torch.equal(i1, i3)
101
+
102
+
103
+ @requires_cuda
104
+ @pytest.mark.kernels_ci
105
+ def test_nee_matches_brdf_estimator():
106
+ """The NEE-only and BRDF-only estimators target the same integral; their
107
+ image means must agree within Monte Carlo tolerance."""
108
+ verts, faces, mat = [], [], []
109
+ add_box(verts, faces, mat, (0, 0, 0), (5, 5, 5), 0)
110
+ base = len(verts)
111
+ verts.extend([(1.0, 4.999, 1.0), (4.5, 4.999, 1.0), (4.5, 4.999, 4.5),
112
+ (1.0, 4.999, 4.5)])
113
+ faces.extend(quad(base, base + 1, base + 2, base + 3))
114
+ mat.extend([1, 1])
115
+ scene = ptd.Scene(verts, faces, mat,
116
+ albedo=[[0.6, 0.6, 0.6], [0.0, 0.0, 0.0]],
117
+ emission=[[0, 0, 0], [3.0, 3.0, 3.0]])
118
+ cam = ptd.Camera(position=(2.5, 2.5, 0.2), look_at=(2.5, 2.5, 5.0),
119
+ vfov_deg=70.0)
120
+ m_nee = ptd.render(scene, cam, 64, 64, spp=64, max_bounces=4, nee=True,
121
+ seed=1).mean().item()
122
+ m_brdf = ptd.render(scene, cam, 64, 64, spp=256, max_bounces=4, nee=False,
123
+ seed=2).mean().item()
124
+ assert abs(m_nee - m_brdf) / m_brdf < 0.03, (m_nee, m_brdf)
125
+
126
+
127
+ @requires_cuda
128
+ @pytest.mark.kernels_ci
129
+ def test_gradients_match_finite_differences():
130
+ """Sampling directions never depend on albedo/emission, so with a fixed
131
+ seed the loss is smooth in the parameters along the same paths and
132
+ central differences must match the analytic replay gradients."""
133
+ scene = cornell(with_box=False)
134
+ cam = cornell_camera()
135
+ H = W = 48
136
+ torch.manual_seed(0)
137
+ Wr = torch.rand(H, W, 3, device="cuda")
138
+
139
+ def loss_of(albedo, emission):
140
+ scene.albedo = albedo
141
+ scene.emission = emission
142
+ img = ptd.render(scene, cam, H, W, spp=8, max_bounces=3, nee=True,
143
+ seed=7)
144
+ return (img * Wr).sum()
145
+
146
+ alb0 = scene.albedo.clone()
147
+ emi0 = scene.emission.clone()
148
+
149
+ alb = alb0.clone().requires_grad_(True)
150
+ emi = emi0.clone().requires_grad_(True)
151
+ loss_of(alb, emi).backward()
152
+
153
+ checks = []
154
+ for (i, c) in [(0, 1), (1, 0), (2, 2)]: # white g, red r, green b
155
+ h = 2e-3
156
+ ap = alb0.clone(); ap[i, c] += h
157
+ am = alb0.clone(); am[i, c] -= h
158
+ fd = (loss_of(ap, emi0) - loss_of(am, emi0)).item() / (2 * h)
159
+ an = alb.grad[i, c].item()
160
+ checks.append(("albedo", i, c, an, fd))
161
+ h = 0.05
162
+ ep = emi0.clone(); ep[3, 1] += h
163
+ em = emi0.clone(); em[3, 1] -= h
164
+ fd = (loss_of(alb0, ep) - loss_of(alb0, em)).item() / (2 * h)
165
+ an = emi.grad[3, 1].item()
166
+ checks.append(("emission", 3, 1, an, fd))
167
+
168
+ for kind, i, c, an, fd in checks:
169
+ assert fd != 0.0, (kind, i, c)
170
+ assert abs(an - fd) / max(abs(fd), 1e-6) < 2e-2, \
171
+ (kind, i, c, an, fd)
172
+
173
+
174
+ @requires_cuda
175
+ @pytest.mark.kernels_ci
176
+ def test_inverse_rendering_recovers_wall_albedo():
177
+ """Recover the left-wall albedo from a rendered target by gradient
178
+ descent through the kernel: the flagship no-framework-in-the-loop use."""
179
+ target_row = torch.tensor([0.2, 0.5, 0.7], device="cuda")
180
+ scene = cornell(with_box=False)
181
+ cam = cornell_camera()
182
+ H = W = 48
183
+
184
+ tgt_alb = scene.albedo.clone()
185
+ tgt_alb[1] = target_row
186
+ scene.albedo = tgt_alb
187
+ target = ptd.render(scene, cam, H, W, spp=8, max_bounces=3, seed=3).detach()
188
+
189
+ alb = scene.albedo.clone()
190
+ alb[1] = torch.tensor([0.5, 0.5, 0.5], device="cuda")
191
+ alb = alb.requires_grad_(True)
192
+ opt = torch.optim.Adam([alb], lr=0.05)
193
+ losses = []
194
+ for _ in range(80):
195
+ opt.zero_grad()
196
+ scene.albedo = alb
197
+ img = ptd.render(scene, cam, H, W, spp=8, max_bounces=3, seed=3)
198
+ loss = (img - target).square().mean()
199
+ loss.backward()
200
+ alb.grad[0] = 0
201
+ alb.grad[2] = 0
202
+ alb.grad[3] = 0
203
+ opt.step()
204
+ with torch.no_grad():
205
+ alb.clamp_(0.02, 0.98)
206
+ losses.append(loss.item())
207
+ err = (alb[1].detach() - target_row).abs().max().item()
208
+ assert err < 0.05, (alb[1].detach().tolist(), losses[0], losses[-1])
209
+ assert losses[-1] < losses[0] * 0.1, (losses[0], losses[-1])
210
+
211
+
212
+ @requires_cuda
213
+ @pytest.mark.kernels_ci
214
+ def test_validation():
215
+ verts, faces, mat = [], [], []
216
+ add_box(verts, faces, mat, (0, 0, 0), (1, 1, 1), 0)
217
+ with pytest.raises(ValueError):
218
+ ptd.Scene(verts, faces, mat, albedo=[[0.5] * 3] * 65,
219
+ emission=[[0.0] * 3] * 65)
220
+ scene = ptd.Scene(verts, faces, mat, albedo=[[0.5] * 3],
221
+ emission=[[1.0] * 3])
222
+ cam = ptd.Camera(position=(0.5, 0.5, 0.5), look_at=(0.5, 0.5, 1.0))
223
+ with pytest.raises(ValueError):
224
+ ptd.render(scene, cam, 8, 8, spp=1, max_bounces=17)
225
+ img = ptd.render(scene, cam, 8, 8, spp=2, max_bounces=2)
226
+ assert img.shape == (8, 8, 3) and img.dtype == torch.float32
227
+ assert torch.isfinite(img).all()
torch-ext/pathtracer_diff/__init__.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """pathtracer-diff: a differentiable Monte Carlo path tracer as one kernel.
2
+
3
+ Forward renders a linear-radiance image with a megakernel path tracer
4
+ (diffuse + emissive materials, cosine-weighted BRDF sampling, optional
5
+ next-event estimation, BVH); one thread owns one pixel, so the image is
6
+ bitwise deterministic. Backward replays the identical paths with the
7
+ identical counter-based Philox draws and accumulates analytic gradients to
8
+ per-material albedo and emission, storing no path state between the passes.
9
+ Sampling directions never depend on the differentiable parameters, so the
10
+ replay is exact. No visibility/silhouette gradients: geometry is not a
11
+ differentiable input.
12
+
13
+ from kernels import get_kernel
14
+ ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
15
+
16
+ scene = ptd.Scene(vertices, faces, material_ids, albedo, emission)
17
+ cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8))
18
+ img = ptd.render(scene, cam, 256, 256, spp=64) # autograd to albedo/emission
19
+ img.sum().backward()
20
+ """
21
+ import math
22
+
23
+ import torch
24
+
25
+ from ._ops import ops
26
+
27
+ MAX_MATERIALS = 64
28
+ MAX_BOUNCES = 16
29
+
30
+ __all__ = ["Scene", "Camera", "render", "ops", "MAX_MATERIALS", "MAX_BOUNCES"]
31
+
32
+
33
+ def _build_bvh(tris, leaf_size=4):
34
+ """Median-split BVH over [F, 9] triangles. Returns (nodes_f [N, 6],
35
+ nodes_i [N, 3], order [F]) with leaves as contiguous ranges of the
36
+ reordered face list. nodes_i rows are (left, right, 0) or
37
+ (start, count, 1)."""
38
+ import numpy as np
39
+
40
+ tris = np.asarray(tris, dtype=np.float32)
41
+ F = tris.shape[0]
42
+ v = tris.reshape(F, 3, 3)
43
+ lo = v.min(axis=1)
44
+ hi = v.max(axis=1)
45
+ cen = (lo + hi) * 0.5
46
+
47
+ nodes_f, nodes_i, order = [], [], []
48
+
49
+ def rec(idx):
50
+ nid = len(nodes_f)
51
+ nodes_f.append(None)
52
+ nodes_i.append(None)
53
+ blo = lo[idx].min(axis=0)
54
+ bhi = hi[idx].max(axis=0)
55
+ pad = 1e-5 * (1.0 + np.abs(blo) + np.abs(bhi))
56
+ nodes_f[nid] = np.concatenate([blo - pad, bhi + pad])
57
+ if len(idx) <= leaf_size:
58
+ start = len(order)
59
+ order.extend(idx.tolist())
60
+ nodes_i[nid] = (start, len(idx), 1)
61
+ return nid
62
+ ax = int(np.argmax(bhi - blo))
63
+ srt = idx[np.argsort(cen[idx, ax], kind="stable")]
64
+ mid = len(srt) // 2
65
+ left = rec(srt[:mid])
66
+ right = rec(srt[mid:])
67
+ nodes_i[nid] = (left, right, 0)
68
+ return nid
69
+
70
+ import sys
71
+
72
+ old = sys.getrecursionlimit()
73
+ sys.setrecursionlimit(max(old, 64 + 2 * int(math.log2(max(F, 2))) * 64))
74
+ try:
75
+ rec(np.arange(F))
76
+ finally:
77
+ sys.setrecursionlimit(old)
78
+ return (np.stack(nodes_f).astype(np.float32),
79
+ np.asarray(nodes_i, dtype=np.int32),
80
+ np.asarray(order, dtype=np.int64))
81
+
82
+
83
+ class Camera:
84
+ """Pinhole camera. `tensor(H, W)` packs (pos, forward, right*tan(v/2)*aspect,
85
+ up*tan(v/2)) as a float32 [12] tensor for the kernel."""
86
+
87
+ def __init__(self, position, look_at, up=(0.0, 1.0, 0.0), vfov_deg=40.0):
88
+ p = torch.tensor(position, dtype=torch.float64)
89
+ t = torch.tensor(look_at, dtype=torch.float64)
90
+ u = torch.tensor(up, dtype=torch.float64)
91
+ f = t - p
92
+ f = f / f.norm()
93
+ r = torch.linalg.cross(f, u)
94
+ r = r / r.norm()
95
+ uu = torch.linalg.cross(r, f)
96
+ self.position, self.forward, self.right, self.up = p, f, r, uu
97
+ self.vfov_deg = float(vfov_deg)
98
+
99
+ def tensor(self, H, W, device="cuda"):
100
+ th = math.tan(math.radians(self.vfov_deg) * 0.5)
101
+ rs = self.right * (th * W / H)
102
+ us = self.up * th
103
+ return torch.cat([self.position, self.forward, rs, us]).to(
104
+ device=device, dtype=torch.float32).contiguous()
105
+
106
+
107
+ class Scene:
108
+ """Triangle-mesh scene with per-material albedo and emission.
109
+
110
+ vertices [V, 3] float, faces [F, 3] int, material_ids [F] int,
111
+ albedo [M, 3] float32 (may require grad), emission [M, 3] float32
112
+ (may require grad). Faces are reordered by the BVH build; emissive
113
+ faces become the area-sampled light list."""
114
+
115
+ def __init__(self, vertices, faces, material_ids, albedo, emission,
116
+ device="cuda"):
117
+ vertices = torch.as_tensor(vertices, dtype=torch.float32)
118
+ faces = torch.as_tensor(faces, dtype=torch.int64)
119
+ material_ids = torch.as_tensor(material_ids, dtype=torch.int64)
120
+ if not torch.is_tensor(albedo):
121
+ albedo = torch.tensor(albedo, dtype=torch.float32, device=device)
122
+ if not torch.is_tensor(emission):
123
+ emission = torch.tensor(emission, dtype=torch.float32, device=device)
124
+ M = albedo.shape[0]
125
+ if M > MAX_MATERIALS:
126
+ raise ValueError(f"at most {MAX_MATERIALS} materials, got {M}")
127
+ if albedo.shape != (M, 3) or emission.shape != (M, 3):
128
+ raise ValueError("albedo and emission must be [M, 3]")
129
+ if int(material_ids.max()) >= M or int(material_ids.min()) < 0:
130
+ raise ValueError("material_ids out of range")
131
+
132
+ tris = vertices[faces].reshape(-1, 9)
133
+ nodes_f, nodes_i, order = _build_bvh(tris.numpy())
134
+ order_t = torch.from_numpy(order)
135
+ tris = tris[order_t]
136
+ mat_ids = material_ids[order_t].to(torch.int32)
137
+
138
+ emissive = (emission.detach().cpu().amax(dim=1) > 0)[mat_ids.long()]
139
+ lf = torch.nonzero(emissive, as_tuple=False).flatten().to(torch.int32)
140
+ if lf.numel() > 0:
141
+ t = tris[lf.long()].reshape(-1, 3, 3).double()
142
+ e1 = t[:, 1] - t[:, 0]
143
+ e2 = t[:, 2] - t[:, 0]
144
+ areas = 0.5 * torch.linalg.cross(e1, e2).norm(dim=1)
145
+ total = float(areas.sum())
146
+ cdf = (areas.cumsum(0) / areas.sum()).float()
147
+ else:
148
+ total = 0.0
149
+ cdf = torch.zeros(0, dtype=torch.float32)
150
+
151
+ self.device = device
152
+ self.tris = tris.to(device).contiguous()
153
+ self.mat_ids = mat_ids.to(device).contiguous()
154
+ self.nodes_f = torch.from_numpy(nodes_f).to(device).contiguous()
155
+ self.nodes_i = torch.from_numpy(nodes_i).to(device).contiguous()
156
+ self.light_faces = lf.to(device).contiguous()
157
+ self.light_cdf = cdf.to(device).contiguous()
158
+ self.total_light_area = total
159
+ self.albedo = albedo.to(device)
160
+ self.emission = emission.to(device)
161
+
162
+ @property
163
+ def n_faces(self):
164
+ return self.tris.shape[0]
165
+
166
+
167
+ class _RenderFn(torch.autograd.Function):
168
+ @staticmethod
169
+ def forward(ctx, albedo, emission, scene, cam_t, H, W, spp, max_bounces,
170
+ nee, seed):
171
+ image = torch.empty(H, W, 3, device=albedo.device, dtype=torch.float32)
172
+ ops.pt_forward(scene.tris, scene.mat_ids, scene.nodes_f, scene.nodes_i,
173
+ scene.light_faces, scene.light_cdf,
174
+ float(scene.total_light_area),
175
+ albedo.detach().contiguous(),
176
+ emission.detach().contiguous(), cam_t, spp, max_bounces,
177
+ nee, seed, image)
178
+ ctx.scene = scene
179
+ ctx.cam_t = cam_t
180
+ ctx.params = (H, W, spp, max_bounces, nee, seed)
181
+ ctx.save_for_backward(albedo, emission)
182
+ return image
183
+
184
+ @staticmethod
185
+ def backward(ctx, grad_image):
186
+ albedo, emission = ctx.saved_tensors
187
+ scene, cam_t = ctx.scene, ctx.cam_t
188
+ H, W, spp, max_bounces, nee, seed = ctx.params
189
+ ga = torch.zeros_like(albedo)
190
+ ge = torch.zeros_like(emission)
191
+ ops.pt_backward(scene.tris, scene.mat_ids, scene.nodes_f,
192
+ scene.nodes_i, scene.light_faces, scene.light_cdf,
193
+ float(scene.total_light_area),
194
+ albedo.detach().contiguous(),
195
+ emission.detach().contiguous(), cam_t, spp,
196
+ max_bounces, nee, seed, grad_image.contiguous(), ga,
197
+ ge)
198
+ return ga, ge, None, None, None, None, None, None, None, None
199
+
200
+
201
+ def render(scene, camera, height, width, spp=64, max_bounces=4, nee=True,
202
+ seed=0):
203
+ """Render a linear-radiance image [H, W, 3] float32, differentiable with
204
+ respect to scene.albedo and scene.emission. Fixed seed makes the estimate
205
+ a deterministic function of the parameters (correlated samples), which is
206
+ what inverse-rendering loops want."""
207
+ if not (1 <= max_bounces <= MAX_BOUNCES):
208
+ raise ValueError(f"max_bounces must be in [1, {MAX_BOUNCES}]")
209
+ cam_t = camera.tensor(height, width, device=scene.device)
210
+ return _RenderFn.apply(scene.albedo, scene.emission, scene, cam_t, height,
211
+ width, int(spp), int(max_bounces), bool(nee),
212
+ int(seed))
torch-ext/torch_binding.cpp ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <torch/library.h>
2
+ #include <torch/all.h>
3
+
4
+ #include <ATen/cuda/CUDAContext.h>
5
+ #include <c10/cuda/CUDAGuard.h>
6
+
7
+ #include "registration.h"
8
+ #include "torch_binding.h"
9
+ #include "../pathtracer_diff_cuda/pathtracer_launch.h"
10
+
11
+ namespace {
12
+
13
+ void check_scene(const torch::Tensor& tris, const torch::Tensor& mat_ids,
14
+ const torch::Tensor& nodes_f, const torch::Tensor& nodes_i,
15
+ const torch::Tensor& light_faces,
16
+ const torch::Tensor& light_cdf, const torch::Tensor& albedo,
17
+ const torch::Tensor& emission, const torch::Tensor& cam,
18
+ int64_t spp, int64_t max_bounces) {
19
+ TORCH_CHECK(tris.is_cuda() && tris.is_contiguous() &&
20
+ tris.dtype() == torch::kFloat32 && tris.dim() == 2 &&
21
+ tris.size(1) == 9,
22
+ "tris must be contiguous CUDA f32 [F, 9]");
23
+ TORCH_CHECK(mat_ids.is_cuda() && mat_ids.is_contiguous() &&
24
+ mat_ids.dtype() == torch::kInt32 &&
25
+ mat_ids.numel() == tris.size(0),
26
+ "mat_ids must be contiguous CUDA i32 [F]");
27
+ TORCH_CHECK(nodes_f.is_cuda() && nodes_f.is_contiguous() &&
28
+ nodes_f.dtype() == torch::kFloat32 && nodes_f.dim() == 2 &&
29
+ nodes_f.size(1) == 6,
30
+ "nodes_f must be contiguous CUDA f32 [N, 6]");
31
+ TORCH_CHECK(nodes_i.is_cuda() && nodes_i.is_contiguous() &&
32
+ nodes_i.dtype() == torch::kInt32 && nodes_i.dim() == 2 &&
33
+ nodes_i.size(1) == 3 && nodes_i.size(0) == nodes_f.size(0),
34
+ "nodes_i must be contiguous CUDA i32 [N, 3]");
35
+ TORCH_CHECK(light_faces.is_cuda() && light_faces.is_contiguous() &&
36
+ light_faces.dtype() == torch::kInt32,
37
+ "light_faces must be contiguous CUDA i32 [L]");
38
+ TORCH_CHECK(light_cdf.is_cuda() && light_cdf.is_contiguous() &&
39
+ light_cdf.dtype() == torch::kFloat32 &&
40
+ light_cdf.numel() == light_faces.numel(),
41
+ "light_cdf must be contiguous CUDA f32 [L]");
42
+ TORCH_CHECK(albedo.is_cuda() && albedo.is_contiguous() &&
43
+ albedo.dtype() == torch::kFloat32 && albedo.dim() == 2 &&
44
+ albedo.size(1) == 3,
45
+ "albedo must be contiguous CUDA f32 [M, 3]");
46
+ TORCH_CHECK(emission.is_cuda() && emission.is_contiguous() &&
47
+ emission.dtype() == torch::kFloat32 &&
48
+ emission.sizes() == albedo.sizes(),
49
+ "emission must be contiguous CUDA f32 [M, 3]");
50
+ TORCH_CHECK(albedo.size(0) <= 64, "at most 64 materials");
51
+ TORCH_CHECK(cam.numel() == 12, "cam must have 12 elements");
52
+ TORCH_CHECK(spp >= 1, "spp must be >= 1");
53
+ TORCH_CHECK(max_bounces >= 1 && max_bounces <= 16,
54
+ "max_bounces must be in [1, 16]");
55
+ }
56
+
57
+ } // namespace
58
+
59
+ void pt_forward(torch::Tensor tris, torch::Tensor mat_ids,
60
+ torch::Tensor nodes_f, torch::Tensor nodes_i,
61
+ torch::Tensor light_faces, torch::Tensor light_cdf,
62
+ double total_light_area, torch::Tensor albedo,
63
+ torch::Tensor emission, torch::Tensor cam, int64_t spp,
64
+ int64_t max_bounces, bool nee, int64_t seed,
65
+ torch::Tensor image) {
66
+ check_scene(tris, mat_ids, nodes_f, nodes_i, light_faces, light_cdf, albedo,
67
+ emission, cam, spp, max_bounces);
68
+ TORCH_CHECK(image.is_cuda() && image.is_contiguous() &&
69
+ image.dtype() == torch::kFloat32 && image.dim() == 3 &&
70
+ image.size(2) == 3,
71
+ "image must be contiguous CUDA f32 [H, W, 3]");
72
+ const at::cuda::CUDAGuard guard(tris.device());
73
+ cudaStream_t stream = at::cuda::getCurrentCUDAStream();
74
+ int L = (int)light_faces.numel();
75
+ // The launcher reads the 12 camera floats on the host.
76
+ torch::Tensor cam_h =
77
+ cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
78
+ ptd_forward_launch(
79
+ tris.const_data_ptr<float>(), mat_ids.const_data_ptr<int>(),
80
+ (int)tris.size(0), nodes_f.const_data_ptr<float>(),
81
+ nodes_i.const_data_ptr<int>(), (int)nodes_f.size(0),
82
+ L ? light_faces.const_data_ptr<int>() : nullptr,
83
+ L ? light_cdf.const_data_ptr<float>() : nullptr, L,
84
+ (float)total_light_area, albedo.const_data_ptr<float>(),
85
+ emission.const_data_ptr<float>(), (int)albedo.size(0),
86
+ cam_h.const_data_ptr<float>(), (int)image.size(0), (int)image.size(1),
87
+ (int)spp, (int)max_bounces, nee ? 1 : 0, (long long)seed,
88
+ image.data_ptr<float>(), stream);
89
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
90
+ }
91
+
92
+ void pt_backward(torch::Tensor tris, torch::Tensor mat_ids,
93
+ torch::Tensor nodes_f, torch::Tensor nodes_i,
94
+ torch::Tensor light_faces, torch::Tensor light_cdf,
95
+ double total_light_area, torch::Tensor albedo,
96
+ torch::Tensor emission, torch::Tensor cam, int64_t spp,
97
+ int64_t max_bounces, bool nee, int64_t seed,
98
+ torch::Tensor grad_image, torch::Tensor grad_albedo,
99
+ torch::Tensor grad_emission) {
100
+ check_scene(tris, mat_ids, nodes_f, nodes_i, light_faces, light_cdf, albedo,
101
+ emission, cam, spp, max_bounces);
102
+ TORCH_CHECK(grad_image.is_cuda() && grad_image.is_contiguous() &&
103
+ grad_image.dtype() == torch::kFloat32 &&
104
+ grad_image.dim() == 3 && grad_image.size(2) == 3,
105
+ "grad_image must be contiguous CUDA f32 [H, W, 3]");
106
+ TORCH_CHECK(grad_albedo.is_cuda() && grad_albedo.is_contiguous() &&
107
+ grad_albedo.sizes() == albedo.sizes(),
108
+ "grad_albedo must match albedo");
109
+ TORCH_CHECK(grad_emission.is_cuda() && grad_emission.is_contiguous() &&
110
+ grad_emission.sizes() == emission.sizes(),
111
+ "grad_emission must match emission");
112
+ const at::cuda::CUDAGuard guard(tris.device());
113
+ cudaStream_t stream = at::cuda::getCurrentCUDAStream();
114
+ int L = (int)light_faces.numel();
115
+ torch::Tensor cam_h =
116
+ cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
117
+ ptd_backward_launch(
118
+ tris.const_data_ptr<float>(), mat_ids.const_data_ptr<int>(),
119
+ (int)tris.size(0), nodes_f.const_data_ptr<float>(),
120
+ nodes_i.const_data_ptr<int>(), (int)nodes_f.size(0),
121
+ L ? light_faces.const_data_ptr<int>() : nullptr,
122
+ L ? light_cdf.const_data_ptr<float>() : nullptr, L,
123
+ (float)total_light_area, albedo.const_data_ptr<float>(),
124
+ emission.const_data_ptr<float>(), (int)albedo.size(0),
125
+ cam_h.const_data_ptr<float>(), (int)grad_image.size(0),
126
+ (int)grad_image.size(1), (int)spp, (int)max_bounces, nee ? 1 : 0,
127
+ (long long)seed, grad_image.const_data_ptr<float>(),
128
+ grad_albedo.data_ptr<float>(), grad_emission.data_ptr<float>(), stream);
129
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
130
+ }
131
+
132
+ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
133
+ ops.def(
134
+ "pt_forward(Tensor tris, Tensor mat_ids, Tensor nodes_f, Tensor nodes_i,"
135
+ " Tensor light_faces, Tensor light_cdf, float total_light_area,"
136
+ " Tensor albedo, Tensor emission, Tensor cam, int spp, int max_bounces,"
137
+ " bool nee, int seed, Tensor! image) -> ()");
138
+ ops.impl("pt_forward", torch::kCUDA, &pt_forward);
139
+
140
+ ops.def(
141
+ "pt_backward(Tensor tris, Tensor mat_ids, Tensor nodes_f,"
142
+ " Tensor nodes_i, Tensor light_faces, Tensor light_cdf,"
143
+ " float total_light_area, Tensor albedo, Tensor emission, Tensor cam,"
144
+ " int spp, int max_bounces, bool nee, int seed, Tensor grad_image,"
145
+ " Tensor! grad_albedo, Tensor! grad_emission) -> ()");
146
+ ops.impl("pt_backward", torch::kCUDA, &pt_backward);
147
+ }
148
+
149
+ REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
torch-ext/torch_binding.h ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <torch/torch.h>
4
+
5
+ void pt_forward(torch::Tensor tris, torch::Tensor mat_ids,
6
+ torch::Tensor nodes_f, torch::Tensor nodes_i,
7
+ torch::Tensor light_faces, torch::Tensor light_cdf,
8
+ double total_light_area, torch::Tensor albedo,
9
+ torch::Tensor emission, torch::Tensor cam, int64_t spp,
10
+ int64_t max_bounces, bool nee, int64_t seed,
11
+ torch::Tensor image);
12
+
13
+ void pt_backward(torch::Tensor tris, torch::Tensor mat_ids,
14
+ torch::Tensor nodes_f, torch::Tensor nodes_i,
15
+ torch::Tensor light_faces, torch::Tensor light_cdf,
16
+ double total_light_area, torch::Tensor albedo,
17
+ torch::Tensor emission, torch::Tensor cam, int64_t spp,
18
+ int64_t max_bounces, bool nee, int64_t seed,
19
+ torch::Tensor grad_image, torch::Tensor grad_albedo,
20
+ torch::Tensor grad_emission);