diffct 2.4.2__tar.gz → 2.4.3__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.4.2 → diffct-2.4.3}/CHANGELOG.md +16 -0
- {diffct-2.4.2 → diffct-2.4.3}/CITATION.cff +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/PKG-INFO +9 -9
- {diffct-2.4.2 → diffct-2.4.3}/README.md +8 -8
- {diffct-2.4.2 → diffct-2.4.3}/diffct/__init__.py +1 -1
- {diffct-2.4.2 → diffct-2.4.3}/diffct/_solver_state.py +12 -9
- {diffct-2.4.2 → diffct-2.4.3}/diffct/_solver_tv.py +3 -2
- {diffct-2.4.2 → diffct-2.4.3}/diffct/analytical.py +3 -6
- {diffct-2.4.2 → diffct-2.4.3}/diffct/analytical_execution.py +17 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/backends.py +8 -3
- {diffct-2.4.2 → diffct-2.4.3}/diffct/chunking.py +1 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/kernels/surface_geometry.py +7 -5
- {diffct-2.4.2 → diffct-2.4.3}/diffct/operators.py +233 -95
- {diffct-2.4.2 → diffct-2.4.3}/diffct/solvers.py +22 -11
- {diffct-2.4.2 → diffct-2.4.3}/diffct/surfaces.py +13 -12
- {diffct-2.4.2 → diffct-2.4.3}/diffct/utils.py +10 -0
- {diffct-2.4.2 → diffct-2.4.3}/examples/README.md +3 -0
- {diffct-2.4.2 → diffct-2.4.3}/formal/README.md +1 -0
- {diffct-2.4.2 → diffct-2.4.3}/pyproject.toml +1 -1
- diffct-2.4.3/tests/_multiprocessing.py +26 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_allocator_capacity.py +7 -7
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_batched_surfaces.py +55 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_block_storage.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_cgls_state_admission.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_chunked_projector.py +5 -5
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_completed_transfer_reuse.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_detector_surfaces.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_distributed_training_transforms.py +2 -7
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_execution_buffers.py +67 -4
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_execution_features.py +2 -2
- diffct-2.4.3/tests/test_gpu_performance_contracts.py +229 -0
- diffct-2.4.3/tests/test_multiprocessing.py +78 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_native_volume_layout_distributed.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_parallel3d.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_portable_distributed_geometry.py +2 -2
- diffct-2.4.3/tests/test_pr42_review_regressions.py +301 -0
- diffct-2.4.3/tests/test_reconstruction_work_reuse.py +231 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_solver_admission_regressions.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_solver_collective_failures.py +7 -5
- diffct-2.4.3/tests/test_solver_device_scalars.py +255 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_spatial_partition.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_collective_failures.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_compute_shards.py +3 -3
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_geometry_cudasim.py +4 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_pipeline_contracts.py +10 -6
- diffct-2.4.3/tests/test_surface_saved_snapshot_budget.py +274 -0
- diffct-2.4.3/tests/test_surface_snapshot_admission.py +209 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_trajectory_surface_integration.py +8 -4
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_transfer_pipeline.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_transfer_retirement_lifetimes.py +2 -2
- {diffct-2.4.2 → diffct-2.4.3}/.gitignore +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/LICENSE +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/NOTICE +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/constants.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/differentiable.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/distribution.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/geometry.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/kernels/__init__.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/kernels/analytical_transpose.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/kernels/cone_beam.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/kernels/fan_beam.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/kernels/geometry_vjp.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/kernels/parallel_beam.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/pipeline.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/projectors.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/reconstruction.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/storage.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/streaming.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/diffct/surface_gradients.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/docs/video/README.md +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/examples/_block_cgls.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/examples/_common.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/examples/data/NOTICE +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/examples/data/preprocess_walnut.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/examples/walnut_reconstruction.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/pytest.ini +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/__init__.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/_phantoms.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/benchmarks/README.md +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/benchmarks/__init__.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/benchmarks/_common.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/benchmarks/test_bench_cone.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/benchmarks/test_bench_fan.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/benchmarks/test_bench_parallel.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/distributed_projector_check.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_adjoint_inner_product.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_analytical_autograd.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_analytical_distribution.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_analytical_regressions.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_analytical_weight_distribution.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_angular_low_precision.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_axis_aligned_chords.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_batch_execution_edges.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_block_cgls.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_call_trajectory.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_checkpoint_metadata_validation.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_cone_backprojection_transpose.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_cone_projector_autograd.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_cpu_install.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_cuda_exit_deallocations.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_cuda_sink_lifetimes.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_cuda_smoke.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_detector_surface_dtypes.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_detector_surface_mapping.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_detector_surface_units.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_example_psnr.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_execution_schedules.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_fbp_fan_accuracy.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_fbp_fan_offsets.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_fbp_parallel_accuracy.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_fdk_cone_accuracy.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_fdk_cone_offsets.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_geometry_autograd.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_geometry_conversions.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_geometry_gradients.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_gradcheck.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_lazy_native_observers.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_live_trajectory_validation.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_multi_gpu_exit.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_native_endpoint_precision.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_native_surface_transforms.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_native_volume_layout.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_output_pipeline_overlap.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_parallel3d_surface_oracle.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_partition_lifecycle.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_plan_memory_bounds.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_portable_endpoint_precision.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_portable_execution_bounds.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_portable_numeric_regressions.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_portable_sampler_snapshot.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_portable_surface_admission.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_projector_api.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_projector_degenerate_geometry.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_projector_geometry_devices.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_ramp_filter_windows.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_reconstruction_api.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_reference_backend.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_release_2_0_1.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_review_230.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_review_regressions.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_solver_api.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_solver_checkpoint_edges.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_composition_kernel.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_gpu_execution.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_gradient_replay.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_host_sampling_budget.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_native_regressions.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_parameter_updates.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_surface_trajectory_transform.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_training_transforms.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_trajectory_admission_cost.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_tv_zero_dependency.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_two_far_endpoints.py +0 -0
- {diffct-2.4.2 → diffct-2.4.3}/tests/test_weights.py +0 -0
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.3] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Reduce GPU transfers and host synchronization in native geometry admission: validity flags are reduced on the device and flat detector corners are admitted in one batch. Invalid-geometry errors and their order are unchanged (#42).
|
|
15
|
+
- Compose streamed sampled and per-view detector surfaces for a complete admitted view/pixel batch in one launch, with a bounded fallback workspace. Eligible full legacy-surface calls keep their geometry snapshots on the input GPU (#42).
|
|
16
|
+
- Read the two CGLS scalar norms together, keep tensor-store updates and TV reductions on their device, and reuse SIRT/TV residuals when no callback can change the volume or operator (#42).
|
|
17
|
+
- Reuse filtered view batches across volume tiles in single-rank analytical reconstruction (#42).
|
|
18
|
+
- Add `examples/benchmark_gpu_overheads.py`, a synthetic benchmark for launch and transfer overheads (#42).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Geometry-gradient replay uses the surface workspace that admission selected. Surface admission charges the larger of the composition and replay totals, the largest allocation of either phase, and a reservation bound that counts both phases (#42).
|
|
23
|
+
- Spawned multi-process test workers exit after a successful teardown, which removes intermittent `terminate called without an active exception` aborts in CI (#43).
|
|
24
|
+
- Add a formal check that the union reservation bounds every modelled composition/replay allocation order (#44).
|
|
25
|
+
|
|
10
26
|
## [2.4.2] - 2026-10-21
|
|
11
27
|
|
|
12
28
|
### Fixed
|
|
@@ -13,8 +13,8 @@ authors:
|
|
|
13
13
|
given-names: Yipeng
|
|
14
14
|
email: yipeng.sun@fau.de
|
|
15
15
|
alias: sypsyp97
|
|
16
|
-
version: 2.4.
|
|
17
|
-
date-released: "2026-10-
|
|
16
|
+
version: 2.4.3
|
|
17
|
+
date-released: "2026-10-22"
|
|
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.4.
|
|
3
|
+
Version: 2.4.3
|
|
4
4
|
Summary: Differentiable CT projection and reconstruction: precision modes, streaming, distributed execution and geometry gradients
|
|
5
5
|
Project-URL: Homepage, https://github.com/sypsyp97/diffct
|
|
6
6
|
Project-URL: Documentation, https://sypsyp97.github.io/diffct/
|
|
@@ -68,7 +68,7 @@ Description-Content-Type: text/markdown
|
|
|
68
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).
|
|
69
69
|
> For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
|
|
70
70
|
|
|
71
|
-
This README describes the source checkout
|
|
71
|
+
This README describes the source checkout. Features listed under [Unreleased](CHANGELOG.md#unreleased) require installation from source.
|
|
72
72
|
|
|
73
73
|
## Why diffct
|
|
74
74
|
|
|
@@ -177,18 +177,18 @@ volume slabs use `--partition space` under torchrun. Run
|
|
|
177
177
|
|
|
178
178
|
<p align="center">
|
|
179
179
|
<picture>
|
|
180
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.4.
|
|
181
|
-
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.
|
|
180
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.4.3">
|
|
181
|
+
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.3" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
|
|
182
182
|
</picture>
|
|
183
183
|
</p>
|
|
184
184
|
|
|
185
185
|
| 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
|
|
186
186
|
|---|---:|---:|---:|
|
|
187
|
-
| Forward projection |
|
|
188
|
-
| Adjoint (backprojection) |
|
|
189
|
-
| CGLS iteration |
|
|
187
|
+
| Forward projection | 566.79 ms | 150.76 ms (3.8×) | 81.58 ms (6.9×) |
|
|
188
|
+
| Adjoint (backprojection) | 1060.77 ms | 279.37 ms (3.8×) | 224.04 ms (4.7×) |
|
|
189
|
+
| CGLS iteration | 1753.73 ms | 468.09 ms (3.7×) | 337.39 ms (5.2×) |
|
|
190
190
|
|
|
191
|
-
diffct 2.4.
|
|
191
|
+
diffct 2.4.3, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
|
|
192
192
|
|
|
193
193
|
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).
|
|
194
194
|
|
|
@@ -228,7 +228,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
|
|
|
228
228
|
|
|
229
229
|
## Examples
|
|
230
230
|
|
|
231
|
-
`quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `benchmark_trajectory_admission.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
231
|
+
`quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `benchmark_trajectory_admission.py`, `benchmark_surface_geometry.py`, `benchmark_gpu_overheads.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
232
232
|
|
|
233
233
|
## Documentation
|
|
234
234
|
|
|
@@ -37,7 +37,7 @@
|
|
|
37
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).
|
|
38
38
|
> For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
|
|
39
39
|
|
|
40
|
-
This README describes the source checkout
|
|
40
|
+
This README describes the source checkout. Features listed under [Unreleased](CHANGELOG.md#unreleased) require installation from source.
|
|
41
41
|
|
|
42
42
|
## Why diffct
|
|
43
43
|
|
|
@@ -146,18 +146,18 @@ volume slabs use `--partition space` under torchrun. Run
|
|
|
146
146
|
|
|
147
147
|
<p align="center">
|
|
148
148
|
<picture>
|
|
149
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.4.
|
|
150
|
-
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.
|
|
149
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.4.3">
|
|
150
|
+
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.3" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
|
|
151
151
|
</picture>
|
|
152
152
|
</p>
|
|
153
153
|
|
|
154
154
|
| 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
|
|
155
155
|
|---|---:|---:|---:|
|
|
156
|
-
| Forward projection |
|
|
157
|
-
| Adjoint (backprojection) |
|
|
158
|
-
| CGLS iteration |
|
|
156
|
+
| Forward projection | 566.79 ms | 150.76 ms (3.8×) | 81.58 ms (6.9×) |
|
|
157
|
+
| Adjoint (backprojection) | 1060.77 ms | 279.37 ms (3.8×) | 224.04 ms (4.7×) |
|
|
158
|
+
| CGLS iteration | 1753.73 ms | 468.09 ms (3.7×) | 337.39 ms (5.2×) |
|
|
159
159
|
|
|
160
|
-
diffct 2.4.
|
|
160
|
+
diffct 2.4.3, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
|
|
161
161
|
|
|
162
162
|
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).
|
|
163
163
|
|
|
@@ -197,7 +197,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
|
|
|
197
197
|
|
|
198
198
|
## Examples
|
|
199
199
|
|
|
200
|
-
`quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `benchmark_trajectory_admission.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
200
|
+
`quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `benchmark_trajectory_admission.py`, `benchmark_surface_geometry.py`, `benchmark_gpu_overheads.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
201
201
|
|
|
202
202
|
## Documentation
|
|
203
203
|
|
|
@@ -53,30 +53,32 @@ def owns_values(operator, kind):
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def scalar_sum(operator, value, kind):
|
|
57
|
-
"""Count physical rays/voxels once,
|
|
56
|
+
def scalar_sum(operator, value, kind, *, return_tensor=False):
|
|
57
|
+
"""Count physical rays/voxels once, optionally retaining the reduction tensor."""
|
|
58
58
|
world = getattr(operator, "world_size", 1)
|
|
59
|
+
result_device = value.device if isinstance(value, torch.Tensor) else torch.device("cpu")
|
|
59
60
|
value = value if owns_values(operator, kind) else 0.
|
|
60
61
|
if world > 1:
|
|
61
62
|
group = operator.process_group
|
|
62
63
|
device = torch.device("cuda", torch.cuda.current_device()) if dist.get_backend(group) == "nccl" else torch.device("cpu")
|
|
63
64
|
accumulator = torch.as_tensor(value, dtype=torch.float64, device=device).clone()
|
|
64
65
|
dist.all_reduce(accumulator, group=group)
|
|
65
|
-
return accumulator.item()
|
|
66
|
+
return accumulator if return_tensor else accumulator.item()
|
|
67
|
+
if return_tensor:
|
|
68
|
+
return torch.as_tensor(value, dtype=torch.float64, device=result_device)
|
|
66
69
|
return float(value)
|
|
67
70
|
|
|
68
71
|
|
|
69
|
-
def squared_norm(operator, value, kind, elements=1048576):
|
|
72
|
+
def squared_norm(operator, value, kind, elements=1048576, *, return_tensor=False):
|
|
70
73
|
store = TensorStore(value) if isinstance(value, torch.Tensor) else value
|
|
71
|
-
total =
|
|
74
|
+
total = torch.zeros((), dtype=torch.float64, device=getattr(store, "device", "cpu"))
|
|
72
75
|
if owns_values(operator, kind):
|
|
73
|
-
total = torch.zeros((), dtype=torch.float64, device=getattr(store, "device", "cpu"))
|
|
74
76
|
for selected in _fragments(tuple(store.shape), elements):
|
|
75
77
|
# Clone even an FP64 view: norms must never alter caller measurements.
|
|
76
78
|
block = store.read(selected).to(dtype=torch.float64).clone()
|
|
77
79
|
# Preserve the block reductions and their sequential FP64 order.
|
|
78
80
|
total.add_(block.square_().sum().to(device=total.device))
|
|
79
|
-
return scalar_sum(operator, total, kind)
|
|
81
|
+
return scalar_sum(operator, total, kind, return_tensor=return_tensor)
|
|
80
82
|
|
|
81
83
|
|
|
82
84
|
def copy_blocks(source, target, elements):
|
|
@@ -91,9 +93,10 @@ def fill_blocks(target, elements):
|
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
def update_blocks(target, source, factor, elements, scale=1.):
|
|
96
|
+
device = getattr(target, "device", "cpu")
|
|
94
97
|
for selected in _fragments(tuple(target.shape), elements):
|
|
95
|
-
block = target.read(selected).to(device=
|
|
96
|
-
block.mul_(scale).add_(source.read(selected).to(device=
|
|
98
|
+
block = target.read(selected).to(device=device, dtype=target.dtype).clone()
|
|
99
|
+
block.mul_(scale).add_(source.read(selected).to(device=device, dtype=target.dtype), alpha=factor)
|
|
97
100
|
target.write(selected, block)
|
|
98
101
|
|
|
99
102
|
|
|
@@ -104,7 +104,8 @@ def total_variation(volume, *, operator=None, voxel_spacing=1., eps=1e-6, reduct
|
|
|
104
104
|
total = local
|
|
105
105
|
elements = volume.numel()
|
|
106
106
|
if operator is not None:
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if getattr(operator, "world_size", 1) > 1:
|
|
108
|
+
global_value = scalar_sum(operator, local.detach(), "volume", return_tensor=True)
|
|
109
|
+
total = local + (global_value.to(local) - local.detach())
|
|
109
110
|
elements = math.prod(operator.global_volume_shape)
|
|
110
111
|
return total if reduction == "sum" else total / max(elements, 1)
|
|
@@ -523,8 +523,7 @@ def _fan_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec,
|
|
|
523
523
|
d_sino, n_views, n_det, d_reco, Nx, Ny,
|
|
524
524
|
_DTYPE(detector_spacing), d_src_pos, d_det_center, d_det_u_vec,
|
|
525
525
|
cx, cy, _DTYPE(voxel_spacing),
|
|
526
|
-
_DTYPE(
|
|
527
|
-
_DTYPE(isocenter[1].item() / voxel_spacing),
|
|
526
|
+
*(_DTYPE(value / voxel_spacing) for value in isocenter.tolist()),
|
|
528
527
|
)
|
|
529
528
|
|
|
530
529
|
return reco
|
|
@@ -586,9 +585,7 @@ def _cone_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec, det
|
|
|
586
585
|
_DTYPE(du), _DTYPE(dv),
|
|
587
586
|
d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
|
|
588
587
|
cx, cy, cz, _DTYPE(voxel_spacing),
|
|
589
|
-
_DTYPE(
|
|
590
|
-
_DTYPE(isocenter[1].item() / voxel_spacing),
|
|
591
|
-
_DTYPE(isocenter[2].item() / voxel_spacing),
|
|
588
|
+
*(_DTYPE(value / voxel_spacing) for value in isocenter.tolist()),
|
|
592
589
|
)
|
|
593
590
|
|
|
594
591
|
# Convert back to (D, H, W) layout for the user.
|
|
@@ -640,7 +637,7 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
|
|
|
640
637
|
scale = (sdd_n / (2.0 * math.pi * sid_n)).view(
|
|
641
638
|
(-1,) + (1,) * (len(sinogram_shape) - 1)
|
|
642
639
|
)
|
|
643
|
-
iso = tuple(_DTYPE(value
|
|
640
|
+
iso = tuple(_DTYPE(value / parameters[-1]) for value in isocenter.tolist())
|
|
644
641
|
|
|
645
642
|
if beam == "cone":
|
|
646
643
|
D, H, W, du, dv, spacing = parameters
|
|
@@ -53,6 +53,8 @@ def _sum(value, group, count):
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def _sum_blocks(value, group, count, block_elements):
|
|
56
|
+
if count == 1:
|
|
57
|
+
return value
|
|
56
58
|
flat = value.reshape(-1)
|
|
57
59
|
blocks = [_sum(flat.narrow(0, start, min(block_elements, flat.numel() - start)), group, count)
|
|
58
60
|
for start in range(0, flat.numel(), block_elements)]
|
|
@@ -385,6 +387,21 @@ class AnalyticalExecution:
|
|
|
385
387
|
def tensor_result(self):
|
|
386
388
|
self._prepare_gradients()
|
|
387
389
|
result = torch.zeros(self.volume_shape, device=self.output_device, dtype=self.dtype)
|
|
390
|
+
inputs = (self.source, self.projection_weights, self.angular_weights, self.filter_response)
|
|
391
|
+
active_gradients = torch.is_grad_enabled() and any(value is not None and value.requires_grad
|
|
392
|
+
for value in inputs)
|
|
393
|
+
if self.partition.world_size == 1 and not active_gradients:
|
|
394
|
+
# Each tile still accumulates view batches in their original order.
|
|
395
|
+
# The numerical result itself holds the partial sums between batches.
|
|
396
|
+
for views, device in self._view_batches():
|
|
397
|
+
filtered, geometry = self._filtered(views, device)
|
|
398
|
+
for spatial, _ in _pixel_tiles(self.volume_shape, self.chunk_shape):
|
|
399
|
+
partial = (self._native_gather(filtered, geometry, spatial) if self.backend == "numba"
|
|
400
|
+
else self._portable_gather(filtered, geometry, views, spatial))
|
|
401
|
+
result[spatial].add_(partial.to(self.output_device))
|
|
402
|
+
del partial
|
|
403
|
+
del filtered, geometry
|
|
404
|
+
return result
|
|
388
405
|
# Every owned tile's backward collective precedes the bundled input
|
|
389
406
|
# transpose, even when this rank owns no data views. Empty space groups
|
|
390
407
|
# have no tile collectives but still contribute zero input cotangents.
|
|
@@ -61,7 +61,7 @@ def _rectangles(shape, chunks):
|
|
|
61
61
|
for start, step, size in zip(starts, chunks, shape))
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def _flat_admission_batches(owner, geometry, detector_chunk_shape):
|
|
64
|
+
def _flat_admission_batches(owner, geometry, detector_chunk_shape, *, batched_corners=False):
|
|
65
65
|
"""Bound flat admission with corners and exact ambiguous-view fallback."""
|
|
66
66
|
views = owner.view_slice
|
|
67
67
|
if views.start == views.stop:
|
|
@@ -70,8 +70,13 @@ def _flat_admission_batches(owner, geometry, detector_chunk_shape):
|
|
|
70
70
|
# coordinate; finite corner points and source differences bound the grid.
|
|
71
71
|
endpoints = tuple((slice(0, 1),) if size == 1 else (slice(0, 1), slice(size - 1, size))
|
|
72
72
|
for size in owner.detector_shape)
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
if batched_corners:
|
|
74
|
+
# Native validation can compose the Cartesian corner set in one batch.
|
|
75
|
+
# Stepped pixel slices retain the exact global endpoint coordinates.
|
|
76
|
+
yield views, tuple(slice(0, size, max(1, size - 1)) for size in owner.detector_shape)
|
|
77
|
+
else:
|
|
78
|
+
for pixels in itertools.product(*endpoints):
|
|
79
|
+
yield views, pixels
|
|
75
80
|
if owner.beam in ("parallel", "parallel3d"):
|
|
76
81
|
return
|
|
77
82
|
source, lower = (value[views].to(dtype=owner.dtype) for value in geometry[:2])
|
|
@@ -52,9 +52,10 @@ def compose_surface(frames, offsets, source, points, view_index, pixel_start,
|
|
|
52
52
|
``offset32`` gives the offset arithmetic dtype. Values are stored exactly
|
|
53
53
|
in float64 staging buffers. Results use caller-owned float32 buffers.
|
|
54
54
|
|
|
55
|
-
A launch may compose several views
|
|
55
|
+
A launch may compose several views: grid row ``y``
|
|
56
56
|
writes view ``view_index + y`` from the ``y``-th of ``gridDim.y`` equal
|
|
57
|
-
frame packets in ``frames``.
|
|
57
|
+
frame packets in ``frames``. A shared offset buffer holds ``rank * capacity``
|
|
58
|
+
values plus status; a larger buffer holds one such packet per output view.
|
|
58
59
|
"""
|
|
59
60
|
pixel = cuda.grid(1)
|
|
60
61
|
view_index += cuda.blockIdx.y
|
|
@@ -68,15 +69,16 @@ def compose_surface(frames, offsets, source, points, view_index, pixel_start,
|
|
|
68
69
|
if pixel >= count:
|
|
69
70
|
return
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
offset_base = view_index * rank * capacity if offsets.size > rank * capacity + 1 else 0
|
|
73
|
+
tangent = offsets[offset_base + pixel]
|
|
74
|
+
depth = offsets[offset_base + (rank - 1) * capacity + pixel]
|
|
73
75
|
tangent32 = offset32 and frame32[2]
|
|
74
76
|
point32 = frame32[1] and tangent32
|
|
75
77
|
normal32 = frame32[2]
|
|
76
78
|
vertical = np.float64(0.0)
|
|
77
79
|
vertical32 = False
|
|
78
80
|
if rank == 3:
|
|
79
|
-
vertical = offsets[capacity + pixel]
|
|
81
|
+
vertical = offsets[offset_base + capacity + pixel]
|
|
80
82
|
vertical32 = offset32 and frame32[3]
|
|
81
83
|
point32 = point32 and vertical32
|
|
82
84
|
normal32 = normal32 and frame32[3]
|