diffct 2.4.2__py3-none-any.whl → 2.4.3__py3-none-any.whl
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/__init__.py +1 -1
- diffct/_solver_state.py +12 -9
- diffct/_solver_tv.py +3 -2
- diffct/analytical.py +3 -6
- diffct/analytical_execution.py +17 -0
- diffct/backends.py +8 -3
- diffct/chunking.py +1 -0
- diffct/kernels/surface_geometry.py +7 -5
- diffct/operators.py +233 -95
- diffct/solvers.py +22 -11
- diffct/surfaces.py +13 -12
- diffct/utils.py +10 -0
- {diffct-2.4.2.dist-info → diffct-2.4.3.dist-info}/METADATA +9 -9
- {diffct-2.4.2.dist-info → diffct-2.4.3.dist-info}/RECORD +17 -17
- {diffct-2.4.2.dist-info → diffct-2.4.3.dist-info}/WHEEL +0 -0
- {diffct-2.4.2.dist-info → diffct-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {diffct-2.4.2.dist-info → diffct-2.4.3.dist-info}/licenses/NOTICE +0 -0
diffct/__init__.py
CHANGED
diffct/_solver_state.py
CHANGED
|
@@ -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
|
|
diffct/_solver_tv.py
CHANGED
|
@@ -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)
|
diffct/analytical.py
CHANGED
|
@@ -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
|
diffct/analytical_execution.py
CHANGED
|
@@ -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.
|
diffct/backends.py
CHANGED
|
@@ -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])
|
diffct/chunking.py
CHANGED
|
@@ -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]
|
diffct/operators.py
CHANGED
|
@@ -16,7 +16,7 @@ from .storage import TensorStore
|
|
|
16
16
|
from .streaming import _validate_stores
|
|
17
17
|
from .surfaces import ParameterizedSurface, _GeometrySpec, _sample_geometry, _sample_offsets
|
|
18
18
|
from .surface_gradients import surface_batch_vjp
|
|
19
|
-
from .utils import TorchCUDABridge, _get_numba_external_stream_for
|
|
19
|
+
from .utils import TorchCUDABridge, _get_numba_external_stream_for, _raise_geometry_errors
|
|
20
20
|
from .backends import ReferenceProjector, _flat_admission_batches, select_backend, voxel_spacing_tuple
|
|
21
21
|
from .distribution import Partition
|
|
22
22
|
from .projectors import (
|
|
@@ -77,22 +77,27 @@ def _validate_trajectory(trajectory, beam, *, detector_surface=False, clone=True
|
|
|
77
77
|
if not isinstance(trajectory, (tuple, list)) or len(trajectory) != count:
|
|
78
78
|
raise TypeError(f"{beam} trajectory must contain {count} tensors")
|
|
79
79
|
|
|
80
|
-
geometry = []
|
|
80
|
+
geometry, checks = [], []
|
|
81
81
|
n_views = None
|
|
82
82
|
for component in trajectory:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
try:
|
|
84
|
+
if not isinstance(component, torch.Tensor):
|
|
85
|
+
raise TypeError("trajectory components must be tensors")
|
|
86
|
+
if component.ndim != 2 or component.shape[1] != dimensions:
|
|
87
|
+
raise ValueError(f"trajectory tensors must have shape (views, {dimensions})")
|
|
88
|
+
if not torch.is_floating_point(component):
|
|
89
|
+
raise TypeError("trajectory tensors must have floating-point dtype")
|
|
90
|
+
if component.shape[0] == 0:
|
|
91
|
+
raise ValueError("trajectory must contain at least one view")
|
|
92
|
+
if n_views is not None and component.shape[0] != n_views:
|
|
93
|
+
raise ValueError("trajectory tensors must have the same number of views")
|
|
94
|
+
except (TypeError, ValueError):
|
|
95
|
+
# A deferred value error of an earlier component takes precedence.
|
|
96
|
+
if checks:
|
|
97
|
+
_raise_geometry_errors(checks)
|
|
98
|
+
raise
|
|
93
99
|
n_views = component.shape[0]
|
|
94
|
-
|
|
95
|
-
raise ValueError("trajectory tensors must contain only finite values")
|
|
100
|
+
checks.append((~torch.isfinite(component).all(), "trajectory tensors must contain only finite values"))
|
|
96
101
|
geometry.append(component.detach().clone() if clone else component.detach())
|
|
97
102
|
|
|
98
103
|
device = next((value.device for value in geometry if value.is_cuda), geometry[0].device)
|
|
@@ -103,28 +108,29 @@ def _validate_trajectory(trajectory, beam, *, detector_surface=False, clone=True
|
|
|
103
108
|
)
|
|
104
109
|
for axis in axes:
|
|
105
110
|
norms = torch.linalg.vector_norm(axis, dim=1)
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
checks.append((~torch.isclose(norms, torch.ones_like(norms), rtol=1e-4, atol=1e-4).all(),
|
|
112
|
+
"trajectory direction axes must be unit vectors"))
|
|
108
113
|
if beam in ("parallel", "cone", "parallel3d"):
|
|
109
114
|
first, second = (geometry[0], geometry[2]) if beam == "parallel" else (geometry[2], geometry[3])
|
|
110
115
|
dots = torch.sum(first.to(device) * second.to(device), dim=1)
|
|
111
|
-
|
|
112
|
-
raise ValueError("trajectory direction axes must be orthogonal")
|
|
116
|
+
checks.append((torch.any(torch.abs(dots) > 1e-4), "trajectory direction axes must be orthogonal"))
|
|
113
117
|
if beam == "parallel3d":
|
|
114
118
|
direction = geometry[0].to(device=device, dtype=torch.float64)
|
|
115
119
|
frame = torch.stack((direction, geometry[2].to(direction.device, torch.float64),
|
|
116
120
|
geometry[3].to(direction.device, torch.float64)), dim=-1)
|
|
117
121
|
# Numerical rank rejects a tangent frame at machine roundoff without
|
|
118
122
|
# imposing an application-level minimum angle of incidence.
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
# Bad entries already have a finite-value error above; keep the rank
|
|
124
|
+
# solver well-defined until the combined error flags reach the host.
|
|
125
|
+
frame = torch.where(torch.isfinite(frame), frame, 0.)
|
|
126
|
+
checks.append((torch.any(torch.linalg.matrix_rank(frame) < 3),
|
|
127
|
+
"parallel3d rays must not be tangent to the detector plane"))
|
|
121
128
|
if beam in ("fan", "cone") and not detector_surface:
|
|
122
129
|
# float64 avoids overflow of half-precision input and dtype mismatches in cross().
|
|
123
130
|
source = geometry[0].to(device=device, dtype=torch.float64)
|
|
124
131
|
principal = geometry[1].to(source.device, torch.float64) - source
|
|
125
132
|
lengths = torch.linalg.vector_norm(principal, dim=1)
|
|
126
|
-
|
|
127
|
-
raise ValueError("source and detector center must differ in every view")
|
|
133
|
+
checks.append((torch.any(lengths == 0), "source and detector center must differ in every view"))
|
|
128
134
|
principal = principal / lengths[:, None]
|
|
129
135
|
det_u = geometry[2].to(source.device, torch.float64)
|
|
130
136
|
if beam == "fan":
|
|
@@ -132,9 +138,10 @@ def _validate_trajectory(trajectory, beam, *, detector_surface=False, clone=True
|
|
|
132
138
|
else:
|
|
133
139
|
normal = torch.linalg.cross(det_u, geometry[3].to(source.device, torch.float64), dim=1)
|
|
134
140
|
facing = torch.sum(principal * normal, dim=1)
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
checks.append((torch.any(torch.abs(facing) < 1e-4),
|
|
142
|
+
"the detector must not be edge-on to the source in any view"))
|
|
137
143
|
|
|
144
|
+
_raise_geometry_errors(checks)
|
|
138
145
|
return tuple(geometry), n_views
|
|
139
146
|
|
|
140
147
|
|
|
@@ -544,16 +551,14 @@ class Projector:
|
|
|
544
551
|
)
|
|
545
552
|
for component in self._trajectory[:2]
|
|
546
553
|
])
|
|
547
|
-
|
|
548
|
-
|
|
554
|
+
_raise_geometry_errors([
|
|
555
|
+
(torch.any(distances.min(dim=0).values > 1e6),
|
|
549
556
|
"the source or the detector center must lie within 1e6 voxels "
|
|
550
|
-
"of the volume center in every view"
|
|
551
|
-
)
|
|
552
|
-
if torch.any(distances > 1e15).item():
|
|
553
|
-
raise ValueError(
|
|
557
|
+
"of the volume center in every view"),
|
|
558
|
+
(torch.any(distances > 1e15),
|
|
554
559
|
"the source and the detector center must lie within 1e15 voxels "
|
|
555
|
-
"of the volume center"
|
|
556
|
-
|
|
560
|
+
"of the volume center"),
|
|
561
|
+
])
|
|
557
562
|
|
|
558
563
|
def _agreement_revision(self):
|
|
559
564
|
surface = self.detector_surface
|
|
@@ -768,6 +773,7 @@ class Projector:
|
|
|
768
773
|
peers = len(active_devices) > 1 or (self._distributed and self.world_size > 1)
|
|
769
774
|
extra_rays = (2 * int(self.partition == "space") + int(is_project and foreign_output)
|
|
770
775
|
+ int(is_project and self.partition == "space" and peers))
|
|
776
|
+
surface_batch = True
|
|
771
777
|
|
|
772
778
|
def fits(shape):
|
|
773
779
|
points = surface or pixels != self.detector_shape
|
|
@@ -776,17 +782,21 @@ class Projector:
|
|
|
776
782
|
buffers = _pipeline_buffers(self.beam, shape, batch, pixels, points,
|
|
777
783
|
geometry_grad=self._geometry_trainable,
|
|
778
784
|
ray_accumulators=extra_rays, peers=peers or foreign_output)
|
|
779
|
-
|
|
785
|
+
workspace = self._surface_work_buffers(batch, pixels, spec=geometry_spec,
|
|
786
|
+
streamed=True, batched=surface_batch)
|
|
780
787
|
return (
|
|
781
|
-
sum(buffers) <= budget
|
|
782
|
-
and _reservation_bytes(buffers
|
|
788
|
+
sum(buffers) + sum(workspace) <= budget
|
|
789
|
+
and _reservation_bytes(buffers + self._surface_reserved_buffers(
|
|
790
|
+
batch, pixels, spec=geometry_spec, streamed=True, batched=surface_batch)) <= reservation_budget
|
|
783
791
|
and _largest_buffer_bytes(self.beam, shape, batch, pixels, points) <= allocation_limit
|
|
784
|
-
and max(
|
|
792
|
+
and max(workspace, default=0) <= allocation_limit
|
|
785
793
|
and _host_geometry_bytes(self.beam, batch, pixels, host_points) <= _pipeline._GEOMETRY_WORK_BYTES
|
|
786
794
|
)
|
|
787
795
|
|
|
788
796
|
while not fits(minimum_shape):
|
|
789
|
-
if self.
|
|
797
|
+
if self.detector_surface is not None and surface_batch:
|
|
798
|
+
surface_batch = False
|
|
799
|
+
elif self.view_chunk_size is None and batch > 1:
|
|
790
800
|
batch = max(1, batch // 2)
|
|
791
801
|
elif self.detector_chunk_shape is None and max(pixels) > 1:
|
|
792
802
|
axis = max(range(len(pixels)), key=pixels.__getitem__)
|
|
@@ -796,25 +806,36 @@ class Projector:
|
|
|
796
806
|
else:
|
|
797
807
|
raise RuntimeError("CUDA or host geometry memory budget cannot fit the minimum tile/view/pixel batch; reduce explicit limits")
|
|
798
808
|
while not fits(chunk):
|
|
809
|
+
if self.detector_surface is not None and surface_batch:
|
|
810
|
+
surface_batch = False
|
|
811
|
+
continue
|
|
799
812
|
if self.volume_chunk_shape is not None or max(chunk) == 1:
|
|
800
813
|
raise RuntimeError("CUDA memory budget cannot fit the requested tile/view batch")
|
|
801
814
|
axis = max(range(len(chunk)), key=chunk.__getitem__)
|
|
802
815
|
smaller = list(chunk)
|
|
803
816
|
smaller[axis] = max(1, smaller[axis] // 2)
|
|
804
817
|
chunk = tuple(smaller)
|
|
818
|
+
if not surface_batch:
|
|
819
|
+
# Smaller automatic tiles may now fit the complete workspace.
|
|
820
|
+
surface_batch = True
|
|
821
|
+
if not fits(chunk):
|
|
822
|
+
surface_batch = False
|
|
805
823
|
estimate = _pipeline_bytes(self.beam, chunk, batch, pixels,
|
|
806
824
|
surface or pixels != self.detector_shape,
|
|
807
825
|
geometry_grad=self._geometry_trainable,
|
|
808
826
|
ray_accumulators=extra_rays, peers=peers or foreign_output)
|
|
809
|
-
estimate += sum(self._surface_work_buffers(batch, pixels, spec=geometry_spec,
|
|
827
|
+
estimate += sum(self._surface_work_buffers(batch, pixels, spec=geometry_spec,
|
|
828
|
+
streamed=True, batched=surface_batch))
|
|
810
829
|
return _ChunkPlan(chunk, batch, devices, pixels, budget, allocation_limit,
|
|
811
830
|
estimated_gpu_bytes=estimate, foreign_output=foreign_output,
|
|
831
|
+
surface_batch=surface_batch,
|
|
812
832
|
reservation_budget_bytes=reservation_budget,
|
|
813
833
|
estimated_reserved_bytes=_reservation_bytes(_pipeline_buffers(
|
|
814
834
|
self.beam, chunk, batch, pixels, surface or pixels != self.detector_shape,
|
|
815
835
|
geometry_grad=self._geometry_trainable,
|
|
816
836
|
ray_accumulators=extra_rays, peers=peers or foreign_output)
|
|
817
|
-
+ self.
|
|
837
|
+
+ self._surface_reserved_buffers(batch, pixels, spec=geometry_spec,
|
|
838
|
+
streamed=True, batched=surface_batch)))
|
|
818
839
|
|
|
819
840
|
def _surface_capacity(self, views, pixels):
|
|
820
841
|
rank, columns = len(self.volume_shape), 3 if self.beam == "cone" else 2
|
|
@@ -822,37 +843,80 @@ class Projector:
|
|
|
822
843
|
maximum = max(views * math.prod(pixels) * rank, views * rank * frames)
|
|
823
844
|
return max(1, min(math.prod(pixels), 21, (maximum - 1) // columns))
|
|
824
845
|
|
|
825
|
-
def
|
|
826
|
-
"""
|
|
846
|
+
def _surface_snapshot_buffers(self):
|
|
847
|
+
"""Allocator-rounded FP64 bounds for independent geometry snapshots."""
|
|
848
|
+
return ((_allocation_bytes(2 * self._n_views * math.prod(self.detector_shape) * 3),)
|
|
849
|
+
+ (_allocation_bytes(2 * self._n_views * len(self.volume_shape)),) * len(self._trajectory))
|
|
850
|
+
|
|
851
|
+
def _surface_work_buffers(self, *args, **kwargs):
|
|
852
|
+
"""One inventory bounding every surface workspace phase.
|
|
853
|
+
|
|
854
|
+
Its total is the largest phase total and its largest entry is the
|
|
855
|
+
largest allocation of any phase. Phases do not overlap in time.
|
|
856
|
+
"""
|
|
857
|
+
phases = self._surface_work_phases(*args, **kwargs)
|
|
858
|
+
buffers = list(max(phases, key=sum))
|
|
859
|
+
peak = max(max(phase, default=0) for phase in phases)
|
|
860
|
+
if peak > max(buffers, default=0):
|
|
861
|
+
# The largest total covers the peak allocation: move the excess
|
|
862
|
+
# from the smallest entries into the largest one.
|
|
863
|
+
largest = buffers.index(max(buffers))
|
|
864
|
+
excess, buffers[largest] = peak - buffers[largest], peak
|
|
865
|
+
for index in sorted((i for i in range(len(buffers)) if i != largest), key=buffers.__getitem__):
|
|
866
|
+
taken = min(buffers[index], excess)
|
|
867
|
+
buffers[index] -= taken
|
|
868
|
+
excess -= taken
|
|
869
|
+
buffers = [size for size in buffers if size]
|
|
870
|
+
return tuple(buffers)
|
|
871
|
+
|
|
872
|
+
def _surface_reserved_buffers(self, *args, **kwargs):
|
|
873
|
+
"""Every surface workspace allocation of a call, for reservation bounds.
|
|
874
|
+
|
|
875
|
+
Phases never hold live tensors together, but blocks of one phase can
|
|
876
|
+
leave allocator segments pinned for the next, so all of them count.
|
|
877
|
+
"""
|
|
878
|
+
return tuple(size for phase in self._surface_work_phases(*args, **kwargs) for size in phase)
|
|
879
|
+
|
|
880
|
+
def _surface_work_phases(self, views, pixels, slots=1, spec=None, *, streamed=False, batched=True):
|
|
881
|
+
"""Bound device-local offset staging and world-point expression storage.
|
|
882
|
+
|
|
883
|
+
Returns the buffer inventories of phases that never coexist.
|
|
884
|
+
"""
|
|
827
885
|
if self.detector_surface is None:
|
|
828
|
-
return ()
|
|
886
|
+
return ((),)
|
|
829
887
|
if streamed:
|
|
830
888
|
rank, frames = len(self.volume_shape), 4 if self.beam == "cone" else 3
|
|
831
889
|
capacity = self._surface_capacity(views, pixels)
|
|
832
890
|
columns = 3 if self.beam == "cone" else 2
|
|
833
891
|
shared = spec is not None and spec.kind == "offsets" and spec.shared
|
|
834
|
-
if shared:
|
|
892
|
+
if shared or batched:
|
|
835
893
|
capacity = math.prod(pixels)
|
|
836
|
-
#
|
|
837
|
-
buffers = (_allocation_bytes(2 * (views if shared else 1) * (frames + (3 if rank == 3 else 0)) * rank),
|
|
838
|
-
_allocation_bytes(2 * (columns * capacity + 1)))
|
|
894
|
+
# Batched jobs retain every frame and, unless shared, every offset.
|
|
895
|
+
buffers = (_allocation_bytes(2 * (views if shared or batched else 1) * (frames + (3 if rank == 3 else 0)) * rank),
|
|
896
|
+
_allocation_bytes(2 * (columns * capacity * (views if batched and not shared else 1) + 1)))
|
|
839
897
|
if self._geometry_trainable:
|
|
840
898
|
# Replay one view and a bounded detector row at a time.
|
|
841
|
-
replay = rank * min(
|
|
899
|
+
replay = rank * min(self._surface_capacity(views, pixels), pixels[-1])
|
|
842
900
|
replay_buffers = ((_allocation_bytes(2 * replay),) * 8
|
|
843
901
|
+ (_allocation_bytes(2 * rank),) * (frames + 3))
|
|
844
902
|
if isinstance(self.detector_surface, ParameterizedSurface):
|
|
845
903
|
# A CUDA sampler needs a device cotangent for its selected output.
|
|
846
904
|
replay_buffers += (_allocation_bytes(6 * views * math.prod(pixels)),)
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
return buffers
|
|
905
|
+
# Composition frees its workspace before replay starts.
|
|
906
|
+
return buffers, replay_buffers
|
|
907
|
+
return (buffers,)
|
|
850
908
|
elements = views * math.prod(pixels) * 3
|
|
851
909
|
dtype = torch.float64 if spec is None or spec.dtype is None else spec.dtype
|
|
852
910
|
buffers = (torch.empty((), dtype=dtype).element_size() * elements,) * (4 * slots)
|
|
853
911
|
if dtype != torch.float64:
|
|
854
912
|
buffers += (8 * elements,) * slots
|
|
855
|
-
|
|
913
|
+
buffers += (views * 3 * 8 * (4 if self.beam == "cone" else 3),) * slots
|
|
914
|
+
if spec is None and not isinstance(self.detector_surface, ParameterizedSurface):
|
|
915
|
+
# The preliminary full plan precedes the callback. Admit independent
|
|
916
|
+
# snapshots plus one bounded arena for the callback's saved tensors.
|
|
917
|
+
snapshots = self._surface_snapshot_buffers()
|
|
918
|
+
buffers += (*snapshots, sum(snapshots))
|
|
919
|
+
return (buffers,)
|
|
856
920
|
|
|
857
921
|
def _surface_spec(self, frames, offsets):
|
|
858
922
|
dtype = torch.float32
|
|
@@ -873,9 +937,10 @@ class Projector:
|
|
|
873
937
|
def _surface_validation_inputs(self):
|
|
874
938
|
"""Stage frame metadata once without expanding surface world points."""
|
|
875
939
|
with torch.enable_grad():
|
|
876
|
-
frames =
|
|
940
|
+
frames = self._learnable or self._trajectory
|
|
877
941
|
surface = self.detector_surface
|
|
878
942
|
if isinstance(surface, ParameterizedSurface):
|
|
943
|
+
frames = tuple(value.to(device="cpu") for value in frames)
|
|
879
944
|
parameters = tuple(value.to(device="cpu") for value in surface.parameters)
|
|
880
945
|
geometry = (*frames, *parameters)
|
|
881
946
|
return (geometry, _GeometrySpec("sampled", surface.sampler, torch.float64),
|
|
@@ -1154,6 +1219,7 @@ class Projector:
|
|
|
1154
1219
|
return dict(operation="project" if is_project else "backproject", schedule=plan.schedule,
|
|
1155
1220
|
chunk_shape=tuple(plan.chunk_shape), view_chunk_size=plan.view_chunk_size,
|
|
1156
1221
|
detector_chunk_shape=tuple(plan.detector_chunk_shape or self.detector_shape),
|
|
1222
|
+
surface_batch=plan.surface_batch,
|
|
1157
1223
|
window_size=plan.window_size, gpu_budget_bytes=plan.gpu_budget_bytes,
|
|
1158
1224
|
estimated_gpu_bytes=plan.estimated_gpu_bytes,
|
|
1159
1225
|
reservation_budget_bytes=plan.reservation_budget_bytes,
|
|
@@ -1172,14 +1238,16 @@ class Projector:
|
|
|
1172
1238
|
return _pipeline_bytes(self.beam, shape, plan.view_chunk_size, pixels, points,
|
|
1173
1239
|
slots=slots, retained=retained, geometry_grad=self._geometry_trainable,
|
|
1174
1240
|
peers=peers, ray_accumulators=ray_accumulators) + sum(
|
|
1175
|
-
self._surface_work_buffers(plan.view_chunk_size, pixels, slots, spec,
|
|
1241
|
+
self._surface_work_buffers(plan.view_chunk_size, pixels, slots, spec,
|
|
1242
|
+
streamed=True, batched=plan.surface_batch))
|
|
1176
1243
|
|
|
1177
1244
|
def reserved(shape, slots=1, retained=1, ray_accumulators=0):
|
|
1178
1245
|
return _reservation_bytes(_pipeline_buffers(
|
|
1179
1246
|
self.beam, shape, plan.view_chunk_size, pixels, points,
|
|
1180
1247
|
slots=slots, retained=retained, geometry_grad=self._geometry_trainable,
|
|
1181
1248
|
peers=peers, ray_accumulators=ray_accumulators)
|
|
1182
|
-
+ self.
|
|
1249
|
+
+ self._surface_reserved_buffers(plan.view_chunk_size, pixels, slots, spec,
|
|
1250
|
+
streamed=True, batched=plan.surface_batch))
|
|
1183
1251
|
|
|
1184
1252
|
shapes = [plan.chunk_shape]
|
|
1185
1253
|
if self.volume_chunk_shape is None and len(volume_shape) == 3:
|
|
@@ -1255,8 +1323,8 @@ class Projector:
|
|
|
1255
1323
|
return _pipeline_buffers(
|
|
1256
1324
|
self.beam, plan.chunk_shape, plan.view_chunk_size, pixels, points,
|
|
1257
1325
|
slots=plan.slots, retained=plan.window_size, geometry_grad=self._geometry_trainable,
|
|
1258
|
-
peers=peers, ray_accumulators=extra_rays) + self.
|
|
1259
|
-
plan.view_chunk_size, pixels, plan.slots, spec, streamed=True)
|
|
1326
|
+
peers=peers, ray_accumulators=extra_rays) + self._surface_reserved_buffers(
|
|
1327
|
+
plan.view_chunk_size, pixels, plan.slots, spec, streamed=True, batched=plan.surface_batch)
|
|
1260
1328
|
|
|
1261
1329
|
def _choose_schedule(self, source, sink, is_project, geometry, plan, spec):
|
|
1262
1330
|
candidates = self._candidate_plans(plan, is_project, spec)
|
|
@@ -1266,7 +1334,8 @@ class Projector:
|
|
|
1266
1334
|
tuple(device.index for device in plan.devices), str(source.dtype), plan.gpu_budget_bytes,
|
|
1267
1335
|
plan.allocation_limit, plan.reservation_budget_bytes,
|
|
1268
1336
|
str(getattr(source, "device", "cpu")), str(getattr(sink, "device", "cpu")),
|
|
1269
|
-
spec.kind if spec is not None else "flat", self.schedule
|
|
1337
|
+
spec.kind if spec is not None else "flat", self.schedule, plan.surface_batch,
|
|
1338
|
+
spec.shared if spec is not None else False)
|
|
1270
1339
|
collective = self._distributed and self.world_size > 1
|
|
1271
1340
|
if not collective and key in self._schedule_cache:
|
|
1272
1341
|
return self._schedule_cache[key], [], True
|
|
@@ -1337,6 +1406,7 @@ class Projector:
|
|
|
1337
1406
|
geometry, spec, views, pixels)
|
|
1338
1407
|
origin = torch.cuda.current_stream(offsets.device) if offsets.is_cuda else None
|
|
1339
1408
|
frames = geometry[:frame_count]
|
|
1409
|
+
frame_origins = tuple(torch.cuda.current_stream(value.device) for value in frames if value.is_cuda)
|
|
1340
1410
|
common = torch.float32
|
|
1341
1411
|
for value in (*frames, offsets):
|
|
1342
1412
|
common = torch.promote_types(common, value.dtype)
|
|
@@ -1349,25 +1419,33 @@ class Projector:
|
|
|
1349
1419
|
# Offsets shared by all views are staged once per call and composed
|
|
1350
1420
|
# with one launch for all views instead of per view and row segment.
|
|
1351
1421
|
shared = spec.kind == "offsets" and offsets.ndim == len(pixel_shape) + 1
|
|
1422
|
+
batched = shared or groups.get("_surface_batch", True)
|
|
1352
1423
|
pixel_count = math.prod(pixel_shape)
|
|
1353
1424
|
# One packed frame packet per view: source, center, axes, normal and
|
|
1354
1425
|
# float32 cross-product scratch. Shared offsets pack every view of a job.
|
|
1355
1426
|
stride = (frame_count + (3 if rank == 3 else 0)) * rank
|
|
1356
1427
|
if "_surface_workspace" not in groups:
|
|
1357
|
-
|
|
1428
|
+
maximum_views = groups.get("_max_geometry", (count * rank,))[0] // rank
|
|
1429
|
+
capacity = (groups.get("_max_rays", count * pixel_count) // maximum_views
|
|
1430
|
+
if batched else self._surface_capacity(count, pixel_shape))
|
|
1358
1431
|
groups["_surface_workspace"] = (
|
|
1359
|
-
pipe.empty((
|
|
1360
|
-
pipe.empty((
|
|
1432
|
+
pipe.empty((maximum_views if batched else 1) * stride, dtype=torch.float64),
|
|
1433
|
+
pipe.empty(rank * capacity * (maximum_views if batched and not shared else 1) + 1,
|
|
1434
|
+
dtype=torch.float64), capacity,
|
|
1361
1435
|
)
|
|
1362
1436
|
packed_frames, packed_offsets, capacity = groups["_surface_workspace"]
|
|
1363
1437
|
columns = (0, 1, 2) if rank == 3 else (0, 2)
|
|
1364
1438
|
source, points = targets[0].view(count, rank), targets[1].view(count, -1, rank)
|
|
1365
1439
|
# A workspace sized for an earlier, smaller tile keeps the segment path.
|
|
1366
1440
|
shared = shared and capacity >= pixel_count
|
|
1367
|
-
|
|
1441
|
+
batched = batched and capacity >= pixel_count
|
|
1442
|
+
if batched and packed_frames.numel() < count * stride:
|
|
1368
1443
|
# Earlier launches on the upload stream may still read the smaller buffer.
|
|
1369
1444
|
pipe.retain(pipe.event(pipe.upload), packed_frames, None)
|
|
1370
1445
|
packed_frames = pipe.empty(count * stride, dtype=torch.float64)
|
|
1446
|
+
if not shared:
|
|
1447
|
+
pipe.retain(pipe.event(pipe.upload), packed_offsets, None)
|
|
1448
|
+
packed_offsets = pipe.empty(rank * capacity * count + 1, dtype=torch.float64)
|
|
1371
1449
|
groups["_surface_workspace"] = (packed_frames, packed_offsets, capacity)
|
|
1372
1450
|
# Every job of a call slices the same offset snapshot: stage it once per
|
|
1373
1451
|
# call, pixel tile and workspace. The key holds the snapshot and the
|
|
@@ -1380,39 +1458,55 @@ class Projector:
|
|
|
1380
1458
|
and groups["_surface_shared_staged"][1] is staged_key[1]
|
|
1381
1459
|
and groups["_surface_shared_staged"][2] == staged_key[2])
|
|
1382
1460
|
else:
|
|
1383
|
-
restage =
|
|
1461
|
+
restage = batched
|
|
1384
1462
|
groups.pop("_surface_shared_staged", None)
|
|
1385
1463
|
with torch.cuda.device(pipe.device), torch.cuda.stream(pipe.upload), cuda.gpus[pipe.device.index]:
|
|
1386
1464
|
if origin is not None:
|
|
1387
1465
|
pipe.upload.wait_stream(origin)
|
|
1466
|
+
for frame_origin in frame_origins:
|
|
1467
|
+
pipe.upload.wait_stream(frame_origin)
|
|
1388
1468
|
packed_offsets[-1].zero_()
|
|
1389
1469
|
stream = _get_numba_external_stream_for(pipe.upload)
|
|
1390
1470
|
arrays = tuple(TorchCUDABridge.tensor_to_cuda_array(value)
|
|
1391
1471
|
for value in (packed_frames, packed_offsets, source, points))
|
|
1392
1472
|
if restage:
|
|
1393
|
-
#
|
|
1394
|
-
# column
|
|
1395
|
-
|
|
1473
|
+
# Preserve strided sampler outputs: copy directly into admitted
|
|
1474
|
+
# column storage without flattening the input into a temporary.
|
|
1475
|
+
offset_views = 1 if shared else count
|
|
1476
|
+
packed = packed_offsets[:offset_views * rank * capacity].view(offset_views, rank, capacity)
|
|
1477
|
+
selected_offsets = offsets.detach()
|
|
1478
|
+
if selected_offsets.ndim == len(pixel_shape) + 1:
|
|
1479
|
+
selected_offsets = selected_offsets.unsqueeze(0)
|
|
1396
1480
|
if offsets.is_cuda:
|
|
1397
1481
|
for column, original in enumerate(columns):
|
|
1398
|
-
|
|
1482
|
+
target = packed[:, column, :pixel_count].view(offset_views, *pixel_shape)
|
|
1483
|
+
value = selected_offsets[..., original].expand(offset_views, *pixel_shape)
|
|
1484
|
+
_pipeline._copy(target, value, pipe.stats)
|
|
1485
|
+
pipe.retain(pipe.event(pipe.upload), value, packed_offsets)
|
|
1399
1486
|
else:
|
|
1400
|
-
host =
|
|
1401
|
-
for column in
|
|
1402
|
-
|
|
1403
|
-
|
|
1487
|
+
host = torch.empty((offset_views, rank, capacity), dtype=torch.float64)
|
|
1488
|
+
for column, original in enumerate(columns):
|
|
1489
|
+
host[:, column, :pixel_count].view(offset_views, *pixel_shape).copy_(
|
|
1490
|
+
selected_offsets[..., original])
|
|
1491
|
+
_pipeline._copy(packed, host, pipe.stats)
|
|
1404
1492
|
pipe.retain(pipe.event(pipe.upload), host, packed_offsets)
|
|
1405
|
-
if cacheable:
|
|
1493
|
+
if shared and cacheable:
|
|
1406
1494
|
groups["_surface_shared_staged"] = staged_key
|
|
1407
|
-
if
|
|
1495
|
+
if batched:
|
|
1408
1496
|
# Stage the frame packets of every view in one transfer and
|
|
1409
1497
|
# compose all views with one launch; grid row y composes view y.
|
|
1410
1498
|
block = packed_frames[:count * stride].view(count, stride)
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1499
|
+
if frame_origins:
|
|
1500
|
+
for index, value in enumerate(frames):
|
|
1501
|
+
selected = value[views].detach()
|
|
1502
|
+
_pipeline._copy(block[:, index * rank:(index + 1) * rank], selected, pipe.stats)
|
|
1503
|
+
pipe.retain(pipe.event(pipe.upload), selected, packed_frames)
|
|
1504
|
+
else:
|
|
1505
|
+
packet = torch.zeros((count, stride), dtype=torch.float64)
|
|
1506
|
+
for index, value in enumerate(frames):
|
|
1507
|
+
packet[:, index * rank:(index + 1) * rank].copy_(value[views].detach())
|
|
1508
|
+
_pipeline._copy(block, packet, pipe.stats)
|
|
1509
|
+
pipe.retain(pipe.event(pipe.upload), packet, packed_frames)
|
|
1416
1510
|
if rank == 3:
|
|
1417
1511
|
normal = block[:, 4 * rank:5 * rank]
|
|
1418
1512
|
axis_u, axis_v = block[:, 2 * rank:3 * rank], block[:, 3 * rank:4 * rank]
|
|
@@ -1433,7 +1527,7 @@ class Projector:
|
|
|
1433
1527
|
self.beam == "parallel", frame32, offset32,
|
|
1434
1528
|
tuple(centre) + (0.,) * (3 - rank), self.voxel_spacing,
|
|
1435
1529
|
)
|
|
1436
|
-
for local in range(0 if
|
|
1530
|
+
for local in range(0 if batched else count):
|
|
1437
1531
|
packet = torch.cat([value[views.start + local].detach().to(device="cpu", dtype=torch.float64)
|
|
1438
1532
|
for value in frames])
|
|
1439
1533
|
_pipeline._copy(packed_frames[:frame_count * rank], packet, pipe.stats)
|
|
@@ -1556,7 +1650,7 @@ class Projector:
|
|
|
1556
1650
|
points = spec is not None or self.detector_chunk_shape is not None or pixels != self.detector_shape
|
|
1557
1651
|
sizes = ((plan.view_chunk_size * rank, rays * rank) if points
|
|
1558
1652
|
else (plan.view_chunk_size * rank,) * (4 if self.beam == "cone" else 3))
|
|
1559
|
-
return {"_max_rays": rays, "_max_geometry": sizes}
|
|
1653
|
+
return {"_max_rays": rays, "_max_geometry": sizes, "_surface_batch": plan.surface_batch}
|
|
1560
1654
|
|
|
1561
1655
|
def _prefetch_job(self, job, source, geometry, spec, transfer, groups, index, is_project,
|
|
1562
1656
|
native, reduced, active_rays, next_epoch=False):
|
|
@@ -2127,7 +2221,7 @@ class Projector:
|
|
|
2127
2221
|
spacing = self.detector_spacing if self.beam == "cone" else self.detector_spacing[0]
|
|
2128
2222
|
sampled = geometry_spec is not None and geometry_spec.kind in ("sampled", "offsets")
|
|
2129
2223
|
grads = [torch.zeros(component.shape, dtype=torch.float32,
|
|
2130
|
-
device=
|
|
2224
|
+
device=component.device if sampled else volume.device) for component in geometry]
|
|
2131
2225
|
for device_rank, device in enumerate(devices):
|
|
2132
2226
|
local_slice = _balanced_slice(local_views, device_rank, len(devices))
|
|
2133
2227
|
if local_slice.start == local_slice.stop:
|
|
@@ -2287,7 +2381,8 @@ class Projector:
|
|
|
2287
2381
|
self.last_execution_stats = stats
|
|
2288
2382
|
return tuple(grad.to(device=component.device, dtype=component.dtype)
|
|
2289
2383
|
for grad, component in zip(grads, geometry))
|
|
2290
|
-
|
|
2384
|
+
# Replay composition follows the admitted batched/fallback workspace.
|
|
2385
|
+
groups = {device: {"_surface_batch": plan.surface_batch} for device in plan.devices}
|
|
2291
2386
|
with _pipeline._TransferPipeline(plan.devices, self._pool_elements(plan, geometry_spec), stats,
|
|
2292
2387
|
slots=plan.slots) as transfer:
|
|
2293
2388
|
for spatial_slice, shape, centre in self._volume_tiles(plan.chunk_shape):
|
|
@@ -2514,22 +2609,28 @@ class Projector:
|
|
|
2514
2609
|
staged = tuple(value[owned].to(device=device, dtype=owner.dtype) for value in geometry)
|
|
2515
2610
|
admission_owner = copy(owner)
|
|
2516
2611
|
admission_owner.view_slice = slice(0, shard.stop - shard.start)
|
|
2517
|
-
|
|
2612
|
+
nonfinite = torch.zeros((), device=device, dtype=torch.bool)
|
|
2613
|
+
coincident = torch.zeros_like(nonfinite)
|
|
2614
|
+
for views, selected in _flat_admission_batches(
|
|
2615
|
+
admission_owner, staged, pixels, batched_corners=True,
|
|
2616
|
+
):
|
|
2518
2617
|
source, points = _sample_geometry(
|
|
2519
2618
|
owner.beam, owner.detector_shape, owner.detector_spacing, staged,
|
|
2520
2619
|
None, views, selected, owner.voxel_spacing, device=device,
|
|
2521
2620
|
dtype=owner.dtype, validate=False,
|
|
2522
2621
|
)
|
|
2523
|
-
|
|
2524
|
-
raise ValueError("detector surface world coordinates must be finite")
|
|
2622
|
+
nonfinite = nonfinite | ~torch.isfinite(source).all() | ~torch.isfinite(points).all()
|
|
2525
2623
|
sources = source.reshape(source.shape[0], *([1] * len(selected)), -1)
|
|
2526
|
-
if owner.beam != "parallel"
|
|
2527
|
-
|
|
2624
|
+
if owner.beam != "parallel":
|
|
2625
|
+
coincident = coincident | torch.any(torch.all(points == sources, dim=-1))
|
|
2528
2626
|
directions = (source if owner.beam == "parallel" else
|
|
2529
2627
|
points - sources)
|
|
2530
|
-
|
|
2531
|
-
raise ValueError("detector surface world coordinates must be finite")
|
|
2628
|
+
nonfinite = nonfinite | ~torch.isfinite(directions).all()
|
|
2532
2629
|
del source, points, sources, directions
|
|
2630
|
+
_raise_geometry_errors([
|
|
2631
|
+
(nonfinite, "detector surface world coordinates must be finite"),
|
|
2632
|
+
(coincident, "source and detector surface pixels must differ"),
|
|
2633
|
+
])
|
|
2533
2634
|
del staged
|
|
2534
2635
|
except (TypeError, ValueError, RuntimeError) as failure:
|
|
2535
2636
|
error = failure
|
|
@@ -2591,7 +2692,25 @@ class Projector:
|
|
|
2591
2692
|
def _prepare_execution(self, tensor, is_project, output_store=None):
|
|
2592
2693
|
device = torch.device(getattr(tensor, "device", "cpu"))
|
|
2593
2694
|
if self.detector_surface is not None:
|
|
2594
|
-
|
|
2695
|
+
# Only eligible full calls need a preliminary snapshot reservation.
|
|
2696
|
+
# Explicit streamed jobs first obtain exact shared-offset metadata.
|
|
2697
|
+
devices = self._devices or (device,)
|
|
2698
|
+
active_devices = devices[:min(len(devices), self.projection_shape[0])]
|
|
2699
|
+
full_candidate = (not isinstance(self.detector_surface, ParameterizedSurface)
|
|
2700
|
+
and isinstance(tensor, torch.Tensor) and device.type == "cuda"
|
|
2701
|
+
and self.world_size == 1 and device in active_devices
|
|
2702
|
+
and output_store is None and self.volume_chunk_shape is None
|
|
2703
|
+
and self.view_chunk_size is None and self.detector_chunk_shape is None
|
|
2704
|
+
and self.schedule == "auto" and self.partition == "views")
|
|
2705
|
+
if full_candidate:
|
|
2706
|
+
plan = self._execution_plan(tensor, is_project, output_store)
|
|
2707
|
+
spec, geometry = self._effective_geometry(plan, device)
|
|
2708
|
+
if plan is None:
|
|
2709
|
+
return plan, spec, geometry
|
|
2710
|
+
else:
|
|
2711
|
+
spec, geometry = self._effective_geometry(None, torch.device("cpu"))
|
|
2712
|
+
# Exact dtype/shared metadata can admit a borderline full call;
|
|
2713
|
+
# reuse the one host snapshot if the preliminary plan streamed.
|
|
2595
2714
|
plan = self._execution_plan(tensor, is_project, output_store, spec)
|
|
2596
2715
|
else:
|
|
2597
2716
|
plan = self._execution_plan(tensor, is_project, output_store)
|
|
@@ -2606,11 +2725,28 @@ class Projector:
|
|
|
2606
2725
|
return None, tuple(component.clone() if plan is None or component.device.type == "cpu"
|
|
2607
2726
|
else component.to(device="cpu", copy=True)
|
|
2608
2727
|
for component in self._learnable)
|
|
2609
|
-
#
|
|
2610
|
-
#
|
|
2611
|
-
|
|
2728
|
+
# Full CUDA execution has admitted its snapshots before the callback.
|
|
2729
|
+
# Streamed execution and explicit CPU samplers retain host snapshots.
|
|
2730
|
+
local = plan is None and input_device.type == "cuda"
|
|
2731
|
+
capacity = sum(self._surface_snapshot_buffers()) if local else 0
|
|
2732
|
+
arena, cursor = None, 0
|
|
2733
|
+
|
|
2612
2734
|
def pack(tensor):
|
|
2613
|
-
|
|
2735
|
+
nonlocal arena, cursor
|
|
2736
|
+
if (local and tensor.device == input_device and tensor.layout == torch.strided
|
|
2737
|
+
and not tensor.is_nested and not tensor.is_quantized):
|
|
2738
|
+
itemsize = tensor.element_size()
|
|
2739
|
+
start = (cursor + itemsize - 1) // itemsize * itemsize
|
|
2740
|
+
end = start + tensor.numel() * itemsize
|
|
2741
|
+
if end <= capacity:
|
|
2742
|
+
with torch.no_grad():
|
|
2743
|
+
if arena is None:
|
|
2744
|
+
arena = torch.empty(capacity, dtype=torch.uint8, device=input_device)
|
|
2745
|
+
saved = arena[start:end].view(tensor.dtype).reshape(tensor.shape)
|
|
2746
|
+
saved.copy_(tensor.detach())
|
|
2747
|
+
cursor = end
|
|
2748
|
+
return saved, tensor.device
|
|
2749
|
+
return tensor.detach().to(device="cpu", copy=True), tensor.device
|
|
2614
2750
|
|
|
2615
2751
|
def unpack(saved):
|
|
2616
2752
|
tensor, device = saved
|
|
@@ -2630,12 +2766,14 @@ class Projector:
|
|
|
2630
2766
|
with torch.autograd.graph.saved_tensors_hooks(pack, unpack):
|
|
2631
2767
|
offsets = self._surface_offsets()
|
|
2632
2768
|
values = (*frames, offsets)
|
|
2633
|
-
snapshots = tuple(value.clone() if value.device
|
|
2769
|
+
snapshots = tuple(value.clone() if (local and value.device == input_device)
|
|
2770
|
+
or value.device.type == "cpu" else value.to(device="cpu", copy=True)
|
|
2634
2771
|
for value in values)
|
|
2635
2772
|
spec = self._surface_spec(frames, offsets)
|
|
2636
2773
|
metadata = (tuple(offsets.shape), str(offsets.dtype))
|
|
2637
2774
|
self._snapshot_d2h_bytes += sum(value.numel() * value.element_size()
|
|
2638
|
-
for value in values
|
|
2775
|
+
for value, snapshot in zip(values, snapshots)
|
|
2776
|
+
if value.is_cuda and not snapshot.is_cuda)
|
|
2639
2777
|
except (TypeError, ValueError, RuntimeError) as failure:
|
|
2640
2778
|
error = failure
|
|
2641
2779
|
self._partition.preflight(error, metadata, cache_key="surface snapshot",
|
diffct/solvers.py
CHANGED
|
@@ -96,7 +96,7 @@ def cgls(operator, measurements, iterations=30, *, x0=None, rtol=1e-6, atol=0.,
|
|
|
96
96
|
x = _initial(operator, data, x0)
|
|
97
97
|
residual = data.clone() if x0 is None else data - operator.project(x)
|
|
98
98
|
norm = math.sqrt(squared_norm(operator, data, "rays"))
|
|
99
|
-
history = [math.sqrt(squared_norm(operator, residual, "rays"))]
|
|
99
|
+
history = [norm if x0 is None else math.sqrt(squared_norm(operator, residual, "rays"))]
|
|
100
100
|
completed = 0
|
|
101
101
|
adjoint = (operator.backproject(residual) if iterations and history[0] > atol + rtol * norm
|
|
102
102
|
else torch.zeros_like(x))
|
|
@@ -117,11 +117,15 @@ def cgls(operator, measurements, iterations=30, *, x0=None, rtol=1e-6, atol=0.,
|
|
|
117
117
|
states["x"].add_(states["p"], alpha=alpha)
|
|
118
118
|
states["r"].sub_(states["q"], alpha=alpha)
|
|
119
119
|
states["s"] = operator.backproject(states["r"])
|
|
120
|
-
next_gamma = squared_norm(operator, states["s"], "volume")
|
|
120
|
+
next_gamma = squared_norm(operator, states["s"], "volume", return_tensor=True)
|
|
121
|
+
residual_norm = squared_norm(operator, states["r"], "rays", return_tensor=True)
|
|
122
|
+
# Both reductions retain the same FP64 block/collective order. Read
|
|
123
|
+
# their results together; denominator breakdown is still checked first.
|
|
124
|
+
next_gamma, residual_norm = torch.stack((next_gamma, residual_norm.to(next_gamma.device))).cpu().tolist()
|
|
121
125
|
states["p"].mul_(next_gamma / gamma).add_(states["s"])
|
|
122
126
|
gamma = next_gamma
|
|
123
127
|
completed += 1
|
|
124
|
-
history.append(math.sqrt(
|
|
128
|
+
history.append(math.sqrt(residual_norm))
|
|
125
129
|
if checkpoint_directory is not None:
|
|
126
130
|
save_checkpoint(operator, checkpoint_directory, states, metadata, completed, history,
|
|
127
131
|
norm, 1048576, gamma=gamma)
|
|
@@ -148,11 +152,12 @@ def sirt(operator, measurements, iterations=100, *, x0=None, rtol=1e-6, atol=0.,
|
|
|
148
152
|
saved, stores = load_checkpoint(operator, resume, metadata, iterations, 1048576, ("x", "row", "column"))
|
|
149
153
|
states = {name: _read_tensor(store, data.device) for name, store in stores.items()}
|
|
150
154
|
completed, history, norm = saved["iteration"], list(saved["residual_history"]), saved["measurement_norm"]
|
|
155
|
+
residual = None
|
|
151
156
|
else:
|
|
152
157
|
states = dict(x=_initial(operator, data, x0))
|
|
153
158
|
completed, norm = 0, math.sqrt(squared_norm(operator, data, "rays"))
|
|
154
|
-
|
|
155
|
-
history = [math.sqrt(squared_norm(operator,
|
|
159
|
+
residual = data if x0 is None else data - operator.project(states["x"])
|
|
160
|
+
history = [norm if x0 is None else math.sqrt(squared_norm(operator, residual, "rays"))]
|
|
156
161
|
if iterations and history[0] > atol + rtol * norm:
|
|
157
162
|
row = operator.project(torch.ones_like(states["x"]))
|
|
158
163
|
column = operator.backproject(torch.ones_like(data))
|
|
@@ -160,12 +165,15 @@ def sirt(operator, measurements, iterations=100, *, x0=None, rtol=1e-6, atol=0.,
|
|
|
160
165
|
states["column"] = torch.where(column > 0, column.reciprocal(), 0.)
|
|
161
166
|
threshold, stopped = atol + rtol * norm, False
|
|
162
167
|
while completed < iterations and history[-1] > threshold:
|
|
163
|
-
|
|
168
|
+
# A callback may change either the iterate or the operator after history.
|
|
169
|
+
if callbacks or residual is None:
|
|
170
|
+
residual = data - operator.project(states["x"])
|
|
164
171
|
states["x"].add_(states["column"] * operator.backproject(states["row"] * residual), alpha=relaxation)
|
|
165
172
|
if nonnegative:
|
|
166
173
|
states["x"].clamp_(min=0)
|
|
167
174
|
completed += 1
|
|
168
|
-
|
|
175
|
+
residual = data - operator.project(states["x"])
|
|
176
|
+
history.append(math.sqrt(squared_norm(operator, residual, "rays")))
|
|
169
177
|
if checkpoint_directory is not None:
|
|
170
178
|
save_checkpoint(operator, checkpoint_directory, states, metadata, completed, history, norm, 1048576)
|
|
171
179
|
if callbacks:
|
|
@@ -195,14 +203,16 @@ def tv_reconstruction(operator, measurements, iterations=100, weight=1., lr=.1,
|
|
|
195
203
|
for name in ("exp_avg", "exp_avg_sq", "step")}
|
|
196
204
|
optimizer.state[x]["step"] = optimizer.state[x]["step"].reshape(())
|
|
197
205
|
completed, history, norm = saved["iteration"], list(saved["residual_history"]), saved["measurement_norm"]
|
|
206
|
+
residual = None
|
|
198
207
|
else:
|
|
199
208
|
completed, norm = 0, math.sqrt(squared_norm(operator, data, "rays"))
|
|
200
|
-
|
|
201
|
-
history = [math.sqrt(squared_norm(operator,
|
|
209
|
+
residual = -data if x0 is None else operator.project(x) - data
|
|
210
|
+
history = [norm if x0 is None else math.sqrt(squared_norm(operator, residual, "rays"))]
|
|
202
211
|
threshold, stopped = atol + rtol * norm, False
|
|
203
212
|
ray_count = scalar_sum(operator, data.numel(), "rays")
|
|
204
213
|
while completed < iterations and history[-1] > threshold:
|
|
205
|
-
|
|
214
|
+
if callbacks or residual is None:
|
|
215
|
+
residual = operator.project(x) - data
|
|
206
216
|
gradient = operator.backproject(residual) / max(ray_count, 1)
|
|
207
217
|
if weight:
|
|
208
218
|
with torch.enable_grad():
|
|
@@ -214,7 +224,8 @@ def tv_reconstruction(operator, measurements, iterations=100, weight=1., lr=.1,
|
|
|
214
224
|
optimizer.step()
|
|
215
225
|
x.clamp_(min=0)
|
|
216
226
|
completed += 1
|
|
217
|
-
|
|
227
|
+
residual = operator.project(x) - data
|
|
228
|
+
history.append(math.sqrt(squared_norm(operator, residual, "rays")))
|
|
218
229
|
if checkpoint_directory is not None:
|
|
219
230
|
states = dict(x=x, **optimizer.state[x])
|
|
220
231
|
states["step"] = states["step"].reshape(1)
|
diffct/surfaces.py
CHANGED
|
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
|
|
5
5
|
import torch
|
|
6
6
|
|
|
7
|
+
from .utils import _raise_geometry_errors
|
|
7
8
|
|
|
8
9
|
@dataclass(frozen=True)
|
|
9
10
|
class _GeometrySpec:
|
|
@@ -41,7 +42,8 @@ class ParameterizedSurface:
|
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def _detector_grids(detector_shape, spacing, pixels):
|
|
44
|
-
axes = [(torch.arange(selected.start, selected.stop,
|
|
45
|
+
axes = [(torch.arange(selected.start, selected.stop, selected.step or 1,
|
|
46
|
+
dtype=torch.float64, device="cpu") + .5 - size / 2) * pitch
|
|
45
47
|
for selected, size, pitch in zip(pixels, detector_shape, spacing)]
|
|
46
48
|
if len(axes) == 2:
|
|
47
49
|
return torch.meshgrid(*axes, indexing="ij")
|
|
@@ -106,9 +108,7 @@ def _sample_geometry(beam, detector_shape, spacing, geometry, spec, views, pixel
|
|
|
106
108
|
depth = offsets[..., 2].to(device=device, dtype=offset_dtype)
|
|
107
109
|
if offsets.ndim == dimensions + 1:
|
|
108
110
|
tangent, depth = tangent.unsqueeze(0), depth.unsqueeze(0)
|
|
109
|
-
if validate
|
|
110
|
-
if not torch.isfinite(offsets).all().item():
|
|
111
|
-
raise ValueError("detector surface coordinates must be finite")
|
|
111
|
+
checks = [(~torch.isfinite(offsets).all(), "detector surface coordinates must be finite")] if validate else []
|
|
112
112
|
|
|
113
113
|
def frame(component):
|
|
114
114
|
selected = component if local_geometry else component[views]
|
|
@@ -132,17 +132,18 @@ def _sample_geometry(beam, detector_shape, spacing, geometry, spec, views, pixel
|
|
|
132
132
|
source = (frames[0] if local_geometry else frames[0][views]).to(device=device, dtype=source_dtype)
|
|
133
133
|
if validate:
|
|
134
134
|
for component in (source, points):
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
checks.append((~torch.isfinite(component).all() | ~torch.isfinite(component.float()).all(),
|
|
136
|
+
"surface world coordinates must be finite in float32"))
|
|
137
137
|
if beam not in ("parallel", "parallel3d") and view_count:
|
|
138
138
|
sources = source.unsqueeze(1)
|
|
139
139
|
endpoints = points.reshape(view_count, -1, source.shape[-1])
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
checks.append((torch.any(torch.all(sources.float() == endpoints.float(), dim=-1)),
|
|
141
|
+
"source and detector surface pixels must differ"))
|
|
142
142
|
source_distance = torch.linalg.vector_norm(sources.double(), dim=-1) / voxel_spacing
|
|
143
143
|
pixel_distance = torch.linalg.vector_norm(endpoints.double(), dim=-1) / voxel_spacing
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
checks.append((torch.any(torch.minimum(source_distance, pixel_distance) > 1e6),
|
|
145
|
+
"surface source or pixel must lie within 1e6 voxels"))
|
|
146
|
+
checks.append((torch.any(source_distance > 1e15) | torch.any(pixel_distance > 1e15),
|
|
147
|
+
"surface coordinates must lie within 1e15 voxels"))
|
|
148
|
+
_raise_geometry_errors(checks)
|
|
148
149
|
return source, points
|
diffct/utils.py
CHANGED
|
@@ -18,6 +18,16 @@ import torch
|
|
|
18
18
|
from .constants import _DTYPE, _TPB_2D, _TPB_3D, _INF, _EPSILON
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _raise_geometry_errors(checks):
|
|
22
|
+
"""Read one validity scalar; fetch individual error flags only on failure."""
|
|
23
|
+
device = next((flag.device for flag, _ in checks if flag.is_cuda), checks[0][0].device)
|
|
24
|
+
failed = torch.stack([flag.to(device=device) for flag, _ in checks])
|
|
25
|
+
if failed.any().item():
|
|
26
|
+
for invalid, (_, message) in zip(failed.tolist(), checks):
|
|
27
|
+
if invalid:
|
|
28
|
+
raise ValueError(message)
|
|
29
|
+
|
|
30
|
+
|
|
21
31
|
# ============================================================================
|
|
22
32
|
# Device Management Utilities
|
|
23
33
|
# ============================================================================
|
|
@@ -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
|
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
diffct/__init__.py,sha256=
|
|
2
|
-
diffct/_solver_state.py,sha256=
|
|
3
|
-
diffct/_solver_tv.py,sha256=
|
|
4
|
-
diffct/analytical.py,sha256=
|
|
5
|
-
diffct/analytical_execution.py,sha256=
|
|
6
|
-
diffct/backends.py,sha256=
|
|
7
|
-
diffct/chunking.py,sha256=
|
|
1
|
+
diffct/__init__.py,sha256=m5Oy9KZOD9hqS_vMAk-FTd2BoMUqACyyitYrDAEVhFA,2965
|
|
2
|
+
diffct/_solver_state.py,sha256=ExWTTGftGOsPo9MQwxv-ylmYvhiPYXUo3Tmhi6VEvg0,16606
|
|
3
|
+
diffct/_solver_tv.py,sha256=2x16Bq5Bx9WCBszgPy3K-wicsH9AeuzzJS82uyiHBkE,5847
|
|
4
|
+
diffct/analytical.py,sha256=YTyI_lwgnhHdoglO9fboshfNQtrZsacK-gra63ja-xE,33130
|
|
5
|
+
diffct/analytical_execution.py,sha256=Al452qBRevGcEpwcdf7LpY8XbgZzgJ4YoupyRD_1LWA,26788
|
|
6
|
+
diffct/backends.py,sha256=zNNjO_sDJ8KzstTmP0WqPeUYIJDBeYg7m5KD5zQ8ZFw,48278
|
|
7
|
+
diffct/chunking.py,sha256=jJo7JlR8GrQz-_Rj44_HQLsNJeNgL1SpleJsL4QKJvE,11373
|
|
8
8
|
diffct/constants.py,sha256=oy3QNC6an43Ogdn1m81znz1-EMp9GeinVf2wkGYiXfE,3160
|
|
9
9
|
diffct/differentiable.py,sha256=cFirhqXRpUemL7y7Sl_0uqZKGJNj__ptQEcTwYNKzys,1879
|
|
10
10
|
diffct/distribution.py,sha256=iLC3BgAQ9lOkndbEOvNB_j_UaA2Ug9w1KiTUHkOzG8o,9982
|
|
11
11
|
diffct/geometry.py,sha256=iwWXQksIJGOPSuNWJ8tImWj6yPZCC71G3RBh2VDVylY,42581
|
|
12
|
-
diffct/operators.py,sha256=
|
|
12
|
+
diffct/operators.py,sha256=yGDie8Kvjq8wkBU3hnIKUuKWR9VxgkYFFMINE03a0LY,167009
|
|
13
13
|
diffct/pipeline.py,sha256=jp7AvDeQQOME3OPkO9XTcjWFLvOLrv8wczhAf1jcklM,10891
|
|
14
14
|
diffct/projectors.py,sha256=0oAFSA5mycN59dayTAWK2W7o96MP8oLuVp2V9X-1388,45334
|
|
15
15
|
diffct/reconstruction.py,sha256=KdDrpxQ31ufyUlL_bkzvDzaTcrK4BB97tnJ04ZfVfos,29368
|
|
16
|
-
diffct/solvers.py,sha256=
|
|
16
|
+
diffct/solvers.py,sha256=B7M4akFUTuwb_MBpQbpZLdlMv5Eyxy3WczExDAs613s,18000
|
|
17
17
|
diffct/storage.py,sha256=EdcFTzRtEAPRvAZhRPrSf8gRNF5o_JVbPLE8eKPhNdY,4153
|
|
18
18
|
diffct/streaming.py,sha256=2LB5xpl6vIJE1kXMBtQbG0OVxlutEEqXLBrcvlzBHl8,6086
|
|
19
19
|
diffct/surface_gradients.py,sha256=LeM_eDY1Alv604SdiPUx2CWFzr-J9NqCEcJ_ovVFGa8,6290
|
|
20
|
-
diffct/surfaces.py,sha256=
|
|
21
|
-
diffct/utils.py,sha256=
|
|
20
|
+
diffct/surfaces.py,sha256=JQ-PyMnKTTtK8VUykJ3kvIcquQHeDThVHQ18umlOYQg,8009
|
|
21
|
+
diffct/utils.py,sha256=ZOZpR_qpVSxv_uSQJ6--x-RXAqUrZJjWXYJoPKhu5nY,15240
|
|
22
22
|
diffct/kernels/__init__.py,sha256=YNdsqAkexN25pI7QaHy4phAnQNICQ7tO8K8S1zt7TnI,1899
|
|
23
23
|
diffct/kernels/analytical_transpose.py,sha256=D4vr_ZFPrjFeQyhoDqTyijbkTY-g-sHJet52ho-cbkQ,9187
|
|
24
24
|
diffct/kernels/cone_beam.py,sha256=jKlbqQPHhrng-pTbyMFx2b2l0pdQIMiaaB5hj6ayPQI,26753
|
|
25
25
|
diffct/kernels/fan_beam.py,sha256=NPha0Wl-_ayOdS6kAoXJngQffsUczp3rRnojXZ-F1y8,17556
|
|
26
26
|
diffct/kernels/geometry_vjp.py,sha256=YiPMdsuXINLu1oFhPcSbG1LW2P3WSfeJeEvHNLrmQzU,22008
|
|
27
27
|
diffct/kernels/parallel_beam.py,sha256=tCiSx9W8nO4mn8DVyRuK4E0Ry8q38g3pAUACab0UYVU,17143
|
|
28
|
-
diffct/kernels/surface_geometry.py,sha256=
|
|
29
|
-
diffct-2.4.
|
|
30
|
-
diffct-2.4.
|
|
31
|
-
diffct-2.4.
|
|
32
|
-
diffct-2.4.
|
|
33
|
-
diffct-2.4.
|
|
28
|
+
diffct/kernels/surface_geometry.py,sha256=W0G0ilIJJ_EotU4SELeppeWrzokzFWJqpm6qqKaWN7Y,5842
|
|
29
|
+
diffct-2.4.3.dist-info/METADATA,sha256=PeKQAUhUPwodcv-NYQahBD1mceTa5w7WPQDvBDXmolE,17612
|
|
30
|
+
diffct-2.4.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
31
|
+
diffct-2.4.3.dist-info/licenses/LICENSE,sha256=a1fjzGs44LpDnV_LmA2Tzp8z-eJKoNVVdLOnh-F6J6E,11344
|
|
32
|
+
diffct-2.4.3.dist-info/licenses/NOTICE,sha256=du9I2sDF5IDfiY9vF64WWzMJh-MarpVXyLfWQmhbvmI,714
|
|
33
|
+
diffct-2.4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|