flexray-mlx 0.2.1__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.
Potentially problematic release.
This version of flexray-mlx might be problematic. Click here for more details.
- flexray_mlx/__init__.py +21 -0
- flexray_mlx/_kernels.py +98 -0
- flexray_mlx/_random.py +91 -0
- flexray_mlx/_types.py +126 -0
- flexray_mlx/medmlx.py +1208 -0
- flexray_mlx/mlx_model.py +168 -0
- flexray_mlx/py.typed +0 -0
- flexray_mlx/tta.py +248 -0
- flexray_mlx-0.2.1.dist-info/METADATA +69 -0
- flexray_mlx-0.2.1.dist-info/RECORD +14 -0
- flexray_mlx-0.2.1.dist-info/WHEEL +4 -0
- flexray_mlx-0.2.1.dist-info/entry_points.txt +2 -0
- flexray_mlx-0.2.1.dist-info/licenses/LICENSE +202 -0
- flexray_mlx-0.2.1.dist-info/licenses/THIRD_PARTY_NOTICES.md +173 -0
flexray_mlx/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Native MLX implementation of FleXray."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from flexray_mlx.mlx_model import FleXrayMlxModel
|
|
9
|
+
|
|
10
|
+
__all__ = ["FleXrayMlxModel"]
|
|
11
|
+
|
|
12
|
+
__version__ = "0.2.1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name: str) -> object:
|
|
16
|
+
# MLX loads with the network, so importing the medmlx declaration stays light.
|
|
17
|
+
if name == "FleXrayMlxModel":
|
|
18
|
+
from flexray_mlx.mlx_model import FleXrayMlxModel
|
|
19
|
+
|
|
20
|
+
return FleXrayMlxModel
|
|
21
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
flexray_mlx/_kernels.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""FP32 Metal fusions for the released FleXray decoder's NHWC tensors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
|
+
|
|
8
|
+
from medmlx_core.typing import MetalKernel
|
|
9
|
+
|
|
10
|
+
from flexray_mlx._types import NativeRuntime
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from mlx.core import array
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_RESIZE_CONCAT = """
|
|
17
|
+
// Preserve the original graph's separate multiply/add rounding.
|
|
18
|
+
#pragma clang fp contract(off)
|
|
19
|
+
uint i = thread_position_in_grid.x;
|
|
20
|
+
constexpr int C = CI + CS;
|
|
21
|
+
if (i >= N * OH * OW * C) return;
|
|
22
|
+
uint c = i % C;
|
|
23
|
+
uint ox = (i / C) % OW;
|
|
24
|
+
uint oy = (i / (C * OW)) % OH;
|
|
25
|
+
uint n = i / (C * OW * OH);
|
|
26
|
+
if (c >= CI) {
|
|
27
|
+
out[i] = skip[((n * OH + oy) * OW + ox) * CS + c - CI];
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// align_corners=True: match the FP32 source-to-destination endpoint ratio.
|
|
31
|
+
float iy = float(oy) * (float(H - 1) / float(OH - 1));
|
|
32
|
+
float ix = float(ox) * (float(W - 1) / float(OW - 1));
|
|
33
|
+
iy = metal::clamp(iy, 0.0f, float(H - 1));
|
|
34
|
+
ix = metal::clamp(ix, 0.0f, float(W - 1));
|
|
35
|
+
uint y0 = uint(metal::floor(iy)), y1 = uint(metal::ceil(iy));
|
|
36
|
+
uint x0 = uint(metal::floor(ix)), x1 = uint(metal::ceil(ix));
|
|
37
|
+
float fy = iy - float(y0), fx = ix - float(x0);
|
|
38
|
+
float a = inp[((n * H + y0) * W + x0) * CI + c];
|
|
39
|
+
float b = inp[((n * H + y0) * W + x1) * CI + c];
|
|
40
|
+
float d = inp[((n * H + y1) * W + x0) * CI + c];
|
|
41
|
+
float e = inp[((n * H + y1) * W + x1) * CI + c];
|
|
42
|
+
out[i] = ((1.0f - fy) * (1.0f - fx)) * a
|
|
43
|
+
+ ((1.0f - fy) * fx) * b
|
|
44
|
+
+ (fy * (1.0f - fx)) * d
|
|
45
|
+
+ (fy * fx) * e;
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@lru_cache(maxsize=1)
|
|
50
|
+
def _resize_concat_kernel(mx: NativeRuntime) -> MetalKernel:
|
|
51
|
+
return cast(
|
|
52
|
+
MetalKernel,
|
|
53
|
+
mx.fast.metal_kernel(
|
|
54
|
+
name="flexray_bilinear_concat",
|
|
55
|
+
input_names=["inp", "skip"],
|
|
56
|
+
output_names=["out"],
|
|
57
|
+
source=_RESIZE_CONCAT,
|
|
58
|
+
compile_options={"math_mode": "safe"},
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def upsample_concat(x: array, skip: array, *, mx: NativeRuntime) -> array:
|
|
64
|
+
"""Join a 2x bilinear NHWC resize with ``skip``, keeping resized channels first.
|
|
65
|
+
|
|
66
|
+
Inputs must be FP32, with the same positive batch size and a skip grid
|
|
67
|
+
exactly twice ``x`` in both spatial dimensions. Corner-aligned sampling
|
|
68
|
+
matches the released decoder, including singleton source dimensions.
|
|
69
|
+
One kernel writes the joined tensor without materializing the four bilinear
|
|
70
|
+
sample tensors or the separate resized image. Noncontiguous inputs are
|
|
71
|
+
made contiguous by MLX's custom-kernel boundary.
|
|
72
|
+
|
|
73
|
+
Raises ``ValueError`` when the output exceeds signed 32-bit indexing, before
|
|
74
|
+
dispatch. The output is larger than either input, so this also bounds their
|
|
75
|
+
index products.
|
|
76
|
+
"""
|
|
77
|
+
if x.ndim != 4 or skip.ndim != 4:
|
|
78
|
+
raise ValueError("FleXray decoder fusion requires NHWC tensors")
|
|
79
|
+
n, h, w, ci = x.shape
|
|
80
|
+
if min(n, h, w, ci, skip.shape[-1]) < 1 or skip.shape[:3] != (n, 2 * h, 2 * w):
|
|
81
|
+
raise ValueError("FleXray MLX decoder grid mismatch")
|
|
82
|
+
if x.dtype != mx.float32 or skip.dtype != mx.float32:
|
|
83
|
+
raise TypeError("FleXray decoder fusion requires float32 inputs")
|
|
84
|
+
oh, ow, cs = skip.shape[1:]
|
|
85
|
+
shape = (n, oh, ow, ci + cs)
|
|
86
|
+
count = n * oh * ow * (ci + cs)
|
|
87
|
+
if count > 2**31 - 1:
|
|
88
|
+
raise ValueError("FleXray decoder fusion exceeds signed 32-bit indexing")
|
|
89
|
+
return _resize_concat_kernel(mx)(
|
|
90
|
+
inputs=[x, skip],
|
|
91
|
+
template=[
|
|
92
|
+
("N", n), ("H", h), ("W", w), ("CI", ci), ("CS", cs), ("OH", oh), ("OW", ow),
|
|
93
|
+
],
|
|
94
|
+
grid=(count, 1, 1),
|
|
95
|
+
threadgroup=(256, 1, 1),
|
|
96
|
+
output_shapes=[shape],
|
|
97
|
+
output_dtypes=[mx.float32],
|
|
98
|
+
)[0]
|
flexray_mlx/_random.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Torch CPU draw order for the pinned FleXray float32 augmentation chain.
|
|
2
|
+
|
|
3
|
+
MT19937 uses its low 24 bits for float uniforms. Contiguous normal tensors use
|
|
4
|
+
16-value Box-Muller blocks; short tensors use cached double-precision pairs.
|
|
5
|
+
Transcendental rounding can differ between CPU libraries, as it does in Torch.
|
|
6
|
+
Adapted from PyTorch's ATen/core and native/cpu distribution helpers (BSD;
|
|
7
|
+
see root THIRD_PARTY_NOTICES.md).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from flexray_mlx._types import FloatArray, UIntArray
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TorchRandom:
|
|
20
|
+
"""An isolated stream that reproduces Torch CPU draws, without importing Torch.
|
|
21
|
+
|
|
22
|
+
Draws follow Torch's order of consumption for a given seed and call sequence;
|
|
23
|
+
the module docstring notes where float rounding can still differ. Values are
|
|
24
|
+
float32.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, seed: int | None) -> None:
|
|
28
|
+
"""Seed from the low 32 bits of ``seed`` (negative values wrap); None is unseeded."""
|
|
29
|
+
|
|
30
|
+
self._engine = np.random.RandomState(None if seed is None else seed & 0xFFFFFFFF)
|
|
31
|
+
self._next_normal: float | None = None
|
|
32
|
+
|
|
33
|
+
def _words(self, count: int) -> UIntArray:
|
|
34
|
+
return self._engine.randint(0, 2**32, size=count, dtype=np.uint32)
|
|
35
|
+
|
|
36
|
+
def _units(self, count: int) -> FloatArray:
|
|
37
|
+
return (self._words(count) & np.uint32(0xFFFFFF)).astype(np.float32) * np.float32(2**-24)
|
|
38
|
+
|
|
39
|
+
def uniform(self, low: float = 0.0, high: float = 1.0) -> float:
|
|
40
|
+
"""Draw one float32 uniform between ``low`` and ``high``, like Torch's ``uniform_``."""
|
|
41
|
+
# Deferred: flexray_mlx.medmlx imports this module (via tta) at load time.
|
|
42
|
+
from flexray_mlx.medmlx import fma32
|
|
43
|
+
|
|
44
|
+
lower, upper = np.float32(low), np.float32(high)
|
|
45
|
+
return float(fma32(self._units(1)[0], upper - lower, lower))
|
|
46
|
+
|
|
47
|
+
def chance(self, probability: float) -> np.bool_:
|
|
48
|
+
"""Consume one uniform draw and return whether it is below ``probability``."""
|
|
49
|
+
return self.uniform() < np.float32(probability)
|
|
50
|
+
|
|
51
|
+
def _double_unit(self) -> float:
|
|
52
|
+
words = self._words(2).astype(np.uint64)
|
|
53
|
+
value = (int(words[0]) << 32) | int(words[1])
|
|
54
|
+
return (value & (2**53 - 1)) * 2**-53
|
|
55
|
+
|
|
56
|
+
def _short_normal(self) -> float:
|
|
57
|
+
if self._next_normal is not None:
|
|
58
|
+
value, self._next_normal = self._next_normal, None
|
|
59
|
+
return value
|
|
60
|
+
theta = 2.0 * math.pi * self._double_unit()
|
|
61
|
+
radius = math.sqrt(-2.0 * math.log1p(-self._double_unit()))
|
|
62
|
+
self._next_normal = radius * math.sin(theta)
|
|
63
|
+
return radius * math.cos(theta)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _normal_blocks(uniforms: FloatArray) -> FloatArray:
|
|
67
|
+
blocks = uniforms.reshape(-1, 16)
|
|
68
|
+
radius = np.sqrt(np.float32(-2.0) * np.log(np.float32(1.0) - blocks[:, :8]))
|
|
69
|
+
# The scalar CPU kernel multiplies by double pi before rounding theta.
|
|
70
|
+
theta = (2.0 * math.pi * blocks[:, 8:].astype(np.float64)).astype(np.float32)
|
|
71
|
+
return np.concatenate((radius * np.cos(theta), radius * np.sin(theta)), axis=1).ravel()
|
|
72
|
+
|
|
73
|
+
def normal(self, shape: tuple[int, ...]) -> FloatArray:
|
|
74
|
+
"""Draw a float32 standard-normal array of ``shape``.
|
|
75
|
+
|
|
76
|
+
Fewer than 16 values use cached double-precision Box-Muller pairs; 16 or
|
|
77
|
+
more use float32 blocks of 16, as Torch's CPU kernel does.
|
|
78
|
+
"""
|
|
79
|
+
count = math.prod(shape)
|
|
80
|
+
if count < 16:
|
|
81
|
+
return np.asarray([self._short_normal() for _ in range(count)], np.float32).reshape(
|
|
82
|
+
shape
|
|
83
|
+
)
|
|
84
|
+
uniforms = self._units(count)
|
|
85
|
+
complete = count // 16 * 16
|
|
86
|
+
values = np.empty(count, dtype=np.float32)
|
|
87
|
+
values[:complete] = self._normal_blocks(uniforms[:complete])
|
|
88
|
+
if count % 16:
|
|
89
|
+
# Torch redraws the final 16, including the overlapping completed block.
|
|
90
|
+
values[-16:] = self._normal_blocks(self._units(16))
|
|
91
|
+
return values.reshape(shape)
|
flexray_mlx/_types.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Array and namespace contracts for the pinned native and file boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from os import PathLike
|
|
7
|
+
from typing import TYPE_CHECKING, Literal, Protocol, TypedDict
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from medmlx_core.typing import FastRuntime
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from mlx.core import Dtype, array
|
|
15
|
+
|
|
16
|
+
type HostArray = NDArray[np.generic]
|
|
17
|
+
type RealArray = NDArray[np.bool_ | np.integer | np.floating]
|
|
18
|
+
type FloatArray = NDArray[np.float32]
|
|
19
|
+
type DoubleArray = NDArray[np.float64]
|
|
20
|
+
type BoolArray = NDArray[np.bool_]
|
|
21
|
+
type IntArray = NDArray[np.int64]
|
|
22
|
+
type UIntArray = NDArray[np.uint32]
|
|
23
|
+
type JsonValue = bool | int | float | str | list[JsonValue] | dict[str, JsonValue] | None
|
|
24
|
+
type JsonObject = dict[str, JsonValue]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NativeRuntime(Protocol):
|
|
28
|
+
fast: FastRuntime
|
|
29
|
+
float32: Dtype
|
|
30
|
+
|
|
31
|
+
# Constructor inputs here are the NumPy arrays prepared by this network.
|
|
32
|
+
def array(self, value: FloatArray) -> array: ...
|
|
33
|
+
def eval(self, *args: array) -> None: ...
|
|
34
|
+
def conv2d(self, input: array, weight: array, /, *, padding: int) -> array: ...
|
|
35
|
+
def mean(self, a: array, /, *, axis: Sequence[int], keepdims: bool) -> array: ...
|
|
36
|
+
def square(self, a: array, /) -> array: ...
|
|
37
|
+
def rsqrt(self, a: array, /) -> array: ...
|
|
38
|
+
def maximum(self, a: array, b: array, /) -> array: ...
|
|
39
|
+
def concatenate(self, arrays: list[array], *, axis: int) -> array: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NativeOperation(Protocol):
|
|
43
|
+
def __call__(self, x: array) -> array: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NeuralRuntime(Protocol):
|
|
47
|
+
def MaxPool2d(self, *, kernel_size: int, stride: int) -> NativeOperation: ...
|
|
48
|
+
def Upsample(self, *, scale_factor: int, mode: str, align_corners: bool) -> NativeOperation: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TensorReader(Protocol):
|
|
52
|
+
def __enter__(self) -> TensorReader: ...
|
|
53
|
+
def __exit__(self, *args: object) -> None: ...
|
|
54
|
+
def keys(self) -> list[str]: ...
|
|
55
|
+
def get_tensor(self, name: str) -> HostArray: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SafetensorsRuntime(Protocol):
|
|
59
|
+
def safe_open(self, filename: PathLike[str], *, framework: Literal["np"]) -> TensorReader: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DicomDataset(Protocol):
|
|
63
|
+
pixel_array: RealArray
|
|
64
|
+
|
|
65
|
+
def __contains__(self, name: str) -> bool: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DicomRuntime(Protocol):
|
|
69
|
+
def dcmread(self, fp: str, *, force: bool) -> DicomDataset: ...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Normalization(TypedDict):
|
|
73
|
+
scheme: str
|
|
74
|
+
percentiles: tuple[float, float] | None
|
|
75
|
+
eps: float
|
|
76
|
+
mean: float | None
|
|
77
|
+
std: float | None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class EnsembleMember(TypedDict):
|
|
81
|
+
subfolder: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class EnsembleManifest(TypedDict):
|
|
85
|
+
members: list[EnsembleMember]
|
|
86
|
+
flagship: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class RunOutputs(TypedDict):
|
|
90
|
+
"""POSIX paths of the files ``run`` writes to its output directory."""
|
|
91
|
+
|
|
92
|
+
mask: str
|
|
93
|
+
display_source: str
|
|
94
|
+
probabilities: str
|
|
95
|
+
prediction: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RunDetails(TypedDict):
|
|
99
|
+
"""Settings and geometry of one ``run``.
|
|
100
|
+
|
|
101
|
+
``source_size`` is ``[width, height]`` of the input; ``model_grid`` is
|
|
102
|
+
``[height, width]``. ``content_box_tlbr`` is ``[top, left, bottom, right]``
|
|
103
|
+
in model-grid pixels with exclusive bottom/right.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
selected_label: str
|
|
107
|
+
selected_channel: int
|
|
108
|
+
threshold: float
|
|
109
|
+
ensemble: bool
|
|
110
|
+
members: list[str]
|
|
111
|
+
tta_samples: int
|
|
112
|
+
tta_seed: int | None
|
|
113
|
+
normalization_scheme: str
|
|
114
|
+
source_size: list[int]
|
|
115
|
+
model_grid: list[int]
|
|
116
|
+
content_box_tlbr: list[int]
|
|
117
|
+
mask_pixels: int
|
|
118
|
+
probability_mode: str
|
|
119
|
+
backend: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class RunResult(TypedDict):
|
|
123
|
+
"""Return value of ``run``: output file paths and run details."""
|
|
124
|
+
|
|
125
|
+
outputs: RunOutputs
|
|
126
|
+
details: RunDetails
|