segresnet-mlx 0.2.1__tar.gz → 0.2.2__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.
Potentially problematic release.
This version of segresnet-mlx might be problematic. Click here for more details.
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/PKG-INFO +8 -18
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/README.md +6 -16
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/pyproject.toml +3 -3
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/__init__.py +1 -1
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/_types.py +2 -1
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/predictor.py +21 -54
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/transforms.py +19 -9
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/.gitignore +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/LICENSE +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/THIRD_PARTY_NOTICES.md +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/_arrays.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/_nifti.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/_records.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/_runtime.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/inference.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/medmlx.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/network.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/py.typed +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/telemetry.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/topology.py +0 -0
- {segresnet_mlx-0.2.1 → segresnet_mlx-0.2.2}/src/segresnet_mlx/weights.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: segresnet-mlx
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Native MLX SegResNet with NumPy sliding-window logit inference
|
|
5
5
|
Project-URL: Homepage, https://github.com/MedMLX/segresnet-mlx
|
|
6
6
|
Project-URL: Documentation, https://github.com/MedMLX/segresnet-mlx/blob/main/docs/technical.md
|
|
@@ -12,7 +12,7 @@ License-File: LICENSE
|
|
|
12
12
|
License-File: THIRD_PARTY_NOTICES.md
|
|
13
13
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
14
14
|
Requires-Python: <3.15,>=3.13
|
|
15
|
-
Requires-Dist: medmlx-core==0.2.
|
|
15
|
+
Requires-Dist: medmlx-core==0.2.4
|
|
16
16
|
Requires-Dist: nibabel<6,>=5.3
|
|
17
17
|
Requires-Dist: numpy<3,>=2.0
|
|
18
18
|
Provides-Extra: conversion
|
|
@@ -47,19 +47,12 @@ Segment brain tumors, renal structures and whole-body CT anatomy with MONAI SegR
|
|
|
47
47
|
## Quick start
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
uv venv --python 3.13
|
|
50
|
+
uv venv --python 3.13
|
|
51
51
|
source .venv/bin/activate
|
|
52
|
-
|
|
52
|
+
uv pip install segresnet-mlx huggingface-hub
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
repo="https://github.com/MedMLX/segresnet-mlx.git@v0.2.1"
|
|
59
|
-
python -m pip install "segresnet-mlx @ git+$repo" huggingface-hub
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Download the public converted BraTS bundle separately, then run:
|
|
55
|
+
Download the converted [BraTS](https://huggingface.co/MedMLX/segresnet-brats-mlx) weights, then run:
|
|
63
56
|
|
|
64
57
|
```python
|
|
65
58
|
from huggingface_hub import hf_hub_download
|
|
@@ -80,12 +73,9 @@ result = run(
|
|
|
80
73
|
print(result["outputs"]["mask"])
|
|
81
74
|
```
|
|
82
75
|
|
|
83
|
-
Keep input volumes on the same voxel grid.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
[renal CECT](https://huggingface.co/MedMLX/segresnet-renal-cect-mlx); follow each
|
|
87
|
-
weight card and its terms. Docs cover local upstream checkpoint conversion and
|
|
88
|
-
whole-body CT bundles. Qualification differs by bundle; see the technical notes.
|
|
76
|
+
Keep input volumes on the same voxel grid. Converted weights are also available for
|
|
77
|
+
[renal CECT](https://huggingface.co/MedMLX/segresnet-renal-cect-mlx). Docs cover
|
|
78
|
+
checkpoint conversion and whole-body CT.
|
|
89
79
|
|
|
90
80
|
## License
|
|
91
81
|
|
|
@@ -18,19 +18,12 @@ Segment brain tumors, renal structures and whole-body CT anatomy with MONAI SegR
|
|
|
18
18
|
## Quick start
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
uv venv --python 3.13
|
|
21
|
+
uv venv --python 3.13
|
|
22
22
|
source .venv/bin/activate
|
|
23
|
-
|
|
23
|
+
uv pip install segresnet-mlx huggingface-hub
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
repo="https://github.com/MedMLX/segresnet-mlx.git@v0.2.1"
|
|
30
|
-
python -m pip install "segresnet-mlx @ git+$repo" huggingface-hub
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Download the public converted BraTS bundle separately, then run:
|
|
26
|
+
Download the converted [BraTS](https://huggingface.co/MedMLX/segresnet-brats-mlx) weights, then run:
|
|
34
27
|
|
|
35
28
|
```python
|
|
36
29
|
from huggingface_hub import hf_hub_download
|
|
@@ -51,12 +44,9 @@ result = run(
|
|
|
51
44
|
print(result["outputs"]["mask"])
|
|
52
45
|
```
|
|
53
46
|
|
|
54
|
-
Keep input volumes on the same voxel grid.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
[renal CECT](https://huggingface.co/MedMLX/segresnet-renal-cect-mlx); follow each
|
|
58
|
-
weight card and its terms. Docs cover local upstream checkpoint conversion and
|
|
59
|
-
whole-body CT bundles. Qualification differs by bundle; see the technical notes.
|
|
47
|
+
Keep input volumes on the same voxel grid. Converted weights are also available for
|
|
48
|
+
[renal CECT](https://huggingface.co/MedMLX/segresnet-renal-cect-mlx). Docs cover
|
|
49
|
+
checkpoint conversion and whole-body CT.
|
|
60
50
|
|
|
61
51
|
## License
|
|
62
52
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "segresnet-mlx"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Native MLX SegResNet with NumPy sliding-window logit inference"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.15"
|
|
@@ -13,7 +13,7 @@ license = "Apache-2.0"
|
|
|
13
13
|
license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]
|
|
14
14
|
authors = [{ name = "Joseph Sandoval" }, { name = "Alfredo Sandoval Jr." }]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"medmlx-core==0.2.
|
|
16
|
+
"medmlx-core==0.2.4",
|
|
17
17
|
"nibabel>=5.3,<6",
|
|
18
18
|
"numpy>=2.0,<3",
|
|
19
19
|
]
|
|
@@ -78,7 +78,7 @@ ban-relative-imports = "all"
|
|
|
78
78
|
"typing_extensions.Any" = {msg = "Use a precise type and parse untyped values at the boundary."}
|
|
79
79
|
|
|
80
80
|
[tool.uv.sources]
|
|
81
|
-
medmlx-core = { git = "https://github.com/MedMLX/medmlx.git", rev = "
|
|
81
|
+
medmlx-core = { git = "https://github.com/MedMLX/medmlx.git", rev = "f98e0924f50228e2b9509474947b9672054b3217", subdirectory = "packages/core" }
|
|
82
82
|
|
|
83
83
|
[tool.hatch.build.targets.sdist]
|
|
84
84
|
only-include = [
|
|
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
|
|
21
21
|
from segresnet_mlx.weights import load_model as load_model
|
|
22
22
|
from segresnet_mlx.weights import required_shapes as required_shapes
|
|
23
23
|
|
|
24
|
-
__version__ = "0.2.
|
|
24
|
+
__version__ = "0.2.2"
|
|
25
25
|
|
|
26
26
|
_EXPORTS = {
|
|
27
27
|
"BUNDLE_CONFIGS": "segresnet_mlx.predictor",
|
|
@@ -47,8 +47,9 @@ if TYPE_CHECKING:
|
|
|
47
47
|
overlap: float,
|
|
48
48
|
mode: str,
|
|
49
49
|
padding_mode: str,
|
|
50
|
-
with_coord: bool,
|
|
50
|
+
with_coord: bool = False,
|
|
51
51
|
predictor_input: Literal["numpy"],
|
|
52
|
+
preserve_window_strides: bool = False,
|
|
52
53
|
) -> Float32Array: ...
|
|
53
54
|
|
|
54
55
|
class CoreSlidingWindow(Protocol):
|
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
from contextlib import nullcontext
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
-
from typing import TYPE_CHECKING,
|
|
9
|
+
from typing import TYPE_CHECKING, cast
|
|
10
10
|
|
|
11
11
|
from segresnet_mlx._records import BundleConfig, InfererConfig
|
|
12
12
|
from segresnet_mlx._runtime import import_mlx
|
|
@@ -71,13 +71,6 @@ BUNDLE_CONFIGS = {
|
|
|
71
71
|
for name, bundle in BUNDLES.items()
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
_PAD_MODES: dict[str, Literal["constant", "reflect", "edge", "wrap"]] = {
|
|
75
|
-
"constant": "constant",
|
|
76
|
-
"reflect": "reflect",
|
|
77
|
-
"replicate": "edge",
|
|
78
|
-
"circular": "wrap",
|
|
79
|
-
}
|
|
80
|
-
|
|
81
74
|
|
|
82
75
|
class BundlePredictor:
|
|
83
76
|
"""Synchronized MLX window callback with an injected network.
|
|
@@ -195,48 +188,18 @@ class BundlePredictor:
|
|
|
195
188
|
require_ncdhw(host, name="SegResNet input")
|
|
196
189
|
if self.telemetry is not None:
|
|
197
190
|
self.telemetry.set_metadata({"sw_batch_size": configuration.sw_batch_size})
|
|
198
|
-
callback: (
|
|
199
|
-
Callable[[HostArray | Array], Float32Array]
|
|
200
|
-
| Callable[[HostArray | Array, list[list[slice]]], Float32Array]
|
|
201
|
-
) = self._forward_window
|
|
202
|
-
use_coordinates = configuration.sw_batch_size == 1
|
|
203
|
-
if use_coordinates:
|
|
204
|
-
# MONAI preserves the sliced input's strides for batch size 1;
|
|
205
|
-
# the shared core concatenates even a single patch. MLX selects
|
|
206
|
-
# different reduction kernels for strided/contiguous inputs.
|
|
207
|
-
# Open gap: a core change that skips the single-patch
|
|
208
|
-
# concatenation would make this workaround unnecessary.
|
|
209
|
-
padding = [(0, 0), (0, 0)]
|
|
210
|
-
for size, roi in zip(host.shape[2:], configuration.roi_size, strict=True):
|
|
211
|
-
extra = max(roi - size, 0)
|
|
212
|
-
padding.append((extra // 2, extra - extra // 2))
|
|
213
|
-
# Torch padding materializes a contiguous array; unpadded data
|
|
214
|
-
# retains its original strides. Match the bundle's padding_mode
|
|
215
|
-
# so this fast path agrees with the core's own padding.
|
|
216
|
-
pad_kind = _PAD_MODES[configuration.padding_mode]
|
|
217
|
-
padded = (
|
|
218
|
-
np.ascontiguousarray(np.pad(host, padding, mode=pad_kind))
|
|
219
|
-
if any(before or after for before, after in padding)
|
|
220
|
-
else host
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
def coordinate_callback(
|
|
224
|
-
values: HostArray | Array, coordinates: list[list[slice]]
|
|
225
|
-
) -> Float32Array:
|
|
226
|
-
return self._forward_window(padded[tuple(coordinates[0])])
|
|
227
|
-
|
|
228
|
-
callback = coordinate_callback
|
|
229
|
-
|
|
230
191
|
return sliding_window_inference(
|
|
231
192
|
host,
|
|
232
193
|
configuration.roi_size,
|
|
233
194
|
configuration.sw_batch_size,
|
|
234
|
-
|
|
195
|
+
self._forward_window,
|
|
235
196
|
overlap=configuration.overlap,
|
|
236
197
|
mode=configuration.mode,
|
|
237
198
|
padding_mode=configuration.padding_mode,
|
|
238
|
-
with_coord=use_coordinates,
|
|
239
199
|
predictor_input="numpy",
|
|
200
|
+
# MLX reduction kernels depend on layout. Match MONAI's sliced
|
|
201
|
+
# batch-one input without a second padded volume or patch copy.
|
|
202
|
+
preserve_window_strides=True,
|
|
240
203
|
)
|
|
241
204
|
except BaseException as error:
|
|
242
205
|
if self.telemetry is not None:
|
|
@@ -287,17 +250,21 @@ def predict_wide_logits(
|
|
|
287
250
|
if not np.isfinite(output).all():
|
|
288
251
|
raise RuntimeError("SegResNet produced non-finite paired logits")
|
|
289
252
|
del result
|
|
290
|
-
mx.clear_cache()
|
|
291
253
|
return output
|
|
292
254
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
255
|
+
try:
|
|
256
|
+
logits = sliding_window_inference(
|
|
257
|
+
values.astype(np.float64),
|
|
258
|
+
configuration.roi_size,
|
|
259
|
+
configuration.sw_batch_size,
|
|
260
|
+
window,
|
|
261
|
+
overlap=configuration.overlap,
|
|
262
|
+
mode=configuration.mode,
|
|
263
|
+
padding_mode=configuration.padding_mode,
|
|
264
|
+
predictor_input="numpy",
|
|
265
|
+
)
|
|
266
|
+
return logits.astype(np.float32)
|
|
267
|
+
finally:
|
|
268
|
+
# Reuse same-shaped Metal allocations across windows, including when
|
|
269
|
+
# paired arithmetic requires a wider host accumulator.
|
|
270
|
+
mx.clear_cache()
|
|
@@ -517,12 +517,15 @@ def _grid_sample(
|
|
|
517
517
|
return result
|
|
518
518
|
|
|
519
519
|
|
|
520
|
-
def _fma_array(
|
|
520
|
+
def _fma_array(
|
|
521
|
+
left: Float64Array, right: float | Float64Array, addend: Float64Array
|
|
522
|
+
) -> Float64Array:
|
|
521
523
|
"""Elementwise ``left * right + addend`` with one rounding (error-free transforms)."""
|
|
522
|
-
left, addend = np.broadcast_arrays(
|
|
523
|
-
|
|
524
|
+
left, right_value, addend = np.broadcast_arrays(
|
|
525
|
+
np.asarray(left, np.float64), np.asarray(right, np.float64), np.asarray(addend, np.float64)
|
|
526
|
+
)
|
|
524
527
|
product = left * right_value
|
|
525
|
-
if right_value
|
|
528
|
+
if not np.any(right_value) or not np.any(left):
|
|
526
529
|
return product + addend
|
|
527
530
|
error = _product_error(left, right_value, product)
|
|
528
531
|
total = product + addend
|
|
@@ -531,7 +534,9 @@ def _fma_array(left: Float64Array, right: float, addend: Float64Array) -> Float6
|
|
|
531
534
|
return total + (sum_error + error)
|
|
532
535
|
|
|
533
536
|
|
|
534
|
-
def _product_error(
|
|
537
|
+
def _product_error(
|
|
538
|
+
left: Float64Array, right: Float64Array, product: Float64Array
|
|
539
|
+
) -> Float64Array:
|
|
535
540
|
"""Dekker's exact remainder ``left * right - product``."""
|
|
536
541
|
|
|
537
542
|
def split[T: (Float64Array, np.float64)](value: T) -> tuple[T, T]:
|
|
@@ -580,7 +585,12 @@ def _sample_separable(
|
|
|
580
585
|
gy: Float64Array,
|
|
581
586
|
gz: Float64Array,
|
|
582
587
|
) -> Float64Array:
|
|
583
|
-
"""Trilinear corners
|
|
588
|
+
"""Trilinear corners in ATen's order, with its fused multiply-add accumulation.
|
|
589
|
+
|
|
590
|
+
Separate product/sum rounding can move a float64 interpolation across a
|
|
591
|
+
float32 midpoint. Preserve the source CPU kernel's fusion before the final
|
|
592
|
+
float32 cast; the network still receives ordinary float32 values.
|
|
593
|
+
"""
|
|
584
594
|
depth, height, width = data.shape[1:]
|
|
585
595
|
x_index, x_weight = _corners(_source_index(gx, width))
|
|
586
596
|
y_index, y_weight = _corners(_source_index(gy, height))
|
|
@@ -602,7 +612,7 @@ def _sample_separable(
|
|
|
602
612
|
.astype(np.float64)
|
|
603
613
|
)
|
|
604
614
|
inside = ((zi < depth)[along_d] & (yi < height)[along_h]) & (xi < width)[along_w]
|
|
605
|
-
result
|
|
615
|
+
result = np.where(inside, _fma_array(values, weight, result), result)
|
|
606
616
|
return result
|
|
607
617
|
|
|
608
618
|
|
|
@@ -612,7 +622,7 @@ def _sample(
|
|
|
612
622
|
gy: Float64Array,
|
|
613
623
|
gz: Float64Array,
|
|
614
624
|
) -> Float64Array:
|
|
615
|
-
"""
|
|
625
|
+
"""General-grid corners with the same fused accumulation as the separable path."""
|
|
616
626
|
depth, height, width = data.shape[1:]
|
|
617
627
|
x_index, x_weight = _corners(_source_index(gx, width))
|
|
618
628
|
y_index, y_weight = _corners(_source_index(gy, height))
|
|
@@ -631,7 +641,7 @@ def _sample(
|
|
|
631
641
|
np.minimum(yi, height - 1),
|
|
632
642
|
np.minimum(xi, width - 1),
|
|
633
643
|
].astype(np.float64)
|
|
634
|
-
result
|
|
644
|
+
result = np.where(inside, _fma_array(values, weight, result), result)
|
|
635
645
|
return result
|
|
636
646
|
|
|
637
647
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|