diffct 2.4.1__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.
Files changed (154) hide show
  1. {diffct-2.4.1 → diffct-2.4.3}/CHANGELOG.md +24 -0
  2. {diffct-2.4.1 → diffct-2.4.3}/CITATION.cff +2 -2
  3. {diffct-2.4.1 → diffct-2.4.3}/PKG-INFO +9 -9
  4. {diffct-2.4.1 → diffct-2.4.3}/README.md +8 -8
  5. {diffct-2.4.1 → diffct-2.4.3}/diffct/__init__.py +1 -1
  6. {diffct-2.4.1 → diffct-2.4.3}/diffct/_solver_state.py +12 -9
  7. {diffct-2.4.1 → diffct-2.4.3}/diffct/_solver_tv.py +3 -2
  8. {diffct-2.4.1 → diffct-2.4.3}/diffct/analytical.py +3 -6
  9. {diffct-2.4.1 → diffct-2.4.3}/diffct/analytical_execution.py +17 -0
  10. {diffct-2.4.1 → diffct-2.4.3}/diffct/backends.py +8 -3
  11. {diffct-2.4.1 → diffct-2.4.3}/diffct/chunking.py +1 -0
  12. {diffct-2.4.1 → diffct-2.4.3}/diffct/kernels/surface_geometry.py +18 -10
  13. {diffct-2.4.1 → diffct-2.4.3}/diffct/operators.py +312 -80
  14. {diffct-2.4.1 → diffct-2.4.3}/diffct/solvers.py +22 -11
  15. {diffct-2.4.1 → diffct-2.4.3}/diffct/surfaces.py +15 -12
  16. {diffct-2.4.1 → diffct-2.4.3}/diffct/utils.py +10 -0
  17. {diffct-2.4.1 → diffct-2.4.3}/docs/video/README.md +2 -2
  18. {diffct-2.4.1 → diffct-2.4.3}/examples/README.md +3 -0
  19. {diffct-2.4.1 → diffct-2.4.3}/formal/README.md +1 -0
  20. {diffct-2.4.1 → diffct-2.4.3}/pyproject.toml +1 -1
  21. diffct-2.4.3/tests/_multiprocessing.py +26 -0
  22. {diffct-2.4.1 → diffct-2.4.3}/tests/test_allocator_capacity.py +7 -7
  23. {diffct-2.4.1 → diffct-2.4.3}/tests/test_batched_surfaces.py +55 -2
  24. {diffct-2.4.1 → diffct-2.4.3}/tests/test_block_storage.py +2 -2
  25. {diffct-2.4.1 → diffct-2.4.3}/tests/test_cgls_state_admission.py +2 -2
  26. {diffct-2.4.1 → diffct-2.4.3}/tests/test_chunked_projector.py +5 -5
  27. {diffct-2.4.1 → diffct-2.4.3}/tests/test_completed_transfer_reuse.py +2 -2
  28. {diffct-2.4.1 → diffct-2.4.3}/tests/test_detector_surfaces.py +2 -2
  29. {diffct-2.4.1 → diffct-2.4.3}/tests/test_distributed_training_transforms.py +2 -7
  30. {diffct-2.4.1 → diffct-2.4.3}/tests/test_execution_buffers.py +67 -4
  31. {diffct-2.4.1 → diffct-2.4.3}/tests/test_execution_features.py +2 -2
  32. diffct-2.4.3/tests/test_gpu_performance_contracts.py +229 -0
  33. diffct-2.4.3/tests/test_multiprocessing.py +78 -0
  34. {diffct-2.4.1 → diffct-2.4.3}/tests/test_native_volume_layout_distributed.py +2 -2
  35. {diffct-2.4.1 → diffct-2.4.3}/tests/test_parallel3d.py +2 -2
  36. {diffct-2.4.1 → diffct-2.4.3}/tests/test_portable_distributed_geometry.py +2 -2
  37. diffct-2.4.3/tests/test_pr42_review_regressions.py +301 -0
  38. diffct-2.4.3/tests/test_reconstruction_work_reuse.py +231 -0
  39. {diffct-2.4.1 → diffct-2.4.3}/tests/test_solver_admission_regressions.py +2 -2
  40. {diffct-2.4.1 → diffct-2.4.3}/tests/test_solver_collective_failures.py +7 -5
  41. diffct-2.4.3/tests/test_solver_device_scalars.py +255 -0
  42. {diffct-2.4.1 → diffct-2.4.3}/tests/test_spatial_partition.py +2 -2
  43. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_collective_failures.py +2 -2
  44. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_compute_shards.py +3 -3
  45. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_geometry_cudasim.py +111 -18
  46. diffct-2.4.3/tests/test_surface_host_sampling_budget.py +257 -0
  47. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_pipeline_contracts.py +24 -0
  48. diffct-2.4.3/tests/test_surface_saved_snapshot_budget.py +274 -0
  49. diffct-2.4.3/tests/test_surface_snapshot_admission.py +209 -0
  50. {diffct-2.4.1 → diffct-2.4.3}/tests/test_trajectory_surface_integration.py +8 -4
  51. {diffct-2.4.1 → diffct-2.4.3}/tests/test_transfer_pipeline.py +2 -2
  52. {diffct-2.4.1 → diffct-2.4.3}/tests/test_transfer_retirement_lifetimes.py +2 -2
  53. {diffct-2.4.1 → diffct-2.4.3}/.gitignore +0 -0
  54. {diffct-2.4.1 → diffct-2.4.3}/LICENSE +0 -0
  55. {diffct-2.4.1 → diffct-2.4.3}/NOTICE +0 -0
  56. {diffct-2.4.1 → diffct-2.4.3}/diffct/constants.py +0 -0
  57. {diffct-2.4.1 → diffct-2.4.3}/diffct/differentiable.py +0 -0
  58. {diffct-2.4.1 → diffct-2.4.3}/diffct/distribution.py +0 -0
  59. {diffct-2.4.1 → diffct-2.4.3}/diffct/geometry.py +0 -0
  60. {diffct-2.4.1 → diffct-2.4.3}/diffct/kernels/__init__.py +0 -0
  61. {diffct-2.4.1 → diffct-2.4.3}/diffct/kernels/analytical_transpose.py +0 -0
  62. {diffct-2.4.1 → diffct-2.4.3}/diffct/kernels/cone_beam.py +0 -0
  63. {diffct-2.4.1 → diffct-2.4.3}/diffct/kernels/fan_beam.py +0 -0
  64. {diffct-2.4.1 → diffct-2.4.3}/diffct/kernels/geometry_vjp.py +0 -0
  65. {diffct-2.4.1 → diffct-2.4.3}/diffct/kernels/parallel_beam.py +0 -0
  66. {diffct-2.4.1 → diffct-2.4.3}/diffct/pipeline.py +0 -0
  67. {diffct-2.4.1 → diffct-2.4.3}/diffct/projectors.py +0 -0
  68. {diffct-2.4.1 → diffct-2.4.3}/diffct/reconstruction.py +0 -0
  69. {diffct-2.4.1 → diffct-2.4.3}/diffct/storage.py +0 -0
  70. {diffct-2.4.1 → diffct-2.4.3}/diffct/streaming.py +0 -0
  71. {diffct-2.4.1 → diffct-2.4.3}/diffct/surface_gradients.py +0 -0
  72. {diffct-2.4.1 → diffct-2.4.3}/examples/_block_cgls.py +0 -0
  73. {diffct-2.4.1 → diffct-2.4.3}/examples/_common.py +0 -0
  74. {diffct-2.4.1 → diffct-2.4.3}/examples/data/NOTICE +0 -0
  75. {diffct-2.4.1 → diffct-2.4.3}/examples/data/preprocess_walnut.py +0 -0
  76. {diffct-2.4.1 → diffct-2.4.3}/examples/walnut_reconstruction.py +0 -0
  77. {diffct-2.4.1 → diffct-2.4.3}/pytest.ini +0 -0
  78. {diffct-2.4.1 → diffct-2.4.3}/tests/__init__.py +0 -0
  79. {diffct-2.4.1 → diffct-2.4.3}/tests/_phantoms.py +0 -0
  80. {diffct-2.4.1 → diffct-2.4.3}/tests/benchmarks/README.md +0 -0
  81. {diffct-2.4.1 → diffct-2.4.3}/tests/benchmarks/__init__.py +0 -0
  82. {diffct-2.4.1 → diffct-2.4.3}/tests/benchmarks/_common.py +0 -0
  83. {diffct-2.4.1 → diffct-2.4.3}/tests/benchmarks/test_bench_cone.py +0 -0
  84. {diffct-2.4.1 → diffct-2.4.3}/tests/benchmarks/test_bench_fan.py +0 -0
  85. {diffct-2.4.1 → diffct-2.4.3}/tests/benchmarks/test_bench_parallel.py +0 -0
  86. {diffct-2.4.1 → diffct-2.4.3}/tests/distributed_projector_check.py +0 -0
  87. {diffct-2.4.1 → diffct-2.4.3}/tests/test_adjoint_inner_product.py +0 -0
  88. {diffct-2.4.1 → diffct-2.4.3}/tests/test_analytical_autograd.py +0 -0
  89. {diffct-2.4.1 → diffct-2.4.3}/tests/test_analytical_distribution.py +0 -0
  90. {diffct-2.4.1 → diffct-2.4.3}/tests/test_analytical_regressions.py +0 -0
  91. {diffct-2.4.1 → diffct-2.4.3}/tests/test_analytical_weight_distribution.py +0 -0
  92. {diffct-2.4.1 → diffct-2.4.3}/tests/test_angular_low_precision.py +0 -0
  93. {diffct-2.4.1 → diffct-2.4.3}/tests/test_axis_aligned_chords.py +0 -0
  94. {diffct-2.4.1 → diffct-2.4.3}/tests/test_batch_execution_edges.py +0 -0
  95. {diffct-2.4.1 → diffct-2.4.3}/tests/test_block_cgls.py +0 -0
  96. {diffct-2.4.1 → diffct-2.4.3}/tests/test_call_trajectory.py +0 -0
  97. {diffct-2.4.1 → diffct-2.4.3}/tests/test_checkpoint_metadata_validation.py +0 -0
  98. {diffct-2.4.1 → diffct-2.4.3}/tests/test_cone_backprojection_transpose.py +0 -0
  99. {diffct-2.4.1 → diffct-2.4.3}/tests/test_cone_projector_autograd.py +0 -0
  100. {diffct-2.4.1 → diffct-2.4.3}/tests/test_cpu_install.py +0 -0
  101. {diffct-2.4.1 → diffct-2.4.3}/tests/test_cuda_exit_deallocations.py +0 -0
  102. {diffct-2.4.1 → diffct-2.4.3}/tests/test_cuda_sink_lifetimes.py +0 -0
  103. {diffct-2.4.1 → diffct-2.4.3}/tests/test_cuda_smoke.py +0 -0
  104. {diffct-2.4.1 → diffct-2.4.3}/tests/test_detector_surface_dtypes.py +0 -0
  105. {diffct-2.4.1 → diffct-2.4.3}/tests/test_detector_surface_mapping.py +0 -0
  106. {diffct-2.4.1 → diffct-2.4.3}/tests/test_detector_surface_units.py +0 -0
  107. {diffct-2.4.1 → diffct-2.4.3}/tests/test_example_psnr.py +0 -0
  108. {diffct-2.4.1 → diffct-2.4.3}/tests/test_execution_schedules.py +0 -0
  109. {diffct-2.4.1 → diffct-2.4.3}/tests/test_fbp_fan_accuracy.py +0 -0
  110. {diffct-2.4.1 → diffct-2.4.3}/tests/test_fbp_fan_offsets.py +0 -0
  111. {diffct-2.4.1 → diffct-2.4.3}/tests/test_fbp_parallel_accuracy.py +0 -0
  112. {diffct-2.4.1 → diffct-2.4.3}/tests/test_fdk_cone_accuracy.py +0 -0
  113. {diffct-2.4.1 → diffct-2.4.3}/tests/test_fdk_cone_offsets.py +0 -0
  114. {diffct-2.4.1 → diffct-2.4.3}/tests/test_geometry_autograd.py +0 -0
  115. {diffct-2.4.1 → diffct-2.4.3}/tests/test_geometry_conversions.py +0 -0
  116. {diffct-2.4.1 → diffct-2.4.3}/tests/test_geometry_gradients.py +0 -0
  117. {diffct-2.4.1 → diffct-2.4.3}/tests/test_gradcheck.py +0 -0
  118. {diffct-2.4.1 → diffct-2.4.3}/tests/test_lazy_native_observers.py +0 -0
  119. {diffct-2.4.1 → diffct-2.4.3}/tests/test_live_trajectory_validation.py +0 -0
  120. {diffct-2.4.1 → diffct-2.4.3}/tests/test_multi_gpu_exit.py +0 -0
  121. {diffct-2.4.1 → diffct-2.4.3}/tests/test_native_endpoint_precision.py +0 -0
  122. {diffct-2.4.1 → diffct-2.4.3}/tests/test_native_surface_transforms.py +0 -0
  123. {diffct-2.4.1 → diffct-2.4.3}/tests/test_native_volume_layout.py +0 -0
  124. {diffct-2.4.1 → diffct-2.4.3}/tests/test_output_pipeline_overlap.py +0 -0
  125. {diffct-2.4.1 → diffct-2.4.3}/tests/test_parallel3d_surface_oracle.py +0 -0
  126. {diffct-2.4.1 → diffct-2.4.3}/tests/test_partition_lifecycle.py +0 -0
  127. {diffct-2.4.1 → diffct-2.4.3}/tests/test_plan_memory_bounds.py +0 -0
  128. {diffct-2.4.1 → diffct-2.4.3}/tests/test_portable_endpoint_precision.py +0 -0
  129. {diffct-2.4.1 → diffct-2.4.3}/tests/test_portable_execution_bounds.py +0 -0
  130. {diffct-2.4.1 → diffct-2.4.3}/tests/test_portable_numeric_regressions.py +0 -0
  131. {diffct-2.4.1 → diffct-2.4.3}/tests/test_portable_sampler_snapshot.py +0 -0
  132. {diffct-2.4.1 → diffct-2.4.3}/tests/test_portable_surface_admission.py +0 -0
  133. {diffct-2.4.1 → diffct-2.4.3}/tests/test_projector_api.py +0 -0
  134. {diffct-2.4.1 → diffct-2.4.3}/tests/test_projector_degenerate_geometry.py +0 -0
  135. {diffct-2.4.1 → diffct-2.4.3}/tests/test_projector_geometry_devices.py +0 -0
  136. {diffct-2.4.1 → diffct-2.4.3}/tests/test_ramp_filter_windows.py +0 -0
  137. {diffct-2.4.1 → diffct-2.4.3}/tests/test_reconstruction_api.py +0 -0
  138. {diffct-2.4.1 → diffct-2.4.3}/tests/test_reference_backend.py +0 -0
  139. {diffct-2.4.1 → diffct-2.4.3}/tests/test_release_2_0_1.py +0 -0
  140. {diffct-2.4.1 → diffct-2.4.3}/tests/test_review_230.py +0 -0
  141. {diffct-2.4.1 → diffct-2.4.3}/tests/test_review_regressions.py +0 -0
  142. {diffct-2.4.1 → diffct-2.4.3}/tests/test_solver_api.py +0 -0
  143. {diffct-2.4.1 → diffct-2.4.3}/tests/test_solver_checkpoint_edges.py +0 -0
  144. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_composition_kernel.py +0 -0
  145. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_gpu_execution.py +0 -0
  146. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_gradient_replay.py +0 -0
  147. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_native_regressions.py +0 -0
  148. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_parameter_updates.py +0 -0
  149. {diffct-2.4.1 → diffct-2.4.3}/tests/test_surface_trajectory_transform.py +0 -0
  150. {diffct-2.4.1 → diffct-2.4.3}/tests/test_training_transforms.py +0 -0
  151. {diffct-2.4.1 → diffct-2.4.3}/tests/test_trajectory_admission_cost.py +0 -0
  152. {diffct-2.4.1 → diffct-2.4.3}/tests/test_tv_zero_dependency.py +0 -0
  153. {diffct-2.4.1 → diffct-2.4.3}/tests/test_two_far_endpoints.py +0 -0
  154. {diffct-2.4.1 → diffct-2.4.3}/tests/test_weights.py +0 -0
@@ -7,6 +7,30 @@ 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
+
26
+ ## [2.4.2] - 2026-10-21
27
+
28
+ ### Fixed
29
+
30
+ - Count host world points in the streamed geometry budget only for surfaces that expand them on the host. Shared, non-trainable legacy offsets no longer shrink view batches (#40).
31
+ - Compose shared legacy offsets with one launch per job: the frames of all views are staged in one transfer and `compose_surface` runs on a 2-D grid whose rows are views. The offset snapshot is staged once per call and pixel tile; offsets supplied by the caller, such as in the geometry VJP replay, are staged on every call (#40).
32
+ - Count the batched frame staging in the streamed surface memory budget, and pack shared offsets into the workspace without a device staging buffer (#40).
33
+
10
34
  ## [2.4.1] - 2026-10-20
11
35
 
12
36
  ### 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.1
17
- date-released: "2026-10-20"
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.1
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, including [unreleased APIs](CHANGELOG.md#unreleased). Per-call trajectories, angle-tensor circular helpers, `transform_trajectory`, `parallel3d` and `native_volume_layout` require installation from source.
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.1">
181
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.1" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
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 | 569.99 ms | 151.02 ms (3.8×) | 81.50 ms (7.0×) |
188
- | Adjoint (backprojection) | 1061.36 ms | 278.75 ms (3.8×) | 220.34 ms (4.8×) |
189
- | CGLS iteration | 1756.03 ms | 468.05 ms (3.8×) | 333.08 ms (5.3×) |
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.1, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
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, including [unreleased APIs](CHANGELOG.md#unreleased). Per-call trajectories, angle-tensor circular helpers, `transform_trajectory`, `parallel3d` and `native_volume_layout` require installation from source.
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.1">
150
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.1" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
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 | 569.99 ms | 151.02 ms (3.8×) | 81.50 ms (7.0×) |
157
- | Adjoint (backprojection) | 1061.36 ms | 278.75 ms (3.8×) | 220.34 ms (4.8×) |
158
- | CGLS iteration | 1756.03 ms | 468.05 ms (3.8×) | 333.08 ms (5.3×) |
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.1, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
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
 
@@ -64,7 +64,7 @@ try:
64
64
  except ImportError:
65
65
  pass
66
66
 
67
- __version__ = '2.4.1'
67
+ __version__ = '2.4.3'
68
68
 
69
69
  __all__ = [
70
70
  'Projector',
@@ -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, including view/space replicas."""
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 = 0.
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="cpu", dtype=target.dtype).clone()
96
- block.mul_(scale).add_(source.read(selected).to(device="cpu", dtype=target.dtype), alpha=factor)
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
- global_value = scalar_sum(operator, local.detach().item(), "volume")
108
- total = local + (local.new_tensor(global_value) - local.detach())
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(isocenter[0].item() / voxel_spacing),
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(isocenter[0].item() / voxel_spacing),
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.item() / parameters[-1]) for value in isocenter)
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
- for pixels in itertools.product(*endpoints):
74
- yield views, pixels
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])
@@ -23,6 +23,7 @@ class _ChunkPlan:
23
23
  foreign_output: bool = False
24
24
  reservation_budget_bytes: int = (1 << 63) - 1
25
25
  estimated_reserved_bytes: int = 0
26
+ surface_batch: bool = True
26
27
 
27
28
 
28
29
  def _pixel_tiles(shape, chunk_shape):
@@ -51,26 +51,34 @@ def compose_surface(frames, offsets, source, points, view_index, pixel_start,
51
51
  ``frame32`` gives the arithmetic dtype of each packed frame component.
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
+
55
+ A launch may compose several views: grid row ``y``
56
+ writes view ``view_index + y`` from the ``y``-th of ``gridDim.y`` equal
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.
54
59
  """
55
60
  pixel = cuda.grid(1)
61
+ view_index += cuda.blockIdx.y
62
+ base = cuda.blockIdx.y * (frames.size // cuda.gridDim.y)
56
63
  if pixel == 0:
57
64
  for axis in range(rank):
58
- value = frames[axis]
65
+ value = frames[base + axis]
59
66
  if not parallel:
60
67
  value = _surface_add(value, -centre[axis], frame32[0])
61
68
  source[view_index, axis] = np.float32(value)
62
69
  if pixel >= count:
63
70
  return
64
71
 
65
- tangent = offsets[pixel]
66
- depth = offsets[(rank - 1) * capacity + pixel]
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]
67
75
  tangent32 = offset32 and frame32[2]
68
76
  point32 = frame32[1] and tangent32
69
77
  normal32 = frame32[2]
70
78
  vertical = np.float64(0.0)
71
79
  vertical32 = False
72
80
  if rank == 3:
73
- vertical = offsets[capacity + pixel]
81
+ vertical = offsets[offset_base + capacity + pixel]
74
82
  vertical32 = offset32 and frame32[3]
75
83
  point32 = point32 and vertical32
76
84
  normal32 = normal32 and frame32[3]
@@ -85,20 +93,20 @@ def compose_surface(frames, offsets, source, points, view_index, pixel_start,
85
93
  point_squared = np.float64(0.0)
86
94
  for axis in range(rank):
87
95
  point = _surface_add(
88
- frames[rank + axis],
89
- _surface_multiply(tangent, frames[2 * rank + axis], tangent32),
96
+ frames[base + rank + axis],
97
+ _surface_multiply(tangent, frames[base + 2 * rank + axis], tangent32),
90
98
  frame32[1] and tangent32,
91
99
  )
92
100
  if rank == 3:
93
101
  point = _surface_add(
94
- point, _surface_multiply(vertical, frames[3 * rank + axis], vertical32),
102
+ point, _surface_multiply(vertical, frames[base + 3 * rank + axis], vertical32),
95
103
  point32,
96
104
  )
97
- normal = frames[4 * rank + axis]
105
+ normal = frames[base + 4 * rank + axis]
98
106
  else:
99
- normal = frames[2 * rank + 1] if axis == 0 else -frames[2 * rank]
107
+ normal = frames[base + 2 * rank + 1] if axis == 0 else -frames[base + 2 * rank]
100
108
  point = _surface_add(point, _surface_multiply(depth, normal, depth32), final32)
101
- source_value = frames[axis]
109
+ source_value = frames[base + axis]
102
110
  source_native = np.float32(source_value)
103
111
  point_native = np.float32(point)
104
112
  finite = (finite and math.isfinite(source_value) and math.isfinite(point)