diffct 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diffct-2.1.0 → diffct-2.2.0}/CHANGELOG.md +43 -0
- {diffct-2.1.0 → diffct-2.2.0}/CITATION.cff +2 -2
- {diffct-2.1.0 → diffct-2.2.0}/PKG-INFO +22 -17
- {diffct-2.1.0 → diffct-2.2.0}/README.md +21 -16
- {diffct-2.1.0 → diffct-2.2.0}/diffct/__init__.py +6 -1
- diffct-2.2.0/diffct/chunking.py +201 -0
- diffct-2.2.0/diffct/operators.py +2047 -0
- diffct-2.2.0/diffct/pipeline.py +258 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/projectors.py +109 -204
- diffct-2.2.0/diffct/storage.py +125 -0
- diffct-2.2.0/diffct/streaming.py +48 -0
- diffct-2.2.0/diffct/surfaces.py +103 -0
- {diffct-2.1.0 → diffct-2.2.0}/examples/README.md +38 -0
- {diffct-2.1.0 → diffct-2.2.0}/formal/README.md +37 -0
- {diffct-2.1.0 → diffct-2.2.0}/pyproject.toml +1 -1
- diffct-2.2.0/tests/test_allocator_capacity.py +349 -0
- diffct-2.2.0/tests/test_axis_aligned_chords.py +118 -0
- diffct-2.2.0/tests/test_batched_surfaces.py +705 -0
- diffct-2.2.0/tests/test_block_cgls.py +996 -0
- diffct-2.2.0/tests/test_block_storage.py +539 -0
- diffct-2.2.0/tests/test_cgls_state_admission.py +173 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_chunked_projector.py +135 -39
- diffct-2.2.0/tests/test_completed_transfer_reuse.py +92 -0
- diffct-2.2.0/tests/test_cuda_sink_lifetimes.py +125 -0
- diffct-2.2.0/tests/test_example_psnr.py +75 -0
- diffct-2.2.0/tests/test_execution_buffers.py +613 -0
- diffct-2.2.0/tests/test_execution_schedules.py +582 -0
- diffct-2.2.0/tests/test_output_pipeline_overlap.py +130 -0
- diffct-2.2.0/tests/test_spatial_partition.py +433 -0
- diffct-2.2.0/tests/test_transfer_pipeline.py +646 -0
- diffct-2.2.0/tests/test_transfer_retirement_lifetimes.py +166 -0
- diffct-2.1.0/diffct/chunking.py +0 -73
- diffct-2.1.0/diffct/operators.py +0 -1033
- {diffct-2.1.0 → diffct-2.2.0}/.gitignore +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/LICENSE +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/NOTICE +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/analytical.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/constants.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/differentiable.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/geometry.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/kernels/__init__.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/kernels/cone_beam.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/kernels/fan_beam.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/kernels/geometry_vjp.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/kernels/parallel_beam.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/diffct/utils.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/docs/video/README.md +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/examples/data/NOTICE +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/pytest.ini +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/__init__.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/_phantoms.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/benchmarks/README.md +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/benchmarks/__init__.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/benchmarks/_common.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/benchmarks/test_bench_cone.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/benchmarks/test_bench_fan.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/benchmarks/test_bench_parallel.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/distributed_projector_check.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_adjoint_inner_product.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_analytical_regressions.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_cone_projector_autograd.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_cuda_exit_deallocations.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_cuda_smoke.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_detector_surface_dtypes.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_detector_surface_mapping.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_detector_surface_units.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_detector_surfaces.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_fbp_fan_accuracy.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_fbp_fan_offsets.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_fbp_parallel_accuracy.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_fdk_cone_accuracy.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_fdk_cone_offsets.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_geometry_autograd.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_geometry_gradients.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_gradcheck.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_multi_gpu_exit.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_projector_api.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_projector_degenerate_geometry.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_projector_geometry_devices.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_ramp_filter_windows.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_release_2_0_1.py +0 -0
- {diffct-2.1.0 → diffct-2.2.0}/tests/test_weights.py +0 -0
|
@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.2.0] - 2026-10-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Operation-specific automatic schedules and slab/block comparisons for
|
|
15
|
+
native projection and matched backprojection, with bounded CUDA pilots
|
|
16
|
+
and per-call execution/copy metadata.
|
|
17
|
+
- Detector-pixel tiling and explicit `ParameterizedSurface` batches using
|
|
18
|
+
global pixel/view coordinates, saved parameter/frame values and local VJP
|
|
19
|
+
reduction. Legacy two-argument surface callbacks remain compatible.
|
|
20
|
+
- Caller-owned numerical `project_into`/`backproject_into` outputs, tensor
|
|
21
|
+
block access and mapped `.npy` stores for data exceeding host RAM.
|
|
22
|
+
- Distributed `partition="space"` with fixed volume slabs, ownership-aware
|
|
23
|
+
autograd/norms and disk CGLS state/checkpoints without full-volume gathering.
|
|
24
|
+
- Independent CUDA tests and bounded formal protocol/ownership verification.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- GPU backprojection accumulators now span all contributing view batches,
|
|
29
|
+
with final transfer after tile completion. Contiguous native volume layouts
|
|
30
|
+
and ray buffers are reused across batches.
|
|
31
|
+
- Transfer execution uses bounded pinned slots, separate streams and events;
|
|
32
|
+
all extra staging/cached GPU buffers are included in capacity estimates.
|
|
33
|
+
Local GPU and NCCL reductions occur before completed-result downloads.
|
|
34
|
+
- Example CGLS norms accumulate FP64 blocks and vector updates reuse buffers.
|
|
35
|
+
The disk example keeps all three volume states plus residual/projection state
|
|
36
|
+
blockwise and retains `q` until the global `alpha` decision.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Tensor CGLS ranks agree on CPU fallback before jointly replanning unequal
|
|
41
|
+
spatial shards, preserving collective order.
|
|
42
|
+
- Parameterized detector surfaces use a common arithmetic dtype of at least
|
|
43
|
+
float32, supporting mixed frame dtypes and finite half-precision inputs.
|
|
44
|
+
- CUDA store uploads wait for newly generated blocks on their source stream,
|
|
45
|
+
including transfers within one GPU.
|
|
46
|
+
- Single-slot execution retires prior native computation before allocating
|
|
47
|
+
the next tile, keeping deferred CUDA allocations within its admission bound.
|
|
48
|
+
- Mapped input and output stores reject hard links to the same file before writes.
|
|
49
|
+
- Example PSNR supports CPU reconstruction results with CUDA reference volumes.
|
|
50
|
+
- Deferred CPU output writes retain their submission order across spatial tiles.
|
|
51
|
+
- Coronal slice displays preserve the measured detector row orientation.
|
|
52
|
+
|
|
10
53
|
## [2.1.0] - 2026-10-10
|
|
11
54
|
|
|
12
55
|
### Added
|
|
@@ -13,8 +13,8 @@ authors:
|
|
|
13
13
|
given-names: Yipeng
|
|
14
14
|
email: yipeng.sun@fau.de
|
|
15
15
|
alias: sypsyp97
|
|
16
|
-
version: 2.
|
|
17
|
-
date-released: "2026-10-
|
|
16
|
+
version: 2.2.0
|
|
17
|
+
date-released: "2026-10-11"
|
|
18
18
|
license: Apache-2.0
|
|
19
19
|
repository-code: "https://github.com/sypsyp97/diffct"
|
|
20
20
|
url: "https://sypsyp97.github.io/diffct/"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: diffct
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Differentiable CUDA projectors for CT: arbitrary trajectories, multi-GPU and multi-node execution, geometry gradients
|
|
5
5
|
Project-URL: Homepage, https://github.com/sypsyp97/diffct
|
|
6
6
|
Project-URL: Documentation, https://sypsyp97.github.io/diffct/
|
|
@@ -65,8 +65,6 @@ Description-Content-Type: text/markdown
|
|
|
65
65
|
<a href="#citation">Citation</a>
|
|
66
66
|
</p>
|
|
67
67
|
|
|
68
|
-
> **Install** with the CUDA extra, `pip install "diffct[cu12]"` or `pip install "diffct[cu13]"`, after PyTorch. A plain `pip install diffct` needs a system CUDA Toolkit.
|
|
69
|
-
>
|
|
70
68
|
> **Version 2.0** replaces the circular-orbit API of 1.x with per-view trajectories. Code written for 1.x needs changes; see the [migration notes](https://github.com/sypsyp97/diffct/blob/main/docs/MIGRATION.md).
|
|
71
69
|
> For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
|
|
72
70
|
|
|
@@ -75,10 +73,10 @@ Description-Content-Type: text/markdown
|
|
|
75
73
|
To our knowledge, diffct is the only open-source GPU CT library that combines arbitrary per-view trajectories, autograd through the volume, first-order gradients through the acquisition geometry, and built-in multi-GPU and multi-node execution. We compared it with LEAP, TIGRE, ASTRA/tomosipo, DiffDRR and others in October 2026.
|
|
76
74
|
|
|
77
75
|
- **Arbitrary trajectories.** Each view has its own source, detector centre and detector axes. Circular, helical, saddle, sinusoidal, random and calibrated scans use the same code. Trajectory tensors with `requires_grad=True` receive geometry gradients for calibration.
|
|
78
|
-
- **Curved detectors.**
|
|
76
|
+
- **Curved detectors.** `ParameterizedSurface` defines native arc, cylindrical or arbitrary pixels from global coordinates and explicit parameters, sampled in bounded view/pixel batches. Legacy `detector_surface(u, v)` callbacks remain available. Projection and matched backprojection support first-order surface gradients. See [the surface API](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#parameterized-detector-surfaces).
|
|
79
77
|
- **Matched operators.** `project()` and `backproject()` form an exact adjoint pair for the cell-constant Siddon model. Both support PyTorch autograd, with volume/sinogram gradients and Hessian-vector products.
|
|
80
|
-
- **Large volumes.**
|
|
81
|
-
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or
|
|
78
|
+
- **Large volumes.** Automatic spatial, view and pixel blocks with bounded transfer buffers control working memory. `project_into`/`backproject_into` accept caller-owned tensor or disk stores; tensor autograd keeps complete data on its input device. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
|
|
79
|
+
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or torchrun and NCCL across ranks. Default `partition="views"` shards projections and replicates volumes. `partition="space"` gives each rank an owned volume slab and combines ray contributions. Speedup depends on workload and communication costs.
|
|
82
80
|
- **Analytical helpers.** `diffct.analytical` provides ramp filters (ram-lak, shepp-logan, cosine, hamming, hann), fan, cone and Parker weights, and FBP and FDK backprojection.
|
|
83
81
|
|
|
84
82
|
Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
|
|
@@ -140,7 +138,7 @@ loss.backward() # x.grad = A^T (A x - y)
|
|
|
140
138
|
|
|
141
139
|
**Geometry gradients.** Set `requires_grad=True` on trajectory tensors before you build `Projector`. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#geometry-gradients).
|
|
142
140
|
|
|
143
|
-
**Several GPUs and nodes.**
|
|
141
|
+
**Several GPUs and nodes.** See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#several-gpus-and-nodes) and [DISTRIBUTED.md](https://github.com/sypsyp97/diffct/blob/main/docs/DISTRIBUTED.md).
|
|
144
142
|
|
|
145
143
|
```python
|
|
146
144
|
A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, devices=[0, 1, 2, 3])
|
|
@@ -161,29 +159,36 @@ sinogram.square().mean().backward() # CPU volume.grad
|
|
|
161
159
|
Run `python examples/chunked_reconstruction.py` for CPU-backed CGLS and measured
|
|
162
160
|
CUDA allocation. Chunking preserves the Siddon model, with float32 summation
|
|
163
161
|
differences; smaller blocks can increase transfer and launch overhead. Full
|
|
164
|
-
arrays and iterative state still need enough host RAM.
|
|
162
|
+
tensor arrays and iterative state still need enough host RAM. For disk-backed
|
|
163
|
+
state and sharded checkpoints, run
|
|
164
|
+
`python examples/disk_reconstruction.py --output disk-run`; for fixed rank-local
|
|
165
|
+
volume slabs use `--partition space` under torchrun. Run
|
|
166
|
+
`python examples/benchmark_execution.py` to compare schedules and copy payloads.
|
|
165
167
|
|
|
166
168
|
## Performance
|
|
167
169
|
|
|
168
170
|
<p align="center">
|
|
169
171
|
<picture>
|
|
170
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
|
|
171
|
-
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs,
|
|
172
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.2.0">
|
|
173
|
+
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.2.0" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
|
|
172
174
|
</picture>
|
|
173
175
|
</p>
|
|
174
176
|
|
|
175
|
-
|
|
|
177
|
+
| 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
|
|
176
178
|
|---|---:|---:|---:|
|
|
177
|
-
| Forward projection |
|
|
178
|
-
| Adjoint (backprojection) |
|
|
179
|
-
| CGLS iteration |
|
|
179
|
+
| Forward projection | 493.05 ms | 132.22 ms (3.7×) | 73.18 ms (6.7×) |
|
|
180
|
+
| Adjoint (backprojection) | 1400.80 ms | 369.29 ms (3.8×) | 265.87 ms (5.3×) |
|
|
181
|
+
| CGLS iteration | 2054.36 ms | 547.06 ms (3.8×) | 373.38 ms (5.5×) |
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
diffct 2.2.0, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
|
|
182
184
|
|
|
183
|
-
|
|
185
|
+
Projection times are medians of seven samples after three warmups. CGLS uses three ten-iteration solves after a two-iteration warmup; the median includes initialization and is divided by ten. Timings use synchronized wall time and the slowest rank. Raw samples and configuration: [scaling.json](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
|
|
186
|
+
|
|
187
|
+
Run the same workload:
|
|
184
188
|
|
|
185
189
|
```bash
|
|
186
|
-
python examples/
|
|
190
|
+
python examples/benchmark_scaling.py --output one.json
|
|
191
|
+
torchrun --standalone --nproc-per-node=4 examples/benchmark_scaling.py --expected-nodes 1 --output four.json
|
|
187
192
|
```
|
|
188
193
|
|
|
189
194
|
## Gallery
|
|
@@ -34,8 +34,6 @@
|
|
|
34
34
|
<a href="#citation">Citation</a>
|
|
35
35
|
</p>
|
|
36
36
|
|
|
37
|
-
> **Install** with the CUDA extra, `pip install "diffct[cu12]"` or `pip install "diffct[cu13]"`, after PyTorch. A plain `pip install diffct` needs a system CUDA Toolkit.
|
|
38
|
-
>
|
|
39
37
|
> **Version 2.0** replaces the circular-orbit API of 1.x with per-view trajectories. Code written for 1.x needs changes; see the [migration notes](https://github.com/sypsyp97/diffct/blob/main/docs/MIGRATION.md).
|
|
40
38
|
> For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
|
|
41
39
|
|
|
@@ -44,10 +42,10 @@
|
|
|
44
42
|
To our knowledge, diffct is the only open-source GPU CT library that combines arbitrary per-view trajectories, autograd through the volume, first-order gradients through the acquisition geometry, and built-in multi-GPU and multi-node execution. We compared it with LEAP, TIGRE, ASTRA/tomosipo, DiffDRR and others in October 2026.
|
|
45
43
|
|
|
46
44
|
- **Arbitrary trajectories.** Each view has its own source, detector centre and detector axes. Circular, helical, saddle, sinusoidal, random and calibrated scans use the same code. Trajectory tensors with `requires_grad=True` receive geometry gradients for calibration.
|
|
47
|
-
- **Curved detectors.**
|
|
45
|
+
- **Curved detectors.** `ParameterizedSurface` defines native arc, cylindrical or arbitrary pixels from global coordinates and explicit parameters, sampled in bounded view/pixel batches. Legacy `detector_surface(u, v)` callbacks remain available. Projection and matched backprojection support first-order surface gradients. See [the surface API](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#parameterized-detector-surfaces).
|
|
48
46
|
- **Matched operators.** `project()` and `backproject()` form an exact adjoint pair for the cell-constant Siddon model. Both support PyTorch autograd, with volume/sinogram gradients and Hessian-vector products.
|
|
49
|
-
- **Large volumes.**
|
|
50
|
-
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or
|
|
47
|
+
- **Large volumes.** Automatic spatial, view and pixel blocks with bounded transfer buffers control working memory. `project_into`/`backproject_into` accept caller-owned tensor or disk stores; tensor autograd keeps complete data on its input device. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
|
|
48
|
+
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or torchrun and NCCL across ranks. Default `partition="views"` shards projections and replicates volumes. `partition="space"` gives each rank an owned volume slab and combines ray contributions. Speedup depends on workload and communication costs.
|
|
51
49
|
- **Analytical helpers.** `diffct.analytical` provides ramp filters (ram-lak, shepp-logan, cosine, hamming, hann), fan, cone and Parker weights, and FBP and FDK backprojection.
|
|
52
50
|
|
|
53
51
|
Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
|
|
@@ -109,7 +107,7 @@ loss.backward() # x.grad = A^T (A x - y)
|
|
|
109
107
|
|
|
110
108
|
**Geometry gradients.** Set `requires_grad=True` on trajectory tensors before you build `Projector`. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#geometry-gradients).
|
|
111
109
|
|
|
112
|
-
**Several GPUs and nodes.**
|
|
110
|
+
**Several GPUs and nodes.** See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#several-gpus-and-nodes) and [DISTRIBUTED.md](https://github.com/sypsyp97/diffct/blob/main/docs/DISTRIBUTED.md).
|
|
113
111
|
|
|
114
112
|
```python
|
|
115
113
|
A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, devices=[0, 1, 2, 3])
|
|
@@ -130,29 +128,36 @@ sinogram.square().mean().backward() # CPU volume.grad
|
|
|
130
128
|
Run `python examples/chunked_reconstruction.py` for CPU-backed CGLS and measured
|
|
131
129
|
CUDA allocation. Chunking preserves the Siddon model, with float32 summation
|
|
132
130
|
differences; smaller blocks can increase transfer and launch overhead. Full
|
|
133
|
-
arrays and iterative state still need enough host RAM.
|
|
131
|
+
tensor arrays and iterative state still need enough host RAM. For disk-backed
|
|
132
|
+
state and sharded checkpoints, run
|
|
133
|
+
`python examples/disk_reconstruction.py --output disk-run`; for fixed rank-local
|
|
134
|
+
volume slabs use `--partition space` under torchrun. Run
|
|
135
|
+
`python examples/benchmark_execution.py` to compare schedules and copy payloads.
|
|
134
136
|
|
|
135
137
|
## Performance
|
|
136
138
|
|
|
137
139
|
<p align="center">
|
|
138
140
|
<picture>
|
|
139
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
|
|
140
|
-
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs,
|
|
141
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.2.0">
|
|
142
|
+
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.2.0" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
|
|
141
143
|
</picture>
|
|
142
144
|
</p>
|
|
143
145
|
|
|
144
|
-
|
|
|
146
|
+
| 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
|
|
145
147
|
|---|---:|---:|---:|
|
|
146
|
-
| Forward projection |
|
|
147
|
-
| Adjoint (backprojection) |
|
|
148
|
-
| CGLS iteration |
|
|
148
|
+
| Forward projection | 493.05 ms | 132.22 ms (3.7×) | 73.18 ms (6.7×) |
|
|
149
|
+
| Adjoint (backprojection) | 1400.80 ms | 369.29 ms (3.8×) | 265.87 ms (5.3×) |
|
|
150
|
+
| CGLS iteration | 2054.36 ms | 547.06 ms (3.8×) | 373.38 ms (5.5×) |
|
|
149
151
|
|
|
150
|
-
|
|
152
|
+
diffct 2.2.0, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
Projection times are medians of seven samples after three warmups. CGLS uses three ten-iteration solves after a two-iteration warmup; the median includes initialization and is divided by ten. Timings use synchronized wall time and the slowest rank. Raw samples and configuration: [scaling.json](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
|
|
155
|
+
|
|
156
|
+
Run the same workload:
|
|
153
157
|
|
|
154
158
|
```bash
|
|
155
|
-
python examples/
|
|
159
|
+
python examples/benchmark_scaling.py --output one.json
|
|
160
|
+
torchrun --standalone --nproc-per-node=4 examples/benchmark_scaling.py --expected-nodes 1 --output four.json
|
|
156
161
|
```
|
|
157
162
|
|
|
158
163
|
## Gallery
|
|
@@ -15,6 +15,8 @@ from .projectors import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
from .operators import Projector
|
|
18
|
+
from .storage import TensorStore, NpyStore
|
|
19
|
+
from .surfaces import ParameterizedSurface
|
|
18
20
|
|
|
19
21
|
from .geometry import (
|
|
20
22
|
circular_trajectory_3d,
|
|
@@ -49,10 +51,13 @@ try:
|
|
|
49
51
|
except ImportError:
|
|
50
52
|
pass
|
|
51
53
|
|
|
52
|
-
__version__ = '2.
|
|
54
|
+
__version__ = '2.2.0'
|
|
53
55
|
|
|
54
56
|
__all__ = [
|
|
55
57
|
'Projector',
|
|
58
|
+
'TensorStore',
|
|
59
|
+
'NpyStore',
|
|
60
|
+
'ParameterizedSurface',
|
|
56
61
|
'ParallelProjectorFunction',
|
|
57
62
|
'ParallelBackprojectorFunction',
|
|
58
63
|
'FanProjectorFunction',
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Spatial slices and immutable execution limits for streamed projectors."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from itertools import product
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class _ChunkPlan:
|
|
12
|
+
chunk_shape: tuple
|
|
13
|
+
view_chunk_size: int
|
|
14
|
+
devices: tuple
|
|
15
|
+
detector_chunk_shape: tuple = None
|
|
16
|
+
gpu_budget_bytes: int = 0
|
|
17
|
+
allocation_limit: int = (1 << 63) - 1
|
|
18
|
+
schedule: str = "spatial"
|
|
19
|
+
window_size: int = 1
|
|
20
|
+
slots: int = 1
|
|
21
|
+
estimated_gpu_bytes: int = 0
|
|
22
|
+
fallback_reason: str = ""
|
|
23
|
+
foreign_output: bool = False
|
|
24
|
+
reservation_budget_bytes: int = (1 << 63) - 1
|
|
25
|
+
estimated_reserved_bytes: int = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _pixel_tiles(shape, chunk_shape):
|
|
29
|
+
for starts in product(*(range(0, size, part) for size, part in zip(shape, chunk_shape))):
|
|
30
|
+
clipped = tuple(min(part, size - start) for size, start, part in zip(shape, starts, chunk_shape))
|
|
31
|
+
yield tuple(slice(start, start + size) for start, size in zip(starts, clipped)), clipped
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _memory_budget(device):
|
|
35
|
+
"""Usable allocator bytes, retaining 25% headroom for allocation overhead.
|
|
36
|
+
|
|
37
|
+
Reusable reserved blocks count as available, but never bypass the process
|
|
38
|
+
fraction ceiling. Older PyTorch without its public fraction getter can
|
|
39
|
+
only observe driver/allocator memory; explicit limits remain available.
|
|
40
|
+
"""
|
|
41
|
+
free, total = torch.cuda.mem_get_info(device)
|
|
42
|
+
allocated = torch.cuda.memory_allocated(device)
|
|
43
|
+
reusable = max(0, torch.cuda.memory_reserved(device) - allocated)
|
|
44
|
+
getter = getattr(torch.cuda, "get_per_process_memory_fraction", None)
|
|
45
|
+
fraction = getter(device) if getter is not None else 1.0
|
|
46
|
+
allocator_room = max(0, int(total * fraction) - allocated)
|
|
47
|
+
backend = getattr(torch.cuda, "get_allocator_backend", lambda: "native")()
|
|
48
|
+
foreign_splits = 0
|
|
49
|
+
if backend == "native":
|
|
50
|
+
caller = torch.cuda.current_stream(device).cuda_stream
|
|
51
|
+
for segment in _native_segments(device):
|
|
52
|
+
blocks = segment["blocks"]
|
|
53
|
+
if segment["stream"] != caller and any(block["state"] != "inactive" for block in blocks):
|
|
54
|
+
foreign_splits += sum(block["size"] for block in blocks if block["state"] == "inactive")
|
|
55
|
+
available = max(0, min(free + reusable, allocator_room) - foreign_splits)
|
|
56
|
+
budget = available * 3 // 4
|
|
57
|
+
allocation_limit = (1 << 63) - 1 # Unbounded limit in scalar int64 agreement.
|
|
58
|
+
# Native requests >1 MiB and <10 MiB reserve a 20 MiB segment. A smaller
|
|
59
|
+
# current available room must use the <=1 MiB small pool, whose segments are 2 MiB.
|
|
60
|
+
# Source: pytorch/c10/cuda/CUDACachingAllocator.cpp (Native allocator notes).
|
|
61
|
+
if backend == "native" and available < 20 * 1024 ** 2:
|
|
62
|
+
allocation_limit = 1024 ** 2
|
|
63
|
+
return budget, allocation_limit
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _native_segments(device):
|
|
67
|
+
index = torch.device(device).index if not isinstance(device, int) else device
|
|
68
|
+
index = torch.cuda.current_device() if index is None else index
|
|
69
|
+
return [segment for segment in torch.cuda.memory_snapshot() if segment["device"] == index]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _reservation_budget(device):
|
|
73
|
+
"""Room for fresh segments, conservatively excluding every pinned segment.
|
|
74
|
+
|
|
75
|
+
Free splits in a live segment may serve the caller, but cannot guarantee
|
|
76
|
+
placement of the complete new inventory. Wholly inactive segments can be
|
|
77
|
+
released by the native allocator, including those owned by other streams.
|
|
78
|
+
"""
|
|
79
|
+
if torch.cuda.get_allocator_backend() != "native":
|
|
80
|
+
return (1 << 63) - 1
|
|
81
|
+
free, total = torch.cuda.mem_get_info(device)
|
|
82
|
+
segments = _native_segments(device)
|
|
83
|
+
pinned = sum(segment["total_size"] for segment in segments
|
|
84
|
+
if any(block["state"] != "inactive" for block in segment["blocks"]))
|
|
85
|
+
reclaimable = sum(segment["total_size"] for segment in segments
|
|
86
|
+
if all(block["state"] == "inactive" for block in segment["blocks"]))
|
|
87
|
+
getter = getattr(torch.cuda, "get_per_process_memory_fraction", None)
|
|
88
|
+
fraction = getter(device) if getter is not None else 1.0
|
|
89
|
+
return max(0, min(free + reclaimable, int(total * fraction) - pinned))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _working_set_bytes(beam, shape, views, detector_shape, surface):
|
|
93
|
+
"""Conservative native staging/layout, cotangent, geometry and VJP scratch."""
|
|
94
|
+
rank = len(shape)
|
|
95
|
+
voxels = math.prod(shape)
|
|
96
|
+
rays = views * math.prod(detector_shape)
|
|
97
|
+
geometry = (rank * (views + rays) if surface
|
|
98
|
+
else rank * (4 if beam == "cone" else 3) * views)
|
|
99
|
+
# A resident native tile, staging and completed cone layout can coexist.
|
|
100
|
+
# Two ray buffers cover reusable output/cotangent and transfer scratch; two
|
|
101
|
+
# geometry buffers cover staged values and their first-order VJP/reduction.
|
|
102
|
+
tile_buffers = 3 if beam == "cone" else 2
|
|
103
|
+
return 4 * (tile_buffers * voxels + 2 * rays + 2 * geometry)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _largest_buffer_bytes(beam, shape, views, detector_shape, surface):
|
|
107
|
+
rank = len(shape)
|
|
108
|
+
rays = views * math.prod(detector_shape)
|
|
109
|
+
geometry = (rank * (views + rays) if surface
|
|
110
|
+
else rank * (4 if beam == "cone" else 3) * views)
|
|
111
|
+
return 4 * max(math.prod(shape), rays, geometry)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _allocation_bytes(elements):
|
|
115
|
+
size = 4 * elements
|
|
116
|
+
if torch.cuda.get_allocator_backend() == "native":
|
|
117
|
+
return (size + 511) // 512 * 512
|
|
118
|
+
return size
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _reservation_bytes(buffer_sizes):
|
|
122
|
+
"""Fresh, monotone fixed-buffer inventory bound for standard native pools.
|
|
123
|
+
|
|
124
|
+
Arbitrary free holes and allocator overrides are outside this model. Large
|
|
125
|
+
and middle requests share one pool, so mixed inventories use separate fresh
|
|
126
|
+
segments per request instead of assuming their placement is independent.
|
|
127
|
+
"""
|
|
128
|
+
sizes = [size for size in buffer_sizes if size > 0]
|
|
129
|
+
if torch.cuda.get_allocator_backend() != "native":
|
|
130
|
+
return sum(sizes)
|
|
131
|
+
mib = 1024 ** 2
|
|
132
|
+
sizes = [(size + 511) // 512 * 512 for size in sizes]
|
|
133
|
+
large = [size for size in sizes if size >= 10 * mib]
|
|
134
|
+
reservation = sum((size + 2 * mib - 1) // (2 * mib) * (2 * mib) for size in large)
|
|
135
|
+
middle = [size for size in sizes if mib < size < 10 * mib]
|
|
136
|
+
if large:
|
|
137
|
+
reservation += len(middle) * 20 * mib
|
|
138
|
+
middle = []
|
|
139
|
+
for segment_size, requests in (
|
|
140
|
+
(2 * mib, [size for size in sizes if size <= mib]),
|
|
141
|
+
(20 * mib, middle),
|
|
142
|
+
):
|
|
143
|
+
if not requests:
|
|
144
|
+
continue
|
|
145
|
+
total = sum(requests)
|
|
146
|
+
# At a new segment's birth every older segment holds more than
|
|
147
|
+
# segment_size - max(requests); the new one holds at least min(requests).
|
|
148
|
+
# This bounds online best-fit placement in any allocation order.
|
|
149
|
+
count = (1 if total <= segment_size else
|
|
150
|
+
(total - min(requests)) // (segment_size - max(requests)) + 1)
|
|
151
|
+
reservation += count * segment_size
|
|
152
|
+
return reservation
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _pipeline_buffers(beam, shape, views, pixels, points, *, slots=1, retained=1,
|
|
156
|
+
geometry_grad=False, peers=False, ray_accumulators=0):
|
|
157
|
+
"""Live typed allocation inventory, with native rounding per buffer."""
|
|
158
|
+
rank, rays = len(shape), views * math.prod(pixels)
|
|
159
|
+
native = _allocation_bytes(math.prod(shape))
|
|
160
|
+
# Two producer epochs permit the preceding native/layout/output storage
|
|
161
|
+
# to stay in flight while the next retained tile window queues work.
|
|
162
|
+
tiles = slots * (retained + int(beam == "cone")) + int(peers)
|
|
163
|
+
sizes = ((views * rank, rays * rank) if points
|
|
164
|
+
else (views * rank,) * (4 if beam == "cone" else 3))
|
|
165
|
+
geometry = tuple(_allocation_bytes(size) for size in sizes)
|
|
166
|
+
return ((native,) * tiles + (_allocation_bytes(rays),) * (slots + ray_accumulators)
|
|
167
|
+
+ geometry * (slots * (1 + int(geometry_grad))))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _pipeline_bytes(beam, shape, views, pixels, points, *, slots=1, retained=1,
|
|
171
|
+
geometry_grad=False, peers=False, ray_accumulators=0):
|
|
172
|
+
"""Live typed bytes; segment reservation is admitted separately."""
|
|
173
|
+
rank, rays = len(shape), views * math.prod(pixels)
|
|
174
|
+
native = _allocation_bytes(math.prod(shape))
|
|
175
|
+
tile = native * (slots * (retained + int(beam == "cone")) + int(peers))
|
|
176
|
+
sizes = ((views * rank, rays * rank) if points
|
|
177
|
+
else (views * rank,) * (4 if beam == "cone" else 3))
|
|
178
|
+
geometry = sum(_allocation_bytes(size) for size in sizes)
|
|
179
|
+
return (tile + slots * (_allocation_bytes(rays) + geometry)
|
|
180
|
+
+ ray_accumulators * _allocation_bytes(rays) + (slots * geometry if geometry_grad else 0))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _host_geometry_bytes(beam, views, pixels, points):
|
|
184
|
+
rank = 3 if beam == "cone" else 2
|
|
185
|
+
if not points:
|
|
186
|
+
return 8 * 2 * rank * views * (4 if beam == "cone" else 3)
|
|
187
|
+
# Grids/offsets, world expressions and first-order cotangent/graph work.
|
|
188
|
+
# Arbitrary sampler internals and explicit parameter snapshots are separate.
|
|
189
|
+
return 8 * rank * (math.prod(pixels) * (1 + 6 * views) + 4 * views)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _tiles(shape, chunk_shape, spacing):
|
|
193
|
+
"""Yield tensor slices, clipped shapes and physical centres in xyz order."""
|
|
194
|
+
starts = (range(0, size, chunk) for size, chunk in zip(shape, chunk_shape))
|
|
195
|
+
for start in product(*starts):
|
|
196
|
+
tile_shape = tuple(min(chunk, size - first)
|
|
197
|
+
for size, chunk, first in zip(shape, chunk_shape, start))
|
|
198
|
+
slices = tuple(slice(first, first + size) for first, size in zip(start, tile_shape))
|
|
199
|
+
centre = tuple((first + size / 2 - full / 2) * spacing
|
|
200
|
+
for first, size, full in zip(start, tile_shape, shape))
|
|
201
|
+
yield slices, tile_shape, centre[::-1]
|